From cfccdeb246f7efecb85efac433a6cc7a050d4636 Mon Sep 17 00:00:00 2001 From: rookie4show Date: Thu, 26 Feb 2026 14:04:59 +0800 Subject: [PATCH] fix: normalize asset urls for subpath deployment --- src/YysEditorEmbed.vue | 14 +- src/components/Toolbar.vue | 4 +- .../common/GenericImageSelector.vue | 13 +- .../flow/nodes/common/AssetSelectorNode.vue | 4 +- .../flow/panels/AssetSelectorPanel.vue | 24 ++- src/index.js | 1 + src/utils/assetUrl.ts | 162 ++++++++++++++++++ 7 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 src/utils/assetUrl.ts diff --git a/src/YysEditorEmbed.vue b/src/YysEditorEmbed.vue index f5dd5d5..98497df 100644 --- a/src/YysEditorEmbed.vue +++ b/src/YysEditorEmbed.vue @@ -64,6 +64,7 @@ import { type FlowNodeRegistration, type FlowPlugin } from './flowRuntime' +import { rewriteAssetUrlsDeep, setAssetBaseUrl } from '@/utils/assetUrl' // 类型定义 export interface GraphData { @@ -121,7 +122,7 @@ const sanitizeGraphData = (input?: GraphData | null): GraphData => { const nextNode: NodeData = { ...node } const nextProperties = sanitizeLabelProperty(nextNode.properties) if (nextProperties) { - nextNode.properties = nextProperties + nextNode.properties = rewriteAssetUrlsDeep(nextProperties) } return nextNode }) @@ -132,7 +133,7 @@ const sanitizeGraphData = (input?: GraphData | null): GraphData => { const nextEdge: EdgeData = { ...edge } const nextProperties = sanitizeLabelProperty(nextEdge.properties) if (nextProperties) { - nextEdge.properties = nextProperties + nextEdge.properties = rewriteAssetUrlsDeep(nextProperties) } return nextEdge }) @@ -161,6 +162,7 @@ const props = withDefaults(defineProps<{ config?: EditorConfig plugins?: FlowPlugin[] nodeRegistrations?: FlowNodeRegistration[] + assetBaseUrl?: string }>(), { mode: 'edit', width: '100%', @@ -384,6 +386,14 @@ watch(() => props.data, (newData) => { } }, { deep: true }) +watch( + () => props.assetBaseUrl, + (value) => { + setAssetBaseUrl(value) + }, + { immediate: true } +) + // 监听模式变化 watch(() => props.mode, (newMode) => { if (newMode === 'preview') { diff --git a/src/components/Toolbar.vue b/src/components/Toolbar.vue index 740e451..0a1d3be 100644 --- a/src/components/Toolbar.vue +++ b/src/components/Toolbar.vue @@ -52,7 +52,7 @@ 备注阴阳师
-
@@ -124,6 +124,7 @@ import { getLogicFlowInstance } from "@/ts/useLogicFlow"; import { useCanvasSettings } from '@/ts/useCanvasSettings'; import { useSafeI18n } from '@/ts/useSafeI18n'; import type { Pinia } from 'pinia'; +import { resolveAssetUrl } from '@/utils/assetUrl'; const props = withDefaults(defineProps<{ isEmbed?: boolean; @@ -133,6 +134,7 @@ const props = withDefaults(defineProps<{ }); const filesStore = props.piniaInstance ? useFilesStore(props.piniaInstance) : useFilesStore(); +const contactImageUrl = resolveAssetUrl('/assets/Other/Contact.png') as string; const { showMessage } = useGlobalMessage(); const { selectionEnabled, snapGridEnabled, snaplineEnabled } = useCanvasSettings(); diff --git a/src/components/common/GenericImageSelector.vue b/src/components/common/GenericImageSelector.vue index 1a39542..0ab5c01 100644 --- a/src/components/common/GenericImageSelector.vue +++ b/src/components/common/GenericImageSelector.vue @@ -39,7 +39,7 @@ > @@ -56,6 +56,7 @@ \ No newline at end of file + diff --git a/src/index.js b/src/index.js index bdc9bf6..8c659ac 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import 'element-plus/dist/index.css' import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css' import YysEditorEmbed from './YysEditorEmbed.vue' +export { setAssetBaseUrl, getAssetBaseUrl, resolveAssetUrl } from './utils/assetUrl' // 导出组件 export { YysEditorEmbed } diff --git a/src/utils/assetUrl.ts b/src/utils/assetUrl.ts new file mode 100644 index 0000000..373c3a1 --- /dev/null +++ b/src/utils/assetUrl.ts @@ -0,0 +1,162 @@ +const ASSET_PREFIX = '/assets/' +const PROTOCOL_RE = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\// + +let explicitAssetBaseUrl: string | null = null +let inferredAssetBaseUrl: string | null = null + +const ensureTrailingSlash = (value: string): string => (value.endsWith('/') ? value : `${value}/`) + +const normalizeBaseUrl = (baseUrl: string): string => { + const trimmed = baseUrl.trim() + if (!trimmed) { + return '/' + } + + if (PROTOCOL_RE.test(trimmed)) { + return ensureTrailingSlash(trimmed) + } + + const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}` + return ensureTrailingSlash(withLeadingSlash) +} + +const inferFromNuxtRuntime = (): string | null => { + if (typeof globalThis === 'undefined') { + return null + } + + const runtimeBase = (globalThis as any)?.__NUXT__?.config?.app?.baseURL + if (typeof runtimeBase === 'string' && runtimeBase.trim()) { + return normalizeBaseUrl(runtimeBase) + } + return null +} + +const inferFromScripts = (): string | null => { + if (typeof document === 'undefined' || typeof window === 'undefined') { + return null + } + + const scripts = Array.from(document.querySelectorAll('script[src]')) + .map((script) => script.getAttribute('src') || '') + .filter(Boolean) + .reverse() + + for (const src of scripts) { + try { + const pathname = new URL(src, window.location.origin).pathname + const markers = ['/_nuxt/', '/assets/', '/dist/'] + for (const marker of markers) { + const markerIndex = pathname.indexOf(marker) + if (markerIndex >= 0) { + const prefix = pathname.slice(0, markerIndex) + return prefix ? ensureTrailingSlash(prefix) : '/' + } + } + } catch { + // 忽略非法 src + } + } + + return null +} + +const inferFromLocation = (): string => { + if (typeof window === 'undefined') { + return '/' + } + const segments = window.location.pathname.split('/').filter(Boolean) + if (segments.length === 0) { + return '/' + } + return `/${segments[0]}/` +} + +export const setAssetBaseUrl = (baseUrl?: string | null) => { + if (!baseUrl) { + explicitAssetBaseUrl = null + inferredAssetBaseUrl = null + return + } + explicitAssetBaseUrl = normalizeBaseUrl(baseUrl) +} + +export const getAssetBaseUrl = (): string => { + if (explicitAssetBaseUrl) { + return explicitAssetBaseUrl + } + + if (inferredAssetBaseUrl) { + return inferredAssetBaseUrl + } + + const nuxtBase = inferFromNuxtRuntime() + if (nuxtBase) { + inferredAssetBaseUrl = nuxtBase + return inferredAssetBaseUrl + } + + const scriptBase = inferFromScripts() + if (scriptBase) { + inferredAssetBaseUrl = normalizeBaseUrl(scriptBase) + return inferredAssetBaseUrl + } + + inferredAssetBaseUrl = normalizeBaseUrl(inferFromLocation()) + return inferredAssetBaseUrl +} + +export const resolveAssetUrl = (value: unknown): unknown => { + if (typeof value !== 'string') { + return value + } + if (!value.startsWith(ASSET_PREFIX)) { + return value + } + + const baseUrl = getAssetBaseUrl() + if (baseUrl === '/') { + return value + } + + return `${baseUrl}${value.slice(1)}` +} + +const isPlainObject = (value: unknown): value is Record => ( + Object.prototype.toString.call(value) === '[object Object]' +) + +export const rewriteAssetUrlsDeep = (input: T): T => { + if (typeof input === 'string') { + return resolveAssetUrl(input) as T + } + + if (Array.isArray(input)) { + return input.map((item) => rewriteAssetUrlsDeep(item)) as T + } + + if (isPlainObject(input)) { + const output: Record = {} + Object.keys(input).forEach((key) => { + output[key] = rewriteAssetUrlsDeep((input as Record)[key]) + }) + return output as T + } + + return input +} + +export const resolveAssetUrlsInDataSource = >( + dataSource: T[], + imageField: string +): T[] => dataSource.map((item) => { + const imageValue = item?.[imageField] + if (typeof imageValue !== 'string') { + return item + } + return { + ...item, + [imageField]: resolveAssetUrl(imageValue) + } +}) +