修复小程序端的BUG

This commit is contained in:
taiyi 2026-05-13 15:42:18 +08:00
parent 6ec4b114da
commit c64a701b10
9 changed files with 319 additions and 119 deletions

View File

@ -1,4 +1,4 @@
const app = getApp() const appContext = require('../../utils/app-context.js')
Page({ Page({
data: { data: {
@ -10,18 +10,31 @@ Page({
this.loadStats() this.loadStats()
}, },
async loadStats() { async loadStats() {
const app = appContext.withApp()
this.setData({ loading: true, error: '' }) this.setData({ loading: true, error: '' })
await app.ensureLogin() await appContext.ensureLogin()
if (!app.globalData.authToken) { if (!app.globalData.authToken) {
this.setData({ error: '登录失败,请稍后重试', loading: false }) this.setData({ error: '请先登录后查看首页数据', loading: false })
return return
} }
wx.request({ wx.request({
url: `${app.globalData.baseUrl}/api/v1/home/stats`, url: `${app.globalData.baseUrl}/api/v1/home/stats`,
header: { 'X-Auth-Token': app.globalData.authToken }, header: { 'X-Auth-Token': app.globalData.authToken },
success: (res) => { success: (res) => {
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
const payload = res.data?.data const payload = res.data?.data
if (payload) this.setData({ stats: payload }) const ok = res.statusCode >= 200 && res.statusCode < 300 && res.data?.code === 0 && payload
if (!ok) {
this.setData({ error: res.data?.detail || res.data?.message || '首页数据加载失败' })
return
}
this.setData({ stats: payload })
}, },
fail: () => this.setData({ error: '首页数据加载失败' }), fail: () => this.setData({ error: '首页数据加载失败' }),
complete: () => this.setData({ loading: false }), complete: () => this.setData({ loading: false }),

View File

@ -1,17 +1,22 @@
<view class="page"> <view class="page">
<scroll-view scroll-y class="content"> <scroll-view scroll-y class="content">
<view class="greeting"> <view class="greeting">
<text class="title">你好,同学</text> <text class="title">你好,同学</text>
<text class="subtitle">专注高频复习,持续积累词库</text> <text class="subtitle">专注高频复习,持续积累你的词库与掌握度</text>
</view> </view>
<view wx:if="{{!stats}}" class="empty-state"> <view wx:if="{{loading}}" class="empty-state">
<text class="empty-title">加载中...</text> <text class="empty-title">加载中...</text>
<text class="empty-subtitle">正在同步你的学习数据</text> <text class="empty-subtitle">正在同步你的学习数据</text>
</view> </view>
<view wx:elif="{{error}}" class="empty-state">
<text class="empty-title">暂时无法加载首页</text>
<text class="empty-subtitle">{{error}}</text>
</view>
<view wx:elif="{{stats.review_count === 0 && stats.total_entries === 0}}" class="empty-state"> <view wx:elif="{{stats.review_count === 0 && stats.total_entries === 0}}" class="empty-state">
<text class="empty-title">还没有词库</text> <text class="empty-title">还没有词库内容</text>
<text class="empty-subtitle">先去“我的”里上传一些单词或句子吧</text> <text class="empty-subtitle">先去“我的”里上传一些单词或句子吧</text>
</view> </view>
@ -23,7 +28,7 @@
<text class="hero-number">{{stats.review_count}}</text> <text class="hero-number">{{stats.review_count}}</text>
<text class="label">今日待复习</text> <text class="label">今日待复习</text>
</view> </view>
<view class="hero-icon">🧠</view> <view class="hero-icon">📘</view>
</view> </view>
<view class="hero-grid"> <view class="hero-grid">
<view class="metric-card"><text class="metric-value">{{stats.total_entries}}</text><text class="label small">词库总数</text></view> <view class="metric-card"><text class="metric-value">{{stats.total_entries}}</text><text class="label small">词库总数</text></view>
@ -51,7 +56,7 @@
<view class="bottom-nav"> <view class="bottom-nav">
<view class="nav-item active" bindtap="goHome"><text class="nav-icon">⌂</text><text class="nav-label">首页</text></view> <view class="nav-item active" bindtap="goHome"><text class="nav-icon">⌂</text><text class="nav-label">首页</text></view>
<view class="nav-item" bindtap="goToReview"><text class="nav-icon">📖</text><text class="nav-label">复习</text></view> <view class="nav-item" bindtap="goToReview"><text class="nav-icon"></text><text class="nav-label">复习</text></view>
<view class="nav-item" bindtap="goToMe"><text class="nav-icon">☺</text><text class="nav-label">我的</text></view> <view class="nav-item" bindtap="goToMe"><text class="nav-icon">☺</text><text class="nav-label">我的</text></view>
</view> </view>
</view> </view>

141
me/me.js
View File

@ -1,4 +1,4 @@
const app = getApp() const appContext = require('../../utils/app-context.js')
function normalizeAvatarUrl(avatarUrl, baseUrl) { function normalizeAvatarUrl(avatarUrl, baseUrl) {
if (!avatarUrl) return '' if (!avatarUrl) return ''
@ -6,13 +6,27 @@ function normalizeAvatarUrl(avatarUrl, baseUrl) {
} }
function requestFailHint(errMsg) { function requestFailHint(errMsg) {
const m = String(errMsg || '').toLowerCase() const message = String(errMsg || '').toLowerCase()
if (m.includes('domain')) return '域名未配置或未开启 HTTPS' if (message.includes('domain')) return '域名未配置或未开启 HTTPS'
if (m.includes('ssl') || m.includes('certificate')) return 'HTTPS 证书异常,请检查服务端配置' if (message.includes('ssl') || message.includes('certificate')) return 'HTTPS 证书异常,请检查服务端配置'
if (m.includes('timeout') || m.includes('timed out')) return '连接超时,请稍后重试' if (message.includes('timeout') || message.includes('timed out')) return '连接超时,请稍后重试'
return '网络异常,请稍后重试' return '网络异常,请稍后重试'
} }
function buildLoggedOutState() {
return {
loading: false,
name: '未登录',
nickname: '未登录',
username: '',
avatarUrl: '',
nicknameDraft: '',
badges: 0,
days: 0,
error: '',
}
}
const DEFAULT_AVATAR = 'data:image/svg+xml;utf8,' + encodeURIComponent(` const DEFAULT_AVATAR = 'data:image/svg+xml;utf8,' + encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320" viewBox="0 0 320 320"> <svg xmlns="http://www.w3.org/2000/svg" width="320" height="320" viewBox="0 0 320 320">
<defs> <defs>
@ -52,6 +66,7 @@ Page({
this.loadProfile() this.loadProfile()
}, },
isLoggedIn() { isLoggedIn() {
const app = appContext.withApp()
return !!app.globalData.authToken return !!app.globalData.authToken
}, },
onAuthModeChange(e) { onAuthModeChange(e) {
@ -63,7 +78,8 @@ Page({
this.setData({ [`authForm.${field}`]: value }) this.setData({ [`authForm.${field}`]: value })
}, },
submitAuth() { submitAuth() {
const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl const app = appContext.withApp()
const baseUrl = appContext.getApiBaseUrl()
const mode = this.data.authMode const mode = this.data.authMode
const username = (this.data.authForm.username || '').trim() const username = (this.data.authForm.username || '').trim()
const password = (this.data.authForm.password || '').trim() const password = (this.data.authForm.password || '').trim()
@ -87,6 +103,10 @@ Page({
data: mode === 'register' ? { username, password, nickname } : { username, password }, data: mode === 'register' ? { username, password, nickname } : { username, password },
success: (res) => { success: (res) => {
const payload = res.data?.data || null const payload = res.data?.data || null
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
if (res.statusCode < 200 || res.statusCode >= 300 || !payload?.auth_token) { if (res.statusCode < 200 || res.statusCode >= 300 || !payload?.auth_token) {
const message = res.data?.detail || res.data?.message || `${mode === 'register' ? '注册' : '登录'}失败` const message = res.data?.detail || res.data?.message || `${mode === 'register' ? '注册' : '登录'}失败`
this.setData({ error: message }) this.setData({ error: message })
@ -94,10 +114,11 @@ Page({
return return
} }
app.setLoginSession(payload) appContext.setLoginSession(payload)
this.setData({ this.setData({
authForm: { username: '', password: '', nickname: '' }, authForm: { username: '', password: '', nickname: '' },
username: payload.username || '', username: payload.username || '',
error: '',
}) })
wx.showToast({ title: mode === 'register' ? '注册成功' : '登录成功', icon: 'success' }) wx.showToast({ title: mode === 'register' ? '注册成功' : '登录成功', icon: 'success' })
this.loadProfile() this.loadProfile()
@ -111,30 +132,26 @@ Page({
}) })
}, },
loadProfile() { loadProfile() {
const token = app.ensureLogin() const app = appContext.withApp()
const token = appContext.ensureLogin()
Promise.resolve(token).then((authToken) => { Promise.resolve(token).then((authToken) => {
if (!authToken) { if (!authToken) {
this.setData({ this.setData(buildLoggedOutState())
loading: false,
name: '未登录',
nickname: '未登录',
username: '',
avatarUrl: '',
nicknameDraft: '',
badges: 0,
days: 0,
error: '',
})
return return
} }
this.setData({ loading: true, error: '' }) this.setData({ loading: true, error: '' })
const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl const baseUrl = appContext.getApiBaseUrl()
wx.request({ wx.request({
url: `${baseUrl}/api/v1/auth/profile`, url: `${baseUrl}/api/v1/auth/profile`,
method: 'GET', method: 'GET',
header: app.getAuthHeader(), header: appContext.getAuthHeader(),
success: (res) => { success: (res) => {
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
const body = res.data || {} const body = res.data || {}
const profile = body.data const profile = body.data
const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0 && profile const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0 && profile
@ -189,19 +206,34 @@ Page({
this.setData({ nicknameDraft: e.detail.value }) this.setData({ nicknameDraft: e.detail.value })
}, },
saveNickname() { saveNickname() {
const app = appContext.withApp()
if (!this.isLoggedIn()) return if (!this.isLoggedIn()) return
const nickname = (this.data.nicknameDraft || '').trim() const nickname = (this.data.nicknameDraft || '').trim()
if (!nickname) { if (!nickname) {
wx.showToast({ title: '请输入昵称', icon: 'none' }) wx.showToast({ title: '请输入昵称', icon: 'none' })
return return
} }
wx.request({ wx.request({
url: `${app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl}/api/v1/auth/profile`, url: `${appContext.getApiBaseUrl()}/api/v1/auth/profile`,
method: 'PUT', method: 'PUT',
header: app.getAuthHeader({ 'Content-Type': 'application/json' }), header: appContext.getAuthHeader({ 'Content-Type': 'application/json' }),
data: { nickname }, data: { nickname },
success: (res) => { success: (res) => {
const profile = res.data?.data || {} if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
const body = res.data || {}
const profile = body.data || {}
const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0
if (!ok) {
wx.showToast({ title: body.detail || body.message || '保存失败', icon: 'none' })
return
}
const nextNickname = profile.nickname || nickname const nextNickname = profile.nickname || nickname
const currentUser = { ...(app.globalData.currentUser || {}), nickname: nextNickname } const currentUser = { ...(app.globalData.currentUser || {}), nickname: nextNickname }
app.globalData.currentUser = currentUser app.globalData.currentUser = currentUser
@ -209,10 +241,11 @@ Page({
this.setData({ nickname: nextNickname, name: nextNickname, nicknameDraft: nextNickname, showNicknameModal: false }) this.setData({ nickname: nextNickname, name: nextNickname, nicknameDraft: nextNickname, showNicknameModal: false })
wx.showToast({ title: '保存成功', icon: 'success' }) wx.showToast({ title: '保存成功', icon: 'success' })
}, },
fail: () => wx.showToast({ title: '保存失败', icon: 'none' }), fail: (err) => wx.showToast({ title: requestFailHint(err.errMsg), icon: 'none' }),
}) })
}, },
onChooseAvatar(e) { onChooseAvatar(e) {
const app = appContext.withApp()
const tempPath = e.detail?.avatarUrl || '' const tempPath = e.detail?.avatarUrl || ''
if (!this.isLoggedIn()) { if (!this.isLoggedIn()) {
wx.showToast({ title: '请先登录', icon: 'none' }) wx.showToast({ title: '请先登录', icon: 'none' })
@ -222,13 +255,19 @@ Page({
wx.showToast({ title: '未选择头像', icon: 'none' }) wx.showToast({ title: '未选择头像', icon: 'none' })
return return
} }
const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl
const baseUrl = appContext.getApiBaseUrl()
wx.uploadFile({ wx.uploadFile({
url: `${baseUrl}/api/v1/auth/avatar`, url: `${baseUrl}/api/v1/auth/avatar`,
filePath: tempPath, filePath: tempPath,
name: 'file', name: 'file',
header: app.getAuthHeader(), header: appContext.getAuthHeader(),
success: (uploadRes) => { success: (uploadRes) => {
if (Number(uploadRes.statusCode) === 401) {
appContext.handleUnauthorized()
return
}
try { try {
const payload = JSON.parse(uploadRes.data || '{}') const payload = JSON.parse(uploadRes.data || '{}')
const ok = uploadRes.statusCode >= 200 && uploadRes.statusCode < 300 && payload.code === 0 && payload.data const ok = uploadRes.statusCode >= 200 && uploadRes.statusCode < 300 && payload.code === 0 && payload.data
@ -236,6 +275,7 @@ Page({
wx.showToast({ title: payload.message || '头像上传失败', icon: 'none' }) wx.showToast({ title: payload.message || '头像上传失败', icon: 'none' })
return return
} }
const rel = payload.data.avatar_url || '' const rel = payload.data.avatar_url || ''
const full = normalizeAvatarUrl(rel, baseUrl) const full = normalizeAvatarUrl(rel, baseUrl)
const key = this.data.username || app.globalData.currentUser?.username || '' const key = this.data.username || app.globalData.currentUser?.username || ''
@ -251,39 +291,50 @@ Page({
}, },
loadAchievements() { loadAchievements() {
if (!this.isLoggedIn()) return if (!this.isLoggedIn()) return
const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl
wx.request({ wx.request({
url: `${baseUrl}/api/v1/achievements`, url: `${appContext.getApiBaseUrl()}/api/v1/achievements`,
header: app.getAuthHeader(), header: appContext.getAuthHeader(),
success: (res) => { success: (res) => {
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
const body = res.data || {} const body = res.data || {}
const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0 && Array.isArray(body.data) const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0 && Array.isArray(body.data)
if (!ok) return if (!ok) return
const badges = body.data.filter((i) => i.is_unlocked).length const badges = body.data.filter((item) => item.is_unlocked).length
this.setData({ badges }) this.setData({ badges })
}, },
}) })
}, },
logout() { logout() {
if (!this.isLoggedIn()) return if (!this.isLoggedIn()) return
wx.request({ wx.request({
url: `${app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl}/api/v1/auth/logout`, url: `${appContext.getApiBaseUrl()}/api/v1/auth/logout`,
method: 'POST', method: 'POST',
header: app.getAuthHeader(), header: appContext.getAuthHeader(),
complete: () => { success: (res) => {
app.clearLoginSession() if (appContext.isUnauthorizedResponse(res)) {
this.setData({ appContext.handleUnauthorized({ title: '登录状态已失效,请重新登录' })
name: '未登录', return
nickname: '未登录', }
username: '',
avatarUrl: '', const ok = res.statusCode >= 200 && res.statusCode < 300 && res.data?.code === 0
nicknameDraft: '', if (!ok) {
badges: 0, wx.showToast({ title: res.data?.detail || res.data?.message || '退出登录失败', icon: 'none' })
days: 0, return
error: '', }
})
appContext.clearLoginSession()
this.setData(buildLoggedOutState())
wx.showToast({ title: '已退出登录', icon: 'success' }) wx.showToast({ title: '已退出登录', icon: 'success' })
}, },
fail: (err) => {
wx.showToast({ title: requestFailHint(err.errMsg), icon: 'none' })
},
}) })
}, },
goTo(e) { goTo(e) {

View File

@ -1,4 +1,4 @@
const app = getApp() const appContext = require('../../utils/app-context.js')
Page({ Page({
data: { data: {
@ -16,6 +16,7 @@ Page({
error: '', error: '',
}, },
onLoad() { onLoad() {
const app = appContext.withApp()
const plan = app.globalData.reviewPlan const plan = app.globalData.reviewPlan
this.setData({ this.setData({
activeTimeRange: plan.timeRangeIndex, activeTimeRange: plan.timeRangeIndex,
@ -28,6 +29,7 @@ Page({
}) })
}, },
persistPatch(patch) { persistPatch(patch) {
const app = appContext.withApp()
app.globalData.reviewPlan = { ...app.globalData.reviewPlan, ...patch } app.globalData.reviewPlan = { ...app.globalData.reviewPlan, ...patch }
}, },
setTimeRange(e) { setTimeRange(e) {
@ -61,6 +63,7 @@ Page({
this.persistPatch({ questionCount }) this.persistPatch({ questionCount })
}, },
startReview() { startReview() {
const app = appContext.withApp()
const scopeTypeMap = ['today', 'week', 'month'] const scopeTypeMap = ['today', 'week', 'month']
const contentTypeMap = ['word', 'sentence'] const contentTypeMap = ['word', 'sentence']
const questionModeMap = ['A', 'B', 'C'] const questionModeMap = ['A', 'B', 'C']
@ -68,12 +71,14 @@ Page({
const content_type = contentTypeMap[this.data.activeContentType] const content_type = contentTypeMap[this.data.activeContentType]
const question_mode = questionModeMap[this.data.activeMode] const question_mode = questionModeMap[this.data.activeMode]
const scope_config = {} const scope_config = {}
this.setData({ loading: true, error: '' }) this.setData({ loading: true, error: '' })
app.ensureLogin().then((authToken) => { appContext.ensureLogin().then((authToken) => {
if (!authToken) { if (!authToken) {
this.setData({ loading: false, error: '请先登录后再开始复习' }) this.setData({ loading: false, error: '请先登录后再开始复习' })
return return
} }
wx.request({ wx.request({
url: `${app.globalData.baseUrl}/api/v1/review/generate`, url: `${app.globalData.baseUrl}/api/v1/review/generate`,
method: 'POST', method: 'POST',
@ -88,6 +93,11 @@ Page({
total_count: this.data.questionCount, total_count: this.data.questionCount,
}, },
success: (res) => { success: (res) => {
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
const session = res.data?.data || null const session = res.data?.data || null
if (res.statusCode < 200 || res.statusCode >= 300 || !session?.id) { if (res.statusCode < 200 || res.statusCode >= 300 || !session?.id) {
const message = res.data?.detail || res.data?.message || '生成复习会话失败' const message = res.data?.detail || res.data?.message || '生成复习会话失败'
@ -95,12 +105,14 @@ Page({
wx.showToast({ title: message, icon: 'none' }) wx.showToast({ title: message, icon: 'none' })
return return
} }
if (!session.record_ids?.length) { if (!session.record_ids?.length) {
const message = '当前条件下没有可复习题目,请切换时间范围或关闭仅复习' const message = '当前条件下没有可复习题目,请切换时间范围或关闭复习'
this.setData({ error: message }) this.setData({ error: message })
wx.showToast({ title: message, icon: 'none' }) wx.showToast({ title: message, icon: 'none' })
return return
} }
app.globalData.reviewSessionMeta = session app.globalData.reviewSessionMeta = session
wx.setStorageSync('reviewSessionMeta', session) wx.setStorageSync('reviewSessionMeta', session)
wx.navigateTo({ url: '/pages/review-session/review-session' }) wx.navigateTo({ url: '/pages/review-session/review-session' })

View File

@ -2,7 +2,7 @@
<scroll-view scroll-y class="content config-content"> <scroll-view scroll-y class="content config-content">
<view class="heading"> <view class="heading">
<text class="title">配置复习计划</text> <text class="title">配置复习计划</text>
<text class="subtitle">定制你的复习范围、题型和题量</text> <text class="subtitle">定制你的复习范围、题型和题量</text>
</view> </view>
<view class="config-card"> <view class="config-card">
@ -20,7 +20,7 @@
</view> </view>
<view class="config-card"> <view class="config-card">
<text class="section-title">练模式</text> <text class="section-title">练模式</text>
<view class="mode-list"> <view class="mode-list">
<view wx:for="{{modes}}" wx:key="index" data-index="{{index}}" bindtap="setMode" class="mode-item {{index===activeMode?'active':''}}">{{item}}</view> <view wx:for="{{modes}}" wx:key="index" data-index="{{index}}" bindtap="setMode" class="mode-item {{index===activeMode?'active':''}}">{{item}}</view>
</view> </view>
@ -34,12 +34,16 @@
<slider min="5" max="100" step="5" value="{{questionCount}}" activeColor="#c19cff" backgroundColor="#23262c" bindchanging="onQuestionCountInput" bindchange="onQuestionCountInput" /> <slider min="5" max="100" step="5" value="{{questionCount}}" activeColor="#c19cff" backgroundColor="#23262c" bindchanging="onQuestionCountInput" bindchange="onQuestionCountInput" />
</view> </view>
<button class="start-btn" bindtap="startReview">开始复习</button> <view wx:if="{{error}}" class="config-card">
<text class="subtitle">{{error}}</text>
</view>
<button class="start-btn" loading="{{loading}}" bindtap="startReview">开始复习</button>
</scroll-view> </scroll-view>
<view class="bottom-nav"> <view class="bottom-nav">
<view class="nav-item" bindtap="goHome"><text class="nav-icon">⌂</text><text class="nav-label">首页</text></view> <view class="nav-item" bindtap="goHome"><text class="nav-icon">⌂</text><text class="nav-label">首页</text></view>
<view class="nav-item active"><text class="nav-icon">📖</text><text class="nav-label">复习</text></view> <view class="nav-item active"><text class="nav-icon"></text><text class="nav-label">复习</text></view>
<view class="nav-item" bindtap="goToMe"><text class="nav-icon">☺</text><text class="nav-label">我的</text></view> <view class="nav-item" bindtap="goToMe"><text class="nav-icon">☺</text><text class="nav-label">我的</text></view>
</view> </view>
</view> </view>

View File

@ -1,4 +1,4 @@
const app = getApp() const appContext = require('../../utils/app-context.js')
Page({ Page({
data: { data: {
@ -17,8 +17,10 @@ Page({
this.loadResult() this.loadResult()
}, },
loadResult() { loadResult() {
const sessionId = app.globalData.reviewSessionMeta?.id const app = appContext.withApp()
const sessionId = app.globalData.reviewSessionMeta?.id || wx.getStorageSync('reviewSessionMeta')?.id
const fallbackSession = app.globalData.reviewSession const fallbackSession = app.globalData.reviewSession
if (!sessionId) { if (!sessionId) {
if (fallbackSession?.completed) { if (fallbackSession?.completed) {
this.setData({ error: '本地复习已完成,但结果会话信息已丢失' }) this.setData({ error: '本地复习已完成,但结果会话信息已丢失' })
@ -27,11 +29,23 @@ Page({
} }
return return
} }
this.setData({ loading: true, error: '' }) this.setData({ loading: true, error: '' })
wx.request({ wx.request({
url: `${app.globalData.baseUrl}/api/v1/review-result?session_id=${sessionId}`, url: `${app.globalData.baseUrl}/api/v1/review-result?session_id=${sessionId}`,
header: { 'X-Auth-Token': app.globalData.authToken || '' }, header: { 'X-Auth-Token': app.globalData.authToken || '' },
success: (res) => { success: (res) => {
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
const ok = res.statusCode >= 200 && res.statusCode < 300 && res.data?.code === 0
if (!ok) {
this.setData({ error: res.data?.detail || res.data?.message || '结果加载失败' })
return
}
const data = res.data?.data || {} const data = res.data?.data || {}
const total = Number(data.total || 0) const total = Number(data.total || 0)
const correct = Number(data.correct || 0) const correct = Number(data.correct || 0)
@ -52,9 +66,10 @@ Page({
is_correct: !!blank.is_correct, is_correct: !!blank.is_correct,
})) }))
: [], : [],
is_correct: !!item.blanks?.every?.((blank) => blank.is_correct), is_correct: Array.isArray(item.blanks) ? item.blanks.every((blank) => blank.is_correct) : false,
})) }))
: [] : []
this.setData({ this.setData({
total, total,
accuracy, accuracy,
@ -71,7 +86,17 @@ Page({
}) })
}, },
restart() { restart() {
app.globalData.reviewSession = { deck: [], currentIndex: 0, correctCount: 0, wrongCount: 0, completed: false, currentAnswer: '' } const app = appContext.withApp()
app.globalData.reviewSession = {
deck: [],
currentIndex: 0,
correctCount: 0,
wrongCount: 0,
completed: false,
currentAnswer: '',
currentAnswers: [],
lastClozeResults: [],
}
wx.reLaunch({ url: '/pages/review-config/review-config' }) wx.reLaunch({ url: '/pages/review-config/review-config' })
}, },
goHome() { goHome() {

View File

@ -1,6 +1,7 @@
const app = getApp() const appContext = require('../../utils/app-context.js')
const getReviewSettings = () => { const getReviewSettings = () => {
const app = appContext.withApp()
const settings = app.globalData.userSettings || {} const settings = app.globalData.userSettings || {}
return { return {
autoNextQuestion: Number(settings.auto_next_question || 0) === 1, autoNextQuestion: Number(settings.auto_next_question || 0) === 1,
@ -45,17 +46,30 @@ Page({
onLoad() { onLoad() {
this.loadSettingsAndQuestions() this.loadSettingsAndQuestions()
}, },
getReviewSessionState() {
const app = appContext.withApp()
return app.globalData.reviewSession
},
loadSettingsAndQuestions() { loadSettingsAndQuestions() {
const app = appContext.withApp()
if (!app.globalData.authToken) { if (!app.globalData.authToken) {
this.setData({ error: '请先登录' }) this.setData({ error: '请先登录' })
return return
} }
wx.request({ wx.request({
url: `${app.globalData.baseUrl}/api/v1/settings`, url: `${app.globalData.baseUrl}/api/v1/settings`,
header: { 'X-Auth-Token': app.globalData.authToken }, header: { 'X-Auth-Token': app.globalData.authToken },
success: (res) => { success: (res) => {
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.code === 0) {
app.globalData.userSettings = res.data?.data || {} app.globalData.userSettings = res.data?.data || {}
this.setData({ settings: getReviewSettings() }) this.setData({ settings: getReviewSettings() })
}
this.loadQuestions() this.loadQuestions()
}, },
fail: () => { fail: () => {
@ -98,11 +112,6 @@ Page({
return segments return segments
} }
if (blanks.length === 1 && workingPrompt === (clozeQuestion.original_text || prompt)) {
// Keep the original word context visible for single-word cloze questions.
// The general start/end slicing below will render visible letters around the blank.
}
let cursor = 0 let cursor = 0
blanks.forEach((blank, idx) => { blanks.forEach((blank, idx) => {
const start = Math.max(0, Number(blank.start || 0)) const start = Math.max(0, Number(blank.start || 0))
@ -114,27 +123,35 @@ Page({
cursor = end cursor = end
}) })
if (cursor < workingPrompt.length) { if (cursor < workingPrompt.length) {
segments.push({ type: 'text', text: workingPrompt.slice(cursor), index: `text-tail` }) segments.push({ type: 'text', text: workingPrompt.slice(cursor), index: 'text-tail' })
} }
return segments return segments
}, },
loadQuestions() { loadQuestions() {
const app = appContext.withApp()
const session = app.globalData.reviewSessionMeta || wx.getStorageSync('reviewSessionMeta') const session = app.globalData.reviewSessionMeta || wx.getStorageSync('reviewSessionMeta')
if (!session?.id) { if (!session?.id) {
this.setData({ error: '暂无复习会话' }) this.setData({ error: '暂无复习会话' })
return return
} }
app.globalData.reviewSessionMeta = session app.globalData.reviewSessionMeta = session
this.setData({ loading: true, error: '' }) this.setData({ loading: true, error: '' })
wx.request({ wx.request({
url: `${app.globalData.baseUrl}/api/v1/review/questions?session_id=${session.id}`, url: `${app.globalData.baseUrl}/api/v1/review/questions?session_id=${session.id}`,
header: { 'X-Auth-Token': app.globalData.authToken || '' }, header: { 'X-Auth-Token': app.globalData.authToken || '' },
success: (res) => { success: (res) => {
if (res.statusCode < 200 || res.statusCode >= 300) { if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
if (res.statusCode < 200 || res.statusCode >= 300 || res.data?.code !== 0) {
const message = res.data?.detail || res.data?.message || '题目加载失败' const message = res.data?.detail || res.data?.message || '题目加载失败'
this.setData({ error: message, questions: [] }) this.setData({ error: message, questions: [] })
return return
} }
const questions = (res.data?.data || []).map((item) => { const questions = (res.data?.data || []).map((item) => {
if (item.question_mode === 'C') { if (item.question_mode === 'C') {
return { return {
@ -148,10 +165,12 @@ Page({
entry_type: item.entry_type || 'word', entry_type: item.entry_type || 'word',
} }
}) })
if (!questions.length) { if (!questions.length) {
this.setData({ error: '当前复习会话没有题目,请返回重新选择范围', questions: [] }) this.setData({ error: '当前复习会话没有题目,请返回重新选择范围', questions: [] })
return return
} }
app.globalData.reviewSession = { app.globalData.reviewSession = {
deck: questions, deck: questions,
currentIndex: 0, currentIndex: 0,
@ -160,6 +179,7 @@ Page({
completed: false, completed: false,
currentAnswer: '', currentAnswer: '',
currentAnswers: [], currentAnswers: [],
lastClozeResults: [],
startedAt: Date.now(), startedAt: Date.now(),
} }
this.setData({ questions }) this.setData({ questions })
@ -170,8 +190,10 @@ Page({
}) })
}, },
syncSession() { syncSession() {
const session = app.globalData.reviewSession const session = this.getReviewSessionState()
const current = session.deck[session.currentIndex] const current = session.deck[session.currentIndex]
if (!current) return
const total = session.deck.length || 1 const total = session.deck.length || 1
const isCloze = current?.question_mode === 'C' const isCloze = current?.question_mode === 'C'
const clozeQuestion = isCloze ? (current?.cloze_question || null) : null const clozeQuestion = isCloze ? (current?.cloze_question || null) : null
@ -191,7 +213,7 @@ Page({
completed: session.completed, completed: session.completed,
masterHint: false, masterHint: false,
questionBadge: isCloze ? `填空 · ${current?.entry_type === 'sentence' ? '句子' : '单词'}` : `${current?.entry_type === 'sentence' ? '句子' : '单词'} · ${current?.question_mode || 'A'}`, questionBadge: isCloze ? `填空 · ${current?.entry_type === 'sentence' ? '句子' : '单词'}` : `${current?.entry_type === 'sentence' ? '句子' : '单词'} · ${current?.question_mode || 'A'}`,
entryTypeHint: isCloze ? '请逐空作答,提交后会显示每个空的结果。' : (current?.entry_type === 'sentence' ? '当前题目来自句子库,注意上下文表达。' : ''), entryTypeHint: isCloze ? '请逐空作答,提交后会显示每个空的结果。' : (current?.entry_type === 'sentence' ? '当前题目来自句子库,注意上下文表达。' : ''),
isCloze, isCloze,
currentClozeQuestion: clozeQuestion, currentClozeQuestion: clozeQuestion,
currentClozeResults: session.lastClozeResults || [], currentClozeResults: session.lastClozeResults || [],
@ -201,20 +223,22 @@ Page({
}) })
}, },
onAnswerInput(e) { onAnswerInput(e) {
const session = this.getReviewSessionState()
const answer = e.detail.value const answer = e.detail.value
app.globalData.reviewSession.currentAnswer = answer session.currentAnswer = answer
this.setData({ answer, showFeedback: false, revealAnswer: '', feedback: '', feedbackType: '', judged: false, showClozeFeedback: false }) this.setData({ answer, showFeedback: false, revealAnswer: '', feedback: '', feedbackType: '', judged: false, showClozeFeedback: false })
}, },
onClozeLetterInput(e) { onClozeLetterInput(e) {
const session = this.getReviewSessionState()
const { index, letterIndex } = e.currentTarget.dataset const { index, letterIndex } = e.currentTarget.dataset
const rawValue = e.detail.value || '' const rawValue = e.detail.value || ''
const cleaned = rawValue.slice(-1) const cleaned = rawValue.slice(-1)
const answers = [...(app.globalData.reviewSession.currentAnswers || [])] const answers = [...(session.currentAnswers || [])]
const current = answers[index] || '' const current = answers[index] || ''
const chars = current.split('') const chars = current.split('')
chars[letterIndex] = cleaned chars[letterIndex] = cleaned
answers[index] = chars.join('').slice(0, Math.max(chars.length, letterIndex + 1)) answers[index] = chars.join('').slice(0, Math.max(chars.length, letterIndex + 1))
app.globalData.reviewSession.currentAnswers = answers session.currentAnswers = answers
this.setData({ this.setData({
[`clozeAnswers[${index}]`]: answers[index], [`clozeAnswers[${index}]`]: answers[index],
showFeedback: false, showFeedback: false,
@ -239,17 +263,18 @@ Page({
}) })
}, },
handleLetterKeydown(e) { handleLetterKeydown(e) {
const session = this.getReviewSessionState()
const key = e?.detail?.key || e?.detail?.value || '' const key = e?.detail?.key || e?.detail?.value || ''
const { index, letterIndex } = e.currentTarget.dataset const { index, letterIndex } = e.currentTarget.dataset
if (key === 'Backspace' || key === 'Delete') { if (key === 'Backspace' || key === 'Delete') {
const answers = [...(app.globalData.reviewSession.currentAnswers || [])] const answers = [...(session.currentAnswers || [])]
const current = answers[index] || '' const current = answers[index] || ''
const chars = current.split('') const chars = current.split('')
const pos = Number(letterIndex) const pos = Number(letterIndex)
if (chars[pos]) { if (chars[pos]) {
chars[pos] = '' chars[pos] = ''
answers[index] = chars.join('') answers[index] = chars.join('')
app.globalData.reviewSession.currentAnswers = answers session.currentAnswers = answers
this.setData({ [`clozeAnswers[${index}]`]: answers[index] }) this.setData({ [`clozeAnswers[${index}]`]: answers[index] })
return return
} }
@ -267,15 +292,16 @@ Page({
} }
}, },
onClozeAnswerInput(e) { onClozeAnswerInput(e) {
const session = this.getReviewSessionState()
const { index } = e.currentTarget.dataset const { index } = e.currentTarget.dataset
const rawValue = e.detail.value || '' const rawValue = e.detail.value || ''
const clozeQuestion = this.data.currentClozeQuestion const clozeQuestion = this.data.currentClozeQuestion
const blank = clozeQuestion?.blanks?.[index] const blank = clozeQuestion?.blanks?.[index]
const maxLength = Math.max(1, Number(blank?.answer?.length || 1)) const maxLength = Math.max(1, Number(blank?.answer?.length || 1))
const normalizedValue = rawValue.slice(-maxLength).slice(0, maxLength) const normalizedValue = rawValue.slice(-maxLength).slice(0, maxLength)
const answers = [...(app.globalData.reviewSession.currentAnswers || [])] const answers = [...(session.currentAnswers || [])]
answers[index] = normalizedValue answers[index] = normalizedValue
app.globalData.reviewSession.currentAnswers = answers session.currentAnswers = answers
const nextIndex = Number(index) + (normalizedValue.length >= maxLength ? 1 : 0) const nextIndex = Number(index) + (normalizedValue.length >= maxLength ? 1 : 0)
const prevIndex = Number(index) - 1 const prevIndex = Number(index) - 1
this.setData({ this.setData({
@ -307,7 +333,8 @@ Page({
}) })
}, },
submitAnswer(showExplanation = false, advanceAfterSubmit = false) { submitAnswer(showExplanation = false, advanceAfterSubmit = false) {
const session = app.globalData.reviewSession const app = appContext.withApp()
const session = this.getReviewSessionState()
const current = session.deck[session.currentIndex] const current = session.deck[session.currentIndex]
if (!current || this.submitting || this.judged) return if (!current || this.submitting || this.judged) return
@ -318,18 +345,15 @@ Page({
record_id: current.record_id, record_id: current.record_id,
user_answer: answer, user_answer: answer,
} }
if (current.question_mode === 'C' && clozeQuestion?.blanks?.length) { if (current.question_mode === 'C' && clozeQuestion?.blanks?.length) {
const answers = (this.data.clozeAnswers || []).map((item) => (item || '').trim()) const answers = (this.data.clozeAnswers || []).map((item) => (item || '').trim())
while (answers.length < clozeQuestion.blanks.length) answers.push('') while (answers.length < clozeQuestion.blanks.length) answers.push('')
const isLetterCellMode = Array.isArray(this.data.clozeSegments) && this.data.clozeSegments.some((segment) => segment.type === 'letter-cell') if (!answers.some((item) => item)) {
const normalizedAnswers = isLetterCellMode
? answers
: answers
if (!normalizedAnswers.some((item) => item)) {
wx.showToast({ title: '请先输入答案', icon: 'none' }) wx.showToast({ title: '请先输入答案', icon: 'none' })
return return
} }
payload.user_answers = normalizedAnswers payload.user_answers = answers
} else if (!answer) { } else if (!answer) {
wx.showToast({ title: '请先输入答案', icon: 'none' }) wx.showToast({ title: '请先输入答案', icon: 'none' })
return return
@ -342,10 +366,22 @@ Page({
header: { 'Content-Type': 'application/json', 'X-Auth-Token': app.globalData.authToken || '' }, header: { 'Content-Type': 'application/json', 'X-Auth-Token': app.globalData.authToken || '' },
data: payload, data: payload,
success: (res) => { success: (res) => {
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
const ok = res.statusCode >= 200 && res.statusCode < 300 && res.data?.code === 0
if (!ok) {
wx.showToast({ title: res.data?.detail || res.data?.message || '提交答案失败', icon: 'none' })
return
}
const result = res.data?.data || {} const result = res.data?.data || {}
session.correctCount += result.is_correct ? 1 : 0 session.correctCount += result.is_correct ? 1 : 0
session.wrongCount += result.is_correct ? 0 : 1 session.wrongCount += result.is_correct ? 0 : 1
this.judged = true this.judged = true
const shouldShowAnswer = showExplanation || this.data.settings.showAnswerAfterSubmit const shouldShowAnswer = showExplanation || this.data.settings.showAnswerAfterSubmit
const correctAnswerText = result.correct_answer_text || '' const correctAnswerText = result.correct_answer_text || ''
const clozeResults = Array.isArray(result.cloze_results) ? result.cloze_results : [] const clozeResults = Array.isArray(result.cloze_results) ? result.cloze_results : []
@ -372,15 +408,17 @@ Page({
is_correct: Boolean(item.is_correct), is_correct: Boolean(item.is_correct),
} }
}) })
const derivedClozeResults = normalizedClozeResults.length const derivedClozeResults = normalizedClozeResults.length
? normalizedClozeResults ? normalizedClozeResults
: (correctAnswerText : (correctAnswerText
? [{ index: 0, user_answer: '', correct_answers: [correctAnswerText], correct_answer_text: correctAnswerText, standard_answer_text: correctAnswerText, is_correct: false }] ? [{ index: 0, user_answer: '', correct_answers: [correctAnswerText], correct_answer_text: correctAnswerText, standard_answer_text: correctAnswerText, is_correct: false }]
: []) : [])
if (derivedClozeResults.length) { if (derivedClozeResults.length) {
session.lastClozeResults = derivedClozeResults session.lastClozeResults = derivedClozeResults
app.globalData.reviewSession.lastClozeResults = derivedClozeResults
} }
const clozeFeedbackSummary = result.is_correct ? '回答正确' : '回答错误' const clozeFeedbackSummary = result.is_correct ? '回答正确' : '回答错误'
this.setData({ this.setData({
feedback: clozeFeedbackSummary, feedback: clozeFeedbackSummary,
@ -391,10 +429,12 @@ Page({
currentClozeResults: derivedClozeResults, currentClozeResults: derivedClozeResults,
clozeFeedbackSummary, clozeFeedbackSummary,
}) })
if (this.data.isCloze) { if (this.data.isCloze) {
app.globalData.reviewSession.currentAnswers = [] session.currentAnswers = []
this.setData({ clozeAnswers: [] }) this.setData({ clozeAnswers: [] })
} }
if (this.data.settings.autoNextQuestion || advanceAfterSubmit) { if (this.data.settings.autoNextQuestion || advanceAfterSubmit) {
const delay = shouldShowAnswer ? 2500 : 0 const delay = shouldShowAnswer ? 2500 : 0
setTimeout(() => this.nextQuestion(), delay) setTimeout(() => this.nextQuestion(), delay)
@ -409,12 +449,14 @@ Page({
this.submitAnswer(true) this.submitAnswer(true)
}, },
nextQuestion() { nextQuestion() {
const session = app.globalData.reviewSession const app = appContext.withApp()
const session = this.getReviewSessionState()
if (!session.deck.length) return if (!session.deck.length) return
if (!this.judged) { if (!this.judged) {
this.submitAnswer(true, true) this.submitAnswer(true, true)
return return
} }
this.judged = false this.judged = false
if (session.currentIndex >= session.deck.length - 1) { if (session.currentIndex >= session.deck.length - 1) {
session.completed = true session.completed = true
@ -423,14 +465,21 @@ Page({
url: `${app.globalData.baseUrl}/api/v1/review/finish?session_id=${app.globalData.reviewSessionMeta.id}`, url: `${app.globalData.baseUrl}/api/v1/review/finish?session_id=${app.globalData.reviewSessionMeta.id}`,
method: 'POST', method: 'POST',
header: { 'X-Auth-Token': app.globalData.authToken || '' }, header: { 'X-Auth-Token': app.globalData.authToken || '' },
success: () => wx.navigateTo({ url: '/pages/review-result/review-result' }), success: (res) => {
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
wx.navigateTo({ url: '/pages/review-result/review-result' })
},
fail: () => { fail: () => {
wx.showToast({ title: '提交结果失败,已本地完成', icon: 'none' }) wx.showToast({ title: '提交结果失败,已本地完成处理', icon: 'none' })
wx.navigateTo({ url: '/pages/review-result/review-result' }) wx.navigateTo({ url: '/pages/review-result/review-result' })
}, },
}) })
return return
} }
session.currentIndex += 1 session.currentIndex += 1
session.currentAnswer = '' session.currentAnswer = ''
session.currentAnswers = [] session.currentAnswers = []

View File

@ -1,4 +1,4 @@
const app = getApp() const appContext = require('../../utils/app-context.js')
const difficultyOptions = [ const difficultyOptions = [
{ label: '简单', value: 'easy' }, { label: '简单', value: 'easy' },
@ -11,6 +11,16 @@ const reviewOrderOptions = [
{ label: '随机复习', value: 'shuffle' }, { label: '随机复习', value: 'shuffle' },
] ]
function normalizeSettings(settings) {
return {
blank_count: Number(settings?.blank_count || 2),
blank_difficulty: settings?.blank_difficulty || 'normal',
review_order: settings?.review_order || 'sequential',
auto_next_question: Number(settings?.auto_next_question || 0),
show_answer_after_submit: Number(settings?.show_answer_after_submit || 0),
}
}
Page({ Page({
data: { data: {
settings: null, settings: null,
@ -27,26 +37,42 @@ Page({
this.loadSettings() this.loadSettings()
}, },
syncDerivedState(settings) { syncDerivedState(settings) {
const difficultyIndex = Math.max(0, difficultyOptions.findIndex((item) => item.value === settings.blank_difficulty)) const nextSettings = normalizeSettings(settings)
const reviewOrderIndex = Math.max(0, reviewOrderOptions.findIndex((item) => item.value === settings.review_order)) const difficultyIndex = difficultyOptions.findIndex((item) => item.value === nextSettings.blank_difficulty)
const reviewOrderIndex = reviewOrderOptions.findIndex((item) => item.value === nextSettings.review_order)
const safeDifficultyIndex = difficultyIndex >= 0 ? difficultyIndex : 1
const safeReviewOrderIndex = reviewOrderIndex >= 0 ? reviewOrderIndex : 0
this.setData({ this.setData({
settings, settings: nextSettings,
difficultyIndex: difficultyIndex >= 0 ? difficultyIndex : 1, difficultyIndex: safeDifficultyIndex,
difficultyLabel: difficultyOptions[difficultyIndex >= 0 ? difficultyIndex : 1].label, difficultyLabel: difficultyOptions[safeDifficultyIndex].label,
reviewOrderIndex: reviewOrderIndex >= 0 ? reviewOrderIndex : 0, reviewOrderIndex: safeReviewOrderIndex,
reviewOrderLabel: reviewOrderOptions[reviewOrderIndex >= 0 ? reviewOrderIndex : 0].label, reviewOrderLabel: reviewOrderOptions[safeReviewOrderIndex].label,
}) })
}, },
loadSettings() { loadSettings() {
const app = appContext.withApp()
if (!app.globalData.authToken) { if (!app.globalData.authToken) {
this.setData({ error: '请先登录' }) this.setData({ error: '请先登录' })
return return
} }
this.setData({ loading: true, error: '' }) this.setData({ loading: true, error: '' })
wx.request({ wx.request({
url: `${app.globalData.baseUrl}/api/v1/settings`, url: `${app.globalData.baseUrl}/api/v1/settings`,
header: { 'X-Auth-Token': app.globalData.authToken }, header: { 'X-Auth-Token': app.globalData.authToken },
success: (res) => { success: (res) => {
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
const ok = res.statusCode >= 200 && res.statusCode < 300 && res.data?.code === 0
if (!ok) {
this.setData({ error: res.data?.detail || res.data?.message || '设置加载失败' })
return
}
this.syncDerivedState(res.data?.data || null) this.syncDerivedState(res.data?.data || null)
}, },
fail: () => this.setData({ error: '设置加载失败' }), fail: () => this.setData({ error: '设置加载失败' }),
@ -57,7 +83,7 @@ Page({
const value = Number(e.detail.value || 0) const value = Number(e.detail.value || 0)
this.setData({ this.setData({
settings: { settings: {
...this.data.settings, ...normalizeSettings(this.data.settings),
blank_count: Number.isFinite(value) ? value : 0, blank_count: Number.isFinite(value) ? value : 0,
}, },
}) })
@ -69,7 +95,7 @@ Page({
difficultyIndex: index, difficultyIndex: index,
difficultyLabel: option.label, difficultyLabel: option.label,
settings: { settings: {
...this.data.settings, ...normalizeSettings(this.data.settings),
blank_difficulty: option.value, blank_difficulty: option.value,
}, },
}) })
@ -81,29 +107,44 @@ Page({
reviewOrderIndex: index, reviewOrderIndex: index,
reviewOrderLabel: option.label, reviewOrderLabel: option.label,
settings: { settings: {
...this.data.settings, ...normalizeSettings(this.data.settings),
review_order: option.value, review_order: option.value,
}, },
}) })
}, },
toggleSetting(e) { toggleSetting(e) {
const key = e.currentTarget.dataset.key const key = e.currentTarget.dataset.key
const settings = { ...this.data.settings } const settings = normalizeSettings(this.data.settings)
settings[key] = settings[key] ? 0 : 1 settings[key] = settings[key] ? 0 : 1
this.setData({ settings }) this.setData({ settings })
}, },
save() { save() {
const app = appContext.withApp()
if (!app.globalData.authToken) { if (!app.globalData.authToken) {
wx.showToast({ title: '请先登录', icon: 'none' }) wx.showToast({ title: '请先登录', icon: 'none' })
return return
} }
const settings = this.data.settings || {}
const settings = normalizeSettings(this.data.settings)
wx.request({ wx.request({
url: `${app.globalData.baseUrl}/api/v1/settings`, url: `${app.globalData.baseUrl}/api/v1/settings`,
method: 'PUT', method: 'PUT',
header: { 'Content-Type': 'application/json', 'X-Auth-Token': app.globalData.authToken }, header: { 'Content-Type': 'application/json', 'X-Auth-Token': app.globalData.authToken },
data: settings, data: settings,
success: () => wx.showToast({ title: '已保存', icon: 'success' }), success: (res) => {
if (appContext.isUnauthorizedResponse(res)) {
appContext.handleUnauthorized()
return
}
const ok = res.statusCode >= 200 && res.statusCode < 300 && res.data?.code === 0
if (!ok) {
wx.showToast({ title: res.data?.detail || res.data?.message || '保存失败', icon: 'none' })
return
}
this.syncDerivedState(res.data?.data || settings)
wx.showToast({ title: '已保存', icon: 'success' })
},
fail: () => wx.showToast({ title: '保存失败', icon: 'none' }), fail: () => wx.showToast({ title: '保存失败', icon: 'none' }),
}) })
}, },

View File

@ -2,7 +2,7 @@
<scroll-view scroll-y class="content"> <scroll-view scroll-y class="content">
<view class="header"> <view class="header">
<text class="title">设置</text> <text class="title">设置</text>
<text class="subtitle">调整你的学习体验与交互偏好</text> <text class="subtitle">调整你的复习偏好与交互方式</text>
</view> </view>
<view wx:if="{{loading}}" class="empty-state"> <view wx:if="{{loading}}" class="empty-state">
@ -16,18 +16,18 @@
<view wx:if="{{settings}}" class="section-block"> <view wx:if="{{settings}}" class="section-block">
<view class="section-head"> <view class="section-head">
<view class="section-ico"></view> <view class="section-ico"></view>
<text class="section-title">复习参数</text> <text class="section-title">复习参数</text>
</view> </view>
<view class="panel"> <view class="panel">
<view class="row"> <view class="row">
<view><text class="label">挖空数量</text><text class="desc">题型 C 中每题挖空的词数</text></view> <view><text class="label">挖空数量</text><text class="desc">题型 C 中每题需要挖空的词数</text></view>
<view class="value-group"> <view class="value-group">
<input class="setting-input" type="number" value="{{settings.blank_count}}" bindinput="onBlankCountInput" /> <input class="setting-input" type="number" value="{{settings.blank_count}}" bindinput="onBlankCountInput" />
</view> </view>
</view> </view>
<view class="row"> <view class="row">
<view><text class="label">挖空难度</text><text class="desc">影响挖空比例与保留字</text></view> <view><text class="label">挖空难度</text><text class="desc">影响挖空比例与保留字</text></view>
<view class="value-group"> <view class="value-group">
<picker mode="selector" range="{{difficultyOptions}}" range-key="label" value="{{difficultyIndex}}" bindchange="onDifficultyChange"> <picker mode="selector" range="{{difficultyOptions}}" range-key="label" value="{{difficultyIndex}}" bindchange="onDifficultyChange">
<view class="picker-value accent">{{difficultyLabel}}</view> <view class="picker-value accent">{{difficultyLabel}}</view>
@ -35,7 +35,7 @@
</view> </view>
</view> </view>
<view class="row"> <view class="row">
<view><text class="label">复习顺序</text><text class="desc">控制复习题目按顺序还是随机展示</text></view> <view><text class="label">复习顺序</text><text class="desc">控制题目按顺序还是随机展示</text></view>
<view class="value-group"> <view class="value-group">
<picker mode="selector" range="{{reviewOrderOptions}}" range-key="label" value="{{reviewOrderIndex}}" bindchange="onReviewOrderChange"> <picker mode="selector" range="{{reviewOrderOptions}}" range-key="label" value="{{reviewOrderIndex}}" bindchange="onReviewOrderChange">
<view class="picker-value accent">{{reviewOrderLabel}}</view> <view class="picker-value accent">{{reviewOrderLabel}}</view>
@ -47,16 +47,16 @@
<view wx:if="{{settings}}" class="section-block"> <view wx:if="{{settings}}" class="section-block">
<view class="section-head"> <view class="section-head">
<view class="section-ico"></view> <view class="section-ico"></view>
<text class="section-title">交互设置</text> <text class="section-title">交互设置</text>
</view> </view>
<view class="panel"> <view class="panel">
<view class="row" bindtap="toggleSetting" data-key="auto_next_question"> <view class="row" bindtap="toggleSetting" data-key="auto_next_question">
<view><text class="label">自动进入下一词</text><text class="desc">答题后自动跳转,保持心流状态</text></view> <view><text class="label">自动进入下一题</text><text class="desc">答题后自动跳转,保持连续复习节奏</text></view>
<view class="toggle {{settings.auto_next_question ? 'on' : ''}}">{{settings.auto_next_question ? 'ON' : 'OFF'}}</view> <view class="toggle {{settings.auto_next_question ? 'on' : ''}}">{{settings.auto_next_question ? 'ON' : 'OFF'}}</view>
</view> </view>
<view class="row" bindtap="toggleSetting" data-key="show_answer_after_submit"> <view class="row" bindtap="toggleSetting" data-key="show_answer_after_submit">
<view><text class="label">提交后显示答案</text><text class="desc">答题后直接显示正确答案</text></view> <view><text class="label">提交后显示答案</text><text class="desc">答题后立即展示标准答案</text></view>
<view class="toggle {{settings.show_answer_after_submit ? 'on' : ''}}">{{settings.show_answer_after_submit ? 'ON' : 'OFF'}}</view> <view class="toggle {{settings.show_answer_after_submit ? 'on' : ''}}">{{settings.show_answer_after_submit ? 'ON' : 'OFF'}}</view>
</view> </view>
</view> </view>