背景。做小程序用到了自定义表单。前后端都是分开写的,没有使用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>