diff --git a/AGENTS.md b/AGENTS.md index 59bd2d8..7f5d108 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -262,6 +262,7 @@ const directionLabels = getLabels(row.directionCodes, { separator: ',' }); - 如果后端当前接口暂时还返回数值型 ID,前端也必须尽可能在 `typings`、API 适配层或进入业务层前转成 `string`,不要把 ID 按 `number` 扩散到页面、store 和组件里。 - 但要注意:如果后端把超出 JS 安全整数范围的 Long 直接作为 JSON 数字返回,前端在业务层再 `String(number)` 只能得到“已经丢精度后的错误字符串”。这种情况必须明确记为接口契约风险,不要误判为“前端已安全处理”。 - 因此,新增或改造接口时,最稳妥的契约仍然是:后端长整型 ID 直接按字符串返回;前端全链路按字符串接收和传递。若后端暂未改,前端侧也不得新增 `number` 口径 ID 用法。 +- API 适配层兜底(实操约束):所有从后端接收的数值型 ID 字段(不论后端实际返回 `string`、`number` 或两者混合),都必须在 `src/service/api/*` 的 normalize 或 map 函数中显式调用 `String(rawId)` 归一一次;前端业务层(`views`、`store`、组件、`Map` 键、路由参数)只接收 `string` 形态,永远不需要自己 `String()`。这条与后端是否做了 Long → String 全局序列化无关——后端做了是双保险,没做且字段取值始终在 JS 安全整数内(例如 `infra_file_config.id` 永远是两位数)也是合理选择,前端 normalize 已经把口径收死,业务层无感。但这条不开按字段取值范围豁免的口子:前端 normalize 是无差别的,任何 ID 都要 `String()`,不要按某个字段当前取值大小决定要不要走 normalize,避免后续逐步污染仓库的 ID 纪律。 - 对仓库中的历史代码,原则是“不再新增 number 口径 ID,当前任务触达相关链路时优先顺手矫正”;不要继续复制历史写法。 - 遵循仓库现有的 Vue SFC 风格:`script setup`、类型化 store、职责单一的小型 composable/helper。 - 修改界面时优先延续 `src/layouts` 和 `src/theme` 中已有的 UI 模式,不要平行引入另一套设计体系。 diff --git a/CLAUDE.md b/CLAUDE.md index da5109c..165da22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -285,6 +285,15 @@ const directionLabels = getLabels(row.directionCodes, { separator: ',' }); - **但如果后端把超 JS 安全整数的 Long 直接作为 JSON 数字返回,前端再 `String(number)` 只能得到"已经丢精度后的错误字符串"**。这种情况必须明确记为接口契约风险,不能误判为"已安全处理"。 - 最稳妥契约:**后端 Long ID 直接按字符串返回**;前端全链路按字符串。后端未改,前端也不得新增 `number` 口径 ID。 +### API 适配层兜底(操作约束) +- 所有从后端接收的数值型 ID 字段,**必须**在 `src/service/api/*` 的 normalize/map 函数里显式 `String(rawId)` 一次——**不管后端返回 string、number、还是混合**。 +- 业务层(views / store / 组件 / `Map` key / 路由参数)**只接收 string**,从不需要自己 `String()`。 +- 与"后端是否已经全局 Long → String"**无关**: + - 后端做了 → 双保险 + - 后端没做但取值在 JS 安全整数内 → 单层防御也对(实际值不丢精度) + - 后端没做且取值超安全整数 → 不安全,必须推后端改 +- **不开"按取值范围豁免"的口子**:哪怕后端说"这个字段永远是两位数"(如 `infra_file_config.id`),前端照样 `String()`。否则后续会冒出"projectStatus 是 Long 但只有 0-99,也可以保留 number"等连锁例外,铁律字面被掏空。 + ### 历史代码原则 不再新增 `number` 口径;当前任务触达相关链路时**顺手矫正**;不要继续复制历史写法。 diff --git a/build/config/proxy.ts b/build/config/proxy.ts index 085c424..694017d 100644 --- a/build/config/proxy.ts +++ b/build/config/proxy.ts @@ -1,6 +1,7 @@ import type { ProxyOptions } from 'vite'; import { bgRed, bgYellow, green, lightBlue } from 'kolorist'; import { consola } from 'consola'; +import { WEB_SERVICE_PREFIX } from '../../src/constants/service'; import { createServiceConfig } from '../../src/utils/service'; /** @@ -24,6 +25,14 @@ export function createViteProxy(env: Env.ImportMeta, enable: boolean) { Object.assign(proxy, createProxyItem(item, isEnableProxyLog)); }); + // 富文本图片 由浏览器直接发起, + // 不经过 axios,没有 baseURL 前缀。这里加一条原样透传,避免被 Vite SPA fallback 兜底成 index.html。 + // 不带 rewrite —— 原样把 /admin-api/* 转发到后端;不影响现有 /proxy-default 链路。 + proxy[WEB_SERVICE_PREFIX] = { + target: baseURL, + changeOrigin: true + }; + return proxy; } diff --git a/package.json b/package.json index 34fd3dc..b44c7bc 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "@sa/uno-preset": "workspace:*", "@soybeanjs/eslint-config": "1.7.1", "@types/bmapgl": "0.0.7", - "@types/dompurify": "3.2.0", "@types/node": "24.3.0", "@types/nprogress": "0.2.3", "@unocss/eslint-config": "66.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d32abad..1ebce73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,9 +162,6 @@ importers: '@types/bmapgl': specifier: 0.0.7 version: 0.0.7 - '@types/dompurify': - specifier: 3.2.0 - version: 3.2.0 '@types/node': specifier: 24.3.0 version: 24.3.0 @@ -1491,10 +1488,6 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - '@types/dompurify@3.2.0': - resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} - deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. - '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -6695,10 +6688,6 @@ snapshots: '@types/d3-timer@3.0.2': {} - '@types/dompurify@3.2.0': - dependencies: - dompurify: 3.2.6 - '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 diff --git a/src/components/custom/business-rich-text-editor.vue b/src/components/custom/business-rich-text-editor.vue index 8bdf9d9..8d2e07c 100644 --- a/src/components/custom/business-rich-text-editor.vue +++ b/src/components/custom/business-rich-text-editor.vue @@ -4,7 +4,7 @@ import '@wangeditor/editor/dist/css/style.css'; import { ElImageViewer } from 'element-plus'; import { Editor, Toolbar } from '@wangeditor/editor-for-vue'; import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'; -import { deleteFile, uploadFile } from '@/service/api/file'; +import { buildFileProxyUrl, deleteFile, uploadFile } from '@/service/api/file'; defineOptions({ name: 'BusinessRichTextEditor' }); @@ -198,10 +198,12 @@ const editorConfig: Partial = { return; } - const { id, url } = result.data; + // 用永久代理路径塞 ,不要用 result.data.url(24h 签名会过期) + const { id, configId, path } = result.data; + const proxyUrl = buildFileProxyUrl(configId, path); // 记录 url -> fileId,后续 commit/rollback 才知道删哪个 - session.uploadedMap.set(url, id); - insertFn(url, file.name, url); + session.uploadedMap.set(proxyUrl, id); + insertFn(proxyUrl, file.name, proxyUrl); } } } diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts index 6689d3f..64ccf6d 100644 --- a/src/locales/langs/en-us.ts +++ b/src/locales/langs/en-us.ts @@ -216,9 +216,6 @@ const local: App.I18n.Schema = { plugin_charts_echarts: 'ECharts', plugin_charts_antv: 'AntV', plugin_charts_vchart: 'VChart', - plugin_editor: 'Editor', - plugin_editor_quill: 'Quill', - plugin_editor_markdown: 'Markdown', plugin_icon: 'Icon', plugin_map: 'Map', plugin_print: 'Print', diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts index e789e47..19c9cee 100644 --- a/src/locales/langs/zh-cn.ts +++ b/src/locales/langs/zh-cn.ts @@ -216,9 +216,6 @@ const local: App.I18n.Schema = { plugin_charts_echarts: 'ECharts', plugin_charts_antv: 'AntV', plugin_charts_vchart: 'VChart', - plugin_editor: '编辑器', - plugin_editor_quill: '富文本编辑器', - plugin_editor_markdown: 'MD 编辑器', plugin_icon: '图标', plugin_map: '地图', plugin_print: '打印', diff --git a/src/router/elegant/imports.ts b/src/router/elegant/imports.ts index e093da4..754dc61 100644 --- a/src/router/elegant/imports.ts +++ b/src/router/elegant/imports.ts @@ -44,8 +44,6 @@ export const views: Record Promise import("@/views/plugin/charts/echarts/index.vue"), plugin_charts_vchart: () => import("@/views/plugin/charts/vchart/index.vue"), plugin_copy: () => import("@/views/plugin/copy/index.vue"), - plugin_editor_markdown: () => import("@/views/plugin/editor/markdown/index.vue"), - plugin_editor_quill: () => import("@/views/plugin/editor/quill/index.vue"), plugin_excel: () => import("@/views/plugin/excel/index.vue"), plugin_gantt_dhtmlx: () => import("@/views/plugin/gantt/dhtmlx/index.vue"), plugin_gantt_vtable: () => import("@/views/plugin/gantt/vtable/index.vue"), diff --git a/src/router/elegant/routes.ts b/src/router/elegant/routes.ts index f085092..5f1a871 100644 --- a/src/router/elegant/routes.ts +++ b/src/router/elegant/routes.ts @@ -425,37 +425,6 @@ export const generatedRoutes: GeneratedRoute[] = [ icon: 'mdi:clipboard-outline' } }, - { - name: 'plugin_editor', - path: '/plugin/editor', - meta: { - title: 'plugin_editor', - i18nKey: 'route.plugin_editor', - icon: 'icon-park-outline:editor' - }, - children: [ - { - name: 'plugin_editor_markdown', - path: '/plugin/editor/markdown', - component: 'view.plugin_editor_markdown', - meta: { - title: 'plugin_editor_markdown', - i18nKey: 'route.plugin_editor_markdown', - icon: 'ri:markdown-line' - } - }, - { - name: 'plugin_editor_quill', - path: '/plugin/editor/quill', - component: 'view.plugin_editor_quill', - meta: { - title: 'plugin_editor_quill', - i18nKey: 'route.plugin_editor_quill', - icon: 'mdi:file-document-edit-outline' - } - } - ] - }, { name: 'plugin_excel', path: '/plugin/excel', diff --git a/src/router/elegant/transform.ts b/src/router/elegant/transform.ts index 18cbe25..ee360d8 100644 --- a/src/router/elegant/transform.ts +++ b/src/router/elegant/transform.ts @@ -203,9 +203,6 @@ const routeMap: RouteMap = { "plugin_charts_echarts": "/plugin/charts/echarts", "plugin_charts_vchart": "/plugin/charts/vchart", "plugin_copy": "/plugin/copy", - "plugin_editor": "/plugin/editor", - "plugin_editor_markdown": "/plugin/editor/markdown", - "plugin_editor_quill": "/plugin/editor/quill", "plugin_excel": "/plugin/excel", "plugin_gantt": "/plugin/gantt", "plugin_gantt_dhtmlx": "/plugin/gantt/dhtmlx", diff --git a/src/service/api/file.ts b/src/service/api/file.ts index f98d103..ca500ff 100644 --- a/src/service/api/file.ts +++ b/src/service/api/file.ts @@ -1,28 +1,61 @@ import { SYSTEM_SERVICE_PREFIX } from '@/constants/service'; import { request } from '../request'; +import { type ServiceRequestResult, mapServiceResult } from './shared'; const FILE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/file`; +/** + * 拼接文件永久代理路径,用于富文本 。 + * + * 后端 GET 接口匿名访问、Content-Disposition: inline,私有桶下也不会过期。 + * 调用方拿到上传响应里的 configId + path 后直接调用本函数得到可写入 HTML 的 url。 + */ +export function buildFileProxyUrl(configId: string, path: string) { + return `${FILE_PREFIX}/${configId}/get/${encodeURI(path)}`; +} + export interface UploadFileResult { /** infra_file.id 的字符串形式(避免 Long 精度丢失) */ id: string; - /** 文件访问 URL:私有桶带签名、公开桶裸 URL */ + /** 对象存储配置编号(字符串形式),与 path 一起拼接永久代理路径 */ + configId: string; + /** 文件相对路径(含日期目录、文件名),与 configId 一起拼接永久代理路径 */ + path: string; + /** + * 文件访问 URL:私有桶带签名(24h 过期)、公开桶裸 URL。 + * ⚠️ 仅供后端调试 / 历史兼容,禁止写进富文本 —— 会随签名过期导致回显失效。 + * 富文本图片请用 buildFileProxyUrl(configId, path) 的返回值。 + */ url: string; } +type UploadFileResponse = { + id: string | number; + configId: string | number; + path: string; + url: string; +}; + /** 上传文件(模式一:后端中转) */ -export function uploadFile(file: File, directory?: string) { +export async function uploadFile(file: File, directory?: string) { const formData = new FormData(); formData.append('file', file); if (directory) { formData.append('directory', directory); } - return request({ + const result = await request({ url: `${FILE_PREFIX}/upload`, method: 'post', data: formData }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + id: String(data.id), + configId: String(data.configId), + path: data.path, + url: data.url + })); } /** diff --git a/src/typings/elegant-router.d.ts b/src/typings/elegant-router.d.ts index 5626b89..6089926 100644 --- a/src/typings/elegant-router.d.ts +++ b/src/typings/elegant-router.d.ts @@ -57,9 +57,6 @@ declare module "@elegant-router/types" { "plugin_charts_echarts": "/plugin/charts/echarts"; "plugin_charts_vchart": "/plugin/charts/vchart"; "plugin_copy": "/plugin/copy"; - "plugin_editor": "/plugin/editor"; - "plugin_editor_markdown": "/plugin/editor/markdown"; - "plugin_editor_quill": "/plugin/editor/quill"; "plugin_excel": "/plugin/excel"; "plugin_gantt": "/plugin/gantt"; "plugin_gantt_dhtmlx": "/plugin/gantt/dhtmlx"; @@ -194,8 +191,6 @@ declare module "@elegant-router/types" { | "plugin_charts_echarts" | "plugin_charts_vchart" | "plugin_copy" - | "plugin_editor_markdown" - | "plugin_editor_quill" | "plugin_excel" | "plugin_gantt_dhtmlx" | "plugin_gantt_vtable" diff --git a/src/views/plugin/editor/markdown/index.vue b/src/views/plugin/editor/markdown/index.vue deleted file mode 100644 index e2ade59..0000000 --- a/src/views/plugin/editor/markdown/index.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - - - diff --git a/src/views/plugin/editor/quill/index.vue b/src/views/plugin/editor/quill/index.vue deleted file mode 100644 index 4ce81d3..0000000 --- a/src/views/plugin/editor/quill/index.vue +++ /dev/null @@ -1,19 +0,0 @@ - - -