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 { 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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user