@rao2126340634/yt-ui 1.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/package.json +43 -0
  2. package/src/components/yt-avatar/yt-avatar.vue +108 -0
  3. package/src/components/yt-badge/yt-badge.vue +65 -0
  4. package/src/components/yt-button/yt-button.vue +79 -0
  5. package/src/components/yt-calendar/yt-calendar.vue +327 -0
  6. package/src/components/yt-card/yt-card.vue +93 -0
  7. package/src/components/yt-checkbox-group/yt-checkbox-group.vue +80 -0
  8. package/src/components/yt-collapse/yt-collapse.vue +115 -0
  9. package/src/components/yt-divider/yt-divider.vue +110 -0
  10. package/src/components/yt-dots/yt-dots.vue +83 -0
  11. package/src/components/yt-empty/yt-empty.vue +43 -0
  12. package/src/components/yt-fab/yt-fab.vue +132 -0
  13. package/src/components/yt-form/yt-form.vue +86 -0
  14. package/src/components/yt-icon/icon-map.ts +52 -0
  15. package/src/components/yt-icon/yt-icon.vue +67 -0
  16. package/src/components/yt-input/yt-input.vue +162 -0
  17. package/src/components/yt-line/yt-line.vue +71 -0
  18. package/src/components/yt-loading/yt-loading.vue +66 -0
  19. package/src/components/yt-movable-view/yt-movable-view.vue +121 -0
  20. package/src/components/yt-notice-bar/yt-notice-bar.vue +91 -0
  21. package/src/components/yt-overlay/yt-overlay.vue +59 -0
  22. package/src/components/yt-picker/yt-picker.vue +111 -0
  23. package/src/components/yt-popup/yt-popup.vue +222 -0
  24. package/src/components/yt-radio-group/yt-radio-group.vue +89 -0
  25. package/src/components/yt-result/yt-result.vue +52 -0
  26. package/src/components/yt-schedule/yt-schedule.vue +737 -0
  27. package/src/components/yt-search/yt-search.vue +127 -0
  28. package/src/components/yt-segment/yt-segment.vue +65 -0
  29. package/src/components/yt-slider/yt-slider.vue +87 -0
  30. package/src/components/yt-steps/yt-steps.vue +90 -0
  31. package/src/components/yt-swiper/yt-swiper.vue +553 -0
  32. package/src/components/yt-switch/yt-switch.vue +76 -0
  33. package/src/components/yt-switch/yt-tabbar/yt-tabbar.vue +198 -0
  34. package/src/components/yt-tabbar/yt-tabbar.vue +198 -0
  35. package/src/components/yt-tag/yt-tag.vue +68 -0
  36. package/src/components/yt-textarea/yt-textarea.vue +172 -0
  37. package/src/components/yt-top-tabbar/yt-top-tabbar.vue +87 -0
  38. package/src/components/yt-virtual-list/yt-virtual-list.vue +140 -0
  39. package/src/configs/scheduleConfig.ts +31 -0
  40. package/src/hooks/useCalendar.ts +79 -0
  41. package/src/hooks/useInterval.ts +28 -0
  42. package/src/hooks/useSchedule.ts +83 -0
  43. package/src/shims.d.ts +4 -0
  44. package/src/static/icons/QRcode.png +0 -0
  45. package/src/static/icons/arrow_down.png +0 -0
  46. package/src/static/icons/arrow_down_white.png +0 -0
  47. package/src/static/icons/arrow_left.png +0 -0
  48. package/src/static/icons/arrow_left_white.png +0 -0
  49. package/src/static/icons/arrow_right.png +0 -0
  50. package/src/static/icons/arrow_right_white.png +0 -0
  51. package/src/static/icons/arrow_up.png +0 -0
  52. package/src/static/icons/arrow_up_white.png +0 -0
  53. package/src/static/icons/calendar.png +0 -0
  54. package/src/static/icons/champion.png +0 -0
  55. package/src/static/icons/close.png +0 -0
  56. package/src/static/icons/community.png +0 -0
  57. package/src/static/icons/community_active.png +0 -0
  58. package/src/static/icons/course.png +0 -0
  59. package/src/static/icons/course_active.png +0 -0
  60. package/src/static/icons/default_avatar.png +0 -0
  61. package/src/static/icons/done.png +0 -0
  62. package/src/static/icons/door_enter.png +0 -0
  63. package/src/static/icons/door_exit.png +0 -0
  64. package/src/static/icons/edit.png +0 -0
  65. package/src/static/icons/empty.png +0 -0
  66. package/src/static/icons/error.png +0 -0
  67. package/src/static/icons/fail.png +0 -0
  68. package/src/static/icons/fail_result.png +0 -0
  69. package/src/static/icons/first_place.png +0 -0
  70. package/src/static/icons/home.png +0 -0
  71. package/src/static/icons/home_active.png +0 -0
  72. package/src/static/icons/hot_topic.png +0 -0
  73. package/src/static/icons/identity.png +0 -0
  74. package/src/static/icons/info_result.png +0 -0
  75. package/src/static/icons/me.png +0 -0
  76. package/src/static/icons/me_active.png +0 -0
  77. package/src/static/icons/official.png +0 -0
  78. package/src/static/icons/passed.png +0 -0
  79. package/src/static/icons/plus.png +0 -0
  80. package/src/static/icons/read.png +0 -0
  81. package/src/static/icons/search.png +0 -0
  82. package/src/static/icons/second_place.png +0 -0
  83. package/src/static/icons/slider.png +0 -0
  84. package/src/static/icons/success_result.png +0 -0
  85. package/src/static/icons/third_place.png +0 -0
  86. package/src/static/icons/time.png +0 -0
  87. package/src/static/icons/unpassed.png +0 -0
  88. package/src/static/icons/winner.png +0 -0
  89. package/src/styles/_anim.scss +130 -0
  90. package/src/styles/_mixins.scss +6 -0
  91. package/src/styles/_theme-utils.scss +11 -0
  92. package/src/styles/_themes.scss +392 -0
  93. package/src/styles/_var.scss +65 -0
  94. package/src/styles/components/_avatar.scss +21 -0
  95. package/src/styles/components/_badge.scss +35 -0
  96. package/src/styles/components/_button.scss +131 -0
  97. package/src/styles/components/_calendar.scss +145 -0
  98. package/src/styles/components/_card.scss +34 -0
  99. package/src/styles/components/_collapse.scss +58 -0
  100. package/src/styles/components/_divider.scss +57 -0
  101. package/src/styles/components/_dots.scss +42 -0
  102. package/src/styles/components/_fab.scss +104 -0
  103. package/src/styles/components/_icon.scss +6 -0
  104. package/src/styles/components/_line.scss +66 -0
  105. package/src/styles/components/_loading.scss +36 -0
  106. package/src/styles/components/_notice-bar.scss +39 -0
  107. package/src/styles/components/_overlay.scss +17 -0
  108. package/src/styles/components/_popup.scss +118 -0
  109. package/src/styles/components/_schedule.scss +324 -0
  110. package/src/styles/components/_search.scss +35 -0
  111. package/src/styles/components/_segment.scss +40 -0
  112. package/src/styles/components/_steps.scss +97 -0
  113. package/src/styles/components/_swiper.scss +122 -0
  114. package/src/styles/components/_tabbar.scss +106 -0
  115. package/src/styles/components/_tag.scss +84 -0
  116. package/src/styles/components/_top-tabbar.scss +40 -0
  117. package/src/types/prop-types.ts +47 -0
  118. package/src/types/theme-types.ts +16 -0
  119. package/src/utils/date.ts +31 -0
  120. package/src/utils/util.ts +72 -0
