已添加2个文件
已修改29个文件
284 ■■■■■ 文件已修改
.env 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
deploy.bat 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-lock.yaml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/logos/JSSM.ico 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/channel/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/channel/material/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/channel/message/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/face/item/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/face/pack/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/face/userItem/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/friend/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/friend/request/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/group/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/group/request/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/message/group/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/message/private/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/rtc/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/im/manager/sensitiveword/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/wxcp/activity.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/@core/base/shared/src/utils/util.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/fileUrl.ts 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/_core/authentication/login.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/_core/profile/modules/user-social.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wxcp/activity/data.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wxcp/activity/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wxcp/activity/modules/form.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wxcp/activity/modules/signup-list.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env
@@ -11,7 +11,9 @@
VITE_NITRO_MOCK=false
# ç§Ÿæˆ·å¼€å…³
VITE_APP_TENANT_ENABLE=true
# å¿…须与后端 yudao.tenant.enable ä¿æŒä¸€è‡´ï¼šåŽç«¯å·²æ˜¯ false,前端开着会在登录页多出
# ã€Œè¯·é€‰æ‹©ç§Ÿæˆ·ã€ä¸‹æ‹‰ï¼Œå¹¶ä¸”每个请求都带上 tenant-id å¤´ï¼Œä¸ŽåŽç«¯å®žé™…跑的模式矛盾。
VITE_APP_TENANT_ENABLE=false
# éªŒè¯ç çš„开关
VITE_APP_CAPTCHA_ENABLE=false
.env.production
@@ -3,9 +3,9 @@
VITE_BASE=/
# è¯·æ±‚路径
VITE_BASE_URL=http://36.213.90.123:9501
VITE_BASE_URL=http://36.213.150.237:9007
# æŽ¥å£åœ°å€
VITE_GLOB_API_URL=http://36.213.90.123:9501/admin-api
VITE_GLOB_API_URL=http://36.213.150.237:9008/admin-api
# æ–‡ä»¶ä¸Šä¼ ç±»åž‹ï¼šserver - åŽç«¯ä¸Šä¼ ï¼Œ client - å‰ç«¯ç›´è¿žä¸Šä¼ ï¼Œä»…支持S3服务
VITE_UPLOAD_TYPE=server
company.config.ts
@@ -18,10 +18,10 @@
 */
export const companies: Record<string, CompanyConfig> = {
  TEST: {
    name: '测试公司',
    logo: '/logos/TEST.png',
    favicon: '/logos/TEST.ico',
    apiUrl: 'http://36.213.90.123:9501/admin-api',
    name: '江苏商贸供销社',
    logo: '/logos/JSSM.svg',
    favicon: '/logos/JSSM.ico',
    apiUrl: 'http://36.213.150.237:9007/admin-api',
  },
  JHHG: {
    name: '宁夏九泓化工',
deploy.bat
@@ -12,13 +12,13 @@
set "COMPANY=TEST"
set "REMOTE_USER=root"
set "REMOTE_HOST=36.213.90.123"
set "REMOTE_HOST=36.213.150.237"
set "REMOTE_PORT=65022"
set "PASSWORD=Xdrjkj@10086#"
set "PASSWORD=Sh@1008600"
set "ZIP_FILE=dist.zip"
set "REMOTE_DIR=/data/project/mom/vue/test/dist"
set "REMOTE_DIR=/data/vue/mom/jssm/dist"
set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
package.json
@@ -66,6 +66,7 @@
    "highlight.js": "catalog:",
    "livekit-client": "^2.20.0",
    "pinia": "catalog:",
    "playwright": "^1.63.0",
    "sortablejs": "catalog:",
    "steady-xml": "catalog:",
    "tinymce": "catalog:",
pnpm-lock.yaml
@@ -662,6 +662,9 @@
      pinia:
        specifier: 'catalog:'
        version: 3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
      playwright:
        specifier: ^1.63.0
        version: 1.63.0
      sortablejs:
        specifier: 'catalog:'
        version: 1.15.7
@@ -7994,6 +7997,16 @@
  pkg-types@2.3.1:
    resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
  playwright-core@1.63.0:
    resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==}
    engines: {node: '>=20'}
    hasBin: true
  playwright@1.63.0:
    resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==}
    engines: {node: '>=20'}
    hasBin: true
  pluralize@8.0.0:
    resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
