小程序难调的组件

背景。做小程序用到了自定义表单。前后端都是分开写的,没有使用web-view。所以要做到功能对称

  • 时间选择器。需要区分datetime, year, day等类型使用uview组件较方便
js 复制代码
<template>
  <view class="u-date-picker" v-if="visible">
    <view class="label-container">
      <text v-if="required" class="required-mark">*</text>
      <text class="label">{{ label }}</text>
    </view>
    <view class="picker-container" :class="{ 'picker-disabled': disabled }" @click="showPicker">
      <text class="picker-text" :class="{ 'text-disabled': disabled }">{{ displayText }}</text>
      <text class="picker-arrow" :class="{ 'arrow-disabled': disabled }">></text>
    </view>
    
    <!-- 日期选择器弹窗 -->
    <u-picker
      v-model="showPickerFlag"
      mode="time"
      :params="pickerParams"
      :default-time="defaultTime"
      :start-year="startYear"
      :end-year="endYear"
      :show-time-tag="showTimeTag"
      @confirm="onConfirm"
      @cancel="onCancel">
    </u-picker>
  </view>
</template>

<script>
export default {
  name: 'UDatePicker',
  props: {
    /**
     * 标签文本
     */
    label: {
      type: String,
      default: '选择日期'
    },
    /**
     * 占位符文本
     */
    placeholder: {
      type: String,
      default: '请选择日期'
    },
    /**
     * 当前选中的值
     */
    value: {
      type: String,
      default: ''
    },
    /**
     * 日期类型:date-仅日期,datetime-日期时间
     */
    type: {
      type: String,
      default: 'date'
    },
    /**
     * 开始年份
     */
    startYear: {
      type: [String, Number],
      default: 1950
    },
    /**
     * 结束年份
     */
    endYear: {
      type: [String, Number],
      default: 2050
    },
    /**
     * 是否显示时间标签(年月日时分秒)
     */
    showTimeTag: {
      type: Boolean,
      default: true
    },
    /**
     * 日期格式
     */
    format: {
      type: String,
      default: 'YYYY-MM-DD'
    },
    /**
     * 是否禁用
     */
    disabled: {
      type: Boolean,
      default: false
    },
    /**
     * 是否必填
     */
    required: {
      type: Boolean,
      default: false
    },
    /**
     * 是否可见
     */
    visible: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      showPickerFlag: false,
      pickerParams: this.getPickerParams(),
      defaultTime: '',
      selectedDate: ''
    };
  },
  computed: {
    /**
     * 显示的文本
     */
    displayText() {
      if (this.value) {
        const t = (this.type || 'date').toLowerCase();
        if (t === 'year') {
          // 年份类型:直接显示年份
          return this.value;
        } else if (t === 'month') {
          // 月份类型:显示年月
          return this.value;
        } else if (t === 'date') {
          // 日期类型:显示完整日期
          return this.value;
        } else if (t === 'week') {
          // 周类型:显示日期并计算周数
          try {
            const date = new Date(this.value);
            if (!isNaN(date.getTime())) {
              const weekNumber = this.getWeekNumber(date);
              return `${this.value} (第${weekNumber}周)`;
            }
          } catch (e) {
            console.error('日期解析错误:', e);
          }
          return this.value;
        } else if (t === 'datetime') {
          // 日期时间类型:显示完整日期时间
          return this.value;
        } else if (t.indexOf('time') !== -1) {
          // 时间类型:显示时间
          return this.value;
        }
        return this.value;
      }
      return this.placeholder;
    },
    /**
     * 根据类型确定选择器模式
     */
    pickerMode() {
      const t = (this.type || 'date').toLowerCase();
      if (t === 'year') return 'date';
      if (t === 'month') return 'date';
      if (t === 'week') return 'date';
      if (t === 'date') return 'date';
      if (t.indexOf('time') !== -1) return 'time';
      if (t === 'datetime') return 'date'; // datetime 降级为 date
      return 'date';
    }
  },
  watch: {
    /**
     * 监听类型变化,更新选择器参数
     */
    type: {
      handler(newType) {
        this.pickerParams = this.getPickerParams();
      },
      immediate: true
    },
    /**
     * 监听值变化,更新默认时间
     */
    value: {
      handler(newValue) {
        if (newValue) {
          this.defaultTime = newValue;
        }
      },
      immediate: true
    }
  },
  methods: {
    /**
     * 根据类型获取选择器参数
     */
    getPickerParams() {
      const t = (this.type || 'date').toLowerCase();
      
      if (t === 'year') {
        // 年份选择:只显示年份
        return {
          year: true,
          month: false,
          day: false,
          hour: false,
          minute: false,
          second: false
        };
      } else if (t === 'month') {
        // 月份选择:显示年月
        return {
          year: true,
          month: true,
          day: false,
          hour: false,
          minute: false,
          second: false
        };
      } else if (t === 'date') {
        // 日期选择:显示年月日
        return {
          year: true,
          month: true,
          day: true,
          hour: false,
          minute: false,
          second: false
        };
      } else if (t === 'week') {
        // 周选择:显示年月日(周选择需要完整日期来计算周数)
        return {
          year: true,
          month: true,
          day: true,
          hour: false,
          minute: false,
          second: false
        };
      } else if (t === 'datetime') {
        // 日期时间选择:显示年月日时分秒
        return {
          year: true,
          month: true,
          day: true,
          hour: true,
          minute: true,
          second: true
        };
      } else if (t.indexOf('time') !== -1) {
        // 时间选择:只显示时分秒
        return {
          year: false,
          month: false,
          day: false,
          hour: true,
          minute: true,
          second: true
        };
      } else {
        // 默认日期选择
        return {
          year: true,
          month: true,
          day: true,
          hour: false,
          minute: false,
          second: false
        };
      }
    },
    /**
     * 显示选择器
     */
    showPicker() {
      if (this.disabled) {
        return;
      }
      this.showPickerFlag = true;
    },
    
    /**
     * 确认选择
     * @param {Object} e 选择结果
     */
    onConfirm(e) {
      const { year, month, day, hour, minute, second } = e;

      console.log('onConfirm', e)
      
      // 格式化日期
      let formattedDate = '';
      const t = (this.type || 'date').toLowerCase();
      
      if (t === 'year') {
        // 年份选择:只返回年份
        formattedDate = `${year}`;
      } else if (t === 'month') {
        // 月份选择:返回年月
        formattedDate = `${year}-${this.formatNumber(month)}`;
      } else if (t === 'date') {
        // 日期选择:返回完整日期
        formattedDate = `${year}-${this.formatNumber(month)}-${this.formatNumber(day)}`;
      } else if (t === 'week') {
        // 周选择:返回完整日期(用于计算周数)
        const date = new Date(year, month - 1, day);
        const weekNumber = this.getWeekNumber(date);
        formattedDate = `${year}-${this.formatNumber(month)}-${this.formatNumber(day)}`;
        // 可以添加周数信息到返回值中
        console.log(`第${weekNumber}周`);
      } else if (t === 'datetime') {
        // 日期时间选择:返回完整日期时间
        formattedDate = `${year}-${this.formatNumber(month)}-${this.formatNumber(day)} ${this.formatNumber(hour)}:${this.formatNumber(minute)}:${this.formatNumber(second)}`;
      } else if (t.indexOf('time') !== -1) {
        // 时间选择:返回时间
        formattedDate = `${this.formatNumber(hour)}:${this.formatNumber(minute)}:${this.formatNumber(second)}`;
      } else {
        // 默认日期格式
        formattedDate = `${year}-${this.formatNumber(month)}-${this.formatNumber(day)}`;
      }
      
      this.selectedDate = formattedDate;
      this.$emit('input', formattedDate);
      this.$emit('change', formattedDate);
      this.showPickerFlag = false;
    },
    
    /**
     * 取消选择
     */
    onCancel() {
      this.showPickerFlag = false;
    },
    
    /**
     * 格式化数字,补零
     * @param {Number} num 数字
     * @returns {String} 格式化后的字符串
     */
    formatNumber(num) {
      return num < 10 ? `${num}` : `${num}`;
    },
    /**
     * 计算周数
     * @param {Date} date 日期对象
     * @returns {Number} 周数
     */
    getWeekNumber(date) {
      const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
      const pastDaysOfYear = (date - firstDayOfYear) / 86400000;
      return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
    }
  }
};
</script>

