20260813173539

This commit is contained in:
oneao committed 2026-08-13 17:35:39 +08:00
1 parent 4bd730f649
commit ab30604fe4
13 files changed
+1267 -555

No files matched your search

+191 -124
View File
@@ -3,149 +3,216 @@ 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";
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 [dsDrawerFieldId, setDsDrawerFieldId] = useState<number | null>(null);
const [dsFieldId, setDsFieldId] = useState<number | null>(null);
const [savedTip, setSavedTip] = useState(false);
const nextGroupId = useRef(10);
const dsDrawerField = dsDrawerFieldId != null ? fields.find((f) => f.id === dsDrawerFieldId) ?? null : null;
// ======== Field mutations ========
const updateField = useCallback((id: number, key: string, value: string | number) => {
setFields((prev) => prev.map((f) => (f.id === id ? { ...f, [key]: value } : f)));
}, []);
const toggleField = useCallback((id: number, key: string) => {
setFields((prev) => prev.map((f) => (f.id === id ? { ...f, [key]: !(f as any)[key] } : f)));
}, []);
const onTypeChange = useCallback((id: number, newType: string) => {
setFields((prev) =>
prev.map((f) => {
if (f.id !== id) return f;
let ds = f.ds;
if (newType === "select" && !ds) {
ds = { type: "view", view: "", vf: "b_id", lf: "b_name" };
}
if (newType !== "select") ds = null;
return { ...f, type: newType, ds };
})
);
}, []);
const addField = useCallback(() => {
const firstGroup = groups[0]?.id ?? null;
const order = (fields.length + 1) * 10;
const f = makeField(firstGroup, "input", "新字段");
f.order = order;
setFields((prev) => [...prev, f]);
setActiveId(f.id);
}, [fields.length, groups]);
const deleteField = useCallback(
(id: number) => {
const f = fields.find((f) => f.id === id);
if (!f || !window.confirm(`删除「${f.label}」(${f.code})?`)) return;
setFields((prev) => prev.filter((f) => f.id !== id));
if (activeId === id) setActiveId(null);
},
[fields, activeId]
);
// ======== Drag reorder ========
const reorderFields = useCallback((activeId: number, overId: number) => {
setFields((prev) => {
const src = prev.find((f) => f.id === activeId);
const tgt = prev.find((f) => f.id === overId);
if (!src || !tgt) return prev;
const tmp = src.order;
return prev.map((f) => {
if (f.id === activeId) return { ...f, order: tgt.order };
if (f.id === overId) return { ...f, order: tmp };
return f;
});
});
}, []);
// ======== Data source ========
const updateDs = useCallback((ds: FieldDataSource | null) => {
if (dsDrawerFieldId == null) return;
setFields((prev) => prev.map((f) => (f.id === dsDrawerFieldId ? { ...f, ds } : f)));
}, [dsDrawerFieldId]);
// ======== Group ========
const addGroup = useCallback(() => {
const name = window.prompt("分组名称:");
if (!name) return;
const id = `g${nextGroupId.current++}`;
setGroups((prev) => [...prev, { id, name }]);
}, []);
const renameGroup = useCallback((id: string) => {
const g = groups.find((g) => g.id === id);
if (!g) return;
const name = window.prompt("新名称:", g.name);
if (name) {
setGroups((prev) => prev.map((g) => (g.id === id ? { ...g, name } : g)));
}
}, [groups]);
const activateField = useCallback(
(id: number) => {
setActiveId((prev) => (prev === id ? null : id));
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 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";
return (
<div className="min-h-screen flex flex-col">
<div className="flex-1 flex flex-col max-w-[1280px] mx-auto w-full p-5 gap-4">
{/* Module Settings */}
<ModuleSettings editMode={editMode} onEditModeChange={setEditMode} />
{/* Grouped field table (single column) */}
<div className="card flex flex-col min-h-0 flex-1">
<div className="flex items-center justify-between px-4 py-2.5 border-b border-slate-100 shrink-0">
<span className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
字段配置 <span className="text-slate-300 font-normal ml-1">{fields.length}</span>
</span>
<div className="flex items-center gap-1.5">
<button className="btn btn-primary text-[10px] py-1 px-2.5" onClick={addField}>
+ 新增
</button>
<button className="btn btn-ghost text-[10px] py-1 px-2" onClick={addGroup}>
+ 分组
</button>
</div>
<div className="h-screen flex flex-col overflow-hidden">
{/* 顶栏 */}
<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>
</div>
<FieldConfigList
fields={fields}
groups={groups}
activeId={activeId}
onActivate={activateField}
onToggle={toggleField}
onUpdate={updateField}
onTypeChange={onTypeChange}
onReorder={reorderFields}
onGroupRename={renameGroup}
onDelete={deleteField}
onOpenDs={setDsDrawerFieldId}
/>
</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>
<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);
}}
>
保存
</button>
</div>
</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}
/>
</div>
</section>
</main>
{/* Data Source Drawer */}
<DataSourceDrawer
field={dsDrawerField}
open={dsDrawerFieldId != null}
onClose={() => setDsDrawerFieldId(null)}
onUpdate={updateDs}
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>
)}
</div>
);
}