@@ -16441,6 +16454,12 @@
      exsolve: 1.1.0
      pathe: 2.0.3
  playwright-core@1.63.0: {}
  playwright@1.63.0:
    dependencies:
      playwright-core: 1.63.0
  pluralize@8.0.0: {}
  pngjs@5.0.0: {}
public/logos/JSSM.ico
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="江苏商贸">
  <defs>
    <!-- è—é’主色:左上角略亮,右下角沉下去,形成体积感 -->
    <linearGradient id="jssmNavy" x1="0" y1="0" x2="0.85" y2="1">
      <stop offset="0%" stop-color="#1E4571"/>
      <stop offset="52%" stop-color="#123156"/>
      <stop offset="100%" stop-color="#0A1D35"/>
    </linearGradient>
    <!-- é‡‘色:上亮下深,模拟金属光泽 -->
    <linearGradient id="jssmGold" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="#EBD07E"/>
      <stop offset="45%" stop-color="#D4AF37"/>
      <stop offset="100%" stop-color="#B8901F"/>
    </linearGradient>
  </defs>
  <!-- ç›¾å½¢å¾½æ ‡ä¸»ä½“ -->
  <path d="M8 14 A6 6 0 0 1 14 8 H50 A6 6 0 0 1 56 14 V32 C56 46 45.5 55.5 32 60 C18.5 55.5 8 46 8 32 Z"
        fill="url(#jssmNavy)"/>
  <!-- å¤–圈金边:深色背景下靠它勾勒轮廓 -->
  <path d="M8 14 A6 6 0 0 1 14 8 H50 A6 6 0 0 1 56 14 V32 C56 46 45.5 55.5 32 60 C18.5 55.5 8 46 8 32 Z"
        fill="none" stroke="url(#jssmGold)" stroke-width="1.5" opacity="0.9"/>
  <!-- å†…圈金线:增加徽章的层次 -->
  <path d="M12.5 15 A3.5 3.5 0 0 1 16 11.5 H48 A3.5 3.5 0 0 1 51.5 15 V32 C51.5 44 42.5 52.5 32 56.5 C21.5 52.5 12.5 44 12.5 32 Z"
        fill="none" stroke="url(#jssmGold)" stroke-width="1" opacity="0.55"/>
  <!-- é¡¶éƒ¨å­¦æœ¯ä¹‹æ˜Ÿ -->
  <path d="M32 15.6 L33.9 19 L32 22.4 L30.1 19 Z" fill="url(#jssmGold)"/>
  <!-- å±•开的书本:左页 -->
  <path d="M31.2 29.5 C27.4 26.1 22.2 24.6 17.8 25.5 V41.4 C22.2 40.5 27.4 42 31.2 45.4 Z"
        fill="url(#jssmGold)"/>
  <!-- å±•开的书本:右页(略暗,拉开左右层次) -->
  <path d="M32.8 29.5 C36.6 26.1 41.8 24.6 46.2 25.5 V41.4 C41.8 40.5 36.6 42 32.8 45.4 Z"
        fill="url(#jssmGold)" opacity="0.78"/>
  <!-- ä¹¦è„Š -->
  <path d="M32 29.5 V45.4" stroke="#0A1D35" stroke-width="1.6" stroke-linecap="round" opacity="0.55"/>
</svg>
src/api/im/manager/channel/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/channel/material/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/channel/message/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/face/item/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/face/pack/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/face/userItem/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/friend/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/friend/request/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/group/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/group/request/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/message/group/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/message/private/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/rtc/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/im/manager/sensitiveword/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '..\..\..\..\packages\effects\request\src';
import type { PageParam, PageResult } from '../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
src/api/wxcp/activity.ts
@@ -23,7 +23,15 @@
    id?: number;
    title?: string;
    content?: string;
    coverUrl?: string;
    /**
     * å°é¢å›¾æ–‡ä»¶ç¼–号(最多 1 å¼ ï¼‰
     *
     * å°é¢ä¸è½ä¸šåŠ¡è¡¨å­—æ®µï¼Œç»Ÿä¸€èµ° system_storage_attachment ä¸­é—´è¡¨ï¼Œ
     * ä¿å­˜æ—¶æäº¤ blobIds,回显时读 attachmentList。
     */
    blobIds?: number[];
    /** å°é¢å›¾é™„件列表(后端返回,最多 1 å¼ ï¼‰ */
    attachmentList?: SignupAttachment[];
    location?: string;
    startTime?: string;
    endTime?: string;
