fix: 统一AI详情页面包屑与Tab导航表现
- 为工作流、插件、知识库、聊天助手详情路由恢复面包屑与Tab参与并保持父模块高亮 - 列表卡片跳转统一追加 navTitle 与 pageKey,详情页Tab标题与面包屑优先显示卡片名称 - 工作流设计页、知识库详情页、聊天助手设置页在深链缺少 navTitle 时自动 replace 补写
This commit is contained in:
@@ -22,8 +22,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: 'Bots',
|
title: 'Bots',
|
||||||
openInNewWindow: true,
|
openInNewWindow: true,
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
hideInBreadcrumb: true,
|
|
||||||
hideInTab: true,
|
|
||||||
activePath: '/ai/bots',
|
activePath: '/ai/bots',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: $t('documentCollection.documentManagement'),
|
title: $t('documentCollection.documentManagement'),
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
hideInTab: true,
|
|
||||||
hideInBreadcrumb: true,
|
|
||||||
fullPathKey: true,
|
fullPathKey: true,
|
||||||
activePath: '/ai/documentCollection',
|
activePath: '/ai/documentCollection',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: $t('plugin.toolsManagement'),
|
title: $t('plugin.toolsManagement'),
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
hideInTab: true,
|
|
||||||
hideInBreadcrumb: true,
|
|
||||||
fullPathKey: true,
|
fullPathKey: true,
|
||||||
|
activePath: '/ai/plugin',
|
||||||
},
|
},
|
||||||
name: 'PluginTools',
|
name: 'PluginTools',
|
||||||
path: '/ai/plugin/tools',
|
path: '/ai/plugin/tools',
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
icon: 'ant-design:apartment-outlined',
|
icon: 'ant-design:apartment-outlined',
|
||||||
title: $t('datacenterTable.title'),
|
title: $t('datacenterTable.title'),
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
hideInTab: true,
|
activePath: '/ai/workflow',
|
||||||
hideInBreadcrumb: true,
|
|
||||||
},
|
},
|
||||||
name: 'WorkflowDesign',
|
name: 'WorkflowDesign',
|
||||||
path: '/ai/workflow/design',
|
path: '/ai/workflow/design',
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ const headerButtons = [
|
|||||||
permission: '/api/v1/documentCollection/save',
|
permission: '/api/v1/documentCollection/save',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
function resolveNavTitle(row: BotInfo) {
|
||||||
|
return (row as Record<string, any>)?.title || row?.name || '';
|
||||||
|
}
|
||||||
const actions: ActionButton[] = [
|
const actions: ActionButton[] = [
|
||||||
{
|
{
|
||||||
icon: Edit,
|
icon: Edit,
|
||||||
@@ -83,7 +86,13 @@ const actions: ActionButton[] = [
|
|||||||
className: '',
|
className: '',
|
||||||
permission: '',
|
permission: '',
|
||||||
onClick(row: BotInfo) {
|
onClick(row: BotInfo) {
|
||||||
router.push({ path: `/ai/bots/setting/${row.id}` });
|
router.push({
|
||||||
|
path: '/ai/bots/setting/' + row.id,
|
||||||
|
query: {
|
||||||
|
pageKey: '/ai/bots',
|
||||||
|
navTitle: resolveNavTitle(row),
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type {BotInfo} from '@easyflow/types';
|
import type {BotInfo} from '@easyflow/types';
|
||||||
|
|
||||||
import {computed, onMounted, ref} from 'vue';
|
import {computed, onMounted, ref} from 'vue';
|
||||||
import {useRoute} from 'vue-router';
|
import {useRoute, useRouter} from 'vue-router';
|
||||||
|
|
||||||
import {tryit} from 'radash';
|
import {tryit} from 'radash';
|
||||||
|
|
||||||
@@ -14,11 +14,33 @@ import Preview from './preview.vue';
|
|||||||
import Prompt from './prompt.vue';
|
import Prompt from './prompt.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const hasSavePermission = computed(() =>
|
const hasSavePermission = computed(() =>
|
||||||
hasPermission(['/api/v1/bot/save', '/api/v1/bot/updateLlmId']),
|
hasPermission(['/api/v1/bot/save', '/api/v1/bot/updateLlmId']),
|
||||||
);
|
);
|
||||||
const bot = ref<BotInfo>();
|
const bot = ref<BotInfo>();
|
||||||
|
|
||||||
|
function syncNavTitle(title: string) {
|
||||||
|
if (!title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const query = route.query as Record<string, any>;
|
||||||
|
const navTitle = Array.isArray(query.navTitle)
|
||||||
|
? query.navTitle[0]
|
||||||
|
: query.navTitle;
|
||||||
|
if (typeof navTitle === 'string' && navTitle.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.replace({
|
||||||
|
path: route.path,
|
||||||
|
query: {
|
||||||
|
...query,
|
||||||
|
pageKey: query.pageKey || '/ai/bots',
|
||||||
|
navTitle: title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (route.params.id) {
|
if (route.params.id) {
|
||||||
fetchBotDetail(route.params.id as string);
|
fetchBotDetail(route.params.id as string);
|
||||||
@@ -30,6 +52,7 @@ const fetchBotDetail = async (id: string) => {
|
|||||||
|
|
||||||
if (res?.errorCode === 0) {
|
if (res?.errorCode === 0) {
|
||||||
bot.value = res.data;
|
bot.value = res.data;
|
||||||
|
syncNavTitle((res.data?.title || res.data?.name || '') as string);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -26,6 +26,27 @@ const activeMenu = ref<string>((route.query.activeMenu as string) || '');
|
|||||||
const knowledgeInfo = ref<any>({});
|
const knowledgeInfo = ref<any>({});
|
||||||
const selectedCategory = ref('');
|
const selectedCategory = ref('');
|
||||||
|
|
||||||
|
const syncNavTitle = (title: string) => {
|
||||||
|
if (!title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const query = route.query as Record<string, any>;
|
||||||
|
const navTitle = Array.isArray(query.navTitle)
|
||||||
|
? query.navTitle[0]
|
||||||
|
: query.navTitle;
|
||||||
|
if (typeof navTitle === 'string' && navTitle.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.replace({
|
||||||
|
path: route.path,
|
||||||
|
query: {
|
||||||
|
...query,
|
||||||
|
pageKey: query.pageKey || '/ai/documentCollection',
|
||||||
|
navTitle: title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const resolveDefaultMenu = (collectionType: string, menuKey: string) => {
|
const resolveDefaultMenu = (collectionType: string, menuKey: string) => {
|
||||||
const faqMenus = new Set(['config', 'faqList', 'knowledgeSearch']);
|
const faqMenus = new Set(['config', 'faqList', 'knowledgeSearch']);
|
||||||
const documentMenus = new Set(['config', 'documentList', 'knowledgeSearch']);
|
const documentMenus = new Set(['config', 'documentList', 'knowledgeSearch']);
|
||||||
@@ -46,6 +67,7 @@ const getKnowledge = () => {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
knowledgeInfo.value = res.data;
|
knowledgeInfo.value = res.data;
|
||||||
|
syncNavTitle(res.data?.title || '');
|
||||||
const initialMenu = resolveDefaultMenu(
|
const initialMenu = resolveDefaultMenu(
|
||||||
res.data.collectionType || 'DOCUMENT',
|
res.data.collectionType || 'DOCUMENT',
|
||||||
activeMenu.value,
|
activeMenu.value,
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ const collectionTypeLabelMap = {
|
|||||||
DOCUMENT: $t('documentCollection.collectionTypeDocument'),
|
DOCUMENT: $t('documentCollection.collectionTypeDocument'),
|
||||||
FAQ: $t('documentCollection.collectionTypeFaq'),
|
FAQ: $t('documentCollection.collectionTypeFaq'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveNavTitle(row: Record<string, any>) {
|
||||||
|
return row?.title || row?.name || '';
|
||||||
|
}
|
||||||
interface FieldDefinition {
|
interface FieldDefinition {
|
||||||
// 字段名称
|
// 字段名称
|
||||||
prop: string;
|
prop: string;
|
||||||
@@ -67,6 +71,7 @@ const actions: ActionButton[] = [
|
|||||||
query: {
|
query: {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
pageKey: '/ai/documentCollection',
|
pageKey: '/ai/documentCollection',
|
||||||
|
navTitle: resolveNavTitle(row),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -82,6 +87,7 @@ const actions: ActionButton[] = [
|
|||||||
query: {
|
query: {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
pageKey: '/ai/documentCollection',
|
pageKey: '/ai/documentCollection',
|
||||||
|
navTitle: resolveNavTitle(row),
|
||||||
activeMenu: 'knowledgeSearch',
|
activeMenu: 'knowledgeSearch',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ interface CategoryFormData {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveNavTitle(item: PluginRecord) {
|
||||||
|
return (item.title as string) || (item.name as string) || '';
|
||||||
|
}
|
||||||
|
|
||||||
// 操作按钮配置
|
// 操作按钮配置
|
||||||
const actions: ActionButton[] = [
|
const actions: ActionButton[] = [
|
||||||
{
|
{
|
||||||
@@ -70,6 +74,7 @@ const actions: ActionButton[] = [
|
|||||||
query: {
|
query: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
pageKey: '/ai/plugin',
|
pageKey: '/ai/plugin',
|
||||||
|
navTitle: resolveNavTitle(item),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -203,6 +203,27 @@ const pluginSelectRef = ref();
|
|||||||
const updatePluginNode = ref<any>(null);
|
const updatePluginNode = ref<any>(null);
|
||||||
const pageLoading = ref(false);
|
const pageLoading = ref(false);
|
||||||
const chainInfo = ref<any>(null);
|
const chainInfo = ref<any>(null);
|
||||||
|
|
||||||
|
function syncNavTitle(title: string) {
|
||||||
|
if (!title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const query = route.query as Record<string, any>;
|
||||||
|
const navTitle = Array.isArray(query.navTitle)
|
||||||
|
? query.navTitle[0]
|
||||||
|
: query.navTitle;
|
||||||
|
if (typeof navTitle === 'string' && navTitle.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.replace({
|
||||||
|
path: route.path,
|
||||||
|
query: {
|
||||||
|
...query,
|
||||||
|
pageKey: query.pageKey || '/ai/workflow',
|
||||||
|
navTitle: title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
// functions
|
// functions
|
||||||
async function loadCustomNode() {
|
async function loadCustomNode() {
|
||||||
customNode.value = await getCustomNode({
|
customNode.value = await getCustomNode({
|
||||||
@@ -264,6 +285,7 @@ async function getWorkflowInfo(workflowId: any) {
|
|||||||
tinyFlowData.value = workflowInfo.value.content
|
tinyFlowData.value = workflowInfo.value.content
|
||||||
? JSON.parse(workflowInfo.value.content)
|
? JSON.parse(workflowInfo.value.content)
|
||||||
: {};
|
: {};
|
||||||
|
syncNavTitle(workflowInfo.value?.title || '');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function getLlmList() {
|
async function getLlmList() {
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ function reset() {
|
|||||||
function showDialog(row: any, importMode = false) {
|
function showDialog(row: any, importMode = false) {
|
||||||
saveDialog.value.openDialog({ ...row }, importMode);
|
saveDialog.value.openDialog({ ...row }, importMode);
|
||||||
}
|
}
|
||||||
|
function resolveNavTitle(row: any) {
|
||||||
|
return row?.title || row?.name || '';
|
||||||
|
}
|
||||||
function remove(row: any) {
|
function remove(row: any) {
|
||||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||||
confirmButtonText: $t('message.ok'),
|
confirmButtonText: $t('message.ok'),
|
||||||
@@ -202,6 +205,8 @@ function toDesignPage(row: any) {
|
|||||||
name: 'WorkflowDesign',
|
name: 'WorkflowDesign',
|
||||||
query: {
|
query: {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
pageKey: '/ai/workflow',
|
||||||
|
navTitle: resolveNavTitle(row),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,12 +85,27 @@ export function useTabbar() {
|
|||||||
await closeTabByKey(key);
|
await closeTabByKey(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveNavTitle(navTitleQuery: unknown): string {
|
||||||
|
const rawValue = Array.isArray(navTitleQuery)
|
||||||
|
? navTitleQuery[0]
|
||||||
|
: navTitleQuery;
|
||||||
|
if (typeof rawValue !== 'string' || !rawValue.trim()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(rawValue);
|
||||||
|
} catch {
|
||||||
|
return rawValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function wrapperTabLocale(tab: RouteLocationNormalizedGeneric) {
|
function wrapperTabLocale(tab: RouteLocationNormalizedGeneric) {
|
||||||
|
const navTitle = tab?.meta?.navTitle as string | undefined;
|
||||||
return {
|
return {
|
||||||
...tab,
|
...tab,
|
||||||
meta: {
|
meta: {
|
||||||
...tab?.meta,
|
...tab?.meta,
|
||||||
title: $t(tab?.meta?.title as string),
|
title: navTitle || $t(tab?.meta?.title as string),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -106,10 +121,16 @@ export function useTabbar() {
|
|||||||
watch(
|
watch(
|
||||||
() => route.fullPath,
|
() => route.fullPath,
|
||||||
() => {
|
() => {
|
||||||
const meta = route.matched?.[route.matched.length - 1]?.meta;
|
const routeMeta = route.matched?.[route.matched.length - 1]?.meta;
|
||||||
|
const navTitle = resolveNavTitle(route.query.navTitle);
|
||||||
|
const meta = {
|
||||||
|
...(routeMeta || route.meta),
|
||||||
|
navTitle,
|
||||||
|
title: navTitle || (routeMeta?.title ?? route.meta?.title),
|
||||||
|
};
|
||||||
tabbarStore.addTab({
|
tabbarStore.addTab({
|
||||||
...route,
|
...route,
|
||||||
meta: meta || route.meta,
|
meta,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|||||||
@@ -26,8 +26,34 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
function resolveNavTitle(navTitleQuery: unknown): string {
|
||||||
|
const rawValue = Array.isArray(navTitleQuery)
|
||||||
|
? navTitleQuery[0]
|
||||||
|
: navTitleQuery;
|
||||||
|
if (typeof rawValue !== 'string' || !rawValue.trim()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(rawValue);
|
||||||
|
} catch {
|
||||||
|
return rawValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const breadcrumbs = computed((): IBreadcrumb[] => {
|
const breadcrumbs = computed((): IBreadcrumb[] => {
|
||||||
const matched = route.matched;
|
const matched = route.matched;
|
||||||
|
const navTitle = resolveNavTitle(route.query.navTitle);
|
||||||
|
|
||||||
|
if (navTitle) {
|
||||||
|
const currentRoute = matched[matched.length - 1];
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: currentRoute?.meta?.icon,
|
||||||
|
path: currentRoute?.path || route.path,
|
||||||
|
title: navTitle,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const resultBreadcrumb: IBreadcrumb[] = [];
|
const resultBreadcrumb: IBreadcrumb[] = [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user