| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530 |
- <template>
- <view class="px-30 py-38 bg-white">
- <view class="flex gap-x-20">
- <view class="flex items-center gap-x-20 flex-wrap">
- <view class="picker-wrap">
- <ie-picker ref="pickerRef" v-model="form.year" :list="yearList" placeholder="请选择" title="选择年份" :fontSize="26"
- icon="arrow-down" key-label="label" key-value="value" @change="handleYearChange">
- <template #default="{ label }">
- <text>{{ label }}年</text>
- </template>
- </ie-picker>
- </view>
- <view class="picker-wrap">
- <ie-picker ref="pickerRef" :disabled="!form.year" v-model="form.month" :list="monthList" placeholder="请选择"
- title="选择月份" :fontSize="26" icon="arrow-down" key-label="label" key-value="value"
- @change="handleMonthChange">
- <template #default="{ label }">
- <text>{{ label }}</text>
- </template>
- </ie-picker>
- </view>
- <view :class="calendarButtonClass" @click="canOpenCalendar ? handleOpenCalendar() : null">
- <text>刷题日历</text>
- <uv-icon name="search" size="16" :color="canOpenCalendar ? 'white' : '#CCCCCC'" />
- </view>
- </view>
- </view>
- <view class="mt-30 flex h-280 gap-x-20">
- <view class="flex-1 h-full">
- <ie-echart :option="options1" />
- </view>
- <view class="flex-1 h-full">
- <ie-echart :option="options2" />
- </view>
- </view>
- <view class="mt-30 flex items-center">
- <text>已累计刷题</text>
- <text class="text-32 text-primary">{{ practiceDays }}</text>
- <text>天~</text>
- </view>
- <view v-if="(form.month) && displayMode !== 'year'" class="mt-30">
- <ie-table :tableColumns="tableColumns" :data="tableDate">
- <template #date="{ item }">
- <text class="font-bold">{{ item.date }}</text>
- </template>
- <template #questionNum="{ item }">
- <text class="font-bold">{{ item.questionNum }}</text>
- </template>
- <template #correctNum="{ item }">
- <text class="font-bold" :class="[item.info < 70 ? 'text-danger' : 'text-fore-title']">{{ item.info }}%</text>
- </template>
- </ie-table>
- </view>
- <!-- #ifdef H5 -->
- <teleport to="body">
- <!-- #endif -->
- <!-- #ifdef MP-WEIXIN -->
- <root-portal externalClass="theme-ie">
- <!-- #endif -->
- <uv-popup ref="calendarPopupRef" mode="bottom" :round="16" v-if="canOpenCalendar">
- <view class="h-[480px]">
- <view class="h-108 flex items-center justify-center border-0 border-b border-solid border-border">
- <view :class="prevButtonClass" @click="canGoPrev && !loading ? handlePrev() : null">
- <uv-icon name="arrow-left" size="10" :color="canGoPrev && !loading ? '#808080' : '#CCCCCC'" />
- </view>
- <view class="mx-40 text-30 text-fore-title font-bold">
- <text>{{ calendarTitle }}</text>
- </view>
- <view :class="nextButtonClass" @click="canGoNext && !loading ? handleNext() : null">
- <uv-icon name="arrow-right" size="10" :color="canGoNext && !loading ? '#808080' : '#CCCCCC'" />
- </view>
- </view>
- <view class="relative">
- <view class="px-40 py-20 flex items-center justify-between">
- <view class=" text-28">
- <text>{{ calendarSubTitle }}</text>
- <text class="text-32 text-primary font-bold">{{ practiceDays }}</text>
- <text>天~</text>
- </view>
- <!-- <uv-icon name="question-circle" size="18" color="#31a0fc" /> -->
- </view>
- <uni-calendar ref="calendarRef" :insert="true" :lunar="false" :readonly="true" :showMonth="false"
- :sundayFirst="false" :highlightToday="false" :showToolbar="false" :displayMode="displayMode"
- :selected="selected" :date="currentDate" @change-week="handleCalendarWeekChange"
- @monthSwitch="handleCalendarMonthSwitch">
- <template #calendar-item="{ weeks }">
- <view class="calendar-item" :class="{
- 'calendar-item--week-mode-disabled': weeks.isWeekModeDisabled,
- 'uni-calendar-item--disable': !weeks.isCurrentMonth,
- 'calendar-item--valid': weeks.extraInfo && weeks.extraInfo.info >= 70,
- 'calendar-item--invalid': weeks.extraInfo && weeks.extraInfo.info < 70
- }">
- <view class="date">{{ weeks.date }}</view>
- <view class="info">
- <text v-if="weeks.extraInfo && weeks.extraInfo.info">{{ weeks.extraInfo.info }}%</text>
- </view>
- </view>
- </template>
- </uni-calendar>
- <!-- Loading 覆盖层 -->
- <view v-if="loading" class="calendar-loading-overlay">
- <uv-loading-icon mode="circle" size="32" color="#31a0fc" />
- </view>
- </view>
- </view>
- </uv-popup>
- <!-- #ifdef MP-WEIXIN -->
- </root-portal>
- <!-- #endif -->
- <!-- #ifdef H5 -->
- </teleport>
- <!-- #endif -->
- </view>
- </template>
- <script lang="ts" setup>
- import { TableColumnConfig } from '@/types';
- import ieEchart from './ie-echart/ie-echart.vue';
- import { useCalendar } from '@/composables/useCalendar';
- const props = defineProps<{
- recordId?: number;
- }>();
- // 使用 useCalendar composable
- const {
- selected,
- statistics,
- currentDate,
- yearList,
- displayMode,
- currentMonthRange,
- canGoPrev,
- canGoNext,
- loading,
- currentYear,
- currentMonth,
- goToPrevMonth,
- goToNextMonth,
- goToYear,
- goToYearMonth,
- initializeFormData,
- init: initCalendar
- } = useCalendar();
- // 表单数据 - 与日历同步
- const form = ref({
- year: '',
- month: '',
- })
- // 月份列表(不能超过当前月份)
- const monthList = computed(() => {
- const today = new Date();
- const currentYearToday = today.getFullYear();
- const currentMonthToday = today.getMonth() + 1;
- const selectedYear = parseInt(form.value.year) || currentYear.value;
- const months = [];
- const maxMonth = selectedYear === currentYearToday ? currentMonthToday : 12;
- for (let i = 1; i <= maxMonth; i++) {
- months.push({
- label: `${i}月`,
- value: i.toString()
- });
- }
- return months;
- });
- // 检查是否可以选择日历
- const canOpenCalendar = computed(() => {
- // 必须选择年份
- if (!form.value.year) {
- return false;
- }
- // 如果是month模式,必须选择月份
- if (displayMode.value === 'month') {
- return !!form.value.month;
- }
- // 如果是year模式,不能打开日历(年份模式不显示记录表)
- return false;
- });
- // 导航按钮样式计算属性
- const prevButtonClass = computed(() => {
- return {
- 'w-34 h-34 rounded-full flex items-center justify-center transition-all duration-200': true,
- 'bg-[#EEF4FA] cursor-pointer': canGoPrev.value && !loading.value,
- 'bg-[#F5F5F5] cursor-not-allowed': !canGoPrev.value || loading.value,
- 'opacity-50': loading.value
- };
- });
- const nextButtonClass = computed(() => {
- return {
- 'w-34 h-34 rounded-full flex items-center justify-center transition-all duration-200': true,
- 'bg-[#EEF4FA] cursor-pointer': canGoNext.value && !loading.value,
- 'bg-[#F5F5F5] cursor-not-allowed': !canGoNext.value || loading.value,
- 'opacity-50': loading.value
- };
- });
- const calendarButtonClass = computed(() => {
- return {
- 'btn-wrap transition-all duration-200': true,
- 'opacity-50 cursor-not-allowed': !canOpenCalendar.value,
- 'cursor-pointer': canOpenCalendar.value
- };
- });
- // 图表配置
- const options1 = computed(() => {
- return {
- title: {
- text: statistics.value.total.toString(),
- subtext: '{a|刷题总量}',
- left: 'center',
- top: '34%',
- textStyle: {
- fontSize: 20,
- fontWeight: 'bold',
- color: '#222'
- },
- subtextStyle: {
- fontSize: 12,
- color: '#666',
- lineHeight: 12,
- rich: {
- a: {
- color: '#B3B3B3',
- padding: [0, 0, 10, 0]
- },
- }
- }
- },
- series: [
- {
- type: 'pie',
- radius: ['60%', '80%'],
- startAngle: 90,
- silent: true,
- label: { show: false },
- data: [
- {
- value: 45,
- itemStyle: {
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 1,
- y2: 1,
- colorStops: [
- { offset: 0, color: '#FEC048' },
- { offset: 1, color: '#F9942F' }
- ]
- }
- }
- },
- {
- value: 55,
- itemStyle: { color: '#FFF5DE' }
- }
- ]
- }
- ]
- };
- });
- const options2 = computed(() => {
- const accuracy = statistics.value.rate;
- return {
- title: {
- text: `${accuracy}%`,
- subtext: '{a|正确率}',
- left: 'center',
- top: '34%',
- textStyle: {
- fontSize: 20,
- fontWeight: 'bold',
- color: '#222'
- },
- subtextStyle: {
- fontSize: 12,
- color: '#666',
- lineHeight: 12,
- rich: {
- a: {
- color: '#B3B3B3',
- padding: [0, 0, 10, 0]
- },
- }
- }
- },
- series: [
- {
- type: 'pie',
- radius: ['60%', '80%'],
- startAngle: 90,
- silent: true,
- label: { show: false },
- data: [
- {
- value: accuracy,
- itemStyle: {
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 1,
- y2: 1,
- colorStops: [
- { offset: 0, color: '#70C8FD' },
- { offset: 1, color: '#31A0FC' }
- ]
- }
- }
- },
- {
- value: 100 - accuracy,
- itemStyle: { color: '#EBF9FF' }
- }
- ]
- }
- ]
- };
- });
- // 表格配置
- const tableColumns = ref<TableColumnConfig[]>([
- {
- prop: 'date',
- label: '日期',
- flex: 1,
- slot: 'date'
- },
- {
- prop: 'questionNum',
- label: '题量',
- flex: 1,
- slot: 'questionNum'
- },
- {
- prop: 'correctNum',
- label: '正确率',
- flex: 1,
- slot: 'correctNum'
- }
- ]);
- // 表格数据(从 selected 数据转换)
- const tableDate = computed(() => {
- return statistics.value.list;
- });
- // 练习天数统计
- const practiceDays = computed(() => {
- return statistics.value.studyDays;
- });
- // 日历标题
- const calendarTitle = computed(() => {
- const year = currentMonthRange.value?.startDate?.split('-')[0] || new Date().getFullYear();
- const month = currentMonthRange.value?.startDate?.split('-')[1] || new Date().getMonth() + 1;
- return `${year}年-${month}月`;
- });
- // 日历副标题
- const calendarSubTitle = computed(() => {
- const month = currentMonthRange.value?.startDate?.split('-')[1] || new Date().getMonth() + 1;
- return `本月(${month}月)已累计刷题`;
- });
- // 导航方法
- const handlePrev = async () => {
- if (loading.value) return; // 加载中时禁止操作
- uni.$ie.showLoading();
- await goToPrevMonth();
- uni.$ie.hideLoading();
- };
- const handleNext = async () => {
- if (loading.value) return; // 加载中时禁止操作
- uni.$ie.showLoading();
- await goToNextMonth();
- uni.$ie.hideLoading();
- };
- // 表单处理 - 与日历同步
- const handleYearChange = async () => {
- // 年份切换时清空月份和周
- form.value.month = '';
- // 年份切换时请求年度数据,但不显示记录表
- if (form.value.year) {
- const year = parseInt(form.value.year);
- // 请求年度数据来更新统计信息
- await goToYear(year);
- }
- };
- const handleMonthChange = async () => {
- if (form.value.year && form.value.month) {
- // 切换到月份模式并跳转到指定年月
- await goToYearMonth(parseInt(form.value.year), parseInt(form.value.month));
- }
- };
- // 监听日历状态变化,同步到表单
- watch([currentYear, currentMonth, displayMode], ([year, month, mode]) => {
- form.value.year = year.toString();
- // 只有在非年份模式下才同步月份,年份模式下保持用户选择
- if (mode !== 'year') {
- form.value.month = month.toString();
- }
- }, { immediate: true });
- // 初始化默认选中当前时间
- const initializeDefaultSelection = () => {
- const formData = initializeFormData();
- form.value.year = formData.year;
- form.value.month = formData.month;
- };
- // 监听日历组件内部事件,确保数据同步
- const handleCalendarWeekChange = (event: any) => {
- };
- const handleCalendarMonthSwitch = (event: any) => {
- // 更新外部状态以保持同步
- if (event.year && event.month) {
- // 通过 goToYearMonth 方法来更新状态
- goToYearMonth(event.year, event.month);
- }
- };
- const calendarRef = ref();
- const calendarPopupRef = ref();
- const handleOpenCalendar = () => {
- if (canOpenCalendar.value && calendarPopupRef.value) {
- calendarPopupRef.value.open();
- }
- };
- // 初始化
- onMounted(async () => {
- // 先初始化默认选中
- initializeDefaultSelection();
- // 显示全屏loading
- uni.$ie.showLoading();
- try {
- // 初始化日历
- await initCalendar(props.recordId);
- } finally {
- // 隐藏loading
- uni.$ie.hideLoading();
- }
- });
- </script>
- <style lang="scss" scoped>
- .picker-wrap {
- @apply flex items-center px-12 w-fit border border-solid border-border rounded-4 h-56;
- }
- .btn-wrap {
- @apply flex items-center gap-x-10 bg-primary text-white text-26 px-10 h-56 rounded-4;
- }
- .calendar-item {
- @apply rounded-5 text-center w-64 h-56 mx-auto py-8;
- .date {
- color: #222;
- font-size: 30rpx;
- line-height: 30rpx;
- }
- .info {
- font-size: 20rpx;
- line-height: 20rpx;
- margin-top: 4rpx;
- }
- }
- .uni-calendar-item--disable {
- opacity: 0;
- }
- .calendar-item--week-mode-disabled {
- .date {
- color: #cccccc;
- }
- }
- .calendar-item--valid {
- --valid-color: #22C55E;
- background-color: #F0FDF4;
- border: 1px solid var(--valid-color);
- .info {
- color: var(--valid-color);
- }
- }
- .calendar-item--invalid {
- --invalid-color: #FF5B5C;
- background-color: #FEEDE9;
- border: 1px solid var(--invalid-color);
- .info {
- color: var(--invalid-color);
- }
- }
- .calendar-loading-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(255, 255, 255, 0.8);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10;
- }
- </style>
|