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.
This commit is contained in:
2026-07-15 15:36:51 +08:00
commit 0a07538dce
37 changed files with 2550 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "学生档案预设项"
}

83
pages/defaults/index.ts Normal file
View File

@@ -0,0 +1,83 @@
import { getApiErrorMessage, getProfileDefaults, updateProfileDefaults } from '../../utils/api'
import type { StudentProfileDefaults } from '../../utils/types'
const grades = ['艺术类', '幼儿园', '一年级', '二年级', '三年级', '四年级', '五年级', '六年级']
const subjects = ['美术', '书法', '音乐', '舞蹈', '语文', '数学', '英语']
const initialDefaults: StudentProfileDefaults = {
grade: '艺术类',
subject: '美术',
lessonDurationMinutes: 60
}
type PickerEvent = { detail: { value: string | number } }
type InputEvent = { detail: { value: string } }
function normalizeDuration(value: number): number {
if (Number.isNaN(value)) return 60
return Math.min(360, Math.max(15, Math.round(value / 15) * 15))
}
Page({
data: {
defaults: initialDefaults,
grades,
subjects,
loading: false,
saving: false,
errorMessage: ''
},
onShow() {
void this.loadDefaults()
},
async loadDefaults(showSuccess = false) {
this.setData({ loading: true, errorMessage: '' })
try {
this.setData({ defaults: await getProfileDefaults() })
if (showSuccess) wx.showToast({ title: '预设已刷新', icon: 'success' })
} catch (error) {
this.setData({ errorMessage: getApiErrorMessage(error) })
} finally {
this.setData({ loading: false })
}
},
onReload() {
void this.loadDefaults(true)
},
onGradeChange(event: PickerEvent) {
this.setData({ 'defaults.grade': grades[Number(event.detail.value)] })
},
onSubjectChange(event: PickerEvent) {
this.setData({ 'defaults.subject': subjects[Number(event.detail.value)] })
},
onDurationInput(event: InputEvent) {
this.setData({ 'defaults.lessonDurationMinutes': normalizeDuration(Number(event.detail.value)) })
},
onDurationChange(event: PickerEvent) {
this.setData({ 'defaults.lessonDurationMinutes': normalizeDuration(Number(event.detail.value)) })
},
async save() {
const defaults = {
...this.data.defaults,
lessonDurationMinutes: normalizeDuration(this.data.defaults.lessonDurationMinutes)
}
this.setData({ saving: true })
try {
const saved = await updateProfileDefaults(defaults)
this.setData({ defaults: saved, errorMessage: '' })
wx.showToast({ title: '预设项已保存', icon: 'success' })
setTimeout(() => wx.navigateBack(), 450)
} catch (error) {
wx.showToast({ title: getApiErrorMessage(error), icon: 'none' })
} finally {
this.setData({ saving: false })
}
}
})

58
pages/defaults/index.wxml Normal file
View File

@@ -0,0 +1,58 @@
<view class="page-shell defaults-page">
<view class="top-row">
<view>
<text class="eyebrow">档案预设</text>
<view class="page-heading">
<text class="heading-icon"></text>
<text class="page-title">学生档案预设项</text>
</view>
<text class="page-subtitle">用于新增学生时自动带出</text>
</view>
<button class="reload-button secondary-button" loading="{{loading}}" disabled="{{loading}}" bindtap="onReload">↻ 刷新</button>
</view>
<view wx:if="{{errorMessage}}" class="status-banner">
<text>{{errorMessage}}</text>
<text class="status-action" bindtap="onReload">重试</text>
</view>
<view class="defaults-card surface">
<view class="defaults-grid">
<picker class="select-field" mode="selector" range="{{grades}}" bindchange="onGradeChange">
<text class="field-label">年级</text>
<text class="field-value">{{defaults.grade}}</text>
</picker>
<picker class="select-field" mode="selector" range="{{subjects}}" bindchange="onSubjectChange">
<text class="field-label">学科</text>
<text class="field-value">{{defaults.subject}}</text>
</picker>
</view>
<view class="duration-field">
<text class="field-label">上课时长</text>
<view class="duration-input-row">
<input class="duration-input" type="number" value="{{defaults.lessonDurationMinutes}}" bindblur="onDurationInput" />
<text class="duration-unit">分钟</text>
</view>
<slider
class="duration-slider"
min="15"
max="360"
step="15"
value="{{defaults.lessonDurationMinutes}}"
activeColor="#2B67E8"
backgroundColor="#DBE7F5"
blockColor="#2B67E8"
blockSize="26"
bindchange="onDurationChange"
/>
<view class="duration-bounds">
<text>15 分钟</text>
<text>360 分钟</text>
</view>
</view>
<view class="time-help">新增学生时会按开始时间自动推算结束时间。</view>
<button class="save-defaults primary-button" loading="{{saving}}" disabled="{{saving || loading}}" bindtap="save">保存</button>
</view>
</view>

106
pages/defaults/index.wxss Normal file
View File

@@ -0,0 +1,106 @@
.defaults-page {
padding-top: 38rpx;
}
.top-row {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18rpx;
}
.top-row .page-heading {
max-width: 480rpx;
}
.top-row .page-title {
font-size: 46rpx;
}
.reload-button {
flex: none;
min-width: 148rpx;
min-height: 70rpx;
font-size: 26rpx;
}
.defaults-card {
margin-top: 34rpx;
padding: 24rpx;
}
.defaults-grid {
display: flex;
gap: 16rpx;
}
.defaults-grid .select-field {
width: calc((100% - 16rpx) / 2);
}
.select-field,
.duration-field {
padding: 24rpx;
border: 2rpx solid #d8e5f2;
border-radius: 14rpx;
background: #fbfdff;
}
.select-field {
min-height: 158rpx;
}
.duration-field {
margin-top: 16rpx;
}
.duration-input-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-top: 20rpx;
}
.duration-input {
width: 150rpx;
height: 64rpx;
padding: 0 16rpx;
border: 2rpx solid #bed4e8;
border-radius: 12rpx;
background: #ffffff;
color: #17233a;
font-size: 38rpx;
font-weight: 800;
text-align: center;
}
.duration-unit {
color: #65758a;
font-size: 28rpx;
}
.duration-slider {
margin: 12rpx -12rpx 0;
}
.duration-bounds {
display: flex;
justify-content: space-between;
color: #8a97a6;
font-size: 23rpx;
}
.time-help {
margin-top: 18rpx;
padding: 20rpx;
border: 2rpx dashed #c6ddea;
border-radius: 12rpx;
color: #65758a;
font-size: 25rpx;
line-height: 1.5;
}
.save-defaults {
width: 420rpx;
margin: 26rpx auto 4rpx;
}