20260819213933
This commit is contained in:
1 parent
a0a4273f4c
commit
8e75c7989a
27 files changed
+3852
-2434
No files matched your search
+180
-196
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user