背景。做小程序用到了自定義表單。前后端都是分開寫的,沒有使用web-view。所以要做到功能對稱
- 時間選擇器。需要區分datetime, year, day等類型使用uview組件較方便
<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-pickerv-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 降級為 datereturn '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選擇器,帶著其他輸入框
<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-checkboxv-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-inputv-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設置為truethis.localOptions.forEach(option => {if (this.checkboxValues.some(opt => opt.value === option.value)) {option.checked = true;}});// 遍歷this.checkboxValues, 若value == '-99', 則將otherValue賦值給this.otherValuethis.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'的對象添加otherValuethis.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>