From 0bad0eb306c0ca5a498264e1486616a63cfdb2b9 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Tue, 16 Jun 2026 22:00:30 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=9A=90=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-web/src/core/page-layout/sidebar-menu.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend-web/src/core/page-layout/sidebar-menu.ts b/frontend-web/src/core/page-layout/sidebar-menu.ts index ca1c76ff..c49e90dc 100644 --- a/frontend-web/src/core/page-layout/sidebar-menu.ts +++ b/frontend-web/src/core/page-layout/sidebar-menu.ts @@ -208,12 +208,12 @@ export const sidebarMenuItems: MenuItem[] = [ 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-chain-mapping", + // label: "配置管理", + // icon: Network, + // path: "/page/strategy/business-chain-mapping", + // }, { id: "mgmt-business-chains", label: "业务链条配置", From 6c5a2cffe3d18bc337d55690b45eb5bed6555db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Tue, 16 Jun 2026 22:01:06 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/LightAppManagementPage.tsx | 138 +++++++++++++++++- .../src/pages/light-app-management.css | 15 ++ 2 files changed, 145 insertions(+), 8 deletions(-) diff --git a/frontend-web/src/pages/LightAppManagementPage.tsx b/frontend-web/src/pages/LightAppManagementPage.tsx index 2e28b2c0..679c6e95 100644 --- a/frontend-web/src/pages/LightAppManagementPage.tsx +++ b/frontend-web/src/pages/LightAppManagementPage.tsx @@ -3,13 +3,16 @@ * * 深蓝主题适配见 `./light-app-management.css`(《深蓝色 UI 规范》tokens.css)。 */ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { AlertCircle, AlignLeft, + ArrowDown, + ArrowUp, ChevronDown, ChevronUp, Copy, + GripVertical, ExternalLink, FolderTree, Globe, @@ -239,6 +242,68 @@ export default function LightAppManagementPage() { [apps, categoryFilter, mountFilter], ); + // ── 排序调整:仅当「iframe 嵌套 + 指定某个挂载菜单」时开启 ── + // 同一菜单下的多个 iframe 子项可拖拽 / 上下移调整顺序;调整即写回各应用的 sortOrder, + // 侧栏(use-sidebar-menu 按列表顺序注入,后端按 sort_order 升序返回)随之同步。 + const reorderable = categoryFilter === "iframe" && !!mountFilter && viewMode === "table"; + // 乐观顺序覆盖(保存期间稳定显示,刷新回来与服务端一致);切换筛选/搜索即清空,回落服务端顺序。 + const [orderIds, setOrderIds] = useState(null); + const [reordering, setReordering] = useState(false); + const dragIndexRef = useRef(null); + useEffect(() => { + setOrderIds(null); + }, [categoryFilter, mountFilter, search]); + + const displayApps = useMemo(() => { + if (!reorderable || !orderIds) return tableApps; + const pos = new Map(orderIds.map((id, i) => [id, i] as const)); + return [...tableApps].sort( + (a, b) => (pos.get(a.appId) ?? 1e9) - (pos.get(b.appId) ?? 1e9), + ); + }, [reorderable, orderIds, tableApps]); + + // 把整组按新顺序重排 sortOrder(组内 0-based;只 PUT 顺序变了的项),乐观先行。 + const persistOrder = async (ordered: LightApp[]) => { + setOrderIds(ordered.map((a) => a.appId)); + const changed = ordered.filter((a, i) => a.sortOrder !== i); + if (changed.length === 0) return; + setReordering(true); + try { + await Promise.all( + ordered.map((a, i) => + a.sortOrder === i + ? Promise.resolve() + : actions.update.mutateAsync({ appId: a.appId, patch: { sortOrder: i } }), + ), + ); + } catch (err) { + toast.error((err as Error).message || "排序保存失败"); + setOrderIds(null); + } finally { + setReordering(false); + } + }; + + const moveApp = (index: number, dir: -1 | 1) => { + if (reordering) return; + const target = index + dir; + if (target < 0 || target >= displayApps.length) return; + const next = [...displayApps]; + [next[index], next[target]] = [next[target]!, next[index]!]; + void persistOrder(next); + }; + + const handleRowDrop = (toIndex: number) => { + const from = dragIndexRef.current; + dragIndexRef.current = null; + if (from === null || from === toIndex || reordering) return; + const next = [...displayApps]; + const [moved] = next.splice(from, 1); + if (!moved) return; + next.splice(toIndex, 0, moved); + void persistOrder(next); + }; + return (
@@ -349,9 +414,18 @@ export default function LightAppManagementPage() {
-
- 搜寻到 {tableApps.length}{" "} - 个注册完成的子模块轻应用 +
+ + 搜寻到 {tableApps.length}{" "} + 个注册完成的子模块轻应用 + + {reorderable && ( + + + 拖拽行或用上 / 下按钮调整「{mountLabel(mountFilter)}」下的菜单顺序,调整即生效 + {reordering && · 保存中…} + + )}
{viewMode === "table" ? ( @@ -359,6 +433,7 @@ export default function LightAppManagementPage() { + {reorderable && @@ -369,8 +444,32 @@ export default function LightAppManagementPage() { - {tableApps.map((app) => ( - + {displayApps.map((app, index) => ( + { + dragIndexRef.current = index; + e.dataTransfer.effectAllowed = "move"; + } + : undefined + } + onDragOver={reorderable ? (e) => e.preventDefault() : undefined} + onDrop={reorderable ? () => handleRowDrop(index) : undefined} + > + {reorderable && ( + + )} ))} - {tableApps.length === 0 && ( + {displayApps.length === 0 && ( -
} 名称 类型 地址
+ + {app.name} @@ -505,6 +604,29 @@ export default function LightAppManagementPage() {
+ {reorderable && ( + <> + + + + + )}
+
diff --git a/frontend-web/src/pages/light-app-management.css b/frontend-web/src/pages/light-app-management.css index 8f0fa408..22250842 100644 --- a/frontend-web/src/pages/light-app-management.css +++ b/frontend-web/src/pages/light-app-management.css @@ -678,3 +678,18 @@ [data-theme="lightblue"] .light-app-management-page .lam-dialog-save:active { background-color: #0056C6; } + +/* ── iframe 菜单排序:拖拽行 + 拖拽手柄 ───────────────────────────────── */ +.light-app-management-page .lam-reorder-row { + cursor: grab; +} +.light-app-management-page .lam-reorder-row:active { + cursor: grabbing; +} +/* 深蓝主题下手柄/箭头用次级文字色,避免在深底上发灰看不清 */ +.dark .light-app-management-page .lam-drag-handle { + color: #4A6491; +} +.dark .light-app-management-page .lam-reorder-row:hover .lam-drag-handle { + color: #6E88B0; +} From 318f6c0c0cddf8122785b47fa0b5c661050feea5 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Tue, 16 Jun 2026 22:42:42 +0800 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=E4=B8=9A=E5=8A=A1=E9=93=BE?= =?UTF-8?q?=E6=9D=A1=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E4=B8=8E=E9=A1=B6=E6=A0=8F=E7=94=A8=E6=88=B7=E5=90=8D=E7=BB=9F?= =?UTF-8?q?=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 业务链条页增加配置管理入口与返回导航;配置管理页通过 aliasPaths 保持侧栏选中;顶栏用户名与设置个人信息页共用 getAccountDisplayName。 --- .../src/components/page-sidebar-dark.tsx | 20 +- frontend-web/src/components/page-sidebar.tsx | 23 +- .../settings/account-settings-page.tsx | 5 +- frontend-web/src/core/auth/index.ts | 197 ++--- .../src/core/page-layout/sidebar-menu.ts | 719 +++++++++--------- .../src/pages/BusinessChainMappingPage.tsx | 17 + .../pages/BusinessChainListPage.tsx | 33 +- .../src/strategy-components/Header.tsx | 37 +- 8 files changed, 544 insertions(+), 507 deletions(-) diff --git a/frontend-web/src/components/page-sidebar-dark.tsx b/frontend-web/src/components/page-sidebar-dark.tsx index af274aff..4851a5d5 100644 --- a/frontend-web/src/components/page-sidebar-dark.tsx +++ b/frontend-web/src/components/page-sidebar-dark.tsx @@ -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( @@ -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 (
{ - 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() {
+ + {/* ── 表格区 ── */} diff --git a/frontend-web/src/roundtable-planning/pages/BusinessChainListPage.tsx b/frontend-web/src/roundtable-planning/pages/BusinessChainListPage.tsx index aab9004e..cc7bc3cf 100644 --- a/frontend-web/src/roundtable-planning/pages/BusinessChainListPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/BusinessChainListPage.tsx @@ -43,6 +43,7 @@ import { SearchIcon, SendIcon, SettingsIcon, + SlidersHorizontalIcon, Trash2Icon, UserIcon, UsersIcon, @@ -252,17 +253,27 @@ export default function BusinessChainListPage() {

- +
+ + +
diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index 27c617ba..4d75ccbd 100644 --- a/frontend-web/src/strategy-components/Header.tsx +++ b/frontend-web/src/strategy-components/Header.tsx @@ -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 (