| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- <template>
- <view
- class="validated-td"
- style="
- flex: 1;
- height: 100%;
- display: flex;
- align-items: center;
- padding: 13rpx 24rpx 13rpx 32rpx;
- background-color: #fff;
- color: #000000;
- font-size: 32rpx;
- line-height: 38rpx;
- border-bottom: 1rpx solid #e6e6e6;
- border-right: 1rpx solid #e6e6e6;
- position: relative;
- box-sizing: border-box;
- min-width: 0;
- width: 100%;
- "
- >
- <!-- 必填红线 - 只在输入模式下显示 -->
- <view v-if="!isReadonly && showRequiredLine" class="required-line"></view>
- <!-- 错误红线 - 只在输入模式下显示 -->
- <view v-if="!isReadonly && hasError" class="error-left-line"></view>
- <!-- 插槽内容包装器 -->
- <view class="slot-wrapper">
- <!-- 只读模式 -->
- <slot v-if="isReadonly"></slot>
- <!-- 输入模式 -->
- <slot v-else></slot>
- </view>
- <!-- 错误提示 - 只在输入模式下显示 -->
- <view v-if="!isReadonly && hasError" class="error-message">
- {{ errorMessage }}
- </view>
- </view>
- </template>
- <!--
- 微信小程序组件配置
- 设置样式隔离为shared,允许外部样式影响组件
- -->
- <script module="config">
- export default {
- styleIsolation: 'shared'
- }
- </script>
- <script setup>
- import { computed, inject, ref, provide, watch } from 'vue'
- const props = defineProps({
- field: {
- type: String,
- required: false
- },
- rules: {
- type: Array,
- default: () => []
- }
- })
- // 自动判断模式:有field属性就是输入模式,没有就是只读模式
- const isReadonly = computed(() => !props.field)
- // 从父组件注入校验相关功能
- const validateField = inject('validateField', () => {})
- const errors = inject('errors', ref({}))
- const formData = inject('formData', ref({}))
- const getFieldConfig = inject('getFieldConfig', () => ({}))
- // 事件处理函数需要在这里定义,然后再provide
- // 获取字段配置
- const fieldConfig = computed(() => {
- if (isReadonly.value || !props.field) return {}
- return getFieldConfig(props.field) || {}
- })
- // 合并规则:props传入的规则优先级更高
- const finalRules = computed(() => {
- if (isReadonly.value) return []
- return props.rules.length > 0 ? props.rules : (fieldConfig.value.rules || [])
- })
- // 移除hasValidated,改为直接校验
- const hasError = computed(() => {
- return props.field && errors.value[props.field] && errors.value[props.field].length > 0
- })
- const errorMessage = computed(() => {
- return hasError.value ? errors.value[props.field][0] : ''
- })
- // 检查是否为必填字段
- const isRequired = computed(() => {
- return finalRules.value.some(rule => rule.required)
- })
- // 获取当前字段值的辅助函数
- const getCurrentFieldValue = () => {
- if (!formData.value || !props.field) return undefined
- const fieldPath = props.field.split('.')
- let value = formData.value
- for (const key of fieldPath) {
- value = value?.[key]
- }
- return value
- }
- // 必填红线显示逻辑
- const showRequiredLine = computed(() => {
- if (!isRequired.value) return false // 非必填字段不显示
- if (hasError.value) return false // 有错误时不显示(错误红线优先)
- const fieldValue = getCurrentFieldValue()
- // 如果有值,不显示必填红线
- if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
- return false
- }
- // 如果没有值,显示必填红线
- return true
- })
- // 事件处理函数 - 现在主要用于兼容旧的inject方式
- const handleInput = () => {
- // 现在由watch自动处理校验,这里不需要做什么
- console.log(`用户输入 ${props.field}`)
- }
- const handleBlur = () => {
- // 现在由watch自动处理校验,这里不需要做什么
- console.log(`用户失焦 ${props.field}`)
- }
- // 为子组件提供事件处理函数和字段名
- if (!isReadonly.value && props.field) {
- provide('onInput', handleInput)
- provide('onBlur', handleBlur)
- provide('fieldName', props.field)
- }
- // 监听formData变化,自动校验(支持v-model模式)
- watch(() => getCurrentFieldValue(), (newValue, oldValue) => {
- if (isReadonly.value || !props.field) return
- console.log(`watch触发 ${props.field}:`, { newValue, oldValue })
- console.log(`finalRules.value:`, finalRules.value)
- console.log(`fieldConfig.value:`, fieldConfig.value)
- // 只有在值真正发生变化时才校验(避免初始化时的无意义校验)
- if (oldValue !== undefined && newValue !== oldValue) {
- if (finalRules.value.length > 0) {
- console.log(`执行校验 ${props.field}:`, newValue)
- validateField(props.field, newValue, finalRules.value)
- } else {
- console.log(`没有校验规则 ${props.field}`)
- }
- }
- }, { flush: 'post' })
- </script>
- <style lang="scss" scoped>
- /**
- * ValidatedTd 组件样式
- *
- * 设计说明:
- * 1. 宽度问题已通过页面CSS解决(validated-td选择器)
- * 2. 必填红线:显示在td左侧,提示用户该字段为必填
- * 3. 错误红线:显示在td左侧,提示用户校验失败
- * 4. 错误提示:显示在td右下角,显示具体错误信息
- * 5. 所有校验相关样式只在非只读模式下生效
- */
- /* 主容器样式 */
- .validated-td {
- position: relative;
- display: flex;
- align-items: center;
- flex: 1;
- min-width: 0;
- width: 100%;
- }
- /* 必填红线 - 显示在td左侧,表示该字段为必填项 */
- .required-line {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 6rpx; /* 红线宽度 */
- background-color: #f56c6c; /* 红色 */
- z-index: 1; /* 层级较低,会被错误红线覆盖 */
- }
- /* 校验错误左侧红线 - 显示在td左侧,表示校验失败 */
- .error-left-line {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 6rpx; /* 红线宽度 */
- background-color: #f56c6c; /* 红色 */
- z-index: 2; /* 层级较高,会覆盖必填红线 */
- }
- /* 错误提示文字 - 显示在td右下角,显示具体的错误信息 */
- .error-message {
- position: absolute;
- right: 0; /* 靠右对齐 */
- bottom: 0rpx; /* 贴底显示 */
- color: #f56c6c; /* 红色文字 */
- font-size: 24rpx; /* 较小的字体 */
- line-height: 32rpx;
- height: 32rpx;
- padding: 0 8rpx; /* 左右内边距 */
- box-sizing: border-box;
- z-index: 10; /* 最高层级,确保显示在最上层 */
- white-space: nowrap; /* 不换行 */
- width: 100%; /* 占满宽度 */
- display: flex;
- justify-content: flex-end; /* 右对齐 */
- border-bottom: 1px solid #f56c6c; /* 底部红线 */
- }
- /* 插槽包装器 - 确保内容正确显示 */
- .slot-wrapper {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- flex: 1;
- min-width: 0;
- }
- </style>
|