/** * 资源 URL 解析 * * dashboard 后台填写素材地址时支持相对路径(如 /uploads/2026/07/xxx.mp4), * www 端渲染时自动拼接资源服务地址(NUXT_PUBLIC_ASSET_BASE_URL, * 未配置时默认取 apiBaseUrl 去掉 /api 后缀的 origin)。 * 完整 URL(http(s)://、//、data: 等)保持原样。 * 仅解析 /uploads/ 开头的素材路径;其他相对路径(如 /products/luxsin-x8) * 是站内路由链接(CTA 按钮等),保持原样由 NuxtLink 消费。 */ // 匹配 *_url / *_path / url 字段(如 cover_url、file_path、gallery 的 url) const ASSET_KEY_RE = /^url$|_(url|path)$/; export function resolveAssetUrl(url?: string | null): string { if (!url) return ''; // 绝对地址(协议开头 / 协议相对 // / data: / blob:)保持原样 if (/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(url)) return url; // 仅解析 /uploads/ 下的素材相对路径;其他 / 开头路径是站内路由链接,不能拼后端基地址 if (!url.startsWith('/uploads/')) return url; const config = useRuntimeConfig(); const assetBase = (config.public.assetBaseUrl as string) || (config.public.apiBaseUrl as string).replace(/\/api\/?$/, ''); return assetBase.replace(/\/+$/, '') + url; } /** * 深度遍历响应数据,将所有 *_url / *_path 字段的相对路径解析为完整地址。 * 在 useRequest 统一调用,页面/组件无需逐个处理。 */ export function resolveAssetUrls(data: T): T { if (!data || typeof data !== 'object') return data; if (Array.isArray(data)) { return data.map(item => resolveAssetUrls(item)) as unknown as T; } const result: Record = {}; for (const [key, value] of Object.entries(data as Record)) { if (typeof value === 'string' && ASSET_KEY_RE.test(key)) { result[key] = resolveAssetUrl(value); } else if (value && typeof value === 'object') { result[key] = resolveAssetUrls(value); } else { result[key] = value; } } return result as T; }