feat(ui): refine profile workflows and visual system

This commit is contained in:
2026-07-21 20:50:52 +08:00
parent 12a000845a
commit 04e80b184c
16 changed files with 1105 additions and 334 deletions

View File

@@ -7,11 +7,11 @@
"pages/defaults/index" "pages/defaults/index"
], ],
"window": { "window": {
"navigationBarBackgroundColor": "#F4F8FE", "navigationBarBackgroundColor": "#F4F7FB",
"navigationBarTextStyle": "black", "navigationBarTextStyle": "black",
"navigationBarTitleText": "教学反馈助手", "navigationBarTitleText": "教学反馈助手",
"backgroundColor": "#F4F8FE", "backgroundColor": "#F4F7FB",
"backgroundTextStyle": "light" "backgroundTextStyle": "dark"
}, },
"permission": { "permission": {
"scope.record": { "scope.record": {
@@ -20,9 +20,9 @@
}, },
"tabBar": { "tabBar": {
"custom": true, "custom": true,
"color": "#7B8796", "color": "#8E8E93",
"selectedColor": "#2B67E8", "selectedColor": "#007AFF",
"backgroundColor": "#FFFFFF", "backgroundColor": "#F8FAFC",
"borderStyle": "white", "borderStyle": "white",
"list": [ "list": [
{ "pagePath": "pages/feedback/index", "text": "反馈生成" }, { "pagePath": "pages/feedback/index", "text": "反馈生成" },

108
app.wxss
View File

@@ -1,15 +1,18 @@
page { page {
min-height: 100%; min-height: 100%;
box-sizing: border-box; box-sizing: border-box;
background: #f4f8fe; background: #f4f7fb;
color: #17233a; color: #1c1c1e;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
} }
view, view,
text, text,
input, input,
button { button,
textarea,
picker,
scroll-view {
box-sizing: border-box; box-sizing: border-box;
} }
@@ -19,50 +22,42 @@ button::after {
.page-shell { .page-shell {
min-height: 100vh; min-height: 100vh;
padding: 40rpx 32rpx 164rpx; padding: 44rpx 32rpx 184rpx;
} }
.eyebrow { .eyebrow {
color: #2b67e8; color: #007aff;
font-size: 28rpx; font-size: 26rpx;
font-weight: 700; font-weight: 700;
line-height: 1.4; line-height: 1.35;
} }
.page-heading { .page-heading {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 14rpx; margin-top: 10rpx;
margin-top: 12rpx;
}
.heading-icon {
color: #2b67e8;
font-size: 46rpx;
font-weight: 500;
line-height: 1;
} }
.page-title { .page-title {
color: #111b32; color: #1c1c1e;
font-size: 52rpx; font-size: 50rpx;
font-weight: 800; font-weight: 800;
line-height: 1.2; line-height: 1.15;
} }
.page-subtitle { .page-subtitle {
margin-top: 18rpx; margin-top: 16rpx;
color: #718096; color: #6e6e73;
font-size: 30rpx; font-size: 28rpx;
line-height: 1.55; line-height: 1.5;
} }
.surface { .surface {
overflow: hidden; overflow: hidden;
border: 2rpx solid #d7e5f4; border: 1rpx solid #e5e5ea;
border-radius: 16rpx; border-radius: 16rpx;
background: #ffffff; background: #ffffff;
box-shadow: 0 8rpx 22rpx rgba(45, 78, 120, 0.08); box-shadow: 0 4rpx 18rpx rgba(60, 60, 67, 0.06);
} }
.primary-button, .primary-button,
@@ -76,19 +71,40 @@ button::after {
font-size: 30rpx; font-size: 30rpx;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
transform-origin: center;
} }
.primary-button { .primary-button {
border: 2rpx solid #2b67e8; border: 0;
background: #2b67e8; background: #007aff;
color: #ffffff; color: #ffffff;
} }
.secondary-button, .secondary-button,
.icon-button { .icon-button {
border: 2rpx solid #c9dced; border: 1rpx solid #d1d1d6;
background: #ffffff; background: #ffffff;
color: #2b67e8; color: #007aff;
}
.primary-button:active,
.secondary-button:active,
.icon-button:active {
transform: scale(0.98);
}
.primary-button:active {
background: #006ee6;
}
.secondary-button:active,
.icon-button:active {
background: #f2f7ff;
}
button[disabled]:active {
transform: none;
} }
.button-icon { .button-icon {
@@ -98,21 +114,21 @@ button::after {
} }
.helper-text { .helper-text {
color: #7b8796; color: #6e6e73;
font-size: 26rpx; font-size: 26rpx;
line-height: 1.55; line-height: 1.55;
} }
.field-label { .field-label {
color: #2b67e8; color: #6e6e73;
font-size: 28rpx; font-size: 26rpx;
font-weight: 700; font-weight: 700;
} }
.field-value { .field-value {
margin-top: 18rpx; margin-top: 14rpx;
color: #17233a; color: #1c1c1e;
font-size: 32rpx; font-size: 30rpx;
font-weight: 700; font-weight: 700;
} }
@@ -123,9 +139,9 @@ button::after {
gap: 20rpx; gap: 20rpx;
margin-top: 24rpx; margin-top: 24rpx;
padding: 20rpx 22rpx; padding: 20rpx 22rpx;
border: 2rpx solid #efc7c7; border: 1rpx solid #f1c4c4;
border-radius: 12rpx; border-radius: 12rpx;
background: #fff7f7; background: #fff2f2;
color: #a83d3d; color: #a83d3d;
font-size: 25rpx; font-size: 25rpx;
line-height: 1.45; line-height: 1.45;
@@ -133,13 +149,27 @@ button::after {
.status-action { .status-action {
flex: none; flex: none;
color: #2b67e8; color: #007aff;
font-weight: 700; font-weight: 700;
} }
.loading-state { .loading-state {
padding: 70rpx 32rpx; padding: 70rpx 32rpx;
color: #718096; color: #6e6e73;
font-size: 28rpx; font-size: 28rpx;
text-align: center; text-align: center;
} }
@media (prefers-reduced-motion: reduce) {
.primary-button,
.secondary-button,
.icon-button {
transition: none;
}
.primary-button:active,
.secondary-button:active,
.icon-button:active {
transform: none;
}
}

View File

@@ -7,8 +7,10 @@
display: flex; display: flex;
min-height: 112rpx; min-height: 112rpx;
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
border-top: 1rpx solid #edf1f6; border-top: 1rpx solid rgba(209, 209, 214, 0.72);
background: #ffffff; background: rgba(248, 250, 252, 0.92);
-webkit-backdrop-filter: blur(24rpx) saturate(180%);
backdrop-filter: blur(24rpx) saturate(180%);
} }
.tab-item { .tab-item {
@@ -19,11 +21,17 @@
justify-content: center; justify-content: center;
gap: 6rpx; gap: 6rpx;
min-height: 112rpx; min-height: 112rpx;
color: #8995a5; color: #8e8e93;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
transform-origin: center;
} }
.tab-item-active { .tab-item-active {
color: #2b67e8; color: #007aff;
}
.tab-item:active {
transform: scale(0.96);
} }
.tab-glyph { .tab-glyph {
@@ -36,3 +44,21 @@
font-size: 24rpx; font-size: 24rpx;
line-height: 1.2; line-height: 1.2;
} }
@media (prefers-reduced-motion: reduce) {
.tab-item {
transition: none;
}
.tab-item:active {
transform: none;
}
}
@media (prefers-reduced-transparency: reduce) {
.tabbar {
background: #f8fafc;
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
}

View File

@@ -1,3 +1,3 @@
{ {
"navigationBarTitleText": "学生档案预设项" "navigationBarTitleText": "设置预设项"
} }

View File

@@ -3,6 +3,10 @@ import type { StudentProfileDefaults } from '../../utils/types'
const grades = ['初一', '初二', '初三'] const grades = ['初一', '初二', '初三']
const subjects = ['美术', '书法', '音乐', '舞蹈', '语文', '数学', '英语'] const subjects = ['美术', '书法', '音乐', '舞蹈', '语文', '数学', '英语']
const durationPresets = [45, 60, 90, 120]
const durationStep = 15
const minimumDuration = 15
const maximumDuration = 360
const initialDefaults: StudentProfileDefaults = { const initialDefaults: StudentProfileDefaults = {
grade: '初一', grade: '初一',
subject: '数学', subject: '数学',
@@ -10,11 +14,10 @@ const initialDefaults: StudentProfileDefaults = {
} }
type PickerEvent = { detail: { value: string | number } } type PickerEvent = { detail: { value: string | number } }
type InputEvent = { detail: { value: string } }
function normalizeDuration(value: number): number { function normalizeDuration(value: number): number {
if (Number.isNaN(value)) return 60 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({ Page({
@@ -22,6 +25,9 @@ Page({
defaults: initialDefaults, defaults: initialDefaults,
grades, grades,
subjects, subjects,
durationPresets,
minimumDuration,
maximumDuration,
loading: false, loading: false,
saving: false, saving: false,
errorMessage: '' errorMessage: ''
@@ -34,7 +40,13 @@ Page({
async loadDefaults(showSuccess = false) { async loadDefaults(showSuccess = false) {
this.setData({ loading: true, errorMessage: '' }) this.setData({ loading: true, errorMessage: '' })
try { 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' }) if (showSuccess) wx.showToast({ title: '预设已刷新', icon: 'success' })
} catch (error) { } catch (error) {
this.setData({ errorMessage: getApiErrorMessage(error) }) this.setData({ errorMessage: getApiErrorMessage(error) })
@@ -55,12 +67,25 @@ Page({
this.setData({ 'defaults.subject': subjects[Number(event.detail.value)] }) this.setData({ 'defaults.subject': subjects[Number(event.detail.value)] })
}, },
onDurationInput(event: InputEvent) { decreaseDuration() {
this.setData({ 'defaults.lessonDurationMinutes': normalizeDuration(Number(event.detail.value)) }) this.setData({
'defaults.lessonDurationMinutes': normalizeDuration(
this.data.defaults.lessonDurationMinutes - durationStep
)
})
}, },
onDurationChange(event: PickerEvent) { increaseDuration() {
this.setData({ 'defaults.lessonDurationMinutes': normalizeDuration(Number(event.detail.value)) }) 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() { async save() {

View File

@@ -3,12 +3,13 @@
<view> <view>
<text class="eyebrow">档案预设</text> <text class="eyebrow">档案预设</text>
<view class="page-heading"> <view class="page-heading">
<text class="heading-icon"></text> <text class="page-title">新增学生默认值</text>
<text class="page-title">学生档案预设项</text>
</view> </view>
<text class="page-subtitle">用于新增学生时自动带出</text> <text class="page-subtitle">新增学生时自动带出以下信息</text>
</view> </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>
<view wx:if="{{errorMessage}}" class="status-banner"> <view wx:if="{{errorMessage}}" class="status-banner">
@@ -18,41 +19,70 @@
<view class="defaults-card surface"> <view class="defaults-card surface">
<view class="defaults-grid"> <view class="defaults-grid">
<picker class="select-field" mode="selector" range="{{grades}}" bindchange="onGradeChange"> <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-label">年级</text>
<text class="field-value">{{defaults.grade}}</text> <text class="field-value">{{defaults.grade}}</text>
</view>
<text class="select-chevron" aria-hidden="true"></text>
</view>
</picker> </picker>
<picker class="select-field" mode="selector" range="{{subjects}}" bindchange="onSubjectChange"> <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-label">学科</text>
<text class="field-value">{{defaults.subject}}</text> <text class="field-value">{{defaults.subject}}</text>
</view>
<text class="select-chevron" aria-hidden="true"></text>
</view>
</picker> </picker>
</view> </view>
<view class="duration-field"> <view class="duration-field">
<view class="duration-heading">
<text class="field-label">上课时长</text> <text class="field-label">上课时长</text>
<view class="duration-input-row"> <text class="duration-step-label">15 分钟间隔</text>
<input class="duration-input" type="number" value="{{defaults.lessonDurationMinutes}}" bindblur="onDurationInput" /> </view>
<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> <text class="duration-unit">分钟</text>
</view> </view>
<slider <button
class="duration-slider" class="duration-step-button"
min="15" disabled="{{loading || saving || defaults.lessonDurationMinutes >= maximumDuration}}"
max="360" bindtap="increaseDuration"
step="15" aria-label="增加 15 分钟"
value="{{defaults.lessonDurationMinutes}}" ></button>
activeColor="#2B67E8" </view>
backgroundColor="#DBE7F5"
blockColor="#2B67E8" <view class="duration-presets" aria-label="常用上课时长">
blockSize="26" <button
bindchange="onDurationChange" wx:for="{{durationPresets}}"
/> wx:key="*this"
<view class="duration-bounds"> class="duration-preset {{defaults.lessonDurationMinutes === item ? 'duration-preset-active' : ''}}"
<text>15 分钟</text> disabled="{{loading || saving}}"
<text>360 分钟</text> data-value="{{item}}"
bindtap="selectDurationPreset"
aria-label="设置为 {{item}} 分钟"
>{{item}}</button>
</view> </view>
</view> </view>
<view class="time-help">新增学生时会按开始时间自动推算结束时间。</view> <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> <button class="save-defaults primary-button" loading="{{saving}}" disabled="{{saving || loading}}" bindtap="save">保存</button>
</view> </view>
</view> </view>
</view>

View File

@@ -1,5 +1,5 @@
.defaults-page { .defaults-page {
padding-top: 38rpx; padding-top: 44rpx;
} }
.top-row { .top-row {
@@ -27,93 +27,287 @@
} }
.reload-button { .reload-button {
display: flex;
flex: none; flex: none;
width: 148rpx; align-items: center;
min-width: 148rpx; justify-content: center;
max-width: 148rpx; width: 72rpx;
min-height: 70rpx; min-width: 72rpx;
max-width: 72rpx;
height: 72rpx;
margin: 0; margin: 0;
padding: 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 { .defaults-card {
margin-top: 34rpx; margin-top: 28rpx;
padding: 24rpx; padding: 0;
} }
.defaults-grid { .defaults-grid {
display: flex; display: flex;
gap: 16rpx; gap: 0;
} }
.defaults-grid .select-field { .defaults-grid .select-field {
width: calc((100% - 16rpx) / 2); width: 50%;
} }
.select-field, .select-field,
.duration-field { .duration-field {
padding: 24rpx; background: transparent;
border: 2rpx solid #d8e5f2;
border-radius: 14rpx;
background: #fbfdff;
} }
.select-field { .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 { .duration-field {
margin-top: 16rpx; padding: 24rpx;
border-top: 1rpx solid #e5e5ea;
} }
.duration-input-row { .duration-heading {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12rpx; justify-content: space-between;
margin-top: 20rpx; gap: 16rpx;
} }
.duration-input { .duration-step-label {
width: 150rpx; color: #8e8e93;
height: 64rpx; font-size: 22rpx;
padding: 0 16rpx; }
border: 2rpx solid #bed4e8;
border-radius: 12rpx; .duration-stepper {
background: #ffffff; display: grid;
color: #17233a; 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-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; font-weight: 800;
text-align: center; line-height: 1;
} }
.duration-unit { .duration-unit {
color: #65758a; color: #6e6e73;
font-size: 28rpx; font-size: 22rpx;
} }
.duration-slider { .duration-presets {
margin: 12rpx -12rpx 0;
}
.duration-bounds {
display: flex; display: flex;
justify-content: space-between; align-items: stretch;
color: #8a97a6; gap: 4rpx;
font-size: 23rpx; 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 { .time-help {
margin-top: 18rpx; display: flex;
padding: 20rpx; align-items: flex-start;
border: 2rpx dashed #c6ddea; gap: 10rpx;
border-radius: 12rpx; margin: 0 24rpx;
color: #65758a; padding: 2rpx 0 0;
font-size: 25rpx; color: #6e6e73;
font-size: 24rpx;
line-height: 1.5; line-height: 1.5;
} }
.save-defaults { .time-help-icon {
width: 420rpx; flex: none;
margin: 26rpx auto 4rpx; 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;
}
} }

View File

@@ -1,7 +1,6 @@
<view class="page-shell feedback-page"> <view class="page-shell feedback-page">
<text class="eyebrow">课堂反馈</text> <text class="eyebrow">课堂反馈</text>
<view class="page-heading"> <view class="page-heading">
<text class="heading-icon">✦</text>
<text class="page-title">反馈生成</text> <text class="page-title">反馈生成</text>
</view> </view>
<text class="page-subtitle">填写课堂表现并保存到教学反馈记录。</text> <text class="page-subtitle">填写课堂表现并保存到教学反馈记录。</text>

View File

@@ -1,23 +1,34 @@
.feedback-page { .feedback-page {
padding-top: 44rpx; padding-top: 48rpx;
} }
.feedback-form { .feedback-form {
margin-top: 32rpx; margin-top: 30rpx;
padding: 24rpx; padding: 0;
} }
.feedback-field, .feedback-field,
.content-field { .content-field {
padding: 22rpx; padding: 24rpx;
border: 2rpx solid #d8e5f2; background: transparent;
border-radius: 14rpx; }
background: #fbfdff;
.feedback-field .form-label,
.feedback-field .form-value {
display: block;
}
.feedback-field .form-value {
margin-top: 10rpx;
color: #1c1c1e;
font-size: 30rpx;
line-height: 1.35;
} }
.feedback-field + .feedback-field, .feedback-field + .feedback-field,
.content-field { .content-field {
margin-top: 16rpx; margin-top: 0;
border-top: 1rpx solid #e5e5ea;
} }
.content-label-row, .content-label-row,
@@ -29,45 +40,51 @@
} }
.content-count { .content-count {
color: #8a97a6; color: #8e8e93;
font-size: 23rpx; font-size: 23rpx;
} }
.feedback-textarea { .feedback-textarea {
width: 100%; width: 100%;
min-height: 300rpx; min-height: 280rpx;
margin-top: 18rpx; margin-top: 16rpx;
color: #17233a; padding: 18rpx;
font-size: 29rpx; border-radius: 12rpx;
line-height: 1.6; background: #f1f3f6;
color: #1c1c1e;
font-size: 28rpx;
line-height: 1.65;
} }
.voice-input-row { .voice-input-row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: 14rpx; gap: 12rpx;
margin-top: 18rpx; margin-top: 20rpx;
padding-top: 18rpx;
border-top: 2rpx solid #e7eef6;
} }
.voice-button { .voice-button {
display: flex; display: flex;
align-self: stretch;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12rpx; gap: 12rpx;
width: 100%; width: 100%;
height: 80rpx; min-width: 100%;
max-width: 100%;
height: 84rpx;
margin: 0; margin: 0;
padding: 0 20rpx; padding: 0 20rpx;
border: 2rpx solid #b8cbe1; border: 0;
border-radius: 12rpx; border-radius: 12rpx;
background: #ffffff; background: #eaf3ff;
color: #2b67e8; color: #007aff;
font-size: 26rpx; font-size: 26rpx;
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 1;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
transform-origin: center;
} }
.voice-button::after { .voice-button::after {
@@ -75,16 +92,24 @@
} }
.voice-button[disabled] { .voice-button[disabled] {
background: #f4f7fa; background: #f1f3f6;
color: #99a5b3; color: #aeaeb2;
opacity: 1; opacity: 1;
} }
.voice-button-recording, .voice-button-recording,
.voice-button-recording[disabled] { .voice-button-recording[disabled] {
border-color: #e46b65; background: #fff0f0;
background: #fff4f3; color: #d13f3a;
color: #c83f39; }
.voice-button:active {
transform: scale(0.98);
background: #dcecff;
}
.voice-button-recording:active {
background: #ffe2e1;
} }
.voice-button-icon { .voice-button-icon {
@@ -99,7 +124,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
min-width: 0; min-width: 0;
color: #65758a; color: #6e6e73;
font-size: 24rpx; font-size: 24rpx;
} }
@@ -113,12 +138,22 @@
.voice-state-actionable { .voice-state-actionable {
min-height: 64rpx; min-height: 64rpx;
padding: 14rpx 16rpx;
border: 1rpx solid #e5e5ea;
border-radius: 12rpx;
background: #f7f7f8;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
}
.voice-state-actionable:active {
transform: scale(0.99);
background: #ededf0;
} }
.voice-state-arrow { .voice-state-arrow {
flex: none; flex: none;
margin-left: 10rpx; margin-left: 10rpx;
color: #7f8da0; color: #8e8e93;
font-size: 38rpx; font-size: 38rpx;
line-height: 1; line-height: 1;
} }
@@ -126,20 +161,36 @@
.recording-time { .recording-time {
flex: none; flex: none;
margin-left: 12rpx; margin-left: 12rpx;
color: #c83f39; color: #d13f3a;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.save-feedback { .save-feedback {
margin-top: 24rpx; width: calc(100% - 48rpx);
min-width: calc(100% - 48rpx);
max-width: calc(100% - 48rpx);
margin: 20rpx 24rpx 24rpx;
} }
.profiles-hint { .profiles-hint {
margin-top: 22rpx; margin-top: 22rpx;
padding: 20rpx 22rpx; padding: 20rpx 22rpx;
border: 2rpx dashed #c7ddeb; border: 1rpx solid #d1e3fb;
border-radius: 12rpx; border-radius: 12rpx;
color: #65758a; background: #f7fbff;
color: #6e6e73;
font-size: 24rpx; font-size: 24rpx;
line-height: 1.45; line-height: 1.45;
} }
@media (prefers-reduced-motion: reduce) {
.voice-button,
.voice-state-actionable {
transition: none;
}
.voice-button:active,
.voice-state-actionable:active {
transform: none;
}
}

View File

@@ -1,7 +1,6 @@
<view class="page-shell profile-page"> <view class="page-shell profile-page">
<text class="eyebrow">个人中心</text> <text class="eyebrow">个人中心</text>
<view class="page-heading"> <view class="page-heading">
<text class="heading-icon">◉</text>
<text class="page-title">我的</text> <text class="page-title">我的</text>
</view> </view>
<text class="page-subtitle">管理小程序连接的教学反馈服务。</text> <text class="page-subtitle">管理小程序连接的教学反馈服务。</text>

View File

@@ -1,11 +1,11 @@
.profile-page { .profile-page {
padding-top: 44rpx; padding-top: 48rpx;
} }
.server-card, .server-card,
.identity-card { .identity-card {
margin-top: 30rpx; margin-top: 28rpx;
padding: 26rpx; padding: 24rpx;
} }
.identity-card { .identity-card {
@@ -18,8 +18,8 @@
} }
.section-title { .section-title {
color: #17233a; color: #1c1c1e;
font-size: 32rpx; font-size: 30rpx;
font-weight: 800; font-weight: 800;
} }
@@ -32,10 +32,10 @@
height: 78rpx; height: 78rpx;
margin-top: 12rpx; margin-top: 12rpx;
padding: 0 20rpx; padding: 0 20rpx;
border: 2rpx solid #cbdced; border: 1rpx solid #e5e5ea;
border-radius: 12rpx; border-radius: 12rpx;
background: #fbfdff; background: #f1f3f6;
color: #17233a; color: #1c1c1e;
font-size: 27rpx; font-size: 27rpx;
} }
@@ -50,23 +50,23 @@
width: 16rpx; width: 16rpx;
height: 16rpx; height: 16rpx;
border-radius: 50%; border-radius: 50%;
background: #8a97a6; background: #8e8e93;
} }
.connection-success { .connection-success {
background: #23855b; background: #248a3d;
} }
.connection-warning { .connection-warning {
background: #c17a18; background: #c66a00;
} }
.connection-error { .connection-error {
background: #c34343; background: #d13f3a;
} }
.connection-text { .connection-text {
color: #566577; color: #3a3a3c;
font-size: 25rpx; font-size: 25rpx;
} }
@@ -76,7 +76,7 @@
.identity-value { .identity-value {
margin-top: 18rpx; margin-top: 18rpx;
color: #2b67e8; color: #007aff;
font-size: 24rpx; font-size: 24rpx;
font-weight: 700; font-weight: 700;
word-break: break-all; word-break: break-all;

View File

@@ -1,7 +1,6 @@
<view class="page-shell records-page"> <view class="page-shell records-page">
<text class="eyebrow">课堂反馈</text> <text class="eyebrow">课堂反馈</text>
<view class="page-heading"> <view class="page-heading">
<text class="heading-icon">◴</text>
<text class="page-title">反馈记录</text> <text class="page-title">反馈记录</text>
</view> </view>
<text class="page-subtitle">按学生查阅已保存的课堂反馈。</text> <text class="page-subtitle">按学生查阅已保存的课堂反馈。</text>

View File

@@ -1,10 +1,10 @@
.records-page { .records-page {
padding-top: 44rpx; padding-top: 48rpx;
} }
.records-filter { .records-filter {
margin-top: 30rpx; margin-top: 28rpx;
padding: 22rpx 24rpx; padding: 24rpx;
} }
.filter-label, .filter-label,
@@ -13,13 +13,13 @@
} }
.filter-label { .filter-label {
color: #718096; color: #6e6e73;
font-size: 23rpx; font-size: 23rpx;
} }
.filter-value { .filter-value {
margin-top: 8rpx; margin-top: 8rpx;
color: #17233a; color: #1c1c1e;
font-size: 29rpx; font-size: 29rpx;
font-weight: 700; font-weight: 700;
} }
@@ -34,13 +34,13 @@
} }
.empty-icon { .empty-icon {
color: #2b67e8; color: #007aff;
font-size: 56rpx; font-size: 56rpx;
} }
.empty-title { .empty-title {
margin: 20rpx 0 12rpx; margin: 20rpx 0 12rpx;
color: #17233a; color: #1c1c1e;
font-size: 36rpx; font-size: 36rpx;
font-weight: 800; font-weight: 800;
} }
@@ -53,12 +53,12 @@
.record-list { .record-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 18rpx; gap: 14rpx;
margin-top: 22rpx; margin-top: 18rpx;
} }
.record-card { .record-card {
padding: 24rpx; padding: 26rpx 24rpx;
} }
.record-top { .record-top {
@@ -75,22 +75,22 @@
} }
.record-profile { .record-profile {
color: #17233a; color: #1c1c1e;
font-size: 31rpx; font-size: 31rpx;
font-weight: 800; font-weight: 800;
} }
.record-date { .record-date {
margin-top: 6rpx; margin-top: 6rpx;
color: #718096; color: #6e6e73;
font-size: 23rpx; font-size: 23rpx;
} }
.record-content { .record-content {
margin-top: 20rpx; margin-top: 20rpx;
padding-top: 18rpx; padding-top: 18rpx;
border-top: 1rpx solid #e8eef5; border-top: 1rpx solid #e5e5ea;
color: #334258; color: #3a3a3c;
font-size: 27rpx; font-size: 27rpx;
line-height: 1.65; line-height: 1.65;
white-space: pre-wrap; white-space: pre-wrap;
@@ -99,10 +99,10 @@
.delete-button { .delete-button {
margin: 0; margin: 0;
padding: 8rpx 14rpx; padding: 8rpx 14rpx;
border: 1rpx solid #f2c6c6; border: 1rpx solid #f1c4c4;
border-radius: 10rpx; border-radius: 10rpx;
background: #ffffff; background: #fff2f2;
color: #c34343; color: #d13f3a;
font-size: 22rpx; font-size: 22rpx;
line-height: 1.3; line-height: 1.3;
} }

View File

@@ -3,10 +3,11 @@ import {
deleteProfile as deleteProfileRequest, deleteProfile as deleteProfileRequest,
getApiErrorMessage, getApiErrorMessage,
getProfileDefaults, getProfileDefaults,
listFeedbackRecords,
listProfiles listProfiles
} from '../../utils/api' } from '../../utils/api'
import { addMinutes, formatUpdatedAt } from '../../utils/time' import { addMinutes, formatUpdatedAt } from '../../utils/time'
import type { StudentDraft, StudentProfile, StudentProfileDefaults, Term } from '../../utils/types' import type { FeedbackRecord, StudentDraft, StudentProfile, StudentProfileDefaults, Term } from '../../utils/types'
const grades = ['初一', '初二', '初三'] const grades = ['初一', '初二', '初三']
const subjects = ['美术', '书法', '音乐', '舞蹈', '语文', '数学', '英语'] const subjects = ['美术', '书法', '音乐', '舞蹈', '语文', '数学', '英语']
@@ -17,9 +18,23 @@ const initialDefaults: StudentProfileDefaults = {
subject: '数学', subject: '数学',
lessonDurationMinutes: 60 lessonDurationMinutes: 60
} }
const termLabels: Record<Term, string> = {
: '春季',
: '暑期',
: '秋季',
: '寒假'
}
type InputEvent = { detail: { value: string } } type InputEvent = { detail: { value: string } }
type PickerEvent = { detail: { value: string | number } } type PickerEvent = { detail: { value: string | number } }
type StudentProfileView = StudentProfile & { academicTermLabel: string }
function withAcademicTermLabel(profile: StudentProfile): StudentProfileView {
return {
...profile,
academicTermLabel: `${profile.academicYear}${termLabels[profile.term]}`
}
}
function getCurrentTerm(): Term { function getCurrentTerm(): Term {
const month = new Date().getMonth() + 1 const month = new Date().getMonth() + 1
@@ -44,11 +59,11 @@ function buildDraft(defaults: StudentProfileDefaults = initialDefaults): Student
} }
function filterProfiles( function filterProfiles(
profiles: StudentProfile[], profiles: StudentProfileView[],
query: string, query: string,
filterGrade: string, filterGrade: string,
filterSubject: string filterSubject: string
): StudentProfile[] { ): StudentProfileView[] {
const keyword = query.trim().toLocaleLowerCase() const keyword = query.trim().toLocaleLowerCase()
return profiles.filter((profile) => { return profiles.filter((profile) => {
const searchable = [profile.name, profile.grade, profile.subject, profile.guardianTitle] const searchable = [profile.name, profile.grade, profile.subject, profile.guardianTitle]
@@ -69,12 +84,17 @@ function getLastUpdatedText(profiles: StudentProfile[]): string {
Page({ Page({
data: { data: {
profiles: [] as StudentProfile[], profiles: [] as StudentProfileView[],
filteredProfiles: [] as StudentProfile[], filteredProfiles: [] as StudentProfileView[],
query: '', query: '',
filterVisible: false, filterVisible: false,
filterGrade: '', filterGrade: '',
filterSubject: '', filterSubject: '',
filterCount: 0,
expandedProfileId: '',
recentFeedbackByProfile: {} as Record<string, FeedbackRecord[]>,
feedbackLoadingByProfile: {} as Record<string, boolean>,
feedbackErrorsByProfile: {} as Record<string, string>,
grades, grades,
subjects, subjects,
terms, terms,
@@ -101,7 +121,7 @@ Page({
async refreshProfiles(showSuccess = false) { async refreshProfiles(showSuccess = false) {
this.setData({ loading: true, errorMessage: '' }) this.setData({ loading: true, errorMessage: '' })
try { try {
const profiles = await listProfiles() const profiles = (await listProfiles()).map(withAcademicTermLabel)
const filteredProfiles = filterProfiles( const filteredProfiles = filterProfiles(
profiles, profiles,
this.data.query, this.data.query,
@@ -111,7 +131,11 @@ Page({
this.setData({ this.setData({
profiles, profiles,
filteredProfiles, filteredProfiles,
lastUpdatedText: getLastUpdatedText(profiles) lastUpdatedText: getLastUpdatedText(profiles),
expandedProfileId: '',
recentFeedbackByProfile: {},
feedbackLoadingByProfile: {},
feedbackErrorsByProfile: {}
}) })
if (showSuccess) wx.showToast({ title: '档案已刷新', icon: 'success' }) if (showSuccess) wx.showToast({ title: '档案已刷新', icon: 'success' })
} catch (error) { } catch (error) {
@@ -153,6 +177,7 @@ Page({
this.setData({ this.setData({
filterGrade, filterGrade,
filterSubject, filterSubject,
filterCount: Number(Boolean(filterGrade)) + Number(Boolean(filterSubject)),
filteredProfiles: filterProfiles(this.data.profiles, this.data.query, filterGrade, filterSubject) filteredProfiles: filterProfiles(this.data.profiles, this.data.query, filterGrade, filterSubject)
}) })
}, },
@@ -162,10 +187,67 @@ Page({
query: '', query: '',
filterGrade: '', filterGrade: '',
filterSubject: '', filterSubject: '',
filterCount: 0,
filteredProfiles: this.data.profiles filteredProfiles: this.data.profiles
}) })
}, },
toggleProfile(event: WechatMiniprogram.TouchEvent) {
const { id } = event.currentTarget.dataset as { id: string }
if (this.data.expandedProfileId === id) {
this.setData({ expandedProfileId: '' })
return
}
const hasFeedback = Object.prototype.hasOwnProperty.call(this.data.recentFeedbackByProfile, id)
this.setData({
expandedProfileId: id,
...(hasFeedback
? {}
: {
feedbackLoadingByProfile: { ...this.data.feedbackLoadingByProfile, [id]: true },
feedbackErrorsByProfile: { ...this.data.feedbackErrorsByProfile, [id]: '' }
})
})
if (!hasFeedback) void this.loadRecentFeedback(id)
},
async loadRecentFeedback(profileId: string) {
try {
const records = await listFeedbackRecords(profileId)
this.setData({
recentFeedbackByProfile: {
...this.data.recentFeedbackByProfile,
[profileId]: records.slice(0, 3)
}
})
} catch (error) {
this.setData({
feedbackErrorsByProfile: {
...this.data.feedbackErrorsByProfile,
[profileId]: getApiErrorMessage(error)
}
})
} finally {
this.setData({
feedbackLoadingByProfile: {
...this.data.feedbackLoadingByProfile,
[profileId]: false
}
})
}
},
retryRecentFeedback(event: WechatMiniprogram.TouchEvent) {
const { id } = event.currentTarget.dataset as { id: string }
if (this.data.feedbackLoadingByProfile[id]) return
this.setData({
feedbackLoadingByProfile: { ...this.data.feedbackLoadingByProfile, [id]: true },
feedbackErrorsByProfile: { ...this.data.feedbackErrorsByProfile, [id]: '' }
})
void this.loadRecentFeedback(id)
},
async openCreate() { async openCreate() {
this.setData({ this.setData({
sheetVisible: true, sheetVisible: true,
@@ -262,11 +344,13 @@ Page({
this.setData({ saving: true }) this.setData({ saving: true })
try { try {
const profile = await createProfile({ const profile = withAcademicTermLabel(
await createProfile({
...draft, ...draft,
name: draft.name.trim(), name: draft.name.trim(),
guardianTitle: draft.guardianTitle.trim() || `${draft.name.trim()}妈妈` guardianTitle: draft.guardianTitle.trim() || `${draft.name.trim()}妈妈`
}) })
)
const profiles = [profile, ...this.data.profiles] const profiles = [profile, ...this.data.profiles]
this.setData({ this.setData({
sheetVisible: false, sheetVisible: false,
@@ -284,25 +368,48 @@ Page({
} }
}, },
deleteProfile(event: WechatMiniprogram.TouchEvent) { openProfileActions(event: WechatMiniprogram.TouchEvent) {
const { id } = event.currentTarget.dataset as { id: string } const { id } = event.currentTarget.dataset as { id: string }
const profile = this.data.profiles.find((item) => item.id === id) const profile = this.data.profiles.find((item) => item.id === id)
if (!profile) return if (!profile) return
wx.showActionSheet({
itemList: ['删除学生档案'],
itemColor: '#D13F3A',
success: (result) => {
if (result.tapIndex === 0) this.confirmDeleteProfile(id)
}
})
},
confirmDeleteProfile(id: string) {
const profile = this.data.profiles.find((item) => item.id === id)
if (!profile) return
wx.showModal({ wx.showModal({
title: '删除学生档案', title: '删除学生档案',
content: `确认删除“${profile.name}”吗?`, content: `确认删除“${profile.name}”吗?`,
confirmColor: '#D14343', confirmColor: '#D13F3A',
success: async (result) => { success: async (result) => {
if (!result.confirm) return if (!result.confirm) return
this.setData({ deletingId: id }) this.setData({ deletingId: id })
try { try {
await deleteProfileRequest(id) await deleteProfileRequest(id)
const profiles = this.data.profiles.filter((item) => item.id !== id) const profiles = this.data.profiles.filter((item) => item.id !== id)
const recentFeedbackByProfile = { ...this.data.recentFeedbackByProfile }
const feedbackLoadingByProfile = { ...this.data.feedbackLoadingByProfile }
const feedbackErrorsByProfile = { ...this.data.feedbackErrorsByProfile }
delete recentFeedbackByProfile[id]
delete feedbackLoadingByProfile[id]
delete feedbackErrorsByProfile[id]
this.setData({ this.setData({
profiles, profiles,
filteredProfiles: filterProfiles(profiles, this.data.query, this.data.filterGrade, this.data.filterSubject), filteredProfiles: filterProfiles(profiles, this.data.query, this.data.filterGrade, this.data.filterSubject),
lastUpdatedText: getLastUpdatedText(profiles), lastUpdatedText: getLastUpdatedText(profiles),
expandedProfileId: this.data.expandedProfileId === id ? '' : this.data.expandedProfileId,
recentFeedbackByProfile,
feedbackLoadingByProfile,
feedbackErrorsByProfile,
errorMessage: '' errorMessage: ''
}) })
wx.showToast({ title: '已删除', icon: 'success' }) wx.showToast({ title: '已删除', icon: 'success' })

View File

@@ -1,7 +1,6 @@
<view class="page-shell"> <view class="page-shell">
<text class="eyebrow">个人档案</text> <text class="eyebrow">个人档案</text>
<view class="page-heading"> <view class="page-heading">
<text class="heading-icon">◎</text>
<text class="page-title">学生档案</text> <text class="page-title">学生档案</text>
</view> </view>
<text class="page-subtitle">管理学生默认信息,用于快速生成课堂反馈</text> <text class="page-subtitle">管理学生默认信息,用于快速生成课堂反馈</text>
@@ -13,7 +12,7 @@
<view class="search-row"> <view class="search-row">
<view class="search-box"> <view class="search-box">
<text class="search-icon">⌕</text> <icon class="search-icon" type="search" size="18" color="#6E6E73" />
<input <input
class="search-input" class="search-input"
placeholder="搜索学生姓名、年级、学科或称呼" placeholder="搜索学生姓名、年级、学科或称呼"
@@ -23,8 +22,13 @@
confirm-type="search" confirm-type="search"
/> />
</view> </view>
<button class="filter-button" bindtap="toggleFilter" aria-label="筛选学生档案"> <button class="filter-button {{filterVisible || filterCount ? 'filter-button-active' : ''}}" bindtap="toggleFilter" aria-label="筛选学生档案,已启用 {{filterCount}} 项">
<text>▽</text> <view class="filter-icon" aria-hidden="true">
<view class="filter-icon-line filter-icon-line-first"></view>
<view class="filter-icon-line filter-icon-line-second"></view>
<view class="filter-icon-line filter-icon-line-third"></view>
</view>
<text wx:if="{{filterCount}}" class="filter-count">{{filterCount}}</text>
</button> </button>
</view> </view>
@@ -45,13 +49,9 @@
</view> </view>
</view> </view>
<view class="archive-toolbar surface"> <view class="archive-toolbar">
<view class="toolbar-copy"> <view class="toolbar-copy">
<view class="toolbar-title-row"> <text class="toolbar-title">学生列表</text>
<text class="toolbar-icon">▣</text>
<text class="toolbar-title">档案卡片</text>
</view>
<text class="toolbar-description">点击卡片展开最近三条反馈</text>
<text class="toolbar-updated">上次更新:{{lastUpdatedText}}</text> <text class="toolbar-updated">上次更新:{{lastUpdatedText}}</text>
</view> </view>
<view class="toolbar-actions"> <view class="toolbar-actions">
@@ -78,23 +78,63 @@
<view wx:else class="profile-list"> <view wx:else class="profile-list">
<view wx:for="{{filteredProfiles}}" wx:key="id" class="profile-card surface"> <view wx:for="{{filteredProfiles}}" wx:key="id" class="profile-card surface">
<view
class="profile-summary"
data-id="{{item.id}}"
bindtap="toggleProfile"
aria-role="button"
aria-label="{{expandedProfileId === item.id ? '收起' : '展开'}}{{item.name}}的最近反馈"
aria-expanded="{{expandedProfileId === item.id}}"
>
<view class="profile-card-top"> <view class="profile-card-top">
<view> <view>
<text class="profile-name">{{item.name}}</text> <text class="profile-name">{{item.name}}</text>
<text class="profile-guardian">{{item.guardianTitle}}</text> <text class="profile-guardian">{{item.guardianTitle}}</text>
</view> </view>
<button class="delete-button" loading="{{deletingId === item.id}}" disabled="{{deletingId === item.id}}" data-id="{{item.id}}" catchtap="deleteProfile" aria-label="删除学生档案">删除</button> <view class="profile-card-actions">
<button
class="profile-menu-button"
disabled="{{deletingId === item.id}}"
data-id="{{item.id}}"
catchtap="openProfileActions"
aria-label="{{item.name}}的更多操作"
>
<text>{{deletingId === item.id ? '…' : '•••'}}</text>
</button>
<text class="profile-chevron {{expandedProfileId === item.id ? 'profile-chevron-expanded' : ''}}" aria-hidden="true"></text>
</view> </view>
</view>
<view class="profile-tags"> <view class="profile-tags">
<text class="profile-tag">{{item.grade}}</text> <text class="profile-tag">{{item.grade}}</text>
<text class="profile-tag">{{item.subject}}</text> <text class="profile-tag">{{item.subject}}</text>
<text class="profile-tag">{{item.academicYear}}{{item.term}}</text> <text class="profile-tag">{{item.academicTermLabel}}</text>
</view> </view>
<view class="profile-time-row"> <view class="profile-time-row">
<text class="profile-time-label">上课时间</text> <text class="profile-time-label">上课时间</text>
<text class="profile-time-value">{{item.startTime}} - {{item.endTime}}</text> <text class="profile-time-value">{{item.startTime}} - {{item.endTime}}</text>
</view> </view>
</view> </view>
<view wx:if="{{expandedProfileId === item.id}}" class="profile-feedback-panel">
<view class="profile-feedback-heading">
<text class="profile-feedback-title">最近反馈</text>
<text wx:if="{{recentFeedbackByProfile[item.id] && recentFeedbackByProfile[item.id].length}}" class="profile-feedback-count">{{recentFeedbackByProfile[item.id].length}} 条</text>
</view>
<view wx:if="{{feedbackLoadingByProfile[item.id]}}" class="profile-feedback-status">正在读取反馈...</view>
<view wx:elif="{{feedbackErrorsByProfile[item.id]}}" class="profile-feedback-status profile-feedback-error">
<text>{{feedbackErrorsByProfile[item.id]}}</text>
<text class="profile-feedback-retry" data-id="{{item.id}}" catchtap="retryRecentFeedback">重试</text>
</view>
<view wx:elif="{{!recentFeedbackByProfile[item.id] || recentFeedbackByProfile[item.id].length === 0}}" class="profile-feedback-status">暂无反馈记录</view>
<view wx:else class="recent-feedback-list">
<view wx:for="{{recentFeedbackByProfile[item.id]}}" wx:for-item="feedback" wx:key="id" class="recent-feedback-item">
<text class="recent-feedback-date">{{feedback.feedbackDate}}</text>
<text class="recent-feedback-content">{{feedback.content}}</text>
</view>
</view>
</view>
</view>
</view> </view>
</view> </view>
@@ -103,12 +143,14 @@
<view class="sheet-top"> <view class="sheet-top">
<view> <view>
<view class="sheet-title-row"> <view class="sheet-title-row">
<text class="sheet-title-icon">▣</text>
<text class="sheet-title">新增学生档案</text> <text class="sheet-title">新增学生档案</text>
</view> </view>
<text class="sheet-subtitle">保存后会同步到学生档案列表</text> <text class="sheet-subtitle">保存后会同步到学生档案列表</text>
</view> </view>
<view class="sheet-actions">
<text class="defaults-link" bindtap="goDefaults">设置预设项 </text> <text class="defaults-link" bindtap="goDefaults">设置预设项 </text>
<button class="sheet-close" bindtap="closeCreate" aria-label="关闭新增学生弹窗">×</button>
</view>
</view> </view>
<scroll-view class="sheet-scroll" scroll-y="true"> <scroll-view class="sheet-scroll" scroll-y="true">

View File

@@ -2,7 +2,7 @@
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) 88rpx; grid-template-columns: minmax(0, 1fr) 88rpx;
gap: 16rpx; gap: 16rpx;
margin-top: 42rpx; margin-top: 34rpx;
} }
.search-box { .search-box {
@@ -12,31 +12,29 @@
min-width: 0; min-width: 0;
height: 88rpx; height: 88rpx;
padding: 0 24rpx; padding: 0 24rpx;
border: 2rpx solid #cbdced; border: 1rpx solid transparent;
border-radius: 16rpx; border-radius: 16rpx;
background: #ffffff; background: #f1f3f6;
box-shadow: 0 4rpx 12rpx rgba(45, 78, 120, 0.05);
} }
.search-icon { .search-icon {
flex: none;
margin-right: 14rpx; margin-right: 14rpx;
color: #2b67e8;
font-size: 44rpx;
line-height: 1;
} }
.search-input { .search-input {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
color: #17233a; color: #1c1c1e;
font-size: 28rpx; font-size: 28rpx;
} }
.search-placeholder { .search-placeholder {
color: #9aa8b8; color: #8e8e93;
} }
.filter-button { .filter-button {
position: relative;
display: flex; display: flex;
flex: none; flex: none;
align-items: center; align-items: center;
@@ -47,12 +45,86 @@
height: 88rpx; height: 88rpx;
margin: 0; margin: 0;
padding: 0; padding: 0;
border: 2rpx solid #cbdced; border: 1rpx solid transparent;
border-radius: 16rpx; border-radius: 16rpx;
background: #ffffff; background: #f1f3f6;
color: #2b67e8; color: #6e6e73;
font-size: 44rpx;
line-height: 1; line-height: 1;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
}
.filter-button-active {
border-color: #b8d9ff;
background: #eaf3ff;
color: #007aff;
}
.filter-button:active {
transform: scale(0.96);
}
.filter-icon {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 34rpx;
height: 30rpx;
}
.filter-icon-line {
position: relative;
width: 34rpx;
height: 3rpx;
border-radius: 2rpx;
background: currentColor;
}
.filter-icon-line::after {
position: absolute;
top: -4rpx;
width: 10rpx;
height: 10rpx;
border: 3rpx solid currentColor;
border-radius: 50%;
background: #f1f3f6;
content: '';
}
.filter-button-active .filter-icon-line::after {
background: #eaf3ff;
}
.filter-icon-line-first::after,
.filter-icon-line-third::after {
left: 5rpx;
}
.filter-icon-line-second::after {
right: 5rpx;
}
.filter-count {
position: absolute;
top: 8rpx;
right: 8rpx;
display: flex;
align-items: center;
justify-content: center;
min-width: 28rpx;
height: 28rpx;
padding: 0 5rpx;
box-sizing: border-box;
border: 3rpx solid #f1f3f6;
border-radius: 14rpx;
background: #007aff;
color: #ffffff;
font-size: 18rpx;
font-weight: 700;
line-height: 1;
}
.filter-button-active .filter-count {
border-color: #eaf3ff;
} }
.filter-panel { .filter-panel {
@@ -68,20 +140,20 @@
} }
.filter-title { .filter-title {
color: #17233a; color: #1c1c1e;
font-size: 30rpx; font-size: 30rpx;
font-weight: 700; font-weight: 700;
} }
.filter-clear { .filter-clear {
color: #2b67e8; color: #007aff;
font-size: 26rpx; font-size: 26rpx;
} }
.filter-label { .filter-label {
display: block; display: block;
margin: 16rpx 0 12rpx; margin: 16rpx 0 12rpx;
color: #718096; color: #6e6e73;
font-size: 24rpx; font-size: 24rpx;
} }
@@ -93,17 +165,23 @@
.filter-chip { .filter-chip {
padding: 10rpx 18rpx; padding: 10rpx 18rpx;
border: 1rpx solid #d6e1ed; border: 1rpx solid #e5e5ea;
border-radius: 12rpx; border-radius: 10rpx;
color: #566577; background: #f1f3f6;
color: #3a3a3c;
font-size: 24rpx; font-size: 24rpx;
line-height: 1.2; line-height: 1.2;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
} }
.filter-chip-active { .filter-chip-active {
border-color: #2b67e8; border-color: #b8d9ff;
background: #eaf1ff; background: #eaf3ff;
color: #2b67e8; color: #007aff;
}
.filter-chip:active {
transform: scale(0.97);
} }
.archive-toolbar { .archive-toolbar {
@@ -111,8 +189,8 @@
grid-template-columns: minmax(0, 1fr) 264rpx; grid-template-columns: minmax(0, 1fr) 264rpx;
align-items: center; align-items: center;
gap: 20rpx; gap: 20rpx;
margin-top: 20rpx; margin-top: 22rpx;
padding: 28rpx; padding: 18rpx 0 4rpx;
} }
.toolbar-copy { .toolbar-copy {
@@ -120,36 +198,21 @@
min-width: 0; min-width: 0;
} }
.toolbar-title-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.toolbar-icon {
color: #2b67e8;
font-size: 34rpx;
}
.toolbar-title { .toolbar-title {
color: #17233a; display: block;
font-size: 34rpx; color: #1c1c1e;
font-size: 29rpx;
font-weight: 800; font-weight: 800;
} }
.toolbar-description,
.toolbar-updated { .toolbar-updated {
display: block; display: block;
margin-top: 8rpx; margin-top: 5rpx;
color: #7b8796; color: #6e6e73;
font-size: 24rpx; font-size: 22rpx;
line-height: 1.4; line-height: 1.4;
} }
.toolbar-updated {
font-weight: 600;
}
.toolbar-actions { .toolbar-actions {
display: grid; display: grid;
grid-template-columns: 62rpx 190rpx; grid-template-columns: 62rpx 190rpx;
@@ -162,18 +225,27 @@
.refresh-button { .refresh-button {
flex: none; flex: none;
display: flex;
align-items: center;
justify-content: center;
width: 62rpx; width: 62rpx;
min-width: 62rpx; min-width: 62rpx;
max-width: 62rpx; max-width: 62rpx;
height: 62rpx; height: 62rpx;
margin: 0; margin: 0;
padding: 0; padding: 0;
border: 2rpx solid #cbdced; border: 0;
border-radius: 12rpx; border-radius: 12rpx;
background: #ffffff; background: #f1f3f6;
color: #2b67e8; color: #007aff;
font-size: 36rpx; font-size: 36rpx;
line-height: 1; line-height: 1;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
}
.refresh-button:active {
transform: scale(0.94);
background: #e5e5ea;
} }
.new-student-button { .new-student-button {
@@ -187,13 +259,19 @@
height: 62rpx; height: 62rpx;
margin: 0; margin: 0;
padding: 0 18rpx; padding: 0 18rpx;
border: 2rpx solid #cbdced; border: 0;
border-radius: 12rpx; border-radius: 12rpx;
background: #ffffff; background: #007aff;
color: #2b67e8; color: #ffffff;
font-size: 26rpx; font-size: 26rpx;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
}
.new-student-button:active {
transform: scale(0.97);
background: #006ee6;
} }
.new-student-icon { .new-student-icon {
@@ -217,21 +295,21 @@
} }
.empty-icon { .empty-icon {
color: #2b67e8; color: #007aff;
font-size: 54rpx; font-size: 54rpx;
line-height: 1; line-height: 1;
} }
.empty-title { .empty-title {
margin-top: 18rpx; margin-top: 18rpx;
color: #17233a; color: #1c1c1e;
font-size: 36rpx; font-size: 36rpx;
font-weight: 800; font-weight: 800;
} }
.empty-description { .empty-description {
margin-top: 14rpx; margin-top: 14rpx;
color: #7b8796; color: #6e6e73;
font-size: 28rpx; font-size: 28rpx;
} }
@@ -253,7 +331,17 @@
} }
.profile-card { .profile-card {
overflow: hidden;
padding: 0;
}
.profile-summary {
padding: 24rpx; padding: 24rpx;
transition: opacity 140ms cubic-bezier(0.23, 1, 0.32, 1);
}
.profile-summary:active {
opacity: 0.72;
} }
.profile-card-top, .profile-card-top,
@@ -264,9 +352,16 @@
gap: 20rpx; gap: 20rpx;
} }
.profile-card-actions {
display: flex;
flex: none;
align-items: center;
gap: 4rpx;
}
.profile-name { .profile-name {
display: block; display: block;
color: #17233a; color: #1c1c1e;
font-size: 34rpx; font-size: 34rpx;
font-weight: 800; font-weight: 800;
} }
@@ -274,19 +369,46 @@
.profile-guardian { .profile-guardian {
display: block; display: block;
margin-top: 8rpx; margin-top: 8rpx;
color: #718096; color: #6e6e73;
font-size: 24rpx; font-size: 24rpx;
} }
.delete-button { .profile-menu-button {
display: flex;
align-items: center;
justify-content: center;
width: 56rpx;
min-width: 56rpx;
max-width: 56rpx;
height: 56rpx;
margin: 0; margin: 0;
padding: 8rpx 14rpx; padding: 0;
border: 1rpx solid #f2c6c6; border: 0;
border-radius: 10rpx; border-radius: 50%;
background: #ffffff; background: transparent;
color: #c34343; color: #6e6e73;
font-size: 22rpx; font-size: 25rpx;
line-height: 1.3; line-height: 1;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
}
.profile-menu-button:active {
transform: scale(0.94);
background: #f1f3f6;
}
.profile-chevron {
width: 24rpx;
color: #8e8e93;
font-size: 40rpx;
line-height: 1;
text-align: center;
transform: rotate(0deg);
transition: transform 180ms cubic-bezier(0.23, 1, 0.32, 1);
}
.profile-chevron-expanded {
transform: rotate(90deg);
} }
.profile-tags { .profile-tags {
@@ -299,8 +421,8 @@
.profile-tag { .profile-tag {
padding: 8rpx 12rpx; padding: 8rpx 12rpx;
border-radius: 8rpx; border-radius: 8rpx;
background: #eef4ff; background: #f1f3f6;
color: #3563b5; color: #3a3a3c;
font-size: 22rpx; font-size: 22rpx;
line-height: 1.2; line-height: 1.2;
} }
@@ -308,20 +430,99 @@
.profile-time-row { .profile-time-row {
margin-top: 20rpx; margin-top: 20rpx;
padding-top: 18rpx; padding-top: 18rpx;
border-top: 1rpx solid #e8eef5; border-top: 1rpx solid #e5e5ea;
} }
.profile-time-label { .profile-time-label {
color: #7b8796; color: #6e6e73;
font-size: 24rpx; font-size: 24rpx;
} }
.profile-time-value { .profile-time-value {
color: #17233a; color: #1c1c1e;
font-size: 26rpx; font-size: 26rpx;
font-weight: 700; font-weight: 700;
} }
.profile-feedback-panel {
padding: 20rpx 24rpx 24rpx;
border-top: 1rpx solid #e5e5ea;
background: #fbfcfe;
}
.profile-feedback-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.profile-feedback-title {
color: #1c1c1e;
font-size: 25rpx;
font-weight: 700;
}
.profile-feedback-count,
.profile-feedback-status {
color: #6e6e73;
font-size: 22rpx;
}
.profile-feedback-status {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
min-height: 64rpx;
padding-top: 14rpx;
}
.profile-feedback-error {
color: #d13f3a;
}
.profile-feedback-retry {
flex: none;
padding: 10rpx 0 10rpx 18rpx;
color: #007aff;
font-weight: 700;
}
.recent-feedback-list {
margin-top: 10rpx;
}
.recent-feedback-item {
padding: 14rpx 0;
}
.recent-feedback-item + .recent-feedback-item {
border-top: 1rpx solid #e5e5ea;
}
.recent-feedback-date,
.recent-feedback-content {
display: block;
}
.recent-feedback-date {
color: #6e6e73;
font-size: 21rpx;
}
.recent-feedback-content {
display: -webkit-box;
margin-top: 6rpx;
overflow: hidden;
color: #3a3a3c;
font-size: 24rpx;
line-height: 1.55;
word-break: break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.sheet-mask { .sheet-mask {
position: fixed; position: fixed;
z-index: 200; z-index: 200;
@@ -332,19 +533,22 @@
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
padding: 16rpx; padding: 16rpx;
background: rgba(17, 27, 50, 0.42); background: rgba(244, 247, 251, 0.82);
-webkit-backdrop-filter: blur(10rpx) saturate(110%);
backdrop-filter: blur(10rpx) saturate(110%);
} }
.create-sheet { .create-sheet {
width: 100%; width: 100%;
height: 90vh; height: auto;
max-height: 90vh; max-height: 88vh;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1rpx solid rgba(255, 255, 255, 0.72);
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(28, 28, 30, 0.24);
} }
.sheet-top { .sheet-top {
@@ -353,7 +557,9 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 16rpx; gap: 16rpx;
padding: 24rpx 24rpx 16rpx; padding: 24rpx 24rpx 18rpx;
border-bottom: 1rpx solid #e5e5ea;
background: #ffffff;
} }
.sheet-top > view { .sheet-top > view {
@@ -367,13 +573,8 @@
gap: 10rpx; gap: 10rpx;
} }
.sheet-title-icon {
color: #2b67e8;
font-size: 34rpx;
}
.sheet-title { .sheet-title {
color: #17233a; color: #1c1c1e;
font-size: 34rpx; font-size: 34rpx;
font-weight: 800; font-weight: 800;
} }
@@ -381,25 +582,58 @@
.sheet-subtitle { .sheet-subtitle {
display: block; display: block;
margin-top: 12rpx; margin-top: 12rpx;
color: #7b8796; color: #6e6e73;
font-size: 24rpx; font-size: 24rpx;
} }
.sheet-actions {
display: flex;
flex: none;
align-items: center;
gap: 12rpx;
}
.defaults-link { .defaults-link {
flex: none; flex: none;
padding: 10rpx 0 10rpx 12rpx; padding: 10rpx 0;
color: #2b67e8; color: #007aff;
font-size: 24rpx; font-size: 24rpx;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
white-space: nowrap; white-space: nowrap;
} }
.sheet-close {
display: flex;
flex: none;
align-items: center;
justify-content: center;
width: 56rpx;
min-width: 56rpx;
max-width: 56rpx;
height: 56rpx;
margin: 0;
padding: 0;
border: 0;
border-radius: 50%;
background: #f1f3f6;
color: #6e6e73;
font-size: 38rpx;
font-weight: 400;
line-height: 1;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
}
.sheet-close:active {
transform: scale(0.94);
background: #e5e5ea;
}
.sheet-scroll { .sheet-scroll {
flex: 1; flex: none;
height: 0; height: auto;
min-height: 0; min-height: 0;
max-height: none; max-height: calc(88vh - 236rpx);
width: 100%; width: 100%;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
@@ -407,7 +641,7 @@
.sheet-form { .sheet-form {
width: 100%; width: 100%;
padding: 0 24rpx 16rpx; padding: 16rpx 24rpx 16rpx;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -431,9 +665,9 @@ picker.form-field {
.form-field { .form-field {
min-height: 108rpx; min-height: 108rpx;
padding: 16rpx; padding: 16rpx;
border: 2rpx solid #d8e5f2; border: 1rpx solid #e5e5ea;
border-radius: 14rpx; border-radius: 14rpx;
background: #fbfdff; background: #f1f3f6;
} }
.full-field { .full-field {
@@ -442,7 +676,7 @@ picker.form-field {
.form-label { .form-label {
display: block; display: block;
color: #2b67e8; color: #6e6e73;
font-size: 24rpx; font-size: 24rpx;
font-weight: 700; font-weight: 700;
} }
@@ -450,7 +684,7 @@ picker.form-field {
.form-value { .form-value {
display: block; display: block;
margin-top: 14rpx; margin-top: 14rpx;
color: #17233a; color: #1c1c1e;
font-size: 28rpx; font-size: 28rpx;
font-weight: 700; font-weight: 700;
} }
@@ -459,7 +693,7 @@ picker.form-field {
width: 100%; width: 100%;
height: 48rpx; height: 48rpx;
margin-top: 10rpx; margin-top: 10rpx;
color: #17233a; color: #1c1c1e;
font-size: 28rpx; font-size: 28rpx;
font-weight: 600; font-weight: 600;
} }
@@ -477,10 +711,45 @@ picker.form-field {
display: flex; display: flex;
gap: 16rpx; gap: 16rpx;
padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom)); padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #e7eef6; border-top: 1rpx solid #e5e5ea;
background: #ffffff;
} }
.sheet-cancel, .sheet-cancel,
.sheet-save { .sheet-save {
flex: 1; flex: 1;
} }
@media (prefers-reduced-motion: reduce) {
.filter-button,
.filter-chip,
.refresh-button,
.new-student-button,
.profile-summary,
.profile-menu-button,
.profile-chevron,
.sheet-close {
transition: none;
}
.filter-button:active,
.filter-chip:active,
.refresh-button:active,
.new-student-button:active,
.profile-menu-button:active,
.sheet-close:active {
transform: none;
}
.profile-summary:active {
opacity: 1;
}
}
@media (prefers-reduced-transparency: reduce) {
.sheet-mask {
background: rgba(244, 247, 251, 0.96);
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
}