mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 15:05:27 +00:00
fix: normalize asset urls for subpath deployment
This commit is contained in:
@@ -64,6 +64,7 @@ import {
|
|||||||
type FlowNodeRegistration,
|
type FlowNodeRegistration,
|
||||||
type FlowPlugin
|
type FlowPlugin
|
||||||
} from './flowRuntime'
|
} from './flowRuntime'
|
||||||
|
import { rewriteAssetUrlsDeep, setAssetBaseUrl } from '@/utils/assetUrl'
|
||||||
|
|
||||||
// 类型定义
|
// 类型定义
|
||||||
export interface GraphData {
|
export interface GraphData {
|
||||||
@@ -121,7 +122,7 @@ const sanitizeGraphData = (input?: GraphData | null): GraphData => {
|
|||||||
const nextNode: NodeData = { ...node }
|
const nextNode: NodeData = { ...node }
|
||||||
const nextProperties = sanitizeLabelProperty(nextNode.properties)
|
const nextProperties = sanitizeLabelProperty(nextNode.properties)
|
||||||
if (nextProperties) {
|
if (nextProperties) {
|
||||||
nextNode.properties = nextProperties
|
nextNode.properties = rewriteAssetUrlsDeep(nextProperties)
|
||||||
}
|
}
|
||||||
return nextNode
|
return nextNode
|
||||||
})
|
})
|
||||||
@@ -132,7 +133,7 @@ const sanitizeGraphData = (input?: GraphData | null): GraphData => {
|
|||||||
const nextEdge: EdgeData = { ...edge }
|
const nextEdge: EdgeData = { ...edge }
|
||||||
const nextProperties = sanitizeLabelProperty(nextEdge.properties)
|
const nextProperties = sanitizeLabelProperty(nextEdge.properties)
|
||||||
if (nextProperties) {
|
if (nextProperties) {
|
||||||
nextEdge.properties = nextProperties
|
nextEdge.properties = rewriteAssetUrlsDeep(nextProperties)
|
||||||
}
|
}
|
||||||
return nextEdge
|
return nextEdge
|
||||||
})
|
})
|
||||||
@@ -161,6 +162,7 @@ const props = withDefaults(defineProps<{
|
|||||||
config?: EditorConfig
|
config?: EditorConfig
|
||||||
plugins?: FlowPlugin[]
|
plugins?: FlowPlugin[]
|
||||||
nodeRegistrations?: FlowNodeRegistration[]
|
nodeRegistrations?: FlowNodeRegistration[]
|
||||||
|
assetBaseUrl?: string
|
||||||
}>(), {
|
}>(), {
|
||||||
mode: 'edit',
|
mode: 'edit',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -384,6 +386,14 @@ watch(() => props.data, (newData) => {
|
|||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.assetBaseUrl,
|
||||||
|
(value) => {
|
||||||
|
setAssetBaseUrl(value)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
// 监听模式变化
|
// 监听模式变化
|
||||||
watch(() => props.mode, (newMode) => {
|
watch(() => props.mode, (newMode) => {
|
||||||
if (newMode === 'preview') {
|
if (newMode === 'preview') {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
<el-dialog v-if="!props.isEmbed" v-model="state.showFeedbackFormDialog" title="更新日志" width="60%">
|
<el-dialog v-if="!props.isEmbed" v-model="state.showFeedbackFormDialog" title="更新日志" width="60%">
|
||||||
<span style="font-size: 24px;">备注阴阳师</span>
|
<span style="font-size: 24px;">备注阴阳师</span>
|
||||||
<br/>
|
<br/>
|
||||||
<img src="/assets/Other/Contact.png"
|
<img :src="contactImageUrl"
|
||||||
style="cursor: pointer; vertical-align: bottom; width: 200px; height: auto;"/>
|
style="cursor: pointer; vertical-align: bottom; width: 200px; height: auto;"/>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -124,6 +124,7 @@ import { getLogicFlowInstance } from "@/ts/useLogicFlow";
|
|||||||
import { useCanvasSettings } from '@/ts/useCanvasSettings';
|
import { useCanvasSettings } from '@/ts/useCanvasSettings';
|
||||||
import { useSafeI18n } from '@/ts/useSafeI18n';
|
import { useSafeI18n } from '@/ts/useSafeI18n';
|
||||||
import type { Pinia } from 'pinia';
|
import type { Pinia } from 'pinia';
|
||||||
|
import { resolveAssetUrl } from '@/utils/assetUrl';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
isEmbed?: boolean;
|
isEmbed?: boolean;
|
||||||
@@ -133,6 +134,7 @@ const props = withDefaults(defineProps<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
const filesStore = props.piniaInstance ? useFilesStore(props.piniaInstance) : useFilesStore();
|
const filesStore = props.piniaInstance ? useFilesStore(props.piniaInstance) : useFilesStore();
|
||||||
|
const contactImageUrl = resolveAssetUrl('/assets/Other/Contact.png') as string;
|
||||||
const { showMessage } = useGlobalMessage();
|
const { showMessage } = useGlobalMessage();
|
||||||
const { selectionEnabled, snapGridEnabled, snaplineEnabled } = useCanvasSettings();
|
const { selectionEnabled, snapGridEnabled, snaplineEnabled } = useCanvasSettings();
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="selector-image-frame"
|
class="selector-image-frame"
|
||||||
:style="`width: ${imageSize - 1}px; height: ${imageSize - 1}px; background-image: url('${item[config.itemRender.imageField]}');`"
|
:style="`width: ${imageSize - 1}px; height: ${imageSize - 1}px; background-image: url('${getItemImageUrl(item)}');`"
|
||||||
/>
|
/>
|
||||||
</el-button>
|
</el-button>
|
||||||
<span style="text-align: center; display: block;">
|
<span style="text-align: center; display: block;">
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { SelectorConfig, GroupConfig } from '@/types/selector'
|
import type { SelectorConfig, GroupConfig } from '@/types/selector'
|
||||||
|
import { resolveAssetUrl } from '@/utils/assetUrl'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
config: SelectorConfig
|
config: SelectorConfig
|
||||||
@@ -75,6 +76,7 @@ const show = computed({
|
|||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const activeTab = ref('ALL')
|
const activeTab = ref('ALL')
|
||||||
const imageSize = computed(() => props.config.itemRender.imageSize || 100)
|
const imageSize = computed(() => props.config.itemRender.imageSize || 100)
|
||||||
|
const imageField = computed(() => props.config.itemRender.imageField)
|
||||||
|
|
||||||
// 过滤逻辑
|
// 过滤逻辑
|
||||||
const filteredItems = (group: GroupConfig) => {
|
const filteredItems = (group: GroupConfig) => {
|
||||||
@@ -107,10 +109,17 @@ const filteredItems = (group: GroupConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = (item: any) => {
|
const handleSelect = (item: any) => {
|
||||||
emit('select', item)
|
const field = imageField.value
|
||||||
|
const normalizedItem = {
|
||||||
|
...item,
|
||||||
|
[field]: resolveAssetUrl(item?.[field])
|
||||||
|
}
|
||||||
|
emit('select', normalizedItem)
|
||||||
searchText.value = ''
|
searchText.value = ''
|
||||||
activeTab.value = 'ALL'
|
activeTab.value = 'ALL'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getItemImageUrl = (item: any) => resolveAssetUrl(item?.[imageField.value]) as string
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed, ref, inject, onMounted, onBeforeUnmount } from 'vue';
|
import { computed, ref, inject, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import { toTextStyle } from '@/ts/nodeStyle';
|
import { toTextStyle } from '@/ts/nodeStyle';
|
||||||
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
||||||
|
import { resolveAssetUrl } from '@/utils/assetUrl';
|
||||||
|
|
||||||
const currentAsset = ref({ name: '未选择资产', avatar: '', library: 'shikigami' });
|
const currentAsset = ref({ name: '未选择资产', avatar: '', library: 'shikigami' });
|
||||||
const getNode = inject('getNode') as (() => any) | undefined;
|
const getNode = inject('getNode') as (() => any) | undefined;
|
||||||
@@ -47,6 +48,7 @@ const { containerStyle, textStyle } = useNodeAppearance({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mergedContainerStyle = computed(() => ({ ...containerStyle.value, boxSizing: 'border-box' }));
|
const mergedContainerStyle = computed(() => ({ ...containerStyle.value, boxSizing: 'border-box' }));
|
||||||
|
const normalizedAvatar = computed(() => resolveAssetUrl(currentAsset.value.avatar) as string);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -54,7 +56,7 @@ const mergedContainerStyle = computed(() => ({ ...containerStyle.value, boxSizin
|
|||||||
<div class="zindex-badge">{{ zIndex }}</div>
|
<div class="zindex-badge">{{ zIndex }}</div>
|
||||||
<img
|
<img
|
||||||
v-if="currentAsset.avatar"
|
v-if="currentAsset.avatar"
|
||||||
:src="currentAsset.avatar"
|
:src="normalizedAvatar"
|
||||||
:alt="currentAsset.name"
|
:alt="currentAsset.name"
|
||||||
class="asset-image"
|
class="asset-image"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useDialogs } from '@/ts/useDialogs';
|
|||||||
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
|
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
|
||||||
import { SELECTOR_PRESETS } from '@/configs/selectorPresets';
|
import { SELECTOR_PRESETS } from '@/configs/selectorPresets';
|
||||||
import type { SelectorConfig } from '@/types/selector';
|
import type { SelectorConfig } from '@/types/selector';
|
||||||
|
import { resolveAssetUrl, resolveAssetUrlsInDataSource } from '@/utils/assetUrl';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
node: any;
|
node: any;
|
||||||
@@ -30,15 +31,32 @@ const handleOpenSelector = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageField = preset.itemRender.imageField;
|
||||||
|
const selectedAsset = node.properties?.selectedAsset || null;
|
||||||
|
const normalizedSelectedAsset = selectedAsset && typeof selectedAsset === 'object'
|
||||||
|
? {
|
||||||
|
...selectedAsset,
|
||||||
|
[imageField]: resolveAssetUrl(selectedAsset?.[imageField])
|
||||||
|
}
|
||||||
|
: selectedAsset;
|
||||||
|
|
||||||
const config: SelectorConfig = {
|
const config: SelectorConfig = {
|
||||||
...preset,
|
...preset,
|
||||||
currentItem: node.properties?.selectedAsset || null
|
dataSource: resolveAssetUrlsInDataSource(preset.dataSource as any[], imageField),
|
||||||
|
currentItem: normalizedSelectedAsset
|
||||||
};
|
};
|
||||||
|
|
||||||
openGenericSelector(config, (selectedItem) => {
|
openGenericSelector(config, (selectedItem) => {
|
||||||
|
const normalizedSelected = selectedItem && typeof selectedItem === 'object'
|
||||||
|
? {
|
||||||
|
...selectedItem,
|
||||||
|
[imageField]: resolveAssetUrl(selectedItem?.[imageField])
|
||||||
|
}
|
||||||
|
: selectedItem;
|
||||||
|
|
||||||
lf.setProperties(node.id, {
|
lf.setProperties(node.id, {
|
||||||
...node.properties,
|
...node.properties,
|
||||||
selectedAsset: selectedItem,
|
selectedAsset: normalizedSelected,
|
||||||
assetLibrary: library
|
assetLibrary: library
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -80,4 +98,4 @@ const handleOpenSelector = () => {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
|
import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
|
||||||
import YysEditorEmbed from './YysEditorEmbed.vue'
|
import YysEditorEmbed from './YysEditorEmbed.vue'
|
||||||
|
export { setAssetBaseUrl, getAssetBaseUrl, resolveAssetUrl } from './utils/assetUrl'
|
||||||
|
|
||||||
// 导出组件
|
// 导出组件
|
||||||
export { YysEditorEmbed }
|
export { YysEditorEmbed }
|
||||||
|
|||||||
162
src/utils/assetUrl.ts
Normal file
162
src/utils/assetUrl.ts
Normal file
@@ -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<string, any> => (
|
||||||
|
Object.prototype.toString.call(value) === '[object Object]'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const rewriteAssetUrlsDeep = <T>(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<string, any> = {}
|
||||||
|
Object.keys(input).forEach((key) => {
|
||||||
|
output[key] = rewriteAssetUrlsDeep((input as Record<string, any>)[key])
|
||||||
|
})
|
||||||
|
return output as T
|
||||||
|
}
|
||||||
|
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveAssetUrlsInDataSource = <T extends Record<string, any>>(
|
||||||
|
dataSource: T[],
|
||||||
|
imageField: string
|
||||||
|
): T[] => dataSource.map((item) => {
|
||||||
|
const imageValue = item?.[imageField]
|
||||||
|
if (typeof imageValue !== 'string') {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
[imageField]: resolveAssetUrl(imageValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
Reference in New Issue
Block a user