配置页面修改
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)。
|
* 深蓝主题适配见 `./light-app-management.css`(《深蓝色 UI 规范》tokens.css)。
|
||||||
*/
|
*/
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
AlignLeft,
|
AlignLeft,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Copy,
|
Copy,
|
||||||
|
GripVertical,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
FolderTree,
|
FolderTree,
|
||||||
Globe,
|
Globe,
|
||||||
@@ -239,6 +242,68 @@ export default function LightAppManagementPage() {
|
|||||||
[apps, categoryFilter, mountFilter],
|
[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 (
|
return (
|
||||||
<div className="light-app-management-page h-full overflow-y-auto bg-neutral-50 dark:bg-[#0C1836] dark:text-[#C5CFE0]">
|
<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">
|
<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>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="lam-count text-xs font-medium text-neutral-500">
|
<div className="lam-count flex flex-wrap items-center gap-x-3 gap-y-1 text-xs font-medium text-neutral-500">
|
||||||
搜寻到 <strong className="font-bold text-[#F5222D]">{tableApps.length}</strong>{" "}
|
<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>
|
</div>
|
||||||
|
|
||||||
{viewMode === "table" ? (
|
{viewMode === "table" ? (
|
||||||
@@ -359,6 +433,7 @@ export default function LightAppManagementPage() {
|
|||||||
<table className="lam-table w-full table-fixed border-collapse text-left">
|
<table className="lam-table w-full table-fixed border-collapse text-left">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="select-none border-b border-neutral-100/80 bg-neutral-50/40 text-[12.5px] font-semibold text-neutral-500">
|
<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-[15%] px-6 py-4">名称</th>
|
||||||
<th className="w-[14%] px-4 py-4">类型</th>
|
<th className="w-[14%] px-4 py-4">类型</th>
|
||||||
<th className="w-[27%] px-4 py-4">地址</th>
|
<th className="w-[27%] px-4 py-4">地址</th>
|
||||||
@@ -369,8 +444,32 @@ export default function LightAppManagementPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-neutral-100">
|
<tbody className="divide-y divide-neutral-100">
|
||||||
{tableApps.map((app) => (
|
{displayApps.map((app, index) => (
|
||||||
<tr key={app.appId} className="group align-middle transition-colors hover:bg-neutral-50/40">
|
<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]">
|
<td className="px-6 py-[18px]">
|
||||||
<span className="lam-row-name text-[14px] font-bold text-neutral-800 transition-colors group-hover:text-[#F5222D]">
|
<span className="lam-row-name text-[14px] font-bold text-neutral-800 transition-colors group-hover:text-[#F5222D]">
|
||||||
{app.name}
|
{app.name}
|
||||||
@@ -505,6 +604,29 @@ export default function LightAppManagementPage() {
|
|||||||
|
|
||||||
<td className="px-4 py-[18px] pr-6 text-center">
|
<td className="px-4 py-[18px] pr-6 text-center">
|
||||||
<div className="flex items-center justify-center space-x-1">
|
<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
|
<button
|
||||||
onClick={() => openEdit(app)}
|
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"
|
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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{tableApps.length === 0 && (
|
{displayApps.length === 0 && (
|
||||||
<tr>
|
<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="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">
|
<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" />
|
<Info className="h-6 w-6" />
|
||||||
|
|||||||
@@ -678,3 +678,18 @@
|
|||||||
[data-theme="lightblue"] .light-app-management-page .lam-dialog-save:active {
|
[data-theme="lightblue"] .light-app-management-page .lam-dialog-save:active {
|
||||||
background-color: #0056C6;
|
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