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 # Logs and OS metadata
*.log *.log
.DS_Store .DS_Store
# Local UI screenshots
screenshots/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -112,44 +112,49 @@
</view> </view>
<scroll-view class="sheet-scroll" scroll-y="true"> <scroll-view class="sheet-scroll" scroll-y="true">
<view class="form-field full-field"> <view class="sheet-form">
<text class="form-label">学生姓名</text> <view class="form-field full-field">
<input class="form-input" placeholder="请输入学生姓名" value="{{draft.name}}" bindinput="onNameInput" maxlength="20" /> <text class="form-label">学生姓名</text>
</view> <input class="form-input" placeholder="请输入学生姓名" value="{{draft.name}}" bindinput="onNameInput" maxlength="20" />
</view>
<view class="form-grid"> <view class="form-grid">
<picker class="form-field" mode="selector" range="{{grades}}" bindchange="onGradeChange"> <picker class="form-field" mode="selector" range="{{grades}}" bindchange="onGradeChange">
<text class="form-label">年级</text> <text class="form-label">年级</text>
<text class="form-value">{{draft.grade}}</text> <text class="form-value">{{draft.grade}}</text>
</picker> </picker>
<picker class="form-field" mode="selector" range="{{subjects}}" bindchange="onSubjectChange"> <picker class="form-field" mode="selector" range="{{subjects}}" bindchange="onSubjectChange">
<text class="form-label">学科</text> <text class="form-label">学科</text>
<text class="form-value">{{draft.subject}}</text> <text class="form-value">{{draft.subject}}</text>
</picker> </picker>
<picker class="form-field" mode="selector" range="{{years}}" bindchange="onYearChange"> </view>
<text class="form-label">日期年份</text>
<text class="form-value">{{draft.academicYear}}</text>
</picker>
<picker class="form-field" mode="selector" range="{{terms}}" bindchange="onTermChange">
<text class="form-label">日期</text>
<text class="form-value">{{draft.term}}</text>
</picker>
</view>
<view class="form-field full-field guardian-field"> <view class="form-grid">
<text class="form-label">默认家长称呼</text> <picker class="form-field" mode="selector" range="{{years}}" bindchange="onYearChange">
<input class="form-input" placeholder="默认:学生姓名 + 妈妈" value="{{draft.guardianTitle}}" bindinput="onGuardianInput" maxlength="24" /> <text class="form-label">日期年份</text>
</view> <text class="form-value">{{draft.academicYear}}</text>
</picker>
<picker class="form-field" mode="selector" range="{{terms}}" bindchange="onTermChange">
<text class="form-label">日期</text>
<text class="form-value">{{draft.term}}</text>
</picker>
</view>
<view class="form-grid bottom-grid"> <view class="form-field full-field guardian-field">
<picker class="form-field" mode="time" value="{{draft.startTime}}" bindchange="onStartTimeChange"> <text class="form-label">默认家长称呼</text>
<text class="form-label">开始时间</text> <input class="form-input" placeholder="默认:学生姓名 + 妈妈" value="{{draft.guardianTitle}}" bindinput="onGuardianInput" maxlength="24" />
<text class="form-value">{{draft.startTime}}</text> </view>
</picker>
<picker class="form-field" mode="time" value="{{draft.endTime}}" bindchange="onEndTimeChange"> <view class="form-grid bottom-grid">
<text class="form-label">结束时间</text> <picker class="form-field" mode="time" value="{{draft.startTime}}" bindchange="onStartTimeChange">
<text class="form-value">{{draft.endTime}}</text> <text class="form-label">开始时间</text>
</picker> <text class="form-value">{{draft.startTime}}</text>
</picker>
<picker class="form-field" mode="time" value="{{draft.endTime}}" bindchange="onEndTimeChange">
<text class="form-label">结束时间</text>
<text class="form-value">{{draft.endTime}}</text>
</picker>
</view>
</view> </view>
</scroll-view> </scroll-view>

View File

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

View File

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