src/packages/@core/base/shared/src/utils/util.ts
@@ -67,7 +67,11 @@
  urlStr: string = location.href,
): string {
  if (!urlStr || !key) return '';
  const url = new URL(decodeURIComponent(urlStr));
  // hash è·¯ç”±ï¼ˆç”Ÿäº§æž„建 VITE_ROUTER_HISTORY=hash)下,? åŽé¢çš„参数落在 URL çš„ fragment é‡Œï¼Œ
  // URL.searchParams è¯»ä¸åˆ°ï¼Œå‚数会被误判成"丢失"。这里先摘掉 # å†è§£æžï¼›
  // history æ¨¡å¼ä¸‹åœ°å€é‡Œæ²¡æœ‰ #,走原逻辑,行为不变。
  const normalized = urlStr.includes('#') ? urlStr.replace('#', '') : urlStr;
  const url = new URL(decodeURIComponent(normalized));
  return url.searchParams.get(key) ?? '';
}
src/utils/fileUrl.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
/**
 * ä¿®æ­£åŽç«¯è¿”回的附件访问地址
 *
 * åŽç«¯ç”Ÿæˆ url / previewURL / downloadURL æ—¶ä¼šå¸¦ä¸Šå®ƒè‡ªå·±é…ç½®çš„访问域名
 * ï¼ˆå½“前后端配的是 192.168.1.6:48080)。只要该域名与前端不同源、或者该主机
 * åœ¨å½“前网络里不可达,`<img src>` å°±ä¼šç›´æŽ¥åŠ è½½å¤±è´¥ï¼Œé¡µé¢ä¸Šè¡¨çŽ°ä¸ºã€Œå›¾ç‰‡ä¸æ˜¾ç¤ºã€ã€‚
 *
 * è¿™é‡ŒæŠŠè¿™ç±»ç»å¯¹åœ°å€æ”¹å†™æˆç›¸å¯¹ API å‰ç¼€çš„路径,让它走当前站点的同源请求:
 * å¼€å‘环境经 vite çš„ /admin-api ä»£ç†è½¬å‘到后端,生产环境前后端同源时同样成立。
 *
 * å·²ç»æ˜¯ç›¸å¯¹è·¯å¾„、同源地址,以及 data:/blob: ä¸€å¾‹åŽŸæ ·è¿”å›žã€‚
 */
