配置页面修改
This commit is contained in:
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;
|
||||
}
|
||||
Reference in new issue
Block a user