Emit correlated JSON logs from the API and FunASR services, propagate request IDs to the client, and configure bounded opt-in Docker logging. Document the host-wide Grafana/Loki query workflow and keep observability deployment ownership outside the application repository.
215 lines
10 KiB
Plaintext
215 lines
10 KiB
Plaintext
<view class="page-shell">
|
||
<text class="eyebrow">个人档案</text>
|
||
<view class="page-heading">
|
||
<text class="page-title">学生档案</text>
|
||
</view>
|
||
<text class="page-subtitle">管理学生默认信息,用于快速生成课堂反馈</text>
|
||
|
||
<view wx:if="{{errorMessage}}" class="status-banner">
|
||
<view class="status-copy">
|
||
<text>{{errorMessage}}</text>
|
||
<view wx:if="{{errorRequestId}}" class="status-request-id">
|
||
<text>请求编号:{{errorRequestId}}</text>
|
||
<text class="status-action" bindtap="copyRequestId">复制</text>
|
||
</view>
|
||
</view>
|
||
<text class="status-action" bindtap="retryLoad">重试</text>
|
||
</view>
|
||
|
||
<view class="search-row">
|
||
<view class="search-box">
|
||
<icon class="search-icon" type="search" size="18" color="#6E6E73" />
|
||
<input
|
||
class="search-input"
|
||
placeholder="搜索学生姓名、年级、学科或称呼"
|
||
placeholder-class="search-placeholder"
|
||
value="{{query}}"
|
||
bindinput="onSearch"
|
||
confirm-type="search"
|
||
/>
|
||
</view>
|
||
<button class="filter-button {{filterVisible || filterCount ? 'filter-button-active' : ''}}" bindtap="toggleFilter" aria-label="筛选学生档案,已启用 {{filterCount}} 项">
|
||
<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>
|
||
</view>
|
||
|
||
<view wx:if="{{filterVisible}}" class="filter-panel surface">
|
||
<view class="filter-header">
|
||
<text class="filter-title">筛选学生档案</text>
|
||
<text class="filter-clear" bindtap="clearFilters">清除</text>
|
||
</view>
|
||
<text class="filter-label">年级</text>
|
||
<view class="filter-options">
|
||
<text class="filter-chip {{!filterGrade ? 'filter-chip-active' : ''}}" data-type="grade" data-value="all" bindtap="selectFilter">全部</text>
|
||
<text wx:for="{{grades}}" wx:key="*this" class="filter-chip {{filterGrade === item ? 'filter-chip-active' : ''}}" data-type="grade" data-value="{{item}}" bindtap="selectFilter">{{item}}</text>
|
||
</view>
|
||
<text class="filter-label">学科</text>
|
||
<view class="filter-options">
|
||
<text class="filter-chip {{!filterSubject ? 'filter-chip-active' : ''}}" data-type="subject" data-value="all" bindtap="selectFilter">全部</text>
|
||
<text wx:for="{{subjects}}" wx:key="*this" class="filter-chip {{filterSubject === item ? 'filter-chip-active' : ''}}" data-type="subject" data-value="{{item}}" bindtap="selectFilter">{{item}}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="archive-toolbar">
|
||
<view class="toolbar-copy">
|
||
<text class="toolbar-title">学生列表</text>
|
||
<text class="toolbar-updated">上次更新:{{lastUpdatedText}}</text>
|
||
</view>
|
||
<view class="toolbar-actions">
|
||
<button class="refresh-button" loading="{{loading}}" disabled="{{loading}}" bindtap="onRefresh" aria-label="刷新学生档案">↻</button>
|
||
<button class="new-student-button" bindtap="openCreate"><text class="new-student-icon">+</text>新增学生</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view wx:if="{{loading && profiles.length === 0}}" class="loading-state">正在从服务端读取学生档案...</view>
|
||
|
||
<view wx:elif="{{profiles.length === 0}}" class="empty-state">
|
||
<text class="empty-icon">◎</text>
|
||
<text class="empty-title">暂无学生档案</text>
|
||
<text class="empty-description">请先创建学生档案</text>
|
||
<button class="create-empty-button primary-button" bindtap="openCreate"><text class="button-icon">+</text>创建学生档案</button>
|
||
</view>
|
||
|
||
<view wx:elif="{{filteredProfiles.length === 0}}" class="empty-state compact-empty">
|
||
<text class="empty-icon">⌕</text>
|
||
<text class="empty-title">没有匹配的档案</text>
|
||
<text class="empty-description">试试清除关键词或筛选条件</text>
|
||
<button class="clear-search-button secondary-button" bindtap="clearFilters">清除搜索与筛选</button>
|
||
</view>
|
||
|
||
<view wx:else class="profile-list">
|
||
<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>
|
||
<text class="profile-name">{{item.name}}</text>
|
||
<text class="profile-guardian">{{item.guardianTitle}}</text>
|
||
</view>
|
||
<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 class="profile-tags">
|
||
<text class="profile-tag">{{item.grade}}</text>
|
||
<text class="profile-tag">{{item.subject}}</text>
|
||
<text class="profile-tag">{{item.academicTermLabel}}</text>
|
||
</view>
|
||
<view class="profile-time-row">
|
||
<text class="profile-time-label">上课时间</text>
|
||
<text class="profile-time-value">{{item.startTime}} - {{item.endTime}}</text>
|
||
</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 wx:if="{{sheetVisible}}" class="sheet-mask" bindtap="closeCreate">
|
||
<view class="create-sheet" catchtap="noop">
|
||
<view class="sheet-top">
|
||
<view>
|
||
<view class="sheet-title-row">
|
||
<text class="sheet-title">新增学生档案</text>
|
||
</view>
|
||
<text class="sheet-subtitle">保存后会同步到学生档案列表</text>
|
||
</view>
|
||
<view class="sheet-actions">
|
||
<text class="defaults-link" bindtap="goDefaults">设置预设项 ›</text>
|
||
<button class="sheet-close" bindtap="closeCreate" aria-label="关闭新增学生弹窗">×</button>
|
||
</view>
|
||
</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" />
|
||
</view>
|
||
|
||
<view class="form-grid">
|
||
<picker class="form-field" mode="selector" range="{{grades}}" bindchange="onGradeChange">
|
||
<text class="form-label">年级</text>
|
||
<text class="form-value">{{draft.grade}}</text>
|
||
</picker>
|
||
<picker class="form-field" mode="selector" range="{{subjects}}" bindchange="onSubjectChange">
|
||
<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>
|
||
</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">
|
||
<text class="form-label">默认家长称呼</text>
|
||
<input class="form-input" placeholder="默认:学生姓名 + 妈妈" value="{{draft.guardianTitle}}" bindinput="onGuardianInput" maxlength="24" />
|
||
</view>
|
||
|
||
<view class="form-grid bottom-grid">
|
||
<picker class="form-field" mode="time" value="{{draft.startTime}}" bindchange="onStartTimeChange">
|
||
<text class="form-label">开始时间</text>
|
||
<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>
|
||
</scroll-view>
|
||
|
||
<view class="sheet-footer">
|
||
<button class="sheet-cancel secondary-button" bindtap="closeCreate">取消</button>
|
||
<button class="sheet-save primary-button" loading="{{saving}}" disabled="{{saving}}" bindtap="saveStudent">保存</button>
|
||
</view>
|
||
</view>
|
||
</view>
|