feat(profile): refine student creation workflow
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,3 +20,6 @@ project.private.config.json
|
||||
# Logs and OS metadata
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# Local UI screenshots
|
||||
screenshots/
|
||||
|
||||
@@ -8,6 +8,7 @@ const tabItems = [
|
||||
Component({
|
||||
data: {
|
||||
selected: 0,
|
||||
visible: true,
|
||||
tabItems
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<view class="tabbar" role="tablist">
|
||||
<view wx:if="{{visible}}" class="tabbar" role="tablist">
|
||||
<view
|
||||
wx:for="{{tabItems}}"
|
||||
wx:key="pagePath"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user