From 2b695bfabdceccb0ee19c12c8e655bd7f47a6e2f Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期二, 27 十月 2026 18:06:44 +0800
Subject: [PATCH] feat(wxcp): 通知前端支持图片/视频/Markdown/多图文与成员预览

---
 src/views/wxcp/notice/modules/article-editor.vue |  157 +++++++++++++
 src/views/wxcp/notice/modules/form.vue           |  151 +++++++++--
 src/views/wxcp/notice/index.vue                  |   23 +
 src/views/wxcp/notice/data.ts                    |   60 ++++
 src/views/wxcp/notice/modules/push-modal.vue     |  268 ++++++++++++++++++++--
 src/api/wxcp/notice.ts                           |   57 ++++
 6 files changed, 648 insertions(+), 68 deletions(-)

diff --git a/src/api/wxcp/notice.ts b/src/api/wxcp/notice.ts
index fe3ebb9..de46b1c 100644
--- a/src/api/wxcp/notice.ts
+++ b/src/api/wxcp/notice.ts
@@ -10,6 +10,41 @@
     REVOKED: 2,
   } as const;
 
+  /** 閫氱煡妯℃澘绫诲瀷 */
+  export const NOTICE_TYPE = {
+    TEXT: 1,
+    CARD: 2,
+    FILE: 3,
+    NEWS: 4,
+    IMAGE: 5,
+    VIDEO: 6,
+    MARKDOWN: 7,
+    MULTI_NEWS: 8,
+  } as const;
+
+  /** 澶氬浘鏂囨潯鐩� */
+  export interface NoticeArticle {
+    id?: number;
+    title?: string;
+    description?: string;
+    linkUrl?: string;
+    coverBlobId?: number;
+    coverAttachment?: NoticeAttachment;
+    sortNo?: number;
+  }
+
+  /** 閫氱煡闄勪欢锛堝浘鐗�/鏂囦欢/瑙嗛閫氱敤锛� */
+  export interface NoticeAttachment {
+    id?: number;
+    name?: string;
+    originalFilename?: string;
+    contentType?: string;
+    byteSize?: number;
+    url?: string;
+    previewURL?: string;
+    downloadURL?: string;
+  }
+
   /** 閫氱煡 */
   export interface Notice {
     id?: number;
@@ -21,8 +56,26 @@
     linkUrl?: string;
     linkBtnText?: string;
     coverUrl?: string;
+    /** 鏃х増鍗曢檮浠跺瓧娈碉紝鍏煎鍘嗗彶鏁版嵁 */
     fileBlobId?: number;
+    /** 鏃х増鍗曢檮浠舵枃浠跺悕 */
     fileName?: string;
+    /** 鍥剧墖閫氱煡鐨勫浘鐗� blobId 鍒楄〃 */
+    imageBlobIds?: number[];
+    /** 鍥剧墖閫氱煡鐨勫浘鐗囬檮浠跺垪琛� */
+    imageAttachmentList?: NoticeAttachment[];
+    /** 闄勪欢閫氱煡鐨勯檮浠� blobId 鍒楄〃 */
+    fileBlobIds?: number[];
+    /** 闄勪欢閫氱煡鐨勯檮浠跺垪琛� */
+    fileAttachmentList?: NoticeAttachment[];
+    /** 瑙嗛閫氱煡鐨勮棰� blobId 鍒楄〃 */
+    videoBlobIds?: number[];
+    /** 瑙嗛閫氱煡鐨勮棰戝垪琛� */
+    videoAttachmentList?: NoticeAttachment[];
+    /** 澶氬浘鏂囨秷鎭殑鍥炬枃鍒楄〃 */
+    articleList?: NoticeArticle[];
+    /** 鎴愬憳棰勮椤靛湴鍧� */
+    previewUrl?: string;
     status?: number;
     statusName?: string;
     pushScope?: number;
@@ -100,6 +153,10 @@
   coverUrl?: string;
   fileBlobId?: number;
   fileName?: string;
+  imageBlobIds?: number[];
+  fileBlobIds?: number[];
+  videoBlobIds?: number[];
+  articleList?: WxCpNoticeApi.NoticeArticle[];
 }) {
   return requestClient.post<{
     success: boolean;
diff --git a/src/views/wxcp/notice/data.ts b/src/views/wxcp/notice/data.ts
index 1b28e06..e09d0fb 100644
--- a/src/views/wxcp/notice/data.ts
+++ b/src/views/wxcp/notice/data.ts
@@ -13,12 +13,20 @@
   CARD: 2,
   FILE: 3,
   NEWS: 4,
+  IMAGE: 5,
+  VIDEO: 6,
+  MARKDOWN: 7,
+  MULTI_NEWS: 8,
 } as const;
 
 export const NOTICE_TYPE_OPTIONS = [
   { label: '绾枃鏈�氱煡', value: NOTICE_TYPE.TEXT },
   { label: '鍥炬枃鍗$墖锛堝~鎶�/閾炬帴锛�', value: NOTICE_TYPE.CARD },
   { label: '鏂囦欢閫氱煡锛堜笂浼犳枃浠舵帹閫侊級', value: NOTICE_TYPE.FILE },
+  { label: '鍥剧墖閫氱煡锛堜笂浼犲浘鐗囨帹閫侊級', value: NOTICE_TYPE.IMAGE },
+  { label: '瑙嗛閫氱煡锛堜笂浼犺棰戞帹閫侊級', value: NOTICE_TYPE.VIDEO },
+  { label: 'Markdown 閫氱煡锛堝瘜鏂囨湰鎺掔増锛�', value: NOTICE_TYPE.MARKDOWN },
+  { label: '澶氬浘鏂囨秷鎭紙澶氭潯閾炬帴鍥炬枃锛�', value: NOTICE_TYPE.MULTI_NEWS },
   { label: '鍥炬枃娑堟伅锛堟爣棰�+鎽樿+灏侀潰锛�', value: NOTICE_TYPE.NEWS },
 ];
 
@@ -58,7 +66,7 @@
         placeholder: '绾枃鏈�氱煡蹇呭~锛涘叾瀹冩ā鏉垮彲浣滀负琛ュ厖璇存槑',
         disabled: readonly,
       },
