diff --git a/src/components/DynamicForm/README.md b/src/components/DynamicForm/README.md new file mode 100644 index 0000000..1f092fb --- /dev/null +++ b/src/components/DynamicForm/README.md @@ -0,0 +1,258 @@ +# DynamicForm 动态表单组件 + +一个通用的动态表单组件,支持多种输入类型和灵活的配置。 + +## 功能特性 + +- 🎯 **多种输入类型**:支持数字、文本、选择器、日期、开关、单选框、复选框、滑块等 +- 🔧 **灵活配置**:支持自定义验证规则、占位符、禁用状态等 +- 📱 **响应式布局**:支持自定义列数和间距 +- 🎨 **自定义插槽**:支持自定义输入组件 +- 📊 **数据绑定**:支持 v-model 双向绑定 +- 🔍 **类型安全**:完整的 TypeScript 类型定义 + +## 基础用法 + +```vue + + + +``` + +## 支持的输入类型 + +### 1. 数字输入 (number) + +```javascript +createNumberItem('price', '价格', { + min: 0, + max: 10000, + precision: 2, + step: 0.01, + placeholder: '请输入价格' +}) +``` + +### 2. 文本输入 (text/textarea) + +```javascript +// 单行文本 +createTextItem('name', '姓名', { + placeholder: '请输入姓名', + required: true +}) + +// 多行文本 +createTextItem('description', '描述', { + rows: 4, + placeholder: '请输入描述' +}) +``` + +### 3. 选择器 (select) + +```javascript +createSelectItem('status', '状态', [ + { label: '启用', value: 'enabled' }, + { label: '禁用', value: 'disabled' } +], { + placeholder: '请选择状态', + filterable: true +}) +``` + +### 4. 日期选择 (date) + +```javascript +createDateItem('birthday', '生日', { + dateType: 'date', + format: 'YYYY-MM-DD', + valueFormat: 'YYYY-MM-DD' +}) +``` + +### 5. 开关 (switch) + +```javascript +createSwitchItem('enabled', '启用状态', { + activeText: '启用', + inactiveText: '禁用' +}) +``` + +### 6. 单选框组 (radio) + +```javascript +createRadioItem('level', '等级', [ + { label: '初级', value: 'beginner' }, + { label: '中级', value: 'intermediate' }, + { label: '高级', value: 'advanced' } +]) +``` + +### 7. 复选框组 (checkbox) + +```javascript +createCheckboxItem('hobbies', '爱好', [ + { label: '阅读', value: 'reading' }, + { label: '运动', value: 'sports' }, + { label: '音乐', value: 'music' } +]) +``` + +### 8. 滑块 (slider) + +```javascript +createSliderItem('volume', '音量', { + min: 0, + max: 100, + step: 1, + showInput: true +}) +``` + +## Props + +| 参数 | 说明 | 类型 | 默认值 | +|------|------|------|--------| +| modelValue | 表单数据 | `Record` | `{}` | +| items | 表单项配置 | `DynamicFormItem[]` | `[]` | +| title | 表单标题 | `string` | `''` | +| gutter | 列间距 | `number` | `16` | +| columnSpan | 列跨度 | `number` | `12` | +| formProp | 表单属性前缀 | `string` | `''` | + +## Events + +| 事件名 | 说明 | 参数 | +|--------|------|------| +| update:modelValue | 表单数据更新 | `(value: Record)` | +| fieldChange | 单个字段变化 | `(key: string, value: any, item: DynamicFormItem)` | +| formChange | 整个表单变化 | `(value: Record)` | + +## Methods + +| 方法名 | 说明 | 参数 | 返回值 | +|--------|------|------|--------| +| getFormData | 获取表单数据 | - | `Record` | +| setFormData | 设置表单数据 | `data: Record` | - | +| resetForm | 重置表单 | - | - | +| validateForm | 验证表单 | - | `boolean` | + +## 自定义插槽 + +```vue + + + +``` + +## 高级用法 + +### 条件显示 + +```javascript +const formItems = computed(() => { + const items = [ + createSelectItem('type', '类型', [ + { label: '类型A', value: 'typeA' }, + { label: '类型B', value: 'typeB' } + ]) + ] + + // 根据类型动态添加字段 + if (formData.value.type === 'typeA') { + items.push(createTextItem('fieldA', '字段A')) + } else if (formData.value.type === 'typeB') { + items.push(createNumberItem('fieldB', '字段B')) + } + + return items +}) +``` + +### 表单验证 + +```javascript +const formItems = [ + createTextItem('email', '邮箱', { + required: true, + rules: [ + { required: true, message: '请输入邮箱', trigger: 'blur' }, + { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' } + ] + }) +] +``` + +## 类型定义 + +```typescript +interface DynamicFormItem { + key: string + label: string + type: 'number' | 'text' | 'textarea' | 'select' | 'date' | 'time' | 'switch' | 'radio' | 'checkbox' | 'slider' | 'custom' + placeholder?: string + required?: boolean + disabled?: boolean + clearable?: boolean + rules?: any[] + // ... 更多配置项 +} +``` + +## 注意事项 + +1. 每个表单项的 `key` 必须唯一 +2. 选择器类型的表单项必须提供 `options` 配置 +3. 自定义插槽类型需要指定 `slotName` +4. 表单数据会自动同步到 `v-model` 绑定的变量 +5. 建议使用工具函数创建表单项,确保类型安全 diff --git a/src/components/DynamicForm/index.ts b/src/components/DynamicForm/index.ts new file mode 100644 index 0000000..0bc0116 --- /dev/null +++ b/src/components/DynamicForm/index.ts @@ -0,0 +1,48 @@ +import DynamicForm from './index.vue' +import type { + DynamicFormItem, + DynamicFormConfig, + DynamicFormEvents, + DynamicFormMethods +} from './types' +import { + createNumberItem, + createTextItem, + createSelectItem, + createDateItem, + createSwitchItem, + createRadioItem, + createCheckboxItem, + createSliderItem, + createCustomItem, + createDynamicFormConfig, + validateFormItem, + validateFormConfig +} from './utils' + +// 导出组件 +export default DynamicForm + +// 导出类型 +export type { + DynamicFormItem, + DynamicFormConfig, + DynamicFormEvents, + DynamicFormMethods +} + +// 导出工具函数 +export { + createNumberItem, + createTextItem, + createSelectItem, + createDateItem, + createSwitchItem, + createRadioItem, + createCheckboxItem, + createSliderItem, + createCustomItem, + createDynamicFormConfig, + validateFormItem, + validateFormConfig +} diff --git a/src/components/DynamicForm/index.vue b/src/components/DynamicForm/index.vue new file mode 100644 index 0000000..cf73405 --- /dev/null +++ b/src/components/DynamicForm/index.vue @@ -0,0 +1,322 @@ + + + + + diff --git a/src/components/DynamicForm/types.ts b/src/components/DynamicForm/types.ts new file mode 100644 index 0000000..ef7646d --- /dev/null +++ b/src/components/DynamicForm/types.ts @@ -0,0 +1,83 @@ +// 动态表单项目接口 +export interface DynamicFormItem { + key: string + label: string + type: 'number' | 'text' | 'textarea' | 'select' | 'date' | 'time' | 'switch' | 'radio' | 'checkbox' | 'slider' | 'custom' + placeholder?: string + required?: boolean + disabled?: boolean + clearable?: boolean + rules?: any[] + defaultValue?: any + + // 质检标准相关 + standard?: string + standardType?: 'text' | 'number' | 'range' | 'select' + standardValue?: any + standardRange?: { min: number; max: number } + standardOptions?: Array<{ label: string; value: any; disabled?: boolean }> + showStandard?: boolean + + // 数字输入相关 + min?: number + max?: number + precision?: number + step?: number + + // 文本输入相关 + inputType?: string + rows?: number + + // 选择器相关 + options?: Array<{ + label: string + value: any + disabled?: boolean + }> + filterable?: boolean + multiple?: boolean + + // 日期时间相关 + dateType?: 'date' | 'datetime' | 'daterange' | 'datetimerange' + format?: string + valueFormat?: string + + // 开关相关 + activeText?: string + inactiveText?: string + + // 滑块相关 + showInput?: boolean + showStops?: boolean + showTooltip?: boolean + + // 自定义插槽 + slotName?: string + + // 布局相关 + span?: number + fullWidth?: boolean +} + +// 动态表单配置接口 +export interface DynamicFormConfig { + title?: string + gutter?: number + columnSpan?: number + formProp?: string + items: DynamicFormItem[] +} + +// 动态表单事件接口 +export interface DynamicFormEvents { + fieldChange: (key: string, value: any, item: DynamicFormItem) => void + formChange: (value: Record) => void +} + +// 动态表单方法接口 +export interface DynamicFormMethods { + getFormData: () => Record + setFormData: (data: Record) => void + resetForm: () => void + validateForm: () => boolean +} diff --git a/src/components/DynamicForm/utils.ts b/src/components/DynamicForm/utils.ts new file mode 100644 index 0000000..24aacf0 --- /dev/null +++ b/src/components/DynamicForm/utils.ts @@ -0,0 +1,322 @@ +import type { DynamicFormItem, DynamicFormConfig } from './types' + +/** + * 创建数字输入表单项 + */ +export const createNumberItem = ( + key: string, + label: string, + options: { + min?: number + max?: number + precision?: number + step?: number + placeholder?: string + required?: boolean + disabled?: boolean + span?: number + fullWidth?: boolean + } = {} +): DynamicFormItem => { + return { + key, + label, + type: 'number', + min: options.min, + max: options.max, + precision: options.precision || 0, + step: options.step || 1, + placeholder: options.placeholder || `请输入${label}`, + required: options.required, + disabled: options.disabled, + span: options.span, + fullWidth: options.fullWidth + } +} + +/** + * 创建文本输入表单项 + */ +export const createTextItem = ( + key: string, + label: string, + options: { + inputType?: string + rows?: number + placeholder?: string + required?: boolean + disabled?: boolean + span?: number + fullWidth?: boolean + } = {} +): DynamicFormItem => { + return { + key, + label, + type: options.rows && options.rows > 1 ? 'textarea' : 'text', + inputType: options.inputType || 'text', + rows: options.rows, + placeholder: options.placeholder || `请输入${label}`, + required: options.required, + disabled: options.disabled, + span: options.span, + fullWidth: options.fullWidth + } +} + +/** + * 创建选择器表单项 + */ +export const createSelectItem = ( + key: string, + label: string, + options: Array<{ label: string; value: any; disabled?: boolean }>, + config: { + placeholder?: string + required?: boolean + disabled?: boolean + filterable?: boolean + multiple?: boolean + span?: number + fullWidth?: boolean + } = {} +): DynamicFormItem => { + return { + key, + label, + type: 'select', + options, + placeholder: config.placeholder || `请选择${label}`, + required: config.required, + disabled: config.disabled, + filterable: config.filterable, + multiple: config.multiple, + span: config.span, + fullWidth: config.fullWidth + } +} + +/** + * 创建日期选择表单项 + */ +export const createDateItem = ( + key: string, + label: string, + options: { + dateType?: 'date' | 'datetime' | 'daterange' | 'datetimerange' + format?: string + valueFormat?: string + placeholder?: string + required?: boolean + disabled?: boolean + span?: number + fullWidth?: boolean + } = {} +): DynamicFormItem => { + return { + key, + label, + type: 'date', + dateType: options.dateType || 'date', + format: options.format, + valueFormat: options.valueFormat, + placeholder: options.placeholder || `请选择${label}`, + required: options.required, + disabled: options.disabled, + span: options.span, + fullWidth: options.fullWidth + } +} + +/** + * 创建开关表单项 + */ +export const createSwitchItem = ( + key: string, + label: string, + options: { + activeText?: string + inactiveText?: string + required?: boolean + disabled?: boolean + span?: number + fullWidth?: boolean + } = {} +): DynamicFormItem => { + return { + key, + label, + type: 'switch', + activeText: options.activeText, + inactiveText: options.inactiveText, + required: options.required, + disabled: options.disabled, + span: options.span, + fullWidth: options.fullWidth + } +} + +/** + * 创建单选框组表单项 + */ +export const createRadioItem = ( + key: string, + label: string, + options: Array<{ label: string; value: any; disabled?: boolean }>, + config: { + required?: boolean + disabled?: boolean + span?: number + fullWidth?: boolean + } = {} +): DynamicFormItem => { + return { + key, + label, + type: 'radio', + options, + required: config.required, + disabled: config.disabled, + span: config.span, + fullWidth: config.fullWidth + } +} + +/** + * 创建复选框组表单项 + */ +export const createCheckboxItem = ( + key: string, + label: string, + options: Array<{ label: string; value: any; disabled?: boolean }>, + config: { + required?: boolean + disabled?: boolean + span?: number + fullWidth?: boolean + } = {} +): DynamicFormItem => { + return { + key, + label, + type: 'checkbox', + options, + required: config.required, + disabled: config.disabled, + span: config.span, + fullWidth: config.fullWidth + } +} + +/** + * 创建滑块表单项 + */ +export const createSliderItem = ( + key: string, + label: string, + options: { + min?: number + max?: number + step?: number + showInput?: boolean + showStops?: boolean + showTooltip?: boolean + required?: boolean + disabled?: boolean + span?: number + fullWidth?: boolean + } = {} +): DynamicFormItem => { + return { + key, + label, + type: 'slider', + min: options.min || 0, + max: options.max || 100, + step: options.step || 1, + showInput: options.showInput, + showStops: options.showStops, + showTooltip: options.showTooltip, + required: options.required, + disabled: options.disabled, + span: options.span, + fullWidth: options.fullWidth + } +} + +/** + * 创建自定义插槽表单项 + */ +export const createCustomItem = ( + key: string, + label: string, + slotName: string, + options: { + required?: boolean + disabled?: boolean + span?: number + fullWidth?: boolean + } = {} +): DynamicFormItem => { + return { + key, + label, + type: 'custom', + slotName, + required: options.required, + disabled: options.disabled, + span: options.span, + fullWidth: options.fullWidth + } +} + +/** + * 创建动态表单配置 + */ +export const createDynamicFormConfig = ( + items: DynamicFormItem[], + config: { + title?: string + gutter?: number + columnSpan?: number + formProp?: string + } = {} +): DynamicFormConfig => { + return { + title: config.title, + gutter: config.gutter || 16, + columnSpan: config.columnSpan || 12, + formProp: config.formProp, + items + } +} + +/** + * 验证表单项配置 + */ +export const validateFormItem = (item: DynamicFormItem): boolean => { + if (!item.key || !item.label || !item.type) { + console.error('表单项配置不完整:', item) + return false + } + + // 验证选择器类型必须有选项 + if (['select', 'radio', 'checkbox'].includes(item.type) && (!item.options || item.options.length === 0)) { + console.error('选择器类型表单项必须有选项:', item) + return false + } + + return true +} + +/** + * 验证表单配置 + */ +export const validateFormConfig = (config: DynamicFormConfig): boolean => { + if (!config.items || config.items.length === 0) { + console.error('表单配置必须有表单项') + return false + } + + return config.items.every(validateFormItem) +} diff --git a/src/components/FormBuilder/index.ts b/src/components/FormBuilder/index.ts new file mode 100644 index 0000000..c080ade --- /dev/null +++ b/src/components/FormBuilder/index.ts @@ -0,0 +1,45 @@ +import FormBuilder from './index.vue' +import type { + FormBuilderItem, + FormBuilderConfig, + FormBuilderEvents, + FormBuilderMethods +} from './types' +import { + createFormBuilderItem, + createFormBuilderConfig, + validateFormBuilderItem, + validateFormBuilderConfig, + generateDefaultItems, + importFromJson, + exportToJson, + duplicateItem, + createBatchItems +} from './utils' + +// 导出组件 +export default FormBuilder + +// 导出类型 +export type { + FormBuilderItem, + FormBuilderConfig, + FormBuilderEvents, + FormBuilderMethods +} + +// 导出工具函数 +export { + createFormBuilderItem, + createFormBuilderConfig, + validateFormBuilderItem, + validateFormBuilderConfig, + generateDefaultItems, + importFromJson, + exportToJson, + duplicateItem, + createBatchItems +} + + + diff --git a/src/components/FormBuilder/index.vue b/src/components/FormBuilder/index.vue new file mode 100644 index 0000000..d956270 --- /dev/null +++ b/src/components/FormBuilder/index.vue @@ -0,0 +1,706 @@ + + + + + diff --git a/src/components/FormBuilder/types.ts b/src/components/FormBuilder/types.ts new file mode 100644 index 0000000..6aa3dd9 --- /dev/null +++ b/src/components/FormBuilder/types.ts @@ -0,0 +1,82 @@ +// 表单字段类型 +export type FormFieldType = 'text' | 'number' | 'select' | 'textarea' | 'date' | 'switch' | 'radio' | 'checkbox' | 'custom' | 'time' | 'slider' + +// 表单字段选项 +export interface FormFieldOption { + label: string + value: any + disabled?: boolean +} + +// 表单构建器项目接口 +export interface FormBuilderItem { + id: string + label: string + key: string + type: FormFieldType + defaultValue: any + placeholder?: string + required?: boolean + disabled?: boolean + // 质检标准相关 + standard?: string // 质检标准描述 + standardType?: 'text' | 'number' | 'range' | 'select' // 标准类型 + standardValue?: any // 标准值 + standardRange?: { min: number; max: number } // 标准范围 + standardOptions?: FormFieldOption[] // 标准选项 + showStandard?: boolean // 是否显示标准 + // 选择器相关 + options?: FormFieldOption[] + // 数字输入相关 + min?: number + max?: number + step?: number + precision?: number + // 文本输入相关 + rows?: number + maxlength?: number + // 日期相关 + dateType?: 'date' | 'datetime' | 'daterange' | 'datetimerange' + format?: string + valueFormat?: string + // 开关相关 + activeText?: string + inactiveText?: string + // 自定义验证规则 + rules?: any[] +} + +// 表单构建器配置接口 +export interface FormBuilderConfig { + title?: string + items: FormBuilderItem[] + // 使用场景配置 + mode?: 'simple' | 'advanced' | 'custom' + // 是否显示预览 + showPreview?: boolean + // 是否允许拖拽排序 + allowDrag?: boolean + // 自定义字段类型 + customFieldTypes?: FormFieldType[] +} + +// 表单构建器事件接口 +export interface FormBuilderEvents { + change: (items: FormBuilderItem[]) => void + itemAdd: (item: FormBuilderItem) => void + itemRemove: (item: FormBuilderItem) => void + itemUpdate: (item: FormBuilderItem, index: number) => void +} + +// 表单构建器方法接口 +export interface FormBuilderMethods { + getFormItems: () => FormBuilderItem[] + setFormItems: (items: FormBuilderItem[]) => void + clearItems: () => void + addItemWithData: (label: string, key: string, type?: FormFieldType, defaultValue?: any) => void + exportConfig: () => FormBuilderConfig + importConfig: (config: FormBuilderConfig) => void +} + + + diff --git a/src/components/FormBuilder/utils.ts b/src/components/FormBuilder/utils.ts new file mode 100644 index 0000000..19c6b61 --- /dev/null +++ b/src/components/FormBuilder/utils.ts @@ -0,0 +1,147 @@ +import type { FormBuilderItem, FormBuilderConfig } from './types' + +/** + * 创建表单构建器项目 + */ +export const createFormBuilderItem = ( + label: string, + key: string, + defaultValue: string = '', + id?: string +): FormBuilderItem => { + return { + id: id || `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + label, + key, + defaultValue + } +} + +/** + * 创建表单构建器配置 + */ +export const createFormBuilderConfig = ( + items: FormBuilderItem[], + title: string = '表单构建器' +): FormBuilderConfig => { + return { + title, + items + } +} + +/** + * 验证表单构建器项目 + */ +export const validateFormBuilderItem = (item: FormBuilderItem): boolean => { + if (!item.id || !item.label || !item.key) { + console.error('表单构建器项目配置不完整:', item) + return false + } + + // 验证键值格式 + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(item.key)) { + console.error('键值格式不正确,只能包含字母、数字和下划线,且不能以数字开头:', item.key) + return false + } + + return true +} + +/** + * 验证表单构建器配置 + */ +export const validateFormBuilderConfig = (config: FormBuilderConfig): boolean => { + if (!config.items || config.items.length === 0) { + console.error('表单构建器配置必须有表单项') + return false + } + + // 检查键值是否重复 + const keys = config.items.map(item => item.key) + const uniqueKeys = new Set(keys) + if (keys.length !== uniqueKeys.size) { + console.error('表单项的键值不能重复') + return false + } + + return config.items.every(validateFormBuilderItem) +} + +/** + * 生成默认的表单构建器项目 + */ +export const generateDefaultItems = (count: number = 3): FormBuilderItem[] => { + const items: FormBuilderItem[] = [] + + for (let i = 1; i <= count; i++) { + items.push(createFormBuilderItem( + `项目${i}`, + `item_${i}`, + '' + )) + } + + return items +} + +/** + * 从JSON数据导入表单构建器项目 + */ +export const importFromJson = (jsonData: any[]): FormBuilderItem[] => { + const items: FormBuilderItem[] = [] + + jsonData.forEach((item, index) => { + if (typeof item === 'object' && item !== null) { + items.push(createFormBuilderItem( + item.label || `项目${index + 1}`, + item.key || `item_${index + 1}`, + item.defaultValue || '', + item.id + )) + } + }) + + return items +} + +/** + * 导出表单构建器项目为JSON + */ +export const exportToJson = (items: FormBuilderItem[]): string => { + return JSON.stringify(items, null, 2) +} + +/** + * 复制表单构建器项目 + */ +export const duplicateItem = (item: FormBuilderItem): FormBuilderItem => { + return createFormBuilderItem( + `${item.label}_副本`, + `${item.key}_copy`, + item.defaultValue + ) +} + +/** + * 批量创建表单构建器项目 + */ +export const createBatchItems = ( + labels: string[], + keys?: string[], + defaultValues?: string[] +): FormBuilderItem[] => { + const items: FormBuilderItem[] = [] + + labels.forEach((label, index) => { + const key = keys?.[index] || `item_${index + 1}` + const defaultValue = defaultValues?.[index] || '' + + items.push(createFormBuilderItem(label, key, defaultValue)) + }) + + return items +} + + + diff --git a/src/views/erp/purchase/evaluation/SupplierScoreDashboard.vue b/src/views/erp/purchase/evaluation/SupplierScoreDashboard.vue index cfc1305..6d72d6f 100644 --- a/src/views/erp/purchase/evaluation/SupplierScoreDashboard.vue +++ b/src/views/erp/purchase/evaluation/SupplierScoreDashboard.vue @@ -3,7 +3,11 @@
- + 刷新
@@ -88,7 +92,7 @@
- 不及格 (<6) + 不及格 (<6) {{ failCount }} 个