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; +}