-      help: '绾枃鏈�氱煡鎺ㄩ�佹椂瓒呰繃 600 瀛椾細鑷姩鎴柇锛屽缓璁妸鍏抽敭淇℃伅鏀惧墠闈�',
+      help: '绾枃鏈帹閫佽秴杩� 600 瀛椾細鑷姩鎴柇锛汳arkdown 妯℃澘鏀寔 # 鏍囬銆�**鍔犵矖**銆�- 鍒楄〃銆�> 寮曠敤锛屾鏂囬渶鎺у埗鍦� 2048 瀛楄妭鍐�',
       formItemClass: 'col-span-2',
     },
     {
@@ -122,13 +130,33 @@
       formItemClass: 'col-span-2',
     },
     {
-      fieldName: 'fileBlobId',
-      label: '閫氱煡鏂囦欢',
+      // 鍥剧墖閫氱煡锛氭帹閫佹椂閫愬紶鍙戦�佸浘鐗囨秷鎭紝鏈�澶� 9 寮�
+      fieldName: 'imageBlobIds',
+      label: '鎺ㄩ�佸浘鐗�',
+      component: 'ImageUpload',
+      componentProps: {
+        valueKey: 'id',
+        resultField: 'id',
+        maxNumber: 9,
+        multiple: true,
+        disabled: readonly,
+      },
+      dependencies: {
+        triggerFields: ['noticeType'],
+        show: (values) => values.noticeType === NOTICE_TYPE.IMAGE,
+      },
+      help: '鎺ㄩ�佹椂閫愬紶鍙戦�佸浘鐗囨秷鎭紝鍗曞紶涓嶈秴杩� 10MB锛屾渶澶� 9 寮狅紱鎺ㄩ�佸悗闄勩�屾煡鐪嬪叏閮ㄥ浘鐗囥�嶅崱鐗囷紝鎴愬憳鍙湪椤甸潰鍐呮斁澶ф煡鐪�',
+      formItemClass: 'col-span-2',
+    },
+    {
+      // 闄勪欢閫氱煡锛氭帹閫佹椂閫愪釜鍙戞枃浠舵秷鎭紝鎺ユ敹浜哄彲鐩存帴涓嬭浇
+      fieldName: 'fileBlobIds',
+      label: '鎺ㄩ�侀檮浠�',
       component: 'FileUpload',
       componentProps: {
         valueKey: 'id',
-        maxNumber: 1,
-        multiple: false,
+        maxNumber: 5,
+        multiple: true,
         showDescription: !readonly,
         disabled: readonly,
       },
@@ -136,7 +164,27 @@
         triggerFields: ['noticeType'],
         show: (values) => values.noticeType === NOTICE_TYPE.FILE,
       },
