This commit is contained in:
363
scripts/tests/wecomSceneMappings.test.mjs
Normal file
363
scripts/tests/wecomSceneMappings.test.mjs
Normal file
@@ -0,0 +1,363 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as vue from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { compileScript, parse } from '@vue/compiler-sfc'
|
||||
import ts from 'typescript'
|
||||
|
||||
const { descriptor } = parse(readFileSync('src/views/settings/wecom/scenes/index.vue', 'utf8'))
|
||||
const compiled = ts.transpileModule(
|
||||
compileScript(descriptor, { id: 'wecom-mappings-test' }).content,
|
||||
{
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
||||
}
|
||||
).outputText
|
||||
const field = (code, name = code, required_mapping = false) => ({
|
||||
code,
|
||||
name,
|
||||
required_mapping,
|
||||
value_type: 'string',
|
||||
description: `${name}说明`
|
||||
})
|
||||
const refundFields = [
|
||||
['refund_no', '退款单号'],
|
||||
['order_id', '订单 ID'],
|
||||
['order_no', '订单号'],
|
||||
['asset_identifier', '资产标识'],
|
||||
['actual_received_amount', '订单实收金额'],
|
||||
['requested_refund_amount', '申请退款金额'],
|
||||
['refund_voucher_key', '退款凭证'],
|
||||
['refund_reason', '退款原因'],
|
||||
['package_usage_id', '套餐使用记录 ID'],
|
||||
['refund_asset_type', '资产类型', true],
|
||||
['refund_device_type', '设备类型', true],
|
||||
['refund_device_model', '设备型号', true],
|
||||
['refund_package_used_mb', '当前退款套餐已用量(MB)', true],
|
||||
['refund_package_total_mb', '当前退款套餐总量(MB)', true],
|
||||
['refund_channel_trade_no', '原支付渠道交易流水号', true],
|
||||
['refund_applicant_remark', '申请人备注', true],
|
||||
['refund_method_code', '退款方式编码'],
|
||||
['refund_method', '退款方式', true],
|
||||
['customer_account_info', '客户收款信息'],
|
||||
['customer_voucher_key', '客户收款凭证'],
|
||||
['submitter_id', '提交人账号 ID'],
|
||||
['submitter_name', '提交人名称']
|
||||
].map((args) => field(...args))
|
||||
const control = (id, required = false) => ({ id, title: `控件 ${id}`, type: 'Text', required })
|
||||
const response = (data) => ({ code: 0, data })
|
||||
const scene = (mapping = []) => ({
|
||||
business_type: 'refund_approval',
|
||||
application_id: 1,
|
||||
template_id: 'template-1',
|
||||
status: 1,
|
||||
control_mapping: mapping
|
||||
})
|
||||
const settle = () => new Promise(setImmediate)
|
||||
function deferred() {
|
||||
let resolve, reject
|
||||
const promise = new Promise((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
function harness(overrides = {}) {
|
||||
const calls = { fields: [], templates: [], saves: [], warnings: [], errors: [] }
|
||||
const service = {
|
||||
getBusinessFields: async (type) => {
|
||||
calls.fields.push(type)
|
||||
return response({ items: refundFields })
|
||||
},
|
||||
inspectTemplate: async (...args) => {
|
||||
calls.templates.push(args)
|
||||
return response({ name: '模板', controls: [control('text-1')] })
|
||||
},
|
||||
saveScene: async (...args) => {
|
||||
calls.saves.push(args)
|
||||
return response({})
|
||||
},
|
||||
getApplications: async () => response({ items: [] }),
|
||||
getScenes: async () => response({ items: [] }),
|
||||
...overrides
|
||||
}
|
||||
const imports = {
|
||||
vue: { ...vue, onMounted() {} },
|
||||
'element-plus': {
|
||||
ElMessage: {
|
||||
warning: (message) => calls.warnings.push(message),
|
||||
error: (message) => calls.errors.push(message),
|
||||
success() {}
|
||||
},
|
||||
ElTag: {}
|
||||
},
|
||||
'@/composables/useCheckedColumns': {
|
||||
useCheckedColumns: () => ({ columns: vue.ref([]), columnChecks: vue.ref([]) })
|
||||
},
|
||||
'@/composables/useAuth': { useAuth: () => ({ hasAuth: () => true }) },
|
||||
'@/api/modules': { WecomService: service },
|
||||
'@/config/constants': { JULY_PERMISSIONS: { wecom: {} } },
|
||||
'@/utils/business/format': { formatDateTime: (value) => value }
|
||||
}
|
||||
const module = { exports: {} }
|
||||
new Function('require', 'module', 'exports', compiled)(
|
||||
(name) => {
|
||||
assert.ok(imports[name], `Unexpected dependency: ${name}`)
|
||||
return imports[name]
|
||||
},
|
||||
module,
|
||||
module.exports
|
||||
)
|
||||
const state = module.exports.default.setup({}, { expose() {} })
|
||||
return { state, service, calls }
|
||||
}
|
||||
|
||||
test('opening a new scene displays all 22 API fields before any template is synchronized', async () => {
|
||||
const { state, calls } = harness()
|
||||
state.createScene()
|
||||
await settle()
|
||||
assert.deepEqual(calls.fields, ['refund_approval'])
|
||||
assert.equal(calls.templates.length, 0)
|
||||
assert.equal(state.mappingRows.value.length, 22)
|
||||
assert.deepEqual(
|
||||
state.mappingRows.value.map((row) => row.business_field),
|
||||
refundFields.map((f) => f.code)
|
||||
)
|
||||
for (const [index, row] of state.mappingRows.value.entries()) {
|
||||
assert.equal(row.field_name, refundFields[index].name)
|
||||
assert.equal(row.required_mapping, refundFields[index].required_mapping)
|
||||
assert.equal(row.control_id, '')
|
||||
}
|
||||
})
|
||||
|
||||
test('selecting a business type immediately replaces old rows with all fields for that type', async () => {
|
||||
const { state, service } = harness()
|
||||
state.createScene()
|
||||
await settle()
|
||||
service.getBusinessFields = async (type) => {
|
||||
assert.equal(type, 'offline_recharge_approval')
|
||||
return response({ items: [field('recharge_no', '充值单号', true), field('remark', '备注')] })
|
||||
}
|
||||
state.sceneForm.business_type = 'offline_recharge_approval'
|
||||
state.handleBusinessTypeChange()
|
||||
assert.equal(state.mappingRows.value.length, 0)
|
||||
await settle()
|
||||
assert.deepEqual(
|
||||
state.mappingRows.value.map((row) => row.business_field),
|
||||
['recharge_no', 'remark']
|
||||
)
|
||||
})
|
||||
|
||||
for (const templateFirst of [false, true]) {
|
||||
test(`editing retains saved bindings and option mappings when ${templateFirst ? 'template' : 'fields'} responds first`, async () => {
|
||||
const fields = deferred()
|
||||
const template = deferred()
|
||||
const { state } = harness({
|
||||
getBusinessFields: () => fields.promise,
|
||||
inspectTemplate: () => template.promise
|
||||
})
|
||||
state.editScene(
|
||||
scene([
|
||||
{
|
||||
business_field: 'refund_method',
|
||||
control_id: 'select-1',
|
||||
control_type: 'Selector',
|
||||
option_mapping: { original: '原路退回' }
|
||||
}
|
||||
])
|
||||
)
|
||||
const finishFields = () => fields.resolve(response({ items: refundFields }))
|
||||
const finishTemplate = () =>
|
||||
template.resolve(
|
||||
response({
|
||||
name: '退款模板',
|
||||
controls: [{ ...control('select-1'), type: 'Selector' }]
|
||||
})
|
||||
)
|
||||
;(templateFirst ? finishTemplate : finishFields)()
|
||||
await settle()
|
||||
;(templateFirst ? finishFields : finishTemplate)()
|
||||
await settle()
|
||||
assert.equal(state.mappingRows.value.length, 22)
|
||||
const mapping = state.mappingRows.value.find((row) => row.business_field === 'refund_method')
|
||||
assert.equal(mapping.control_id, 'select-1')
|
||||
assert.equal(mapping.control_title, '控件 select-1')
|
||||
assert.equal(mapping.control_type, 'Selector')
|
||||
assert.deepEqual(JSON.parse(mapping.option_mapping), { original: '原路退回' })
|
||||
})
|
||||
}
|
||||
|
||||
test('resyncing templates keeps all business rows but clears controls removed from the template', async () => {
|
||||
const { state, service } = harness()
|
||||
state.editScene(
|
||||
scene([{ business_field: 'refund_no', control_id: 'text-1', control_type: 'Text' }])
|
||||
)
|
||||
await settle()
|
||||
service.inspectTemplate = async () => response({ name: '新模板', controls: [control('text-2')] })
|
||||
await state.syncTemplate()
|
||||
assert.equal(state.mappingRows.value.length, 22)
|
||||
assert.equal(state.mappingRows.value[0].control_id, '')
|
||||
assert.equal(state.mappingRows.value[0].control_type, '')
|
||||
})
|
||||
|
||||
test('required_mapping blocks saving missing business bindings; optional blank fields are not submitted', async () => {
|
||||
const { state, calls } = harness({
|
||||
getBusinessFields: async () =>
|
||||
response({ items: [field('required', '必填字段', true), field('optional')] })
|
||||
})
|
||||
state.editScene(scene())
|
||||
await settle()
|
||||
await state.saveScene()
|
||||
assert.equal(calls.saves.length, 0)
|
||||
assert.match(calls.warnings.at(-1), /必填业务字段“必填字段”/)
|
||||
const row = state.mappingRows.value[0]
|
||||
row.control_id = 'text-1'
|
||||
state.handleControlChange(row)
|
||||
await state.saveScene()
|
||||
assert.deepEqual(calls.saves[0], [
|
||||
'refund_approval',
|
||||
{
|
||||
application_id: 1,
|
||||
template_id: 'template-1',
|
||||
status: 1,
|
||||
control_mapping: [
|
||||
{
|
||||
business_field: 'required',
|
||||
control_id: 'text-1',
|
||||
control_type: 'Text',
|
||||
option_mapping: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
test('required template controls still require a business field independently of required_mapping', async () => {
|
||||
const { state, calls } = harness({
|
||||
getBusinessFields: async () => response({ items: [field('optional')] }),
|
||||
inspectTemplate: async () => response({ controls: [control('required-control', true)] })
|
||||
})
|
||||
state.editScene(scene())
|
||||
await settle()
|
||||
assert.equal(state.parseMapping(), undefined)
|
||||
assert.match(calls.warnings.at(-1), /必填控件/)
|
||||
})
|
||||
|
||||
test('selecting and clearing controls updates control IDs and types without changing the business field', async () => {
|
||||
const { state } = harness()
|
||||
state.editScene(scene())
|
||||
await settle()
|
||||
const row = state.mappingRows.value[0]
|
||||
row.control_id = 'text-1'
|
||||
state.handleControlChange(row)
|
||||
assert.equal(row.control_type, 'Text')
|
||||
assert.equal(row.control_title, '控件 text-1')
|
||||
row.control_id = ''
|
||||
state.handleControlChange(row)
|
||||
assert.equal(row.control_type, '')
|
||||
assert.equal(row.control_title, '')
|
||||
assert.equal(row.business_field, 'refund_no')
|
||||
})
|
||||
|
||||
test('out-of-order business field responses cannot overwrite a newer business type', async () => {
|
||||
const older = deferred()
|
||||
const newer = deferred()
|
||||
const { state } = harness({
|
||||
getBusinessFields: (type) => (type === 'refund_approval' ? older.promise : newer.promise)
|
||||
})
|
||||
state.createScene()
|
||||
state.sceneForm.business_type = 'offline_recharge_approval'
|
||||
state.handleBusinessTypeChange()
|
||||
newer.resolve(response({ items: [field('new-type')] }))
|
||||
await settle()
|
||||
older.resolve(response({ items: refundFields }))
|
||||
await settle()
|
||||
assert.equal(state.mappingRows.value[0].business_field, 'new-type')
|
||||
assert.equal(state.fieldsLoading.value, false)
|
||||
})
|
||||
|
||||
test('resetting the editor rejects pending responses even when the business type is unchanged', async () => {
|
||||
const pending = deferred()
|
||||
const { state, service } = harness({ getBusinessFields: () => pending.promise })
|
||||
state.createScene()
|
||||
service.getBusinessFields = async () => response({ items: [field('new-session')] })
|
||||
state.createScene()
|
||||
await settle()
|
||||
pending.resolve(response({ items: refundFields }))
|
||||
await settle()
|
||||
assert.equal(state.mappingRows.value[0].business_field, 'new-session')
|
||||
})
|
||||
|
||||
test('changing template settings rejects pending template responses and clears old control bindings', async () => {
|
||||
const pending = deferred()
|
||||
const { state } = harness({ inspectTemplate: () => pending.promise })
|
||||
state.editScene(scene())
|
||||
await settle()
|
||||
state.sceneForm.template_id = 'new-template'
|
||||
state.handleTemplateChange()
|
||||
pending.resolve(response({ name: '过期模板', controls: [control('old-control')] }))
|
||||
await settle()
|
||||
assert.equal(state.templateControls.value.length, 0)
|
||||
assert.equal(state.templateLoaded.value, false)
|
||||
assert.equal(state.mappingRows.value.length, 22)
|
||||
})
|
||||
|
||||
test('loading or failed field discovery cannot save an empty incomplete mapping', async (t) => {
|
||||
const pending = deferred()
|
||||
const { state, calls } = harness({ getBusinessFields: () => pending.promise })
|
||||
t.mock.method(console, 'error', () => {})
|
||||
state.sceneForm.template_id = 'template'
|
||||
const loading = state.loadBusinessFields('refund_approval')
|
||||
await state.saveScene()
|
||||
assert.equal(calls.saves.length, 0)
|
||||
pending.reject(new Error('network unavailable'))
|
||||
await loading
|
||||
await state.saveScene()
|
||||
assert.equal(calls.saves.length, 0)
|
||||
assert.equal(state.businessFieldsLoaded.value, false)
|
||||
assert.equal(calls.errors.length, 1)
|
||||
})
|
||||
|
||||
function findMappingSection(node) {
|
||||
if (
|
||||
node.type === 1 &&
|
||||
node.props.some(
|
||||
(prop) => prop.name === 'class' && prop.value?.content.includes('mapping-section')
|
||||
)
|
||||
)
|
||||
return node
|
||||
return node.children?.map(findMappingSection).find(Boolean)
|
||||
}
|
||||
|
||||
test('rendered mapping section shows all 22 field names and exactly eight required stars before template sync', async () => {
|
||||
const { state } = harness()
|
||||
state.createScene()
|
||||
await settle()
|
||||
const section = findMappingSection(descriptor.template.ast)
|
||||
const app = vue.createSSRApp({
|
||||
setup: () => ({ ...state }),
|
||||
render: vue.compile(section.loc.source)
|
||||
})
|
||||
for (const name of ['ElTag', 'ElSelect', 'ElOption', 'ElInput', 'ElEmpty']) {
|
||||
app.component(
|
||||
name,
|
||||
vue.defineComponent({
|
||||
setup:
|
||||
(_, { attrs, slots }) =>
|
||||
() =>
|
||||
vue.h('div', attrs, slots.default?.())
|
||||
})
|
||||
)
|
||||
}
|
||||
app.directive('loading', { getSSRProps: () => ({}) })
|
||||
const html = await renderToString(app)
|
||||
assert.equal((html.match(/class="mapping-row"/g) || []).length, 22)
|
||||
assert.equal((html.match(/aria-label="必填"/g) || []).length, 8)
|
||||
for (const field of refundFields) assert.ok(html.includes(field.name), field.name)
|
||||
assert.match(html, /请先同步模板/)
|
||||
assert.doesNotMatch(html, /暂无控件映射,请新增一条/)
|
||||
assert.doesNotMatch(html, /控件 ID|选择模板控件后自动填充/)
|
||||
assert.doesNotMatch(html, /mapping-field__description/)
|
||||
for (const field of refundFields) assert.ok(!html.includes(field.description), field.description)
|
||||
})
|
||||
@@ -97,6 +97,7 @@ export interface WecomBusinessField {
|
||||
description: string
|
||||
name: string
|
||||
value_type: string
|
||||
required_mapping: boolean
|
||||
}
|
||||
|
||||
export interface WecomBusinessFieldList {
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
clearable
|
||||
filterable
|
||||
class="full-width"
|
||||
@change="handleTemplateChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="application in applications"
|
||||
@@ -91,7 +92,11 @@
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="模板 ID" required>
|
||||
<ElInput v-model="sceneForm.template_id" placeholder="请输入企微后台模板 ID">
|
||||
<ElInput
|
||||
v-model="sceneForm.template_id"
|
||||
placeholder="请输入企微后台模板 ID"
|
||||
@input="handleTemplateChange"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton :loading="inspectingTemplate" @click="syncTemplate()">
|
||||
同步模板
|
||||
@@ -113,60 +118,75 @@
|
||||
<div class="dialog-section__header mapping-heading">
|
||||
<div>
|
||||
<div class="dialog-section__title">控件映射</div>
|
||||
<div class="dialog-section__description">将业务字段绑定到企业微信模板控件</div>
|
||||
<div class="dialog-section__description">
|
||||
将业务字段绑定到企业微信模板控件,<span class="mapping-required">*</span>
|
||||
表示必填映射
|
||||
</div>
|
||||
</div>
|
||||
<ElTag v-if="!templateControls.length" type="info" effect="plain" class="mapping-tip">
|
||||
<div v-if="!templateControls.length" class="mapping-tip">
|
||||
请先填写模板 ID 并点击“同步模板”,再选择模板控件。
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mappingRows.length" class="mapping-list">
|
||||
<div v-for="mapping in mappingRows" :key="mapping.key" class="mapping-row">
|
||||
<div class="mapping-row__grid">
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">业务字段</span>
|
||||
<ElSelect
|
||||
v-model="mapping.business_field"
|
||||
filterable
|
||||
clearable
|
||||
:loading="fieldsLoading"
|
||||
placeholder="选择业务字段"
|
||||
>
|
||||
<ElOption
|
||||
v-for="field in businessFields"
|
||||
:key="field.code"
|
||||
:label="field.name"
|
||||
:value="field.code"
|
||||
<div v-loading="fieldsLoading" class="mapping-content">
|
||||
<div v-if="mappingRows.length" class="mapping-list">
|
||||
<div v-for="mapping in mappingRows" :key="mapping.key" class="mapping-row">
|
||||
<div class="mapping-row__grid">
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">业务字段</span>
|
||||
<div class="mapping-field__name">
|
||||
<span
|
||||
v-if="mapping.required_mapping"
|
||||
class="mapping-required"
|
||||
aria-label="必填"
|
||||
>*</span
|
||||
>
|
||||
{{ mapping.field_name }}
|
||||
</div>
|
||||
<span class="mapping-field__code">{{ mapping.business_field }}</span>
|
||||
</div>
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">模板控件</span>
|
||||
<ElSelect
|
||||
v-model="mapping.control_id"
|
||||
filterable
|
||||
clearable
|
||||
:disabled="!templateLoaded"
|
||||
:loading="inspectingTemplate"
|
||||
:aria-label="`${mapping.field_name}的模板控件`"
|
||||
:aria-required="mapping.required_mapping"
|
||||
:placeholder="templateLoaded ? '请选择模板控件' : '请先同步模板'"
|
||||
@change="handleControlChange(mapping)"
|
||||
>
|
||||
<ElOption
|
||||
v-for="control in templateControls"
|
||||
:key="control.id"
|
||||
:label="control.title"
|
||||
:value="control.id"
|
||||
/>
|
||||
<ElOption
|
||||
v-if="!templateLoaded && mapping.control_id"
|
||||
:label="mapping.control_title || '已配置控件(待同步)'"
|
||||
:value="mapping.control_id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</div>
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">控件类型</span>
|
||||
<ElInput
|
||||
:model-value="mapping.control_type"
|
||||
readonly
|
||||
placeholder="同步模板后自动填充"
|
||||
/>
|
||||
</ElSelect>
|
||||
</div>
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">模板控件</span>
|
||||
<ElInput
|
||||
:model-value="mapping.control_title"
|
||||
readonly
|
||||
placeholder="模板控件名称"
|
||||
/>
|
||||
</div>
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">控件 ID</span>
|
||||
<ElInput
|
||||
:model-value="mapping.control_id"
|
||||
readonly
|
||||
placeholder="选择模板控件后自动填充"
|
||||
/>
|
||||
</div>
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">控件类型</span>
|
||||
<ElInput
|
||||
:model-value="mapping.control_type"
|
||||
readonly
|
||||
placeholder="同步模板后自动填充"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty
|
||||
v-else-if="!fieldsLoading"
|
||||
description="当前业务类型暂无可映射字段"
|
||||
:image-size="60"
|
||||
/>
|
||||
</div>
|
||||
<ElEmpty v-else description="暂无控件映射,请新增一条" :image-size="60" />
|
||||
</div>
|
||||
</ElForm>
|
||||
|
||||
@@ -174,7 +194,14 @@
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="editorVisible = false">取消</ElButton>
|
||||
<ElButton @click="createScene">清空表单</ElButton>
|
||||
<ElButton type="primary" :loading="saving" @click="saveScene"> 保存并校验 </ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
:disabled="fieldsLoading || !businessFieldsLoaded || inspectingTemplate"
|
||||
@click="saveScene"
|
||||
>
|
||||
保存并校验
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
@@ -203,10 +230,11 @@
|
||||
interface MappingEditor {
|
||||
key: number
|
||||
business_field: string
|
||||
field_name: string
|
||||
required_mapping: boolean
|
||||
control_title: string
|
||||
control_id: string
|
||||
control_type: string
|
||||
required: boolean
|
||||
option_mapping: string
|
||||
}
|
||||
|
||||
@@ -215,12 +243,16 @@
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const fieldsLoading = ref(false)
|
||||
const businessFieldsLoaded = ref(false)
|
||||
const inspectingTemplate = ref(false)
|
||||
const editorVisible = ref(false)
|
||||
const applications = ref<WecomApplication[]>([])
|
||||
const businessFields = ref<WecomBusinessField[]>([])
|
||||
const templateControls = ref<WecomTemplateControl[]>([])
|
||||
const templateName = ref('')
|
||||
const templateLoaded = ref(false)
|
||||
let fieldsRequestId = 0
|
||||
let templateRequestId = 0
|
||||
const scenes = ref<WecomScene[]>([])
|
||||
const selectedBusinessType = ref<WecomBusinessType>()
|
||||
const nextMappingKey = ref(1)
|
||||
@@ -244,22 +276,29 @@
|
||||
const formatDate = (value?: string | null) => (value ? formatDateTime(value) : '-')
|
||||
|
||||
const loadBusinessFields = async (businessType: WecomBusinessType) => {
|
||||
const requestId = ++fieldsRequestId
|
||||
fieldsLoading.value = true
|
||||
businessFieldsLoaded.value = false
|
||||
try {
|
||||
const response = await WecomService.getBusinessFields(businessType)
|
||||
if (requestId !== fieldsRequestId || businessType !== sceneForm.business_type) return
|
||||
if (response.code === 0) {
|
||||
businessFields.value = response.data.items || []
|
||||
businessFieldsLoaded.value = true
|
||||
syncMappingRows()
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestId !== fieldsRequestId) return
|
||||
console.error('加载审批业务字段失败:', error)
|
||||
ElMessage.error('加载业务字段失败,请重新选择业务类型或重新打开编辑')
|
||||
} finally {
|
||||
fieldsLoading.value = false
|
||||
if (requestId === fieldsRequestId) fieldsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleBusinessTypeChange = () => {
|
||||
businessFields.value = []
|
||||
mappingRows.value.forEach((mapping) => {
|
||||
mapping.business_field = ''
|
||||
})
|
||||
mappingRows.value = []
|
||||
void loadBusinessFields(sceneForm.business_type)
|
||||
}
|
||||
|
||||
@@ -295,8 +334,15 @@
|
||||
])
|
||||
|
||||
const fillEditor = (scene?: WecomScene) => {
|
||||
fieldsRequestId += 1
|
||||
templateRequestId += 1
|
||||
businessFields.value = []
|
||||
businessFieldsLoaded.value = false
|
||||
fieldsLoading.value = false
|
||||
inspectingTemplate.value = false
|
||||
templateControls.value = []
|
||||
templateName.value = ''
|
||||
templateLoaded.value = false
|
||||
if (!scene) {
|
||||
selectedBusinessType.value = undefined
|
||||
Object.assign(sceneForm, {
|
||||
@@ -318,10 +364,11 @@
|
||||
mappingRows.value = (scene.control_mapping || []).map((mapping) => ({
|
||||
key: nextMappingKey.value++,
|
||||
business_field: mapping.business_field,
|
||||
field_name: mapping.business_field,
|
||||
required_mapping: false,
|
||||
control_title: '',
|
||||
control_id: mapping.control_id,
|
||||
control_type: mapping.control_type,
|
||||
required: true,
|
||||
option_mapping: JSON.stringify(mapping.option_mapping || {})
|
||||
}))
|
||||
}
|
||||
@@ -339,24 +386,48 @@
|
||||
void syncTemplate(false)
|
||||
}
|
||||
|
||||
const syncMappingRows = (controls: WecomTemplateControl[]) => {
|
||||
const syncMappingRows = () => {
|
||||
if (!businessFieldsLoaded.value) return
|
||||
const existingMappings = new Map(
|
||||
mappingRows.value.map((mapping) => [mapping.control_id, mapping])
|
||||
mappingRows.value.map((mapping) => [mapping.business_field, mapping])
|
||||
)
|
||||
mappingRows.value = controls.map((control) => {
|
||||
const existing = existingMappings.get(control.id)
|
||||
mappingRows.value = businessFields.value.map((field) => {
|
||||
const existing = existingMappings.get(field.code)
|
||||
const control = templateControls.value.find((item) => item.id === existing?.control_id)
|
||||
const keepExisting = !templateLoaded.value || Boolean(control)
|
||||
return {
|
||||
key: existing?.key || nextMappingKey.value++,
|
||||
business_field: existing?.business_field || '',
|
||||
control_title: control.title,
|
||||
control_id: control.id,
|
||||
control_type: control.type,
|
||||
required: control.required,
|
||||
option_mapping: existing?.option_mapping || '{}'
|
||||
business_field: field.code,
|
||||
field_name: field.name,
|
||||
required_mapping: field.required_mapping === true,
|
||||
control_title: control?.title || '',
|
||||
control_id: control?.id || (keepExisting ? existing?.control_id || '' : ''),
|
||||
control_type: control?.type || (keepExisting ? existing?.control_type || '' : ''),
|
||||
option_mapping: keepExisting ? existing?.option_mapping || '{}' : '{}'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleControlChange = (mapping: MappingEditor) => {
|
||||
const control = templateControls.value.find((item) => item.id === mapping.control_id)
|
||||
mapping.control_id = control?.id || ''
|
||||
mapping.control_title = control?.title || ''
|
||||
mapping.control_type = control?.type || ''
|
||||
mapping.option_mapping = '{}'
|
||||
}
|
||||
|
||||
const handleTemplateChange = () => {
|
||||
templateRequestId += 1
|
||||
inspectingTemplate.value = false
|
||||
templateControls.value = []
|
||||
templateName.value = ''
|
||||
templateLoaded.value = false
|
||||
mappingRows.value.forEach((mapping) => {
|
||||
mapping.control_id = ''
|
||||
handleControlChange(mapping)
|
||||
})
|
||||
}
|
||||
|
||||
const syncTemplate = async (showMessage = true) => {
|
||||
if (!sceneForm.application_id) {
|
||||
ElMessage.warning('请先选择企微应用')
|
||||
@@ -367,21 +438,28 @@
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = ++templateRequestId
|
||||
inspectingTemplate.value = true
|
||||
try {
|
||||
const response = await WecomService.inspectTemplate(sceneForm.application_id, {
|
||||
template_id: sceneForm.template_id.trim()
|
||||
})
|
||||
if (requestId !== templateRequestId) return
|
||||
if (response.code === 0) {
|
||||
templateControls.value = response.data.controls || []
|
||||
templateName.value = response.data.name || ''
|
||||
syncMappingRows(templateControls.value)
|
||||
templateLoaded.value = true
|
||||
syncMappingRows()
|
||||
if (showMessage) {
|
||||
ElMessage.success(`模板控件已同步,共 ${templateControls.value.length} 项`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestId !== templateRequestId) return
|
||||
console.error('同步审批模板失败:', error)
|
||||
if (showMessage) ElMessage.error('同步模板失败,请重试')
|
||||
} finally {
|
||||
inspectingTemplate.value = false
|
||||
if (requestId === templateRequestId) inspectingTemplate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,16 +503,14 @@
|
||||
const parseMapping = (): WecomSceneControlMapping[] | undefined => {
|
||||
const result: WecomSceneControlMapping[] = []
|
||||
for (const mapping of mappingRows.value) {
|
||||
if (!mapping.business_field) {
|
||||
if (mapping.required) {
|
||||
ElMessage.warning(
|
||||
`请为必填控件“${mapping.control_title || mapping.control_id}”选择业务字段`
|
||||
)
|
||||
if (!mapping.control_id) {
|
||||
if (mapping.required_mapping) {
|
||||
ElMessage.warning(`请为必填业务字段“${mapping.field_name}”选择模板控件`)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!mapping.control_id || !mapping.control_type) {
|
||||
if (!mapping.control_type) {
|
||||
ElMessage.warning(`控件“${mapping.control_title || mapping.control_id}”信息不完整`)
|
||||
return
|
||||
}
|
||||
@@ -455,10 +531,21 @@
|
||||
option_mapping: optionMapping
|
||||
})
|
||||
}
|
||||
const missingControl = templateControls.value.find(
|
||||
(control) => control.required && !result.some((mapping) => mapping.control_id === control.id)
|
||||
)
|
||||
if (missingControl) {
|
||||
ElMessage.warning(`请为必填控件“${missingControl.title || missingControl.id}”选择业务字段`)
|
||||
return
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const saveScene = async () => {
|
||||
if (fieldsLoading.value || !businessFieldsLoaded.value || inspectingTemplate.value) {
|
||||
ElMessage.warning('请等待业务字段和模板控件加载完成')
|
||||
return
|
||||
}
|
||||
if (!sceneForm.template_id.trim()) {
|
||||
ElMessage.warning('请输入模板 ID')
|
||||
return
|
||||
@@ -537,9 +624,13 @@
|
||||
}
|
||||
|
||||
.mapping-tip {
|
||||
flex-shrink: 0;
|
||||
flex-shrink: 1;
|
||||
max-width: 320px;
|
||||
margin-top: 0;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.template-name {
|
||||
@@ -568,6 +659,24 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mapping-content {
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.mapping-required {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.mapping-field__name {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.mapping-field__code {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.mapping-row {
|
||||
padding: 14px;
|
||||
margin-top: 6px;
|
||||
@@ -578,7 +687,7 @@
|
||||
|
||||
.mapping-row__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user