修改完成待测试

This commit is contained in:
2026-09-13 17:45:47 +08:00
parent 9ad572907b
commit bbc024f7f2
80 changed files with 3844 additions and 1672 deletions
+423
View File
@@ -0,0 +1,423 @@
<template>
<view v-if="visible" class="member-date-picker" role="dialog" :aria-label="title">
<view class="member-date-picker__mask" @click="$emit('cancel')" />
<view class="member-date-picker__panel">
<view class="member-date-picker__heading">
<text class="member-date-picker__title">{{ title }}</text>
<text class="member-date-picker__hint">点击年份可直接切换左右按钮切换月份</text>
</view>
<view class="member-date-picker__toolbar">
<button
class="member-date-picker__month-button"
aria-label="上个月"
:disabled="!canSelectPreviousMonth"
@click="changeMonth(-1)"
>
<image
class="member-date-picker__month-icon member-date-picker__month-icon--previous"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</button>
<view class="member-date-picker__current-month">
<picker
:range="yearLabels"
:value="selectedYearIndex"
@change="selectYear"
>
<button class="member-date-picker__year-button">
<text>选择年份</text>
<text>{{ selectedYear }}</text>
</button>
</picker>
<picker
:range="monthLabels"
:value="selectedMonthIndex"
@change="selectMonth"
>
<button class="member-date-picker__month-button-direct" aria-label="选择月份">
<text>选择月份</text>
<text>{{ selectedMonth }}</text>
</button>
</picker>
</view>
<button
class="member-date-picker__month-button"
aria-label="下个月"
:disabled="!canSelectNextMonth"
@click="changeMonth(1)"
>
<image
class="member-date-picker__month-icon"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</button>
</view>
<view class="member-date-picker__weekdays">
<text v-for="weekday in weekdays" :key="weekday">{{ weekday }}</text>
</view>
<view class="member-date-picker__calendar">
<view
v-for="calendarDay in calendarDays"
:key="calendarDay.key"
class="member-date-picker__day-slot"
>
<button
v-if="calendarDay.day"
class="member-date-picker__day"
:class="{
'member-date-picker__day--selected': calendarDay.day === selectedDay,
}"
:disabled="calendarDay.disabled"
:aria-label="`${selectedYear}年${selectedMonth}月${calendarDay.day}日`"
@click="selectedDay = calendarDay.day"
>{{ calendarDay.day }}</button>
</view>
</view>
<view class="member-date-picker__footer">
<button class="member-date-picker__cancel" @click="$emit('cancel')">取消</button>
<button class="member-date-picker__confirm" @click="confirmSelection">确定</button>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref, watch } from "vue";
const props = defineProps({
visible: { type: Boolean, default: false },
title: { type: String, default: "选择日期" },
value: { type: String, default: "" },
});
const emit = defineEmits(["cancel", "confirm"]);
const MIN_YEAR = 1900;
const today = new Date();
const CURRENT_YEAR = today.getFullYear();
const CURRENT_MONTH = today.getMonth() + 1;
const CURRENT_DAY = today.getDate();
const years = Array.from(
{ length: CURRENT_YEAR - MIN_YEAR + 1 },
(_, index) => MIN_YEAR + index,
);
const yearLabels = years.map((year) => `${year}`);
const weekdays = Object.freeze(["日", "一", "二", "三", "四", "五", "六"]);
const selectedYear = ref(new Date().getFullYear());
const selectedMonth = ref(new Date().getMonth() + 1);
const selectedDay = ref(new Date().getDate());
const selectedYearIndex = computed(() => selectedYear.value - MIN_YEAR);
const availableMonths = computed(() =>
Array.from(
{ length: selectedYear.value === CURRENT_YEAR ? CURRENT_MONTH : 12 },
(_, index) => index + 1,
),
);
const monthLabels = computed(() =>
availableMonths.value.map((month) => `${month}`),
);
const selectedMonthIndex = computed(() =>
Math.max(0, availableMonths.value.indexOf(selectedMonth.value)),
);
const daysInSelectedMonth = computed(() =>
new Date(selectedYear.value, selectedMonth.value, 0).getDate(),
);
const selectedMonthKey = computed(
() => selectedYear.value * 12 + selectedMonth.value - 1,
);
const firstMonthKey = MIN_YEAR * 12;
const currentMonthKey = CURRENT_YEAR * 12 + CURRENT_MONTH - 1;
const canSelectPreviousMonth = computed(() => selectedMonthKey.value > firstMonthKey);
const canSelectNextMonth = computed(() => selectedMonthKey.value < currentMonthKey);
const calendarDays = computed(() => {
const leadingDays = new Date(selectedYear.value, selectedMonth.value - 1, 1).getDay();
const slots = Array.from({ length: leadingDays }, (_, index) => ({
key: `empty-${index}`,
day: 0,
}));
for (let day = 1; day <= daysInSelectedMonth.value; day += 1) {
const disabled =
selectedYear.value === CURRENT_YEAR &&
selectedMonth.value === CURRENT_MONTH &&
day > CURRENT_DAY;
slots.push({ key: `day-${day}`, day, disabled });
}
return slots;
});
const clampSelectedDay = () => {
const latestDay =
selectedYear.value === CURRENT_YEAR && selectedMonth.value === CURRENT_MONTH
? CURRENT_DAY
: daysInSelectedMonth.value;
selectedDay.value = Math.min(selectedDay.value, latestDay);
};
const parseInitialDate = () => {
const matched = /^(\d{4})-(\d{2})-(\d{2})$/.exec(props.value);
const fallback = new Date();
const isFutureDate = matched
? new Date(Number(matched[1]), Number(matched[2]) - 1, Number(matched[3])) > today
: false;
if (isFutureDate) {
selectedYear.value = CURRENT_YEAR;
selectedMonth.value = CURRENT_MONTH;
selectedDay.value = CURRENT_DAY;
return;
}
selectedYear.value = matched
? Math.max(MIN_YEAR, Math.min(CURRENT_YEAR, Number(matched[1])))
: fallback.getFullYear();
selectedMonth.value = matched
? Math.max(1, Math.min(12, Number(matched[2])))
: fallback.getMonth() + 1;
selectedDay.value = matched ? Math.max(1, Number(matched[3])) : fallback.getDate();
if (selectedMonthKey.value > currentMonthKey) {
selectedMonth.value = CURRENT_MONTH;
}
clampSelectedDay();
};
watch(
() => props.visible,
(visible) => {
if (visible) parseInitialDate();
},
);
const selectYear = (event) => {
selectedYear.value = years[Number(event.detail.value)] || selectedYear.value;
if (selectedMonthKey.value > currentMonthKey) {
selectedMonth.value = CURRENT_MONTH;
}
clampSelectedDay();
};
const selectMonth = (event) => {
const nextMonth = availableMonths.value[Number(event.detail.value)];
if (!nextMonth) return;
selectedMonth.value = nextMonth;
clampSelectedDay();
};
const changeMonth = (offset) => {
const nextDate = new Date(selectedYear.value, selectedMonth.value - 1 + offset, 1);
const nextYear = nextDate.getFullYear();
const nextMonth = nextDate.getMonth() + 1;
const nextMonthKey = nextYear * 12 + nextMonth - 1;
if (nextMonthKey < firstMonthKey || nextMonthKey > currentMonthKey) return;
selectedYear.value = nextYear;
selectedMonth.value = nextMonth;
clampSelectedDay();
};
const padDatePart = (value) => String(value).padStart(2, "0");
const confirmSelection = () => {
if (selectedMonthKey.value > currentMonthKey) return;
clampSelectedDay();
emit(
"confirm",
`${selectedYear.value}-${padDatePart(selectedMonth.value)}-${padDatePart(
selectedDay.value,
)}`,
);
};
</script>
<style scoped lang="scss">
.member-date-picker {
position: fixed;
z-index: 90;
inset: 0;
display: flex;
align-items: flex-end;
}
.member-date-picker__mask {
position: absolute;
inset: 0;
background: rgba(43, 30, 20, 0.5);
}
.member-date-picker__panel {
position: relative;
box-sizing: border-box;
width: 100%;
padding: 28rpx 30rpx calc(28rpx + env(safe-area-inset-bottom));
border-radius: 28rpx 28rpx 0 0;
background: #fdf8ed;
box-shadow: 0 -12rpx 36rpx rgba(43, 30, 20, 0.2);
}
.member-date-picker__heading {
text-align: center;
}
.member-date-picker__title,
.member-date-picker__hint {
display: block;
}
.member-date-picker__title {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(20px, 36rpx, 25px);
font-weight: 700;
}
.member-date-picker__hint {
margin-top: 6rpx;
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
}
.member-date-picker__toolbar {
display: grid;
grid-template-columns: 76rpx minmax(0, 1fr) 76rpx;
align-items: center;
gap: 14rpx;
margin-top: 20rpx;
}
.member-date-picker__month-button {
display: flex;
width: 76rpx;
min-height: 76rpx;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
border: 1rpx solid rgba(181, 137, 63, 0.34);
border-radius: 10rpx;
background: rgba(247, 237, 218, 0.7);
}
.member-date-picker__month-button[disabled],
.member-date-picker__day[disabled] {
opacity: 0.32;
}
.member-date-picker__month-button::after,
.member-date-picker__year-button::after,
.member-date-picker__month-button-direct::after,
.member-date-picker__day::after,
.member-date-picker__cancel::after,
.member-date-picker__confirm::after {
display: none;
}
.member-date-picker__month-icon {
width: 28rpx;
height: 28rpx;
}
.member-date-picker__month-icon--previous {
transform: rotate(180deg);
}
.member-date-picker__current-month {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 14rpx;
}
.member-date-picker__year-button {
display: grid;
width: 100%;
min-height: 76rpx;
align-content: center;
margin: 0;
padding: 6rpx 18rpx;
border: 1rpx solid rgba(159, 23, 15, 0.38);
border-radius: 10rpx;
background: rgba(255, 250, 240, 0.92);
color: $brand-red;
line-height: 1.25;
}
.member-date-picker__year-button text:first-child {
font-size: clamp(12px, 19rpx, 15px);
}
.member-date-picker__year-button text:last-child {
font-size: clamp(17px, 29rpx, 21px);
font-weight: 700;
}
.member-date-picker__month-button-direct {
display: grid;
min-width: 116rpx;
min-height: 76rpx;
align-content: center;
margin: 0;
padding: 6rpx 12rpx;
border: 1rpx solid rgba(181, 137, 63, 0.34);
border-radius: 10rpx;
background: rgba(247, 237, 218, 0.7);
color: $ink;
line-height: 1.25;
white-space: nowrap;
}
.member-date-picker__month-button-direct text:first-child {
color: $ink-muted;
font-size: clamp(12px, 19rpx, 15px);
}
.member-date-picker__month-button-direct text:last-child {
font-family: STKaiti, KaiTi, serif;
font-size: clamp(17px, 29rpx, 21px);
font-weight: 700;
}
.member-date-picker__weekdays,
.member-date-picker__calendar {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.member-date-picker__weekdays {
margin-top: 20rpx;
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
text-align: center;
}
.member-date-picker__calendar {
margin-top: 8rpx;
padding: 8rpx;
border: 1rpx solid rgba(181, 137, 63, 0.3);
border-radius: 12rpx;
background: rgba(255, 252, 245, 0.76);
}
.member-date-picker__day-slot {
display: flex;
min-height: 70rpx;
align-items: center;
justify-content: center;
}
.member-date-picker__day {
display: flex;
width: 58rpx;
min-height: 58rpx;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
border: 0;
border-radius: 50%;
background: transparent;
color: $ink;
font-size: clamp(14px, 23rpx, 18px);
line-height: 1;
}
.member-date-picker__day--selected {
background: $brand-red;
color: #fff9ed;
font-weight: 700;
}
.member-date-picker__footer {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
margin-top: 20rpx;
}
.member-date-picker__cancel,
.member-date-picker__confirm {
min-height: 82rpx;
margin: 0;
border-radius: 10rpx;
font-size: clamp(16px, 28rpx, 20px);
font-weight: 700;
}
.member-date-picker__cancel {
border: 1rpx solid rgba(159, 23, 15, 0.34);
background: transparent;
color: $brand-red;
}
.member-date-picker__confirm {
border: 0;
background: $brand-red;
color: #fff9ed;
}
</style>