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)
+ }
+})
+