From a6ec8129c9e443aeb19538a26707000c785ab18a Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 10 May 2026 20:00:39 +0800 Subject: [PATCH] =?UTF-8?q?refactor(web,health):=20=E6=B6=88=E9=99=A4?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81=E8=B7=AF=E5=BE=84=20=E2=80=94=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=20resolveMediaUrl=20+=20=E5=8A=A8=E6=80=81?= =?UTF-8?q?=20base=5Furl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增 resolveMediaUrl() 工具函数,统一处理 storage_path 前缀和 JWT token 2. MediaLibrary 和 MediaPicker 改用 resolveMediaUrl,消除重复逻辑 3. banner_handler 不再硬编码 localhost:3000,改为从 Host header 动态构建 base_url --- apps/web/src/components/MediaPicker/index.tsx | 11 +++-------- apps/web/src/pages/health/MediaLibrary.tsx | 7 ++----- apps/web/src/utils/media.ts | 14 ++++++++++++++ crates/erp-health/src/handler/banner_handler.rs | 12 +++++++++++- 4 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 apps/web/src/utils/media.ts diff --git a/apps/web/src/components/MediaPicker/index.tsx b/apps/web/src/components/MediaPicker/index.tsx index af0a897..ead5718 100644 --- a/apps/web/src/components/MediaPicker/index.tsx +++ b/apps/web/src/components/MediaPicker/index.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Modal, Input, Upload, Image, Empty, Spin, message } from 'antd'; import { SearchOutlined, UploadOutlined } from '@ant-design/icons'; +import { resolveMediaUrl } from '../../utils/media'; import { mediaApi, type MediaItem } from '../../api/health/media'; import { uploadFile } from '../../api/upload'; @@ -58,9 +59,7 @@ export default function MediaPicker({ open, onClose, onSelect, accept = 'image/* }; const handleSelect = (item: MediaItem) => { - const token = localStorage.getItem('access_token'); - const rawPath = (item.storage_path || '').replace(/^\.\//, '/'); - const url = token ? `${rawPath}?token=${token}` : rawPath; + const url = resolveMediaUrl(item.storage_path); onSelect(url, item); onClose(); }; @@ -137,11 +136,7 @@ export default function MediaPicker({ open, onClose, onSelect, accept = 'image/*
{item.content_type.startsWith('image/') ? ( { - const token = localStorage.getItem('access_token'); - const base = (item.thumbnail_path || item.storage_path || '').replace(/^\.\//, '/'); - return token ? `${base}?token=${token}` : base; - })()} + src={resolveMediaUrl(item.thumbnail_path || item.storage_path)} alt={item.alt_text || item.filename} style={{ maxWidth: '100%', maxHeight: 100, objectFit: 'cover' }} preview={false} diff --git a/apps/web/src/pages/health/MediaLibrary.tsx b/apps/web/src/pages/health/MediaLibrary.tsx index c5bf4a6..abcc59e 100644 --- a/apps/web/src/pages/health/MediaLibrary.tsx +++ b/apps/web/src/pages/health/MediaLibrary.tsx @@ -8,6 +8,7 @@ import { EllipsisOutlined, InboxOutlined, ReloadOutlined, } from '@ant-design/icons'; import type { UploadProps } from 'antd'; +import { resolveMediaUrl } from '../../utils/media'; import { mediaApi, mediaFolderApi, type MediaItem, type FolderItem } from '../../api/health/media'; import { AuthButton } from '../../components/AuthButton'; import { formatDateTime } from '../../utils/format'; @@ -158,11 +159,7 @@ export default function MediaLibrary() { cover={
toggleSelect(item.id)} style={{ height: 140, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--ant-color-fill-quaternary, #f5f5f5)', overflow: 'hidden', position: 'relative', cursor: 'pointer' }}> {isImage(item.content_type) ? ( - { - const base = (item.thumbnail_path || item.storage_path).replace(/^\.\//, '/'); - const token = localStorage.getItem('access_token'); - return token ? `${base}?token=${token}` : base; - })()} alt={item.alt_text || item.filename} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> + {item.alt_text ) : ( )} diff --git a/apps/web/src/utils/media.ts b/apps/web/src/utils/media.ts new file mode 100644 index 0000000..5b113dc --- /dev/null +++ b/apps/web/src/utils/media.ts @@ -0,0 +1,14 @@ +/** + * 将后端返回的 storage_path / thumbnail_path 转换为可访问的前端 URL。 + * + * 后端存储路径格式: "./uploads/{tenant_id}/{filename}" 或 "/uploads/..." + * 前端统一使用相对路径 "/uploads/...",由 Vite(dev)或 nginx(prod)代理到后端。 + * + * 如需认证,自动附加 ?token= 参数。 + */ +export function resolveMediaUrl(rawPath: string | null | undefined): string { + if (!rawPath) return ''; + const base = rawPath.replace(/^\.\//, '/'); + const token = localStorage.getItem('access_token'); + return token ? `${base}?token=${token}` : base; +} diff --git a/crates/erp-health/src/handler/banner_handler.rs b/crates/erp-health/src/handler/banner_handler.rs index dd0556f..2c08143 100644 --- a/crates/erp-health/src/handler/banner_handler.rs +++ b/crates/erp-health/src/handler/banner_handler.rs @@ -136,7 +136,17 @@ where .or(params.tenant_id) .ok_or_else(|| AppError::Validation("缺少 tenant_id".to_string()))?; - let base_url = "http://localhost:3000".to_string(); + let base_url = headers + .get("host") + .and_then(|v| v.to_str().ok()) + .map(|h| { + if h.starts_with("localhost") || h.starts_with("127.0.0.1") { + format!("http://{}", h) + } else { + format!("https://{}", h) + } + }) + .unwrap_or_else(|| "http://localhost:3000".to_string()); let result = banner_service::list_public_banners(&state, tenant_id, &base_url).await?; Ok(Json(ApiResponse::ok(result))) }