cl-form-item.uvue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. <template>
  2. <view
  3. class="cl-form-item"
  4. :class="[
  5. {
  6. 'cl-form-item--error': isError
  7. },
  8. pt.className
  9. ]"
  10. >
  11. <view class="cl-form-item__inner" :class="[`is-${labelPosition}`, pt.inner?.className]">
  12. <view
  13. class="cl-form-item__label"
  14. :class="[`is-${labelPosition}`, pt.label?.className]"
  15. :style="{
  16. width: labelPosition != 'top' ? labelWidth : 'auto'
  17. }"
  18. v-if="label != ''"
  19. >
  20. <cl-text>{{ label }}</cl-text>
  21. <cl-text
  22. color="error"
  23. :pt="{
  24. className: 'ml-1'
  25. }"
  26. v-if="showAsterisk"
  27. >
  28. *
  29. </cl-text>
  30. </view>
  31. <view class="cl-form-item__content" :class="[pt.content?.className]">
  32. <slot></slot>
  33. </view>
  34. </view>
  35. <view class="cl-form-item__error" v-if="isError && showMessage">
  36. <slot name="error" :error="errorText">
  37. <cl-text
  38. color="error"
  39. :pt="{
  40. className: parseClass(['mt-2 !text-sm', pt.error?.className])
  41. }"
  42. >
  43. {{ errorText }}
  44. </cl-text>
  45. </slot>
  46. </view>
  47. </view>
  48. </template>
  49. <script setup lang="ts">
  50. import { computed, onMounted, onUnmounted, watch, type PropType } from "vue";
  51. import { isEqual, parseClass, parsePt } from "@/cool";
  52. import type { ClFormLabelPosition, PassThroughProps } from "../../types";
  53. import { useForm } from "../../hooks";
  54. defineOptions({
  55. name: "cl-form-item"
  56. });
  57. defineSlots<{
  58. error(props: { error: string }): any;
  59. }>();
  60. // 组件属性定义
  61. const props = defineProps({
  62. // 透传样式
  63. pt: {
  64. type: Object,
  65. default: () => ({})
  66. },
  67. // 字段标签
  68. label: {
  69. type: String,
  70. default: ""
  71. },
  72. // 字段名称
  73. prop: {
  74. type: String,
  75. default: ""
  76. },
  77. // 标签位置
  78. labelPosition: {
  79. type: String as PropType<ClFormLabelPosition>,
  80. default: null
  81. },
  82. // 标签宽度
  83. labelWidth: {
  84. type: String as PropType<string | null>,
  85. default: null
  86. },
  87. // 是否显示必填星号
  88. showAsterisk: {
  89. type: Boolean as PropType<boolean | null>,
  90. default: null
  91. },
  92. // 是否显示错误信息
  93. showMessage: {
  94. type: Boolean as PropType<boolean | null>,
  95. default: null
  96. },
  97. // 是否必填
  98. required: {
  99. type: Boolean,
  100. default: false
  101. }
  102. });
  103. // cl-form 上下文
  104. const { formRef, getError, getValue, validateField, addField, removeField } = useForm();
  105. // 透传样式类型
  106. type PassThrough = {
  107. className?: string;
  108. inner?: PassThroughProps;
  109. label?: PassThroughProps;
  110. content?: PassThroughProps;
  111. error?: PassThroughProps;
  112. };
  113. // 解析透传样式
  114. const pt = computed(() => parsePt<PassThrough>(props.pt));
  115. // 当前错误信息
  116. const errorText = computed<string>(() => {
  117. return getError(props.prop);
  118. });
  119. // 是否有错误
  120. const isError = computed<boolean>(() => {
  121. return errorText.value != "";
  122. });
  123. // 当前标签位置
  124. const labelPosition = computed<ClFormLabelPosition>(() => {
  125. return props.labelPosition ?? formRef.value?.labelPosition ?? "left";
  126. });
  127. // 标签宽度
  128. const labelWidth = computed<string>(() => {
  129. return props.labelWidth ?? formRef.value?.labelWidth ?? "120rpx";
  130. });
  131. // 是否显示必填星号
  132. const showAsterisk = computed<boolean>(() => {
  133. if (!props.required) {
  134. return false;
  135. }
  136. return props.showAsterisk ?? formRef.value?.showAsterisk ?? true;
  137. });
  138. // 是否显示错误信息
  139. const showMessage = computed<boolean>(() => {
  140. if (!props.required) {
  141. return false;
  142. }
  143. return props.showMessage ?? formRef.value?.showMessage ?? true;
  144. });
  145. watch(
  146. computed(() => props.required),
  147. (val: boolean) => {
  148. if (val) {
  149. addField(props.prop);
  150. } else {
  151. removeField(props.prop);
  152. }
  153. },
  154. {
  155. immediate: true
  156. }
  157. );
  158. onMounted(() => {
  159. watch(
  160. computed(() => {
  161. const value = getValue(props.prop);
  162. if (value == null) {
  163. return "";
  164. }
  165. return value;
  166. }),
  167. (a: any, b: any) => {
  168. if (props.required) {
  169. if (!isEqual(a, b)) {
  170. validateField(props.prop);
  171. }
  172. }
  173. },
  174. {
  175. deep: true
  176. }
  177. );
  178. });
  179. onUnmounted(() => {
  180. removeField(props.prop);
  181. });
  182. defineExpose({
  183. prop: props.prop
  184. });
  185. </script>
  186. <style lang="scss" scoped>
  187. .cl-form-item {
  188. @apply w-full mb-6;
  189. &__inner {
  190. @apply w-full;
  191. &.is-top {
  192. @apply flex flex-col;
  193. }
  194. &.is-left {
  195. @apply flex flex-row;
  196. }
  197. &.is-right {
  198. @apply flex flex-row;
  199. }
  200. }
  201. &__label {
  202. @apply flex flex-row items-center;
  203. &.is-top {
  204. @apply w-full mb-2;
  205. }
  206. &.is-left {
  207. @apply mr-3;
  208. }
  209. &.is-right {
  210. @apply mr-3 justify-end;
  211. }
  212. }
  213. &__content {
  214. @apply relative flex-1 w-full;
  215. }
  216. }
  217. </style>