feat(profile): refine student creation workflow

This commit is contained in:
2026-07-21 16:59:06 +08:00
parent 26e4ec3271
commit 12a000845a
10 changed files with 144 additions and 87 deletions

3
.gitignore vendored
View File

@@ -20,3 +20,6 @@ project.private.config.json
# Logs and OS metadata
*.log
.DS_Store
# Local UI screenshots
screenshots/

View File

@@ -8,6 +8,7 @@ const tabItems = [
Component({
data: {
selected: 0,
visible: true,
tabItems
},
methods: {

View File

@@ -1,4 +1,4 @@
<view class="tabbar" role="tablist">
<view wx:if="{{visible}}" class="tabbar" role="tablist">
<view
wx:for="{{tabItems}}"
wx:key="pagePath"

View File

@@ -1,11 +1,11 @@
import { getApiErrorMessage, getProfileDefaults, updateProfileDefaults } from '../../utils/api'
import type { StudentProfileDefaults } from '../../utils/types'
const grades = ['艺术类', '幼儿园', '一年级', '二年级', '三年级', '四年级', '五年级', '六年级']
const grades = ['初一', '二', '三']
const subjects = ['美术', '书法', '音乐', '舞蹈', '语文', '数学', '英语']
const initialDefaults: StudentProfileDefaults = {
grade: '艺术类',
subject: '美术',
grade: '初一',
subject: '数学',
lessonDurationMinutes: 60
}

View File

@@ -4,23 +4,36 @@
.top-row {
display: flex;
align-items: flex-end;
align-items: center;
justify-content: space-between;
gap: 18rpx;
}
.top-row > view {
flex: 1;
min-width: 0;
}
.top-row .page-heading {
max-width: 480rpx;
max-width: none;
flex-wrap: nowrap;
white-space: nowrap;
}
.top-row .page-title {
font-size: 46rpx;
flex: none;
font-size: 44rpx;
white-space: nowrap;
}
.reload-button {
flex: none;
width: 148rpx;
min-width: 148rpx;
max-width: 148rpx;
min-height: 70rpx;
margin: 0;
padding: 0;
font-size: 26rpx;
}

View File

@@ -44,8 +44,9 @@
.voice-input-row {
display: flex;
align-items: center;
gap: 20rpx;
flex-direction: column;
align-items: stretch;
gap: 14rpx;
margin-top: 18rpx;
padding-top: 18rpx;
border-top: 2rpx solid #e7eef6;
@@ -56,8 +57,8 @@
align-items: center;
justify-content: center;
gap: 12rpx;
width: 220rpx;
height: 72rpx;
width: 100%;
height: 80rpx;
margin: 0;
padding: 0 20rpx;
border: 2rpx solid #b8cbe1;
@@ -93,7 +94,8 @@
.voice-state {
display: flex;
flex: 1;
width: 100%;
min-height: 40rpx;
align-items: center;
justify-content: space-between;
min-width: 0;
@@ -103,9 +105,10 @@
.voice-status {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: visible;
line-height: 1.45;
white-space: normal;
word-break: break-all;
}
.voice-state-actionable {

View File

@@ -8,13 +8,13 @@ import {
import { addMinutes, formatUpdatedAt } from '../../utils/time'
import type { StudentDraft, StudentProfile, StudentProfileDefaults, Term } from '../../utils/types'
const grades = ['艺术类', '幼儿园', '一年级', '二年级', '三年级', '四年级', '五年级', '六年级']
const grades = ['初一', '二', '三']
const subjects = ['美术', '书法', '音乐', '舞蹈', '语文', '数学', '英语']
const terms: Term[] = ['春', '暑', '秋', '寒']
const years = Array.from({ length: 5 }, (_, index) => new Date().getFullYear() - 1 + index)
const initialDefaults: StudentProfileDefaults = {
grade: '艺术类',
subject: '美术',
grade: '初一',
subject: '数学',
lessonDurationMinutes: 60
}
@@ -91,7 +91,7 @@ Page({
},
onShow() {
this.getTabBar()?.setData({ selected: 1 })
this.getTabBar()?.setData({ selected: 1, visible: !this.data.sheetVisible })
void this.refreshProfiles()
if (this.data.sheetVisible && this.data.refreshDraftFromDefaults) {
void this.refreshDraftDefaults()
@@ -172,6 +172,7 @@ Page({
guardianAuto: true,
draft: buildDraft()
})
this.getTabBar()?.setData({ visible: false })
await this.refreshDraftDefaults()
},
@@ -193,6 +194,7 @@ Page({
closeCreate() {
this.setData({ sheetVisible: false })
this.getTabBar()?.setData({ visible: true })
},
noop() {},
@@ -273,6 +275,7 @@ Page({
lastUpdatedText: formatUpdatedAt(profile.updatedAt),
errorMessage: ''
})
this.getTabBar()?.setData({ visible: true })
wx.showToast({ title: '学生档案已保存', icon: 'success' })
} catch (error) {
wx.showToast({ title: getApiErrorMessage(error), icon: 'none' })

View File

@@ -112,6 +112,7 @@
</view>
<scroll-view class="sheet-scroll" scroll-y="true">
<view class="sheet-form">
<view class="form-field full-field">
<text class="form-label">学生姓名</text>
<input class="form-input" placeholder="请输入学生姓名" value="{{draft.name}}" bindinput="onNameInput" maxlength="20" />
@@ -126,6 +127,9 @@
<text class="form-label">学科</text>
<text class="form-value">{{draft.subject}}</text>
</picker>
</view>
<view class="form-grid">
<picker class="form-field" mode="selector" range="{{years}}" bindchange="onYearChange">
<text class="form-label">日期年份</text>
<text class="form-value">{{draft.academicYear}}</text>
@@ -151,6 +155,7 @@
<text class="form-value">{{draft.endTime}}</text>
</picker>
</view>
</view>
</scroll-view>
<view class="sheet-footer">

View File

@@ -331,14 +331,17 @@
left: 0;
display: flex;
align-items: flex-end;
padding: 28rpx;
padding: 16rpx;
background: rgba(17, 27, 50, 0.42);
}
.create-sheet {
width: 100%;
max-height: 88vh;
height: 90vh;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
border-radius: 16rpx;
background: #ffffff;
box-shadow: 0 24rpx 56rpx rgba(17, 27, 50, 0.22);
@@ -346,10 +349,16 @@
.sheet-top {
display: flex;
align-items: flex-start;
flex: none;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 30rpx 28rpx 20rpx;
padding: 24rpx 24rpx 16rpx;
}
.sheet-top > view {
flex: 1;
min-width: 0;
}
.sheet-title-row {
@@ -365,7 +374,7 @@
.sheet-title {
color: #17233a;
font-size: 38rpx;
font-size: 34rpx;
font-weight: 800;
}
@@ -378,76 +387,96 @@
.defaults-link {
flex: none;
margin-top: 8rpx;
padding: 10rpx 0 10rpx 12rpx;
color: #2b67e8;
font-size: 26rpx;
font-size: 24rpx;
font-weight: 700;
line-height: 1.2;
white-space: nowrap;
}
.sheet-scroll {
max-height: calc(88vh - 250rpx);
padding: 0 28rpx;
flex: 1;
height: 0;
min-height: 0;
max-height: none;
width: 100%;
padding: 0;
box-sizing: border-box;
}
.sheet-form {
width: 100%;
padding: 0 24rpx 16rpx;
box-sizing: border-box;
}
.form-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 16rpx;
gap: 12rpx;
margin-top: 12rpx;
}
.form-grid .form-field {
width: calc((100% - 16rpx) / 2);
flex: 1 1 0;
width: auto;
min-width: 0;
}
picker.form-field {
box-sizing: border-box;
}
.form-field {
min-height: 136rpx;
padding: 22rpx;
min-height: 108rpx;
padding: 16rpx;
border: 2rpx solid #d8e5f2;
border-radius: 14rpx;
background: #fbfdff;
}
.full-field {
min-height: 150rpx;
min-height: 112rpx;
}
.form-label {
display: block;
color: #2b67e8;
font-size: 27rpx;
font-size: 24rpx;
font-weight: 700;
}
.form-value {
display: block;
margin-top: 28rpx;
margin-top: 14rpx;
color: #17233a;
font-size: 31rpx;
font-size: 28rpx;
font-weight: 700;
}
.form-input {
width: 100%;
height: 58rpx;
margin-top: 16rpx;
height: 48rpx;
margin-top: 10rpx;
color: #17233a;
font-size: 30rpx;
font-size: 28rpx;
font-weight: 600;
}
.guardian-field {
margin-top: 16rpx;
margin-top: 12rpx;
}
.bottom-grid {
margin-bottom: 24rpx;
margin-bottom: 16rpx;
}
.sheet-footer {
flex: none;
display: flex;
gap: 16rpx;
padding: 20rpx 28rpx 28rpx;
padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #e7eef6;
}

View File

@@ -23,8 +23,8 @@ use crate::{
type SharedState = Arc<AppState>;
const DEFAULT_GRADE: &str = "艺术类";
const DEFAULT_SUBJECT: &str = "美术";
const DEFAULT_GRADE: &str = "初一";
const DEFAULT_SUBJECT: &str = "数学";
const DEFAULT_DURATION_MINUTES: i16 = 60;
pub fn router() -> (Router<SharedState>, OpenApi) {
@@ -469,8 +469,8 @@ async fn delete_profile(
/// 当前用户的新建档案预设。
#[derive(Debug, Serialize, FromRow, ToSchema)]
#[schema(example = json!({
"grade": "艺术类",
"subject": "美术",
"grade": "初一",
"subject": "数学",
"lesson_duration_minutes": 60,
"updated_at": "2026-07-15T10:00:00Z"
}))]
@@ -488,8 +488,8 @@ struct ProfileDefaults {
/// 保存档案预设的请求体。
#[derive(Debug, Deserialize, ToSchema)]
#[schema(example = json!({
"grade": "艺术类",
"subject": "美术",
"grade": "初一",
"subject": "数学",
"lesson_duration_minutes": 60
}))]
struct ProfileDefaultsInput {
@@ -507,7 +507,7 @@ struct ProfileDefaultsInput {
path = "/api/v1/profile-defaults",
tag = "档案预设",
summary = "读取档案预设",
description = "读取当前用户的预设;尚未保存时直接返回艺术类、美术、60 分钟的系统默认值。",
description = "读取当前用户的预设;尚未保存时直接返回初一、数学、60 分钟的系统默认值。",
params(
("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>")
),
@@ -551,8 +551,8 @@ async fn get_defaults(
description = "要保存的档案预设",
content_type = "application/json",
example = json!({
"grade": "艺术类",
"subject": "美术",
"grade": "初一",
"subject": "数学",
"lesson_duration_minutes": 60
})
),