打包问题的修复,以及质量模块的合并

This commit is contained in:
2026-03-06 16:45:32 +08:00
parent 956834103e
commit 6d706e8961
10 changed files with 2019 additions and 2 deletions

View 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. 建议使用工具函数创建表单项,确保类型安全

View 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
}

View 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>

View 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
}

View 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)
}

View 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
}

View 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>

View 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
}

View 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
}

View File

@@ -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">不及格 (&lt;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" />