index.vue 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. <template>
  2. <view
  3. class="validated-td"
  4. style="
  5. flex: 1;
  6. height: 100%;
  7. display: flex;
  8. align-items: center;
  9. padding: 13rpx 24rpx 13rpx 32rpx;
  10. background-color: #fff;
  11. color: #000000;
  12. font-size: 32rpx;
  13. line-height: 38rpx;
  14. border-bottom: 1rpx solid #e6e6e6;
  15. border-right: 1rpx solid #e6e6e6;
  16. position: relative;
  17. box-sizing: border-box;
  18. min-width: 0;
  19. width: 100%;
  20. "
  21. >
  22. <!-- 必填红线 - 只在输入模式下显示 -->
  23. <view v-if="!isReadonly && showRequiredLine" class="required-line"></view>
  24. <!-- 错误红线 - 只在输入模式下显示 -->
  25. <view v-if="!isReadonly && hasError" class="error-left-line"></view>
  26. <!-- 插槽内容包装器 -->
  27. <view class="slot-wrapper">
  28. <!-- 只读模式 -->
  29. <slot v-if="isReadonly"></slot>
  30. <!-- 输入模式 -->
  31. <slot v-else></slot>
  32. </view>
  33. <!-- 错误提示 - 只在输入模式下显示 -->
  34. <view v-if="!isReadonly && hasError" class="error-message">
  35. {{ errorMessage }}
  36. </view>
  37. </view>
  38. </template>
  39. <!--
  40. 微信小程序组件配置
  41. 设置样式隔离为shared,允许外部样式影响组件
  42. -->
  43. <script module="config">
  44. export default {
  45. styleIsolation: 'shared'
  46. }
  47. </script>
  48. <script setup>
  49. import { computed, inject, ref, provide, watch } from 'vue'
  50. const props = defineProps({
  51. field: {
  52. type: String,
  53. required: false
  54. },
  55. rules: {
  56. type: Array,
  57. default: () => []
  58. }
  59. })
  60. // 自动判断模式:有field属性就是输入模式,没有就是只读模式
  61. const isReadonly = computed(() => !props.field)
  62. // 从父组件注入校验相关功能
  63. const validateField = inject('validateField', () => {})
  64. const errors = inject('errors', ref({}))
  65. const formData = inject('formData', ref({}))
  66. const getFieldConfig = inject('getFieldConfig', () => ({}))
  67. // 事件处理函数需要在这里定义,然后再provide
  68. // 获取字段配置
  69. const fieldConfig = computed(() => {
  70. if (isReadonly.value || !props.field) return {}
  71. return getFieldConfig(props.field) || {}
  72. })
  73. // 合并规则:props传入的规则优先级更高
  74. const finalRules = computed(() => {
  75. if (isReadonly.value) return []
  76. return props.rules.length > 0 ? props.rules : (fieldConfig.value.rules || [])
  77. })
  78. // 移除hasValidated,改为直接校验
  79. const hasError = computed(() => {
  80. return props.field && errors.value[props.field] && errors.value[props.field].length > 0
  81. })
  82. const errorMessage = computed(() => {
  83. return hasError.value ? errors.value[props.field][0] : ''
  84. })
  85. // 检查是否为必填字段
  86. const isRequired = computed(() => {
  87. return finalRules.value.some(rule => rule.required)
  88. })
  89. // 获取当前字段值的辅助函数
  90. const getCurrentFieldValue = () => {
  91. if (!formData.value || !props.field) return undefined
  92. const fieldPath = props.field.split('.')
  93. let value = formData.value
  94. for (const key of fieldPath) {
  95. value = value?.[key]
  96. }
  97. return value
  98. }
  99. // 必填红线显示逻辑
  100. const showRequiredLine = computed(() => {
  101. if (!isRequired.value) return false // 非必填字段不显示
  102. if (hasError.value) return false // 有错误时不显示(错误红线优先)
  103. const fieldValue = getCurrentFieldValue()
  104. // 如果有值,不显示必填红线
  105. if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
  106. return false
  107. }
  108. // 如果没有值,显示必填红线
  109. return true
  110. })
  111. // 事件处理函数 - 现在主要用于兼容旧的inject方式
  112. const handleInput = () => {
  113. // 现在由watch自动处理校验,这里不需要做什么
  114. console.log(`用户输入 ${props.field}`)
  115. }
  116. const handleBlur = () => {
  117. // 现在由watch自动处理校验,这里不需要做什么
  118. console.log(`用户失焦 ${props.field}`)
  119. }
  120. // 为子组件提供事件处理函数和字段名
  121. if (!isReadonly.value && props.field) {
  122. provide('onInput', handleInput)
  123. provide('onBlur', handleBlur)
  124. provide('fieldName', props.field)
  125. }
  126. // 监听formData变化,自动校验(支持v-model模式)
  127. watch(() => getCurrentFieldValue(), (newValue, oldValue) => {
  128. if (isReadonly.value || !props.field) return
  129. console.log(`watch触发 ${props.field}:`, { newValue, oldValue })
  130. console.log(`finalRules.value:`, finalRules.value)
  131. console.log(`fieldConfig.value:`, fieldConfig.value)
  132. // 只有在值真正发生变化时才校验(避免初始化时的无意义校验)
  133. if (oldValue !== undefined && newValue !== oldValue) {
  134. if (finalRules.value.length > 0) {
  135. console.log(`执行校验 ${props.field}:`, newValue)
  136. validateField(props.field, newValue, finalRules.value)
  137. } else {
  138. console.log(`没有校验规则 ${props.field}`)
  139. }
  140. }
  141. }, { flush: 'post' })
  142. </script>
  143. <style lang="scss" scoped>
  144. /**
  145. * ValidatedTd 组件样式
  146. *
  147. * 设计说明:
  148. * 1. 宽度问题已通过页面CSS解决(validated-td选择器)
  149. * 2. 必填红线:显示在td左侧,提示用户该字段为必填
  150. * 3. 错误红线:显示在td左侧,提示用户校验失败
  151. * 4. 错误提示:显示在td右下角,显示具体错误信息
  152. * 5. 所有校验相关样式只在非只读模式下生效
  153. */
  154. /* 主容器样式 */
  155. .validated-td {
  156. position: relative;
  157. display: flex;
  158. align-items: center;
  159. flex: 1;
  160. min-width: 0;
  161. width: 100%;
  162. }
  163. /* 必填红线 - 显示在td左侧,表示该字段为必填项 */
  164. .required-line {
  165. position: absolute;
  166. left: 0;
  167. top: 0;
  168. bottom: 0;
  169. width: 6rpx; /* 红线宽度 */
  170. background-color: #f56c6c; /* 红色 */
  171. z-index: 1; /* 层级较低,会被错误红线覆盖 */
  172. }
  173. /* 校验错误左侧红线 - 显示在td左侧,表示校验失败 */
  174. .error-left-line {
  175. position: absolute;
  176. left: 0;
  177. top: 0;
  178. bottom: 0;
  179. width: 6rpx; /* 红线宽度 */
  180. background-color: #f56c6c; /* 红色 */
  181. z-index: 2; /* 层级较高,会覆盖必填红线 */
  182. }
  183. /* 错误提示文字 - 显示在td右下角,显示具体的错误信息 */
  184. .error-message {
  185. position: absolute;
  186. right: 0; /* 靠右对齐 */
  187. bottom: 0rpx; /* 贴底显示 */
  188. color: #f56c6c; /* 红色文字 */
  189. font-size: 24rpx; /* 较小的字体 */
  190. line-height: 32rpx;
  191. height: 32rpx;
  192. padding: 0 8rpx; /* 左右内边距 */
  193. box-sizing: border-box;
  194. z-index: 10; /* 最高层级,确保显示在最上层 */
  195. white-space: nowrap; /* 不换行 */
  196. width: 100%; /* 占满宽度 */
  197. display: flex;
  198. justify-content: flex-end; /* 右对齐 */
  199. border-bottom: 1px solid #f56c6c; /* 底部红线 */
  200. }
  201. /* 插槽包装器 - 确保内容正确显示 */
  202. .slot-wrapper {
  203. width: 100%;
  204. height: 100%;
  205. display: flex;
  206. align-items: center;
  207. flex: 1;
  208. min-width: 0;
  209. }
  210. </style>