Files
teaching-feedback-assistant/pages/students/index.ts
shay7sev 0a07538dce feat: establish teaching feedback mini program
Connect student profiles, defaults, and feedback records to the Rust API. Configure the WeChat AppID, custom tab bar, and local development workflow for version 0.1.0.
2026-07-15 15:36:51 +08:00

315 lines
9.2 KiB
TypeScript

import {
createProfile,
deleteProfile as deleteProfileRequest,
getApiErrorMessage,
getProfileDefaults,
listProfiles
} from '../../utils/api'
import { addMinutes, formatUpdatedAt } from '../../utils/time'
import type { 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
}
type InputEvent = { detail: { value: string } }
type PickerEvent = { detail: { value: string | number } }
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: StudentProfile[],
query: string,
filterGrade: string,
filterSubject: string
): StudentProfile[] {
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 StudentProfile[],
filteredProfiles: [] as StudentProfile[],
query: '',
filterVisible: false,
filterGrade: '',
filterSubject: '',
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 })
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()
const filteredProfiles = filterProfiles(
profiles,
this.data.query,
this.data.filterGrade,
this.data.filterSubject
)
this.setData({
profiles,
filteredProfiles,
lastUpdatedText: getLastUpdatedText(profiles)
})
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,
filteredProfiles: filterProfiles(this.data.profiles, this.data.query, filterGrade, filterSubject)
})
},
clearFilters() {
this.setData({
query: '',
filterGrade: '',
filterSubject: '',
filteredProfiles: this.data.profiles
})
},
async openCreate() {
this.setData({
sheetVisible: true,
guardianAuto: true,
draft: buildDraft()
})
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 })
},
noop() {},
goDefaults() {
this.setData({ refreshDraftFromDefaults: true })
wx.navigateTo({ url: '/pages/defaults/index' })
},
onNameInput(event: InputEvent) {
const name = event.detail.value
const nextData: Record<string, unknown> = { '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 = 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: ''
})
wx.showToast({ title: '学生档案已保存', icon: 'success' })
} catch (error) {
wx.showToast({ title: getApiErrorMessage(error), icon: 'none' })
} finally {
this.setData({ saving: false })
}
},
deleteProfile(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.showModal({
title: '删除学生档案',
content: `确认删除“${profile.name}”吗?`,
confirmColor: '#D14343',
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)
this.setData({
profiles,
filteredProfiles: filterProfiles(profiles, this.data.query, this.data.filterGrade, this.data.filterSubject),
lastUpdatedText: getLastUpdatedText(profiles),
errorMessage: ''
})
wx.showToast({ title: '已删除', icon: 'success' })
} catch (error) {
wx.showToast({ title: getApiErrorMessage(error), icon: 'none' })
} finally {
this.setData({ deletingId: '' })
}
}
})
}
})