index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. <template>
  2. <div class="form-builder">
  3. <div>
  4. <h2 class="template-title">{{ templateName }}</h2>
  5. </div>
  6. <el-row :gutter="20" class="main-container">
  7. <el-col :span="5" class="field-selector">
  8. <div class="panel-content">
  9. <h3 class="panel-title">选择组件</h3>
  10. <div class="field-list">
  11. <div
  12. v-for="(field, index) in availableFields"
  13. :key="field.componentId"
  14. class="field-item"
  15. @click="addFieldToForm(field, index)"
  16. >
  17. <el-button size="small">{{ field.label }}</el-button>
  18. </div>
  19. </div>
  20. </div>
  21. </el-col>
  22. <el-col :span="14" class="form-preview">
  23. <template v-if="formFields.length === 0">
  24. <div class="empty-tip">请选择添加左侧样式</div>
  25. </template>
  26. <draggable
  27. v-model="formFields"
  28. 200 animation:
  29. group="form-fields"
  30. @end="handleDragEnd"
  31. >
  32. <el-scrollbar style="height: 500px; width: 100%;">
  33. <div
  34. v-for="(field, index) in formFields"
  35. :key="index"
  36. :class="{ 'form-field-active': activeFieldIndex === index }"
  37. class="form-field-item"
  38. @click="setActiveField(index)"
  39. >
  40. <div class="field-header">
  41. <el-row align="middle" type="flex">
  42. <el-col :span="23" class="field-label">
  43. <span v-if="field.required" class="required-mark">*</span>
  44. {{ field.label }}
  45. </el-col>
  46. <el-col :span="1" class="field-operation">
  47. <i
  48. v-if="activeFieldIndex === index"
  49. class="el-icon-delete"
  50. @click.stop="removeFormField(index)"
  51. ></i>
  52. </el-col>
  53. </el-row>
  54. </div>
  55. <!-- 字段输入组件 -->
  56. <div class="field-input">
  57. <!-- 单选框 -->
  58. <template v-if="field.type === 'radio'">
  59. <el-radio-group v-model="field.value">
  60. <el-radio
  61. v-for="(option, optIdx) in field.options"
  62. :key="optIdx"
  63. :label="option"
  64. style="width: 90px"
  65. >
  66. {{ option }}
  67. </el-radio>
  68. </el-radio-group>
  69. </template>
  70. <!-- 下拉选择框 -->
  71. <template v-else-if="field.type === 'select'">
  72. <el-select
  73. v-if="!field.multiple"
  74. v-model="field.value"
  75. :multiple="field.multiple"
  76. :placeholder="`请选择${field.label}`"
  77. clearable
  78. size="small"
  79. style="width: 100%"
  80. >
  81. <el-option
  82. v-for="(option, optIdx) in field.options"
  83. :key="optIdx"
  84. :label="option"
  85. :value="optIdx"
  86. ></el-option>
  87. </el-select>
  88. <el-select
  89. v-if="field.multiple"
  90. v-model="field.multiples"
  91. :multiple="field.multiple"
  92. :placeholder="`请选择${field.label}`"
  93. clearable
  94. size="small"
  95. style="width: 100%"
  96. >
  97. <el-option
  98. v-for="(option, optIdx) in field.options"
  99. :key="optIdx"
  100. :label="option"
  101. :value="optIdx"
  102. ></el-option>
  103. </el-select>
  104. </template>
  105. <!-- 文本输入框 -->
  106. <template v-else-if="field.type === 'text'">
  107. <el-input
  108. v-model="field.value"
  109. :placeholder="field.placeholder || `请输入${field.label}`"
  110. clearable
  111. size="small"
  112. ></el-input>
  113. </template>
  114. <!-- 复选框 -->
  115. <template v-else-if="field.type === 'checkbox'">
  116. <div class="option-container">
  117. <el-checkbox-group v-model="field.multiples">
  118. <el-checkbox
  119. v-for="(option, optIdx) in field.options"
  120. :key="optIdx"
  121. :label="optIdx"
  122. style="width: 90px"
  123. >{{ option }}
  124. </el-checkbox>
  125. </el-checkbox-group>
  126. </div>
  127. </template>
  128. </div>
  129. </div>
  130. </el-scrollbar>
  131. </draggable>
  132. </el-col>
  133. <el-col :span="5" class="field-configurator">
  134. <template v-if="formFields.length > 0">
  135. <el-form class="config-form">
  136. <!-- 字段名称配置 -->
  137. <el-form-item label="字段名称">
  138. <el-input
  139. v-model="formFields[activeFieldIndex].label"
  140. clearable
  141. size="small"
  142. ></el-input>
  143. </el-form-item>
  144. <!-- <template v-if="formFields[activeFieldIndex].type !== 'radio' && formFields[activeFieldIndex].type !== 'checkbox'">-->
  145. <!-- <el-form-item label="默认值">-->
  146. <!-- <el-select-->
  147. <!-- v-model="defaultValueMode"-->
  148. <!-- size="small"-->
  149. <!-- style="width: 100%; margin-bottom: 8px"-->
  150. <!-- >-->
  151. <!-- <el-option-->
  152. <!-- v-for="mode in valueModes"-->
  153. <!-- :key="mode.value"-->
  154. <!-- :label="mode.label"-->
  155. <!-- :value="mode.value"-->
  156. <!-- ></el-option>-->
  157. <!-- </el-select>-->
  158. <!-- <el-input-->
  159. <!-- :v-model="formFields[activeFieldIndex].value"-->
  160. <!-- clearable-->
  161. <!-- placeholder="请输入默认值"-->
  162. <!-- size="small"-->
  163. <!-- ></el-input>-->
  164. <!-- </el-form-item>-->
  165. <!-- </template>-->
  166. <template v-if="['radio', 'select','checkbox'].includes(formFields[activeFieldIndex].type)">
  167. <el-form-item label="选项配置">
  168. <draggable
  169. v-model="formFields[activeFieldIndex].options"
  170. animation:50
  171. group="options"
  172. @end="handleOptionDragEnd"
  173. >
  174. <el-row
  175. v-for="(option, idx) in formFields[activeFieldIndex].options"
  176. :key="idx"
  177. class="option-item"
  178. >
  179. <el-col :span="2" class="option-drag">
  180. <span class="el-icon-s-operation"></span>
  181. </el-col>
  182. <el-col :span="18">
  183. <el-input
  184. v-model="formFields[activeFieldIndex].options[idx]"
  185. clearable
  186. size="small"
  187. ></el-input>
  188. </el-col>
  189. <el-col :span="2" class="option-delete">
  190. <i
  191. class="el-icon-delete"
  192. @click.stop="removeOption(idx)"
  193. ></i>
  194. </el-col>
  195. </el-row>
  196. </draggable>
  197. <el-button
  198. size="small"
  199. style="width: 100%; margin-top: 8px"
  200. @click="addNewOption"
  201. >
  202. <span class="el-icon-plus"></span> 添加选项
  203. </el-button>
  204. </el-form-item>
  205. </template>
  206. <!-- 通用配置:是否必填 -->
  207. <el-form-item class="required-config" label="是否必填">
  208. <el-switch v-model="formFields[activeFieldIndex].required"></el-switch>
  209. </el-form-item>
  210. <el-form-item v-if="'select' === formFields[activeFieldIndex].type" class="required-config"
  211. label="是否多选"
  212. >
  213. <el-switch v-model="formFields[activeFieldIndex].multiple"></el-switch>
  214. </el-form-item>
  215. <!-- 文本字段:长度限制 -->
  216. <el-form-item
  217. v-if="formFields[activeFieldIndex].type === 'text' && formFields[activeFieldIndex].required"
  218. class="length-limit"
  219. label="长度限制"
  220. >
  221. <el-input
  222. v-model.number="formFields[activeFieldIndex].minLength"
  223. autocomplete="off"
  224. clearable
  225. placeholder="最小"
  226. size="small"
  227. style="width: 40%; display: inline-block"
  228. ></el-input>
  229. <span style="margin: 0 4px">-</span>
  230. <el-input
  231. v-model.number="formFields[activeFieldIndex].maxLength"
  232. autocomplete="off"
  233. clearable
  234. placeholder="最大"
  235. size="small"
  236. style="width: 40%; display: inline-block"
  237. ></el-input>
  238. </el-form-item>
  239. </el-form>
  240. </template>
  241. </el-col>
  242. </el-row>
  243. <el-row class="action-bar">
  244. <el-button size="small" @click="resetForm">重置</el-button>
  245. <el-button size="small" type="primary" @click="submitForm">保存配置</el-button>
  246. </el-row>
  247. </div>
  248. </template>
  249. <script>
  250. import draggable from 'vuedraggable'
  251. import { getTemplateField, saveTemplate } from '@/api/his/physicalReportTemplateField'
  252. export default {
  253. props: {
  254. templateName: {
  255. type: String,
  256. default: '体检报告',
  257. required: true
  258. }
  259. },
  260. components: { draggable },
  261. data() {
  262. return {
  263. templateId: null,
  264. availableFields: [
  265. { componentId: '1', label: '输入框', type: 'text' },
  266. { componentId: '2', label: '单选按钮', type: 'radio' },
  267. { componentId: '3', label: '下拉选项', type: 'select' },
  268. { componentId: '4', label: '复选框', type: 'checkbox' }
  269. ],
  270. // 中间表单已添加的字段列表
  271. formFields: [],
  272. // 当前激活的字段索引
  273. activeFieldIndex: 0,
  274. // 右侧配置:默认值模式
  275. defaultValueMode: 1,
  276. // 临时存储:默认值模式(用于非单选/下拉字段)
  277. valueModes: [{ label: '自定义输入', value: 1 }]
  278. }
  279. },
  280. created() {
  281. },
  282. computed: {
  283. // 当前激活字段的类型
  284. currentFieldType() {
  285. return this.formFields[this.activeFieldIndex]?.type
  286. }
  287. },
  288. methods: {
  289. // 添加字段到表单
  290. addFieldToForm(field, index) {
  291. // 检查字段是否已添加(避免重复)
  292. // const isExist = this.formFields.some(item => item.id === field.id)
  293. // if (isExist) return
  294. // 根据字段类型初始化配置
  295. const fieldConfig = {
  296. componentId: field.componentId,
  297. label: field.label,
  298. type: field.type,
  299. required: false,
  300. value: '',
  301. multiple: false,
  302. multiples: [],
  303. placeholder: ''
  304. }
  305. // 文本字段
  306. if (field.type === 'text') {
  307. fieldConfig.minLength = null
  308. fieldConfig.maxLength = null
  309. }
  310. // 单选/下拉字段
  311. if (['radio', 'select', 'checkbox'].includes(field.type)) {
  312. fieldConfig.options = ['选项1', '选项2']
  313. }
  314. this.formFields.push(fieldConfig)
  315. this.activeFieldIndex = this.formFields.length - 1
  316. },
  317. // 删除表单中的字段
  318. removeFormField(index) {
  319. this.formFields.splice(index, 1)
  320. if (this.activeFieldIndex === index) {
  321. this.activeFieldIndex = Math.min(index, this.formFields.length - 1)
  322. }
  323. },
  324. // 添加新选项
  325. addNewOption() {
  326. const currentField = this.formFields[this.activeFieldIndex]
  327. const optionCount = currentField.options.length + 1
  328. currentField.options.push(`选项${optionCount}`)
  329. },
  330. // 删除选项
  331. removeOption(index) {
  332. this.formFields[this.activeFieldIndex].options.splice(index, 1)
  333. },
  334. // 设置当前激活的字段
  335. setActiveField(index) {
  336. this.activeFieldIndex = index
  337. },
  338. // 拖拽结束事件
  339. handleDragEnd() {
  340. console.log('字段排序已更新----')
  341. },
  342. // 拖拽结束事件
  343. handleOptionDragEnd() {
  344. console.log('选项排序已更新----')
  345. },
  346. // 表单验证
  347. validateForm() {
  348. if (this.formFields.length === 0) {
  349. this.$message.error('请至少添加一个字段')
  350. return false
  351. }
  352. for (let i = 0; i < this.formFields.length; i++) {
  353. const field = this.formFields[i]
  354. if (!field.label.trim()) {
  355. this.activeFieldIndex = i
  356. this.$message.error('字段名称不能为空')
  357. return false
  358. }
  359. // // 验证必填字段
  360. // if (field.required && field.type !== 'checkbox' && !field.multiple && !field.value && field.value !== 0 || field.required && field.type === 'checkbox' && field.options.length === 0) {
  361. // this.activeFieldIndex = i
  362. // this.$message.error(`“${field.label}”为必填项,请补充`)
  363. // return false
  364. // }
  365. //
  366. // // 验证文本字段长度限制
  367. // if (field.type === 'text' && field.required) {
  368. // const valueLength = (field.value || '').length
  369. // if (field.minLength !== null && valueLength < field.minLength) {
  370. // this.$message.error(`“${field.label}”长度不能小于${field.minLength}个字符`)
  371. // return false
  372. // }
  373. // if (field.maxLength !== null && valueLength > field.maxLength) {
  374. // this.$message.error(`“${field.label}”长度不能大于${field.maxLength}个字符`)
  375. // return false
  376. // }
  377. // }
  378. }
  379. return true
  380. },
  381. // 提交表单配置
  382. submitForm() {
  383. if (!this.validateForm()) return
  384. // 数据过滤
  385. const formData = this.formFields.map(field => {
  386. const multiple = field.multiple ? 1 : 0
  387. const required = field.required ? 1 : 0
  388. const {
  389. componentId,
  390. label,
  391. type,
  392. value,
  393. options,
  394. maxLength,
  395. minLength
  396. } = field
  397. const result = {
  398. componentId,
  399. label,
  400. type,
  401. required,
  402. value,
  403. maxLength,
  404. minLength,
  405. multiple
  406. }
  407. if (options) {
  408. result.options = options.join(',')
  409. }
  410. return result
  411. })
  412. saveTemplate({ templateId: this.templateId, templateFieldList: formData }).then(response => {
  413. if (response.code === 200) {
  414. this.$message.success('保存成功!')
  415. }
  416. this.getTemplateField(this.templateId)
  417. })
  418. },
  419. // 重置表单
  420. resetForm() {
  421. this.formFields = []
  422. this.activeFieldIndex = 0
  423. this.$message.info('已重置表单配置')
  424. },
  425. /**
  426. * 获取自定义数据列表
  427. * **/
  428. getTemplateField(templateId) {
  429. this.templateId = templateId
  430. getTemplateField(templateId).then(response => {
  431. if (response.data.length > 0) {
  432. //处理数据处理
  433. this.formFields = response.data.map(item => {
  434. const multiple = item.multiple === 1
  435. const required = item.required === 1
  436. const multiples = []
  437. const {
  438. componentId,
  439. label,
  440. type,
  441. value,
  442. options,
  443. maxLength,
  444. minLength
  445. } = item
  446. const result = {
  447. componentId,
  448. label,
  449. type,
  450. required,
  451. value,
  452. maxLength,
  453. minLength,
  454. multiple,
  455. multiples
  456. }
  457. if (options) {
  458. result.options = options.split(',').map(item => item.trim())
  459. }
  460. return result
  461. })
  462. this.activeFieldIndex = this.formFields.length - 1
  463. }
  464. })
  465. }
  466. }
  467. }
  468. </script>
  469. <style scoped>
  470. .form-builder {
  471. padding: 15px;
  472. background-color: #f5f7fa;
  473. min-height: calc(94vh - 30px);
  474. }
  475. .main-container {
  476. margin-bottom: 20px;
  477. }
  478. .field-selector {
  479. background-color: #fff;
  480. border-radius: 6px;
  481. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  482. padding: 15px !important;
  483. }
  484. .panel-title {
  485. font-size: 15px;
  486. color: #1f2329;
  487. margin: 0 0 15px;
  488. padding-left: 4px;
  489. font-weight: 500;
  490. }
  491. .field-list {
  492. display: flex;
  493. flex-wrap: wrap;
  494. gap: 10px;
  495. }
  496. .field-item {
  497. width: 100px;
  498. box-sizing: border-box;
  499. }
  500. .field-item .el-button {
  501. width: 100%;
  502. }
  503. /* 中间表单预览区 */
  504. .form-preview {
  505. padding: 0 10px !important;
  506. }
  507. .empty-tip {
  508. height: 60px;
  509. line-height: 60px;
  510. text-align: center;
  511. border: 1px dashed #dcdfe6;
  512. color: #8c8c8c;
  513. border-radius: 6px;
  514. margin-top: 10px;
  515. }
  516. .form-field-item {
  517. background-color: #fff;
  518. border: 1px solid #e5e6eb;
  519. border-radius: 6px;
  520. padding: 12px 15px;
  521. margin-bottom: 12px;
  522. transition: all 0.2s;
  523. }
  524. .form-field-item:hover {
  525. border-color: #c0c4cc;
  526. }
  527. .form-field-active {
  528. border-color: #409eff;
  529. background-color: #f0f7ff;
  530. }
  531. .field-header {
  532. margin-bottom: 10px;
  533. }
  534. .field-label {
  535. font-size: 14px;
  536. color: #1f2329;
  537. }
  538. .required-mark {
  539. color: #ff4d4f;
  540. margin-right: 4px;
  541. }
  542. .field-operation {
  543. color: #8c8c8c;
  544. cursor: pointer;
  545. }
  546. .field-operation:hover {
  547. color: #ff4d4f;
  548. }
  549. .field-configurator {
  550. background-color: #fff;
  551. border-radius: 6px;
  552. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  553. padding: 15px !important;
  554. }
  555. .config-form .el-form-item {
  556. margin-bottom: 12px;
  557. }
  558. .config-form .el-form-item__label {
  559. font-size: 13px;
  560. color: #4e5969;
  561. padding: 0 0 6px;
  562. line-height: 1;
  563. width: 100%;
  564. text-align: left;
  565. }
  566. .option-item {
  567. display: flex;
  568. align-items: center;
  569. width: 100%;
  570. margin: 0 !important;
  571. padding: 4px 0;
  572. }
  573. .option-drag {
  574. color: #c9cdD4;
  575. }
  576. .option-delete {
  577. color: #8c8c8c;
  578. cursor: pointer;
  579. }
  580. .option-delete:hover {
  581. color: #ff4d4f;
  582. }
  583. .action-bar {
  584. text-align: center;
  585. padding: 10px 0;
  586. }
  587. .action-bar .el-button {
  588. margin: 0 5px;
  589. }
  590. .empty-tip {
  591. height: 60px;
  592. line-height: 60px;
  593. text-align: center;
  594. border: 1px dashed #dcdfe6;
  595. color: #8c8c8c;
  596. border-radius: 6px;
  597. margin-top: 10px;
  598. }
  599. @media (max-width: 1200px) {
  600. .field-selector,
  601. .field-configurator {
  602. padding: 10px !important;
  603. }
  604. }
  605. .option-container {
  606. display: flex;
  607. flex-wrap: wrap;
  608. align-items: flex-start;
  609. gap: 16px;
  610. }
  611. .template-title {
  612. text-align: center;
  613. margin: 0;
  614. line-height: 50px;
  615. }
  616. </style>