<style lang="scss" scoped>
.u-date-picker {
  margin-bottom: 20rpx;
  
  .label-container {
    display: flex;
    align-items: center;
    margin-bottom: 10rpx;
    
    .required-mark {
      color: #ff4757;
      font-size: 28rpx;
      margin-right: 8rpx;
      font-weight: bold;
    }
    
    .label {
      font-size: 28rpx;
      color: #333;
      font-weight: 500;
    }
  }
  
  .picker-container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20rpx;
    background-color: #fff;
    border-radius: 8rpx;
    border: 1rpx solid #e0e0e0;
    transition: all 0.3s ease;
    min-height: 88rpx;
    box-sizing: border-box;
    
    &.picker-disabled {
      background-color: #f5f5f5;
      border-color: #d9d9d9;
      cursor: not-allowed;
    }
    
    .picker-text {
      flex: 1;
      font-size: 28rpx;
      color: #333;
      line-height: 1.5;
      
      &.text-disabled {
        color: #999;
      }
    }
    
    .picker-arrow {
      font-size: 24rpx;
      color: #999;
      margin-left: 10rpx;
      font-weight: bold;
      
      &.arrow-disabled {
        color: #ccc;
      }
    }
  }
}
</style>
  • checkbox选择器,带着其他输入框
js 复制代码
<template>
  <view class="dynamic-checkbox">
    <view class="checkbox-label">
      <text v-if="required" class="required-mark">*</text>
      <text class="label-text">{{ label }}</text>
    </view>
    
    <u-checkbox-group 
      :disabled="disabled"
      :active-color="activeColor"
      :size="checkboxSize">
      
      <u-checkbox
        v-for="opt in localOptions"
            v-model="opt.checked"
            :name="opt.value"
            :disabled="disabled"
            :active-color="activeColor"
            :size="checkboxSize"
            @change="(value) => handleCheckboxChange(opt, value)">
            <text class="checkbox-text">{{ opt.label }}</text>
          </u-checkbox>
          

          <!-- 其他选项的输入框 -->
          
    </u-checkbox-group>
    <view v-if="otherChoose" class="other-input-container">
            <u-input
              v-model="otherValue"
              :placeholder="otherPlaceholder || '请输入其他内容'"
              class="other-input"
              :border="true"
              @input="handleOtherChange"/>
          </view>
  </view>
