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:
3
pages/defaults/index.json
Normal file
3
pages/defaults/index.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "学生档案预设项"
|
||||
}
|
||||
83
pages/defaults/index.ts
Normal file
83
pages/defaults/index.ts
Normal 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
58
pages/defaults/index.wxml
Normal 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
106
pages/defaults/index.wxss
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user