front/src/views/channel/autoTrigger/index.vue
zzh 67cf30b6e8 feat: 自动触发公式引擎、查价分组运输方式与销售产品预览
新增自动触发中文公式编辑/测试、查价运输方式 Tab 分组选择,并完善销售产品字段预览与菜单展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 18:36:15 +08:00

1253 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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, '&quot;')}">${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>