-      help: '浼佷笟寰俊鏂囦欢娑堟伅涓婇檺 20MB',
+      help: '鎺ㄩ�佹椂閫愪釜鍙戦�佹枃浠舵秷鎭紝鍗曚釜涓嶈秴杩� 20MB锛屾渶澶� 5 涓紱鎺ㄩ�佸悗闄勩�屾煡鐪嬪叏閮ㄩ檮浠躲�嶅崱鐗�',
+      formItemClass: 'col-span-2',
+    },
+    {
+      // 瑙嗛閫氱煡锛氫笂浼� 1 涓棰戯紝鎺ㄩ�佹椂鍙戦�佽棰戞秷鎭�
+      fieldName: 'videoBlobIds',
+      label: '鎺ㄩ�佽棰�',
+      component: 'FileUpload',
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 1,
+        multiple: false,
+        accept: ['mp4', 'mov', 'm4v'],
+        showDescription: !readonly,
+        disabled: readonly,
+      },
+      dependencies: {
+        triggerFields: ['noticeType'],
+        show: (values) => values.noticeType === NOTICE_TYPE.VIDEO,
+      },
+      help: '鎺ㄩ�佹椂鍙戦�佽棰戞秷鎭紝寤鸿浣跨敤 mp4锛屽崟涓笉瓒呰繃 10MB锛涙帹閫佸悗闄勩�屾煡鐪嬮�氱煡璇︽儏銆嶅崱鐗囷紝鎴愬憳鍙叏灞忔挱鏀�',
       formItemClass: 'col-span-2',
     },
     {
diff --git a/src/views/wxcp/notice/index.vue b/src/views/wxcp/notice/index.vue
index 541b046..1245199 100644
--- a/src/views/wxcp/notice/index.vue
+++ b/src/views/wxcp/notice/index.vue
@@ -16,6 +16,7 @@
 
 import { NOTICE_STATUS, NOTICE_STATUS_OPTIONS, toLabelMap } from '../data';
 import {
+  NOTICE_TYPE,
   useNoticeGridColumns,
   useNoticeSearchFormSchema,
 } from './data';
@@ -67,6 +68,18 @@
   await revokeNotice(row.id!);
   message.success('宸叉挙鍥�');
   handleRefresh();
+}
+
+/** 澶嶅埗鎴愬憳棰勮閾炬帴锛屽彲鐩存帴鍙戝埌浼佷笟寰俊缇ら噷 */
+async function handleCopyPreview(row: WxCpNoticeApi.Notice) {
+  if (!row.previewUrl) {
+    message.warning(
+      '鏈厤缃缃戝湴鍧�鍓嶇紑锛坵x.cp.signup-url-prefix锛夛紝鏃犳硶鐢熸垚棰勮閾炬帴',
+    );
+    return;
+  }
+  await navigator.clipboard.writeText(row.previewUrl);
+  message.success('棰勮閾炬帴宸插鍒�');
 }
 
 const [Grid, gridApi] = useVbenVxeGrid({
@@ -176,6 +189,16 @@
               onClick: () => pushModalApi.setData({ notice: row }).open(),
             },
             {
+              label: '棰勮閾炬帴',
+              type: 'link',
+              ifShow:
+                row.status === NOTICE_STATUS.PUBLISHED &&
+                (row.noticeType === NOTICE_TYPE.IMAGE ||
+                  row.noticeType === NOTICE_TYPE.FILE ||
+                  row.noticeType === NOTICE_TYPE.VIDEO),
+              onClick: handleCopyPreview.bind(null, row),
+            },
+            {
               label: '鎾ゅ洖',
               type: 'link',
               auth: ['wxcp:notice:update'],
diff --git a/src/views/wxcp/notice/modules/article-editor.vue b/src/views/wxcp/notice/modules/article-editor.vue
new file mode 100644
index 0000000..a1253c8
--- /dev/null
+++ b/src/views/wxcp/notice/modules/article-editor.vue
@@ -0,0 +1,157 @@
+<script lang="ts" setup>
+import type { WxCpNoticeApi } from '#/api/wxcp/notice';
+
+import { Button, Input } from 'ant-design-vue';
+
+import { ImageUpload } from '#/components/upload';
+import { toAccessibleFileUrl } from '#/utils';
+
+defineOptions({ name: 'WxCpNoticeArticleEditor' });
+
+interface Props {
+  /** 澶氬浘鏂囨潯鐩� */
+  modelValue?: WxCpNoticeApi.NoticeArticle[];
+  disabled?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  modelValue: () => [],
+  disabled: false,
+});
+
+const emit = defineEmits<{
+  'update:modelValue': [value: WxCpNoticeApi.NoticeArticle[]];
+}>();
+
+function update(index: number, patch: Partial<WxCpNoticeApi.NoticeArticle>) {
+  const list = [...props.modelValue];
+  list[index] = { ...list[index], ...patch };
+  emit('update:modelValue', list);
+}
+
+function handleAdd() {
+  emit('update:modelValue', [
+    ...props.modelValue,
+    {
+      title: '',
+      description: '',
+      linkUrl: '',
+      sortNo: props.modelValue.length + 1,
+    },
+  ]);
+}
+
+function handleRemove(index: number) {
+  emit(
+    'update:modelValue',
+    props.modelValue.filter((_, i) => i !== index),
+  );
+}
+
+/** 灏侀潰鍥惧洖鏄炬垚涓婁紶缁勪欢璇嗗埆鐨勬枃浠跺璞� */
+function toCoverValue(item: WxCpNoticeApi.NoticeArticle): unknown[] {
+  const cover = item.coverAttachment;
+  if (!cover || !item.coverBlobId) {
+    return [];
+  }
+  const url = toAccessibleFileUrl(cover.url || cover.previewURL);
+  return [
+    {
+      uid: String(item.coverBlobId),
+      name: cover.name || cover.originalFilename || '',
+      url,
+      status: 'done',
+      id: item.coverBlobId,
+      response: { id: item.coverBlobId },
+    },
+  ];
+}
+
+/** 涓婁紶缁勪欢杩斿洖鍊肩粺涓�鍙栧嚭 blobId */
+function handleCoverChange(index: number, value: unknown) {
+  const list = Array.isArray(value) ? value : value ? [value] : [];
+  const first = list[0];
+  const blobId =
+    first && typeof first === 'object' && 'id' in first
+      ? Number((first as { id: unknown }).id)
+      : Number(first);
+  update(index, {
+    coverBlobId: Number.isFinite(blobId) && blobId > 0 ? blobId : undefined,
+  });
+}
+</script>
+
+<template>
+  <div class="mb-4">
+    <div class="mb-2 flex items-center justify-between">
+      <span class="text-sm font-medium">鍥炬枃鍐呭锛堟渶澶� 8 绡囷級</span>
+      <Button
+        size="small"
+        :disabled="disabled || modelValue.length >= 8"
+        @click="handleAdd"
+      >
+        娣诲姞鍥炬枃
+      </Button>
+    </div>
+
+    <div v-if="!modelValue.length" class="text-xs text-gray-400">
+      杩樻病鏈夊浘鏂囥�備竴鏉″鍥炬枃娑堟伅鏈�澶氬彲甯� 8 绡囷紝姣忕瘒闇�瑕佹爣棰樺拰璺宠浆閾炬帴銆�
+    </div>
+
+    <div
+      v-for="(item, index) in modelValue"
+      :key="index"
+      class="mb-3 rounded border border-gray-200 p-3 dark:border-gray-700"
+    >
+      <div class="mb-2 flex items-center justify-between">
+        <span class="text-xs text-gray-500">绗� {{ index + 1 }} 绡�</span>
+        <Button
+          danger
+          type="link"
+          size="small"
+          :disabled="disabled"
+          @click="handleRemove(index)"
+        >
+          鍒犻櫎
+        </Button>
+      </div>
+
+      <div class="grid grid-cols-1 gap-2">
+        <Input
+          :value="item.title"
+          placeholder="鍥炬枃鏍囬锛堝繀濉級"
+          :disabled="disabled"
+          @update:value="(value: string) => update(index, { title: value })"
+        />
+        <Input
+          :value="item.linkUrl"
+          placeholder="璺宠浆閾炬帴锛堝繀濉級锛屽 https://..."
+          :disabled="disabled"
+          @update:value="(value: string) => update(index, { linkUrl: value })"
+        />
+        <Input
+          :value="item.description"
+          placeholder="鍥炬枃鎽樿锛堥�夊~锛�"
+          :disabled="disabled"
+          @update:value="
+            (value: string) => update(index, { description: value })
+          "
+        />
+        <div>
+          <div class="mb-1 text-xs text-gray-500">灏侀潰鍥撅紙閫夊~锛�</div>
+          <ImageUpload
+            :value="(toCoverValue(item) as unknown as number[])"
+            :max-number="1"
+            :show-description="false"
+            :disabled="disabled"
+            value-key="id"
+            result-field="id"
+            @update:value="
+              (value: number[]) => handleCoverChange(index, value)
+            "
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
\ No newline at end of file
diff --git a/src/views/wxcp/notice/modules/form.vue b/src/views/wxcp/notice/modules/form.vue
index cab9ba2..15b9099 100644
--- a/src/views/wxcp/notice/modules/form.vue
+++ b/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>
diff --git a/src/views/wxcp/notice/modules/push-modal.vue b/src/views/wxcp/notice/modules/push-modal.vue
index 0a82a04..969f41e 100644
--- a/src/views/wxcp/notice/modules/push-modal.vue
+++ b/src/views/wxcp/notice/modules/push-modal.vue
@@ -6,12 +6,13 @@
 import { useVbenModal } from '@vben/common-ui';
 import { Alert, Button, Input, Select, Textarea, message } from 'ant-design-vue';
 
-import { pushNotice } from '#/api/wxcp/notice';
-import FileUpload from '#/components/upload/file-upload.vue';
+import { getNotice, pushNotice } from '#/api/wxcp/notice';
+import { FileUpload, ImageUpload } from '#/components/upload';
 
 import PushScopePicker from '../../components/push-scope-picker.vue';
 import { PUSH_SCOPE } from '../../data';
 import { NOTICE_TYPE, NOTICE_TYPE_OPTIONS } from '../data';
+import ArticleEditor from './article-editor.vue';
 
 defineOptions({ name: 'WxCpNoticePushModal' });
 
@@ -29,20 +30,87 @@
 const linkUrl = ref('');
 const linkBtnText = ref('');
 const coverUrl = ref('');
-const fileName = ref('');
-const fileList = ref<number[]>([]);
+/**
+ * 涓婁紶缁勪欢鐨勫洖鏄惧��
+ *
+ * 缁勪欢 props 澹版槑鐨勭被鍨嬫槸 string | string[] | number[]锛屼絾寮�浜� valueKey/resultField
+ * 鍚庡疄闄呭洖浼犵殑鏄枃浠跺璞°�傝繖閲岀敤 local 绫诲瀷淇濆瓨瀵硅薄锛堜负浜嗗睍绀烘枃浠跺悕锛夛紝
+ * 鍙湪缁戝畾缁欑粍浠舵椂鍋氫竴娆$被鍨嬫敹绐勶紝閬垮厤杩愯閫昏緫琚� props 绫诲瀷闄愬埗銆�
+ */
+type UploadItem = Record<string, unknown>;
+const imageList = ref<UploadItem[]>([]);
+const fileList = ref<UploadItem[]>([]);
+const videoList = ref<UploadItem[]>([]);
+/** 澶氬浘鏂囨潯鐩� */
+const articles = ref<WxCpNoticeApi.NoticeArticle[]>([]);
+
+/** 缁勪欢鍊肩粺涓�鎴愭枃浠跺璞℃暟缁� */
+function toUploadItems(value: unknown): UploadItem[] {
+  const list = Array.isArray(value)
+    ? value
+    : value === '' || value === null || value === undefined
+      ? []
+      : [value];
+  return list.map((item) =>
+    item && typeof item === 'object'
+      ? (item as UploadItem)
+      : { uid: String(item), id: Number(item), name: `鏂囦欢 ${String(item)}` },
+  );
+}
+
+const imageUploadValue = computed({
+  get: () => imageList.value as unknown as number[],
+  set: (value: number[]) => {
+    imageList.value = toUploadItems(value);
+  },
+});
+const fileUploadValue = computed({
+  get: () => fileList.value as unknown as number[],
+  set: (value: number[]) => {
+    fileList.value = toUploadItems(value);
+  },
+});
+const videoUploadValue = computed({
+  get: () => videoList.value as unknown as number[],
+  set: (value: number[]) => {
+    videoList.value = toUploadItems(value);
+  },
+});
 
 const isCard = computed(() => noticeType.value === NOTICE_TYPE.CARD);
 const isNews = computed(() => noticeType.value === NOTICE_TYPE.NEWS);
 const isFile = computed(() => noticeType.value === NOTICE_TYPE.FILE);
+const isImage = computed(() => noticeType.value === NOTICE_TYPE.IMAGE);
+const isVideo = computed(() => noticeType.value === NOTICE_TYPE.VIDEO);
+const isMarkdown = computed(() => noticeType.value === NOTICE_TYPE.MARKDOWN);
+const isMultiNews = computed(
+  () => noticeType.value === NOTICE_TYPE.MULTI_NEWS,
+);
 const showLink = computed(() => isCard.value || isNews.value);
+
+// 鍚庣鏈厤缃缃戝湴鍧�鍓嶇紑鏃朵笉杩斿洖棰勮閾炬帴锛屾鏃朵笉鎻愮ず銆屽彲鏌ョ湅鍏ㄩ儴銆�
+const previewUrl = computed(() => notice.value.previewUrl || '');
 
 const pushHint = computed(() => {
   switch (noticeType.value) {
     case NOTICE_TYPE.CARD:
       return '鎴愬憳浼氭敹鍒板浘鏂囧崱鐗囷紝鐐瑰嚮鎸夐挳鍙烦杞埌濉姤鎴栬鎯呴〉闈€��';
     case NOTICE_TYPE.FILE:
-      return '鎴愬憳浼氭敹鍒颁竴鏉¤鏄庢枃鏈拰涓�涓枃浠讹紝鏂囦欢涓婇檺 20MB銆�';
+      return previewUrl.value
+        ? '鎴愬憳浼氶�愪釜鏀跺埌鏂囦欢锛屽彟鏀跺埌涓�寮犮�屾煡鐪嬪叏閮ㄩ檮浠躲�嶅崱鐗囷紝鐐瑰紑鍙瑙堟垨涓嬭浇銆�'
+        : '鎴愬憳浼氶�愪釜鏀跺埌鏂囦欢娑堟伅锛屾枃浠朵笂闄� 20MB銆�';
+    case NOTICE_TYPE.IMAGE:
+      return previewUrl.value
+        ? '鎴愬憳浼氶�愬紶鏀跺埌鍥剧墖锛屽彟鏀跺埌涓�寮犮�屾煡鐪嬪叏閮ㄥ浘鐗囥�嶅崱鐗囷紝鐐瑰紑鍙湪椤甸潰鍐呮斁澶ф煡鐪嬨��'
+        : '鎴愬憳浼氶�愬紶鏀跺埌鍥剧墖娑堟伅锛屽崟寮犱笂闄� 10MB銆�';
+    case NOTICE_TYPE.VIDEO:
+      return previewUrl.value
+        ? '鎴愬憳浼氭敹鍒拌棰戞秷鎭紝鍙︽敹鍒颁竴寮犮�屾煡鐪嬮�氱煡璇︽儏銆嶅崱鐗囷紝鐐瑰紑鍙叏灞忔挱鏀俱��'
+        : '鎴愬憳浼氭敹鍒拌棰戞秷鎭紝瑙嗛涓婇檺 10MB銆�';
+    case NOTICE_TYPE.MARKDOWN:
+      return '鎴愬憳浼氭敹鍒� Markdown 娑堟伅锛屾敮鎸佹爣棰樸�佸姞绮椼�佸垪琛ㄣ�佸紩鐢ㄧ瓑鎺掔増锛屾鏂囦笂闄� 2048 瀛楄妭銆�';
+    case NOTICE_TYPE.MULTI_NEWS:
+      return '鎴愬憳浼氭敹鍒颁竴鏉″鍥炬枃娑堟伅锛屽彲涓�娆″睍绀烘渶澶� 8 绡囧浘鏂囷紝鐐瑰嚮浠绘剰涓�绡囪烦杞搴旈摼鎺ャ��';
     case NOTICE_TYPE.NEWS:
       return '鎴愬憳浼氭敹鍒板浘鏂囨秷鎭紝灞曠ず鏍囬銆佹憳瑕併�佸皝闈㈠苟鍙偣鍑昏烦杞��';
     default:
@@ -51,7 +119,7 @@
 });
 
 const [Modal, modalApi] = useVbenModal({
-  onOpenChange(isOpen: boolean) {
+  async onOpenChange(isOpen: boolean) {
     if (!isOpen) {
       notice.value = {};
       resultMessage.value = '';
@@ -67,22 +135,74 @@
     linkUrl.value = item.linkUrl ?? '';
     linkBtnText.value = item.linkBtnText ?? '';
     coverUrl.value = item.coverUrl ?? '';
-    fileName.value = item.fileName ?? '';
-    fileList.value = item.fileBlobId ? [item.fileBlobId] : [];
+    imageList.value = toUploadFiles(item.imageAttachmentList, item.imageBlobIds);
+    fileList.value = toUploadFiles(item.fileAttachmentList, item.fileBlobIds, item.fileBlobId);
+    videoList.value = toUploadFiles(item.videoAttachmentList, item.videoBlobIds);
+    articles.value = (item.articleList ?? []).map((article) => ({ ...article }));
+
+    // 鍒楄〃琛屼笉甯﹂檮浠舵槑缁嗭紝杩涘脊绐楀悗琛ユ媺涓�娆¤鎯咃紝淇濊瘉鍥剧墖/闄勪欢鑳芥纭洖鏄�
+    if (!item.imageAttachmentList?.length && !item.fileAttachmentList?.length && item.id) {
+      try {
+        const detail = await getNotice(item.id);
+        notice.value = { ...item, ...detail };
+        noticeType.value = detail.noticeType ?? noticeType.value;
+        summary.value = detail.summary ?? summary.value;
+        linkUrl.value = detail.linkUrl ?? linkUrl.value;
+        linkBtnText.value = detail.linkBtnText ?? linkBtnText.value;
+        coverUrl.value = detail.coverUrl ?? coverUrl.value;
+        imageList.value = toUploadFiles(detail.imageAttachmentList, detail.imageBlobIds);
+        fileList.value = toUploadFiles(detail.fileAttachmentList, detail.fileBlobIds, detail.fileBlobId);
+        videoList.value = toUploadFiles(detail.videoAttachmentList, detail.videoBlobIds);
+        articles.value = (detail.articleList ?? []).map((article) => ({
+          ...article,
+        }));
+      } catch {
+        // 鎷夎鎯呭け璐ヤ笉闃诲鎺ㄩ�侊紝鎸夊垪琛ㄨ宸叉湁淇℃伅缁х画
+      }
+    }
   },
 });
 
-/** 涓婁紶缁勪欢鍙兘杩斿洖鏁扮粍鎴栧璞★紝缁熶竴鍙栧嚭 blobId */
-function resolveFileBlobId(): number | undefined {
-  const raw = fileList.value as unknown;
-  if (Array.isArray(raw)) {
-    const first = raw[0];
-    if (typeof first === 'number') {
-      return first;
-    }
-    return (first as { id?: number } | undefined)?.id;
+/** 鎶婇檮浠跺垪琛紙鎴� blobId 鍒楄〃锛夎浆鎴愪笂浼犵粍浠剁殑鍥炴樉瀵硅薄 */
+function toUploadFiles(
+  attachments: WxCpNoticeApi.NoticeAttachment[] | undefined,
+  blobIds?: number[],
+  fallbackBlobId?: number,
+): UploadItem[] {
+  if (attachments?.length) {
+    return attachments.map((item) => ({
+      uid: String(item.id),
+      name: item.name || item.originalFilename || '',
+      url: item.url || item.previewURL || '',
+      status: 'done',
+      id: item.id,
+      response: { id: item.id, name: item.name || item.originalFilename || '' },
+    }));
   }
-  return typeof raw === 'number' ? raw : undefined;
+  const ids = blobIds?.length ? blobIds : fallbackBlobId ? [fallbackBlobId] : [];
+  return ids.map((id) => ({
+    uid: String(id),
+    name: `鏂囦欢 ${id}`,
+    status: 'done',
+    id,
+    response: { id },
+  }));
+}
+
+/** 涓婁紶缁勪欢鍙兘杩斿洖鏁扮粍鎴栧璞★紝缁熶竴鍙栧嚭 blobId 鏁扮粍 */
+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));
 }
 
 async function handlePush() {
@@ -101,9 +221,41 @@
     message.warning('鍥炬枃鍗$墖/鍥炬枃娑堟伅鐨勮烦杞摼鎺ヤ笉鑳戒负绌�');
     return;
   }
-  const fileBlobId = resolveFileBlobId();
-  if (isFile.value && !fileBlobId) {
-    message.warning('鏂囦欢閫氱煡蹇呴』涓婁紶鏂囦欢');
+  const imageBlobIds = normalizeBlobIds(imageList.value);
+  if (isImage.value && !imageBlobIds.length) {
+    message.warning('鍥剧墖閫氱煡蹇呴』鑷冲皯涓婁紶 1 寮犲浘鐗�');
+    return;
+  }
+  const fileBlobIds = normalizeBlobIds(fileList.value);
+  if (isFile.value && !fileBlobIds.length) {
+    message.warning('鏂囦欢閫氱煡蹇呴』鑷冲皯涓婁紶 1 涓檮浠�');
+    return;
+  }
+  const videoBlobIds = normalizeBlobIds(videoList.value);
+  if (isVideo.value && !videoBlobIds.length) {
+    message.warning('瑙嗛閫氱煡蹇呴』涓婁紶 1 涓棰�');
+    return;
+  }
+  if (isMarkdown.value && !notice.value.content?.trim()) {
+    message.warning('Markdown 閫氱煡鐨勬鏂囦笉鑳戒负绌�');
+    return;
+  }
+  if (isMultiNews.value && !articles.value.length) {
+    message.warning('澶氬浘鏂囨秷鎭嚦灏戦渶瑕� 1 绡囧浘鏂�');
+    return;
+  }
+  if (
+    isMultiNews.value &&
+    articles.value.some((article) => !article.title?.trim())
+  ) {
+    message.warning('璇峰~鍐欐瘡绡囧浘鏂囩殑鏍囬');
+    return;
+  }
+  if (
+    isMultiNews.value &&
+    articles.value.some((article) => !article.linkUrl?.trim())
+  ) {
+    message.warning('璇峰~鍐欐瘡绡囧浘鏂囩殑璺宠浆閾炬帴');
     return;
   }
 
@@ -119,8 +271,10 @@
       linkUrl: linkUrl.value,
       linkBtnText: linkBtnText.value,
       coverUrl: coverUrl.value,
-      fileBlobId,
-      fileName: fileName.value || undefined,
+      imageBlobIds,
+      fileBlobIds,
+      videoBlobIds,
+      articleList: isMultiNews.value ? articles.value : undefined,
     });
     if (res?.success) {
       message.success('鎺ㄩ�佹垚鍔�');
@@ -187,18 +341,76 @@
         />
       </template>
 
+      <template v-if="isImage">
+        <div class="mb-2 text-sm font-medium">鎺ㄩ�佸浘鐗�</div>
+        <ImageUpload
+          v-model:value="imageUploadValue"
+          class="mb-1"
+          :max-number="9"
+          :multiple="true"
+          value-key="id"
+          result-field="id"
+        />
+        <div class="mb-3 text-xs text-gray-500">
+          鍗曞紶涓嶈秴杩� 10MB锛屾渶澶� 9 寮狅紱鎺ㄩ�佸悗鎴愬憳鍙湪棰勮椤靛唴鏀惧ぇ鏌ョ湅鍏ㄩ儴鍥剧墖
+        </div>
+      </template>
+
       <template v-if="isFile">
-        <div class="mb-2 text-sm font-medium">閫氱煡鏂囦欢</div>
+        <div class="mb-2 text-sm font-medium">鎺ㄩ�侀檮浠�</div>
         <FileUpload
-          v-model:value="fileList"
-          class="mb-3"
-          :max-number="1"
-          :multiple="false"
+          v-model:value="fileUploadValue"
+          class="mb-1"
+          :max-number="5"
+          :multiple="true"
           value-key="id"
           :show-description="true"
         />
+        <div class="mb-3 text-xs text-gray-500">
+          鍗曚釜涓嶈秴杩� 20MB锛屾渶澶� 5 涓紱鎺ㄩ�佸悗鎴愬憳鍙湪棰勮椤靛唴鏌ョ湅鍏ㄩ儴闄勪欢
+        </div>
       </template>
 
+      <template v-if="isVideo">
+        <div class="mb-2 text-sm font-medium">鎺ㄩ�佽棰�</div>
+        <FileUpload
+          v-model:value="videoUploadValue"
+          class="mb-1"
+          :max-number="1"
+          :multiple="false"
+          :accept="['mp4', 'mov', 'm4v']"
+          value-key="id"
+          :show-description="true"
+        />
+        <div class="mb-3 text-xs text-gray-500">
+          寤鸿浣跨敤 mp4锛屽崟涓笉瓒呰繃 10MB锛涙帹閫佸悗鎴愬憳鍙湪棰勮椤靛叏灞忔挱鏀�
+        </div>
+      </template>
+
+      <template v-if="isMarkdown">
+        <div class="mb-2 text-sm font-medium">Markdown 姝f枃</div>
+        <div
+          class="mb-3 max-h-40 overflow-auto whitespace-pre-wrap rounded bg-gray-50 p-2 text-xs text-gray-600 dark:bg-gray-800 dark:text-gray-300"
+        >
+          {{ notice.content || '锛堟鏂囦负绌猴紝璇峰厛鍦ㄧ紪杈戦�氱煡涓~鍐� Markdown 鍐呭锛�' }}
+        </div>
+        <div class="mb-3 text-xs text-gray-500">
+          鏀寔 # 鏍囬銆�**鍔犵矖**銆�- 鍒楄〃銆�> 寮曠敤锛涙鏂囦笂闄� 2048 瀛楄妭
+        </div>
+      </template>
+
+      <template v-if="isMultiNews">
+        <ArticleEditor v-model="articles" />
+      </template>
+
+      <div v-if="previewUrl && (isImage || isFile || isVideo)" class="mb-3">
+        <div class="mb-1 text-sm font-medium">鎴愬憳棰勮椤�</div>
+        <Input :value="previewUrl" readonly />
+        <div class="mt-1 text-xs text-gray-500">
+          鎺ㄩ�佹椂浼氳嚜鍔ㄩ檮甯︺�屾煡鐪嬪叏閮ㄥ浘鐗�/闄勪欢銆嶅崱鐗囷紝涔熷彲鐩存帴澶嶅埗姝ら摼鎺ュ彂缁欐垚鍛�
+        </div>
+      </div>
+
       <div class="mb-2 mt-4 text-sm font-medium">鎺ㄩ�佽寖鍥�</div>
       <PushScopePicker v-model:scope="scope" v-model:target="target" />
 

--
Gitblit v1.9.3