refactor(web,health): 消除硬编码路径 — 统一 resolveMediaUrl + 动态 base_url
1. 新增 resolveMediaUrl() 工具函数,统一处理 storage_path 前缀和 JWT token 2. MediaLibrary 和 MediaPicker 改用 resolveMediaUrl,消除重复逻辑 3. banner_handler 不再硬编码 localhost:3000,改为从 Host header 动态构建 base_url
This commit is contained in:
@@ -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/*
|
||||
<div style={{ width: '100%', height: 100, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{item.content_type.startsWith('image/') ? (
|
||||
<Image
|
||||
src={(() => {
|
||||
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}
|
||||
|
||||
@@ -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={
|
||||
<div onClick={() => 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) ? (
|
||||
<img src={(() => {
|
||||
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' }} />
|
||||
<img src={resolveMediaUrl(item.thumbnail_path || item.storage_path)} alt={item.alt_text || item.filename} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
) : (
|
||||
<InboxOutlined style={{ fontSize: 36, color: 'var(--ant-color-text-quaternary)' }} />
|
||||
)}
|
||||
|
||||
14
apps/web/src/utils/media.ts
Normal file
14
apps/web/src/utils/media.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user