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.
315 lines
9.2 KiB
TypeScript
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: '' })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|