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/records/index.json
Normal file
3
pages/records/index.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "教学反馈助手"
|
||||
}
|
||||
97
pages/records/index.ts
Normal file
97
pages/records/index.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
deleteFeedbackRecord,
|
||||
getApiErrorMessage,
|
||||
listFeedbackRecords,
|
||||
listProfiles
|
||||
} from '../../utils/api'
|
||||
import type { FeedbackRecord, StudentProfile } from '../../utils/types'
|
||||
|
||||
type PickerEvent = { detail: { value: string | number } }
|
||||
type FeedbackRecordView = FeedbackRecord & { profileName: string }
|
||||
|
||||
Page({
|
||||
data: {
|
||||
profiles: [] as StudentProfile[],
|
||||
records: [] as FeedbackRecordView[],
|
||||
profileOptions: ['全部学生'],
|
||||
selectedProfileIndex: 0,
|
||||
loading: false,
|
||||
deletingId: '',
|
||||
errorMessage: ''
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.getTabBar()?.setData({ selected: 2 })
|
||||
void this.loadData()
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
this.setData({ loading: true, errorMessage: '' })
|
||||
try {
|
||||
const profiles = await listProfiles()
|
||||
const selectedProfile = profiles[this.data.selectedProfileIndex - 1]
|
||||
const records = await listFeedbackRecords(selectedProfile?.id)
|
||||
this.setData({
|
||||
profiles,
|
||||
profileOptions: ['全部学生', ...profiles.map((profile) => profile.name)],
|
||||
selectedProfileIndex: selectedProfile ? this.data.selectedProfileIndex : 0,
|
||||
records: this.withProfileNames(records, profiles)
|
||||
})
|
||||
} catch (error) {
|
||||
this.setData({ errorMessage: getApiErrorMessage(error) })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
withProfileNames(records: FeedbackRecord[], profiles: StudentProfile[]): FeedbackRecordView[] {
|
||||
return records.map((record) => ({
|
||||
...record,
|
||||
profileName: profiles.find((profile) => profile.id === record.profileId)?.name || '未关联学生'
|
||||
}))
|
||||
},
|
||||
|
||||
retryLoad() {
|
||||
void this.loadData()
|
||||
},
|
||||
|
||||
async onProfileChange(event: PickerEvent) {
|
||||
const selectedProfileIndex = Number(event.detail.value)
|
||||
const profile = this.data.profiles[selectedProfileIndex - 1]
|
||||
this.setData({ selectedProfileIndex, loading: true, errorMessage: '' })
|
||||
try {
|
||||
const records = await listFeedbackRecords(profile?.id)
|
||||
this.setData({ records: this.withProfileNames(records, this.data.profiles) })
|
||||
} catch (error) {
|
||||
this.setData({ errorMessage: getApiErrorMessage(error) })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
deleteRecord(event: WechatMiniprogram.TouchEvent) {
|
||||
const { id } = event.currentTarget.dataset as { id: string }
|
||||
wx.showModal({
|
||||
title: '删除反馈记录',
|
||||
content: '删除后无法恢复,确认继续吗?',
|
||||
confirmColor: '#D14343',
|
||||
success: async (result) => {
|
||||
if (!result.confirm) return
|
||||
this.setData({ deletingId: id })
|
||||
try {
|
||||
await deleteFeedbackRecord(id)
|
||||
this.setData({ records: this.data.records.filter((record) => record.id !== id) })
|
||||
wx.showToast({ title: '记录已删除', icon: 'success' })
|
||||
} catch (error) {
|
||||
wx.showToast({ title: getApiErrorMessage(error), icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ deletingId: '' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goFeedback() {
|
||||
wx.switchTab({ url: '/pages/feedback/index' })
|
||||
}
|
||||
})
|
||||
40
pages/records/index.wxml
Normal file
40
pages/records/index.wxml
Normal file
@@ -0,0 +1,40 @@
|
||||
<view class="page-shell records-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>
|
||||
|
||||
<picker class="records-filter surface" mode="selector" range="{{profileOptions}}" value="{{selectedProfileIndex}}" disabled="{{loading}}" bindchange="onProfileChange">
|
||||
<text class="filter-label">筛选学生</text>
|
||||
<text class="filter-value">{{profileOptions[selectedProfileIndex]}}</text>
|
||||
</picker>
|
||||
|
||||
<view wx:if="{{loading && records.length === 0}}" class="loading-state">正在读取反馈记录...</view>
|
||||
|
||||
<view wx:elif="{{records.length === 0}}" class="records-empty surface">
|
||||
<text class="empty-icon">◴</text>
|
||||
<text class="empty-title">暂无反馈记录</text>
|
||||
<text class="helper-text">保存课堂反馈后,记录会显示在这里。</text>
|
||||
<button class="primary-button empty-action" bindtap="goFeedback">填写反馈</button>
|
||||
</view>
|
||||
|
||||
<view wx:else class="record-list">
|
||||
<view wx:for="{{records}}" wx:key="id" class="record-card surface">
|
||||
<view class="record-top">
|
||||
<view>
|
||||
<text class="record-profile">{{item.profileName}}</text>
|
||||
<text class="record-date">{{item.feedbackDate}}</text>
|
||||
</view>
|
||||
<button class="delete-button" loading="{{deletingId === item.id}}" disabled="{{deletingId === item.id}}" data-id="{{item.id}}" bindtap="deleteRecord">删除</button>
|
||||
</view>
|
||||
<text class="record-content">{{item.content}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
108
pages/records/index.wxss
Normal file
108
pages/records/index.wxss
Normal file
@@ -0,0 +1,108 @@
|
||||
.records-page {
|
||||
padding-top: 44rpx;
|
||||
}
|
||||
|
||||
.records-filter {
|
||||
margin-top: 30rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
}
|
||||
|
||||
.filter-label,
|
||||
.filter-value {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
color: #718096;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.filter-value {
|
||||
margin-top: 8rpx;
|
||||
color: #17233a;
|
||||
font-size: 29rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.records-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 24rpx;
|
||||
padding: 64rpx 36rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
color: #2b67e8;
|
||||
font-size: 56rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
margin: 20rpx 0 12rpx;
|
||||
color: #17233a;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
width: 280rpx;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.record-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.record-profile,
|
||||
.record-date,
|
||||
.record-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.record-profile {
|
||||
color: #17233a;
|
||||
font-size: 31rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.record-date {
|
||||
margin-top: 6rpx;
|
||||
color: #718096;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.record-content {
|
||||
margin-top: 20rpx;
|
||||
padding-top: 18rpx;
|
||||
border-top: 1rpx solid #e8eef5;
|
||||
color: #334258;
|
||||
font-size: 27rpx;
|
||||
line-height: 1.65;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
margin: 0;
|
||||
padding: 8rpx 14rpx;
|
||||
border: 1rpx solid #f2c6c6;
|
||||
border-radius: 10rpx;
|
||||
background: #ffffff;
|
||||
color: #c34343;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
Reference in New Issue
Block a user