export function toAccessibleFileUrl(raw?: string): string {
  if (!raw) {
    return '';
  }
  // ç›¸å¯¹åœ°å€ã€data:、blob: ç›´æŽ¥æ”¾è¡Œ
  if (!/^https?:\/\//i.test(raw)) {
    return raw;
  }
  try {
    const url = new URL(raw);
    // æœ¬æ¥å°±åŒæºï¼Œæ— éœ€å¤„理
    if (url.origin === window.location.origin) {
      return raw;
    }
    const apiPrefix = (import.meta.env.VITE_GLOB_API_URL || '/admin-api').replace(
      /\/+$/,
      '',
    );
    // å‘½ä¸­ API å‰ç¼€æ—¶ï¼Œæˆªå–前缀及其之后的路径与查询串
    const at = url.pathname.indexOf(`${apiPrefix}/`);
    if (at >= 0) {
      return `${url.pathname.slice(at)}${url.search}`;
    }
    // ä¸æ˜¯æ–‡ä»¶æŽ¥å£çš„地址,不做处理
    return raw;
  } catch {
    // URL è§£æžå¤±è´¥æ—¶ä¿æŒåŽŸå€¼ï¼Œé¿å…æŠŠå¯ç”¨åœ°å€æ”¹å
    return raw;
  }
}
src/utils/index.ts
@@ -1,5 +1,6 @@
import type { Recordable } from '@vben/types';
export * from './fileUrl';
export * from './rangePickerProps';
export * from './routerHelper';
src/views/_core/authentication/login.vue
@@ -101,7 +101,11 @@
    wxcpLoading.value = true;
    // æ³¨æ„ï¼štype éœ€è¦å…ˆ encode ä¸€æ¬¡ï¼Œé…åˆ social-login é¡µé¢çš„ getUrlValue() ä½¿ç”¨ï¼›
    // å¦åˆ™ä¼ä¸šå¾®ä¿¡å›žè·³åŽ type å‚数会被吞掉,导致无法识别社交类型
    const redirectUri = `${location.origin}/auth/social-login?${encodeURIComponent(
    // å›žè°ƒåœ°å€å¿…须带 hash å‰ç¼€ï¼šç”Ÿäº§ç”¨ hash è·¯ç”±ï¼ˆVITE_ROUTER_HISTORY=hash),
    // å°‘了 # ä¼ä¸šå¾®ä¿¡å›žè·³åŽ vue-router ä¼šè§£æžåˆ° /,落到登录页而不是 social-login é¡µé¢
    const hashPrefix =
      import.meta.env.VITE_ROUTER_HISTORY === 'hash' ? '/#' : '';
    const redirectUri = `${location.origin}${hashPrefix}/auth/social-login?${encodeURIComponent(
      `type=${WXCP_SOCIAL_TYPE}`,
    )}`;
    window.location.href = await socialAuthRedirect(
src/views/_core/profile/modules/user-social.vue
@@ -122,7 +122,9 @@
  try {
    // è®¡ç®— redirectUri
    // tricky: type éœ€è¦å…ˆ encode ä¸€æ¬¡ï¼Œå¦åˆ™é’‰é’‰å›žè°ƒä¼šä¸¢å¤±ã€‚配合 getUrlValue() ä½¿ç”¨
    const redirectUri = `${location.origin}/profile?${encodeURIComponent(`type=${type}`)}`;
    // hash è·¯ç”±ï¼ˆç”Ÿäº§æž„建)下必须带 # å‰ç¼€ï¼Œå¦åˆ™å›žè·³åŽ vue-router è§£æžåˆ° /,进不了绑定流程
    const hashPrefix = import.meta.env.VITE_ROUTER_HISTORY === 'hash' ? '/#' : '';
    const redirectUri = `${location.origin}${hashPrefix}/profile?${encodeURIComponent(`type=${type}`)}`;
    // è¿›è¡Œè·³è½¬
    window.location.href = await socialAuthRedirect(type, redirectUri);
src/views/wxcp/activity/data.ts
@@ -85,10 +85,17 @@
      help: 'å¡« 0 è¡¨ç¤ºä¸é™åé¢',
    },
    {
      fieldName: 'coverUrl',
      // å°é¢èµ°é™„件中间表:这里提交 blobIds,回显读 attachmentList(见 form.vue çš„æ˜ å°„函数)
      fieldName: 'blobIds',
      label: '封面图',
      component: 'Input',
      componentProps: { placeholder: '封面图地址(选填)', disabled: readonly },
      component: 'ImageUpload',
      componentProps: {
        valueKey: 'id',
        maxNumber: 1,
        multiple: false,
        disabled: readonly,
      },
      help: '选填,建议 16:9;保存后展示在活动报名页顶部',
      formItemClass: 'col-span-2',
    },
    {
src/views/wxcp/activity/index.vue
@@ -14,6 +14,7 @@
  getActivityPage,
  publishActivity,
} from '#/api/wxcp/activity';
import { toAccessibleFileUrl } from '#/utils';
import {
  ACTIVITY_STATUS,
@@ -61,6 +62,13 @@
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** åˆ—表缩略图取封面附件的第一张,没传封面返回空串 */
function coverUrlOf(row: WxCpActivityApi.Activity): string {
  const cover = row.attachmentList?.[0];
  // åŽç«¯è¿”回的是它自己配置域名的绝对地址,需改写成同源路径才能被 <img> åŠ è½½
  return toAccessibleFileUrl(cover?.url || cover?.previewURL);
}
function handleDetail(row: WxCpActivityApi.Activity) {
@@ -144,9 +152,18 @@
      </template>
      <template #title="{ row }">
        <Button type="link" class="p-0" @click="handleDetail(row)">
          {{ row.title }}
        </Button>
        <div class="flex items-center gap-2">
          <!-- å°é¢æ¥è‡ªé™„件中间表,未上传时不占位 -->
          <img
            v-if="coverUrlOf(row)"
            :src="coverUrlOf(row)"
            alt="封面"
            class="h-9 w-14 shrink-0 rounded object-cover"
          />
          <Button type="link" class="p-0" @click="handleDetail(row)">
            {{ row.title }}
          </Button>
        </div>
      </template>
      <template #signupCount="{ row }">
src/views/wxcp/activity/modules/form.vue
@@ -9,6 +9,7 @@
import { Button, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { toAccessibleFileUrl } from '#/utils';
import {
  createActivity,
  getActivity,
@@ -44,6 +45,47 @@
  wrapperClass: 'grid-cols-2',
});
/**
 * æŠŠæ´»åŠ¨è¯¦æƒ…æ˜ å°„æˆè¡¨å•å€¼
 *
 * å°é¢åœ¨åŽç«¯ä¸è½ä¸šåŠ¡è¡¨å­—æ®µï¼Œè€Œæ˜¯å­˜åœ¨é™„ä»¶ä¸­é—´è¡¨é‡Œï¼Œ
 * å›žæ˜¾æ—¶è¦æŠŠ attachmentList è½¬æˆä¸Šä¼ ç»„件能识别的文件对象。
 */
function activityToForm(
  activity: WxCpActivityApi.Activity,
): Record<string, unknown> {
  const form: Record<string, unknown> = { ...activity };
  form.blobIds = (activity.attachmentList ?? []).map((item) => ({
    uid: String(item.id),
    name: item.name || item.originalFilename || '',
    url: toAccessibleFileUrl(item.url || item.previewURL),
    status: 'done',
    id: item.id,
  }));
  return form;
}
/**
 * æŠŠä¸Šä¼ ç»„件的值规整成 blobId æ•°ç»„
 *
 * ImageUpload åœ¨ maxNumber=1 æ—¶è¿”回的是单个值(而不是数组),
 * åŽç«¯ç­¾åæ˜¯ List<Long>,这里统一包成数组再提交。
 */
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));
}
const [Modal, modalApi] = useVbenModal({
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
@@ -59,7 +101,7 @@
      modalApi.lock();
      try {
        const detail = await getActivity(data.id);
        await formApi.setValues(detail as unknown as Record<string, unknown>);
        await formApi.setValues(activityToForm(detail));
      } finally {
        modalApi.unlock();
      }
@@ -76,12 +118,16 @@
  }
  submitting.value = true;
  try {
    const data = (await formApi.getValues()) as WxCpActivityApi.Activity;
    const data = (await formApi.getValues()) as Record<string, unknown>;
    const payload = {
      ...data,
      blobIds: normalizeBlobIds(data.blobIds),
    } as WxCpActivityApi.Activity;
    if (id.value) {
      await updateActivity({ ...data, id: id.value });
      await updateActivity({ ...payload, id: id.value });
      message.success('修改成功');
    } else {
      await createActivity(data);
      await createActivity(payload);
      message.success('创建成功,请在列表中发布后再推送');
    }
    await modalApi.close();
src/views/wxcp/activity/modules/signup-list.vue
@@ -16,6 +16,7 @@
} from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { toAccessibleFileUrl } from '#/utils';
import {
  cancelSignup,
  checkInSignup,
@@ -59,6 +60,16 @@
function isImage(item: WxCpActivityApi.SignupAttachment) {
  return (item.contentType || '').startsWith('image/');
}
/** åŽç«¯é™„件地址带的是后端自己配置的域名,改写成同源路径后才能被 <img> åŠ è½½ */
function fileUrl(item: WxCpActivityApi.SignupAttachment) {
  return toAccessibleFileUrl(item.previewURL || item.url);
}
/** ä¸‹è½½é“¾æŽ¥åŒç†ï¼Œä¼˜å…ˆç”¨ downloadURL */
function downloadUrl(item: WxCpActivityApi.SignupAttachment) {
  return toAccessibleFileUrl(item.downloadURL || item.url);
}
function formatSize(bytes?: number) {
@@ -215,18 +226,18 @@
            </div>
            <a
              v-if="isImage(item)"
              :href="item.previewURL || item.url"
              :href="fileUrl(item)"
              target="_blank"
              rel="noopener noreferrer"
            >
              <img
                :src="item.previewURL || item.url"
                :src="fileUrl(item)"
                alt="附件预览"
                class="max-h-40 rounded object-contain"
              />
            </a>
            <a
              :href="item.downloadURL || item.url"
              :href="downloadUrl(item)"
              target="_blank"
              rel="noopener noreferrer"
              class="text-xs text-blue-500 hover:underline"