|
|
@@ -10,6 +10,21 @@ import { SessionService } from '../../core/session.service';
|
|
|
|
|
|
type Item = Record<string, unknown>;
|
|
|
type ScheduleFilter = 'all' | 'pending' | 'finished';
|
|
|
+type ScheduleRange = 'day' | 'week' | 'all';
|
|
|
+
|
|
|
+interface CalendarDay {
|
|
|
+ date: string;
|
|
|
+ day: number;
|
|
|
+ inMonth: boolean;
|
|
|
+ isToday: boolean;
|
|
|
+ count: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface StudentOption {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ count: number;
|
|
|
+}
|
|
|
|
|
|
@Component({
|
|
|
selector: 'app-course-center-page',
|
|
|
@@ -21,21 +36,33 @@ type ScheduleFilter = 'all' | 'pending' | 'finished';
|
|
|
})
|
|
|
export class CourseCenterPageComponent implements OnInit {
|
|
|
readonly meta = inject(ActivatedRoute).snapshot.data['meta'] as PageMeta;
|
|
|
+ readonly isCoach = ['pages/home/shop', 'pages/member/ke_xiao'].includes(this.meta.path);
|
|
|
+ readonly isHistory = this.meta.path === 'pages/member/ke_xiao';
|
|
|
+ readonly hasScheduleCalendar = !this.isHistory;
|
|
|
+ readonly weekLabels = ['一', '二', '三', '四', '五', '六', '日'];
|
|
|
readonly courses = signal<Item[]>([]);
|
|
|
readonly schedules = signal<Item[]>([]);
|
|
|
readonly scheduleFilter = signal<ScheduleFilter>('all');
|
|
|
+ readonly scheduleRange = signal<ScheduleRange>('day');
|
|
|
+ readonly selectedDate = signal(this.today());
|
|
|
+ readonly selectedStudent = signal('all');
|
|
|
+ readonly calendarMonth = signal(this.firstOfMonth(new Date()));
|
|
|
readonly loading = signal(false);
|
|
|
readonly error = signal('');
|
|
|
- readonly visibleSchedules = computed(() => this.schedules().filter((item) => {
|
|
|
- if (this.scheduleFilter() === 'finished') return this.isFinished(item);
|
|
|
- if (this.scheduleFilter() === 'pending') return !this.isFinished(item);
|
|
|
- return true;
|
|
|
- }));
|
|
|
- readonly pendingCount = computed(() => this.schedules().filter((item) => !this.isFinished(item)).length);
|
|
|
- readonly finishedCount = computed(() => this.schedules().filter((item) => this.isFinished(item)).length);
|
|
|
+ readonly studentOptions = computed(() => this.buildStudentOptions());
|
|
|
+ readonly scopedSchedules = computed(() => this.schedules().filter((item) => this.matchesStudent(item)));
|
|
|
+ readonly filteredSchedulePool = computed(() => this.scopedSchedules().filter((item) => this.matchesStatus(item)));
|
|
|
+ readonly visibleSchedules = computed(() => this.filteredSchedulePool().filter((item) => this.matchesRange(item)));
|
|
|
+ readonly pendingCount = computed(() => this.scopedSchedules().filter((item) => !this.isFinished(item)).length);
|
|
|
+ readonly finishedCount = computed(() => this.scopedSchedules().filter((item) => this.isFinished(item)).length);
|
|
|
+ readonly calendarDays = computed(() => this.buildCalendarDays());
|
|
|
+ readonly calendarLabel = computed(() => new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long' }).format(this.calendarMonth()));
|
|
|
+ readonly scheduleRangeLabel = computed(() => {
|
|
|
+ if (this.scheduleRange() === 'all') return '全部日期';
|
|
|
+ if (this.scheduleRange() === 'week') return `${this.selectedDate()} 所在周`;
|
|
|
+ return this.selectedDate();
|
|
|
+ });
|
|
|
readonly icons = { AlertCircle, BarChart3, BookOpen, CalendarDays, ChevronRight, GraduationCap, LoaderCircle };
|
|
|
- readonly isCoach = ['pages/home/shop', 'pages/member/ke_xiao'].includes(this.meta.path);
|
|
|
- readonly isHistory = this.meta.path === 'pages/member/ke_xiao';
|
|
|
|
|
|
private readonly api = inject(ApiService);
|
|
|
private readonly router = inject(Router);
|
|
|
@@ -51,13 +78,16 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
this.error.set('');
|
|
|
const today = this.today();
|
|
|
if (this.isCoach) {
|
|
|
- const params: Record<string, unknown> = { uid, nid: 29, myfield2: `pl=${uid}`, psize: this.isHistory ? 13 : 200 };
|
|
|
+ const params: Record<string, unknown> = { uid, nid: 29, myfield2: `pl=${uid}`, psize: this.isHistory ? 13 : 500 };
|
|
|
if (this.isHistory) Object.assign(params, { orders: 'B.yysj=DESC', dszt: 30, xiaokejilu: 1 });
|
|
|
- else params['dateFilter'] = today;
|
|
|
+ else params['orders'] = 'B.yysj=ASC';
|
|
|
this.api.get<unknown>('content_list', params)
|
|
|
.pipe(finalize(() => this.loading.set(false)))
|
|
|
.subscribe({
|
|
|
- next: ({ result }) => this.schedules.set(this.asList(result)),
|
|
|
+ next: ({ result }) => {
|
|
|
+ this.schedules.set(this.asList(result));
|
|
|
+ if (!this.isHistory) this.selectDate(today);
|
|
|
+ },
|
|
|
error: (error: ApiError) => this.error.set(error.message),
|
|
|
});
|
|
|
return;
|
|
|
@@ -71,6 +101,7 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
}).pipe(finalize(() => this.loading.set(false))).subscribe(({ courses, schedules }) => {
|
|
|
this.courses.set(this.asList(courses.result));
|
|
|
this.schedules.set(this.asList(schedules.result));
|
|
|
+ this.selectDate(today);
|
|
|
const failures = [courses.retmsg, schedules.retmsg].filter(Boolean);
|
|
|
if (failures.length) this.error.set(`部分数据加载失败:${failures.join(';')}`);
|
|
|
});
|
|
|
@@ -78,8 +109,8 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
|
|
|
groups(): Array<{ date: string; items: Item[] }> {
|
|
|
const grouped = new Map<string, Item[]>();
|
|
|
- for (const item of this.visibleSchedules()) {
|
|
|
- const date = String(item['bxrq'] ?? '未指定日期').slice(0, 10);
|
|
|
+ for (const item of this.sortedVisibleSchedules()) {
|
|
|
+ const date = this.itemDate(item) || '未指定日期';
|
|
|
grouped.set(date, [...(grouped.get(date) ?? []), item]);
|
|
|
}
|
|
|
return Array.from(grouped, ([date, items]) => ({ date, items }));
|
|
|
@@ -125,6 +156,37 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
|
|
|
setScheduleFilter(filter: ScheduleFilter): void { this.scheduleFilter.set(filter); }
|
|
|
|
|
|
+ setStudentFilter(event: Event): void {
|
|
|
+ const value = event.target instanceof HTMLSelectElement ? event.target.value : 'all';
|
|
|
+ this.selectedStudent.set(value || 'all');
|
|
|
+ }
|
|
|
+
|
|
|
+ shiftCalendarMonth(amount: number): void {
|
|
|
+ const month = this.calendarMonth();
|
|
|
+ this.calendarMonth.set(new Date(month.getFullYear(), month.getMonth() + amount, 1, 12));
|
|
|
+ }
|
|
|
+
|
|
|
+ selectCalendarDay(day: CalendarDay): void { this.selectDate(day.date); }
|
|
|
+
|
|
|
+ showToday(): void { this.selectDate(this.today()); }
|
|
|
+
|
|
|
+ showTomorrow(): void { this.selectDate(this.tomorrow()); }
|
|
|
+
|
|
|
+ showWeek(): void {
|
|
|
+ this.scheduleRange.set('week');
|
|
|
+ this.ensureSelectedDate();
|
|
|
+ }
|
|
|
+
|
|
|
+ showAllSchedules(): void { this.scheduleRange.set('all'); }
|
|
|
+
|
|
|
+ scheduleEmptyText(): string {
|
|
|
+ const target = this.isCoach ? '排课' : '伴学记录';
|
|
|
+ if (!this.schedules().length) return this.isCoach ? '暂无排课' : '暂无伴学记录';
|
|
|
+ if (this.scheduleRange() === 'all') return `当前筛选下暂无${target}`;
|
|
|
+ if (this.scheduleRange() === 'week') return `本周暂无${target}`;
|
|
|
+ return `${this.selectedDate()} 暂无${target}`;
|
|
|
+ }
|
|
|
+
|
|
|
open(item: Item): void {
|
|
|
if (!this.isCoach) {
|
|
|
void this.router.navigate(['/pages/shop/stu_get_order'], { queryParams: { id: this.scheduleId(item) } });
|
|
|
@@ -162,6 +224,110 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ private buildStudentOptions(): StudentOption[] {
|
|
|
+ if (!this.isCoach || this.isHistory) return [];
|
|
|
+ const options = new Map<string, StudentOption>();
|
|
|
+ for (const item of this.schedules()) {
|
|
|
+ const id = this.studentId(item);
|
|
|
+ const name = this.studentName(item);
|
|
|
+ const current = options.get(id);
|
|
|
+ if (current) current.count += 1;
|
|
|
+ else options.set(id, { id, name, count: 1 });
|
|
|
+ }
|
|
|
+ return [...options.values()].sort((left, right) => left.name.localeCompare(right.name, 'zh-CN'));
|
|
|
+ }
|
|
|
+
|
|
|
+ private buildCalendarDays(): CalendarDay[] {
|
|
|
+ const month = this.calendarMonth();
|
|
|
+ const year = month.getFullYear();
|
|
|
+ const monthIndex = month.getMonth();
|
|
|
+ const firstDay = new Date(year, monthIndex, 1, 12);
|
|
|
+ const offset = (firstDay.getDay() + 6) % 7;
|
|
|
+ const start = new Date(year, monthIndex, 1 - offset, 12);
|
|
|
+ const today = this.today();
|
|
|
+ const counts = new Map<string, number>();
|
|
|
+ for (const item of this.filteredSchedulePool()) {
|
|
|
+ const date = this.itemDate(item);
|
|
|
+ if (!date) continue;
|
|
|
+ counts.set(date, (counts.get(date) ?? 0) + 1);
|
|
|
+ }
|
|
|
+ return Array.from({ length: 42 }, (_, index) => {
|
|
|
+ const date = new Date(start);
|
|
|
+ date.setDate(start.getDate() + index);
|
|
|
+ const key = this.dateKey(date);
|
|
|
+ const count = counts.get(key);
|
|
|
+ return {
|
|
|
+ date: key,
|
|
|
+ day: date.getDate(),
|
|
|
+ inMonth: date.getMonth() === monthIndex,
|
|
|
+ isToday: key === today,
|
|
|
+ count: count ?? 0,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private matchesStudent(item: Item): boolean {
|
|
|
+ const selected = this.selectedStudent();
|
|
|
+ return selected === 'all' || this.studentId(item) === selected;
|
|
|
+ }
|
|
|
+
|
|
|
+ private matchesStatus(item: Item): boolean {
|
|
|
+ if (this.scheduleFilter() === 'finished') return this.isFinished(item);
|
|
|
+ if (this.scheduleFilter() === 'pending') return !this.isFinished(item);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private matchesRange(item: Item): boolean {
|
|
|
+ if (!this.hasScheduleCalendar || this.scheduleRange() === 'all') return true;
|
|
|
+ const date = this.itemDate(item);
|
|
|
+ if (!date) return false;
|
|
|
+ if (this.scheduleRange() === 'week') {
|
|
|
+ const [start, end] = this.weekRange(this.selectedDate());
|
|
|
+ return date >= start && date <= end;
|
|
|
+ }
|
|
|
+ return date === this.selectedDate();
|
|
|
+ }
|
|
|
+
|
|
|
+ private sortedVisibleSchedules(): Item[] {
|
|
|
+ const list = [...this.visibleSchedules()];
|
|
|
+ if (this.isHistory) return list;
|
|
|
+ return list.sort((left, right) => `${this.itemDate(left)} ${this.scheduleTime(left)}`.localeCompare(`${this.itemDate(right)} ${this.scheduleTime(right)}`));
|
|
|
+ }
|
|
|
+
|
|
|
+ private selectDate(date: string): void {
|
|
|
+ this.selectedDate.set(date);
|
|
|
+ this.scheduleRange.set('day');
|
|
|
+ const value = this.parseDate(date);
|
|
|
+ this.calendarMonth.set(new Date(value.getFullYear(), value.getMonth(), 1, 12));
|
|
|
+ }
|
|
|
+
|
|
|
+ private ensureSelectedDate(): void {
|
|
|
+ if (!this.selectedDate()) this.selectedDate.set(this.today());
|
|
|
+ }
|
|
|
+
|
|
|
+ private itemDate(item: Item): string {
|
|
|
+ return this.dateKey(item['bxrq'] ?? item['yydate'] ?? item['yysj'] ?? item['PubAddTime']);
|
|
|
+ }
|
|
|
+
|
|
|
+ private studentId(item: Item): string {
|
|
|
+ return String(item['szyh'] ?? item['UserID'] ?? item['userId'] ?? this.studentName(item));
|
|
|
+ }
|
|
|
+
|
|
|
+ private studentName(item: Item): string {
|
|
|
+ const explicit = item['honeyname'] ?? item['HoneyName'] ?? item['honeyName'] ?? item['xymz_t'] ?? item['studentName'];
|
|
|
+ if (explicit) return String(explicit);
|
|
|
+ const title = this.scheduleTitle(item);
|
|
|
+ return title.includes('(') ? title.slice(0, title.indexOf('(')) : title || '未命名学员';
|
|
|
+ }
|
|
|
+
|
|
|
+ private weekRange(date: string): [string, string] {
|
|
|
+ const start = this.parseDate(date);
|
|
|
+ start.setDate(start.getDate() - ((start.getDay() + 6) % 7));
|
|
|
+ const end = new Date(start);
|
|
|
+ end.setDate(start.getDate() + 6);
|
|
|
+ return [this.dateKey(start), this.dateKey(end)];
|
|
|
+ }
|
|
|
+
|
|
|
private asList(value: unknown): Item[] {
|
|
|
if (Array.isArray(value)) return value.filter((item): item is Item => Boolean(item) && typeof item === 'object');
|
|
|
if (value && typeof value === 'object') {
|
|
|
@@ -170,8 +336,30 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
- private today(): string {
|
|
|
+ today(): string {
|
|
|
const now = new Date();
|
|
|
- return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
|
+ return this.dateKey(now);
|
|
|
+ }
|
|
|
+
|
|
|
+ tomorrow(): string {
|
|
|
+ const date = this.parseDate(this.today());
|
|
|
+ date.setDate(date.getDate() + 1);
|
|
|
+ return this.dateKey(date);
|
|
|
+ }
|
|
|
+
|
|
|
+ private firstOfMonth(date: Date): Date { return new Date(date.getFullYear(), date.getMonth(), 1, 12); }
|
|
|
+
|
|
|
+ private parseDate(value: string): Date {
|
|
|
+ const [year, month, day] = value.split('-').map(Number);
|
|
|
+ return new Date(year || new Date().getFullYear(), (month || 1) - 1, day || 1, 12);
|
|
|
+ }
|
|
|
+
|
|
|
+ private dateKey(value: unknown): string {
|
|
|
+ const raw = String(value ?? '');
|
|
|
+ const matched = raw.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
|
|
|
+ if (matched) return `${matched[1]}-${matched[2].padStart(2, '0')}-${matched[3].padStart(2, '0')}`;
|
|
|
+ const date = value instanceof Date ? value : new Date(raw);
|
|
|
+ if (Number.isNaN(date.getTime())) return '';
|
|
|
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
|
}
|
|
|
}
|