feat: 业务链条配置管理导航与顶栏用户名统一
业务链条页增加配置管理入口与返回导航;配置管理页通过 aliasPaths 保持侧栏选中;顶栏用户名与设置个人信息页共用 getAccountDisplayName。
This commit is contained in:
1 parent
d8102e8db8
commit
318f6c0c0c
8 files changed
+544
-507
No files matched your search
@@ -6,6 +6,8 @@ import { getStoredAuth } from "@/core/auth";
|
||||
import { isWujieEnv } from "@/core/embed/wujie";
|
||||
import {
|
||||
defaultExpandedIds,
|
||||
menuItemContainsPath,
|
||||
menuItemMatchesPath,
|
||||
type MenuItem,
|
||||
} from "@/core/page-layout/sidebar-menu";
|
||||
import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu";
|
||||
@@ -53,13 +55,7 @@ export function PageSidebarDark({ collapsed, onToggleCollapsed }: PageSidebarDar
|
||||
}, 180);
|
||||
};
|
||||
|
||||
const hasActiveChild = (item: MenuItem): boolean => {
|
||||
if (item.path && (pathname === item.path || pathname.startsWith(item.path + "/"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.children?.some(hasActiveChild) ?? false;
|
||||
};
|
||||
const hasActiveChild = (item: MenuItem): boolean => menuItemContainsPath(item, pathname);
|
||||
|
||||
// lazy init:仅在首次 render 计算一次 active id;之后由下面的 useEffect 跟随 pathname 同步
|
||||
const [activeTopId, setActiveTopId] = useState<string>(
|
||||
@@ -152,9 +148,9 @@ export function PageSidebarDark({ collapsed, onToggleCollapsed }: PageSidebarDar
|
||||
const isSidebarItemActive = (item: MenuItem) =>
|
||||
inWujieEnv
|
||||
? item.id === activeTopId
|
||||
: item.path
|
||||
? pathname === item.path || pathname.startsWith(item.path + "/")
|
||||
: hasActiveChild(item);
|
||||
: item.path || item.aliasPaths?.length
|
||||
? menuItemMatchesPath(item, pathname)
|
||||
: menuItemContainsPath(item, pathname);
|
||||
const handleSidebarItemClick = (item: MenuItem) => {
|
||||
if (inWujieEnv) {
|
||||
handleTopItemClick(item);
|
||||
@@ -176,7 +172,9 @@ export function PageSidebarDark({ collapsed, onToggleCollapsed }: PageSidebarDar
|
||||
{items.map((item) => {
|
||||
const hasChildren = !!item.children?.length;
|
||||
const isExpanded = expandedIds.has(item.id);
|
||||
const isSelected = item.path ? pathname === item.path : hasActiveChild(item);
|
||||
const isSelected = item.path || item.aliasPaths?.length
|
||||
? menuItemMatchesPath(item, pathname)
|
||||
: hasActiveChild(item);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -28,6 +28,8 @@ import { useUnreadCount } from "@/core/notifications";
|
||||
import {
|
||||
defaultExpandedIds,
|
||||
getActiveThirdLevelItems,
|
||||
menuItemContainsPath,
|
||||
menuItemMatchesPath,
|
||||
type MenuItem,
|
||||
} from "@/core/page-layout/sidebar-menu";
|
||||
import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu";
|
||||
@@ -41,12 +43,8 @@ type PageSidebarProps = {
|
||||
|
||||
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about";
|
||||
|
||||
const hasActiveChild = (item: MenuItem, pathname: string): boolean => {
|
||||
if (item.path && (pathname === item.path || pathname.startsWith(item.path + "/"))) {
|
||||
return true;
|
||||
}
|
||||
return item.children?.some((child) => hasActiveChild(child, pathname)) ?? false;
|
||||
};
|
||||
const hasActiveChild = (item: MenuItem, pathname: string): boolean =>
|
||||
menuItemContainsPath(item, pathname);
|
||||
|
||||
const findFirstRoutePath = (items?: MenuItem[]): string | null => {
|
||||
if (!items?.length) {
|
||||
@@ -116,9 +114,9 @@ function SecondaryNavSidebar() {
|
||||
const secondaryItems = activeTopItem?.children ?? [];
|
||||
|
||||
const isItemActive = (item: MenuItem) =>
|
||||
item.path
|
||||
? pathname === item.path || pathname.startsWith(item.path + "/")
|
||||
: hasActiveChild(item, pathname);
|
||||
item.path || item.aliasPaths?.length
|
||||
? menuItemMatchesPath(item, pathname)
|
||||
: menuItemContainsPath(item, pathname);
|
||||
|
||||
const handleItemClick = (item: MenuItem) => {
|
||||
const path = item.path ?? findFirstRoutePath(item.children);
|
||||
@@ -211,9 +209,8 @@ function SecondaryNavSidebar() {
|
||||
<nav className="light-sidebar-subnav" aria-label="三级菜单">
|
||||
{thirdItems.map((child) => {
|
||||
const ChildIcon = child.icon;
|
||||
const childActive = child.path
|
||||
? pathname === child.path || pathname.startsWith(child.path + "/")
|
||||
: hasActiveChild(child, pathname);
|
||||
const childActive = menuItemMatchesPath(child, pathname)
|
||||
|| (child.children?.length ? menuItemContainsPath(child, pathname) : false);
|
||||
return (
|
||||
<button
|
||||
key={child.id}
|
||||
@@ -301,7 +298,7 @@ function LegacyAccordionSidebar({
|
||||
|
||||
const renderLeaf = (item: MenuItem, depth = 0) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = !!item.path && pathname === item.path;
|
||||
const isActive = menuItemMatchesPath(item, pathname);
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -6,7 +6,7 @@ import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { clearStoredAuth, getStoredAuth } from "@/core/auth";
|
||||
import { clearStoredAuth, getAccountDisplayName, getStoredAuth } from "@/core/auth";
|
||||
|
||||
import { SettingsSection } from "./settings-section";
|
||||
|
||||
@@ -37,8 +37,7 @@ export function AccountSettingsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const usernameFromEmail = (auth.email ?? "").split("@")[0] ?? "";
|
||||
const displayName = usernameFromEmail || auth.user_id;
|
||||
const displayName = getAccountDisplayName(auth);
|
||||
|
||||
const handleLogout = () => {
|
||||
clearStoredAuth();
|
||||
|
||||
@@ -1,94 +1,103 @@
|
||||
export interface LoginByUsernameResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
user_id: string;
|
||||
email: string;
|
||||
system_role: string;
|
||||
needs_setup: boolean;
|
||||
created: boolean;
|
||||
}
|
||||
|
||||
const AUTH_STORAGE_KEY = "deerflow.auth";
|
||||
|
||||
function isBrowser(): boolean {
|
||||
return typeof window !== "undefined";
|
||||
}
|
||||
|
||||
export function getStoredAuth(): LoginByUsernameResponse | null {
|
||||
if (!isBrowser()) {
|
||||
return null;
|
||||
}
|
||||
const raw = window.localStorage.getItem(AUTH_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw) as LoginByUsernameResponse;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setStoredAuth(auth: LoginByUsernameResponse) {
|
||||
if (!isBrowser()) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(auth));
|
||||
}
|
||||
|
||||
export function clearStoredAuth() {
|
||||
if (!isBrowser()) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把登录用户名写入 localStorage.userInfo.userName(顶栏用户区据此显示)。
|
||||
* 仅在尚未设置时写入,避免覆盖外部登录(yUserId)从 consumer 拿到的用户名。
|
||||
*/
|
||||
export function setStoredUserName(name: string | null | undefined) {
|
||||
if (!isBrowser()) {
|
||||
return;
|
||||
}
|
||||
const trimmed = name?.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
let info: Record<string, unknown> = {};
|
||||
try {
|
||||
const raw = window.localStorage.getItem("userInfo");
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
info = parsed as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
info = {};
|
||||
}
|
||||
if (typeof info.userName === "string" && info.userName.trim()) {
|
||||
return;
|
||||
}
|
||||
info.userName = trimmed;
|
||||
window.localStorage.setItem("userInfo", JSON.stringify(info));
|
||||
}
|
||||
|
||||
export function getAccessToken(): string | undefined {
|
||||
const auth = getStoredAuth();
|
||||
const token = auth?.access_token?.trim();
|
||||
return token ? token : undefined;
|
||||
}
|
||||
|
||||
export function getAuthorizationHeaderValue(): string | undefined {
|
||||
const auth = getStoredAuth();
|
||||
const token = auth?.access_token?.trim();
|
||||
if (!token) {
|
||||
return undefined;
|
||||
}
|
||||
const rawType = auth?.token_type?.trim();
|
||||
const tokenType = rawType ? rawType : "Bearer";
|
||||
return `${tokenType} ${token}`;
|
||||
}
|
||||
|
||||
export interface LoginByUsernameResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
user_id: string;
|
||||
email: string;
|
||||
system_role: string;
|
||||
needs_setup: boolean;
|
||||
created: boolean;
|
||||
}
|
||||
|
||||
const AUTH_STORAGE_KEY = "deerflow.auth";
|
||||
|
||||
function isBrowser(): boolean {
|
||||
return typeof window !== "undefined";
|
||||
}
|
||||
|
||||
export function getStoredAuth(): LoginByUsernameResponse | null {
|
||||
if (!isBrowser()) {
|
||||
return null;
|
||||
}
|
||||
const raw = window.localStorage.getItem(AUTH_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw) as LoginByUsernameResponse;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setStoredAuth(auth: LoginByUsernameResponse) {
|
||||
if (!isBrowser()) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(auth));
|
||||
}
|
||||
|
||||
export function clearStoredAuth() {
|
||||
if (!isBrowser()) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||
}
|
||||
|
||||
/** 与设置 → 个人信息页「用户名」一致:邮箱 @ 前缀,否则 user_id。 */
|
||||
export function getAccountDisplayName(
|
||||
auth: LoginByUsernameResponse | null = getStoredAuth(),
|
||||
): string {
|
||||
if (!auth) return "用户";
|
||||
const usernameFromEmail = (auth.email ?? "").split("@")[0] ?? "";
|
||||
return usernameFromEmail || auth.user_id || "用户";
|
||||
}
|
||||
|
||||
/**
|
||||
* 把登录用户名写入 localStorage.userInfo.userName(轻应用登录参数等场景使用)。
|
||||
* 仅在尚未设置时写入,避免覆盖外部登录(yUserId)从 consumer 拿到的用户名。
|
||||
*/
|
||||
export function setStoredUserName(name: string | null | undefined) {
|
||||
if (!isBrowser()) {
|
||||
return;
|
||||
}
|
||||
const trimmed = name?.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
let info: Record<string, unknown> = {};
|
||||
try {
|
||||
const raw = window.localStorage.getItem("userInfo");
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
info = parsed as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
info = {};
|
||||
}
|
||||
if (typeof info.userName === "string" && info.userName.trim()) {
|
||||
return;
|
||||
}
|
||||
info.userName = trimmed;
|
||||
window.localStorage.setItem("userInfo", JSON.stringify(info));
|
||||
}
|
||||
|
||||
export function getAccessToken(): string | undefined {
|
||||
const auth = getStoredAuth();
|
||||
const token = auth?.access_token?.trim();
|
||||
return token ? token : undefined;
|
||||
}
|
||||
|
||||
export function getAuthorizationHeaderValue(): string | undefined {
|
||||
const auth = getStoredAuth();
|
||||
const token = auth?.access_token?.trim();
|
||||
if (!token) {
|
||||
return undefined;
|
||||
}
|
||||
const rawType = auth?.token_type?.trim();
|
||||
const tokenType = rawType ? rawType : "Bearer";
|
||||
return `${tokenType} ${token}`;
|
||||
}
|
||||
|
||||
@@ -1,353 +1,366 @@
|
||||
/**
|
||||
* 全局侧边栏菜单数据与权限过滤逻辑。
|
||||
*
|
||||
* 浅色 ``PageSidebar`` 和深色 ``PageSidebarDark`` 共用这份数据 —— 菜单结构与
|
||||
* adminOnly 权限策略对两种皮肤完全一致;UI 渲染(icon 显示/折叠样式/hover flyout)
|
||||
* 在各自组件里实现。
|
||||
*
|
||||
* 修改菜单只需改这一处,两个 sidebar 自动同步。
|
||||
*/
|
||||
import type { ElementType } from "react";
|
||||
import {
|
||||
Activity,
|
||||
BookOpen,
|
||||
Bot,
|
||||
Brain,
|
||||
Briefcase,
|
||||
CalendarClock,
|
||||
CircleHelp,
|
||||
ClipboardList,
|
||||
Database,
|
||||
FileBox,
|
||||
FileText,
|
||||
Gauge,
|
||||
Grid3X3,
|
||||
HelpCircle,
|
||||
History,
|
||||
Layers,
|
||||
Layout,
|
||||
Library,
|
||||
List,
|
||||
ListChecks,
|
||||
ListTodo,
|
||||
Network,
|
||||
Radar,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Target,
|
||||
Trophy,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
|
||||
export type MenuItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: ElementType;
|
||||
path?: string;
|
||||
/** 非 admin 用户使用的路由(菜单对普通用户可见时生效,与简洁模式公开排行榜一致) */
|
||||
userPath?: string;
|
||||
adminOnly?: boolean;
|
||||
children?: MenuItem[];
|
||||
};
|
||||
|
||||
export const sidebarMenuItems: MenuItem[] = [
|
||||
{
|
||||
id: "qa-center",
|
||||
label: "问答管理",
|
||||
icon: HelpCircle,
|
||||
children: [
|
||||
{ id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" },
|
||||
{
|
||||
id: "multi-agent-roundtable",
|
||||
label: "多智能体会商",
|
||||
icon: Users,
|
||||
path: "/page/strategy/qa/multi-agent-roundtable",
|
||||
},
|
||||
// { id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
|
||||
{
|
||||
id: "qa-zzjc",
|
||||
label: "JC支持",
|
||||
icon: Bot,
|
||||
path: "/page/strategy/qa/zzjc",
|
||||
},
|
||||
{ id: "qa-zzxd", label: "XD应用", icon: Layout, path: "/page/strategy/qa/zzxd" },
|
||||
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "task-management",
|
||||
label: "任务管理",
|
||||
icon: Target,
|
||||
children: [
|
||||
{
|
||||
id: "xk-center-task",
|
||||
label: "XK中任务",
|
||||
icon: ClipboardList,
|
||||
children: [
|
||||
// {
|
||||
// id: "xk-task-jc",
|
||||
// label: "任务JC",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/xk-task-jc",
|
||||
// },
|
||||
// {
|
||||
// id: "xk-task-dispatch",
|
||||
// label: "任务调度",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/xk-task-dispatch",
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "coh-center-task",
|
||||
label: "COH中任务",
|
||||
icon: Activity,
|
||||
children: [
|
||||
// {
|
||||
// id: "coh-task-3qfx",
|
||||
// label: "3QFX",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/plan-task-sentiment",
|
||||
// },
|
||||
// {
|
||||
// id: "coh-task-fx",
|
||||
// label: "任务FX",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/plan-task/plan-task-fx",
|
||||
// },
|
||||
// {
|
||||
// id: "coh-task-xdfx",
|
||||
// label: "XDFX",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/plan-task/plan-task-action",
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "pg-center-task",
|
||||
label: "PG中任务",
|
||||
icon: Gauge,
|
||||
children: [
|
||||
// {
|
||||
// id: "pg-task-pg",
|
||||
// label: "任务PG",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/effect-task-pg",
|
||||
// },
|
||||
// {
|
||||
// id: "pg-task-xdpg",
|
||||
// label: "XDPG",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/effect-action-pg",
|
||||
// },
|
||||
// {
|
||||
// id: "pg-task-info-pg",
|
||||
// label: "信息PG",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/effect-info-pg",
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "ty-center-task",
|
||||
label: "TY中任务",
|
||||
icon: Radar,
|
||||
children: [
|
||||
// {
|
||||
// id: "ty-task-xdty",
|
||||
// label: "XDTY任务",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/mnty-task-mechanism",
|
||||
// },
|
||||
// {
|
||||
// id: "ty-task-plan",
|
||||
// label: "方案TY任务",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/mnty-task-plan",
|
||||
// },
|
||||
// {
|
||||
// id: "ty-task-xxty",
|
||||
// label: "XXTY任务",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/mnty-task-zsty",
|
||||
// },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "light-app-center",
|
||||
label: "应用中心",
|
||||
icon: Grid3X3,
|
||||
children: [
|
||||
{
|
||||
id: "light-app-management",
|
||||
label: "应用管理",
|
||||
icon: Layout,
|
||||
path: "/page/strategy/light-app/management",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
id: "light-app-apps",
|
||||
label: "应用列表",
|
||||
icon: List,
|
||||
path: "/page/strategy/light-app/apps",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "comprehensive-management",
|
||||
label: "综合管理",
|
||||
icon: Settings,
|
||||
children: [
|
||||
{
|
||||
id: "mgmt-user-leaderboard",
|
||||
label: "用户排行榜",
|
||||
icon: Trophy,
|
||||
path: "/page/strategy/admin/user-leaderboard",
|
||||
userPath: "/page/strategy/user-leaderboard",
|
||||
},
|
||||
{ id: "mgmt-agents", label: "智能体管理", icon: Bot, path: "/page/strategy/agents" },
|
||||
// {
|
||||
// id: "mgmt-business-chain-mapping",
|
||||
// label: "配置管理",
|
||||
// icon: Network,
|
||||
// path: "/page/strategy/business-chain-mapping",
|
||||
// },
|
||||
{
|
||||
id: "mgmt-business-chains",
|
||||
label: "业务链条配置",
|
||||
icon: Layers,
|
||||
path: "/page/strategy/business-chains",
|
||||
},
|
||||
{ id: "mgmt-skills", label: "技能管理", icon: Sparkles, path: "/page/strategy/skills" },
|
||||
{ id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" },
|
||||
{
|
||||
id: "mgmt-tasks",
|
||||
label: "定时任务",
|
||||
icon: ListChecks,
|
||||
path: "/page/strategy/scheduled/tasks",
|
||||
},
|
||||
{
|
||||
id: "mgmt-llm-metrics",
|
||||
label: "大模型调用统计",
|
||||
icon: Gauge,
|
||||
path: "/page/strategy/settings/llm-metrics",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
id: "mgmt-users",
|
||||
label: "用户管理",
|
||||
icon: Users,
|
||||
path: "/page/strategy/admin/users",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
id: "mgmt-positions",
|
||||
label: "岗位管理",
|
||||
icon: Briefcase,
|
||||
path: "/page/strategy/admin/positions",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
id: "mgmt-admin-scheduled-tasks",
|
||||
label: "定时任务管理",
|
||||
icon: CalendarClock,
|
||||
path: "/page/strategy/admin/scheduled-tasks",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
id: "mgmt-user-activity",
|
||||
label: "用户活动审计",
|
||||
icon: History,
|
||||
path: "/page/strategy/admin/user-activity",
|
||||
adminOnly: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "resource-management",
|
||||
label: "资源管理",
|
||||
icon: Database,
|
||||
children: [
|
||||
// {
|
||||
// id: "resource-network-xwt",
|
||||
// label: "网络xwt资源管理",
|
||||
// icon: Network,
|
||||
// path: "/page/strategy/resource-management/resource-network-actor",
|
||||
// },
|
||||
{
|
||||
id: "resource-knowledge-native",
|
||||
label: "问答梳理",
|
||||
icon: BookOpen,
|
||||
path: "/page/workspace/knowledge",
|
||||
},
|
||||
{
|
||||
id: "resource-knowledge-base",
|
||||
label: "知识库资源管理",
|
||||
icon: Library,
|
||||
path: "/page/strategy/resource-management/resource-knowledge-base",
|
||||
},
|
||||
// {
|
||||
// id: "resource-evaluation-index",
|
||||
// label: "作战资源管理",
|
||||
// icon: FileBox,
|
||||
// path: "/page/strategy/resource-management/resource-evaluation-index",
|
||||
// },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** 默认展开的菜单 id 集合(浅色版可折叠交互的初始状态) */
|
||||
export const defaultExpandedIds: ReadonlySet<string> = new Set([
|
||||
"qa-center",
|
||||
"light-app-center",
|
||||
"light-app-note-management",
|
||||
"task-management",
|
||||
"xk-center-task",
|
||||
"comprehensive-management",
|
||||
]);
|
||||
|
||||
/** 该菜单项自身或任意后代的 path 是否匹配当前路由(支持子路径,如 /knowledge/:id) */
|
||||
export function menuItemContainsPath(item: MenuItem, pathname: string): boolean {
|
||||
if (item.path && (pathname === item.path || pathname.startsWith(item.path + "/"))) {
|
||||
return true;
|
||||
}
|
||||
return item.children?.some((child) => menuItemContainsPath(child, pathname)) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在「一级 → 二级 → 三级」菜单中,取当前路由所在的「激活二级项」的三级子项。
|
||||
* 浅色侧边栏据此在二级右侧展开三级列;深色顶栏据此渲染三级横条。
|
||||
* 全量模式:三级仅一项时不返回(无选择必要,侧边栏保持窄栏 / 不渲染横条)。
|
||||
*/
|
||||
export function getActiveThirdLevelItems(
|
||||
items: MenuItem[],
|
||||
pathname: string,
|
||||
): MenuItem[] {
|
||||
const activeTop =
|
||||
items.find((item) => menuItemContainsPath(item, pathname)) ?? items[0];
|
||||
const secondary = activeTop?.children ?? [];
|
||||
const activeSecond = secondary.find(
|
||||
(item) => (item.children?.length ?? 0) > 0 && menuItemContainsPath(item, pathname),
|
||||
);
|
||||
const third = activeSecond?.children ?? [];
|
||||
return third.length <= 1 ? [] : third;
|
||||
}
|
||||
|
||||
/** 按 admin 角色过滤菜单:非 admin 不展示 ``adminOnly: true`` 的项;``userPath`` 替换 ``path``。 */
|
||||
export function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] {
|
||||
return items
|
||||
.filter((item) => !item.adminOnly || isAdmin)
|
||||
.map((item) => {
|
||||
const children = item.children
|
||||
? filterMenuItemsByRole(item.children, isAdmin)
|
||||
: undefined;
|
||||
const base = children ? { ...item, children } : item;
|
||||
if (!isAdmin && base.userPath) {
|
||||
const { userPath: _userPath, ...rest } = base;
|
||||
return { ...rest, path: base.userPath };
|
||||
}
|
||||
return base;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 全局侧边栏菜单数据与权限过滤逻辑。
|
||||
*
|
||||
* 浅色 ``PageSidebar`` 和深色 ``PageSidebarDark`` 共用这份数据 —— 菜单结构与
|
||||
* adminOnly 权限策略对两种皮肤完全一致;UI 渲染(icon 显示/折叠样式/hover flyout)
|
||||
* 在各自组件里实现。
|
||||
*
|
||||
* 修改菜单只需改这一处,两个 sidebar 自动同步。
|
||||
*/
|
||||
import type { ElementType } from "react";
|
||||
import {
|
||||
Activity,
|
||||
BookOpen,
|
||||
Bot,
|
||||
Brain,
|
||||
Briefcase,
|
||||
CalendarClock,
|
||||
CircleHelp,
|
||||
ClipboardList,
|
||||
Database,
|
||||
FileBox,
|
||||
FileText,
|
||||
Gauge,
|
||||
Grid3X3,
|
||||
HelpCircle,
|
||||
History,
|
||||
Layers,
|
||||
Layout,
|
||||
Library,
|
||||
List,
|
||||
ListChecks,
|
||||
ListTodo,
|
||||
Network,
|
||||
Radar,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Target,
|
||||
Trophy,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
|
||||
export type MenuItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: ElementType;
|
||||
path?: string;
|
||||
/** 额外视为「当前页属于本菜单项」的路由(不在侧栏展示,仅用于高亮)。 */
|
||||
aliasPaths?: string[];
|
||||
/** 非 admin 用户使用的路由(菜单对普通用户可见时生效,与简洁模式公开排行榜一致) */
|
||||
userPath?: string;
|
||||
adminOnly?: boolean;
|
||||
children?: MenuItem[];
|
||||
};
|
||||
|
||||
export const sidebarMenuItems: MenuItem[] = [
|
||||
{
|
||||
id: "qa-center",
|
||||
label: "问答管理",
|
||||
icon: HelpCircle,
|
||||
children: [
|
||||
{ id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" },
|
||||
{
|
||||
id: "multi-agent-roundtable",
|
||||
label: "多智能体会商",
|
||||
icon: Users,
|
||||
path: "/page/strategy/qa/multi-agent-roundtable",
|
||||
},
|
||||
// { id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
|
||||
{
|
||||
id: "qa-zzjc",
|
||||
label: "JC支持",
|
||||
icon: Bot,
|
||||
path: "/page/strategy/qa/zzjc",
|
||||
},
|
||||
{ id: "qa-zzxd", label: "XD应用", icon: Layout, path: "/page/strategy/qa/zzxd" },
|
||||
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "task-management",
|
||||
label: "任务管理",
|
||||
icon: Target,
|
||||
children: [
|
||||
{
|
||||
id: "xk-center-task",
|
||||
label: "XK中任务",
|
||||
icon: ClipboardList,
|
||||
children: [
|
||||
// {
|
||||
// id: "xk-task-jc",
|
||||
// label: "任务JC",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/xk-task-jc",
|
||||
// },
|
||||
// {
|
||||
// id: "xk-task-dispatch",
|
||||
// label: "任务调度",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/xk-task-dispatch",
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "coh-center-task",
|
||||
label: "COH中任务",
|
||||
icon: Activity,
|
||||
children: [
|
||||
// {
|
||||
// id: "coh-task-3qfx",
|
||||
// label: "3QFX",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/plan-task-sentiment",
|
||||
// },
|
||||
// {
|
||||
// id: "coh-task-fx",
|
||||
// label: "任务FX",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/plan-task/plan-task-fx",
|
||||
// },
|
||||
// {
|
||||
// id: "coh-task-xdfx",
|
||||
// label: "XDFX",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/plan-task/plan-task-action",
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "pg-center-task",
|
||||
label: "PG中任务",
|
||||
icon: Gauge,
|
||||
children: [
|
||||
// {
|
||||
// id: "pg-task-pg",
|
||||
// label: "任务PG",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/effect-task-pg",
|
||||
// },
|
||||
// {
|
||||
// id: "pg-task-xdpg",
|
||||
// label: "XDPG",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/effect-action-pg",
|
||||
// },
|
||||
// {
|
||||
// id: "pg-task-info-pg",
|
||||
// label: "信息PG",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/effect-info-pg",
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "ty-center-task",
|
||||
label: "TY中任务",
|
||||
icon: Radar,
|
||||
children: [
|
||||
// {
|
||||
// id: "ty-task-xdty",
|
||||
// label: "XDTY任务",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/mnty-task-mechanism",
|
||||
// },
|
||||
// {
|
||||
// id: "ty-task-plan",
|
||||
// label: "方案TY任务",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/mnty-task-plan",
|
||||
// },
|
||||
// {
|
||||
// id: "ty-task-xxty",
|
||||
// label: "XXTY任务",
|
||||
// icon: ListTodo,
|
||||
// path: "/page/strategy/task-management/mnty-task-zsty",
|
||||
// },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "light-app-center",
|
||||
label: "应用中心",
|
||||
icon: Grid3X3,
|
||||
children: [
|
||||
{
|
||||
id: "light-app-management",
|
||||
label: "应用管理",
|
||||
icon: Layout,
|
||||
path: "/page/strategy/light-app/management",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
id: "light-app-apps",
|
||||
label: "应用列表",
|
||||
icon: List,
|
||||
path: "/page/strategy/light-app/apps",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "comprehensive-management",
|
||||
label: "综合管理",
|
||||
icon: Settings,
|
||||
children: [
|
||||
{
|
||||
id: "mgmt-user-leaderboard",
|
||||
label: "用户排行榜",
|
||||
icon: Trophy,
|
||||
path: "/page/strategy/admin/user-leaderboard",
|
||||
userPath: "/page/strategy/user-leaderboard",
|
||||
},
|
||||
{ id: "mgmt-agents", label: "智能体管理", icon: Bot, path: "/page/strategy/agents" },
|
||||
// {
|
||||
// id: "mgmt-business-chain-mapping",
|
||||
// label: "配置管理",
|
||||
// icon: Network,
|
||||
// path: "/page/strategy/business-chain-mapping",
|
||||
// },
|
||||
{
|
||||
id: "mgmt-business-chains",
|
||||
label: "业务链条配置",
|
||||
icon: Layers,
|
||||
path: "/page/strategy/business-chains",
|
||||
aliasPaths: ["/page/strategy/business-chain-mapping"],
|
||||
},
|
||||
{ id: "mgmt-skills", label: "技能管理", icon: Sparkles, path: "/page/strategy/skills" },
|
||||
{ id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" },
|
||||
{
|
||||
id: "mgmt-tasks",
|
||||
label: "定时任务",
|
||||
icon: ListChecks,
|
||||
path: "/page/strategy/scheduled/tasks",
|
||||
},
|
||||
{
|
||||
id: "mgmt-llm-metrics",
|
||||
label: "大模型调用统计",
|
||||
icon: Gauge,
|
||||
path: "/page/strategy/settings/llm-metrics",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
id: "mgmt-users",
|
||||
label: "用户管理",
|
||||
icon: Users,
|
||||
path: "/page/strategy/admin/users",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
id: "mgmt-positions",
|
||||
label: "岗位管理",
|
||||
icon: Briefcase,
|
||||
path: "/page/strategy/admin/positions",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
id: "mgmt-admin-scheduled-tasks",
|
||||
label: "定时任务管理",
|
||||
icon: CalendarClock,
|
||||
path: "/page/strategy/admin/scheduled-tasks",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
id: "mgmt-user-activity",
|
||||
label: "用户活动审计",
|
||||
icon: History,
|
||||
path: "/page/strategy/admin/user-activity",
|
||||
adminOnly: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "resource-management",
|
||||
label: "资源管理",
|
||||
icon: Database,
|
||||
children: [
|
||||
// {
|
||||
// id: "resource-network-xwt",
|
||||
// label: "网络xwt资源管理",
|
||||
// icon: Network,
|
||||
// path: "/page/strategy/resource-management/resource-network-actor",
|
||||
// },
|
||||
{
|
||||
id: "resource-knowledge-native",
|
||||
label: "问答梳理",
|
||||
icon: BookOpen,
|
||||
path: "/page/workspace/knowledge",
|
||||
},
|
||||
{
|
||||
id: "resource-knowledge-base",
|
||||
label: "知识库资源管理",
|
||||
icon: Library,
|
||||
path: "/page/strategy/resource-management/resource-knowledge-base",
|
||||
},
|
||||
// {
|
||||
// id: "resource-evaluation-index",
|
||||
// label: "作战资源管理",
|
||||
// icon: FileBox,
|
||||
// path: "/page/strategy/resource-management/resource-evaluation-index",
|
||||
// },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** 默认展开的菜单 id 集合(浅色版可折叠交互的初始状态) */
|
||||
export const defaultExpandedIds: ReadonlySet<string> = new Set([
|
||||
"qa-center",
|
||||
"light-app-center",
|
||||
"light-app-note-management",
|
||||
"task-management",
|
||||
"xk-center-task",
|
||||
"comprehensive-management",
|
||||
]);
|
||||
|
||||
/** 单条路由是否与 pathname 精确匹配或为其子路径。 */
|
||||
function routeMatchesPathname(pathname: string, route?: string): boolean {
|
||||
if (!route) return false;
|
||||
return pathname === route || pathname.startsWith(route + "/");
|
||||
}
|
||||
|
||||
/** 菜单项自身 path / aliasPaths 是否匹配当前路由。 */
|
||||
export function menuItemMatchesPath(item: MenuItem, pathname: string): boolean {
|
||||
if (routeMatchesPathname(pathname, item.path)) return true;
|
||||
return item.aliasPaths?.some((p) => routeMatchesPathname(pathname, p)) ?? false;
|
||||
}
|
||||
|
||||
/** 该菜单项自身或任意后代的 path 是否匹配当前路由(支持子路径,如 /knowledge/:id) */
|
||||
export function menuItemContainsPath(item: MenuItem, pathname: string): boolean {
|
||||
if (menuItemMatchesPath(item, pathname)) return true;
|
||||
return item.children?.some((child) => menuItemContainsPath(child, pathname)) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在「一级 → 二级 → 三级」菜单中,取当前路由所在的「激活二级项」的三级子项。
|
||||
* 浅色侧边栏据此在二级右侧展开三级列;深色顶栏据此渲染三级横条。
|
||||
* 全量模式:三级仅一项时不返回(无选择必要,侧边栏保持窄栏 / 不渲染横条)。
|
||||
*/
|
||||
export function getActiveThirdLevelItems(
|
||||
items: MenuItem[],
|
||||
pathname: string,
|
||||
): MenuItem[] {
|
||||
const activeTop =
|
||||
items.find((item) => menuItemContainsPath(item, pathname)) ?? items[0];
|
||||
const secondary = activeTop?.children ?? [];
|
||||
const activeSecond = secondary.find(
|
||||
(item) => (item.children?.length ?? 0) > 0 && menuItemContainsPath(item, pathname),
|
||||
);
|
||||
const third = activeSecond?.children ?? [];
|
||||
return third.length <= 1 ? [] : third;
|
||||
}
|
||||
|
||||
/** 按 admin 角色过滤菜单:非 admin 不展示 ``adminOnly: true`` 的项;``userPath`` 替换 ``path``。 */
|
||||
export function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] {
|
||||
return items
|
||||
.filter((item) => !item.adminOnly || isAdmin)
|
||||
.map((item) => {
|
||||
const children = item.children
|
||||
? filterMenuItemsByRole(item.children, isAdmin)
|
||||
: undefined;
|
||||
const base = children ? { ...item, children } : item;
|
||||
if (!isAdmin && base.userPath) {
|
||||
const { userPath: _userPath, ...rest } = base;
|
||||
return { ...rest, path: base.userPath };
|
||||
}
|
||||
return base;
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { motion } from "motion/react";
|
||||
import { Select } from "tdesign-react";
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowLeft,
|
||||
HelpCircle,
|
||||
Info,
|
||||
Link2,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useRouteNavigate } from "@/core/route-base";
|
||||
import { listAgents } from "@/core/agents/api";
|
||||
import type { Agent } from "@/core/agents/types";
|
||||
import { listChains } from "@/roundtable-planning/api/chains";
|
||||
@@ -34,6 +36,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||
|
||||
export default function BusinessChainMappingPage() {
|
||||
const navigate = useRouteNavigate();
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
// 整片脱敏:业务链条映射页展示文本(链条名/描述/席位名/标题)。输入框 Hook 自动跳过。
|
||||
@@ -233,6 +236,20 @@ export default function BusinessChainMappingPage() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/page/strategy/business-chains")}
|
||||
className={cn(
|
||||
"relative z-10 inline-flex shrink-0 items-center gap-1.5 rounded-[5px] border px-3.5 py-2 text-xs font-semibold transition-all active:scale-95",
|
||||
isDark
|
||||
? "border-white/10 bg-white/5 text-slate-300 hover:bg-white/10 hover:text-slate-100"
|
||||
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50 hover:text-slate-900",
|
||||
)}
|
||||
>
|
||||
<ArrowLeft className="size-3.5" />
|
||||
返回业务链条
|
||||
</button>
|
||||
</motion.div>
|
||||
|
||||
{/* ── 表格区 ── */}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
SearchIcon,
|
||||
SendIcon,
|
||||
SettingsIcon,
|
||||
SlidersHorizontalIcon,
|
||||
Trash2Icon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
@@ -252,17 +253,27 @@ export default function BusinessChainListPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => goConfigure()}
|
||||
className={cn(
|
||||
"flex shrink-0 items-center gap-2.5 self-start rounded-xl px-4 py-2.5 text-xs font-bold shadow-sm shadow-[#CF1322]/10 transition active:scale-95 md:self-auto",
|
||||
BRAND,
|
||||
)}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
<span>新建链条</span>
|
||||
</button>
|
||||
<div className="flex shrink-0 items-center gap-2 self-start md:self-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/page/strategy/business-chain-mapping")}
|
||||
className="flex items-center gap-2 rounded-xl border border-zinc-200 bg-white px-4 py-2.5 text-xs font-bold text-zinc-700 shadow-sm transition hover:bg-zinc-50 active:scale-95 dark:border-[#1A2F5A] dark:bg-[#122347] dark:text-[#C5CFE0] dark:hover:bg-[#1A2F5A]"
|
||||
>
|
||||
<SlidersHorizontalIcon className="size-4" />
|
||||
<span>配置管理</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => goConfigure()}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 rounded-xl px-4 py-2.5 text-xs font-bold shadow-sm shadow-[#CF1322]/10 transition active:scale-95",
|
||||
BRAND,
|
||||
)}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
<span>新建链条</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { Bell, ChevronDown, FileText, Shirt, User } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import "@/components/page-sdebar.css";
|
||||
import { NotificationDialog } from "@/components/workspace/notification-dialog";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { getStoredAuth, getAccountDisplayName } from "@/core/auth";
|
||||
import { isWujieEnv } from "@/core/embed/wujie";
|
||||
import { useUnreadCount } from "@/core/notifications";
|
||||
import { useMyPosition } from "@/core/positions";
|
||||
import {
|
||||
getActiveThirdLevelItems,
|
||||
menuItemContainsPath,
|
||||
menuItemMatchesPath,
|
||||
type MenuItem,
|
||||
} from "@/core/page-layout/sidebar-menu";
|
||||
import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { useTheme, type ThemeType } from "../contexts/ThemeContext";
|
||||
import { readUserInfo } from "./lib/light-app";
|
||||
|
||||
const iconButtonClass = (isDark: boolean) =>
|
||||
cn(
|
||||
@@ -135,28 +136,14 @@ const Header = ({ collapsed }: HeaderProps) => {
|
||||
const { data: myPosition } = useMyPosition();
|
||||
const positionName = myPosition?.name?.trim() || "";
|
||||
|
||||
// 登录时写入的真实用户名(外部登录存于 localStorage.userInfo.userName);
|
||||
// 缺省回退到 stored auth 的邮箱前缀。
|
||||
const displayName = useMemo(() => {
|
||||
const info = readUserInfo();
|
||||
const name = typeof info.userName === "string" ? info.userName.trim() : "";
|
||||
if (name) return name;
|
||||
const email = auth?.email?.trim();
|
||||
return email ? email.split("@")[0] : "用户";
|
||||
}, [auth?.email]);
|
||||
const displayName = getAccountDisplayName(auth);
|
||||
const showTopNav = isDark && !collapsed && !isWujieEnv();
|
||||
// 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。
|
||||
// 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏,
|
||||
// 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。
|
||||
const showLightTopNav = !isDark && !isWujieEnv();
|
||||
|
||||
const hasActiveChild = (item: MenuItem): boolean => {
|
||||
if (item.path && (pathname === item.path || pathname.startsWith(item.path + "/"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.children?.some(hasActiveChild) ?? false;
|
||||
};
|
||||
const hasActiveChild = (item: MenuItem): boolean => menuItemContainsPath(item, pathname);
|
||||
|
||||
const findFirstRoutePath = (items?: MenuItem[]): string | null => {
|
||||
if (!items?.length) {
|
||||
@@ -189,7 +176,9 @@ const Header = ({ collapsed }: HeaderProps) => {
|
||||
};
|
||||
|
||||
const renderTopItem = (item: MenuItem) => {
|
||||
const isSelected = item.path ? (pathname === item.path || pathname.startsWith(item.path + "/")) : hasActiveChild(item);
|
||||
const isSelected = item.path || item.aliasPaths?.length
|
||||
? menuItemMatchesPath(item, pathname)
|
||||
: hasActiveChild(item);
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
@@ -203,7 +192,9 @@ const Header = ({ collapsed }: HeaderProps) => {
|
||||
};
|
||||
|
||||
const renderLightTopItem = (item: MenuItem) => {
|
||||
const isSelected = item.path ? (pathname === item.path || pathname.startsWith(item.path + "/")) : hasActiveChild(item);
|
||||
const isSelected = item.path || item.aliasPaths?.length
|
||||
? menuItemMatchesPath(item, pathname)
|
||||
: hasActiveChild(item);
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
@@ -388,7 +379,9 @@ const Header = ({ collapsed }: HeaderProps) => {
|
||||
<nav className="third-submenu-bar" aria-label="三级菜单">
|
||||
<div className="third-submenu-list">
|
||||
{thirdSubNavItems.map((item) => {
|
||||
const isSelected = item.path ? (pathname === item.path || pathname.startsWith(item.path + "/")) : hasActiveChild(item);
|
||||
const isSelected = item.path || item.aliasPaths?.length
|
||||
? menuItemMatchesPath(item, pathname)
|
||||
: hasActiveChild(item);
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
Reference in new issue
Block a user