feat: 业务链条配置管理导航与顶栏用户名统一

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