| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- <template>
- <div class="flow-form-fields">
- <div v-if="fields.length === 0" class="empty-tip">
- <el-text type="info">该流程未配置表单字段,可直接发起。</el-text>
- </div>
- <div v-else>
- <div class="form-actions">
- <el-button type="primary" size="small" plain @click="handleDownloadTemplate">下载 Excel 模板</el-button>
- <el-upload
- action="#"
- :show-file-list="false"
- :http-request="handleExcelUpload"
- :before-upload="beforeExcelUpload"
- accept=".xlsx,.xls"
- style="display: inline-block; margin-left: 8px;"
- >
- <el-button type="success" size="small" plain>上传 Excel 自动填充</el-button>
- </el-upload>
- </div>
- <div class="field-list">
- <div
- v-for="field in fields"
- :key="field.name"
- class="field-row"
- :class="{ 'field-row-full': field.type === 'textarea' }"
- >
- <label class="field-label">
- <span>{{ field.label }}</span>
- <span v-if="field.required" class="required-star">*</span>
- </label>
- <div class="field-control">
- <el-input
- v-if="!field.type || field.type === 'text'"
- v-model="formData[field.name]"
- :placeholder="`请输入${field.label}`"
- clearable
- size="large"
- />
- <el-input
- v-else-if="field.type === 'textarea'"
- v-model="formData[field.name]"
- type="textarea"
- :rows="4"
- :placeholder="`请输入${field.label}`"
- clearable
- size="large"
- />
- <el-input-number
- v-else-if="field.type === 'number'"
- v-model="formData[field.name]"
- style="width: 100%;"
- :placeholder="`请输入${field.label}`"
- controls-position="right"
- size="large"
- />
- <el-date-picker
- v-else-if="field.type === 'date'"
- v-model="formData[field.name]"
- type="date"
- style="width: 100%;"
- :placeholder="`请选择${field.label}`"
- value-format="YYYY-MM-DD"
- size="large"
- />
- <el-select
- v-else-if="field.type === 'select'"
- v-model="formData[field.name]"
- style="width: 100%;"
- :multiple="field.multiple"
- :placeholder="`请选择${field.label}`"
- clearable
- collapse-tags
- collapse-tags-tooltip
- size="large"
- >
- <el-option
- v-for="opt in normalizeOptions(field.options)"
- :key="String(opt.value)"
- :label="opt.label"
- :value="opt.value"
- />
- </el-select>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, watch } from 'vue'
- import { ElMessage } from 'element-plus'
- import type { FormField, FormFieldOption, FlowDefinition } from '@/types/flow'
- import { downloadTemplate as downloadFlowTemplate } from '@/api/flow/import'
- import request from '@/api/request'
- const props = defineProps<{
- definition: FlowDefinition | null | undefined
- }>()
- const formData = defineModel<Record<string, unknown>>('modelValue', { default: () => ({}) })
- const fields = computed<FormField[]>(() => {
- const schema = props.definition?.formSchema
- if (!schema) return []
- try {
- const parsed = JSON.parse(schema)
- return Array.isArray(parsed) ? parsed : []
- } catch {
- return []
- }
- })
- // 切换流程定义时重置数据;同时为多选下拉设置数组默认值
- watch(() => props.definition?.id, (newId, oldId) => {
- if (newId === oldId) return
- // 保留已有数据(如回退后补充材料场景)
- if (Object.keys(formData.value || {}).length > 0) return
- formData.value = {}
- for (const field of fields.value) {
- if (field.type === 'select' && field.multiple) {
- formData.value[field.name] = []
- }
- }
- }, { immediate: true })
- function normalizeOptions(options?: FormFieldOption[] | string[]): FormFieldOption[] {
- if (!options || !Array.isArray(options)) return []
- return options.map((opt: any) => {
- if (typeof opt === 'string') {
- return { label: opt, value: opt }
- }
- return { label: String(opt.label ?? opt.value), value: opt.value }
- })
- }
- function validate(): boolean {
- for (const field of fields.value) {
- if (!field.required) continue
- const value = formData.value[field.name]
- if (field.type === 'select' && field.multiple) {
- if (!Array.isArray(value) || value.length === 0) {
- ElMessage.warning(`请选择 ${field.label}`)
- return false
- }
- } else {
- if (value === undefined || value === null || String(value).trim() === '') {
- ElMessage.warning(`请填写 ${field.label}`)
- return false
- }
- }
- }
- return true
- }
- async function handleDownloadTemplate() {
- if (!props.definition?.id) return
- try {
- await downloadFlowTemplate(props.definition.id)
- } catch (e: any) {
- ElMessage.error(e?.message || '模板下载失败')
- }
- }
- function beforeExcelUpload(file: File): boolean {
- const ext = file.name.split('.').pop()?.toLowerCase()
- if (!ext || !['xlsx', 'xls'].includes(ext)) {
- ElMessage.warning('请上传 Excel 文件')
- return false
- }
- if (file.size > 10 * 1024 * 1024) {
- ElMessage.warning('文件大小不能超过 10MB')
- return false
- }
- return true
- }
- async function handleExcelUpload(options: any) {
- if (!props.definition?.id) return
- try {
- const mappings: Record<string, string> = {}
- for (const field of fields.value) {
- mappings[field.label] = field.name
- mappings[`${field.label}(${field.name})`] = field.name
- }
- const formData2 = new FormData()
- formData2.append('file', options.file)
- formData2.append('definitionId', String(props.definition.id))
- formData2.append('mappings', JSON.stringify(mappings))
- const res = await request.post('/flow/import/parse', formData2, {
- headers: { 'Content-Type': 'multipart/form-data' }
- })
- const data = res as unknown as Record<string, unknown>
- formData.value = { ...formData.value, ...data }
- ElMessage.success('Excel 数据已填充')
- options.onSuccess?.()
- } catch (e: any) {
- ElMessage.error(e?.message || 'Excel 解析失败')
- options.onError?.(e)
- }
- }
- defineExpose({ validate, normalizeOptions })
- </script>
- <style scoped>
- .flow-form-fields {
- padding: 4px 0;
- }
- .empty-tip {
- padding: 20px 0;
- text-align: center;
- font-size: 14px;
- }
- .form-actions {
- margin-bottom: 16px;
- }
- .field-list {
- display: flex;
- flex-wrap: wrap;
- gap: 18px;
- }
- .field-row {
- width: calc(50% - 9px);
- min-width: 240px;
- flex: 1 1 calc(50% - 9px);
- }
- .field-row-full {
- width: 100%;
- flex: 1 1 100%;
- }
- .field-label {
- display: block;
- margin-bottom: 8px;
- font-size: 14px;
- font-weight: 500;
- color: #606266;
- line-height: 1.4;
- }
- .required-star {
- color: #f56c6c;
- margin-left: 4px;
- }
- .field-control {
- width: 100%;
- }
- .field-control :deep(.el-input__inner),
- .field-control :deep(.el-textarea__inner),
- .field-control :deep(.el-input-number),
- .field-control :deep(.el-date-editor),
- .field-control :deep(.el-select) {
- width: 100%;
- }
- </style>
|