20260819213933

This commit is contained in:
oneao committed 2026-08-19 21:39:33 +08:00
1 parent a0a4273f4c
commit 8e75c7989a
27 files changed
+3852 -2434

No files matched your search

+180 -196
View File
@@ -1,218 +1,202 @@
import { useState, useCallback, useRef } from "react";
import type { ModuleField, FieldGroup, EditMode, FieldDataSource } from "./types";
import { defaultFields, defaultGroups, makeField } from "./data/moduleData";
import ModuleSettings from "./components/ModuleSettings";
import FieldConfigList from "./components/FieldConfigList";
import FormPreview from "./components/FormPreview";
import TablePreview from "./components/TablePreview";
import DataSourceDrawer from "./components/DataSourceDrawer";
type PreviewMode = "form" | "table";
// FMS 新系统元数据配置 Demo —— 主布局
// 主视图:统一元数据树(菜单 → 模块 → 实体 → 字段/分组/编码)全链路维护
// 辅助 Tab:权限(RBAC)
import { useMemo, useState } from "react";
import type {
Autocode,
Entity,
Field,
FieldGroup,
I18n,
Menu,
Module,
ModuleDependency,
Power,
Role,
RolePower,
UserRole,
} from "./types";
import {
initialAutocodes,
initialDependencies,
initialEntities,
initialFieldGroups,
initialFields,
initialI18ns,
initialMenus,
initialModules,
initialPowers,
initialRolePowers,
initialRoles,
initialUserRoles,
} from "./data/mock";
import MetaTree, { type TreeSelection } from "./components/MetaTree";
import NodeEditor from "./components/NodeEditor";
import PermissionPanel from "./components/PermissionPanel";
export default function App() {
const [fields, setFields] = useState<ModuleField[]>(defaultFields);
const [groups, setGroups] = useState<FieldGroup[]>(defaultGroups);
const [editMode, setEditMode] = useState<EditMode>("form");
const [previewMode, setPreviewMode] = useState<PreviewMode>("form");
const [activeId, setActiveId] = useState<number | null>(null);
const [dsFieldId, setDsFieldId] = useState<number | null>(null);
const [savedTip, setSavedTip] = useState(false);
const nextGroupId = useRef(10);
const [view, setView] = useState<"tree" | "permission">("tree");
const [selection, setSelection] = useState<TreeSelection | null>(null);
const [modules, setModules] = useState<Module[]>(initialModules);
const [dependencies, setDependencies] = useState<ModuleDependency[]>(initialDependencies);
const [entities, setEntities] = useState<Entity[]>(initialEntities);
const [fields, setFields] = useState<Field[]>(initialFields);
const [groups, setGroups] = useState<FieldGroup[]>(initialFieldGroups);
const [menus, setMenus] = useState<Menu[]>(initialMenus);
const [autocodes, setAutocodes] = useState<Autocode[]>(initialAutocodes);
const [i18ns, setI18ns] = useState<I18n[]>(initialI18ns);
const [roles, setRoles] = useState<Role[]>(initialRoles);
const [userRoles, setUserRoles] = useState<UserRole[]>(initialUserRoles);
const [powers, setPowers] = useState<Power[]>(initialPowers);
const [rolePowers, setRolePowers] = useState<RolePower[]>(initialRolePowers);
const [resetKey, setResetKey] = useState(0);
const updateField = useCallback(
(id: number, key: keyof ModuleField, value: ModuleField[keyof ModuleField]) => {
setFields((prev) =>
prev.map((f) => (f.id === id ? ({ ...f, [key]: value } as ModuleField) : f))
);
},
[]
const stats = useMemo(
() => [
{ label: "模块", value: modules.length, table: "s_module" },
{ label: "实体", value: entities.length, table: "s_entity" },
{ label: "字段", value: fields.length, table: "s_field" },
{ label: "菜单", value: menus.length, table: "s_menu" },
{ label: "编码", value: autocodes.length, table: "s_autocode" },
{ label: "角色", value: roles.length, table: "s_role" },
{ label: "权限", value: powers.length, table: "s_power" },
{ label: "多语言", value: i18ns.length, table: "s_i18n" },
],
[modules, entities, fields, menus, autocodes, roles, powers, i18ns],
);
const updateFieldList = useCallback(
(id: number, patch: Partial<ModuleField>) => {
setFields((prev) => prev.map((f) => (f.id === id ? { ...f, ...patch } : f)));
},
[]
);
const addField = useCallback(
(groupId: string) => {
const id = Date.now();
setFields((prev) => [...prev, makeField(groupId)]);
setActiveId(id);
},
[]
);
const deleteField = useCallback((id: number) => {
setFields((prev) => prev.filter((f) => f.id !== id));
setActiveId((cur) => (cur === id ? null : cur));
}, []);
// 配置列表:上/下移
const reorderFields = useCallback((id: number, dir: -1 | 1) => {
setFields((prev) => {
const sorted = [...prev].sort((a, b) => a.order - b.order);
const i = sorted.findIndex((f) => f.id === id);
const j = i + dir;
if (j < 0 || j >= sorted.length) return prev;
const t = sorted[i].order;
sorted[i].order = sorted[j].order;
sorted[j].order = t;
return [...sorted];
});
}, []);
// 预览区:拖拽排序(按 id 重排并顺续编号)
const reorderByDrag = useCallback((activeId: number, overId: number) => {
setFields((prev) => {
const sorted = [...prev].sort((a, b) => a.order - b.order);
const from = sorted.findIndex((f) => f.id === activeId);
const to = sorted.findIndex((f) => f.id === overId);
if (from < 0 || to < 0) return prev;
const [moved] = sorted.splice(from, 1);
sorted.splice(to, 0, moved);
return sorted.map((f, i) => ({ ...f, order: (i + 1) * 10 }));
});
}, []);
const resizeField = useCallback(
(id: number, width: number) => updateField(id, "listW", width),
[updateField]
);
const renameGroup = useCallback((id: string, name: string) => {
setGroups((prev) => prev.map((g) => (g.id === id ? { ...g, name } : g)));
}, []);
const addGroup = useCallback((name: string) => {
const id = `g${nextGroupId.current++}`;
setGroups((prev) => [...prev, { id, name }]);
return id;
}, []);
const openDataSource = useCallback((fieldId: number) => setDsFieldId(fieldId), []);
const dsField =
dsFieldId != null ? fields.find((f) => f.id === dsFieldId) ?? null : null;
const readOnly = editMode === "none";
const resetAll = () => {
setModules(initialModules);
setDependencies(initialDependencies);
setEntities(initialEntities);
setFields(initialFields);
setGroups(initialFieldGroups);
setMenus(initialMenus);
setAutocodes(initialAutocodes);
setI18ns(initialI18ns);
setRoles(initialRoles);
setUserRoles(initialUserRoles);
setPowers(initialPowers);
setRolePowers(initialRolePowers);
setSelection(null);
setResetKey((k) => k + 1);
};
return (
<div className="h-screen flex flex-col overflow-hidden">
<div className="min-h-screen bg-slate-100 text-slate-800">
{/* 顶栏 */}
<header className="shrink-0 flex items-center justify-between px-5 h-14 bg-white border-b border-slate-200 shadow-sm z-10">
<div className="flex items-center gap-3">
<span className="text-lg">🚢</span>
<div className="leading-tight">
<div className="font-semibold text-slate-800">海运主单 · 模块设计器</div>
<div className="text-xs text-slate-400">字段配置所见即所得</div>
<header className="sticky top-0 z-10 border-b border-slate-200 bg-white/90 backdrop-blur">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-3">
<div>
<h1 className="text-lg font-bold text-slate-800">FMS 新系统 · 元数据配置 Demo</h1>
<p className="text-xs text-slate-400">
统一树维护:菜单 → 模块 → 数据(实体) → 编码 · 字段/分组在实体面板内维护 · 对应 17 张表 · 命名风格 s_表 / b_字段
</p>
</div>
<div className="flex items-center gap-3">
<div className="hidden items-center gap-2 md:flex">
{stats.map((s) => (
<div key={s.label} className="rounded bg-slate-100 px-2 py-1 text-center" title={s.table}>
<div className="text-sm font-semibold text-slate-700">{s.value}</div>
<div className="text-[10px] text-slate-400">{s.label}</div>
</div>
))}
</div>
<button
onClick={resetAll}
className="rounded border border-slate-300 px-3 py-1.5 text-sm text-slate-600 hover:bg-slate-50"
>
重置演示数据
</button>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center rounded-lg bg-slate-100 p-0.5 text-sm">
<button
className={`px-3 py-1 rounded-md transition ${
previewMode === "form" ? "bg-white shadow text-slate-800" : "text-slate-500"
}`}
onClick={() => setPreviewMode("form")}
>
表单预览
</button>
<button
className={`px-3 py-1 rounded-md transition ${
previewMode === "table" ? "bg-white shadow text-slate-800" : "text-slate-500"
}`}
onClick={() => setPreviewMode("table")}
>
表格预览
</button>
</div>
{/* 视图切换 */}
<nav className="mx-auto flex max-w-7xl gap-1 px-4 pb-2">
<button
className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition"
onClick={() => {
setSavedTip(true);
setTimeout(() => setSavedTip(false), 1600);
}}
onClick={() => setView("tree")}
className={`rounded-t px-4 py-1.5 text-sm transition-colors ${
view === "tree"
? "border-b-2 border-sky-500 bg-sky-50 font-medium text-sky-700"
: "text-slate-500 hover:bg-slate-50 hover:text-slate-700"
}`}
>
保存
元数据维护(统一树)
<span className="ml-1.5 text-[10px] text-slate-400">菜单 → 模块 → 数据 → 编码</span>
</button>
</div>
<button
onClick={() => setView("permission")}
className={`rounded-t px-4 py-1.5 text-sm transition-colors ${
view === "permission"
? "border-b-2 border-sky-500 bg-sky-50 font-medium text-sky-700"
: "text-slate-500 hover:bg-slate-50 hover:text-slate-700"
}`}
>
权限
<span className="ml-1.5 text-[10px] text-slate-400">s_role / s_user_role / s_power / s_role_power</span>
</button>
</nav>
</header>
<main className="flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-[5fr_7fr] gap-4 p-4">
{/* 左栏:实时预览 */}
<section className="min-h-0 flex flex-col bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="shrink-0 px-4 py-2.5 border-b border-slate-100 flex items-center justify-between">
<span className="text-sm font-medium text-slate-700">
{previewMode === "form" ? "表单预览" : "表格预览"}
{readOnly && (
<span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-slate-100 text-slate-500">
只读
</span>
)}
</span>
<span className="text-xs text-slate-400">点击字段可在右侧配置</span>
</div>
<div className="flex-1 min-h-0 overflow-auto p-4 bg-slate-50">
{previewMode === "form" ? (
<FormPreview
fields={fields}
groups={groups}
activeId={activeId}
onActivate={setActiveId}
onReorder={reorderByDrag}
onGroupRename={() => {}}
readOnly={readOnly}
/>
) : (
<TablePreview
fields={fields}
activeId={activeId}
onActivate={setActiveId}
onReorder={reorderByDrag}
onResize={resizeField}
readOnly={readOnly}
/>
)}
</div>
</section>
{/* 右栏:模块设置 + 字段配置 */}
<section className="min-h-0 flex flex-col gap-4 overflow-auto pr-0.5">
<ModuleSettings editMode={editMode} onEditModeChange={setEditMode} />
<div className="card">
<FieldConfigList
fields={fields}
groups={groups}
activeId={activeId}
onActivate={setActiveId}
onAddField={addField}
onDeleteField={deleteField}
onUpdateFieldList={updateFieldList}
onReorder={reorderFields}
onOpenDataSource={openDataSource}
onRenameGroup={renameGroup}
onAddGroup={addGroup}
{/* 内容区 */}
<main key={resetKey} className="mx-auto max-w-7xl px-4 py-4">
{view === "tree" ? (
<div className="grid gap-4 lg:grid-cols-[340px_1fr]">
<MetaTree
modules={modules}
entities={entities}
menus={menus}
autocodes={autocodes}
selection={selection}
onSelect={setSelection}
/>
<div>
{selection ? (
<NodeEditor
key={`${selection.type}-${selection.id}`}
selection={selection}
modules={modules}
dependencies={dependencies}
entities={entities}
fields={fields}
groups={groups}
menus={menus}
autocodes={autocodes}
onChangeModules={setModules}
onChangeDependencies={setDependencies}
onChangeEntities={setEntities}
onChangeFields={setFields}
onChangeGroups={setGroups}
onChangeMenus={setMenus}
onChangeAutocodes={setAutocodes}
/>
) : (
<div className="rounded-lg border border-dashed border-slate-300 bg-white p-10 text-center text-sm text-slate-400">
请在左侧树中选择一个节点开始配置
<div className="mt-2 text-xs">
顺着导航维护:菜单 → 模块 → 数据(实体) → 编码 · 字段/分组在实体面板内维护
</div>
</div>
)}
</div>
</div>
</section>
) : (
<PermissionPanel
roles={roles}
userRoles={userRoles}
powers={powers}
rolePowers={rolePowers}
menus={menus}
onChangeRoles={setRoles}
onChangeUserRoles={setUserRoles}
onChangePowers={setPowers}
onChangeRolePowers={setRolePowers}
/>
)}
</main>
<DataSourceDrawer
field={dsField}
open={dsFieldId != null}
onClose={() => setDsFieldId(null)}
onUpdate={(ds: FieldDataSource | null) => {
if (dsFieldId != null) updateField(dsFieldId, "ds", ds);
}}
/>
{savedTip && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 px-4 py-2 rounded-lg bg-slate-800 text-white text-sm shadow-lg z-50">
已保存 ✔
</div>
)}
<footer className="mx-auto max-w-7xl px-4 pb-6 text-center text-xs text-slate-400">
演示环境:仅前端内存状态,改动不落库。生产环境由服务层校验后触发 Schema Compiler 同步 DDL。
</footer>
</div>
);
}