diff --git a/src/components/DynamicForm.vue b/src/components/DynamicForm.vue index 94a41cd..586e4e1 100644 --- a/src/components/DynamicForm.vue +++ b/src/components/DynamicForm.vue @@ -1,8 +1,9 @@ + + + + \ No newline at end of file diff --git a/src/components/designer/DesignerCanvas.vue b/src/components/designer/DesignerCanvas.vue new file mode 100644 index 0000000..c2a5eed --- /dev/null +++ b/src/components/designer/DesignerCanvas.vue @@ -0,0 +1,230 @@ + + + + + \ No newline at end of file diff --git a/src/components/designer/FieldPropPanel.vue b/src/components/designer/FieldPropPanel.vue new file mode 100644 index 0000000..c07429f --- /dev/null +++ b/src/components/designer/FieldPropPanel.vue @@ -0,0 +1,237 @@ + + + + + \ No newline at end of file diff --git a/src/router/routes.ts b/src/router/routes.ts index 3880cb3..cd9ad56 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -36,6 +36,12 @@ const routes: RouteRecordRaw[] = [ component: () => import("@/views/svc/ServiceFormManage.vue"), meta: { title: "表单管理", modId: 25 }, }, + { + path: "/svc/form/designer", + name: "svc_form_designer", + component: () => import("@/views/svc/FormDesigner.vue"), + meta: { title: "表单设计器", modId: 25, parentId: 25 }, + }, // 管理面:办件记录 { path: "/svc/apply", diff --git a/src/types/designer.ts b/src/types/designer.ts new file mode 100644 index 0000000..1311c5e --- /dev/null +++ b/src/types/designer.ts @@ -0,0 +1,70 @@ +/** + * 表单设计器类型与控件字典(迁移自旧 FormManage,Vue3 版)。 + * 契约对齐后端 FormValidator:不迁移 address/user/users 三类。 + */ + +/** 设计器组件库控件字典(左栏渲染源) */ +export interface DesignerTypeItem { + type: string; + name: string; + ftype: "base" | "enhance"; +} + +export const DESIGNER_TYPES: DesignerTypeItem[] = [ + { type: "text", name: "单行文本", ftype: "base" }, + { type: "textarea", name: "多行文本", ftype: "base" }, + { type: "number", name: "数字", ftype: "base" }, + { type: "date", name: "日期", ftype: "base" }, + { type: "select", name: "下拉", ftype: "base" }, + { type: "radio", name: "单选", ftype: "base" }, + { type: "checkbox", name: "多选", ftype: "base" }, + { type: "multiselect", name: "下拉多选", ftype: "base" }, + { type: "phone", name: "手机", ftype: "enhance" }, + { type: "upload", name: "附件", ftype: "enhance" }, + { type: "image", name: "图片", ftype: "enhance" }, +]; + +/** 需要 options 的控件类型(属性面板也要展示选项编辑) */ +export const OPTION_TYPES = ["select", "radio", "checkbox", "multiselect"]; + +/** 输入提示类控件(属性面板展示 placeholder) */ +export const PLACEHOLDER_TYPES = ["text", "textarea", "number", "date", "select", "radio", "checkbox", "multiselect", "phone"]; + +/** 上传类控件(图片/附件) */ +export const FILE_TYPES = ["upload", "image"]; + +/** 编辑态字段(_uid 为内部拖拽/渲染 key,序列化时剔除) */ +export interface DesignField { + _uid: string; + key: string; + label: string; + type: string; + required?: boolean; + pattern?: string; + min?: number; + max?: number; + maxLength?: number; + tip?: string; + placeholder?: string; + options?: string[]; + rows?: number; + defaultValue?: any; + accept?: string; + maxSize?: number; + maxFiles?: number; +} + +export interface DesignGroup { + _uid: string; + title: string; + fields: DesignField[]; +} + +/** 后端契约属性白名单(与 FormValidator.ALLOWED_PROPS 同步),序列化只能输出这些键 */ +export const ALLOWED_FIELD_PROPS = [ + "key", "label", "type", "required", "pattern", "min", "max", "maxLength", + "tip", "placeholder", "options", "rows", "defaultValue", "accept", "maxSize", "maxFiles", +] as const; + +/** 契约 key 规则(与后端 KEY_PATTERN 同步) */ +export const KEY_PATTERN = /^[A-Za-z0-9_]{1,64}$/; \ No newline at end of file diff --git a/src/utils/designer/schema.ts b/src/utils/designer/schema.ts new file mode 100644 index 0000000..56513ee --- /dev/null +++ b/src/utils/designer/schema.ts @@ -0,0 +1,142 @@ +/** + * 设计器 schema 工具:字段默认值生成、key 自动去重、白名单序列化、编辑态还原。 + * 纪律:产出对象只含契约白名单属性(ALLOWED_FIELD_PROPS),不会有 precision 等非法键。 + */ +import type { DesignField, DesignGroup } from "@/types/designer"; +import { ALLOWED_FIELD_PROPS } from "@/types/designer"; + +let uidSeq = 0; + +/** 内部唯一 id(拖拽/渲染 key,非契约字段) */ +export function genUid(): string { + uidSeq += 1; + return `uid_${Date.now().toString(36)}_${uidSeq}`; +} + +/** 控件类型 → key 前缀 */ +const TYPE_KEY_BASE: Record = { + text: "text", + textarea: "textarea", + number: "number", + date: "date", + select: "select", + radio: "radio", + checkbox: "check", + multiselect: "multi", + phone: "phone", + upload: "file", + image: "img", +}; + +/** key 自动生成器(全表单唯一;用于拖入/复制时分配新 key) */ +export function makeNextKey(used: Set): (type: string) => string { + return (type: string) => { + const base = TYPE_KEY_BASE[type] ?? "field"; + let n = 1; + let k = `${base}_${n}`; + while (used.has(k)) { + n += 1; + k = `${base}_${n}`; + } + used.add(k); + return k; + }; +} + +export const IMAGE_ACCEPT = ".jpg,.jpeg,.png,.gif"; +export const FILE_ACCEPT = ".doc,.docx,.xls,.xlsx,.pdf,.ppt,.pptx,.txt,.zip,.rar,.jpg,.jpeg,.png,.gif"; + +/** 控件默认显示名(点击添加时作为 label) */ +function defaultLabel(type: string): string { + const map: Record = { + text: "单行文本", + textarea: "多行文本", + number: "数字", + date: "日期", + select: "下拉", + radio: "单选", + checkbox: "多选", + multiselect: "下拉多选", + phone: "手机", + upload: "附件", + image: "图片", + }; + return map[type] ?? type; +} + +/** 生成字段默认配置。maxSize 单位为 MB(与 DynamicForm 渲染器一致)。 */ +export function defaultField(type: string, nextKey: (t: string) => string, label?: string): DesignField { + const f: DesignField = { _uid: genUid(), key: nextKey(type), label: label || defaultLabel(type), type }; + switch (type) { + case "textarea": + f.rows = 5; + f.maxLength = 1000; + f.placeholder = "请输入"; + break; + case "select": + case "multiselect": + f.options = ["选项1", "选项2", "选项3"]; + f.placeholder = "请选择"; + break; + case "radio": + case "checkbox": + f.options = ["选项1", "选项2", "选项3"]; + break; + case "phone": + f.placeholder = "请输入手机号"; + f.tip = "请输入 1 开头的 11 位手机号"; + f.pattern = "^1[3-9]\\d{9}$"; + f.maxLength = 11; + break; + case "upload": + f.accept = FILE_ACCEPT; + f.maxSize = 20; + break; + case "image": + f.accept = IMAGE_ACCEPT; + f.maxSize = 10; + break; + default: + break; + } + return f; +} + +/** 序列化字段:仅输出契约白名单属性,剔除内部 _uid 与空值 */ +export function pickAllowed(f: DesignField): Record { + const o: Record = {}; + for (const k of ALLOWED_FIELD_PROPS) { + const v = (f as Record)[k]; + if (v === undefined || v === null) continue; + // 空串必须剔除:`pattern:""` 到后端是「能编译的空正则」,会把所有非空值判成格式不正确 + if (typeof v === "string" && v.trim() === "") continue; + o[k] = v; + } + return o; +} + +/** 从后端 schema 的 groups 还原为编辑态(补 _uid) */ +export function restoreGroups(groups: Record[]): DesignGroup[] { + return (groups || []).map((g) => ({ + _uid: genUid(), + title: g.title || "", + fields: ((g.fields as Record[]) || []).map((raw) => ({ + ...(raw as DesignField), + _uid: genUid(), + })), + })); +} + +/** 收集全表单已占用的 key(校验 + key 自动生成) */ +export function collectUsedKeys(groups: DesignGroup[]): Set { + const set = new Set(); + for (const g of groups) { + for (const f of g.fields || []) set.add(f.key); + } + return set; +} + +/** 全表单字段总数 */ +export function countFields(groups: DesignGroup[]): number { + return (groups || []).reduce((n, g) => n + (g.fields || []).length, 0); +} \ No newline at end of file diff --git a/src/views/svc/FormDesigner.vue b/src/views/svc/FormDesigner.vue new file mode 100644 index 0000000..5a5e0e4 --- /dev/null +++ b/src/views/svc/FormDesigner.vue @@ -0,0 +1,324 @@ + + + + + \ No newline at end of file diff --git a/src/views/svc/ServiceFormManage.vue b/src/views/svc/ServiceFormManage.vue index 6b5386c..54804b9 100644 --- a/src/views/svc/ServiceFormManage.vue +++ b/src/views/svc/ServiceFormManage.vue @@ -1,27 +1,21 @@