配置页面修改

This commit is contained in:
董宏凯 committed 2026-06-16 22:01:06 +08:00
1 parent 56e17f826c
commit 6c5a2cffe3
2 files changed
+145 -8

No files matched your search

@@ -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<string[] | null>(null);
const [reordering, setReordering] = useState(false);
const dragIndexRef = useRef<number | null>(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 (
<div className="light-app-management-page h-full overflow-y-auto bg-neutral-50 dark:bg-[#0C1836] dark:text-[#C5CFE0]">
<div className="mx-auto flex min-h-full w-full max-w-7xl flex-col space-y-7 px-8 py-8">
@@ -349,9 +414,18 @@ export default function LightAppManagementPage() {
</div>
<div className="space-y-4">
<div className="lam-count text-xs font-medium text-neutral-500">
搜寻到 <strong className="font-bold text-[#F5222D]">{tableApps.length}</strong>{" "}
个注册完成的子模块轻应用
<div className="lam-count flex flex-wrap items-center gap-x-3 gap-y-1 text-xs font-medium text-neutral-500">
<span>
搜寻到 <strong className="font-bold text-[#F5222D]">{tableApps.length}</strong>{" "}
个注册完成的子模块轻应用
</span>
{reorderable && (
<span className="inline-flex items-center gap-1 text-[11px] text-sky-600">
<GripVertical className="h-3.5 w-3.5" />
拖拽行或用上 / 下按钮调整「{mountLabel(mountFilter)}」下的菜单顺序,调整即生效
{reordering && <span className="text-neutral-400">· 保存中…</span>}
</span>
)}
</div>
{viewMode === "table" ? (
@@ -359,6 +433,7 @@ export default function LightAppManagementPage() {
<table className="lam-table w-full table-fixed border-collapse text-left">
<thead>
<tr className="select-none border-b border-neutral-100/80 bg-neutral-50/40 text-[12.5px] font-semibold text-neutral-500">
{reorderable && <th className="w-[44px] px-2 py-4" aria-label="拖拽排序" />}
<th className="w-[15%] px-6 py-4">名称</th>
<th className="w-[14%] px-4 py-4">类型</th>
<th className="w-[27%] px-4 py-4">地址</th>
@@ -369,8 +444,32 @@ export default function LightAppManagementPage() {
</tr>
</thead>
<tbody className="divide-y divide-neutral-100">
{tableApps.map((app) => (
<tr key={app.appId} className="group align-middle transition-colors hover:bg-neutral-50/40">
{displayApps.map((app, index) => (
<tr
key={app.appId}
className={`group align-middle transition-colors hover:bg-neutral-50/40 ${
reorderable ? "lam-reorder-row" : ""
}`}
draggable={reorderable}
onDragStart={
reorderable
? (e) => {
dragIndexRef.current = index;
e.dataTransfer.effectAllowed = "move";
}
: undefined
}
onDragOver={reorderable ? (e) => e.preventDefault() : undefined}
onDrop={reorderable ? () => handleRowDrop(index) : undefined}
>
{reorderable && (
<td className="px-2 py-[18px] text-center">
<GripVertical
className="lam-drag-handle mx-auto h-4 w-4 cursor-grab text-neutral-300"
aria-hidden
/>
</td>
)}
<td className="px-6 py-[18px]">
<span className="lam-row-name text-[14px] font-bold text-neutral-800 transition-colors group-hover:text-[#F5222D]">
{app.name}
@@ -505,6 +604,29 @@ export default function LightAppManagementPage() {
<td className="px-4 py-[18px] pr-6 text-center">
<div className="flex items-center justify-center space-x-1">
{reorderable && (
<>
<button
type="button"
disabled={index === 0 || reordering}
onClick={() => moveApp(index, -1)}
className="lam-icon-btn cursor-pointer rounded-[5px] p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-[#F5222D] disabled:cursor-not-allowed disabled:opacity-30"
title="上移"
>
<ArrowUp className="h-3.5 w-3.5" />
</button>
<button
type="button"
disabled={index === displayApps.length - 1 || reordering}
onClick={() => moveApp(index, 1)}
className="lam-icon-btn cursor-pointer rounded-[5px] p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-[#F5222D] disabled:cursor-not-allowed disabled:opacity-30"
title="下移"
>
<ArrowDown className="h-3.5 w-3.5" />
</button>
<span className="mx-0.5 h-4 w-px shrink-0 bg-neutral-200" />
</>
)}
<button
onClick={() => openEdit(app)}
className="lam-icon-btn cursor-pointer rounded-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
@@ -524,9 +646,9 @@ export default function LightAppManagementPage() {
</tr>
))}
{tableApps.length === 0 && (
{displayApps.length === 0 && (
<tr>
<td colSpan={7} className="py-12 text-center">
<td colSpan={reorderable ? 8 : 7} className="py-12 text-center">
<div className="flex flex-col items-center justify-center space-y-3">
<div className="lam-empty-icon flex h-12 w-12 items-center justify-center rounded-full bg-neutral-100 text-neutral-300">
<Info className="h-6 w-6" />
@@ -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;
}