fix:some
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s

This commit is contained in:
luo
2026-10-21 09:54:21 +08:00
parent 22e796ba23
commit d82e716d33
3 changed files with 548 additions and 75 deletions

View File

@@ -97,6 +97,7 @@ export interface WecomBusinessField {
description: string
name: string
value_type: string
required_mapping: boolean
}
export interface WecomBusinessFieldList {

View File

@@ -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;
}