index.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <template>
  2. <div class="flow-form-fields">
  3. <div v-if="fields.length === 0" class="empty-tip">
  4. <el-text type="info">该流程未配置表单字段,可直接发起。</el-text>
  5. </div>
  6. <div v-else>
  7. <div class="form-actions">
  8. <el-button type="primary" size="small" plain @click="handleDownloadTemplate">下载 Excel 模板</el-button>
  9. <el-upload
  10. action="#"
  11. :show-file-list="false"
  12. :http-request="handleExcelUpload"
  13. :before-upload="beforeExcelUpload"
  14. accept=".xlsx,.xls"
  15. style="display: inline-block; margin-left: 8px;"
  16. >
  17. <el-button type="success" size="small" plain>上传 Excel 自动填充</el-button>
  18. </el-upload>
  19. </div>
  20. <div class="field-list">
  21. <div
  22. v-for="field in fields"
  23. :key="field.name"
  24. class="field-row"
  25. :class="{ 'field-row-full': field.type === 'textarea' }"
  26. >
  27. <label class="field-label">
  28. <span>{{ field.label }}</span>
  29. <span v-if="field.required" class="required-star">*</span>
  30. </label>
  31. <div class="field-control">
  32. <el-input
  33. v-if="!field.type || field.type === 'text'"
  34. v-model="formData[field.name]"
  35. :placeholder="`请输入${field.label}`"
  36. clearable
  37. size="large"
  38. />
  39. <el-input
  40. v-else-if="field.type === 'textarea'"
  41. v-model="formData[field.name]"
  42. type="textarea"
  43. :rows="4"
  44. :placeholder="`请输入${field.label}`"
  45. clearable
  46. size="large"
  47. />
  48. <el-input-number
  49. v-else-if="field.type === 'number'"
  50. v-model="formData[field.name]"
  51. style="width: 100%;"
  52. :placeholder="`请输入${field.label}`"
  53. controls-position="right"
  54. size="large"
  55. />
  56. <el-date-picker
  57. v-else-if="field.type === 'date'"
  58. v-model="formData[field.name]"
  59. type="date"
  60. style="width: 100%;"
  61. :placeholder="`请选择${field.label}`"
  62. value-format="YYYY-MM-DD"
  63. size="large"
  64. />
  65. <el-select
  66. v-else-if="field.type === 'select'"
  67. v-model="formData[field.name]"
  68. style="width: 100%;"
  69. :multiple="field.multiple"
  70. :placeholder="`请选择${field.label}`"
  71. clearable
  72. collapse-tags
  73. collapse-tags-tooltip
  74. size="large"
  75. >
  76. <el-option
  77. v-for="opt in normalizeOptions(field.options)"
  78. :key="String(opt.value)"
  79. :label="opt.label"
  80. :value="opt.value"
  81. />
  82. </el-select>
  83. </div>
  84. </div>
  85. </div>
  86. </div>
  87. </div>
  88. </template>
  89. <script setup lang="ts">
  90. import { computed, watch } from 'vue'
  91. import { ElMessage } from 'element-plus'
  92. import type { FormField, FormFieldOption, FlowDefinition } from '@/types/flow'
  93. import { downloadTemplate as downloadFlowTemplate } from '@/api/flow/import'
  94. import request from '@/api/request'
  95. const props = defineProps<{
  96. definition: FlowDefinition | null | undefined
  97. }>()
  98. const formData = defineModel<Record<string, unknown>>('modelValue', { default: () => ({}) })
  99. const fields = computed<FormField[]>(() => {
  100. const schema = props.definition?.formSchema
  101. if (!schema) return []
  102. try {
  103. const parsed = JSON.parse(schema)
  104. return Array.isArray(parsed) ? parsed : []
  105. } catch {
  106. return []
  107. }
  108. })
  109. // 切换流程定义时重置数据;同时为多选下拉设置数组默认值
  110. watch(() => props.definition?.id, (newId, oldId) => {
  111. if (newId === oldId) return
  112. // 保留已有数据(如回退后补充材料场景)
  113. if (Object.keys(formData.value || {}).length > 0) return
  114. formData.value = {}
  115. for (const field of fields.value) {
  116. if (field.type === 'select' && field.multiple) {
  117. formData.value[field.name] = []
  118. }
  119. }
  120. }, { immediate: true })
  121. function normalizeOptions(options?: FormFieldOption[] | string[]): FormFieldOption[] {
  122. if (!options || !Array.isArray(options)) return []
  123. return options.map((opt: any) => {
  124. if (typeof opt === 'string') {
  125. return { label: opt, value: opt }
  126. }
  127. return { label: String(opt.label ?? opt.value), value: opt.value }
  128. })
  129. }
  130. function validate(): boolean {
  131. for (const field of fields.value) {
  132. if (!field.required) continue
  133. const value = formData.value[field.name]
  134. if (field.type === 'select' && field.multiple) {
  135. if (!Array.isArray(value) || value.length === 0) {
  136. ElMessage.warning(`请选择 ${field.label}`)
  137. return false
  138. }
  139. } else {
  140. if (value === undefined || value === null || String(value).trim() === '') {
  141. ElMessage.warning(`请填写 ${field.label}`)
  142. return false
  143. }
  144. }
  145. }
  146. return true
  147. }
  148. async function handleDownloadTemplate() {
  149. if (!props.definition?.id) return
  150. try {
  151. await downloadFlowTemplate(props.definition.id)
  152. } catch (e: any) {
  153. ElMessage.error(e?.message || '模板下载失败')
  154. }
  155. }
  156. function beforeExcelUpload(file: File): boolean {
  157. const ext = file.name.split('.').pop()?.toLowerCase()
  158. if (!ext || !['xlsx', 'xls'].includes(ext)) {
  159. ElMessage.warning('请上传 Excel 文件')
  160. return false
  161. }
  162. if (file.size > 10 * 1024 * 1024) {
  163. ElMessage.warning('文件大小不能超过 10MB')
  164. return false
  165. }
  166. return true
  167. }
  168. async function handleExcelUpload(options: any) {
  169. if (!props.definition?.id) return
  170. try {
  171. const mappings: Record<string, string> = {}
  172. for (const field of fields.value) {
  173. mappings[field.label] = field.name
  174. mappings[`${field.label}(${field.name})`] = field.name
  175. }
  176. const formData2 = new FormData()
  177. formData2.append('file', options.file)
  178. formData2.append('definitionId', String(props.definition.id))
  179. formData2.append('mappings', JSON.stringify(mappings))
  180. const res = await request.post('/flow/import/parse', formData2, {
  181. headers: { 'Content-Type': 'multipart/form-data' }
  182. })
  183. const data = res as unknown as Record<string, unknown>
  184. formData.value = { ...formData.value, ...data }
  185. ElMessage.success('Excel 数据已填充')
  186. options.onSuccess?.()
  187. } catch (e: any) {
  188. ElMessage.error(e?.message || 'Excel 解析失败')
  189. options.onError?.(e)
  190. }
  191. }
  192. defineExpose({ validate, normalizeOptions })
  193. </script>
  194. <style scoped>
  195. .flow-form-fields {
  196. padding: 4px 0;
  197. }
  198. .empty-tip {
  199. padding: 20px 0;
  200. text-align: center;
  201. font-size: 14px;
  202. }
  203. .form-actions {
  204. margin-bottom: 16px;
  205. }
  206. .field-list {
  207. display: flex;
  208. flex-wrap: wrap;
  209. gap: 18px;
  210. }
  211. .field-row {
  212. width: calc(50% - 9px);
  213. min-width: 240px;
  214. flex: 1 1 calc(50% - 9px);
  215. }
  216. .field-row-full {
  217. width: 100%;
  218. flex: 1 1 100%;
  219. }
  220. .field-label {
  221. display: block;
  222. margin-bottom: 8px;
  223. font-size: 14px;
  224. font-weight: 500;
  225. color: #606266;
  226. line-height: 1.4;
  227. }
  228. .required-star {
  229. color: #f56c6c;
  230. margin-left: 4px;
  231. }
  232. .field-control {
  233. width: 100%;
  234. }
  235. .field-control :deep(.el-input__inner),
  236. .field-control :deep(.el-textarea__inner),
  237. .field-control :deep(.el-input-number),
  238. .field-control :deep(.el-date-editor),
  239. .field-control :deep(.el-select) {
  240. width: 100%;
  241. }
  242. </style>