CzrFormColumn.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. <template>
  2. <el-col
  3. class="czr-form-column"
  4. :class="{
  5. transparent: transparent,
  6. 'has-width': !!width,
  7. }"
  8. :span="span"
  9. :offset="offset"
  10. ref="ref_czrFormColumn"
  11. >
  12. <el-form-item
  13. :label="label"
  14. :label-width="labelWidthCpt"
  15. :class="{
  16. ['link_' + link + ($attrs.type ? '_' + $attrs.type : '')]: true,
  17. required: required !== false,
  18. 'no-label': labelWidth === '0px' && !isValue(label) && !$slots.label,
  19. 'no-label-fit': !labelFit,
  20. 'is-error': !!state.errorMessage,
  21. [`layout-${layout || formLayout}`]: true,
  22. }"
  23. :error="state.errorMessage"
  24. :required="required"
  25. >
  26. <template #label>
  27. <slot name="label" />
  28. </template>
  29. <slot name="czr" :handleValidate="handleValidate">
  30. <template v-if="link === 'input'">
  31. <InputCom
  32. ref="ref_el"
  33. v-bind="$attrs"
  34. :label="labelCpt"
  35. :param="param"
  36. @emitParam="
  37. (val) => {
  38. $emit('update:param', val), handleValidate(val)
  39. }
  40. "
  41. @emitEnter="handleEnter"
  42. >
  43. <template v-if="$slots.prefix" #prefix>
  44. <slot name="prefix" />
  45. </template>
  46. <template v-if="$slots.suffix" #suffix>
  47. <slot name="suffix" />
  48. </template>
  49. <template v-if="$slots.prepend" #prepend>
  50. <slot name="prepend" />
  51. </template>
  52. <template v-if="$slots.append" #append>
  53. <slot name="append" />
  54. </template>
  55. </InputCom>
  56. </template>
  57. <template v-else-if="link === 'select'">
  58. <SelectCom
  59. v-bind="$attrs"
  60. :label="labelCpt"
  61. :param="param"
  62. @emitParam="
  63. (val) => {
  64. $emit('update:param', val), handleValidate(val)
  65. }
  66. "
  67. >
  68. <slot />
  69. <template #row="{ row }">
  70. <slot name="row" :row="row" />
  71. </template>
  72. </SelectCom>
  73. </template>
  74. <template v-else-if="link === 'date'">
  75. <DateCom
  76. v-bind="$attrs"
  77. :label="labelCpt"
  78. :param="param"
  79. @emitParam="
  80. (val) => {
  81. $emit('update:param', val), handleValidate(val)
  82. }
  83. "
  84. >
  85. <template #default="{ cell }">
  86. <slot v-bind="cell" />
  87. </template>
  88. </DateCom>
  89. </template>
  90. <template v-else-if="link === 'datetime'">
  91. <DateTimeCom
  92. v-bind="$attrs"
  93. :label="labelCpt"
  94. :param="param"
  95. @emitParam="
  96. (val) => {
  97. $emit('update:param', val), handleValidate(val)
  98. }
  99. "
  100. >
  101. <template #default="{ cell }">
  102. <slot v-bind="cell" />
  103. </template>
  104. </DateTimeCom>
  105. </template>
  106. <template v-else-if="link === 'time'">
  107. <TimeCom
  108. v-bind="$attrs"
  109. :label="labelCpt"
  110. :param="param"
  111. @emitParam="
  112. (val) => {
  113. $emit('update:param', val), handleValidate(val)
  114. }
  115. "
  116. >
  117. </TimeCom>
  118. </template>
  119. <template v-else-if="link === 'cascader'">
  120. <CascaderCom
  121. v-bind="$attrs"
  122. :label="labelCpt"
  123. :param="param"
  124. @emitParam="
  125. (val) => {
  126. $emit('update:param', val), handleValidate(val)
  127. }
  128. "
  129. >
  130. <template v-if="$slots.default" #default="{ node, data }">
  131. <slot v-bind="{ node, data }" />
  132. </template>
  133. </CascaderCom>
  134. </template>
  135. <template v-else-if="link === 'switch'">
  136. <SwitchCom
  137. v-bind="$attrs"
  138. :label="labelCpt"
  139. :param="param"
  140. @emitParam="
  141. (val) => {
  142. $emit('update:param', val), handleValidate(val)
  143. }
  144. "
  145. >
  146. </SwitchCom>
  147. </template>
  148. <template v-else-if="link === 'radio'">
  149. <RadioCom
  150. v-bind="$attrs"
  151. :label="labelCpt"
  152. :param="param"
  153. @emitParam="
  154. (val) => {
  155. $emit('update:param', val), handleValidate(val)
  156. }
  157. "
  158. >
  159. </RadioCom>
  160. </template>
  161. <template v-else-if="link === 'checkbox'">
  162. <CheckboxCom
  163. v-bind="$attrs"
  164. :label="labelCpt"
  165. :param="param"
  166. @emitParam="
  167. (val) => {
  168. $emit('update:param', val), handleValidate(val)
  169. }
  170. "
  171. >
  172. </CheckboxCom>
  173. </template>
  174. <template v-else-if="link === 'number'">
  175. <NumberCom
  176. v-bind="$attrs"
  177. :label="labelCpt"
  178. :param="param"
  179. @emitParam="
  180. (val) => {
  181. $emit('update:param', val), handleValidate(val)
  182. }
  183. "
  184. @emitEnter="handleEnter"
  185. >
  186. <template v-if="$slots.prefix" #prefix>
  187. <slot name="prefix" />
  188. </template>
  189. <template v-if="$slots.suffix" #suffix>
  190. <slot name="suffix" />
  191. </template>
  192. <template v-if="$slots.prepend" #prepend>
  193. <slot name="prepend" />
  194. </template>
  195. <template v-if="$slots.append" #append>
  196. <slot name="append" />
  197. </template>
  198. </NumberCom>
  199. </template>
  200. <template v-else-if="link === 'input-number'">
  201. <InputNumberCom
  202. v-bind="$attrs"
  203. :label="labelCpt"
  204. :param="param"
  205. @emitParam="
  206. (val) => {
  207. $emit('update:param', val), handleValidate(val)
  208. }
  209. "
  210. @emitEnter="handleEnter"
  211. >
  212. <template v-if="$slots.prefix" #prefix>
  213. <slot name="prefix" />
  214. </template>
  215. <template v-if="$slots.suffix" #suffix>
  216. <slot name="suffix" />
  217. </template>
  218. </InputNumberCom>
  219. </template>
  220. <template v-else-if="link === 'tree-select'">
  221. <TreeSelectCom
  222. v-bind="$attrs"
  223. :label="labelCpt"
  224. :param="param"
  225. @emitParam="
  226. (val) => {
  227. $emit('update:param', val), handleValidate(val)
  228. }
  229. "
  230. >
  231. <template v-if="$slots.default" #default="prop">
  232. <slot name="default" v-bind="prop" />
  233. </template>
  234. </TreeSelectCom>
  235. </template>
  236. <template v-else-if="link === 'upload'">
  237. <UploadCom
  238. v-bind="$attrs"
  239. :label="labelCpt"
  240. :param="param"
  241. @emitParam="
  242. (val) => {
  243. $emit('update:param', val), handleValidate(val)
  244. }
  245. "
  246. />
  247. </template>
  248. <template v-else-if="link === 'rich'">
  249. <RichCom
  250. v-bind="$attrs"
  251. :label="labelCpt"
  252. :param="param"
  253. @emitParam="
  254. (val) => {
  255. $emit('update:param', val), handleValidate(val)
  256. }
  257. "
  258. >
  259. </RichCom>
  260. </template>
  261. <template v-else-if="link === 'markdown'">
  262. <MarkdownCom
  263. v-bind="$attrs"
  264. :label="labelCpt"
  265. :param="param"
  266. @emitParam="
  267. (val) => {
  268. $emit('update:param', val), handleValidate(val)
  269. }
  270. "
  271. >
  272. </MarkdownCom>
  273. </template>
  274. </slot>
  275. <div v-if="unit" class="unit">{{ unit }}</div>
  276. <slot name="unit" />
  277. </el-form-item>
  278. </el-col>
  279. </template>
  280. <script setup lang="ts">
  281. defineOptions({
  282. name: 'CzrFormColumn',
  283. })
  284. import {
  285. computed,
  286. onMounted,
  287. ref,
  288. reactive,
  289. watch,
  290. getCurrentInstance,
  291. ComponentInternalInstance,
  292. inject,
  293. onBeforeUnmount,
  294. } from 'vue'
  295. import InputCom from './czr-form-link/input.vue'
  296. import InputNumberCom from './czr-form-link/input-number.vue'
  297. import SelectCom from './czr-form-link/select.vue'
  298. import DateCom from './czr-form-link/date.vue'
  299. import DateTimeCom from './czr-form-link/datetime.vue'
  300. import TimeCom from './czr-form-link/time.vue'
  301. import CascaderCom from './czr-form-link/cascader.vue'
  302. import SwitchCom from './czr-form-link/switch.vue'
  303. import RadioCom from './czr-form-link/radio.vue'
  304. import CheckboxCom from './czr-form-link/checkbox.vue'
  305. import NumberCom from './czr-form-link/number.vue'
  306. import TreeSelectCom from './czr-form-link/tree-select.vue'
  307. import UploadCom from './czr-form-link/upload.vue'
  308. import RichCom from './czr-form-link/rich.vue'
  309. import MarkdownCom from './czr-form-link/markdown.vue'
  310. import { v4 } from 'uuid'
  311. const props = defineProps({
  312. span: { type: Number, default: 6 }, // 栅格
  313. offset: { type: Number, default: 0 }, // 栅格
  314. param: {}, // 绑定值
  315. label: { type: String, default: '' }, // 标题
  316. required: { default: false }, // 必填项
  317. labelWidth: { type: String, default: '' }, // 标题宽度
  318. link: {
  319. type: String,
  320. default: 'input',
  321. validator(val: string) {
  322. return [
  323. 'cascader',
  324. 'checkbox',
  325. 'date',
  326. 'datetime',
  327. 'input',
  328. 'radio',
  329. 'select',
  330. 'switch',
  331. 'dept',
  332. 'time',
  333. 'upload',
  334. 'number',
  335. 'input-number',
  336. 'tree-select',
  337. 'rich',
  338. 'markdown',
  339. ].includes(val)
  340. },
  341. }, // 类型,为了避免与原生type字段重复
  342. rules: { type: Array, default: () => [] }, // 自定义规则
  343. maxLength: { type: Number, default: null }, // 最大长度
  344. minLength: { type: Number, default: null }, // 最小长度
  345. defaultErrorMsg: { default: null }, // 默认校验错误提示
  346. unit: { default: '', type: String }, // 单位
  347. otherInfo: {}, // 其他信息
  348. layout: {},
  349. transparent: { default: false },
  350. width: { default: '' },
  351. labelFit: { default: true },
  352. })
  353. const attrs = (getCurrentInstance() as ComponentInternalInstance).attrs
  354. const state = reactive({
  355. errorMessage: null,
  356. uuid: '',
  357. })
  358. const ref_el = ref()
  359. const ref_czrFormColumn: any = ref(null)
  360. const isValue = (val: any) => {
  361. if (
  362. val === null ||
  363. val === undefined ||
  364. (typeof val === 'string' && val.trim() === '') ||
  365. (typeof val === 'object' && val?.length === 0)
  366. ) {
  367. return false
  368. }
  369. return true
  370. }
  371. const labelCpt = computed(() => {
  372. return props.label.replace(/[::]([^::]*)$/, '')
  373. })
  374. const rulesCpt = computed(() => {
  375. const r = [...props.rules]
  376. if (isValue(props.minLength)) {
  377. r.unshift({
  378. handle: (val: any) =>
  379. !isValue(val) ||
  380. (isValue(val) && String(val).length >= props.minLength),
  381. message: `内容过短,字数需大于等于${props.minLength}`,
  382. })
  383. }
  384. if (isValue(props.maxLength)) {
  385. r.unshift({
  386. handle: (val: any) =>
  387. !isValue(val) ||
  388. (isValue(val) && String(val).length <= props.maxLength),
  389. message: `内容过长,字数需小于等于${props.maxLength}`,
  390. })
  391. } else if (
  392. props.link === 'input' &&
  393. attrs.type === 'textarea' &&
  394. isValue(attrs.maxlength)
  395. ) {
  396. r.unshift({
  397. handle: (val: any) =>
  398. !isValue(val) ||
  399. (isValue(val) && String(val).length <= Number(attrs.maxlength)),
  400. message: `内容过长,字数需小于等于${attrs.maxlength}`,
  401. })
  402. }
  403. const doStr = [
  404. 'input',
  405. 'number',
  406. 'input-number',
  407. 'rich',
  408. 'markdown',
  409. ].includes(props.link)
  410. ? '输入'
  411. : '选择'
  412. if (
  413. props.required !== false &&
  414. !props.rules.some((v: any) => v.type === 'default')
  415. ) {
  416. r.unshift({
  417. handle: (val: any) => isValue(val),
  418. message: props.defaultErrorMsg ?? `请${doStr}${labelCpt.value}`,
  419. })
  420. }
  421. return r
  422. })
  423. const handleValidate = (val: any = undefined, val2: any = undefined) => {
  424. state.errorMessage = null
  425. for (let i = 0; i < rulesCpt.value.length; i++) {
  426. const item: any = rulesCpt.value[i]
  427. if (!item.handle(val === undefined ? props.param : val)) {
  428. state.errorMessage = item.message
  429. break
  430. }
  431. }
  432. return state.errorMessage
  433. }
  434. const formLayout = inject('form-layout', 'x')
  435. const handleEnterFunc = inject('handle-enter', () => {})
  436. const formLabelWidth = inject('form-label-width', '')
  437. const labelWidthCpt = computed(() => {
  438. if (props.labelWidth) {
  439. return props.labelWidth
  440. }
  441. if (formLabelWidth) {
  442. return formLabelWidth
  443. }
  444. return 'auto'
  445. })
  446. const handleEnter = () => {
  447. handleEnterFunc?.()
  448. }
  449. const reset = () => {
  450. state.errorMessage = null
  451. }
  452. watch(
  453. () => state.errorMessage,
  454. (n) => {
  455. const p =
  456. ref_czrFormColumn.value?.$el?.getElementsByClassName('el-form-item')?.[0]
  457. if (n) {
  458. setTimeout(() => {
  459. const e = p
  460. ?.getElementsByClassName('el-form-item__content')?.[0]
  461. ?.getElementsByClassName('el-form-item__error')?.[0]
  462. if (e?.clientHeight) {
  463. p.style.marginBottom = `${e.clientHeight + 4}px`
  464. }
  465. }, 200)
  466. } else {
  467. p.style.marginBottom = props.transparent ? 0 : '18px'
  468. }
  469. },
  470. )
  471. onMounted(() => {
  472. state.uuid = v4()
  473. const formChildrenMap: any = inject('czr-form-children-map', new Map())
  474. let flag = true
  475. const deep = (dom) => {
  476. if (dom.className === 'hidden-columns') {
  477. flag = false
  478. }
  479. if (dom.parentElement) {
  480. deep(dom.parentElement)
  481. }
  482. }
  483. deep(ref_czrFormColumn.value.$parent.$el)
  484. if (flag) {
  485. formChildrenMap.set(state.uuid, ref_czrFormColumn.value.$parent)
  486. }
  487. })
  488. onBeforeUnmount(() => {
  489. const formChildrenMap: any = inject('czr-form-children-map', new Map())
  490. formChildrenMap.delete(state.uuid)
  491. })
  492. defineExpose({
  493. reset,
  494. handleValidate,
  495. handleEnter,
  496. focus: () => ref_el.value.focus(),
  497. })
  498. </script>
  499. <style scoped lang="scss">
  500. .czr-form-column {
  501. &.has-width {
  502. width: v-bind(width);
  503. max-width: v-bind(width);
  504. flex: unset;
  505. }
  506. &.transparent {
  507. :deep(.el-form-item) {
  508. margin-bottom: 0;
  509. .el-input__wrapper {
  510. background-color: transparent;
  511. box-shadow: none;
  512. .el-input__count-inner {
  513. background-color: transparent;
  514. }
  515. }
  516. }
  517. }
  518. :deep(.el-form-item) {
  519. min-height: 2.25rem;
  520. &.link_time {
  521. margin-top: 1px;
  522. }
  523. &.no-label {
  524. .el-form-item__label {
  525. display: none;
  526. }
  527. }
  528. &.no-label-fit {
  529. .el-form-item__label-wrap {
  530. margin-left: 0 !important;
  531. }
  532. }
  533. &.layout-y {
  534. .el-form-item__label {
  535. justify-content: flex-start;
  536. padding-left: 0;
  537. }
  538. }
  539. &.layout-x {
  540. display: flex;
  541. }
  542. $textColor: #576275;
  543. .el-form-item__label {
  544. line-height: 1;
  545. text-align: right;
  546. display: flex;
  547. align-items: center;
  548. padding-left: 0.25rem;
  549. color: $textColor;
  550. font-weight: normal;
  551. }
  552. .el-form-item__content {
  553. flex-wrap: unset;
  554. > div:first-child {
  555. flex: 1;
  556. height: 100%;
  557. }
  558. .unit {
  559. margin-left: 6px;
  560. font-size: 14px;
  561. font-family:
  562. PingFang SC-Regular,
  563. PingFang SC;
  564. font-weight: 400;
  565. color: #606266;
  566. }
  567. .is-disabled {
  568. .el-select__selected-item:not(.is-transparent),
  569. .el-input__inner,
  570. .el-radio__label,
  571. .el-checkbox__label,
  572. .el-range-input,
  573. .el-range-separator,
  574. .el-textarea__inner {
  575. color: $textColor;
  576. -webkit-text-fill-color: $textColor;
  577. }
  578. }
  579. .el-input__wrapper,
  580. .el-textarea__inner {
  581. border-radius: 0.25rem;
  582. }
  583. }
  584. }
  585. }
  586. </style>