</template>

<script>
export default {
  name: 'DynamicCheckBox',
  props: {
    /** 标签文本 */
    label: {
      type: String,
      default: ''
    },
    /** 当前选中的值 */
    value: {
      type: Array,
      default: () => []
    },
    /** 选项配置数组 */
    options: {
      type: Array,
      default: () => []
    },
    /** 是否禁用 */
    disabled: {
      type: Boolean,
      default: false
    },
    /** 是否必填 */
    required: {
      type: Boolean,
      default: false
    },
    /** 排列方式. horizontal: 水平排列, vertical: 垂直排列 */
    alignDirection: {
      type: String,
      default: 'vertical'
    },
    /** 其他选项:输入框提示 */
    otherPlaceholder: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      checkboxValues: [],
      activeColor: '#2979ff',
      checkboxSize: 34,
      localOptions: [], // 本地选项数据,包含checked状态
      otherValue: '',// 其他输入框值      
    };
  },
  computed: {
    otherChoose() {
      // 判断checkboxValues中是否包含-99,如果包含,则返回true\
      let flag = false;
      if (this.checkboxValues.length > 0) {
        flag = this.checkboxValues.some(opt => opt.value === -99 || opt.value === '-99');
      }
      console.log('otherChoose:', flag);
      return flag;
    }
  },
  watch: {
    value: {
      handler(newVal) {
        // 判断newVal与this.checkboxValues是否相等, 若相等, 则不进行更新
        if (JSON.stringify(newVal) === JSON.stringify(this.checkboxValues)) {
          return;
        }
        // 确保value是数组类型
        const valueArray = Array.isArray(newVal) ? newVal : [];
        this.checkboxValues = [...valueArray];
        
        // 更新本地选项的checked状态
        this.updateLocalOptions();
      },
      immediate: true
    },
    options: {
      handler(newVal) {
        console.log('watch.options:', this.checkboxValues );
        this.localOptions = (newVal || []).map(option => ({
          ...option,
          checked: false
        }));
        this.updateLocalOptions();
      },
      immediate: true
    }
  },
  methods: {
    /**
     * 更新本地选项的checked状态
     */
    updateLocalOptions() {
      console.log('updateLocalOptions.checkboxValues:', this.checkboxValues);
      // 判断this.localOptions中是否有checkboxValues相等的值,  若有, 则将checked设置为true
      this.localOptions.forEach(option => {
        if (this.checkboxValues.some(opt => opt.value === option.value)) {
          option.checked = true;
        }
      });
      // 遍历this.checkboxValues, 若value == '-99', 则将otherValue赋值给this.otherValue
      this.checkboxValues.forEach(opt => {
        if (opt.value == '-99') {
          this.otherValue = opt.otherValue;
        }
      });
    },
    /**
     * 处理单个checkbox变化事件
     * @param {Object} option 选项对象
     * @param {Number} index 选项索引
     * @param {Boolean} value 是否选中
     */
    handleCheckboxChange(option, value) {
      
      // 当option.checked为true时,将option.name添加到checkboxValues中
      if (value.value) {
        this.checkboxValues.push({...option});
      } else {
        // this.checkboxValues.splice(this.checkboxValues.indexOf({...option}), 1);
        // 检查checkboxValues数组中是否有value.name的值,有则删除
        // this.checkboxValues.splice(this.checkboxValues.indexOf({...option}), 1);
        // 对checkboxvalues重新赋值, 若value.name的值在checkboxvalues中存在则删除
        this.checkboxValues = this.checkboxValues.filter(opt => opt.value !== option.value);
      }
      console.log('单个checkbox变化:', option, value, this.checkboxValues);
      this.emitChange();
    },

    /**
     * 触发change事件
     */
    emitChange() {
      this.$emit('change', this.checkboxValues);
    },

    /**
     * 处理其他选项输入框的输入事件
     * @param {String} value 输入的值
     */
    handleOtherChange(value) {

      
      this.otherValue = value;
      // 轮询checkboxValues数组,找到value=='-99'的对象添加otherValue
      this.checkboxValues.forEach(opt => {
        if (opt.value === '-99') {
          opt.otherValue = this.otherValue;
        }
      });
      console.log('handleOtherChange:', value, this.checkboxValues);
      this.emitChange();
    },

    /**
     * 处理其他选项输入框获得焦点事件
     * @param {Number} index 选项索引
     */
    handleOtherFocus(index) {
      console.log(`其他选项输入框获得焦点,索引: ${index}`);
    },

    /**
     * 处理其他选项输入框失去焦点事件
     * @param {Number} index 选项索引
     */
    handleOtherBlur(index) {
      console.log(`其他选项输入框失去焦点,索引: ${index}`);
      // 失焦时也触发change事件,确保数据同步
      // this.emitChange();
    },
    
   
  }
};
</script>

