新增自动触发中文公式编辑/测试、查价运输方式 Tab 分组选择,并完善销售产品字段预览与菜单展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
1253 lines
36 KiB
Vue
1253 lines
36 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { $baseMessage } from '@/utils'
|
||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||
import ModalPro from '@/components/ModalPro/index.vue'
|
||
import useModal from '@/components/ModalPro/useModal'
|
||
import dayjs from 'dayjs'
|
||
import { Modal } from '@arco-design/web-vue'
|
||
import { getDictListAll } from '@/api/system/dict'
|
||
import {
|
||
createAutoTrigger,
|
||
deleteAutoTrigger,
|
||
getAutoTriggerList,
|
||
testAutoTriggerFormula,
|
||
toggleAutoTriggerStatus,
|
||
updateAutoTrigger,
|
||
} from '@/api/channel/autoTrigger'
|
||
import FormulaConditionEditor from './components/FormulaConditionEditor.vue'
|
||
import {
|
||
formatConditionSummary,
|
||
getOversizeExampleFormula,
|
||
parseStoredCondition,
|
||
serializeStoredCondition,
|
||
validateFormulaExpression,
|
||
} from './conditionUtils'
|
||
import { OPERATION_SAVE_LABEL, TIMING_SAVE_LABEL } from './formulaMeta'
|
||
|
||
defineOptions({
|
||
name: 'AutoTrigger'
|
||
})
|
||
|
||
const { t } = useI18n()
|
||
|
||
type AutoTriggerRow = {
|
||
id: number
|
||
status: boolean
|
||
ruleName: string
|
||
priority: number
|
||
operation: string
|
||
triggerTiming: string
|
||
triggerCondition: string
|
||
conditionSummary: string
|
||
promptMessage: string
|
||
notifier: string
|
||
createTime: string
|
||
creator: string
|
||
}
|
||
|
||
const tableData = ref<AutoTriggerRow[]>([])
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(50)
|
||
const totalCount = ref(0)
|
||
const listLoading = ref(false)
|
||
|
||
const processedTableData = computed(() => tableData.value)
|
||
|
||
function mapRow(r: any): AutoTriggerRow {
|
||
const triggerCondition = r.triggerCondition ?? r.TriggerCondition ?? ''
|
||
const parsed = parseStoredCondition(triggerCondition)
|
||
return {
|
||
id: r.id ?? r.Id,
|
||
status: (r.status ?? r.Status) === 1 || (r.status ?? r.Status) === true,
|
||
ruleName: (r.ruleName ?? r.RuleName ?? parsed.ruleName) || '',
|
||
priority: Number(r.priority ?? r.Priority ?? parsed.priority ?? 100) || 100,
|
||
operation: r.operation ?? r.Operation ?? '',
|
||
triggerTiming: r.triggerTiming ?? r.TriggerTiming ?? '',
|
||
triggerCondition,
|
||
conditionSummary: formatConditionSummary(triggerCondition),
|
||
promptMessage: r.promptMessage ?? r.PromptMessage ?? '',
|
||
notifier: r.notifier ?? r.Notifier ?? '',
|
||
createTime: r.createTime ?? r.CreateTime ?? '',
|
||
creator: r.createBy ?? r.CreateBy ?? r.creator ?? r.Creator ?? '',
|
||
}
|
||
}
|
||
|
||
async function loadList() {
|
||
listLoading.value = true
|
||
try {
|
||
const { items, total } = await getAutoTriggerList({
|
||
page: currentPage.value,
|
||
pageSize: pageSize.value,
|
||
})
|
||
const mapped = (items || []).map(mapRow)
|
||
// 防止删除后停留在超出范围的页码,导致“列表不见了”
|
||
if (!mapped.length && Number(total || 0) > 0 && currentPage.value > 1) {
|
||
currentPage.value = 1
|
||
await loadList()
|
||
return
|
||
}
|
||
tableData.value = mapped
|
||
totalCount.value = Number(total || 0)
|
||
} catch (e: any) {
|
||
$baseMessage(e?.message || '加载失败', 'error')
|
||
tableData.value = []
|
||
totalCount.value = 0
|
||
} finally {
|
||
listLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleToggleStatus(row: AutoTriggerRow) {
|
||
const next = !row.status
|
||
try {
|
||
await toggleAutoTriggerStatus(row.id, next ? 1 : 0)
|
||
row.status = next
|
||
$baseMessage(
|
||
next ? t('channel.autoTrigger.message.statusEnabled') : t('channel.autoTrigger.message.statusDisabled'),
|
||
'success'
|
||
)
|
||
await loadList()
|
||
} catch (e: any) {
|
||
$baseMessage(e?.message || '更新失败', 'error')
|
||
}
|
||
}
|
||
|
||
function handleDelete(row: AutoTriggerRow) {
|
||
Modal.confirm({
|
||
title: t('channel.autoTrigger.actions.delete'),
|
||
content: '确定删除该规则吗?',
|
||
onOk: async () => {
|
||
await deleteAutoTrigger(row.id)
|
||
$baseMessage(t('common.deleteSuccess'), 'success')
|
||
await loadList()
|
||
},
|
||
})
|
||
}
|
||
|
||
type ModalMode = 'add' | 'edit'
|
||
const modalMode = ref<ModalMode>('add')
|
||
const modalTitle = computed(() =>
|
||
modalMode.value === 'add' ? t('channel.autoTrigger.actions.add') : t('channel.autoTrigger.actions.edit')
|
||
)
|
||
|
||
const timingOptions = ref<{ label: string; value: string }[]>([])
|
||
const operationOptions = ref<{ label: string; value: string }[]>([])
|
||
|
||
async function loadDictOptions() {
|
||
try {
|
||
const [timingItems, operationItems] = await Promise.all([
|
||
getDictListAll({ dictType: 'auto_trigger_timing' }),
|
||
getDictListAll({ dictType: 'auto_trigger_operation' }),
|
||
])
|
||
const mapDict = (items: any[]) =>
|
||
(items || [])
|
||
.map((raw) => ({
|
||
label: String(raw.dictName ?? raw.DictName ?? raw.dictCode ?? raw.DictCode ?? ''),
|
||
value: String(raw.dictCode ?? raw.DictCode ?? ''),
|
||
}))
|
||
.filter((o) => o.label && o.value)
|
||
|
||
timingOptions.value = mapDict(timingItems)
|
||
if (!timingOptions.value.some((o) => o.value === 'forecast')) {
|
||
timingOptions.value.unshift({ label: '预报', value: 'forecast' })
|
||
}
|
||
|
||
operationOptions.value = mapDict(operationItems)
|
||
} catch (e: any) {
|
||
$baseMessage(e?.message || '加载字典失败', 'error')
|
||
timingOptions.value = [
|
||
{ label: '预报', value: 'forecast' },
|
||
{ label: '下单', value: 'order_create' },
|
||
{ label: '入库', value: 'inbound' },
|
||
{ label: '出库', value: 'outbound' },
|
||
]
|
||
operationOptions.value = [
|
||
{ label: '拦截', value: 'intercept' },
|
||
{ label: '警告', value: 'warn' },
|
||
{ label: '问题件', value: 'problem' },
|
||
]
|
||
}
|
||
}
|
||
|
||
function resolveTimingCode(saved: string): string {
|
||
const s = String(saved ?? '').trim()
|
||
if (!s) return timingOptions.value[0]?.value ?? 'forecast'
|
||
const byValue = timingOptions.value.find((o) => o.value === s)
|
||
if (byValue) return byValue.value
|
||
const byLabel = timingOptions.value.find((o) => o.label === s)
|
||
if (byLabel) return byLabel.value
|
||
const hit = Object.entries(TIMING_SAVE_LABEL).find(([, label]) => label === s)
|
||
return hit?.[0] ?? s
|
||
}
|
||
|
||
function resolveOperationCode(saved: string): string {
|
||
const s = String(saved ?? '').trim()
|
||
if (!s) return 'intercept'
|
||
const byValue = operationOptions.value.find((o) => o.value === s)
|
||
if (byValue) return byValue.value
|
||
const byLabel = operationOptions.value.find((o) => o.label === s)
|
||
if (byLabel) return byLabel.value
|
||
const hit = Object.entries(OPERATION_SAVE_LABEL).find(([, label]) => label === s)
|
||
return hit?.[0] ?? s
|
||
}
|
||
|
||
function timingLabel(code: string): string {
|
||
return timingOptions.value.find((o) => o.value === code)?.label
|
||
|| TIMING_SAVE_LABEL[code]
|
||
|| code
|
||
}
|
||
|
||
function operationLabel(code: string): string {
|
||
return operationOptions.value.find((o) => o.value === code)?.label
|
||
|| OPERATION_SAVE_LABEL[code]
|
||
|| code
|
||
}
|
||
|
||
function timingSaveValue(code: string): string {
|
||
return TIMING_SAVE_LABEL[code] || timingLabel(code)
|
||
}
|
||
|
||
function operationSaveValue(code: string): string {
|
||
return OPERATION_SAVE_LABEL[code] || operationLabel(code)
|
||
}
|
||
|
||
const [registerAutoTriggerModal, modalMethods] = useModal() as any
|
||
|
||
const editId = ref<number | null>(null)
|
||
const legacyConditionText = ref('')
|
||
const formulaText = ref('')
|
||
const formulaEditorRef = ref<InstanceType<typeof FormulaConditionEditor> | null>(null)
|
||
const testLoading = ref(false)
|
||
const testResult = ref<{ matched: boolean; error?: string } | null>(null)
|
||
const testSample = reactive({
|
||
consigneeCountry: 'GB',
|
||
customerName: '阿里国际站/深圳一达通',
|
||
productCode: 'EX_ASP_HKDHL_YD',
|
||
length: 65,
|
||
width: 40,
|
||
height: 30,
|
||
})
|
||
|
||
const formModel = reactive({
|
||
status: true,
|
||
ruleName: '',
|
||
priority: 100,
|
||
operationCode: 'intercept',
|
||
timingCode: 'forecast',
|
||
promptMessage: '',
|
||
notifier: '',
|
||
createTime: '',
|
||
creator: '',
|
||
})
|
||
|
||
function resetFormModel() {
|
||
formulaText.value = ''
|
||
legacyConditionText.value = ''
|
||
Object.assign(formModel, {
|
||
status: true,
|
||
ruleName: '',
|
||
priority: 100,
|
||
operationCode: 'intercept',
|
||
timingCode: 'forecast',
|
||
promptMessage: '',
|
||
notifier: '',
|
||
createTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||
creator: '当前用户',
|
||
})
|
||
}
|
||
|
||
function applyExampleRule() {
|
||
formulaText.value = getOversizeExampleFormula()
|
||
formModel.ruleName = '英国阿里超尺寸拦截'
|
||
formModel.priority = 100
|
||
formModel.timingCode = 'forecast'
|
||
formModel.operationCode = 'intercept'
|
||
formModel.promptMessage = '超尺寸,禁止下单'
|
||
Object.assign(testSample, {
|
||
consigneeCountry: 'GB',
|
||
customerName: '阿里国际站/深圳一达通',
|
||
productCode: 'EX_ASP_HKDHL_YD',
|
||
length: 65,
|
||
width: 40,
|
||
height: 30,
|
||
})
|
||
}
|
||
|
||
async function handleTestFormula() {
|
||
const validateErr = validateFormulaExpression(formulaText.value)
|
||
if (validateErr) {
|
||
$baseMessage(validateErr, 'warning')
|
||
return
|
||
}
|
||
testLoading.value = true
|
||
testResult.value = null
|
||
try {
|
||
const result = await testAutoTriggerFormula({
|
||
expression: formulaText.value.trim(),
|
||
sample: {
|
||
consigneeCountry: testSample.consigneeCountry?.trim() || undefined,
|
||
customerName: testSample.customerName?.trim() || undefined,
|
||
productCode: testSample.productCode?.trim() || undefined,
|
||
length: Number(testSample.length) || undefined,
|
||
width: Number(testSample.width) || undefined,
|
||
height: Number(testSample.height) || undefined,
|
||
},
|
||
})
|
||
testResult.value = { matched: !!result?.matched, error: result?.error }
|
||
$baseMessage(
|
||
result?.error
|
||
? result.error
|
||
: result?.matched
|
||
? t('channel.autoTrigger.form.testMatched')
|
||
: t('channel.autoTrigger.form.testNotMatched'),
|
||
result?.error ? 'warning' : result?.matched ? 'success' : 'info'
|
||
)
|
||
} catch (e: any) {
|
||
testResult.value = { matched: false, error: e?.message }
|
||
$baseMessage(e?.message || t('channel.autoTrigger.form.testFailed'), 'error')
|
||
} finally {
|
||
testLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function openAddModal() {
|
||
modalMode.value = 'add'
|
||
editId.value = null
|
||
resetFormModel()
|
||
modalMethods.setModalProps({ title: modalTitle.value, width: 1000 })
|
||
modalMethods.openModal(true)
|
||
}
|
||
|
||
async function openEditModal(row: AutoTriggerRow) {
|
||
modalMode.value = 'edit'
|
||
editId.value = row.id
|
||
const parsed = parseStoredCondition(row.triggerCondition)
|
||
formulaText.value = parsed.expression
|
||
legacyConditionText.value = parsed.legacyText || ''
|
||
Object.assign(formModel, {
|
||
status: row.status,
|
||
ruleName: parsed.ruleName || row.ruleName || '',
|
||
priority: row.priority ?? parsed.priority ?? 100,
|
||
operationCode: resolveOperationCode(row.operation),
|
||
timingCode: resolveTimingCode(row.triggerTiming),
|
||
promptMessage: row.promptMessage,
|
||
notifier: row.notifier,
|
||
createTime: row.createTime,
|
||
creator: row.creator,
|
||
})
|
||
modalMethods.setModalProps({ title: modalTitle.value, width: 1000 })
|
||
modalMethods.openModal(true)
|
||
}
|
||
|
||
// 编辑入口(Handsontable 操作列)
|
||
function handleEdit(row: AutoTriggerRow) {
|
||
openEditModal(row)
|
||
}
|
||
|
||
// 新增入口(页面新增按钮)
|
||
function handleAdd() {
|
||
openAddModal()
|
||
}
|
||
|
||
async function handleAutoTriggerModalOk() {
|
||
if (!formModel.ruleName?.trim()) {
|
||
$baseMessage(t('channel.autoTrigger.form.ruleNameRequired'), 'warning')
|
||
return
|
||
}
|
||
if (!formModel.promptMessage?.trim()) {
|
||
$baseMessage(t('channel.autoTrigger.form.promptRequired'), 'warning')
|
||
return
|
||
}
|
||
const validateErr = validateFormulaExpression(formulaText.value)
|
||
if (validateErr) {
|
||
$baseMessage(validateErr, 'warning')
|
||
return
|
||
}
|
||
const payload = {
|
||
status: formModel.status ? 1 : 0,
|
||
ruleName: formModel.ruleName.trim(),
|
||
priority: Number(formModel.priority) || 100,
|
||
operation: operationSaveValue(formModel.operationCode),
|
||
triggerTiming: timingSaveValue(formModel.timingCode),
|
||
triggerCondition: serializeStoredCondition(formulaText.value, {
|
||
ruleName: formModel.ruleName.trim(),
|
||
priority: Number(formModel.priority) || 100,
|
||
}),
|
||
promptMessage: formModel.promptMessage.trim(),
|
||
notifier: formModel.notifier?.trim() || '',
|
||
}
|
||
try {
|
||
if (modalMode.value === 'add') {
|
||
await createAutoTrigger(payload)
|
||
currentPage.value = 1
|
||
$baseMessage(t('common.addSuccess'), 'success')
|
||
} else {
|
||
const id = editId.value
|
||
if (id == null) return
|
||
await updateAutoTrigger({ ...payload, id })
|
||
$baseMessage(t('common.editSuccess'), 'success')
|
||
}
|
||
modalMethods.closeModal()
|
||
await loadList()
|
||
} catch (e: any) {
|
||
$baseMessage(e?.message || '保存失败', 'error')
|
||
}
|
||
}
|
||
|
||
function handleAutoTriggerModalCancel() {
|
||
modalMethods.closeModal()
|
||
}
|
||
|
||
const handlePageChange = (page: number) => {
|
||
currentPage.value = page
|
||
loadList()
|
||
}
|
||
|
||
const handlePageSizeChange = (size: number) => {
|
||
pageSize.value = size
|
||
currentPage.value = 1
|
||
loadList()
|
||
}
|
||
|
||
const hotColumns = computed(() => [
|
||
{ key: 'status', label: t('channel.autoTrigger.table.status'), width: 80 },
|
||
{ key: 'ruleName', label: t('channel.autoTrigger.table.ruleName'), width: 160 },
|
||
{ key: 'operation', label: t('channel.autoTrigger.table.operation'), width: 90 },
|
||
{ key: 'triggerTiming', label: t('channel.autoTrigger.table.triggerTiming'), width: 90 },
|
||
{ key: 'conditionSummary', label: t('channel.autoTrigger.table.triggerCondition'), width: 360 },
|
||
{ key: 'promptMessage', label: t('channel.autoTrigger.table.promptMessage'), width: 200 },
|
||
{ key: 'notifier', label: t('channel.autoTrigger.table.notifier'), width: 100 },
|
||
{ key: 'createTime', label: t('channel.autoTrigger.table.createTime'), width: 160 },
|
||
{ key: 'creator', label: t('channel.autoTrigger.table.creator'), width: 100 },
|
||
])
|
||
|
||
// 与其他 Handsontable 页面一致:捕获阶段处理内联开关,避免 Handsontable 抢事件
|
||
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
||
const handsontableHeight = computed(() => Math.max(tableHeight.value || 0, 400))
|
||
|
||
function handleStatusSwitchMouseDown(e: MouseEvent) {
|
||
const switchEl = (e.target as HTMLElement | null)?.closest?.('.customer-status-switch') as HTMLElement | null
|
||
if (!switchEl) return
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
|
||
const rowIndex = parseInt(switchEl.getAttribute('data-row-index') || '0', 10)
|
||
const idx = (currentPage.value - 1) * pageSize.value + rowIndex
|
||
const record = tableData.value[idx]
|
||
if (record) {
|
||
handleToggleStatus(record)
|
||
}
|
||
}
|
||
|
||
function customCellRenderer(row: number, col: number, value: any, rowData: AutoTriggerRow): string | null {
|
||
// HandsontablePro 列结构(本页不显示 checkbox,action 列在末尾):
|
||
// col=0: 序号;col=1..n: 数据列
|
||
const dataColIndex = col - 1
|
||
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null
|
||
const colKey = hotColumns.value[dataColIndex].key
|
||
|
||
if (colKey === 'status') {
|
||
const enabled = !!rowData.status
|
||
return `
|
||
<label class="customer-status-switch" data-row-index="${row}" style="display:inline-flex;align-items:center;cursor:pointer;">
|
||
<input type="checkbox" ${enabled ? 'checked' : ''} style="display:none;" />
|
||
<span class="switch-track" style="position:relative;width:36px;height:20px;background:${enabled ? '#00b42a' : '#e5e6eb'};border-radius:10px;transition:background .2s;">
|
||
<span class="switch-thumb" style="position:absolute;top:2px;left:${enabled ? '18px' : '2px'};width:16px;height:16px;background:#fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.2);transition:left .2s;"></span>
|
||
</span>
|
||
</label>
|
||
`
|
||
}
|
||
|
||
if (colKey === 'operation') {
|
||
const op = operationLabel(resolveOperationCode(rowData.operation))
|
||
const cls = op === '拦截' ? 'intercept' : op === '警告' ? 'warning' : op === '问题件' ? 'problem' : ''
|
||
return `<span class="operation-tag ${cls}">${op}</span>`
|
||
}
|
||
|
||
if (colKey === 'conditionSummary') {
|
||
const text = rowData.conditionSummary || '—'
|
||
return `<span class="condition-summary-cell" title="${text.replace(/"/g, '"')}">${text}</span>`
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
function actionRenderer(row: AutoTriggerRow, rowIndex: number): string {
|
||
return `
|
||
<div class="action-btns" style="display:flex;gap:12px;align-items:center;justify-content:center;">
|
||
<span
|
||
data-action="delete"
|
||
data-row-index="${rowIndex}"
|
||
title="${t('channel.autoTrigger.actions.delete')}"
|
||
style="cursor:pointer;color:#f53f3f;display:inline-flex;align-items:center;justify-content:center;"
|
||
>
|
||
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;">
|
||
<path d="M5 11h5.5m0 0v29a1 1 0 0 0 1 1h25a1 1 0 0 0 1-1V11m-27 0H16m21.5 0H43m-5.5 0H32m-16 0V7h16v4m-16 0h16M20 18v15m8-15v15"/>
|
||
</svg>
|
||
</span>
|
||
<span
|
||
data-action="edit"
|
||
data-row-index="${rowIndex}"
|
||
title="${t('channel.autoTrigger.actions.edit')}"
|
||
style="cursor:pointer;color:var(--app-primary-color);display:inline-flex;align-items:center;justify-content:center;"
|
||
>
|
||
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;">
|
||
<path d="m30.48 19.038 5.733-5.734a1 1 0 0 0 0-1.414l-5.586-5.586a1 1 0 0 0-1.414 0l-5.734 5.734m7 7L15.763 33.754a1 1 0 0 1-.59.286l-6.048.708a1 1 0 0 1-1.113-1.069l.477-6.31a1 1 0 0 1 .29-.631l14.7-14.7m7 7-7-7M6 42h36" />
|
||
</svg>
|
||
</span>
|
||
</div>
|
||
`
|
||
}
|
||
|
||
function handleActionClick(row: AutoTriggerRow, rowIndex: number, action?: string) {
|
||
const idx = (currentPage.value - 1) * pageSize.value + rowIndex
|
||
const record = tableData.value[idx] || row
|
||
switch (action) {
|
||
case 'delete':
|
||
handleDelete(record)
|
||
break
|
||
case 'edit':
|
||
default:
|
||
handleEdit(record)
|
||
break
|
||
}
|
||
}
|
||
|
||
const handsontableRef = ref<InstanceType<typeof HandsontablePro> | null>(null)
|
||
|
||
onMounted(() => {
|
||
void loadDictOptions()
|
||
loadList()
|
||
})
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.autoTrigger-container {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 16px 16px 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #f0f3f4;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-x: hidden;
|
||
overflow-y: hidden;
|
||
}
|
||
|
||
.autoTrigger-page-card {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
padding: 0;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.table-toolbar {
|
||
padding: 16px 16px 0;
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
flex-wrap: nowrap;
|
||
gap: 12px;
|
||
|
||
.toolbar-left {
|
||
flex: 1;
|
||
|
||
.custom-buttons {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
|
||
.arco-btn {
|
||
border-radius: 20px;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
border: 2px solid;
|
||
position: relative;
|
||
overflow: hidden;
|
||
padding: 8px 16px;
|
||
height: auto;
|
||
|
||
// 新增按钮 - 灰色
|
||
&.btn-add {
|
||
background: #f2f3f5;
|
||
border-color: #e5e6eb;
|
||
color: #4e5969;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: #e5e6eb;
|
||
border-color: #d9d9d9;
|
||
color: #1d2129;
|
||
}
|
||
|
||
&:active:not(:disabled) {
|
||
background: #d9d9d9;
|
||
border-color: #bfbfbf;
|
||
}
|
||
}
|
||
|
||
// 禁用状态
|
||
&:disabled {
|
||
opacity: 1;
|
||
cursor: not-allowed;
|
||
background: rgba(169, 169, 169, 0.8) !important;
|
||
border-color: rgba(169, 169, 169, 0.8) !important;
|
||
color: #ffffff !important;
|
||
transform: none !important;
|
||
box-shadow: none !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.toolbar-right {
|
||
.pagination-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
white-space: nowrap;
|
||
flex-wrap: nowrap;
|
||
|
||
.total-records,
|
||
.go-to-text,
|
||
.page-size-text,
|
||
.page-text {
|
||
font-size: 12px;
|
||
color: #4e5969;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.current-page {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #1d2129;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.arco-btn-size-small {
|
||
padding: 0 8px;
|
||
height: 24px;
|
||
min-width: 24px;
|
||
border-radius: 4px;
|
||
color: #4e5969;
|
||
border: 1px solid #e5e6eb;
|
||
background-color: #fff;
|
||
|
||
&:hover {
|
||
background-color: #f2f3f5;
|
||
}
|
||
|
||
&:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
.arco-select-size-small {
|
||
.arco-select-view {
|
||
height: 24px;
|
||
line-height: 24px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.arco-input-number-size-small {
|
||
.arco-input-wrapper {
|
||
height: 24px;
|
||
line-height: 24px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.custom-table-container {
|
||
overflow-x: auto;
|
||
border: 1px solid #e5e6eb;
|
||
border-radius: 4px;
|
||
position: relative;
|
||
}
|
||
|
||
/* 与 system/announcement 列表区一致,保证 flex 链下 clientHeight 与 Handsontable 高度一致 */
|
||
.list-wrap {
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding: 0 16px 16px;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.custom-table-header {
|
||
background: #f7f8fa;
|
||
border-bottom: 1px solid #e5e6eb;
|
||
|
||
.header-row {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 40px;
|
||
min-width: max-content;
|
||
}
|
||
|
||
.header-cell {
|
||
padding: 8px 12px;
|
||
border-right: 1px solid #e5e6eb;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #1d2129;
|
||
|
||
&:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
&.index-cell {
|
||
flex: 0 0 60px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.status-cell {
|
||
flex: 0 0 80px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.operation-cell {
|
||
flex: 0 0 100px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.timing-cell {
|
||
flex: 0 0 100px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.condition-cell {
|
||
flex: 0 0 300px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.message-cell {
|
||
flex: 0 0 200px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.notifier-cell {
|
||
flex: 0 0 120px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.time-cell {
|
||
flex: 0 0 160px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.creator-cell {
|
||
flex: 0 0 100px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.action-cell {
|
||
flex: 0 0 120px;
|
||
justify-content: center;
|
||
position: sticky;
|
||
right: 0;
|
||
background: #f7f8fa;
|
||
z-index: 10;
|
||
border-left: 2px solid #e5e6eb;
|
||
}
|
||
}
|
||
}
|
||
|
||
.custom-table-body {
|
||
.table-row {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 40px;
|
||
border-bottom: 1px solid #e5e6eb;
|
||
min-width: max-content;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
background: #f7f8fa;
|
||
}
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.table-cell {
|
||
padding: 8px 12px;
|
||
border-right: 1px solid #e5e6eb;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
color: #1d2129;
|
||
word-break: break-all;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
|
||
&:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
&.index-cell {
|
||
flex: 0 0 60px;
|
||
justify-content: center;
|
||
color: #4e5969;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&.status-cell {
|
||
flex: 0 0 80px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.operation-cell {
|
||
flex: 0 0 100px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.timing-cell {
|
||
flex: 0 0 100px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.condition-cell {
|
||
flex: 0 0 300px;
|
||
justify-content: flex-start;
|
||
white-space: normal;
|
||
word-break: break-word;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
&.message-cell {
|
||
flex: 0 0 200px;
|
||
justify-content: flex-start;
|
||
white-space: normal;
|
||
word-break: break-word;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
&.notifier-cell {
|
||
flex: 0 0 120px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.time-cell {
|
||
flex: 0 0 160px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.creator-cell {
|
||
flex: 0 0 100px;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.action-cell {
|
||
flex: 0 0 120px;
|
||
justify-content: center;
|
||
position: sticky;
|
||
right: 0;
|
||
background: #fff;
|
||
z-index: 10;
|
||
border-left: 2px solid #e5e6eb;
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
|
||
.action-link {
|
||
color: #165dff;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
|
||
&:hover {
|
||
color: #0e42d2;
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-state {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
// 状态开关样式
|
||
.status-switch {
|
||
:deep(.arco-switch-checked) {
|
||
background-color: #00b42a;
|
||
border-color: #00b42a;
|
||
}
|
||
}
|
||
|
||
// 操作类型样式
|
||
:deep(.operation-tag) {
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
// HandsontablePro 单元格使用 innerHTML 注入,需用 :deep 让样式穿透 scoped
|
||
:deep(.operation-tag.intercept) {
|
||
background: #ffe7ba;
|
||
color: #d46b08;
|
||
}
|
||
|
||
:deep(.operation-tag.warning) {
|
||
background: #fff2e8;
|
||
color: #d4380d;
|
||
}
|
||
|
||
:deep(.operation-tag.problem) {
|
||
background: #fff1f0;
|
||
color: #cf1322;
|
||
}
|
||
|
||
:deep(.condition-summary-cell) {
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
text-align: left;
|
||
line-height: 1.4;
|
||
font-size: 12px;
|
||
color: #4e5969;
|
||
}
|
||
|
||
.auto-trigger-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
.legacy-alert {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 12px 16px;
|
||
}
|
||
|
||
.form-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
|
||
&.full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
.form-label {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #262626;
|
||
|
||
&.required::before {
|
||
content: '*';
|
||
color: #f53f3f;
|
||
margin-right: 4px;
|
||
}
|
||
}
|
||
|
||
.condition-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.section-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.section-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
}
|
||
|
||
.test-sample-panel {
|
||
margin-top: 8px;
|
||
padding: 12px;
|
||
border: 1px dashed #e5e6eb;
|
||
border-radius: 6px;
|
||
background: #fafbfc;
|
||
|
||
.test-sample-title {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #4e5969;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.test-sample-grid {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.test-result-alert {
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<template>
|
||
<div class="autoTrigger-container">
|
||
<div class="autoTrigger-page-card">
|
||
<!-- 表格操作按钮和分页控制 -->
|
||
<div class="table-toolbar">
|
||
<div class="toolbar-left">
|
||
<div class="custom-buttons">
|
||
<a-button
|
||
class="btn-add"
|
||
@click="handleAdd"
|
||
>
|
||
{{ t('channel.autoTrigger.actions.add') }}
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<div class="pagination-controls">
|
||
<span class="total-records">{{ t('channel.autoTrigger.pagination.total', { count: totalCount }) }}</span>
|
||
<a-button
|
||
type="text"
|
||
size="small"
|
||
:disabled="currentPage === 1"
|
||
@click="handlePageChange(currentPage - 1)"
|
||
>
|
||
{{ t('channel.autoTrigger.pagination.prev') }}
|
||
</a-button>
|
||
<span class="current-page">{{ currentPage }}</span>
|
||
<a-button
|
||
type="text"
|
||
size="small"
|
||
:disabled="currentPage * pageSize >= totalCount"
|
||
@click="handlePageChange(currentPage + 1)"
|
||
>
|
||
{{ t('channel.autoTrigger.pagination.next') }}
|
||
</a-button>
|
||
<a-select
|
||
v-model="pageSize"
|
||
size="small"
|
||
:style="{ width: '110px' }"
|
||
@change="handlePageSizeChange"
|
||
>
|
||
<a-option :value="10">10</a-option>
|
||
<a-option :value="20">20</a-option>
|
||
<a-option :value="50">50</a-option>
|
||
<a-option :value="100">100</a-option>
|
||
</a-select>
|
||
<span class="page-size-text">{{ t('channel.autoTrigger.pagination.perPage') }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HandsontablePro 列表区(空数据时也保留表头结构) -->
|
||
<div
|
||
ref="tableContainerRef"
|
||
class="list-wrap"
|
||
@mousedown.capture="handleStatusSwitchMouseDown"
|
||
>
|
||
<HandsontablePro
|
||
ref="handsontableRef"
|
||
:columns="hotColumns"
|
||
:data="processedTableData"
|
||
:total="totalCount"
|
||
:current-page="currentPage"
|
||
:page-size="pageSize"
|
||
:show-action-column="true"
|
||
:action-column-position="'end'"
|
||
:action-column-width="100"
|
||
:serial-no-header="t('channel.autoTrigger.table.serialNo')"
|
||
:action-column-header="t('channel.autoTrigger.table.actions')"
|
||
:action-renderer="actionRenderer"
|
||
:custom-cell-renderer="customCellRenderer"
|
||
:stretch-columns="true"
|
||
:height="handsontableHeight"
|
||
@action-click="handleActionClick"
|
||
/>
|
||
<div v-if="!tableData.length" class="empty-state">
|
||
<a-empty :description="t('channel.autoTrigger.empty')" />
|
||
</div>
|
||
</div>
|
||
|
||
<ModalPro
|
||
:title="modalTitle"
|
||
:width="1000"
|
||
@register="registerAutoTriggerModal"
|
||
@ok="handleAutoTriggerModalOk"
|
||
@cancel="handleAutoTriggerModalCancel"
|
||
>
|
||
<div class="auto-trigger-form">
|
||
<a-alert
|
||
v-if="legacyConditionText"
|
||
type="warning"
|
||
class="legacy-alert"
|
||
:title="t('channel.autoTrigger.form.legacyTitle')"
|
||
>
|
||
{{ legacyConditionText }}
|
||
</a-alert>
|
||
|
||
<div class="form-grid">
|
||
<div class="form-row">
|
||
<label class="form-label required">{{ t('channel.autoTrigger.table.ruleName') }}</label>
|
||
<a-input
|
||
v-model="formModel.ruleName"
|
||
:placeholder="t('channel.autoTrigger.form.ruleNamePlaceholder')"
|
||
/>
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="form-label">{{ t('channel.autoTrigger.table.status') }}</label>
|
||
<a-select v-model="formModel.status">
|
||
<a-option :value="true">{{ t('channel.autoTrigger.message.statusEnabled') }}</a-option>
|
||
<a-option :value="false">{{ t('channel.autoTrigger.message.statusDisabled') }}</a-option>
|
||
</a-select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="form-label required">{{ t('channel.autoTrigger.table.triggerTiming') }}</label>
|
||
<a-select v-model="formModel.timingCode" :placeholder="t('common.selectPlaceholder')">
|
||
<a-option v-for="opt in timingOptions" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="form-label required">{{ t('channel.autoTrigger.table.operation') }}</label>
|
||
<a-select v-model="formModel.operationCode" :placeholder="t('common.selectPlaceholder')">
|
||
<a-option v-for="opt in operationOptions" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="form-label">{{ t('channel.autoTrigger.table.priority') }}</label>
|
||
<a-input-number
|
||
v-model="formModel.priority"
|
||
:min="1"
|
||
:max="9999"
|
||
hide-button
|
||
style="width: 100%"
|
||
/>
|
||
</div>
|
||
<div class="form-row" />
|
||
</div>
|
||
|
||
<div class="condition-section">
|
||
<div class="section-head">
|
||
<label class="form-label required">{{ t('channel.autoTrigger.table.triggerCondition') }}</label>
|
||
<div class="section-actions">
|
||
<a-button type="text" size="small" @click="applyExampleRule">
|
||
{{ t('channel.autoTrigger.form.loadExample') }}
|
||
</a-button>
|
||
<a-button type="text" size="small" :loading="testLoading" @click="handleTestFormula">
|
||
{{ t('channel.autoTrigger.form.testFormula') }}
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
<FormulaConditionEditor ref="formulaEditorRef" v-model="formulaText" />
|
||
<div class="test-sample-panel">
|
||
<div class="test-sample-title">{{ t('channel.autoTrigger.form.testSample') }}</div>
|
||
<div class="form-grid test-sample-grid">
|
||
<div class="form-row">
|
||
<label class="form-label">{{ t('channel.autoTrigger.form.sampleCountry') }}</label>
|
||
<a-input v-model="testSample.consigneeCountry" allow-clear />
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="form-label">{{ t('channel.autoTrigger.form.sampleCustomer') }}</label>
|
||
<a-input v-model="testSample.customerName" allow-clear />
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="form-label">{{ t('channel.autoTrigger.form.sampleProduct') }}</label>
|
||
<a-input v-model="testSample.productCode" allow-clear />
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="form-label">{{ t('channel.autoTrigger.form.sampleLength') }}</label>
|
||
<a-input-number v-model="testSample.length" :min="0" hide-button style="width: 100%" />
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="form-label">{{ t('channel.autoTrigger.form.sampleWidth') }}</label>
|
||
<a-input-number v-model="testSample.width" :min="0" hide-button style="width: 100%" />
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="form-label">{{ t('channel.autoTrigger.form.sampleHeight') }}</label>
|
||
<a-input-number v-model="testSample.height" :min="0" hide-button style="width: 100%" />
|
||
</div>
|
||
</div>
|
||
<a-alert
|
||
v-if="testResult"
|
||
class="test-result-alert"
|
||
:type="testResult.error ? 'warning' : testResult.matched ? 'success' : 'info'"
|
||
:title="testResult.error || (testResult.matched ? t('channel.autoTrigger.form.testMatched') : t('channel.autoTrigger.form.testNotMatched'))"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-grid">
|
||
<div class="form-row full">
|
||
<label class="form-label required">{{ t('channel.autoTrigger.table.promptMessage') }}</label>
|
||
<a-input
|
||
v-model="formModel.promptMessage"
|
||
:placeholder="t('channel.autoTrigger.form.promptPlaceholder')"
|
||
/>
|
||
</div>
|
||
<div class="form-row full">
|
||
<label class="form-label">{{ t('channel.autoTrigger.table.notifier') }}</label>
|
||
<a-input
|
||
v-model="formModel.notifier"
|
||
:placeholder="t('channel.autoTrigger.form.notifierPlaceholder')"
|
||
allow-clear
|
||
/>
|
||
</div>
|
||
<template v-if="modalMode === 'edit'">
|
||
<div class="form-row">
|
||
<label class="form-label">{{ t('channel.autoTrigger.table.createTime') }}</label>
|
||
<a-input v-model="formModel.createTime" disabled />
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="form-label">{{ t('channel.autoTrigger.table.creator') }}</label>
|
||
<a-input v-model="formModel.creator" disabled />
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</ModalPro>
|
||
</div>
|
||
</div>
|
||
</template> |