import { createProfile, deleteProfile as deleteProfileRequest, getApiErrorMessage, getProfileDefaults, listFeedbackRecords, listProfiles } from '../../utils/api' import { addMinutes, formatUpdatedAt } from '../../utils/time' import type { FeedbackRecord, StudentDraft, StudentProfile, StudentProfileDefaults, Term } from '../../utils/types' const grades = ['初一', '初二', '初三'] const subjects = ['美术', '书法', '音乐', '舞蹈', '语文', '数学', '英语'] const terms: Term[] = ['春', '暑', '秋', '寒'] const years = Array.from({ length: 5 }, (_, index) => new Date().getFullYear() - 1 + index) const initialDefaults: StudentProfileDefaults = { grade: '初一', subject: '数学', lessonDurationMinutes: 60 } const termLabels: Record = { 春: '春季', 暑: '暑期', 秋: '秋季', 寒: '寒假' } type InputEvent = { detail: { value: string } } type PickerEvent = { detail: { value: string | number } } type StudentProfileView = StudentProfile & { academicTermLabel: string } function withAcademicTermLabel(profile: StudentProfile): StudentProfileView { return { ...profile, academicTermLabel: `${profile.academicYear} 年${termLabels[profile.term]}` } } function getCurrentTerm(): Term { const month = new Date().getMonth() + 1 if (month >= 6 && month <= 8) return '暑' if (month >= 9 && month <= 12) return '秋' if (month <= 2) return '寒' return '春' } function buildDraft(defaults: StudentProfileDefaults = initialDefaults): StudentDraft { const startTime = '18:00' return { name: '', grade: defaults.grade, subject: defaults.subject, academicYear: new Date().getFullYear(), term: getCurrentTerm(), guardianTitle: '', startTime, endTime: addMinutes(startTime, defaults.lessonDurationMinutes) } } function filterProfiles( profiles: StudentProfileView[], query: string, filterGrade: string, filterSubject: string ): StudentProfileView[] { const keyword = query.trim().toLocaleLowerCase() return profiles.filter((profile) => { const searchable = [profile.name, profile.grade, profile.subject, profile.guardianTitle] .join(' ') .toLocaleLowerCase() const matchesKeyword = !keyword || searchable.includes(keyword) const matchesGrade = !filterGrade || profile.grade === filterGrade const matchesSubject = !filterSubject || profile.subject === filterSubject return matchesKeyword && matchesGrade && matchesSubject }) } function getLastUpdatedText(profiles: StudentProfile[]): string { if (!profiles.length) return '--' const latest = Math.max(...profiles.map((profile) => profile.updatedAt)) return formatUpdatedAt(latest) } Page({ data: { profiles: [] as StudentProfileView[], filteredProfiles: [] as StudentProfileView[], query: '', filterVisible: false, filterGrade: '', filterSubject: '', filterCount: 0, expandedProfileId: '', recentFeedbackByProfile: {} as Record, feedbackLoadingByProfile: {} as Record, feedbackErrorsByProfile: {} as Record, grades, subjects, terms, years, sheetVisible: false, draft: buildDraft(), guardianAuto: true, refreshDraftFromDefaults: false, lastUpdatedText: '--', loading: false, saving: false, deletingId: '', errorMessage: '' }, onShow() { this.getTabBar()?.setData({ selected: 1, visible: !this.data.sheetVisible }) void this.refreshProfiles() if (this.data.sheetVisible && this.data.refreshDraftFromDefaults) { void this.refreshDraftDefaults() } }, async refreshProfiles(showSuccess = false) { this.setData({ loading: true, errorMessage: '' }) try { const profiles = (await listProfiles()).map(withAcademicTermLabel) const filteredProfiles = filterProfiles( profiles, this.data.query, this.data.filterGrade, this.data.filterSubject ) this.setData({ profiles, filteredProfiles, lastUpdatedText: getLastUpdatedText(profiles), expandedProfileId: '', recentFeedbackByProfile: {}, feedbackLoadingByProfile: {}, feedbackErrorsByProfile: {} }) if (showSuccess) wx.showToast({ title: '档案已刷新', icon: 'success' }) } catch (error) { this.setData({ errorMessage: getApiErrorMessage(error) }) } finally { this.setData({ loading: false }) } }, onRefresh() { void this.refreshProfiles(true) }, retryLoad() { void this.refreshProfiles() }, onSearch(event: InputEvent) { const query = event.detail.value this.setData({ query, filteredProfiles: filterProfiles( this.data.profiles, query, this.data.filterGrade, this.data.filterSubject ) }) }, toggleFilter() { this.setData({ filterVisible: !this.data.filterVisible }) }, selectFilter(event: WechatMiniprogram.TouchEvent) { const { type, value } = event.currentTarget.dataset as { type: 'grade' | 'subject'; value: string } const filterGrade = type === 'grade' ? (value === 'all' ? '' : value) : this.data.filterGrade const filterSubject = type === 'subject' ? (value === 'all' ? '' : value) : this.data.filterSubject this.setData({ filterGrade, filterSubject, filterCount: Number(Boolean(filterGrade)) + Number(Boolean(filterSubject)), filteredProfiles: filterProfiles(this.data.profiles, this.data.query, filterGrade, filterSubject) }) }, clearFilters() { this.setData({ query: '', filterGrade: '', filterSubject: '', filterCount: 0, filteredProfiles: this.data.profiles }) }, toggleProfile(event: WechatMiniprogram.TouchEvent) { const { id } = event.currentTarget.dataset as { id: string } if (this.data.expandedProfileId === id) { this.setData({ expandedProfileId: '' }) return } const hasFeedback = Object.prototype.hasOwnProperty.call(this.data.recentFeedbackByProfile, id) this.setData({ expandedProfileId: id, ...(hasFeedback ? {} : { feedbackLoadingByProfile: { ...this.data.feedbackLoadingByProfile, [id]: true }, feedbackErrorsByProfile: { ...this.data.feedbackErrorsByProfile, [id]: '' } }) }) if (!hasFeedback) void this.loadRecentFeedback(id) }, async loadRecentFeedback(profileId: string) { try { const records = await listFeedbackRecords(profileId) this.setData({ recentFeedbackByProfile: { ...this.data.recentFeedbackByProfile, [profileId]: records.slice(0, 3) } }) } catch (error) { this.setData({ feedbackErrorsByProfile: { ...this.data.feedbackErrorsByProfile, [profileId]: getApiErrorMessage(error) } }) } finally { this.setData({ feedbackLoadingByProfile: { ...this.data.feedbackLoadingByProfile, [profileId]: false } }) } }, retryRecentFeedback(event: WechatMiniprogram.TouchEvent) { const { id } = event.currentTarget.dataset as { id: string } if (this.data.feedbackLoadingByProfile[id]) return this.setData({ feedbackLoadingByProfile: { ...this.data.feedbackLoadingByProfile, [id]: true }, feedbackErrorsByProfile: { ...this.data.feedbackErrorsByProfile, [id]: '' } }) void this.loadRecentFeedback(id) }, async openCreate() { this.setData({ sheetVisible: true, guardianAuto: true, draft: buildDraft() }) this.getTabBar()?.setData({ visible: false }) await this.refreshDraftDefaults() }, async refreshDraftDefaults() { try { const defaults = await getProfileDefaults() if (!this.data.sheetVisible) return this.setData({ 'draft.grade': defaults.grade, 'draft.subject': defaults.subject, 'draft.endTime': addMinutes(this.data.draft.startTime, defaults.lessonDurationMinutes), refreshDraftFromDefaults: false }) } catch (error) { this.setData({ refreshDraftFromDefaults: false }) wx.showToast({ title: getApiErrorMessage(error), icon: 'none' }) } }, closeCreate() { this.setData({ sheetVisible: false }) this.getTabBar()?.setData({ visible: true }) }, noop() {}, goDefaults() { this.setData({ refreshDraftFromDefaults: true }) wx.navigateTo({ url: '/pages/defaults/index' }) }, onNameInput(event: InputEvent) { const name = event.detail.value const nextData: Record = { 'draft.name': name } if (this.data.guardianAuto || !this.data.draft.guardianTitle) { nextData['draft.guardianTitle'] = name ? `${name}妈妈` : '' nextData.guardianAuto = true } this.setData(nextData) }, onGuardianInput(event: InputEvent) { this.setData({ 'draft.guardianTitle': event.detail.value, guardianAuto: false }) }, onGradeChange(event: PickerEvent) { this.setData({ 'draft.grade': grades[Number(event.detail.value)] }) }, onSubjectChange(event: PickerEvent) { this.setData({ 'draft.subject': subjects[Number(event.detail.value)] }) }, onYearChange(event: PickerEvent) { this.setData({ 'draft.academicYear': years[Number(event.detail.value)] }) }, onTermChange(event: PickerEvent) { this.setData({ 'draft.term': terms[Number(event.detail.value)] }) }, onStartTimeChange(event: InputEvent) { const startTime = event.detail.value this.setData({ 'draft.startTime': startTime }) void getProfileDefaults() .then((defaults) => { this.setData({ 'draft.endTime': addMinutes(startTime, defaults.lessonDurationMinutes) }) }) .catch(() => { this.setData({ 'draft.endTime': addMinutes(startTime, initialDefaults.lessonDurationMinutes) }) }) }, onEndTimeChange(event: InputEvent) { this.setData({ 'draft.endTime': event.detail.value }) }, async saveStudent() { const draft = this.data.draft if (!draft.name.trim()) { wx.showToast({ title: '请填写学生姓名', icon: 'none' }) return } this.setData({ saving: true }) try { const profile = withAcademicTermLabel( await createProfile({ ...draft, name: draft.name.trim(), guardianTitle: draft.guardianTitle.trim() || `${draft.name.trim()}妈妈` }) ) const profiles = [profile, ...this.data.profiles] this.setData({ sheetVisible: false, profiles, filteredProfiles: filterProfiles(profiles, this.data.query, this.data.filterGrade, this.data.filterSubject), lastUpdatedText: formatUpdatedAt(profile.updatedAt), errorMessage: '' }) this.getTabBar()?.setData({ visible: true }) wx.showToast({ title: '学生档案已保存', icon: 'success' }) } catch (error) { wx.showToast({ title: getApiErrorMessage(error), icon: 'none' }) } finally { this.setData({ saving: false }) } }, openProfileActions(event: WechatMiniprogram.TouchEvent) { const { id } = event.currentTarget.dataset as { id: string } const profile = this.data.profiles.find((item) => item.id === id) if (!profile) return wx.showActionSheet({ itemList: ['删除学生档案'], itemColor: '#D13F3A', success: (result) => { if (result.tapIndex === 0) this.confirmDeleteProfile(id) } }) }, confirmDeleteProfile(id: string) { const profile = this.data.profiles.find((item) => item.id === id) if (!profile) return wx.showModal({ title: '删除学生档案', content: `确认删除“${profile.name}”吗?`, confirmColor: '#D13F3A', success: async (result) => { if (!result.confirm) return this.setData({ deletingId: id }) try { await deleteProfileRequest(id) const profiles = this.data.profiles.filter((item) => item.id !== id) const recentFeedbackByProfile = { ...this.data.recentFeedbackByProfile } const feedbackLoadingByProfile = { ...this.data.feedbackLoadingByProfile } const feedbackErrorsByProfile = { ...this.data.feedbackErrorsByProfile } delete recentFeedbackByProfile[id] delete feedbackLoadingByProfile[id] delete feedbackErrorsByProfile[id] this.setData({ profiles, filteredProfiles: filterProfiles(profiles, this.data.query, this.data.filterGrade, this.data.filterSubject), lastUpdatedText: getLastUpdatedText(profiles), expandedProfileId: this.data.expandedProfileId === id ? '' : this.data.expandedProfileId, recentFeedbackByProfile, feedbackLoadingByProfile, feedbackErrorsByProfile, errorMessage: '' }) wx.showToast({ title: '已删除', icon: 'success' }) } catch (error) { wx.showToast({ title: getApiErrorMessage(error), icon: 'none' }) } finally { this.setData({ deletingId: '' }) } } }) } })