practice-table.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. <template>
  2. <view class="px-30 py-30 bg-white">
  3. <view class="flex gap-x-20">
  4. <view class="flex items-center gap-x-20 flex-wrap">
  5. <view class="picker-wrap">
  6. <ie-picker ref="pickerRef" v-model="form.year" :list="yearList" placeholder="请选择" title="选择年份" :fontSize="26"
  7. icon="arrow-down" key-label="label" key-value="value" @change="handleYearChange">
  8. <template #default="{ label }">
  9. <text>{{ label }}年</text>
  10. </template>
  11. </ie-picker>
  12. </view>
  13. <view class="picker-wrap">
  14. <ie-picker ref="pickerRef" :disabled="!form.year" v-model="form.month" :list="monthList" placeholder="请选择"
  15. title="选择月份" :fontSize="26" icon="arrow-down" key-label="label" key-value="value"
  16. @change="handleMonthChange">
  17. <template #default="{ label }">
  18. <text>{{ label }}</text>
  19. </template>
  20. </ie-picker>
  21. </view>
  22. <view :class="['btn-wrap transition-all duration-200', calendarButtonClass]"
  23. @click="canOpenCalendar ? handleOpenCalendar() : null">
  24. <text>刷题日历</text>
  25. <uv-icon name="search" size="16" :color="canOpenCalendar ? 'white' : '#CCCCCC'" />
  26. </view>
  27. </view>
  28. </view>
  29. <view class="mt-30 flex h-280 gap-x-20">
  30. <view class="flex-1 h-full relative">
  31. <ie-echart :option="options1" />
  32. <div class="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center">
  33. <div class="text-40 text-fore-title font-bold">{{ statistics.total }}</div>
  34. <div class="text-22 text-fore-light">刷题总量</div>
  35. </div>
  36. </view>
  37. <view class="flex-1 h-full relative">
  38. <ie-echart :option="options2" />
  39. <div class="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center">
  40. <div class="text-40 text-fore-title font-bold">{{ statistics.rate }}%</div>
  41. <div class="text-22 text-fore-light">正确率</div>
  42. </div>
  43. </view>
  44. </view>
  45. <view class="mt-30 flex items-center">
  46. <text>已累计刷题</text>
  47. <text class="text-32 text-primary">{{ practiceDays }}</text>
  48. <text>天~</text>
  49. </view>
  50. <view v-if="(form.month) && displayMode !== 'year'" class="mt-30">
  51. <ie-table :tableColumns="tableColumns" :data="tableDate">
  52. <template #date="{ item }">
  53. <text class="font-bold">{{ item.date }}</text>
  54. </template>
  55. <template #questionNum="{ item }">
  56. <text class="font-bold">{{ item.questionNum }}</text>
  57. </template>
  58. <template #correctNum="{ item }">
  59. <text class="font-bold" :class="[item.info < 70 ? 'text-danger' : 'text-fore-title']">{{ item.info }}%</text>
  60. </template>
  61. </ie-table>
  62. </view>
  63. <ie-popup ref="calendarPopupRef" :showToolbar="false" v-if="canOpenCalendar">
  64. <view class="h-[480px]">
  65. <view class="h-108 flex items-center justify-center border-0 border-b border-solid border-border">
  66. <view :class="['w-38 h-38 rounded-full flex items-center justify-center transition-all duration-200', prevButtonClass]" @click="canGoPrev && !loading ? handlePrev() : null">
  67. <uv-icon name="arrow-left" size="12" :color="canGoPrev && !loading ? '#808080' : '#CCCCCC'" />
  68. </view>
  69. <view class="mx-40 text-30 text-fore-title font-bold">
  70. <text>{{ calendarTitle }}</text>
  71. </view>
  72. <view :class="['w-38 h-38 rounded-full flex items-center justify-center transition-all duration-200', nextButtonClass]" @click="canGoNext && !loading ? handleNext() : null">
  73. <uv-icon name="arrow-right" size="12" :color="canGoNext && !loading ? '#808080' : '#CCCCCC'" />
  74. </view>
  75. </view>
  76. <view class="relative">
  77. <view class="px-40 py-20 flex items-center justify-between">
  78. <view class=" text-28">
  79. <text>{{ calendarSubTitle }}</text>
  80. <text class="text-32 text-primary font-bold">{{ practiceDays }}</text>
  81. <text>天~</text>
  82. </view>
  83. <!-- <uv-icon name="question-circle" size="18" color="#31a0fc" /> -->
  84. </view>
  85. <uni-calendar ref="calendarRef" :insert="true" :lunar="false" :readonly="true" :showMonth="false"
  86. :sundayFirst="false" :highlightToday="false" :showToolbar="false" :displayMode="displayMode"
  87. :selected="selected" :date="currentDate" @change-week="handleCalendarWeekChange"
  88. @monthSwitch="handleCalendarMonthSwitch">
  89. <template #calendar-item="{ weeks }">
  90. <view class="calendar-item" :class="{
  91. 'calendar-item--week-mode-disabled': weeks.isWeekModeDisabled,
  92. 'uni-calendar-item--disable': !weeks.isCurrentMonth,
  93. 'calendar-item--valid': weeks.extraInfo && weeks.extraInfo.info >= 70,
  94. 'calendar-item--invalid': weeks.extraInfo && weeks.extraInfo.info < 70
  95. }">
  96. <view class="date">{{ weeks.date }}</view>
  97. <view class="info">
  98. <text v-if="weeks.extraInfo && weeks.extraInfo.info">{{ weeks.extraInfo.info }}%</text>
  99. </view>
  100. </view>
  101. </template>
  102. </uni-calendar>
  103. <!-- Loading 覆盖层 -->
  104. <view v-if="loading" class="calendar-loading-overlay">
  105. <uv-loading-icon mode="circle" size="32" color="#31a0fc" />
  106. </view>
  107. </view>
  108. </view>
  109. </ie-popup>
  110. </view>
  111. </template>
  112. <script lang="ts" setup>
  113. import { TableColumnConfig } from '@/types';
  114. import ieEchart from './ie-echart/ie-echart.vue';
  115. import { useCalendar } from '@/composables/useCalendar';
  116. const props = defineProps<{
  117. recordId?: number;
  118. }>();
  119. // 使用 useCalendar composable
  120. const {
  121. selected,
  122. statistics,
  123. currentDate,
  124. yearList,
  125. displayMode,
  126. currentMonthRange,
  127. canGoPrev,
  128. canGoNext,
  129. loading,
  130. currentYear,
  131. currentMonth,
  132. goToPrevMonth,
  133. goToNextMonth,
  134. goToYear,
  135. goToYearMonth,
  136. initializeFormData,
  137. init: initCalendar
  138. } = useCalendar();
  139. // 表单数据 - 与日历同步
  140. const form = ref({
  141. year: '',
  142. month: '',
  143. })
  144. // 月份列表(不能超过当前月份)
  145. const monthList = computed(() => {
  146. const today = new Date();
  147. const currentYearToday = today.getFullYear();
  148. const currentMonthToday = today.getMonth() + 1;
  149. const selectedYear = parseInt(form.value.year) || currentYear.value;
  150. const months = [];
  151. const maxMonth = selectedYear === currentYearToday ? currentMonthToday : 12;
  152. for (let i = 1; i <= maxMonth; i++) {
  153. months.push({
  154. label: `${i}月`,
  155. value: i.toString()
  156. });
  157. }
  158. return months;
  159. });
  160. // 检查是否可以选择日历
  161. const canOpenCalendar = computed(() => {
  162. // 必须选择年份
  163. if (!form.value.year) {
  164. return false;
  165. }
  166. // 如果是month模式,必须选择月份
  167. if (displayMode.value === 'month') {
  168. return !!form.value.month;
  169. }
  170. // 如果是year模式,不能打开日历(年份模式不显示记录表)
  171. return false;
  172. });
  173. // 导航按钮样式计算属性
  174. const prevButtonClass = computed(() => {
  175. return {
  176. 'btn-enable': canGoPrev.value && !loading.value,
  177. 'btn-disabled': !canGoPrev.value || loading.value,
  178. 'opacity-50': loading.value
  179. };
  180. });
  181. const nextButtonClass = computed(() => {
  182. return {
  183. 'btn-enable': canGoNext.value && !loading.value,
  184. 'btn-disabled': !canGoNext.value || loading.value,
  185. 'opacity-50': loading.value
  186. };
  187. });
  188. const calendarButtonClass = computed(() => {
  189. return {
  190. 'opacity-50 cursor-not-allowed': !canOpenCalendar.value,
  191. 'cursor-pointer': canOpenCalendar.value
  192. };
  193. });
  194. // 图表配置
  195. const options1 = computed(() => {
  196. return {
  197. title: {
  198. show: false,
  199. text: statistics.value.total.toString(),
  200. subtext: '{a|刷题总量}',
  201. left: 'center',
  202. top: '34%',
  203. textStyle: {
  204. fontSize: 20,
  205. fontWeight: 'bold',
  206. color: '#222'
  207. },
  208. subtextStyle: {
  209. fontSize: 12,
  210. color: '#666',
  211. lineHeight: 12,
  212. rich: {
  213. a: {
  214. color: '#B3B3B3',
  215. padding: [0, 0, 10, 0]
  216. },
  217. }
  218. }
  219. },
  220. series: [
  221. {
  222. type: 'pie',
  223. radius: ['60%', '80%'],
  224. startAngle: 90,
  225. silent: true,
  226. label: { show: false },
  227. data: [
  228. {
  229. value: 45,
  230. itemStyle: {
  231. color: {
  232. type: 'linear',
  233. x: 0,
  234. y: 0,
  235. x2: 1,
  236. y2: 1,
  237. colorStops: [
  238. { offset: 0, color: '#FEC048' },
  239. { offset: 1, color: '#F9942F' }
  240. ]
  241. }
  242. }
  243. },
  244. {
  245. value: 55,
  246. itemStyle: { color: '#FFF5DE' }
  247. }
  248. ]
  249. }
  250. ]
  251. };
  252. });
  253. const options2 = computed(() => {
  254. const accuracy = statistics.value.rate;
  255. return {
  256. title: {
  257. show: false,
  258. text: `${accuracy}%`,
  259. subtext: '{a|正确率}',
  260. left: 'center',
  261. top: '34%',
  262. textStyle: {
  263. fontSize: 20,
  264. fontWeight: 'bold',
  265. color: '#222'
  266. },
  267. subtextStyle: {
  268. fontSize: 12,
  269. color: '#666',
  270. lineHeight: 12,
  271. rich: {
  272. a: {
  273. color: '#B3B3B3',
  274. padding: [0, 0, 10, 0]
  275. },
  276. }
  277. }
  278. },
  279. series: [
  280. {
  281. type: 'pie',
  282. radius: ['60%', '80%'],
  283. startAngle: 90,
  284. silent: true,
  285. label: { show: false },
  286. data: [
  287. {
  288. value: accuracy,
  289. itemStyle: {
  290. color: {
  291. type: 'linear',
  292. x: 0,
  293. y: 0,
  294. x2: 1,
  295. y2: 1,
  296. colorStops: [
  297. { offset: 0, color: '#70C8FD' },
  298. { offset: 1, color: '#31A0FC' }
  299. ]
  300. }
  301. }
  302. },
  303. {
  304. value: 100 - accuracy,
  305. itemStyle: { color: '#EBF9FF' }
  306. }
  307. ]
  308. }
  309. ]
  310. };
  311. });
  312. // 表格配置
  313. const tableColumns = ref<TableColumnConfig[]>([
  314. {
  315. prop: 'date',
  316. label: '日期',
  317. flex: 1,
  318. slot: 'date'
  319. },
  320. {
  321. prop: 'questionNum',
  322. label: '题量',
  323. flex: 1,
  324. slot: 'questionNum'
  325. },
  326. {
  327. prop: 'correctNum',
  328. label: '正确率',
  329. flex: 1,
  330. slot: 'correctNum'
  331. }
  332. ]);
  333. // 表格数据(从 selected 数据转换)
  334. const tableDate = computed(() => {
  335. return statistics.value.list;
  336. });
  337. // 练习天数统计
  338. const practiceDays = computed(() => {
  339. return statistics.value.studyDays;
  340. });
  341. // 日历标题
  342. const calendarTitle = computed(() => {
  343. const year = currentMonthRange.value?.startDate?.split('-')[0] || new Date().getFullYear();
  344. const month = currentMonthRange.value?.startDate?.split('-')[1] || new Date().getMonth() + 1;
  345. return `${year}年-${month}月`;
  346. });
  347. // 日历副标题
  348. const calendarSubTitle = computed(() => {
  349. const month = currentMonthRange.value?.startDate?.split('-')[1] || new Date().getMonth() + 1;
  350. return `本月(${month}月)已累计刷题`;
  351. });
  352. // 导航方法
  353. const handlePrev = async () => {
  354. if (loading.value) return; // 加载中时禁止操作
  355. uni.$ie.showLoading();
  356. await goToPrevMonth();
  357. uni.$ie.hideLoading();
  358. };
  359. const handleNext = async () => {
  360. if (loading.value) return; // 加载中时禁止操作
  361. uni.$ie.showLoading();
  362. await goToNextMonth();
  363. uni.$ie.hideLoading();
  364. };
  365. // 表单处理 - 与日历同步
  366. const handleYearChange = async () => {
  367. // 年份切换时清空月份和周
  368. form.value.month = '';
  369. // 年份切换时请求年度数据,但不显示记录表
  370. if (form.value.year) {
  371. const year = parseInt(form.value.year);
  372. // 请求年度数据来更新统计信息
  373. await goToYear(year);
  374. }
  375. };
  376. const handleMonthChange = async () => {
  377. if (form.value.year && form.value.month) {
  378. // 切换到月份模式并跳转到指定年月
  379. await goToYearMonth(parseInt(form.value.year), parseInt(form.value.month));
  380. }
  381. };
  382. // 监听日历状态变化,同步到表单
  383. watch([currentYear, currentMonth, displayMode], ([year, month, mode]) => {
  384. form.value.year = year.toString();
  385. // 只有在非年份模式下才同步月份,年份模式下保持用户选择
  386. if (mode !== 'year') {
  387. form.value.month = month.toString();
  388. }
  389. }, { immediate: true });
  390. // 初始化默认选中当前时间
  391. const initializeDefaultSelection = () => {
  392. const formData = initializeFormData();
  393. form.value.year = formData.year;
  394. form.value.month = formData.month;
  395. };
  396. // 监听日历组件内部事件,确保数据同步
  397. const handleCalendarWeekChange = (event: any) => {
  398. };
  399. const handleCalendarMonthSwitch = (event: any) => {
  400. // 更新外部状态以保持同步
  401. if (event.year && event.month) {
  402. // 通过 goToYearMonth 方法来更新状态
  403. goToYearMonth(event.year, event.month);
  404. }
  405. };
  406. const calendarRef = ref();
  407. const calendarPopupRef = ref();
  408. const handleOpenCalendar = () => {
  409. if (canOpenCalendar.value && calendarPopupRef.value) {
  410. calendarPopupRef.value.open();
  411. }
  412. };
  413. // 初始化
  414. onMounted(async () => {
  415. // 先初始化默认选中
  416. initializeDefaultSelection();
  417. // 显示全屏loading
  418. uni.$ie.showLoading();
  419. try {
  420. // 初始化日历
  421. await initCalendar(props.recordId);
  422. } finally {
  423. // 隐藏loading
  424. uni.$ie.hideLoading();
  425. }
  426. });
  427. </script>
  428. <style lang="scss" scoped>
  429. .picker-wrap {
  430. @apply flex items-center px-12 w-fit border border-solid border-border rounded-4 h-56 min-w-140;
  431. }
  432. .btn-wrap {
  433. @apply flex items-center gap-x-10 bg-primary text-white text-26 px-10 h-56 rounded-4;
  434. }
  435. .btn-enable {
  436. @apply bg-[#EEF4FA] cursor-pointer;
  437. }
  438. .btn-disabled {
  439. @apply bg-[#F5F5F5] cursor-not-allowed;
  440. }
  441. .calendar-item {
  442. @apply rounded-5 text-center w-64 h-56 mx-auto py-8;
  443. .date {
  444. color: #222;
  445. font-size: 30rpx;
  446. line-height: 30rpx;
  447. }
  448. .info {
  449. font-size: 20rpx;
  450. line-height: 20rpx;
  451. margin-top: 4rpx;
  452. }
  453. }
  454. .uni-calendar-item--disable {
  455. opacity: 0;
  456. }
  457. .calendar-item--week-mode-disabled {
  458. .date {
  459. color: #cccccc;
  460. }
  461. }
  462. .calendar-item--valid {
  463. --valid-color: #22C55E;
  464. background-color: #F0FDF4;
  465. border: 1px solid var(--valid-color);
  466. .info {
  467. color: var(--valid-color);
  468. }
  469. }
  470. .calendar-item--invalid {
  471. --invalid-color: #FF5B5C;
  472. background-color: #FEEDE9;
  473. border: 1px solid var(--invalid-color);
  474. .info {
  475. color: var(--invalid-color);
  476. }
  477. }
  478. .calendar-loading-overlay {
  479. position: absolute;
  480. top: 0;
  481. left: 0;
  482. right: 0;
  483. bottom: 0;
  484. background-color: rgba(255, 255, 255, 0.8);
  485. display: flex;
  486. align-items: center;
  487. justify-content: center;
  488. z-index: 10;
  489. }
  490. </style>