20260820173056

This commit is contained in:
oneao committed 2026-08-20 17:30:56 +08:00
1 parent 8e75c7989a
commit 68ac5db038
24 files changed
+975 -6035

No files matched your search

+251 -190
View File
@@ -1,202 +1,263 @@
// 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 { useMemo, useState, type ComponentType, type ReactNode } from "react";
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";
Braces,
ChevronDown,
CircleHelp,
Database,
FileCode2,
FolderTree,
Languages,
List,
MoreHorizontal,
Plus,
Search,
Settings2,
ShieldCheck,
Table2,
Workflow,
X,
} from "lucide-react";
import "./index.css";
type NodeType = "directory" | "page" | "data";
type ModuleNode = {
id: number;
name: string;
code: string;
type: NodeType;
parent: number | null;
route?: string;
component?: string;
view?: string;
save?: string;
key?: string;
dataNodeId?: number;
enabled: boolean;
showInMenu: boolean;
};
type Field = {
id: number;
code: string;
name: string;
type: string;
defaultValue: string;
enabled: boolean;
};
const initialNodes: ModuleNode[] = [
{ id: 1, name: "海运业务", code: "ocean", type: "directory", parent: null, enabled: true, showInMenu: true },
{ id: 2, name: "订舱管理", code: "booking", type: "directory", parent: 1, enabled: true, showInMenu: true },
{ id: 3, name: "订舱列表", code: "booking_list", type: "page", parent: 2, route: "/ocean/booking", component: "ocean/booking/index.vue", dataNodeId: 4, enabled: true, showInMenu: true },
{ id: 4, name: "订舱数据", code: "booking_data", type: "data", parent: 2, view: "v_ocean_booking_list", save: "ocean_booking", key: "b_id", enabled: true, showInMenu: false },
{ id: 5, name: "订舱审核", code: "booking_audit", type: "page", parent: 2, route: "/ocean/booking/audit", component: "ocean/booking/audit.vue", dataNodeId: 4, enabled: true, showInMenu: true },
{ id: 6, name: "集装箱明细", code: "booking_container", type: "data", parent: 2, view: "ocean_booking_container", save: "ocean_booking_container", key: "b_id", enabled: true, showInMenu: false },
{ id: 7, name: "航线报价", code: "rate_query", type: "page", parent: 1, route: "/ocean/rate/query", component: "ocean/rate/index.vue", enabled: true, showInMenu: true },
{ id: 8, name: "基础资料", code: "master_data", type: "directory", parent: null, enabled: true, showInMenu: true },
{ id: 9, name: "客户档案", code: "customer", type: "page", parent: 8, route: "/base/customer", component: "base/customer/index.vue", dataNodeId: 10, enabled: true, showInMenu: true },
{ id: 10, name: "客户数据", code: "customer_data", type: "data", parent: 8, view: "base_customer", save: "base_customer", key: "b_id", enabled: true, showInMenu: false },
];
const initialFields: Field[] = [
{ id: 1, code: "b_ywno", name: "业务单号", type: "input", defaultValue: "自动编码", enabled: true },
{ id: 2, code: "b_customer_id", name: "委托客户", type: "select", defaultValue: "", enabled: true },
{ id: 3, code: "b_ywtype", name: "业务类型", type: "select", defaultValue: "整箱出口", enabled: true },
{ id: 4, code: "b_sales_id", name: "业务员", type: "select", defaultValue: "当前用户", enabled: true },
{ id: 5, code: "b_loadport_id", name: "起运港", type: "select", defaultValue: "", enabled: true },
{ id: 6, code: "b_disport_id", name: "目的港", type: "select", defaultValue: "", enabled: true },
{ id: 7, code: "b_etd", name: "预计开船日", type: "date", defaultValue: "", enabled: true },
{ id: 8, code: "b_eta", name: "预计到港日", type: "date", defaultValue: "", enabled: true },
{ id: 9, code: "b_freight", name: "运费总额", type: "money", defaultValue: "费用汇总", enabled: true },
{ id: 10, code: "b_state", name: "单据状态", type: "select", defaultValue: "草稿", enabled: true },
];
const labels: Record<string, string> = {
basic: "基本信息",
fields: "字段定义",
list: "列表配置",
edit: "编辑配置",
query: "查询配置",
autocode: "自动编码",
power: "权限配置",
i18n: "多语言",
};
const typeLabels: Record<NodeType, string> = { directory: "目录", page: "页面", data: "数据" };
const fieldTypes = [
["input", "输入框"], ["textarea", "文本域"], ["number", "数字"], ["money", "金额"],
["date", "日期"], ["datetime", "日期时间"], ["select", "下拉选择"],
];
const sectionIcons: Record<string, ComponentType<{ size?: number }>> = {
basic: Settings2,
fields: Table2,
list: List,
edit: Workflow,
query: Search,
autocode: Braces,
power: ShieldCheck,
i18n: Languages,
};
function Toggle({ value, onChange }: { value: boolean; onChange: () => void }) {
return <button className={`toggle ${value ? "on" : ""}`} onClick={onChange} type="button" aria-label={value ? "关闭" : "开启"}><i /></button>;
}
function PanelHeader({ title, desc, action }: { title: string; desc: string; action?: ReactNode }) {
return <div className="panel-header"><div><h2>{title}</h2><p>{desc}</p></div>{action}</div>;
}
export default function App() {
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 [nodes, setNodes] = useState(initialNodes);
const [fields, setFields] = useState(initialFields);
const [selectedId, setSelectedId] = useState(3);
const [section, setSection] = useState("basic");
const [treeSearch, setTreeSearch] = useState("");
const [dirty, setDirty] = useState(false);
const [addOpen, setAddOpen] = useState(false);
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 selected = nodes.find((node) => node.id === selectedId) ?? nodes[0];
const filteredNodes = useMemo(() => {
const keyword = treeSearch.trim().toLowerCase();
if (!keyword) return nodes;
const matches = new Set(nodes.filter((node) => `${node.name}${node.code}`.toLowerCase().includes(keyword)).map((node) => node.id));
nodes.forEach((node) => { if (matches.has(node.id) && node.parent) matches.add(node.parent); });
return nodes.filter((node) => matches.has(node.id));
}, [nodes, treeSearch]);
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);
const sections = selected.type === "data"
? ["basic", "fields", "list", "edit", "query", "autocode", "i18n"]
: selected.type === "page" ? ["basic", "power", "i18n"] : ["basic", "i18n"];
const selectNode = (node: ModuleNode) => { setSelectedId(node.id); setSection("basic"); };
const updateSelected = (patch: Partial<ModuleNode>) => {
setNodes((items) => items.map((node) => node.id === selected.id ? { ...node, ...patch } : node));
setDirty(true);
};
const updateField = (id: number, patch: Partial<Field>) => {
setFields((items) => items.map((field) => field.id === id ? { ...field, ...patch } : field));
setDirty(true);
};
return (
<div className="min-h-screen bg-slate-100 text-slate-800">
{/* 顶栏 */}
<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>
return <div className="app">
<header className="topbar">
<div className="brand"><span className="logo"><FolderTree size={17} /></span><div><b>FMS 模块管理</b><small>菜单、页面与数据配置</small></div></div>
<div className="top-module"><span>当前系统</span><b>国际货运管理系统</b></div>
<div className="top-actions"><span className={`save-state ${dirty ? "dirty" : ""}`}><i />{dirty ? "有未保存修改" : "所有修改已保存"}</span><button className="save" disabled={!dirty} onClick={() => setDirty(false)}>保存修改</button><span className="avatar">林</span></div>
</header>
<div className="layout">
<aside className="tree-panel">
<div className="tree-title">
<div><b>模块与菜单</b><small>按实际菜单层级组织</small></div>
<div className="add-wrap"><button className="icon-btn" title="添加节点" onClick={() => setAddOpen(!addOpen)}><Plus size={16} /></button>{addOpen && <div className="add-menu"><button><FolderTree size={14} />新建目录</button><button><FileCode2 size={14} />新建页面</button><button><Database size={14} />新建数据</button></div>}</div>
</div>
{/* 视图切换 */}
<nav className="mx-auto flex max-w-7xl gap-1 px-4 pb-2">
<button
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>
<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>
<label className="tree-search"><Search size={14} /><input value={treeSearch} onChange={(event) => setTreeSearch(event.target.value)} placeholder="搜索菜单、页面或编码" />{treeSearch && <button onClick={() => setTreeSearch("")}><X size={13} /></button>}</label>
<ModuleTree nodes={filteredNodes} selectedId={selectedId} onSelect={selectNode} />
<div className="tree-legend"><span><i className="dot directory" />目录</span><span><i className="dot page" />页面</span><span><i className="dot data" />数据</span></div>
<div className="tree-help"><CircleHelp size={14} /><span>页面由开发人员编写;这里维护菜单、路由以及页面所使用的数据配置。</span></div>
</aside>
{/* 内容区 */}
<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>
) : (
<PermissionPanel
roles={roles}
userRoles={userRoles}
powers={powers}
rolePowers={rolePowers}
menus={menus}
onChangeRoles={setRoles}
onChangeUserRoles={setUserRoles}
onChangePowers={setPowers}
onChangeRolePowers={setRolePowers}
/>
)}
<main className="detail">
<div className="detail-head">
<div><div className="crumb">{getPath(nodes, selected).join(" / ")}</div><div className="title-line"><h1>{selected.name}</h1><span className={`node-badge ${selected.type}`}>{typeLabels[selected.type]}</span><code>{selected.code}</code></div></div>
<div className="head-actions"><label>启用 <Toggle value={selected.enabled} onChange={() => updateSelected({ enabled: !selected.enabled })} /></label>{selected.type !== "data" && <label>显示菜单 <Toggle value={selected.showInMenu} onChange={() => updateSelected({ showInMenu: !selected.showInMenu })} /></label>}<button className="primary" disabled={!dirty} onClick={() => setDirty(false)}>保存</button><button className="icon-btn more-button" title="更多操作"><MoreHorizontal size={16} /></button></div>
</div>
<div className="detail-body">
<nav className="section-nav">{sections.map((key) => { const SectionIcon = sectionIcons[key]; return <button key={key} className={section === key ? "active" : ""} onClick={() => setSection(key)}><SectionIcon size={15} />{labels[key]}</button>; })}</nav>
<section className="content">
{section === "basic" && <BasicPanel node={selected} nodes={nodes} onChange={updateSelected} onSelectNode={selectNode} />}
{section === "fields" && <FieldsPanel fields={fields} update={updateField} />}
{section === "list" && <ListPanel fields={fields} />}
{section === "edit" && <EditPanel fields={fields} />}
{section === "query" && <QueryPanel fields={fields} />}
{section === "autocode" && <AutoCodePanel />}
{section === "power" && <PowerPanel />}
{section === "i18n" && <I18nPanel node={selected} fields={fields} />}
</section>
</div>
</main>
<footer className="mx-auto max-w-7xl px-4 pb-6 text-center text-xs text-slate-400">
演示环境:仅前端内存状态,改动不落库。生产环境由服务层校验后触发 Schema Compiler 同步 DDL。
</footer>
</div>
);
</div>;
}
function getPath(nodes: ModuleNode[], selected: ModuleNode) {
const path = [selected.name];
let parent = nodes.find((node) => node.id === selected.parent);
while (parent) { path.unshift(parent.name); parent = nodes.find((node) => node.id === parent?.parent); }
return path;
}
function ModuleTree({ nodes, selectedId, onSelect }: { nodes: ModuleNode[]; selectedId: number; onSelect: (node: ModuleNode) => void }) {
const children = (parent: number | null) => nodes.filter((node) => node.parent === parent);
const render = (parent: number | null, depth = 0): ReactNode[] => children(parent).flatMap((node) => [
<button key={node.id} className={`tree-row ${selectedId === node.id ? "selected" : ""}`} style={{ paddingLeft: 10 + depth * 17 }} onClick={() => onSelect(node)}>
<span className={`tree-icon ${node.type}`}>{node.type === "directory" ? <ChevronDown size={14} /> : node.type === "page" ? <FileCode2 size={14} /> : <Database size={14} />}</span>
<span className="tree-name">{node.name}<small>{node.code}</small></span>
<span className="tree-type">{typeLabels[node.type]}</span><MoreHorizontal className="row-more" size={14} />
</button>,
...render(node.id, depth + 1),
]);
return <div className="module-tree">{render(null)}</div>;
}
function BasicPanel({ node, nodes, onChange, onSelectNode }: { node: ModuleNode; nodes: ModuleNode[]; onChange: (patch: Partial<ModuleNode>) => void; onSelectNode: (node: ModuleNode) => void }) {
const dataNodes = nodes.filter((item) => item.type === "data");
const relatedData = dataNodes.find((item) => item.id === node.dataNodeId);
return <div className="panel">
<PanelHeader title="基本信息" desc="当前节点最常用、最需要确认的配置。" />
<div className="form-grid">
<label>节点名称<input value={node.name} onChange={(event) => onChange({ name: event.target.value })} /></label>
<label>节点编码<input value={node.code} onChange={(event) => onChange({ code: event.target.value })} /></label>
<label>节点类型<select value={node.type} disabled><option value="directory">目录</option><option value="page">页面</option><option value="data">数据</option></select></label>
<label>上级节点<input value={node.parent ? getPath(nodes, nodes.find((item) => item.id === node.parent)!).join(" / ") : "顶级目录"} readOnly /></label>
{node.type === "page" && <>
<label>页面路由<input value={node.route ?? ""} onChange={(event) => onChange({ route: event.target.value })} placeholder="/ocean/booking" /></label>
<label>前端组件<input value={node.component ?? ""} onChange={(event) => onChange({ component: event.target.value })} placeholder="ocean/booking/index.vue" /></label>
<label className="related-field">关联数据节点<select value={node.dataNodeId ?? ""} onChange={(event) => onChange({ dataNodeId: Number(event.target.value) || undefined })}><option value="">不关联</option>{dataNodes.map((data) => <option key={data.id} value={data.id}>{data.name} ({data.code})</option>)}</select>{relatedData && <button className="link-button" onClick={() => onSelectNode(relatedData)}>查看数据配置</button>}</label>
<label>菜单图标<select defaultValue="clipboard-list"><option value="clipboard-list">订舱单</option><option value="ship">海运</option><option value="search">查询</option></select></label>
</>}
{node.type === "data" && <>
<label>查询视图<input value={node.view ?? ""} onChange={(event) => onChange({ view: event.target.value })} /></label>
<label>保存表<input value={node.save ?? ""} onChange={(event) => onChange({ save: event.target.value })} /></label>
<label>主键字段<input value={node.key ?? ""} onChange={(event) => onChange({ key: event.target.value })} /></label>
<label>默认排序<input defaultValue="b_inputdatetime desc" /></label>
</>}
</div>
<div className="info-strip"><CircleHelp size={14} /><span>{node.type === "page" ? "这里只登记已开发页面的入口与关联数据,不会生成或改写页面代码。" : node.type === "data" ? "数据节点集中维护字段、列表、表单和查询规则,可被一个或多个页面使用。" : "目录与用户实际看到的菜单层级保持一致。"}</span></div>
</div>;
}
function FieldsPanel({ fields, update }: { fields: Field[]; update: (id: number, patch: Partial<Field>) => void }) {
return <div className="panel"><PanelHeader title="字段定义" desc="从查询视图同步字段后,维护业务名称与控件类型。" action={<button className="outline">同步字段</button>} /><div className="data-table"><div className="table-head fields"><span>字段名</span><span>显示名称</span><span>控件类型</span><span>默认值</span><span>启用</span></div>{fields.map((field) => <div className="table-row fields" key={field.id}><code>{field.code}</code><input value={field.name} onChange={(event) => update(field.id, { name: event.target.value })} /><select value={field.type} onChange={(event) => update(field.id, { type: event.target.value })}>{fieldTypes.map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select><input value={field.defaultValue} onChange={(event) => update(field.id, { defaultValue: event.target.value })} /><Toggle value={field.enabled} onChange={() => update(field.id, { enabled: !field.enabled })} /></div>)}</div></div>;
}
function ListPanel({ fields }: { fields: Field[] }) {
return <ConfigTable title="列表配置" desc="决定已开发表格默认显示的列、顺序和宽度。" heads={["字段", "显示名称", "显示", "固定", "宽度"]}>{fields.slice(0, 8).map((field, index) => <div className="table-row config" key={field.id}><code>{field.code}</code><span>{field.name}</span><Toggle value={index < 7} onChange={() => undefined} /><select defaultValue={index === 0 ? "left" : "none"}><option value="none">不固定</option><option value="left">左侧</option><option value="right">右侧</option></select><input defaultValue={index === 0 ? "150" : "120"} /></div>)}</ConfigTable>;
}
function EditPanel({ fields }: { fields: Field[] }) {
return <ConfigTable title="编辑配置" desc="决定已开发表单中的字段规则与布局分组。" heads={["字段", "显示名称", "必填", "只读", "表单分组"]}>{fields.slice(0, 8).map((field, index) => <div className="table-row config" key={field.id}><code>{field.code}</code><span>{field.name}</span><Toggle value={[0, 1, 2, 4, 5].includes(index)} onChange={() => undefined} /><Toggle value={index === 0} onChange={() => undefined} /><select defaultValue={index > 5 ? "运输信息" : "基本信息"}><option>基本信息</option><option>运输信息</option><option>费用信息</option></select></div>)}</ConfigTable>;
}
function QueryPanel({ fields }: { fields: Field[] }) {
return <ConfigTable title="查询配置" desc="售后人员可直接增加条件或调整查询方式。" action={<button className="primary"><Plus size={13} />新增条件</button>} heads={["查询字段", "查询组件", "操作符", "默认值", "启用"]}>{fields.filter((field) => field.code !== "b_freight").slice(0, 7).map((field, index) => <div className="table-row config" key={field.id}><span><b>{field.name}</b><code>{field.code}</code></span><select defaultValue={field.type === "date" ? "日期范围" : field.type === "select" ? "下拉选择" : "输入框"}><option>输入框</option><option>下拉选择</option><option>日期范围</option></select><select defaultValue={field.type === "date" ? "范围" : index === 0 ? "包含" : "等于"}><option>等于</option><option>包含</option><option>范围</option></select><input placeholder="无" /><Toggle value onChange={() => undefined} /></div>)}</ConfigTable>;
}
function ConfigTable({ title, desc, action, heads, children }: { title: string; desc: string; action?: ReactNode; heads: string[]; children: ReactNode }) {
return <div className="panel"><PanelHeader title={title} desc={desc} action={action} /><div className="data-table"><div className="table-head config">{heads.map((head) => <span key={head}>{head}</span>)}</div>{children}</div></div>;
}
function AutoCodePanel() {
return <div className="panel"><PanelHeader title="自动编码" desc="配置业务单号等字段的生成规则。" /><div className="rule-summary"><div><span>目标字段</span><b>b_ywno · 业务单号</b></div><div><span>示例结果</span><code>NBSE2608200001</code></div><Toggle value onChange={() => undefined} /></div><div className="rule-builder"><div className="rule-step"><i>1</i><select defaultValue="固定文本"><option>固定文本</option></select><input defaultValue="NBSE" /><button><MoreHorizontal size={15} /></button></div><div className="rule-step"><i>2</i><select defaultValue="当前日期"><option>当前日期</option></select><select defaultValue="yyMMdd"><option>yyMMdd</option><option>yyyyMMdd</option></select><button><MoreHorizontal size={15} /></button></div><div className="rule-step"><i>3</i><select defaultValue="流水号"><option>流水号</option></select><div className="inline-inputs"><input defaultValue="4" /><span>位,每日重置</span></div><button><MoreHorizontal size={15} /></button></div><button className="add-rule"><Plus size={14} />添加规则段</button></div></div>;
}
function PowerPanel() {
const powers = [["查看订舱", "booking.view"], ["新增订舱", "booking.create"], ["编辑订舱", "booking.update"], ["审核订舱", "booking.audit"], ["导出订舱", "booking.export"]];
return <div className="panel"><PanelHeader title="权限配置" desc="维护页面已有操作对应的权限标识。" action={<button className="primary"><Plus size={13} />新增权限</button>} /><div className="power-list">{powers.map(([name, code], index) => <div key={code}><ShieldCheck size={17} /><span><b>{name}</b><small>{code}</small></span><Toggle value={index !== 3} onChange={() => undefined} /><button className="more"><MoreHorizontal size={16} /></button></div>)}</div></div>;
}
function I18nPanel({ node, fields }: { node: ModuleNode; fields: Field[] }) {
return <div className="panel"><PanelHeader title="多语言" desc="维护菜单、节点和字段名称,不涉及页面代码。" action={<button className="outline">批量翻译</button>} /><div className="i18n-table"><div className="table-head i18n"><span>资源键</span><span>简体中文</span><span>English</span><span>日本語</span></div><div className="table-row i18n"><code>module.{node.code}</code><input defaultValue={node.name} /><input defaultValue="Ocean Booking" /><input defaultValue="海上予約" /></div>{fields.slice(0, 5).map((field) => <div className="table-row i18n" key={field.id}><code>field.{node.code}.{field.code}</code><input defaultValue={field.name} /><input defaultValue="" placeholder="待填写" /><input defaultValue="" placeholder="待填写" /></div>)}</div></div>;
}