@@ -0,0 +1,737 @@
1
+ <script setup lang="ts">
2
+ import { computed, ComputedRef, onUnmounted, ref, shallowRef, toRaw, watch, watchEffect } from 'vue'
3
+ import { Schedule, useSchedule, WeekDate } from '../../hooks/useSchedule'
4
+ import { getLessonCoordinates } from '../../utils/util'
5
+ import { defaultColorList, editFormRules } from '../../configs/scheduleConfig'
6
+ import { ThemeColor } from '../../types/theme-types'
7
+ import { onHide, onShow } from '@dcloudio/uni-app'
8
+
9
+ export interface CourseData {
10
+ type: 'course' | 'agenda'
11
+ name: string
12
+ location?: string
13
+ teacher?: string
14
+ class?: string
15
+ isConflict?: boolean // 课程是否与日程重叠
16
+ style?: string // 缓存内联样式,减少字符串开销
17
+ }
18
+
19
+ export interface Course extends CourseData {
20
+ type: 'course'
21
+ x: number // 横坐标 从1开始
22
+ y: number // 纵坐标 从1开始
23
+ z: number[] // 第i周 从1开始
24
+ }
25
+
26
+ export interface Agenda extends CourseData {
27
+ type: 'agenda'
28
+ x: number
29
+ y: number
30
+ z: number[]
31
+ }
32
+
33
+ export interface ScheduleData {
34
+ term: number
35
+ termYear: string
36
+ start: string
37
+ course: Course[]
38
+ agenda: Agenda[]
39
+ }
40
+
41
+ export type TimeList = {
42
+ first: { start: string; end: string }
43
+ second: { start: string; end: string }
44
+ }[]
45
+
46
+ interface Props {
47
+ theme?: ThemeColor
48
+ width?: number | string
49
+ height?: number | string
50
+ animation?: boolean
51
+ duration?: number
52
+ rows?: number // 行数(课节数)
53
+ weeks?: number // 周数
54
+ activeWeek?: number // 默认选中的周(索引0开始)
55
+ timeList?: TimeList
56
+ colorList?: string[]
57
+ data?: ScheduleData
58
+ }
59
+
60
+ const props = withDefaults(defineProps<Props>(), {
61
+ theme: 'classic',
62
+ width: '100vw',
63
+ height: '100vh',
64
+ animation: true,
65
+ duration: 300,
66
+ rows: 6,
67
+ weeks: 20,
68
+ activeWeek: 0,
69
+ timeList: () => [],
70
+ colorList: () => defaultColorList,
71
+ data: () => ({
72
+ term: 0,
73
+ termYear: '',
74
+ start: '',
75
+ course: [],
76
+ agenda: []
77
+ })
78
+ })
79
+
80
+ const emit = defineEmits<{
81
+ change: [weekIndex: number]
82
+ courseClick: [course: CourseData & { weekIndex: number; gridIndex: number }]
83
+ agendaChange: [agenda: Agenda[]]
84
+ }>()
85
+
86
+ const schedule = shallowRef<Schedule>(null)
87
+ watchEffect(() => {
88
+ schedule.value = useSchedule(props.data.start, props.weeks)
89
+ })
90
+ const curWeek = ref(props.activeWeek) // 当前选中周(索引0开始)
91
+ watch(() => props.activeWeek, (val: number) => {
92
+ curWeek.value = val
93
+ })
94
+ const showWeek = ref(false)
95
+ const enableAutoScrollWeek = ref(true)
96
+ const selectedDay = ref<null | number>(null)
97
+ const isPopping = ref(false)
98
+ const editMode = ref(false)
99
+ const editState = shallowRef({
100
+ cycleRange: [] as string[][],
101
+ cycle: '',
102
+ time: ''
103
+ })
104
+ const selectedCourse = shallowRef<(CourseData & { weekIndex: number; gridIndex: number }) | null>(
105
+ null
106
+ )
107
+ const hasPoppedUp = ref(false) // 手动触发popup前不渲染
108
+ const localAgendas = shallowRef<Agenda[]>(JSON.parse(JSON.stringify(props.data.agenda)))
109
+
110
+ const weekDate: ComputedRef<WeekDate> = computed(() => {
111
+ return schedule.value?.getWeekDate(curWeek.value + 1) || []
112
+ })
113
+ const weekMonth: ComputedRef<number | ''> = computed(() => {
114
+ return schedule.value?.getMonthOfStartDate(curWeek.value + 1) || ''
115
+ })
116
+ const weekList = computed(() => {
117
+ return Array.from({ length: props.weeks }, (_, index) => index)
118
+ })
119
+ const gridList = computed(() => {
120
+ return Array.from({ length: props.rows * 7 }, (_, index) => index)
121
+ })
122
+ const gridIndexes = computed(() => {
123
+ if (selectedDay.value === null) {
124
+ return gridList.value
125
+ } else {
126
+ const day = selectedDay.value // 0-6
127
+ const indexes: number[] = []
128
+ for (let row = 0; row < props.rows; row++) {
129
+ indexes.push(day + row * 7)
130
+ }
131
+ return indexes // 选中天数单列索引
132
+ }
133
+ })
134
+ const scheduleClass = computed(() => {
135
+ return [
136
+ 'yt-schedule',
137
+ `yt-schedule--theme-${props.theme}`,
138
+ {
139
+ 'yt-schedule-show-week': showWeek.value,
140
+ 'yt-schedule-current-week': curWeek.value
141
+ }
142
+ ]
143
+ })
144
+ const scheduleStyle = computed(() => ({
145
+ width: typeof props.width === 'number' ? `${props.width}px` : props.width,
146
+ height: typeof props.height === 'number' ? `${props.height}px` : props.height,
147
+ '--grid-cols': selectedDay.value === null ? 7 : 1,
148
+ '--grid-rows': props.rows
149
+ }))
150
+ const weekTextClass = (index: number) => ({
151
+ 'yt-schedule--week-text': true,
152
+ 'yt-schedule--week-text-active': curWeek.value === index
153
+ })
154
+ const gridItemBoxClass = (weekIndex: number) => {
155
+ const isCurWeek = weekIndex === curWeek.value
156
+ const animEnabled = props.animation
157
+ return {
158
+ 'yt-schedule--table-grid-item-box': true,
159
+ 'yt-schedule--table-grid-item-box-active-anim': animEnabled && isCurWeek,
160
+ 'yt-schedule--table-grid-item-box-active': !animEnabled && isCurWeek
161
+ }
162
+ }
163
+ const dateContainerClass = (index: number) => ({
164
+ 'yt-schedule--date-container': true,
165
+ 'yt-schedule--date-container-active': selectedDay.value === index,
166
+ 'yt-schedule--date-container-current-week': weekDate.value?.[index]?.isCurWeek,
167
+ 'yt-schedule--date-container-today': weekDate.value?.[index]?.isToday
168
+ })
169
+
170
+ let colorCache: Record<string, string> = {}
171
+ let colorCounter = 0
172
+ const COLOR_AGENDA = '#999'
173
+ const weekMapsCache = shallowRef<Map<number, Map<string, CourseData>>>(new Map())
174
+ function loadCourse(weekIndex: number) {
175
+ if (weekMapsCache.value.has(weekIndex)) return
176
+ const map = new Map<string, CourseData>()
177
+ const week = weekIndex + 1
178
+ const { course } = props.data
179
+ const agenda = localAgendas.value
180
+ // 填充当前周课表数据
181
+ const fillCourse = () => {
182
+ course?.forEach(course => {
183
+ if (course.z.includes(week)) {
184
+ const key = `${course.x}-${course.y}`
185
+ if (!colorCache[course.name]) {
186
+ colorCache[course.name] = props.colorList[colorCounter++ % props.colorList.length]
187
+ }
188
+ // 内联样式缓存
189
+ const color = colorCache[course.name]
190
+ const delay = (course.y - 1) * 0.04
191
+ const transform = props.animation ? 'translateY(15px) scale(0.1)' : 'none'
192
+ const style = `--grid-item-box-color: ${color}; --grid-item-anim-delay: ${delay}s; --grid-item-box-transform: ${transform}`
193
+
194
+ map.set(key, {
195
+ type: 'course',
196
+ name: course.name,
197
+ location: course.location,
198
+ teacher: course.teacher,
199
+ class: course.class,
200
+ style
201
+ })
202
+ }
203
+ })
204
+ }
205
+ // 填充当前周日程数据
206
+ const fillAgenda = () => {
207
+ agenda?.forEach(agenda => {
208
+ if (agenda.z.includes(week)) {
209
+ const key = `${agenda.x}-${agenda.y}`
210
+ // 检测并标记冲突
211
+ const item = map.get(key)
212
+ if (item && item.type !== 'agenda') {
213
+ map.set(key, { ...item, isConflict: true })
214
+ } else {
215
+ const color = COLOR_AGENDA
216
+ const delay = (agenda.y - 1) * 0.04
217
+ const transform = props.animation ? 'translateY(15px) scale(0.1)' : 'none'
218
+ const style = `--grid-item-box-color: ${color}; --grid-item-anim-delay: ${delay}s; --grid-item-box-transform: ${transform}`
219
+ map.set(key, {
220
+ type: 'agenda',
221
+ name: agenda.name,
222
+ location: agenda.location,
223
+ style
224
+ })
225
+ }
226
+ }
227
+ })
228
+ }
229
+ fillCourse()
230
+ fillAgenda()
231
+ const newCache = new Map(weekMapsCache.value)
232
+ newCache.set(weekIndex, map)
233
+ weekMapsCache.value = newCache
234
+ }
235
+ function hasCourses(weekIndex: number) {
236
+ return !!weekMapsCache.value.get(weekIndex)?.size
237
+ }
238
+ // 缓存每一格坐标
239
+ const gridCoords = computed(() => {
240
+ return gridList.value.map((_, index) => getLessonCoordinates(index))
241
+ })
242
+ const courseClasses = computed(() => {
243
+ return selectedCourse.value?.class?.split(',') || []
244
+ })
245
+ // 获取某一格课程
246
+ function getCachedCourse(weekIndex: number, index: number) {
247
+ const coord = gridCoords.value[index]
248
+ const key = `${coord.x}-${coord.y}`
249
+ return weekMapsCache.value.get(weekIndex)?.get(key) || null
250
+ }
251
+ function handleWeekClick(index: number) {
252
+ enableAutoScrollWeek.value = false
253
+ if (curWeek.value === index) return
254
+ curWeek.value = index
255
+ emit('change', index)
256
+ }
257
+ function handleDateClick(index: number) {
258
+ if (selectedDay.value === index) selectedDay.value = null
259
+ else selectedDay.value = index
260
+ }
261
+ function handleSwiperChange(index: number) {
262
+ enableAutoScrollWeek.value = true
263
+ if (curWeek.value === index) return
264
+ emit('change', index)
265
+ }
266
+ function handleCourseClick(weekIndex: number, index: number) {
267
+ hasPoppedUp.value = true // 渲染popup
268
+ const course = getCachedCourse(weekIndex, index) as CourseData
269
+ editMode.value = !course
270
+ // 添加日程模式更新表单组件值
271
+ if (editMode.value) {
272
+ const week = weekIndex + 1
273
+ const day = schedule.value?.weekDays[index % 7]
274
+ const lessonNum = Math.floor(index / 7) * 2 + 1
275
+ const range = Array.from({ length: props.weeks }, (_, index) => `第${index + 1}周`)
276
+ editState.value = {
277
+ cycleRange: [range, range],
278
+ cycle: `第${week}周 至 第${week}周`,
279
+ time: `周${day} 第${lessonNum}-${lessonNum + 1}节`
280
+ }
281
+ }
282
+ // 浅拷贝,确保内部无引用类型
283
+ const item = {
284
+ ...course,
285
+ weekIndex,
286
+ gridIndex: index
287
+ }
288
+ selectedCourse.value = {
289
+ ...item
290
+ }
291
+ emit('courseClick', {
292
+ ...item
293
+ })
294
+ isPopping.value = true
295
+ }
296
+ function handleCourseLongPress(weekIndex: number, index: number) {
297
+ const course = getCachedCourse(weekIndex, index)
298
+ if (course?.type !== 'agenda') return
299
+ uni.showModal({
300
+ title: '系统提示',
301
+ content: '确定要删除该日程吗?',
302
+ success: res => {
303
+ if (!res.confirm) return
304
+ const { x, y } = getLessonCoordinates(index)
305
+ const week = weekIndex + 1
306
+ const targetIndex = localAgendas.value.findIndex(
307
+ agenda => agenda.x === x && agenda.y === y && agenda.z.includes(week)
308
+ )
309
+ if (targetIndex !== -1) {
310
+ const targetAgenda = localAgendas.value[targetIndex]
311
+ const newAgendas = [...localAgendas.value]
312
+ newAgendas.splice(targetIndex, 1)
313
+ localAgendas.value = newAgendas
314
+ emit(
315
+ 'agendaChange',
316
+ newAgendas.map(item => toRaw(item))
317
+ )
318
+ const newCache = new Map(weekMapsCache.value)
319
+ targetAgenda?.z?.forEach(zWeek => {
320
+ newCache.delete(zWeek - 1)
321
+ })
322
+ weekMapsCache.value = newCache
323
+ lazyLoadCourses()
324
+ uni.showToast({ title: '删除成功', icon: 'success' })
325
+ }
326
+ }
327
+ })
328
+ }
329
+ // 选择日程周期
330
+ function handleCyclePicked(value: number[]) {
331
+ let [start, end] = [value[0] + 1, value[1] + 1]
332
+ if (start > end) [start, end] = [end, start]
333
+ editState.value = {
334
+ ...editState.value,
335
+ cycle: `第${start}周 至 第${end}周`
336
+ }
337
+ }
338
+ function shouldRender(weekIndex: number) {
339
+ return curWeek.value === weekIndex
340
+ }
341
+ const editFormRef = ref()
342
+ function handleAddAgenda() {
343
+ editFormRef.value.validate((validated: boolean) => {
344
+ if (!validated || !selectedCourse.value) return
345
+ const formData = editFormRef.value.submitForm()
346
+ const { x, y } = getLessonCoordinates(selectedCourse.value.gridIndex)
347
+ const zMatch = formData.cycleInput.match(/^第(\d+)周 至 第(\d+)周$/)
348
+ const zStart = Number(zMatch[1])
349
+ const zEnd = Number(zMatch[2])
350
+ const z = Array.from({ length: zEnd - zStart + 1 }, (_, index) => zStart + index)
351
+ const repeatWeeks = getAgendaRepeatWeeks(x, y, z)
352
+ if (repeatWeeks.length) {
353
+ let repeatStr = ''
354
+ if (repeatWeeks.length > 5) {
355
+ repeatWeeks.splice(5)
356
+ repeatStr = repeatWeeks.join('、') + '...'
357
+ } else {
358
+ repeatStr = repeatWeeks.join('、')
359
+ }
360
+ const title = `第${repeatStr}周已存在日程,无法添加`
361
+ uni.showToast({
362
+ title,
363
+ icon: 'none'
364
+ })
365
+ return
366
+ }
367
+ const agenda: Agenda = {
368
+ type: 'agenda',
369
+ name: formData.nameInput,
370
+ location: formData.locationInput,
371
+ x,
372
+ y,
373
+ z
374
+ }
375
+ const agendas = localAgendas.value.concat(agenda)
376
+ localAgendas.value = agendas
377
+ emit(
378
+ 'agendaChange',
379
+ agendas.map(item => toRaw(item))
380
+ )
381
+ // 清除新日程所在周数的缓存,重新加载
382
+ z?.forEach(week => weekMapsCache.value.delete(week - 1))
383
+ weekMapsCache.value = new Map(weekMapsCache.value)
384
+ isPopping.value = false
385
+ editFormRef.value.resetForm(['nameInput', 'locationInput'])
386
+ if (zStart - 1 !== curWeek.value) {
387
+ // 跳到创建日程的起始周,触发watch自动加载
388
+ curWeek.value = zStart - 1
389
+ } else {
390
+ lazyLoadCourses()
391
+ }
392
+ })
393
+ }
394
+ // 获取与传入的日程重复的周数
395
+ function getAgendaRepeatWeeks(x: number, y: number, z: number[]): number[] {
396
+ const result: number[] = []
397
+ const targetWeeks = new Set(z)
398
+ localAgendas.value?.forEach(agenda => {
399
+ if (agenda.x === x && agenda.y === y) {
400
+ agenda.z?.forEach(existingWeek => {
401
+ if (targetWeeks.has(existingWeek)) {
402
+ result.push(existingWeek)
403
+ }
404
+ })
405
+ }
406
+ })
407
+ return result.sort((a, b) => a - b)
408
+ }
409
+ function handleClearAgenda() {
410
+ uni.showModal({
411
+ title: '提示',
412
+ content: '确定要清空所有日程吗?',
413
+ success: res => {
414
+ if (!res.confirm) return
415
+ localAgendas.value = []
416
+ emit('agendaChange', localAgendas.value)
417
+ weekMapsCache.value = new Map()
418
+ lazyLoadCourses()
419
+ uni.showToast({ title: '清除成功', icon: 'success' })
420
+ }
421
+ })
422
+ }
423
+ // 加载三页数据
424
+ function lazyLoadCourses() {
425
+ const weeks = props.weeks
426
+ const cur = curWeek.value
427
+ const prev = (cur - 1 + weeks) % weeks
428
+ const next = (cur + 1) % weeks
429
+ for (let weekIndex of [cur, prev, next]) {
430
+ loadCourse(weekIndex)
431
+ }
432
+ }
433
+ // 清空所有缓存
434
+ function clearCache() {
435
+ colorCache = {}
436
+ colorCounter = 0
437
+ weekMapsCache.value.clear()
438
+ }
439
+
440
+ // 课表数据变化重新加载
441
+ watch(
442
+ [() => props.data?.course, () => props.animation],
443
+ () => {
444
+ clearCache()
445
+ lazyLoadCourses()
446
+ },
447
+ {
448
+ deep: true,
449
+ flush: 'post'
450
+ }
451
+ )
452
+
453
+ // 加载当前三页数据
454
+ watch(
455
+ () => curWeek.value,
456
+ () => {
457
+ lazyLoadCourses()
458
+ },
459
+ { immediate: true, flush: 'post' }
460
+ )
461
+
462
+ onUnmounted(() => {
463
+ clearCache()
464
+ })
465
+
466
+ let isLeaving = false
467
+ onShow(() => {
468
+ if (isLeaving) {
469
+ isLeaving = false
470
+ lazyLoadCourses()
471
+ }
472
+ })
473
+
474
+ // 小程序切换页面清理缓存,防止内存占用过大
475
+ onHide(() => {
476
+ isLeaving = true
477
+ clearCache()
478
+ })
479
+
480
+ defineOptions({
481
+ name: 'YtSchedule'
482
+ })
483
+ </script>
484
+
485
+ <template>
486
+ <view
487
+ :class="scheduleClass"
488
+ :style="scheduleStyle"
489
+ >
490
+ <!-- schedule-header -->
491
+ <view
492
+ class="yt-schedule--header"
493
+ @click="showWeek = !showWeek"
494
+ >
495
+ <view
496
+ class="yt-schedule--header-clear"
497
+ :style="{
498
+ pointerEvents: localAgendas.length ? 'auto' : 'none',
499
+ opacity: localAgendas.length ? 1 : 0
500
+ }"
501
+ @click.stop="handleClearAgenda"
502
+ >
503
+ 清空日程
504
+ </view>
505
+ <view class="yt-schedule--header-time">
506
+ <span class="yt-schedule--header-arrow-text">◂</span>
507
+ <span>{{ data.termYear }}</span>
508
+ <span>第{{ data.term }}学期</span>
509
+ <span class="yt-schedule--header-time-week">{{ curWeek + 1 }}周</span>
510
+ <span class="yt-schedule--header-arrow-text">▸</span>
511
+ </view>
512
+ <yt-icon
513
+ class="yt-schedule--header-arrow"
514
+ name="ArrowUp"
515
+ :size="12"
516
+ :width="60"
517
+ :height="30"
518
+ />
519
+ </view>
520
+ <!-- schedule-week -->
521
+ <scroll-view
522
+ scroll-x
523
+ scroll-with-animation
524
+ :scroll-into-view="enableAutoScrollWeek ? `${'week-' + curWeek}` : ''"
525
+ :show-scrollbar="false"
526
+ class="yt-schedule--week"
527
+ >
528
+ <view
529
+ v-for="(i, index) in weeks"
530
+ :id="'week-' + index"
531
+ :key="index"
532
+ :class="weekTextClass(index)"
533
+ @click="handleWeekClick(index)"
534
+ >
535
+ 第{{ i }}周
536
+ </view>
537
+ </scroll-view>
538
+ <!-- schedule-date -->
539
+ <view class="yt-schedule--date">
540
+ <view class="yt-schedule--date-month">{{ weekMonth }}月</view>
541
+ <view
542
+ v-for="(day, index) in schedule?.weekDays"
543
+ :class="dateContainerClass(index as number)"
544
+ @click="handleDateClick(index as number)"
545
+ >
546
+ <view class="yt-schedule--date-container-day">{{ day }}</view>
547
+ <view class="yt-schedule--date-container-date">{{ weekDate?.[index as number]?.date || '' }}</view>
548
+ <view class="yt-schedule--date-container-dot" />
549
+ </view>
550
+ </view>
551
+ <!-- schedule-table -->
552
+ <view class="yt-schedule--table">
553
+ <!-- left-side-time-list -->
554
+ <view class="yt-schedule--table-time">
555
+ <template v-for="(i, index) in rows">
556
+ <view class="yt-schedule--table-time-item">
557
+ <view class="yt-schedule--table-time-item-title">{{ i * 2 - 1 }}</view>
558
+ <view class="yt-schedule--table-time-item-subtitle">
559
+ {{ timeList[index].first.start }}
560
+ </view>
561
+ <view class="yt-schedule--table-time-item-subtitle">
562
+ {{ timeList[index].first.end }}
563
+ </view>
564
+ </view>
565
+ <view class="yt-schedule--table-time-item">
566
+ <view class="yt-schedule--table-time-item-title">{{ i * 2 }}</view>
567
+ <view class="yt-schedule--table-time-item-subtitle">
568
+ {{ timeList[index].second.start }}
569
+ </view>
570
+ <view class="yt-schedule--table-time-item-subtitle">
571
+ {{ timeList[index].second.end }}
572
+ </view>
573
+ </view>
574
+ </template>
575
+ </view>
576
+ <!-- lesson-grid -->
577
+ <yt-swiper
578
+ class="yt-schedule--table-swiper"
579
+ v-model="curWeek"
580
+ :list="weekList"
581
+ :duration="duration"
582
+ @change="handleSwiperChange"
583
+ >
584
+ <template #swiper-item="{ index: weekIndex }">
585
+ <view
586
+ class="yt-schedule--table-grid"
587
+ v-if="shouldRender(weekIndex)"
588
+ >
589
+ <view
590
+ class="yt-schedule--table-grid-item"
591
+ v-for="index in gridIndexes"
592
+ :key="`${weekIndex}-${index}`"
593
+ >
594
+ <view
595
+ :class="gridItemBoxClass(weekIndex)"
596
+ :style="getCachedCourse(weekIndex, index)?.style"
597
+ @click="handleCourseClick(weekIndex, index)"
598
+ @longpress="handleCourseLongPress(weekIndex, index)"
599
+ >
600
+ <view class="yt-schedule--table-grid-item-name">
601
+ {{ getCachedCourse(weekIndex, index)?.name }}
602
+ </view>
603
+ <view class="yt-schedule--table-grid-item-location">
604
+ {{ getCachedCourse(weekIndex, index)?.location }}
605
+ </view>
606
+ <view
607
+ class="yt-schedule--table-grid-item-conflict"
608
+ v-if="getCachedCourse(weekIndex, index)?.isConflict"
609
+ >
610
+ {{ '⚠' }}
611
+ </view>
612
+ </view>
613
+ </view>
614
+ <!-- empty-tip -->
615
+ <view
616
+ v-if="!hasCourses(weekIndex)"
617
+ class="yt-schedule--table-grid-empty"
618
+ >
619
+ Empty
620
+ </view>
621
+ </view>
622
+ </template>
623
+ </yt-swiper>
624
+ </view>
625
+ <!-- popup -->
626
+ <yt-popup
627
+ v-if="hasPoppedUp"
628
+ :headerType="editMode ? 'close-only' : 'none'"
629
+ :headerProps="{ title: '自定义日程' }"
630
+ :closeOnOverlayClick="!editMode"
631
+ placement="center"
632
+ v-model:visible="isPopping"
633
+ height="fit-content"
634
+ maxHeight="50vh"
635
+ >
636
+ <!-- preview -->
637
+ <view
638
+ v-if="!editMode"
639
+ class="yt-schedule--popup-preview"
640
+ >
641
+ <!-- course -->
642
+ <view
643
+ v-if="selectedCourse?.type === 'course'"
644
+ class="yt-schedule--popup-preview-course"
645
+ >
646
+ <view>课程 {{ selectedCourse?.name || '未知' }}</view>
647
+ <view>地点 {{ selectedCourse?.location || '未知' }}</view>
648
+ <view>教师 {{ selectedCourse?.teacher || '未知' }}</view>
649
+ <view>
650
+ 班级 {{ courseClasses.length ? '' : '未知' }}
651
+ <view
652
+ v-if="courseClasses.length"
653
+ v-for="className in courseClasses"
654
+ >
655
+ {{ className }}
656
+ </view>
657
+ </view>
658
+ </view>
659
+ <!-- divider-line -->
660
+ <view
661
+ v-if="selectedCourse?.isConflict"
662
+ class="yt-schedule--popup-preview-divider"
663
+ />
664
+ <!-- agenda -->
665
+ <view
666
+ v-if="selectedCourse?.type === 'agenda' || selectedCourse?.isConflict"
667
+ class="yt-schedule--popup-preview-agenda"
668
+ >
669
+ <view>日程 {{ selectedCourse?.name || '无' }}</view>
670
+ <view>地点 {{ selectedCourse?.location || '无' }}</view>
671
+ </view>
672
+ </view>
673
+ <!-- edit -->
674
+ <yt-form
675
+ v-else
676
+ ref="editFormRef"
677
+ :rules="editFormRules"
678
+ >
679
+ <view class="yt-schedule--popup-edit">
680
+ <view class="yt-schedule--popup-edit-name">
681
+ <view>日程</view>
682
+ <yt-input
683
+ name="nameInput"
684
+ theme="classic"
685
+ placeholder="请输入日程安排"
686
+ />
687
+ </view>
688
+ <view class="yt-schedule--popup-edit-cycle">
689
+ <view>周期</view>
690
+ <yt-picker
691
+ type="multi"
692
+ @change="handleCyclePicked"
693
+ :items="editState.cycleRange"
694
+ >
695
+ <yt-input
696
+ name="cycleInput"
697
+ theme="classic"
698
+ disabled
699
+ :value="editState.cycle"
700
+ />
701
+ </yt-picker>
702
+ </view>
703
+ <view class="yt-schedule--popup-edit-time">
704
+ <view>时间</view>
705
+ <yt-input
706
+ name="timeInput"
707
+ theme="classic"
708
+ disabled
709
+ :value="editState.time"
710
+ />
711
+ </view>
712
+ <view class="yt-schedule--popup-edit-location">
713
+ <view>地点</view>
714
+ <yt-input
715
+ name="locationInput"
716
+ theme="classic"
717
+ placeholder="请输入日程地点"
718
+ />
719
+ </view>
720
+
721
+ <yt-button
722
+ class="yt-schedule--popup-edit-submit"
723
+ size="small"
724
+ @click="handleAddAgenda"
725
+ >
726
+ 添加日程
727
+ </yt-button>
728
+ </view>
729
+ </yt-form>
730
+ </yt-popup>
731
+ </view>
732
+ </template>
733
+
734
+ <style lang="scss" scoped>
735
+ @use '../../styles/components/_schedule.scss';
736
+ @use '../../styles/themes';
737
+ </style>