feat(ui): refine profile workflows and visual system
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "学生档案预设项"
|
||||
"navigationBarTitleText": "设置预设项"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import type { StudentProfileDefaults } from '../../utils/types'
|
||||
|
||||
const grades = ['初一', '初二', '初三']
|
||||
const subjects = ['美术', '书法', '音乐', '舞蹈', '语文', '数学', '英语']
|
||||
const durationPresets = [45, 60, 90, 120]
|
||||
const durationStep = 15
|
||||
const minimumDuration = 15
|
||||
const maximumDuration = 360
|
||||
const initialDefaults: StudentProfileDefaults = {
|
||||
grade: '初一',
|
||||
subject: '数学',
|
||||
@@ -10,11 +14,10 @@ const initialDefaults: StudentProfileDefaults = {
|
||||
}
|
||||
|
||||
type PickerEvent = { detail: { value: string | number } }
|
||||
type InputEvent = { detail: { value: string } }
|
||||
|
||||
function normalizeDuration(value: number): number {
|
||||
if (Number.isNaN(value)) return 60
|
||||
return Math.min(360, Math.max(15, Math.round(value / 15) * 15))
|
||||
return Math.min(maximumDuration, Math.max(minimumDuration, Math.round(value / durationStep) * durationStep))
|
||||
}
|
||||
|
||||
Page({
|
||||
@@ -22,6 +25,9 @@ Page({
|
||||
defaults: initialDefaults,
|
||||
grades,
|
||||
subjects,
|
||||
durationPresets,
|
||||
minimumDuration,
|
||||
maximumDuration,
|
||||
loading: false,
|
||||
saving: false,
|
||||
errorMessage: ''
|
||||
@@ -34,7 +40,13 @@ Page({
|
||||
async loadDefaults(showSuccess = false) {
|
||||
this.setData({ loading: true, errorMessage: '' })
|
||||
try {
|
||||
this.setData({ defaults: await getProfileDefaults() })
|
||||
const defaults = await getProfileDefaults()
|
||||
this.setData({
|
||||
defaults: {
|
||||
...defaults,
|
||||
lessonDurationMinutes: normalizeDuration(defaults.lessonDurationMinutes)
|
||||
}
|
||||
})
|
||||
if (showSuccess) wx.showToast({ title: '预设已刷新', icon: 'success' })
|
||||
} catch (error) {
|
||||
this.setData({ errorMessage: getApiErrorMessage(error) })
|
||||
@@ -55,12 +67,25 @@ Page({
|
||||
this.setData({ 'defaults.subject': subjects[Number(event.detail.value)] })
|
||||
},
|
||||
|
||||
onDurationInput(event: InputEvent) {
|
||||
this.setData({ 'defaults.lessonDurationMinutes': normalizeDuration(Number(event.detail.value)) })
|
||||
decreaseDuration() {
|
||||
this.setData({
|
||||
'defaults.lessonDurationMinutes': normalizeDuration(
|
||||
this.data.defaults.lessonDurationMinutes - durationStep
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
onDurationChange(event: PickerEvent) {
|
||||
this.setData({ 'defaults.lessonDurationMinutes': normalizeDuration(Number(event.detail.value)) })
|
||||
increaseDuration() {
|
||||
this.setData({
|
||||
'defaults.lessonDurationMinutes': normalizeDuration(
|
||||
this.data.defaults.lessonDurationMinutes + durationStep
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
selectDurationPreset(event: WechatMiniprogram.TouchEvent) {
|
||||
const { value } = event.currentTarget.dataset as { value: number | string }
|
||||
this.setData({ 'defaults.lessonDurationMinutes': normalizeDuration(Number(value)) })
|
||||
},
|
||||
|
||||
async save() {
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
<view>
|
||||
<text class="eyebrow">档案预设</text>
|
||||
<view class="page-heading">
|
||||
<text class="heading-icon">+</text>
|
||||
<text class="page-title">学生档案预设项</text>
|
||||
<text class="page-title">新增学生默认值</text>
|
||||
</view>
|
||||
<text class="page-subtitle">用于新增学生时自动带出</text>
|
||||
<text class="page-subtitle">新增学生时自动带出以下信息</text>
|
||||
</view>
|
||||
<button class="reload-button secondary-button" loading="{{loading}}" disabled="{{loading}}" bindtap="onReload">↻ 刷新</button>
|
||||
<button class="reload-button {{loading ? 'reload-button-loading' : ''}}" disabled="{{loading || saving}}" bindtap="onReload" aria-label="刷新预设项">
|
||||
<text class="reload-icon">↻</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{errorMessage}}" class="status-banner">
|
||||
@@ -18,41 +19,70 @@
|
||||
|
||||
<view class="defaults-card surface">
|
||||
<view class="defaults-grid">
|
||||
<picker class="select-field" mode="selector" range="{{grades}}" bindchange="onGradeChange">
|
||||
<text class="field-label">年级</text>
|
||||
<text class="field-value">{{defaults.grade}}</text>
|
||||
<picker class="select-field" mode="selector" range="{{grades}}" disabled="{{loading || saving}}" bindchange="onGradeChange" aria-label="默认年级,当前为 {{defaults.grade}}">
|
||||
<view class="select-field-content">
|
||||
<view class="select-copy">
|
||||
<text class="field-label">年级</text>
|
||||
<text class="field-value">{{defaults.grade}}</text>
|
||||
</view>
|
||||
<text class="select-chevron" aria-hidden="true">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
<picker class="select-field" mode="selector" range="{{subjects}}" bindchange="onSubjectChange">
|
||||
<text class="field-label">学科</text>
|
||||
<text class="field-value">{{defaults.subject}}</text>
|
||||
<picker class="select-field" mode="selector" range="{{subjects}}" disabled="{{loading || saving}}" bindchange="onSubjectChange" aria-label="默认学科,当前为 {{defaults.subject}}">
|
||||
<view class="select-field-content">
|
||||
<view class="select-copy">
|
||||
<text class="field-label">学科</text>
|
||||
<text class="field-value">{{defaults.subject}}</text>
|
||||
</view>
|
||||
<text class="select-chevron" aria-hidden="true">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="duration-field">
|
||||
<text class="field-label">上课时长</text>
|
||||
<view class="duration-input-row">
|
||||
<input class="duration-input" type="number" value="{{defaults.lessonDurationMinutes}}" bindblur="onDurationInput" />
|
||||
<text class="duration-unit">分钟</text>
|
||||
<view class="duration-heading">
|
||||
<text class="field-label">上课时长</text>
|
||||
<text class="duration-step-label">15 分钟间隔</text>
|
||||
</view>
|
||||
<slider
|
||||
class="duration-slider"
|
||||
min="15"
|
||||
max="360"
|
||||
step="15"
|
||||
value="{{defaults.lessonDurationMinutes}}"
|
||||
activeColor="#2B67E8"
|
||||
backgroundColor="#DBE7F5"
|
||||
blockColor="#2B67E8"
|
||||
blockSize="26"
|
||||
bindchange="onDurationChange"
|
||||
/>
|
||||
<view class="duration-bounds">
|
||||
<text>15 分钟</text>
|
||||
<text>360 分钟</text>
|
||||
|
||||
<view class="duration-stepper">
|
||||
<button
|
||||
class="duration-step-button"
|
||||
disabled="{{loading || saving || defaults.lessonDurationMinutes <= minimumDuration}}"
|
||||
bindtap="decreaseDuration"
|
||||
aria-label="减少 15 分钟"
|
||||
>−</button>
|
||||
<view class="duration-value" aria-label="当前上课时长 {{defaults.lessonDurationMinutes}} 分钟">
|
||||
<text class="duration-number">{{defaults.lessonDurationMinutes}}</text>
|
||||
<text class="duration-unit">分钟</text>
|
||||
</view>
|
||||
<button
|
||||
class="duration-step-button"
|
||||
disabled="{{loading || saving || defaults.lessonDurationMinutes >= maximumDuration}}"
|
||||
bindtap="increaseDuration"
|
||||
aria-label="增加 15 分钟"
|
||||
>+</button>
|
||||
</view>
|
||||
|
||||
<view class="duration-presets" aria-label="常用上课时长">
|
||||
<button
|
||||
wx:for="{{durationPresets}}"
|
||||
wx:key="*this"
|
||||
class="duration-preset {{defaults.lessonDurationMinutes === item ? 'duration-preset-active' : ''}}"
|
||||
disabled="{{loading || saving}}"
|
||||
data-value="{{item}}"
|
||||
bindtap="selectDurationPreset"
|
||||
aria-label="设置为 {{item}} 分钟"
|
||||
>{{item}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="time-help">新增学生时会按开始时间自动推算结束时间。</view>
|
||||
<button class="save-defaults primary-button" loading="{{saving}}" disabled="{{saving || loading}}" bindtap="save">保存</button>
|
||||
<view class="time-help">
|
||||
<icon class="time-help-icon" type="info" size="16" color="#6E6E73" />
|
||||
<text>新增学生时会根据开始时间自动推算结束时间</text>
|
||||
</view>
|
||||
<view class="save-area">
|
||||
<button class="save-defaults primary-button" loading="{{saving}}" disabled="{{saving || loading}}" bindtap="save">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.defaults-page {
|
||||
padding-top: 38rpx;
|
||||
padding-top: 44rpx;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
@@ -27,93 +27,287 @@
|
||||
}
|
||||
|
||||
.reload-button {
|
||||
display: flex;
|
||||
flex: none;
|
||||
width: 148rpx;
|
||||
min-width: 148rpx;
|
||||
max-width: 148rpx;
|
||||
min-height: 70rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72rpx;
|
||||
min-width: 72rpx;
|
||||
max-width: 72rpx;
|
||||
height: 72rpx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 26rpx;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: #f1f3f6;
|
||||
color: #007aff;
|
||||
line-height: 1;
|
||||
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.reload-button:active {
|
||||
transform: scale(0.94);
|
||||
background: #e5e5ea;
|
||||
}
|
||||
|
||||
.reload-button[disabled] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.reload-icon {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.reload-button-loading .reload-icon {
|
||||
animation: reload-spin 800ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes reload-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.defaults-card {
|
||||
margin-top: 34rpx;
|
||||
padding: 24rpx;
|
||||
margin-top: 28rpx;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.defaults-grid {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.defaults-grid .select-field {
|
||||
width: calc((100% - 16rpx) / 2);
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.select-field,
|
||||
.duration-field {
|
||||
padding: 24rpx;
|
||||
border: 2rpx solid #d8e5f2;
|
||||
border-radius: 14rpx;
|
||||
background: #fbfdff;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.select-field {
|
||||
min-height: 158rpx;
|
||||
min-height: 112rpx;
|
||||
transition: opacity 140ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.select-field-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14rpx;
|
||||
min-height: 112rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
}
|
||||
|
||||
.select-field + .select-field {
|
||||
border-left: 1rpx solid #e5e5ea;
|
||||
}
|
||||
|
||||
.select-field:active {
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.select-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.select-field .field-label,
|
||||
.select-field .field-value {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.select-field .field-label {
|
||||
color: #6e6e73;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.select-field .field-value {
|
||||
margin-top: 7rpx;
|
||||
color: #1c1c1e;
|
||||
font-size: 29rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.select-chevron {
|
||||
flex: none;
|
||||
color: #8e8e93;
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.duration-field {
|
||||
margin-top: 16rpx;
|
||||
padding: 24rpx;
|
||||
border-top: 1rpx solid #e5e5ea;
|
||||
}
|
||||
|
||||
.duration-input-row {
|
||||
.duration-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-top: 20rpx;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.duration-input {
|
||||
width: 150rpx;
|
||||
height: 64rpx;
|
||||
padding: 0 16rpx;
|
||||
border: 2rpx solid #bed4e8;
|
||||
border-radius: 12rpx;
|
||||
background: #ffffff;
|
||||
color: #17233a;
|
||||
.duration-step-label {
|
||||
color: #8e8e93;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.duration-stepper {
|
||||
display: grid;
|
||||
grid-template-columns: 72rpx minmax(0, 1fr) 72rpx;
|
||||
width: 330rpx;
|
||||
height: 76rpx;
|
||||
margin: 18rpx auto 0;
|
||||
overflow: hidden;
|
||||
border: 1rpx solid #e5e5ea;
|
||||
border-radius: 14rpx;
|
||||
background: #f1f3f6;
|
||||
}
|
||||
|
||||
.duration-step-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72rpx;
|
||||
min-width: 72rpx;
|
||||
max-width: 72rpx;
|
||||
height: 76rpx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #007aff;
|
||||
font-size: 38rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.duration-step-button:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.duration-step-button[disabled] {
|
||||
color: #c7c7cc;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.duration-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
min-width: 0;
|
||||
border-right: 1rpx solid #d1d1d6;
|
||||
border-left: 1rpx solid #d1d1d6;
|
||||
}
|
||||
|
||||
.duration-number {
|
||||
color: #1c1c1e;
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.duration-unit {
|
||||
color: #65758a;
|
||||
font-size: 28rpx;
|
||||
color: #6e6e73;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.duration-slider {
|
||||
margin: 12rpx -12rpx 0;
|
||||
}
|
||||
|
||||
.duration-bounds {
|
||||
.duration-presets {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #8a97a6;
|
||||
font-size: 23rpx;
|
||||
align-items: stretch;
|
||||
gap: 4rpx;
|
||||
margin-top: 18rpx;
|
||||
padding: 4rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f1f3f6;
|
||||
}
|
||||
|
||||
.duration-preset {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
min-height: 58rpx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 9rpx;
|
||||
background: transparent;
|
||||
color: #6e6e73;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.duration-preset-active {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(60, 60, 67, 0.12);
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.duration-preset:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.time-help {
|
||||
margin-top: 18rpx;
|
||||
padding: 20rpx;
|
||||
border: 2rpx dashed #c6ddea;
|
||||
border-radius: 12rpx;
|
||||
color: #65758a;
|
||||
font-size: 25rpx;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10rpx;
|
||||
margin: 0 24rpx;
|
||||
padding: 2rpx 0 0;
|
||||
color: #6e6e73;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.save-defaults {
|
||||
width: 420rpx;
|
||||
margin: 26rpx auto 4rpx;
|
||||
.time-help-icon {
|
||||
flex: none;
|
||||
margin-top: 3rpx;
|
||||
}
|
||||
|
||||
.save-area {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.save-defaults {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.reload-button,
|
||||
.select-field,
|
||||
.duration-step-button,
|
||||
.duration-preset {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.reload-button:active,
|
||||
.duration-step-button:active,
|
||||
.duration-preset:active {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.select-field:active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.reload-button-loading .reload-icon {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user