20260813173539
This commit is contained in:
1 parent
4bd730f649
commit
ab30604fe4
13 files changed
+1267
-555
No files matched your search
+191
-124
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user