打包问题的修复,以及质量模块的合并
This commit is contained in:
258
src/components/DynamicForm/README.md
Normal file
258
src/components/DynamicForm/README.md
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# DynamicForm 动态表单组件
|
||||||
|
|
||||||
|
一个通用的动态表单组件,支持多种输入类型和灵活的配置。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 🎯 **多种输入类型**:支持数字、文本、选择器、日期、开关、单选框、复选框、滑块等
|
||||||
|
- 🔧 **灵活配置**:支持自定义验证规则、占位符、禁用状态等
|
||||||
|
- 📱 **响应式布局**:支持自定义列数和间距
|
||||||
|
- 🎨 **自定义插槽**:支持自定义输入组件
|
||||||
|
- 📊 **数据绑定**:支持 v-model 双向绑定
|
||||||
|
- 🔍 **类型安全**:完整的 TypeScript 类型定义
|
||||||
|
|
||||||
|
## 基础用法
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<DynamicForm
|
||||||
|
v-model="formData"
|
||||||
|
:items="formItems"
|
||||||
|
title="用户信息"
|
||||||
|
@form-change="handleFormChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import DynamicForm, {
|
||||||
|
createNumberItem,
|
||||||
|
createTextItem,
|
||||||
|
createSelectItem
|
||||||
|
} from '@/components/DynamicForm'
|
||||||
|
|
||||||
|
const formData = ref({})
|
||||||
|
|
||||||
|
const formItems = [
|
||||||
|
createTextItem('name', '姓名', { required: true }),
|
||||||
|
createNumberItem('age', '年龄', { min: 0, max: 120 }),
|
||||||
|
createSelectItem('gender', '性别', [
|
||||||
|
{ label: '男', value: 'male' },
|
||||||
|
{ label: '女', value: 'female' }
|
||||||
|
])
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleFormChange = (value) => {
|
||||||
|
console.log('表单数据变化:', value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 支持的输入类型
|
||||||
|
|
||||||
|
### 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<string, any>` | `{}` |
|
||||||
|
| items | 表单项配置 | `DynamicFormItem[]` | `[]` |
|
||||||
|
| title | 表单标题 | `string` | `''` |
|
||||||
|
| gutter | 列间距 | `number` | `16` |
|
||||||
|
| columnSpan | 列跨度 | `number` | `12` |
|
||||||
|
| formProp | 表单属性前缀 | `string` | `''` |
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
| 事件名 | 说明 | 参数 |
|
||||||
|
|--------|------|------|
|
||||||
|
| update:modelValue | 表单数据更新 | `(value: Record<string, any>)` |
|
||||||
|
| fieldChange | 单个字段变化 | `(key: string, value: any, item: DynamicFormItem)` |
|
||||||
|
| formChange | 整个表单变化 | `(value: Record<string, any>)` |
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
| 方法名 | 说明 | 参数 | 返回值 |
|
||||||
|
|--------|------|------|--------|
|
||||||
|
| getFormData | 获取表单数据 | - | `Record<string, any>` |
|
||||||
|
| setFormData | 设置表单数据 | `data: Record<string, any>` | - |
|
||||||
|
| resetForm | 重置表单 | - | - |
|
||||||
|
| validateForm | 验证表单 | - | `boolean` |
|
||||||
|
|
||||||
|
## 自定义插槽
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<DynamicForm v-model="formData" :items="formItems">
|
||||||
|
<template #customField="{ item, value, onChange }">
|
||||||
|
<el-input
|
||||||
|
v-model="value"
|
||||||
|
@input="onChange"
|
||||||
|
:placeholder="item.placeholder"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DynamicForm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const formItems = [
|
||||||
|
{
|
||||||
|
key: 'customField',
|
||||||
|
label: '自定义字段',
|
||||||
|
type: 'custom',
|
||||||
|
slotName: 'customField',
|
||||||
|
placeholder: '请输入自定义内容'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 条件显示
|
||||||
|
|
||||||
|
```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. 建议使用工具函数创建表单项,确保类型安全
|
||||||
48
src/components/DynamicForm/index.ts
Normal file
48
src/components/DynamicForm/index.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
322
src/components/DynamicForm/index.vue
Normal file
322
src/components/DynamicForm/index.vue
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dynamic-form">
|
||||||
|
<!-- 表单标题 -->
|
||||||
|
<el-divider v-if="title" content-position="left">{{ title }}</el-divider>
|
||||||
|
|
||||||
|
<!-- 动态表单项目 -->
|
||||||
|
<el-row :gutter="gutter">
|
||||||
|
<el-col v-for="item in formItems" :key="item.key" :span="getColumnSpan(item)">
|
||||||
|
<el-form-item :label="item.label" :required="item.required">
|
||||||
|
<div class="form-field-container">
|
||||||
|
<!-- 输入控件区域 -->
|
||||||
|
<div class="form-field-input" :class="{ 'with-standard': item.showStandard && item.standard }">
|
||||||
|
<!-- 数字输入 -->
|
||||||
|
<el-input-number v-if="item.type === 'number'" v-model="formData[item.key]" :min="item.min"
|
||||||
|
:max="item.max" :precision="item.precision || 0" :step="item.step || 1" :placeholder="item.placeholder"
|
||||||
|
:disabled="item.disabled" :clearable="item.clearable !== false" style="width: 100%"
|
||||||
|
@change="handleFieldChange(item.key, $event)" />
|
||||||
|
|
||||||
|
<!-- 文本输入 -->
|
||||||
|
<el-input v-else-if="item.type === 'text'" v-model="formData[item.key]" :type="item.inputType || 'text'"
|
||||||
|
:placeholder="item.placeholder" :disabled="item.disabled" :clearable="item.clearable !== false"
|
||||||
|
:rows="item.rows || 1" :maxlength="item.maxlength" :show-word-limit="!!item.maxlength"
|
||||||
|
@input="handleFieldChange(item.key, $event)" />
|
||||||
|
|
||||||
|
<!-- 多行文本 -->
|
||||||
|
<el-input v-else-if="item.type === 'textarea'" v-model="formData[item.key]" type="textarea"
|
||||||
|
:placeholder="item.placeholder" :disabled="item.disabled" :clearable="item.clearable !== false"
|
||||||
|
:rows="item.rows || 3" @input="handleFieldChange(item.key, $event)" />
|
||||||
|
|
||||||
|
<!-- 选择器 -->
|
||||||
|
<el-select v-else-if="item.type === 'select'" v-model="formData[item.key]" :placeholder="item.placeholder"
|
||||||
|
:disabled="item.disabled" :clearable="item.clearable !== false" :filterable="item.filterable"
|
||||||
|
:multiple="item.multiple" style="width: 100%" @change="handleFieldChange(item.key, $event)">
|
||||||
|
<el-option v-for="option in item.options" :key="option.value" :label="option.label"
|
||||||
|
:value="option.value" :disabled="option.disabled" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<!-- 日期选择 -->
|
||||||
|
<el-date-picker v-else-if="item.type === 'date'" v-model="formData[item.key]"
|
||||||
|
:type="item.dateType || 'date'" :placeholder="item.placeholder" :disabled="item.disabled"
|
||||||
|
:clearable="item.clearable !== false" :format="item.format" :value-format="item.valueFormat"
|
||||||
|
style="width: 100%" @change="handleFieldChange(item.key, $event)" />
|
||||||
|
|
||||||
|
<!-- 时间选择 -->
|
||||||
|
<el-time-picker v-else-if="item.type === 'time'" v-model="formData[item.key]"
|
||||||
|
:placeholder="item.placeholder" :disabled="item.disabled" :clearable="item.clearable !== false"
|
||||||
|
:format="item.format || 'HH:mm:ss'" :value-format="item.valueFormat || 'HH:mm:ss'" style="width: 100%"
|
||||||
|
@change="handleFieldChange(item.key, $event)" />
|
||||||
|
|
||||||
|
<!-- 开关 -->
|
||||||
|
<el-switch v-else-if="item.type === 'switch'" v-model="formData[item.key]" :disabled="item.disabled"
|
||||||
|
:active-text="item.activeText" :inactive-text="item.inactiveText"
|
||||||
|
@change="handleFieldChange(item.key, $event)" />
|
||||||
|
|
||||||
|
<!-- 单选框组 -->
|
||||||
|
<el-radio-group v-else-if="item.type === 'radio'" v-model="formData[item.key]" :disabled="item.disabled"
|
||||||
|
@change="handleFieldChange(item.key, $event)">
|
||||||
|
<el-radio v-for="option in item.options" :key="option.value" :value="option.value"
|
||||||
|
:disabled="option.disabled">
|
||||||
|
{{ option.label }}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
|
||||||
|
<!-- 复选框组 -->
|
||||||
|
<el-checkbox-group v-else-if="item.type === 'checkbox'" v-model="formData[item.key]"
|
||||||
|
:disabled="item.disabled" @change="handleFieldChange(item.key, $event)">
|
||||||
|
<el-checkbox v-for="option in item.options" :key="option.value" :value="option.value"
|
||||||
|
:disabled="option.disabled">
|
||||||
|
{{ option.label }}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
|
||||||
|
<!-- 滑块 -->
|
||||||
|
<el-slider v-else-if="item.type === 'slider'" v-model="formData[item.key]" :min="item.min || 0"
|
||||||
|
:max="item.max || 100" :step="item.step || 1" :disabled="item.disabled" :show-input="item.showInput"
|
||||||
|
:show-stops="item.showStops" :show-tooltip="item.showTooltip"
|
||||||
|
@change="handleFieldChange(item.key, $event)" />
|
||||||
|
|
||||||
|
<!-- 自定义插槽 -->
|
||||||
|
<slot v-else-if="item.type === 'custom'" :name="item.slotName || item.key" :item="item"
|
||||||
|
:value="formData[item.key]" :onChange="(value) => handleFieldChange(item.key, value)"></slot>
|
||||||
|
|
||||||
|
<!-- 未知类型提示 -->
|
||||||
|
<div v-else class="unknown-type">
|
||||||
|
<el-alert :title="`未知的表单类型: ${item.type}`" type="warning" :closable="false" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 质检标准显示区域 -->
|
||||||
|
<div
|
||||||
|
v-if="item.showStandard && (item.standard || item.standardValue || item.standardRange || item.standardOptions?.length)"
|
||||||
|
class="form-field-standard">
|
||||||
|
<el-tooltip :content="getStandardTooltip(item)" placement="top">
|
||||||
|
<div class="standard-display">
|
||||||
|
<Icon icon="ep:info-filled" class="standard-icon" />
|
||||||
|
<span class="standard-text">{{ getStandardDisplayText(item) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
import type { DynamicFormItem } from './types'
|
||||||
|
|
||||||
|
// 定义Props
|
||||||
|
interface Props {
|
||||||
|
modelValue?: Record<string, any>
|
||||||
|
items?: DynamicFormItem[]
|
||||||
|
title?: string
|
||||||
|
gutter?: number
|
||||||
|
columnSpan?: number
|
||||||
|
formProp?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义Emits
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: Record<string, any>): void
|
||||||
|
(e: 'fieldChange', key: string, value: any, item: DynamicFormItem): void
|
||||||
|
(e: 'formChange', value: Record<string, any>): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: () => ({}),
|
||||||
|
items: () => [],
|
||||||
|
title: '',
|
||||||
|
gutter: 16,
|
||||||
|
columnSpan: 12,
|
||||||
|
formProp: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = ref<Record<string, any>>({ ...props.modelValue })
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const formItems = computed(() => props.items)
|
||||||
|
|
||||||
|
// 获取列跨度
|
||||||
|
const getColumnSpan = (item: DynamicFormItem) => {
|
||||||
|
if (item.fullWidth) return 24
|
||||||
|
return item.span || props.columnSpan
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 处理字段变化
|
||||||
|
const handleFieldChange = (key: string, value: any) => {
|
||||||
|
formData.value[key] = value
|
||||||
|
|
||||||
|
// 查找对应的表单项配置
|
||||||
|
const item = formItems.value.find(item => item.key === key)
|
||||||
|
|
||||||
|
// 触发事件
|
||||||
|
emit('fieldChange', key, value, item!)
|
||||||
|
emit('update:modelValue', { ...formData.value })
|
||||||
|
emit('formChange', { ...formData.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标准显示文本(只显示标准值)
|
||||||
|
const getStandardDisplayText = (item: DynamicFormItem): string => {
|
||||||
|
// 如果没有标准信息,不显示
|
||||||
|
if (!item.standardValue && !item.standardRange && !item.standardOptions?.length) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据标准类型显示标准值
|
||||||
|
switch (item.standardType) {
|
||||||
|
case 'text':
|
||||||
|
return item.standardValue || ''
|
||||||
|
case 'number':
|
||||||
|
return item.standardValue !== undefined ? String(item.standardValue) : ''
|
||||||
|
case 'range':
|
||||||
|
if (item.standardRange) {
|
||||||
|
const { min, max } = item.standardRange
|
||||||
|
// 如果只有最小值(max为null、undefined或0)
|
||||||
|
if (min !== undefined && min !== null && (max === undefined || max === null || max === 0)) {
|
||||||
|
return `≥${min}`
|
||||||
|
}
|
||||||
|
// 如果只有最大值(min为null、undefined或0)
|
||||||
|
if ((min === undefined || min === null || min === 0) && max !== undefined && max !== null) {
|
||||||
|
return `≤${max}`
|
||||||
|
}
|
||||||
|
// 如果有范围(两个值都有且不为null/undefined/0)
|
||||||
|
if (min !== undefined && min !== null && max !== undefined && max !== null && min !== 0 && max !== 0) {
|
||||||
|
return `${min}~${max}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
case 'select':
|
||||||
|
return item.standardOptions?.length ? item.standardOptions.map(opt => opt.label).join(', ') : ''
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标准提示内容(只显示标准描述)
|
||||||
|
const getStandardTooltip = (item: DynamicFormItem): string => {
|
||||||
|
// 只显示标准描述
|
||||||
|
return item.standard || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听外部数据变化
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
formData.value = { ...newValue }
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
const getFormData = () => {
|
||||||
|
return { ...formData.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFormData = (data: Record<string, any>) => {
|
||||||
|
formData.value = { ...data }
|
||||||
|
emit('update:modelValue', { ...formData.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {}
|
||||||
|
emit('update:modelValue', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
// 这里可以添加表单验证逻辑
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getFormData,
|
||||||
|
setFormData,
|
||||||
|
resetForm,
|
||||||
|
validateForm
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dynamic-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unknown-type {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-input.with-standard {
|
||||||
|
flex: 0 0 calc(100% - 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-standard {
|
||||||
|
flex: 0 0 180px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standard-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standard-display:hover {
|
||||||
|
background: #e0f2fe;
|
||||||
|
border-color: #7dd3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standard-icon {
|
||||||
|
color: #0ea5e9;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standard-text {
|
||||||
|
color: #0369a1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-field-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-input.with-standard {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-standard {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
83
src/components/DynamicForm/types.ts
Normal file
83
src/components/DynamicForm/types.ts
Normal file
@@ -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<string, any>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态表单方法接口
|
||||||
|
export interface DynamicFormMethods {
|
||||||
|
getFormData: () => Record<string, any>
|
||||||
|
setFormData: (data: Record<string, any>) => void
|
||||||
|
resetForm: () => void
|
||||||
|
validateForm: () => boolean
|
||||||
|
}
|
||||||
322
src/components/DynamicForm/utils.ts
Normal file
322
src/components/DynamicForm/utils.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
45
src/components/FormBuilder/index.ts
Normal file
45
src/components/FormBuilder/index.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
706
src/components/FormBuilder/index.vue
Normal file
706
src/components/FormBuilder/index.vue
Normal file
@@ -0,0 +1,706 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-builder">
|
||||||
|
<!-- 表单构建器标题 -->
|
||||||
|
<div class="form-builder-header">
|
||||||
|
<h4>{{ title }}</h4>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-select v-model="selectedFieldType" placeholder="选择字段类型" style="width: 120px; margin-right: 8px;">
|
||||||
|
<el-option v-for="type in availableFieldTypes" :key="type" :label="getFieldTypeLabel(type)" :value="type" />
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" @click="() => addItem(selectedFieldType)">
|
||||||
|
<Icon icon="ep:plus" />
|
||||||
|
添加项目
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表单项列表 -->
|
||||||
|
<div class="form-items-list">
|
||||||
|
<div v-for="(item, index) in formItems" :key="item.id" class="form-item-card">
|
||||||
|
<div class="form-item-header">
|
||||||
|
<span class="item-index">第 {{ index + 1 }} 个质检项目</span>
|
||||||
|
<div class="item-actions">
|
||||||
|
<el-button type="primary" size="small" @click="moveUp(index)" :disabled="index === 0">
|
||||||
|
<Icon icon="ep:arrow-up" />
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" size="small" @click="moveDown(index)" :disabled="index === formItems.length - 1">
|
||||||
|
<Icon icon="ep:arrow-down" />
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" size="small" @click="removeItem(index)">
|
||||||
|
<Icon icon="ep:delete" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item-content">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="项目名称" :prop="`items.${index}.label`">
|
||||||
|
<el-input v-model="item.label" placeholder="请输入项目标题" @input="handleItemChange" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="项目key(建议保持默认)" :prop="`items.${index}.key`">
|
||||||
|
<el-input v-model="item.key" placeholder="请输入项目键值" @input="handleItemChange" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-form-item label="输入类型" :prop="`items.${index}.type`">
|
||||||
|
<el-select v-model="item.type" placeholder="选择类型" @change="handleTypeChange(index, $event)">
|
||||||
|
<el-option v-for="type in availableFieldTypes" :key="type" :label="getFieldTypeLabel(type)"
|
||||||
|
:value="type" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="默认值" :prop="`items.${index}.defaultValue`">
|
||||||
|
<!-- 根据字段类型显示不同的输入控件 -->
|
||||||
|
<el-input v-if="item.type === 'text' || item.type === 'textarea'" v-model="item.defaultValue"
|
||||||
|
:type="item.type === 'textarea' ? 'textarea' : 'text'" :rows="item.type === 'textarea' ? 2 : 1"
|
||||||
|
placeholder="请输入默认值" @input="handleItemChange" />
|
||||||
|
<el-input-number v-else-if="item.type === 'number'" v-model="item.defaultValue" placeholder="请输入数字"
|
||||||
|
@change="handleItemChange" style="width: 100%" />
|
||||||
|
<el-select v-else-if="item.type === 'select'" v-model="item.defaultValue" placeholder="请选择默认值"
|
||||||
|
@change="handleItemChange" style="width: 100%">
|
||||||
|
<el-option label="选项1" value="option1" />
|
||||||
|
<el-option label="选项2" value="option2" />
|
||||||
|
</el-select>
|
||||||
|
<el-switch v-else-if="item.type === 'switch'" v-model="item.defaultValue" @change="handleItemChange" />
|
||||||
|
<el-date-picker v-else-if="item.type === 'date'" v-model="item.defaultValue" type="date"
|
||||||
|
placeholder="选择日期" @change="handleItemChange" style="width: 100%" />
|
||||||
|
<el-input v-else v-model="item.defaultValue" placeholder="请输入默认值" @input="handleItemChange" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- text类型特殊配置 -->
|
||||||
|
<el-row :gutter="16" v-if="item.type === 'text'" style="margin-top: 16px; padding: 16px; background: #f0f9ff; border-radius: 8px; border: 1px solid #e0f2fe;">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 12px;">
|
||||||
|
<Icon icon="ep:document-copy" style="color: #0ea5e9; margin-right: 8px;" />
|
||||||
|
<span style="font-weight: 600; color: #0f172a;">文本字段配置</span>
|
||||||
|
<el-tooltip content="为文本字段设置显示行数和最大长度限制" placement="top">
|
||||||
|
<Icon icon="ep:question-filled" style="color: #64748b; margin-left: 8px; cursor: help;" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="显示行数">
|
||||||
|
<el-input-number v-model="item.rows" :min="1" :max="10" placeholder="行数" @change="handleItemChange" style="width: 100%" />
|
||||||
|
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">
|
||||||
|
设置文本框的显示行数(1-10行)
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="最大长度">
|
||||||
|
<el-input-number v-model="item.maxlength" :min="1" :max="10000" placeholder="字符数" @change="handleItemChange" style="width: 100%" />
|
||||||
|
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">
|
||||||
|
设置允许输入的最大字符数
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 质检标准配置 -->
|
||||||
|
<el-row :gutter="16"
|
||||||
|
style="margin-top: 16px; padding: 16px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 12px;">
|
||||||
|
<Icon icon="ep:info-filled" style="color: #409eff; margin-right: 8px;" />
|
||||||
|
<span style="font-weight: 600; color: #303133;">质检标准配置</span>
|
||||||
|
<el-tooltip content="为这个字段设置质检标准,帮助质检员了解检验要求" placement="top">
|
||||||
|
<Icon icon="ep:question-filled" style="color: #909399; margin-left: 8px; cursor: help;" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="标准描述" :required="true">
|
||||||
|
<el-input v-model="item.standard" placeholder="例如:重量必须符合标准要求" @input="handleItemChange" />
|
||||||
|
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
|
||||||
|
描述这个字段的质检标准要求
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-form-item label="标准类型">
|
||||||
|
<div style="padding: 8px 12px; background: #f5f7fa; border-radius: 4px; color: #606266; font-size: 14px;">
|
||||||
|
{{ getStandardTypeLabel(item.standardType) }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
|
||||||
|
根据字段类型自动设置
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item :label="getStandardValueLabel(item.standardType)">
|
||||||
|
<el-input v-if="item.standardType === 'text'" v-model="item.standardValue" placeholder="例如:合格"
|
||||||
|
@input="handleItemChange" />
|
||||||
|
<div v-else-if="item.standardType === 'range'" style="display: flex; gap: 8px;">
|
||||||
|
<el-input-number v-model="item.standardRange!.min" placeholder="最小值" @change="handleItemChange"
|
||||||
|
style="flex: 1" />
|
||||||
|
<span style="line-height: 32px;">~</span>
|
||||||
|
<el-input-number v-model="item.standardRange!.max" placeholder="最大值" @change="handleItemChange"
|
||||||
|
style="flex: 1" />
|
||||||
|
</div>
|
||||||
|
<el-button v-else-if="item.standardType === 'select'" type="primary" size="small" @click="editStandardOptions(index)">
|
||||||
|
<Icon icon="ep:setting" />
|
||||||
|
编辑选项
|
||||||
|
</el-button>
|
||||||
|
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
|
||||||
|
{{ getStandardValueHint(item.standardType) }}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="3">
|
||||||
|
<el-form-item label="显示标准">
|
||||||
|
<el-switch v-model="item.showStandard" @change="handleItemChange" />
|
||||||
|
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
|
||||||
|
在表单中显示标准
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="3">
|
||||||
|
<el-form-item label="必填">
|
||||||
|
<el-switch v-model="item.required" @change="handleItemChange" />
|
||||||
|
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
|
||||||
|
此字段是否必填
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="formItems.length === 0" class="empty-state">
|
||||||
|
<el-empty description="暂无表单项,点击上方按钮添加">
|
||||||
|
<el-button type="primary" @click="() => addItem()">
|
||||||
|
<Icon icon="ep:plus" />
|
||||||
|
添加第一个项目
|
||||||
|
</el-button>
|
||||||
|
</el-empty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 预览区域 -->
|
||||||
|
<div v-if="formItems.length > 0" class="preview-section">
|
||||||
|
<el-divider content-position="left">预览效果</el-divider>
|
||||||
|
<div class="preview-content">
|
||||||
|
<DynamicForm v-model="previewData" :items="dynamicFormItems" :gutter="16" :column-span="12"
|
||||||
|
@form-change="handlePreviewChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import DynamicForm, { type DynamicFormItem } from '@/components/DynamicForm'
|
||||||
|
import type { FormBuilderItem, FormFieldType } from './types'
|
||||||
|
|
||||||
|
// 定义Props
|
||||||
|
interface Props {
|
||||||
|
modelValue?: FormBuilderItem[]
|
||||||
|
title?: string
|
||||||
|
mode?: 'simple' | 'advanced' | 'custom'
|
||||||
|
showPreview?: boolean
|
||||||
|
allowDrag?: boolean
|
||||||
|
customFieldTypes?: FormFieldType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义Emits
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: FormBuilderItem[]): void
|
||||||
|
(e: 'change', value: FormBuilderItem[]): void
|
||||||
|
(e: 'preview-change', value: Record<string, any>): void
|
||||||
|
(e: 'itemAdd', item: FormBuilderItem): void
|
||||||
|
(e: 'itemRemove', item: FormBuilderItem): void
|
||||||
|
(e: 'itemUpdate', item: FormBuilderItem, index: number): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: () => [],
|
||||||
|
title: '表单构建器',
|
||||||
|
mode: 'simple',
|
||||||
|
showPreview: true,
|
||||||
|
allowDrag: true,
|
||||||
|
customFieldTypes: () => ['text', 'number', 'select']
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formItems = ref<FormBuilderItem[]>([...props.modelValue])
|
||||||
|
const previewData = ref<Record<string, any>>({})
|
||||||
|
const selectedFieldType = ref<FormFieldType>('text')
|
||||||
|
|
||||||
|
// 初始化时确保标准字段有默认值
|
||||||
|
formItems.value.forEach(item => {
|
||||||
|
if (!item.standard) item.standard = ''
|
||||||
|
if (!item.standardType) item.standardType = 'text'
|
||||||
|
if (!item.standardValue) item.standardValue = ''
|
||||||
|
if (!item.standardRange) item.standardRange = { min: 0, max: 100 }
|
||||||
|
if (!item.standardOptions) item.standardOptions = []
|
||||||
|
if (item.showStandard === undefined) item.showStandard = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 可用的字段类型
|
||||||
|
const availableFieldTypes = computed(() => {
|
||||||
|
return props.customFieldTypes || ['text', 'number', 'select']
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取字段类型标签
|
||||||
|
const getFieldTypeLabel = (type: FormFieldType): string => {
|
||||||
|
const labels: Record<FormFieldType, string> = {
|
||||||
|
text: '文本',
|
||||||
|
number: '数字',
|
||||||
|
select: '选择器',
|
||||||
|
textarea: '多行文本',
|
||||||
|
date: '日期',
|
||||||
|
switch: '开关',
|
||||||
|
radio: '单选框',
|
||||||
|
checkbox: '复选框',
|
||||||
|
custom: '自定义',
|
||||||
|
time: '时间',
|
||||||
|
slider: '滑块'
|
||||||
|
}
|
||||||
|
return labels[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一ID
|
||||||
|
const generateId = () => {
|
||||||
|
return `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为DynamicFormItem格式
|
||||||
|
const dynamicFormItems = computed<DynamicFormItem[]>(() => {
|
||||||
|
return formItems.value.map(item => ({
|
||||||
|
key: item.key,
|
||||||
|
label: item.label,
|
||||||
|
type: item.type,
|
||||||
|
placeholder: item.placeholder || `请输入${item.label}`,
|
||||||
|
defaultValue: item.defaultValue,
|
||||||
|
required: item.required,
|
||||||
|
disabled: item.disabled,
|
||||||
|
// 传递质检标准相关属性
|
||||||
|
standard: item.standard,
|
||||||
|
standardType: item.standardType,
|
||||||
|
standardValue: item.standardValue,
|
||||||
|
standardRange: item.standardRange,
|
||||||
|
standardOptions: item.standardOptions,
|
||||||
|
showStandard: item.showStandard,
|
||||||
|
// 传递其他属性
|
||||||
|
...(item.type === 'number' && {
|
||||||
|
min: item.min,
|
||||||
|
max: item.max,
|
||||||
|
step: item.step,
|
||||||
|
precision: item.precision
|
||||||
|
}),
|
||||||
|
...(item.type === 'textarea' && {
|
||||||
|
rows: item.rows,
|
||||||
|
maxlength: item.maxlength
|
||||||
|
}),
|
||||||
|
...(item.type === 'select' && {
|
||||||
|
options: item.options || []
|
||||||
|
}),
|
||||||
|
...(item.type === 'date' && {
|
||||||
|
dateType: item.dateType || 'date',
|
||||||
|
format: item.format,
|
||||||
|
valueFormat: item.valueFormat
|
||||||
|
}),
|
||||||
|
...(item.type === 'switch' && {
|
||||||
|
activeText: item.activeText,
|
||||||
|
inactiveText: item.inactiveText
|
||||||
|
}),
|
||||||
|
rules: item.rules
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加表单项
|
||||||
|
const addItem = (type: FormFieldType = 'text') => {
|
||||||
|
// 根据字段类型自动设置标准类型
|
||||||
|
const standardType = getDefaultStandardType(type)
|
||||||
|
|
||||||
|
const newItem: FormBuilderItem = {
|
||||||
|
id: generateId(),
|
||||||
|
label: `项目${formItems.value.length + 1}`,
|
||||||
|
key: `item_${formItems.value.length + 1}`,
|
||||||
|
type,
|
||||||
|
defaultValue: getDefaultValueByType(type),
|
||||||
|
placeholder: `请输入${type === 'text' ? '文本' : type === 'number' ? '数字' : '内容'}`,
|
||||||
|
// text类型特有属性
|
||||||
|
...(type === 'text' && {
|
||||||
|
rows: 1,
|
||||||
|
maxlength: 100
|
||||||
|
}),
|
||||||
|
// 初始化标准字段(根据类型自动设置)
|
||||||
|
standard: '',
|
||||||
|
standardType,
|
||||||
|
standardValue: '',
|
||||||
|
standardRange: { min: 0, max: 100 },
|
||||||
|
standardOptions: [],
|
||||||
|
showStandard: true,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
formItems.value.push(newItem)
|
||||||
|
handleItemChange()
|
||||||
|
emit('itemAdd', newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据字段类型获取默认标准类型
|
||||||
|
const getDefaultStandardType = (type: FormFieldType): string => {
|
||||||
|
switch (type) {
|
||||||
|
case 'text':
|
||||||
|
return 'text'
|
||||||
|
case 'number':
|
||||||
|
return 'range'
|
||||||
|
case 'select':
|
||||||
|
return 'select'
|
||||||
|
default:
|
||||||
|
return 'text'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标准类型显示标签
|
||||||
|
const getStandardTypeLabel = (standardType: string): string => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
text: '📄 文本标准',
|
||||||
|
number: '🔢 数值标准',
|
||||||
|
range: '📊 范围标准',
|
||||||
|
select: '✅ 选择标准'
|
||||||
|
}
|
||||||
|
return labels[standardType] || standardType
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标准值标签
|
||||||
|
const getStandardValueLabel = (standardType: string): string => {
|
||||||
|
switch (standardType) {
|
||||||
|
case 'text':
|
||||||
|
return '期望文本'
|
||||||
|
case 'range':
|
||||||
|
return '数值范围'
|
||||||
|
case 'select':
|
||||||
|
return '标准选项'
|
||||||
|
default:
|
||||||
|
return '标准值'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标准值提示
|
||||||
|
const getStandardValueHint = (standardType: string): string => {
|
||||||
|
switch (standardType) {
|
||||||
|
case 'text':
|
||||||
|
return '输入期望的文本值'
|
||||||
|
case 'range':
|
||||||
|
return '设置允许的数值范围'
|
||||||
|
case 'select':
|
||||||
|
return '设置可选的标准选项'
|
||||||
|
default:
|
||||||
|
return '设置标准值'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据类型获取默认值
|
||||||
|
const getDefaultValueByType = (type: FormFieldType): any => {
|
||||||
|
switch (type) {
|
||||||
|
case 'number':
|
||||||
|
return 0
|
||||||
|
case 'switch':
|
||||||
|
return false
|
||||||
|
case 'select':
|
||||||
|
case 'radio':
|
||||||
|
case 'checkbox':
|
||||||
|
return []
|
||||||
|
case 'date':
|
||||||
|
return null
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除表单项
|
||||||
|
const removeItem = (index: number) => {
|
||||||
|
formItems.value.splice(index, 1)
|
||||||
|
handleItemChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上移表单项
|
||||||
|
const moveUp = (index: number) => {
|
||||||
|
if (index > 0) {
|
||||||
|
const item = formItems.value.splice(index, 1)[0]
|
||||||
|
formItems.value.splice(index - 1, 0, item)
|
||||||
|
handleItemChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下移表单项
|
||||||
|
const moveDown = (index: number) => {
|
||||||
|
if (index < formItems.value.length - 1) {
|
||||||
|
const item = formItems.value.splice(index, 1)[0]
|
||||||
|
formItems.value.splice(index + 1, 0, item)
|
||||||
|
handleItemChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理字段类型变化
|
||||||
|
const handleTypeChange = (index: number, newType: FormFieldType) => {
|
||||||
|
const item = formItems.value[index]
|
||||||
|
if (item) {
|
||||||
|
// 根据新的字段类型自动更新标准类型
|
||||||
|
item.standardType = getDefaultStandardType(newType)
|
||||||
|
|
||||||
|
// 重置相关字段的值
|
||||||
|
item.standardValue = ''
|
||||||
|
item.standardRange = { min: 0, max: 100 }
|
||||||
|
item.standardOptions = []
|
||||||
|
|
||||||
|
// 为text类型添加默认属性
|
||||||
|
if (newType === 'text') {
|
||||||
|
item.rows = item.rows || 1
|
||||||
|
item.maxlength = item.maxlength || 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleItemChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理表单项变化
|
||||||
|
const handleItemChange = () => {
|
||||||
|
// 确保所有标准字段都有值,并根据字段类型自动设置标准类型
|
||||||
|
formItems.value.forEach(item => {
|
||||||
|
if (!item.standard) item.standard = ''
|
||||||
|
if (!item.standardType) {
|
||||||
|
// 如果没有标准类型,根据字段类型设置默认值
|
||||||
|
item.standardType = getDefaultStandardType(item.type)
|
||||||
|
}
|
||||||
|
if (!item.standardValue) item.standardValue = ''
|
||||||
|
if (!item.standardRange) item.standardRange = { min: 0, max: 100 }
|
||||||
|
if (!item.standardOptions) item.standardOptions = []
|
||||||
|
if (item.showStandard === undefined) item.showStandard = true
|
||||||
|
})
|
||||||
|
|
||||||
|
emit('update:modelValue', [...formItems.value])
|
||||||
|
emit('change', [...formItems.value])
|
||||||
|
|
||||||
|
// 更新预览数据
|
||||||
|
updatePreviewData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新预览数据
|
||||||
|
const updatePreviewData = () => {
|
||||||
|
const newPreviewData: Record<string, any> = {}
|
||||||
|
formItems.value.forEach(item => {
|
||||||
|
if (item.defaultValue !== null && item.defaultValue !== undefined && item.defaultValue !== '') {
|
||||||
|
newPreviewData[item.key] = item.defaultValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
previewData.value = newPreviewData
|
||||||
|
emit('preview-change', newPreviewData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理预览数据变化
|
||||||
|
const handlePreviewChange = (value: Record<string, any>) => {
|
||||||
|
console.log('FormBuilder预览数据变化:', value)
|
||||||
|
previewData.value = value
|
||||||
|
emit('preview-change', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听外部数据变化
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
formItems.value = [...newValue]
|
||||||
|
// 确保标准字段有默认值
|
||||||
|
formItems.value.forEach(item => {
|
||||||
|
if (!item.standard) item.standard = ''
|
||||||
|
if (!item.standardType) item.standardType = 'text'
|
||||||
|
if (!item.standardValue) item.standardValue = ''
|
||||||
|
if (!item.standardRange) item.standardRange = { min: 0, max: 100 }
|
||||||
|
if (!item.standardOptions) item.standardOptions = []
|
||||||
|
if (item.showStandard === undefined) item.showStandard = true
|
||||||
|
})
|
||||||
|
updatePreviewData()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
const getFormItems = () => {
|
||||||
|
return [...formItems.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFormItems = (items: FormBuilderItem[]) => {
|
||||||
|
formItems.value = [...items]
|
||||||
|
handleItemChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearItems = () => {
|
||||||
|
formItems.value = []
|
||||||
|
handleItemChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addItemWithData = (label: string, key: string, type: FormFieldType = 'text', defaultValue: any = '') => {
|
||||||
|
const standardType = getDefaultStandardType(type)
|
||||||
|
|
||||||
|
const newItem: FormBuilderItem = {
|
||||||
|
id: generateId(),
|
||||||
|
label,
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
defaultValue,
|
||||||
|
// 初始化标准字段
|
||||||
|
standard: '',
|
||||||
|
standardType,
|
||||||
|
standardValue: '',
|
||||||
|
standardRange: { min: 0, max: 100 },
|
||||||
|
standardOptions: [],
|
||||||
|
showStandard: true,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
formItems.value.push(newItem)
|
||||||
|
handleItemChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑标准选项
|
||||||
|
const editStandardOptions = (index: number) => {
|
||||||
|
const item = formItems.value[index]
|
||||||
|
if (!item.standardOptions) {
|
||||||
|
item.standardOptions = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建当前选项的文本
|
||||||
|
const currentOptions = item.standardOptions.map(opt => opt.label).join(',')
|
||||||
|
|
||||||
|
// 使用更友好的提示
|
||||||
|
const newOptions = prompt(
|
||||||
|
`📋 编辑标准选项\n\n当前选项:${currentOptions || '无'}\n\n请输入新的标准选项,用逗号分隔:\n例如:合格,不合格,待定`,
|
||||||
|
currentOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
if (newOptions !== null) {
|
||||||
|
if (newOptions.trim()) {
|
||||||
|
// 解析新选项
|
||||||
|
const options = newOptions.split(',').map((label, idx) => ({
|
||||||
|
label: label.trim(),
|
||||||
|
value: `option_${idx + 1}`,
|
||||||
|
disabled: false
|
||||||
|
})).filter(opt => opt.label) // 过滤空选项
|
||||||
|
|
||||||
|
item.standardOptions = options
|
||||||
|
handleItemChange()
|
||||||
|
|
||||||
|
// 显示成功提示
|
||||||
|
console.log(`已设置 ${options.length} 个标准选项:${options.map(opt => opt.label).join(',')}`)
|
||||||
|
} else {
|
||||||
|
// 清空选项
|
||||||
|
item.standardOptions = []
|
||||||
|
handleItemChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getFormItems,
|
||||||
|
setFormItems,
|
||||||
|
clearItems,
|
||||||
|
addItemWithData
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-builder {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-builder-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-builder-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-items-list {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item-card {
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: #fff;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item-card:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-index {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 2px dashed #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button + .el-button) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
82
src/components/FormBuilder/types.ts
Normal file
82
src/components/FormBuilder/types.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
147
src/components/FormBuilder/utils.ts
Normal file
147
src/components/FormBuilder/utils.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -3,7 +3,11 @@
|
|||||||
<!-- 筛选条件 -->
|
<!-- 筛选条件 -->
|
||||||
<div class="mobile-dashboard-filter">
|
<div class="mobile-dashboard-filter">
|
||||||
<el-select v-model="queryParams.supplierId" clearable filterable placeholder="选择供应商" @change="getSupplierStats" size="small" style="flex:1">
|
<el-select v-model="queryParams.supplierId" clearable filterable placeholder="选择供应商" @change="getSupplierStats" size="small" style="flex:1">
|
||||||
<el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in supplierList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button size="small" @click="getAllSuppliersStats">刷新</el-button>
|
<el-button size="small" @click="getAllSuppliersStats">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mobile-distribution__item">
|
<div class="mobile-distribution__item">
|
||||||
<div class="mobile-distribution__header">
|
<div class="mobile-distribution__header">
|
||||||
<span class="mobile-distribution__label">不及格 (<6)</span>
|
<span class="mobile-distribution__label">不及格 (<6)</span>
|
||||||
<span class="mobile-distribution__count">{{ failCount }} 个</span>
|
<span class="mobile-distribution__count">{{ failCount }} 个</span>
|
||||||
</div>
|
</div>
|
||||||
<el-progress :percentage="scoreDistribution.fail" color="#f56c6c" :stroke-width="10" />
|
<el-progress :percentage="scoreDistribution.fail" color="#f56c6c" :stroke-width="10" />
|
||||||
|
|||||||
Reference in New Issue
Block a user