<style scoped>
.dynamic-checkbox {
  background-color: #fff;
  border-radius: 12rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}

.checkbox-label {
  display: flex;
  align-items: center;
  margin-bottom: 20rpx;
}

.required-mark {
  color: #ff4757;
  font-size: 28rpx;
  margin-right: 8rpx;
  font-weight: bold;
}

.label-text {
  font-size: 28rpx;
  color: #333;
  font-weight: 500;
}

.checkbox-container {
  display: flex;
  flex-direction: column;
  gap: 40rpx;
}

.checkbox-container.checkbox-horizontal {
  flex-direction: row;
  flex-wrap: wrap;
  gap: 20rpx;
}

.checkbox-container.checkbox-vertical {
  flex-direction: column;
  gap: 40rpx;
}

.checkbox-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 16rpx;
  border-radius: 8rpx;
  transition: all 0.2s ease;
  border: 2rpx solid transparent;
  min-width: 200rpx;
}

.checkbox-item:not(.checkbox-disabled):hover {
  background-color: #f8f9fa;
  border-color: #e9ecef;
}

.checkbox-item.checkbox-disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.checkbox-item.checkbox-checked {
  background-color: #f0f8ff;
  border-color: #2979ff;
}

.checkbox-option {
  display: flex;
  align-items: center;
  cursor: pointer;
  width: 100%;
}

.checkbox-text {
  font-size: 28rpx;
  color: #333;
  margin-left: 12rpx;
  user-select: none;
  flex: 1;
  line-height: 1.4;
}

/* 其他选项输入框样式 */
.other-input-container {
  width: 100%;
  margin-top: 16rpx;
}

.other-input {
  width: 100%;
}

/* 选中状态下的其他选项样式 */
.checkbox-item.checkbox-checked .other-input {
  border-color: #2979ff;
  background-color: #f8f9ff;
}

/* 水平排列时的样式优化 */
.checkbox-container.checkbox-horizontal .checkbox-item {
  flex: 0 0 auto;
  min-width: 180rpx;
  max-width: 300rpx;
}

.checkbox-container.checkbox-horizontal .other-input-container {
  margin-top: 12rpx;
}
</style>
相关推荐
beijingliushao22 分钟前
31-数据仓库与Apache Hive-Insert插入数据
数据仓库·hive·apache
小楓12011 小时前
後端開發技術教學(三) 表單提交、數據處理
前端·后端·html·php
破刺不会编程1 小时前
linux信号量和日志
java·linux·运维·前端·算法
阿里小阿希2 小时前
Vue 3 表单数据缓存架构设计:从问题到解决方案
前端·vue.js·缓存
JefferyXZF2 小时前
Next.js 核心路由解析:动态路由、路由组、平行路由和拦截路由(四)
前端·全栈·next.js
汪子熙2 小时前
浏览器环境中 window.eval(vOnInit); // csp-ignore-legacy-api 的技术解析与实践意义
前端·javascript
还要啥名字2 小时前
elpis - 动态组件扩展设计
前端
BUG收容所所长2 小时前
🤖 零基础构建本地AI对话机器人:Ollama+React实战指南
前端·javascript·llm
鹏程十八少2 小时前
7. Android RecyclerView吃了80MB内存!KOOM定位+Profiler解剖+MAT验尸全记录
前端
小高0072 小时前
🚀前端异步编程:Promise vs Async/Await,实战对比与应用
前端·javascript·面试