| | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { toAccessibleFileUrl } from '#/utils'; |
| | | import { |
| | | ACTIVITY_TYPE, |
| | | createActivity, |
| | | getActivity, |
| | | updateActivity, |
| | | } from '#/api/wxcp/activity'; |
| | | |
| | | import { useActivityFormSchema } from '../data'; |
| | | import { FIELD_TYPE_OPTIONS, useActivityFormSchema } from '../data'; |
| | | import FieldEditor from './field-editor.vue'; |
| | | |
| | | defineOptions({ name: 'WxCpActivityForm' }); |
| | | |
| | |
| | | const formType = ref<ActivityFormType>('create'); |
| | | const id = ref<number>(); |
| | | const submitting = ref(false); |
| | | |
| | | /** 当前活动模板类型,用于控制采集字段编辑器的显隐 */ |
| | | const activityType = ref<number>(ACTIVITY_TYPE.FORM); |
| | | /** 数据采集模板的自定义字段(表单项之外单独维护) */ |
| | | const fields = ref<WxCpActivityApi.Field[]>([]); |
| | | |
| | | const showFieldEditor = computed( |
| | | () => activityType.value === ACTIVITY_TYPE.FORM && formType.value !== 'detail', |
| | | ); |
| | | |
| | | function fieldTypeLabel(type?: string) { |
| | | return ( |
| | | FIELD_TYPE_OPTIONS.find((item) => item.value === type)?.label ?? |
| | | type ?? |
| | | '-' |
| | | ); |
| | | } |
| | | |
| | | const isEdit = computed(() => formType.value !== 'detail'); |
| | | const title = computed(() => { |
| | |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | handleValuesChange: (values) => { |
| | | activityType.value = |
| | | (values.activityType as number) ?? ACTIVITY_TYPE.FORM; |
| | | }, |
| | | }); |
| | | |
| | | /** |
| | | * 把活动详情映射成表单值 |
| | | * 把附件列表转成上传组件能识别的文件对象 |
| | | * |
| | | * 封面在后端不落业务表字段,而是存在附件中间表里, |
| | | * 回显时要把 attachmentList 转成上传组件能识别的文件对象。 |
| | | * 回显对象必须带 response:组件设了 resultField 后只认 item.response, |
| | | * 不带的话「进编辑页不动图直接保存」会走 URL 分支 → NaN → [] → 图被清空 |
| | | */ |
| | | function activityToForm( |
| | | activity: WxCpActivityApi.Activity, |
| | | ): Record<string, unknown> { |
| | | const form: Record<string, unknown> = { ...activity }; |
| | | form.blobIds = (activity.attachmentList ?? []).map((item) => { |
| | | function attachmentsToUploadFiles( |
| | | list: WxCpActivityApi.SignupAttachment[] | undefined, |
| | | ) { |
| | | return (list ?? []).map((item) => { |
| | | const name = item.name || item.originalFilename || ''; |
| | | const url = toAccessibleFileUrl(item.url || item.previewURL); |
| | | return { |
| | |
| | | url, |
| | | status: 'done', |
| | | id: item.id, |
| | | // 回显对象也必须带 response:组件设了 resultField 后只认 item.response, |
| | | // 不带的话「进编辑页不动图直接保存」会走 URL 分支 → NaN → [] → 封面被清空 |
| | | response: { id: item.id, name, url }, |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 把活动详情映射成表单值 |
| | | * |
| | | * 封面与详情图片在后端都不落业务表字段,而是存在附件中间表里(两套 recordType), |
| | | * 回显时分别把 attachmentList / detailAttachmentList 转成上传组件的文件对象。 |
| | | */ |
| | | function activityToForm( |
| | | activity: WxCpActivityApi.Activity, |
| | | ): Record<string, unknown> { |
| | | const form: Record<string, unknown> = { ...activity }; |
| | | form.blobIds = attachmentsToUploadFiles(activity.attachmentList); |
| | | form.detailBlobIds = attachmentsToUploadFiles(activity.detailAttachmentList); |
| | | // 图片通知 / 附件通知的素材走各自的附件关系,回显时同样转成上传组件对象 |
| | | form.imageBlobIds = attachmentsToUploadFiles(activity.imageAttachmentList); |
| | | form.fileBlobIds = attachmentsToUploadFiles(activity.fileAttachmentList); |
| | | return form; |
| | | } |
| | | |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getActivity(data.id); |
| | | activityType.value = detail.activityType ?? ACTIVITY_TYPE.FORM; |
| | | fields.value = (detail.fieldList ?? []).map((item) => ({ ...item })); |
| | | await formApi.setValues(activityToForm(detail)); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } else { |
| | | await formApi.resetForm(); |
| | | activityType.value = ACTIVITY_TYPE.FORM; |
| | | fields.value = []; |
| | | // 默认给「数据采集/评价」,与改造前的报名活动一致 |
| | | await formApi.setValues( |
| | | { |
| | | activityType: ACTIVITY_TYPE.FORM, |
| | | ratingEnabled: false, |
| | | ratingMax: 5, |
| | | signupLimit: 0, |
| | | }, |
| | | false, |
| | | ); |
| | | } |
| | | }, |
| | | }); |
| | |
| | | submitting.value = true; |
| | | try { |
| | | const data = (await formApi.getValues()) as Record<string, unknown>; |
| | | const type = data.activityType ?? ACTIVITY_TYPE.FORM; |
| | | const payload = { |
| | | ...data, |
| | | blobIds: normalizeBlobIds(data.blobIds), |
| | | detailBlobIds: normalizeBlobIds(data.detailBlobIds), |
| | | imageBlobIds: normalizeBlobIds(data.imageBlobIds), |
| | | fileBlobIds: normalizeBlobIds(data.fileBlobIds), |
| | | // 只有数据采集模板提交字段定义;其它模板不传,避免误清空 |
| | | fieldList: type === ACTIVITY_TYPE.FORM ? fields.value : undefined, |
| | | } as WxCpActivityApi.Activity; |
| | | if (id.value) { |
| | | await updateActivity({ ...payload, id: id.value }); |
| | |
| | | <Modal :title="title" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | |
| | | <div v-if="showFieldEditor" class="mx-4 mt-2"> |
| | | <FieldEditor v-model="fields" /> |
| | | </div> |
| | | <div |
| | | v-else-if=" |
| | | formType === 'detail' && |
| | | activityType === ACTIVITY_TYPE.FORM && |
| | | fields.length |
| | | " |
| | | class="mx-4 mt-2" |
| | | > |
| | | <div class="mb-2 text-sm font-medium">采集字段</div> |
| | | <div |
| | | v-for="(item, index) in fields" |
| | | :key="index" |
| | | class="text-sm text-gray-600 dark:text-gray-400" |
| | | > |
| | | · {{ item.fieldLabel }}({{ fieldTypeLabel(item.fieldType) }}) |
| | | <span v-if="item.required" class="text-red-500">*</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="modalApi.close()">取消</Button> |