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": "教学反馈助手"
}

108
pages/feedback/index.ts Normal file
View File

@@ -0,0 +1,108 @@
import {
createFeedbackRecord,
getApiErrorMessage,
listProfiles
} from '../../utils/api'
import type { FeedbackDraft, StudentProfile } from '../../utils/types'
type PickerEvent = { detail: { value: string | number } }
type InputEvent = { detail: { value: string } }
function today(): string {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
function buildDraft(): FeedbackDraft {
return {
profileId: null,
feedbackDate: today(),
content: ''
}
}
Page({
data: {
profiles: [] as StudentProfile[],
profileOptions: ['不关联学生'],
selectedProfileIndex: 0,
draft: buildDraft(),
contentLength: 0,
loading: false,
saving: false,
errorMessage: ''
},
onShow() {
this.getTabBar()?.setData({ selected: 0 })
void this.loadProfiles()
},
async loadProfiles() {
this.setData({ loading: true, errorMessage: '' })
try {
const profiles = await listProfiles()
const matchedProfileIndex = profiles.findIndex((profile) => profile.id === this.data.draft.profileId)
const selectedProfileIndex = matchedProfileIndex + 1
this.setData({
profiles,
profileOptions: ['不关联学生', ...profiles.map((profile) => `${profile.name} · ${profile.subject}`)],
selectedProfileIndex,
'draft.profileId': matchedProfileIndex >= 0 ? profiles[matchedProfileIndex].id : null
})
} catch (error) {
this.setData({ errorMessage: getApiErrorMessage(error) })
} finally {
this.setData({ loading: false })
}
},
retryLoad() {
void this.loadProfiles()
},
onProfileChange(event: PickerEvent) {
const selectedProfileIndex = Number(event.detail.value)
const profile = this.data.profiles[selectedProfileIndex - 1]
this.setData({
selectedProfileIndex,
'draft.profileId': profile?.id || null
})
},
onDateChange(event: InputEvent) {
this.setData({ 'draft.feedbackDate': event.detail.value })
},
onContentInput(event: InputEvent) {
const content = event.detail.value
this.setData({ 'draft.content': content, contentLength: content.length })
},
async saveFeedback() {
const content = this.data.draft.content.trim()
if (!content) {
wx.showToast({ title: '请填写反馈内容', icon: 'none' })
return
}
this.setData({ saving: true })
try {
await createFeedbackRecord({ ...this.data.draft, content })
this.setData({ draft: buildDraft(), selectedProfileIndex: 0, contentLength: 0 })
wx.showToast({ title: '反馈已保存', icon: 'success' })
setTimeout(() => wx.switchTab({ url: '/pages/records/index' }), 450)
} catch (error) {
wx.showToast({ title: getApiErrorMessage(error), icon: 'none' })
} finally {
this.setData({ saving: false })
}
},
goStudents() {
wx.switchTab({ url: '/pages/students/index' })
}
})

46
pages/feedback/index.wxml Normal file
View File

@@ -0,0 +1,46 @@
<view class="page-shell feedback-page">
<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 wx:if="{{errorMessage}}" class="status-banner">
<text>{{errorMessage}}</text>
<text class="status-action" bindtap="retryLoad">重试</text>
</view>
<view class="feedback-form surface">
<picker class="feedback-field" mode="selector" range="{{profileOptions}}" value="{{selectedProfileIndex}}" disabled="{{loading}}" bindchange="onProfileChange">
<text class="form-label">学生档案</text>
<text class="form-value">{{loading ? '正在加载...' : profileOptions[selectedProfileIndex]}}</text>
</picker>
<picker class="feedback-field" mode="date" value="{{draft.feedbackDate}}" bindchange="onDateChange">
<text class="form-label">反馈日期</text>
<text class="form-value">{{draft.feedbackDate}}</text>
</picker>
<view class="content-field">
<view class="content-label-row">
<text class="form-label">反馈内容</text>
<text class="content-count">{{contentLength}} / 2000</text>
</view>
<textarea
class="feedback-textarea"
value="{{draft.content}}"
maxlength="2000"
placeholder="记录课堂完成情况、进步与后续建议"
bindinput="onContentInput"
/>
</view>
<button class="save-feedback primary-button" loading="{{saving}}" disabled="{{saving}}" bindtap="saveFeedback">保存反馈</button>
</view>
<view wx:if="{{!loading && profiles.length === 0}}" class="profiles-hint">
<text>当前没有学生档案,反馈仍可不关联学生直接保存。</text>
<text class="status-action" bindtap="goStudents">新建档案</text>
</view>
</view>

57
pages/feedback/index.wxss Normal file
View File

@@ -0,0 +1,57 @@
.feedback-page {
padding-top: 44rpx;
}
.feedback-form {
margin-top: 32rpx;
padding: 24rpx;
}
.feedback-field,
.content-field {
padding: 22rpx;
border: 2rpx solid #d8e5f2;
border-radius: 14rpx;
background: #fbfdff;
}
.feedback-field + .feedback-field,
.content-field {
margin-top: 16rpx;
}
.content-label-row,
.profiles-hint {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.content-count {
color: #8a97a6;
font-size: 23rpx;
}
.feedback-textarea {
width: 100%;
min-height: 300rpx;
margin-top: 18rpx;
color: #17233a;
font-size: 29rpx;
line-height: 1.6;
}
.save-feedback {
margin-top: 24rpx;
}
.profiles-hint {
margin-top: 22rpx;
padding: 20rpx 22rpx;
border: 2rpx dashed #c7ddeb;
border-radius: 12rpx;
color: #65758a;
font-size: 24rpx;
line-height: 1.45;
}