fix: 修复子路径部署静态资源引用

- 修复 admin 与 usercenter 登录验证码资源在 /flow 子路径下的加载路径

- 统一 logo、空状态图、兜底头像与模型服务商图标的 BASE_URL 处理

- 补齐 usercenter 公共布局与 loading 注入的子路径兼容
This commit is contained in:
2026-03-20 13:02:39 +08:00
parent 39a6daf8fe
commit 07d8193e80
15 changed files with 102 additions and 19 deletions

View File

@@ -19,6 +19,7 @@ const props = defineProps({
}, },
}); });
const emits = defineEmits(['delete']); const emits = defineEmits(['delete']);
const fallbackAvatarUrl = `${import.meta.env.BASE_URL || '/'}favicon.svg`;
const handleDelete = (item: any) => { const handleDelete = (item: any) => {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), { ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('button.confirm'), confirmButtonText: $t('button.confirm'),
@@ -40,7 +41,7 @@ const handleDelete = (item: any) => {
<div class="el-list-item-container"> <div class="el-list-item-container">
<div class="flex-center"> <div class="flex-center">
<ElAvatar :src="item.icon" v-if="item.icon" /> <ElAvatar :src="item.icon" v-if="item.icon" />
<ElAvatar v-else src="/favicon.svg" shape="circle" /> <ElAvatar v-else :src="fallbackAvatarUrl" shape="circle" />
</div> </div>
<div class="el-list-item-content"> <div class="el-list-item-content">
<div class="title">{{ item[titleKey] }}</div> <div class="title">{{ item[titleKey] }}</div>

View File

@@ -57,6 +57,7 @@ const emit = defineEmits(['getData', 'buttonClick']);
const dialogVisible = ref(false); const dialogVisible = ref(false);
const pageDataRef = ref(); const pageDataRef = ref();
const loading = ref(false); const loading = ref(false);
const fallbackAvatarUrl = `${import.meta.env.BASE_URL || '/'}favicon.svg`;
const selectedIds = ref<(number | string)[]>([]); const selectedIds = ref<(number | string)[]>([]);
// 存储上一级id与选中tool.name的关联关系 // 存储上一级id与选中tool.name的关联关系
const selectedToolMap = ref<Record<number | string, SelectedMcpTool[]>>({}); const selectedToolMap = ref<Record<number | string, SelectedMcpTool[]>>({});
@@ -231,7 +232,11 @@ const handleSearch = (query: string) => {
> >
<div> <div>
<ElAvatar :src="item.icon" v-if="item.icon" /> <ElAvatar :src="item.icon" v-if="item.icon" />
<ElAvatar v-else src="/favicon.svg" shape="circle" /> <ElAvatar
v-else
:src="fallbackAvatarUrl"
shape="circle"
/>
</div> </div>
<div class="title-right-container"> <div class="title-right-container">
<ElText truncated class="title"> <ElText truncated class="title">
@@ -326,7 +331,11 @@ const handleSearch = (query: string) => {
<div class="content-sec-left-container"> <div class="content-sec-left-container">
<div> <div>
<ElAvatar :src="item.icon" v-if="item.icon" /> <ElAvatar :src="item.icon" v-if="item.icon" />
<ElAvatar v-else src="/favicon.svg" shape="circle" /> <ElAvatar
v-else
:src="fallbackAvatarUrl"
shape="circle"
/>
</div> </div>
<div class="title-sec-right-container"> <div class="title-sec-right-container">
<ElText truncated class="title"> <ElText truncated class="title">

View File

@@ -13,6 +13,9 @@ onMounted(() => {});
const authStore = useAuthStore(); const authStore = useAuthStore();
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
const assetBase = import.meta.env.BASE_URL || '/';
const captchaAssetBase = `${assetBase}tac`;
const captchaButtonUrl = `${assetBase}tac-btn.png`;
const formSchema = computed((): EasyFlowFormSchema[] => { const formSchema = computed((): EasyFlowFormSchema[] => {
return [ return [
@@ -71,11 +74,11 @@ function onSubmit(values: any) {
const style = { const style = {
logoUrl: null, // 去除logo logoUrl: null, // 去除logo
// logoUrl: "/xx/xx/xxx.png" // 替换成自定义的logo // logoUrl: "/xx/xx/xxx.png" // 替换成自定义的logo
btnUrl: '/tac-btn.png', btnUrl: captchaButtonUrl,
}; };
window window
// @ts-ignore // @ts-ignore
.initTAC('/tac', config, style) .initTAC(captchaAssetBase, config, style)
.then((tac: any) => { .then((tac: any) => {
tac.init(); // 调用init则显示验证码 tac.init(); // 调用init则显示验证码
}) })

View File

@@ -1,5 +1,7 @@
import providerList from './providerList.json'; import providerList from './providerList.json';
const assetBase = import.meta.env.BASE_URL || '/';
export interface ProviderModelPreset { export interface ProviderModelPreset {
description: string; description: string;
label: string; label: string;
@@ -28,8 +30,25 @@ export interface ProviderPreset {
const providerOptions = providerList as ProviderPreset[]; const providerOptions = providerList as ProviderPreset[];
export const getProviderPresetByValue = (targetValue?: string) => const normalizeAssetUrl = (url?: string) => {
providerOptions.find((item) => item.value === targetValue); if (!url || !url.startsWith('/')) {
return url || '';
}
return `${assetBase}${url.slice(1)}`;
};
export const getProviderPresetByValue = (targetValue?: string) => {
const preset = providerOptions.find((item) => item.value === targetValue);
if (!preset) {
return undefined;
}
return {
...preset,
icon: normalizeAssetUrl(preset.icon),
};
};
/** /**
* 根据传入的value返回对应的icon属性 * 根据传入的value返回对应的icon属性
@@ -63,4 +82,7 @@ export const getProviderBadgeText = (
return source.replaceAll(/\s+/g, '').slice(0, 2).toUpperCase(); return source.replaceAll(/\s+/g, '').slice(0, 2).toUpperCase();
}; };
export const providerPresets = providerOptions; export const providerPresets = providerOptions.map((item) => ({
...item,
icon: normalizeAssetUrl(item.icon),
}));

View File

@@ -142,7 +142,7 @@ const defaultPreferences: Preferences = {
timezone: false, timezone: false,
}, },
auth: { auth: {
sloganImage: '/slogan.svg', sloganImage: `${assetBase}slogan.svg`,
pageTitle: '', pageTitle: '',
pageDescription: '', pageDescription: '',
welcomeBack: '', welcomeBack: '',

View File

@@ -14,6 +14,7 @@ defineProps({
}, },
}); });
const emptyImageUrl = `${import.meta.env.BASE_URL || '/'}empty.png`;
const themeMode = ref(preferences.theme.mode); const themeMode = ref(preferences.theme.mode);
watch( watch(
() => preferences.theme.mode, () => preferences.theme.mode,
@@ -26,7 +27,7 @@ watch(
<template> <template>
<div class="res-container"> <div class="res-container">
<JsonViewer v-if="value" :value="value" copyable :theme="themeMode" /> <JsonViewer v-if="value" :value="value" copyable :theme="themeMode" />
<ElEmpty image="/empty.png" v-else /> <ElEmpty :image="emptyImageUrl" v-else />
</div> </div>
</template> </template>

View File

@@ -17,6 +17,7 @@ const props = withDefaults(defineProps<PageDataProps>(), {
pageSizes: () => [10, 20, 50, 100], pageSizes: () => [10, 20, 50, 100],
extraQueryParams: () => ({}), extraQueryParams: () => ({}),
}); });
const emptyImageUrl = `${import.meta.env.BASE_URL || '/'}empty.png`;
// 响应式数据 // 响应式数据
const pageList = ref([]); const pageList = ref([]);
@@ -119,6 +120,6 @@ onMounted(() => {
/> />
</div> </div>
</template> </template>
<ElEmpty image="/empty.png" v-else /> <ElEmpty :image="emptyImageUrl" v-else />
</div> </div>
</template> </template>

View File

@@ -12,6 +12,9 @@ import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' }); defineOptions({ name: 'Login' });
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
const authStore = useAuthStore(); const authStore = useAuthStore();
const assetBase = import.meta.env.BASE_URL || '/';
const captchaAssetBase = `${assetBase}tac`;
const captchaButtonUrl = `${assetBase}tac-btn.png`;
const formSchema = computed((): EasyFlowFormSchema[] => { const formSchema = computed((): EasyFlowFormSchema[] => {
return [ return [
@@ -69,11 +72,11 @@ function onSubmit(values: any) {
const style = { const style = {
logoUrl: null, // 去除logo logoUrl: null, // 去除logo
// logoUrl: "/xx/xx/xxx.png" // 替换成自定义的logo // logoUrl: "/xx/xx/xxx.png" // 替换成自定义的logo
btnUrl: '/tac-btn.png', btnUrl: captchaButtonUrl,
}; };
window window
// @ts-ignore // @ts-ignore
.initTAC('/tac', config, style) .initTAC(captchaAssetBase, config, style)
.then((tac: any) => { .then((tac: any) => {
tac.init(); // 调用init则显示验证码 tac.init(); // 调用init则显示验证码
}) })

View File

@@ -19,6 +19,7 @@ const props = withDefaults(defineProps<ExecResultProps>(), {
showMessage: true, showMessage: true,
pollingData: {}, pollingData: {},
}); });
const emptyImageUrl = `${import.meta.env.BASE_URL || '/'}empty.png`;
const finalNode = computed(() => { const finalNode = computed(() => {
const nodes = props.nodeJson; const nodes = props.nodeJson;
@@ -93,7 +94,7 @@ function getResult(res: any) {
<ShowJson :value="result" /> <ShowJson :value="result" />
</div> </div>
<div> <div>
<ElEmpty image="/empty.png" v-if="!result" /> <ElEmpty :image="emptyImageUrl" v-if="!result" />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -16,6 +16,7 @@ const ids = reactive({
const conversationInfo = ref<any>(); const conversationInfo = ref<any>();
const messageList = ref<any[]>([]); const messageList = ref<any[]>([]);
const loading = ref(true); const loading = ref(true);
const logoUrl = `${import.meta.env.BASE_URL || '/'}logo.svg`;
onMounted(() => { onMounted(() => {
if (route.params.id) { if (route.params.id) {
@@ -80,7 +81,7 @@ function getMessageList() {
</div> </div>
<!-- Right --> <!-- Right -->
<img src="/logo.svg" class="w-40 max-sm:w-28" /> <img :src="logoUrl" class="w-40 max-sm:w-28" />
</div> </div>
</ElHeader> </ElHeader>
<ElMain class="relative max-sm:mt-2 max-sm:!p-0" v-loading="loading"> <ElMain class="relative max-sm:mt-2 max-sm:!p-0" v-loading="loading">

View File

@@ -20,6 +20,7 @@ async function viteInjectAppLoadingPlugin(
const { version } = await readPackageJSON(process.cwd()); const { version } = await readPackageJSON(process.cwd());
const envRaw = isBuild ? 'prod' : 'dev'; const envRaw = isBuild ? 'prod' : 'dev';
const cacheName = `'${env.VITE_APP_NAMESPACE}-${version}-${envRaw}-preferences-theme'`; const cacheName = `'${env.VITE_APP_NAMESPACE}-${version}-${envRaw}-preferences-theme'`;
const appBase = JSON.stringify(ensureTrailingSlash(env.VITE_BASE || '/'));
// 获取缓存的主题 // 获取缓存的主题
// 保证黑暗主题下刷新页面时loading也是黑暗主题 // 保证黑暗主题下刷新页面时loading也是黑暗主题
@@ -29,7 +30,7 @@ async function viteInjectAppLoadingPlugin(
document.documentElement.classList.toggle('dark', /dark/.test(theme)); document.documentElement.classList.toggle('dark', /dark/.test(theme));
setTimeout(() => { setTimeout(() => {
if (/dark/.test(theme)) { if (/dark/.test(theme)) {
document.querySelector('#__app-loading__ img').src = '/logoDark.svg'; document.querySelector('#__app-loading__ img').src = ${appBase} + 'logoDark.svg';
} }
}) })
</script> </script>
@@ -68,4 +69,8 @@ async function getLoadingRawByHtmlTemplate(loadingTemplate: string) {
return await fsp.readFile(appLoadingPath, 'utf8'); return await fsp.readFile(appLoadingPath, 'utf8');
} }
function ensureTrailingSlash(path: string) {
return path.endsWith('/') ? path : `${path}/`;
}
export { viteInjectAppLoadingPlugin }; export { viteInjectAppLoadingPlugin };

View File

@@ -1,5 +1,7 @@
import type { Preferences } from './types'; import type { Preferences } from './types';
const assetBase = import.meta.env.BASE_URL || '/';
const defaultPreferences: Preferences = { const defaultPreferences: Preferences = {
app: { app: {
accessMode: 'frontend', accessMode: 'frontend',
@@ -65,8 +67,9 @@ const defaultPreferences: Preferences = {
logo: { logo: {
enable: true, enable: true,
fit: 'contain', fit: 'contain',
source: '/logo.svg', source: `${assetBase}logo.svg`,
sourceDark: '/logoDark.svg', sourceDark: `${assetBase}logoDark.svg`,
sourceMini: `${assetBase}logoMini.svg`,
}, },
navigation: { navigation: {
accordion: true, accordion: true,

View File

@@ -148,6 +148,10 @@ interface LogoPreferences {
source: string; source: string;
/** 暗色主题logo地址 (可选,若不设置则使用 source) */ /** 暗色主题logo地址 (可选,若不设置则使用 source) */
sourceDark?: string; sourceDark?: string;
/** 侧边栏收起时 logo 地址 (可选,若不设置则使用 source) */
sourceMini?: string;
/** 暗色主题下侧边栏收起 logo 地址 (可选,若不设置则按 sourceMini/sourceDark/source 回退) */
sourceMiniDark?: string;
} }
interface NavigationPreferences { interface NavigationPreferences {

View File

@@ -30,6 +30,14 @@ interface Props {
* @zh_CN 暗色主题 Logo 图标 (可选,若不设置则使用 src) * @zh_CN 暗色主题 Logo 图标 (可选,若不设置则使用 src)
*/ */
srcDark?: string; srcDark?: string;
/**
* @zh_CN 侧边栏收起时 Logo 图标 (可选,若不设置则使用 src)
*/
srcMini?: string;
/**
* @zh_CN 暗色主题下侧边栏收起时 Logo 图标 (可选,若不设置则按 srcMini/srcDark/src 回退)
*/
srcMiniDark?: string;
/** /**
* @zh_CN Logo 文本 * @zh_CN Logo 文本
*/ */
@@ -50,6 +58,8 @@ const props = withDefaults(defineProps<Props>(), {
logoSize: 120, logoSize: 120,
src: '', src: '',
srcDark: '', srcDark: '',
srcMini: '',
srcMiniDark: '',
theme: 'light', theme: 'light',
fit: 'cover', fit: 'cover',
}); });
@@ -59,7 +69,10 @@ const props = withDefaults(defineProps<Props>(), {
*/ */
const logoSrc = computed(() => { const logoSrc = computed(() => {
if (props.collapsed) { if (props.collapsed) {
return '/logoMini.svg'; if (props.theme === 'dark' && props.srcMiniDark) {
return props.srcMiniDark;
}
return props.srcMini || props.src;
} }
// 如果是暗色主题且提供了 srcDark则使用暗色主题的 logo // 如果是暗色主题且提供了 srcDark则使用暗色主题的 logo
if (props.theme === 'dark' && props.srcDark) { if (props.theme === 'dark' && props.srcDark) {

View File

@@ -260,6 +260,14 @@ const headerSlots = computed(() => {
:collapsed="logoCollapsed" :collapsed="logoCollapsed"
:src="preferences.logo.source" :src="preferences.logo.source"
:src-dark="preferences.logo.sourceDark" :src-dark="preferences.logo.sourceDark"
:src-mini="preferences.logo.sourceMini ?? preferences.logo.source ?? ''"
:src-mini-dark="
preferences.logo.sourceMiniDark ??
preferences.logo.sourceMini ??
preferences.logo.sourceDark ??
preferences.logo.source ??
''
"
:text="preferences.app.name" :text="preferences.app.name"
:theme="showHeaderNav ? headerTheme : theme" :theme="showHeaderNav ? headerTheme : theme"
@click="clickLogo" @click="clickLogo"
@@ -353,6 +361,14 @@ const headerSlots = computed(() => {
:fit="preferences.logo.fit" :fit="preferences.logo.fit"
:src="preferences.logo.source" :src="preferences.logo.source"
:src-dark="preferences.logo.sourceDark" :src-dark="preferences.logo.sourceDark"
:src-mini="preferences.logo.sourceMini ?? preferences.logo.source ?? ''"
:src-mini-dark="
preferences.logo.sourceMiniDark ??
preferences.logo.sourceMini ??
preferences.logo.sourceDark ??
preferences.logo.source ??
''
"
:text="preferences.app.name" :text="preferences.app.name"
:theme="theme" :theme="theme"
> >