liu
3 天以前 2b695bfabdceccb0ee19c12c8e655bd7f47a6e2f
src/views/wxcp/notice/modules/form.vue
@@ -9,6 +9,7 @@
import { Button, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { toAccessibleFileUrl } from '#/utils';
import {
  createNotice,
  getNotice,
@@ -16,6 +17,7 @@
} from '#/api/wxcp/notice';
import { NOTICE_TYPE, useNoticeFormSchema } from '../data';
import ArticleEditor from './article-editor.vue';
defineOptions({ name: 'WxCpNoticeForm' });
@@ -24,6 +26,14 @@
const formType = ref<NoticeFormType>('create');
const id = ref<number>();
const submitting = ref(false);
/** 当前模板类型,用于控制多图文编辑器显隐 */
const noticeType = ref<number>(NOTICE_TYPE.TEXT);
/** 多图文条目(表单项之外单独维护) */
const articles = ref<WxCpNoticeApi.NoticeArticle[]>([]);
const showArticleEditor = computed(
  () => noticeType.value === NOTICE_TYPE.MULTI_NEWS,
);
const isEdit = computed(() => formType.value !== 'detail');
const title = computed(() => {
@@ -31,6 +41,82 @@
  if (id.value) return '编辑通知';
  return '新建通知';
});
/**
 * 把附件列表转成上传组件能识别的文件对象
 *
 * 回显对象必须带 response:组件设了 resultField 后只认 item.response,
 * 不带的话「进编辑页不动图直接保存」会走 URL 分支 → NaN → [] → 图被清空
 */
function attachmentsToUploadFiles(
  list: WxCpNoticeApi.NoticeAttachment[] | undefined,
) {
  return (list ?? []).map((item) => {
    const name = item.name || item.originalFilename || '';
    const url = toAccessibleFileUrl(item.url || item.previewURL);
    return {
      uid: String(item.id),
      name,
      url,
      status: 'done',
      id: item.id,
      response: { id: item.id, name, url },
    };
  });
}
/**
 * 把上传组件的值规整成 blobId 数组
 *
 * ImageUpload 在 maxNumber=1 时返回单个值而不是数组,这里统一包成数组再提交
 */
function normalizeBlobIds(value: unknown): number[] {
  const list = Array.isArray(value)
    ? value
    : value === '' || value === null || value === undefined
      ? []
      : [value];
  return list
    .map((item) =>
      item && typeof item === 'object' && 'id' in item
        ? Number((item as { id: unknown }).id)
        : Number(item),
    )
    .filter((blobId) => Number.isFinite(blobId));
}
/**
 * 把通知详情映射成表单值
 *
 * 图片/附件不落业务表字段,统一存在附件中间表,
 * 回显时把 imageAttachmentList / fileAttachmentList 转成上传组件文件对象。
 * 历史数据只有旧版 fileBlobId,这里兜底成单个附件,避免编辑后附件丢失。
 */
function noticeToForm(
  detail: WxCpNoticeApi.Notice,
): Record<string, unknown> {
  const form: Record<string, unknown> = { ...detail };
  form.imageBlobIds = attachmentsToUploadFiles(detail.imageAttachmentList);
  form.videoBlobIds = attachmentsToUploadFiles(detail.videoAttachmentList);
  form.fileBlobIds =
    detail.fileAttachmentList?.length
      ? attachmentsToUploadFiles(detail.fileAttachmentList)
      : detail.fileBlobId
        ? [
            {
              uid: String(detail.fileBlobId),
              name: detail.fileName || '',
              status: 'done',
              id: detail.fileBlobId,
              response: {
                id: detail.fileBlobId,
                name: detail.fileName || '',
              },
            },
          ]
        : [];
  return form;
}
const [Form, formApi] = useVbenForm({
  commonConfig: {
@@ -42,6 +128,9 @@
  schema: [],
  showDefaultActions: false,
  wrapperClass: 'grid-cols-1',
  handleValuesChange: (values) => {
    noticeType.value = (values.noticeType as number) ?? NOTICE_TYPE.TEXT;
  },
});
const [Modal, modalApi] = useVbenModal({
@@ -59,24 +148,18 @@
      modalApi.lock();
      try {
        const detail = await getNotice(data.id);
        const values: Record<string, unknown> = { ...detail };
        // 文件通知:把 blobId 还原成上传组件可识别的文件对象
        if (detail.noticeType === NOTICE_TYPE.FILE && detail.fileBlobId) {
          values.fileBlobId = [
            {
              uid: String(detail.fileBlobId),
              name: detail.fileName || '',
              status: 'done',
              id: detail.fileBlobId,
            },
          ];
        }
        await formApi.setValues(values);
        noticeType.value = detail.noticeType ?? NOTICE_TYPE.TEXT;
        articles.value = (detail.articleList ?? []).map((item) => ({
          ...item,
        }));
        await formApi.setValues(noticeToForm(detail));
      } finally {
        modalApi.unlock();
      }
    } else {
      await formApi.resetForm();
      noticeType.value = NOTICE_TYPE.TEXT;
      articles.value = [];
      await formApi.setValues({ noticeType: NOTICE_TYPE.TEXT }, false);
    }
  },
@@ -91,30 +174,23 @@
  try {
    const data = (await formApi.getValues()) as WxCpNoticeApi.Notice;
    // 保存时只做文件归一化;具体模板必填项在推送时按所选模板校验
    const noticeType = data.noticeType ?? NOTICE_TYPE.TEXT;
    if (noticeType === NOTICE_TYPE.FILE) {
      const raw = data.fileBlobId as unknown;
      let blobId: number | undefined;
      if (Array.isArray(raw)) {
        const first = raw[0];
        blobId =
          typeof first === 'number'
            ? first
            : (first as { id?: number } | undefined)?.id;
      } else if (typeof raw === 'number') {
        blobId = raw;
      } else if (raw && typeof raw === 'object') {
        blobId = (raw as { id?: number }).id;
      }
      data.fileBlobId = blobId;
    } else {
      data.fileBlobId = undefined;
    }
    const payload = {
      ...data,
      // null = 不改动,[] = 清空;这里统一提交数组,切换模板后旧附件会被清掉
      imageBlobIds: normalizeBlobIds(data.imageBlobIds),
      fileBlobIds: normalizeBlobIds(data.fileBlobIds),
      videoBlobIds: normalizeBlobIds(data.videoBlobIds),
      // 多图文条目单独维护,只有该模板提交;null 表示不改动
      articleList: showArticleEditor.value ? articles.value : undefined,
      // 旧版单附件字段不再使用,避免和 fileBlobIds 重复
      fileBlobId: undefined,
      fileName: undefined,
    } as WxCpNoticeApi.Notice;
    if (id.value) {
      await updateNotice({ ...data, id: id.value });
      await updateNotice({ ...payload, id: id.value });
      message.success('修改成功');
    } else {
      await createNotice(data);
      await createNotice(payload);
      message.success('创建成功,请在列表中发布后再推送');
    }
    await modalApi.close();
@@ -131,6 +207,13 @@
  <Modal :title="title" class="w-3/5">
    <Form class="mx-4" />
    <div v-if="showArticleEditor" class="mx-4 mt-2">
      <ArticleEditor
        v-model="articles"
        :disabled="formType === 'detail'"
      />
    </div>
    <template #footer>
      <div class="flex justify-end gap-2">
        <Button @click="modalApi.close()">取消</Button>