修复我的页面头像登录功能

This commit is contained in:
taiyi 2026-04-23 11:34:54 +08:00
parent 862b4fbe3b
commit 4204761837
20 changed files with 876 additions and 185 deletions

View File

@ -1,25 +1,145 @@
const app = getApp()
const ACHIEVEMENT_ICON_MAP = {
MASTERED_10: '/assets/icons/achievements/core/mastered-10.svg',
MASTERED_50: '/assets/icons/achievements/core/mastered-50.svg',
MASTERED_100: '/assets/icons/achievements/core/mastery-advanced.svg',
MASTERED_500: '/assets/icons/achievements/core/mastery-advanced.svg',
MASTERED_1000: '/assets/icons/achievements/core/mastery-advanced.svg',
MASTERED_2000: '/assets/icons/achievements/core/mastery-2000.svg',
MASTERED_5000: '/assets/icons/achievements/core/mastery-5000.svg',
MASTERED_10000: '/assets/icons/achievements/core/mastery-10000.svg',
TOTAL_100: '/assets/icons/achievements/core/total-100.svg',
CHECKIN_7: '/assets/icons/achievements/core/checkin-30.svg',
CHECKIN_30: '/assets/icons/achievements/core/checkin-30.svg',
CHECKIN_100: '/assets/icons/achievements/core/streak.svg',
CHECKIN_180: '/assets/icons/achievements/core/streak.svg',
CHECKIN_360: '/assets/icons/achievements/core/streak.svg',
CHECKIN_STREAK_7: '/assets/icons/achievements/core/streak.svg',
CHECKIN_STREAK_30: '/assets/icons/achievements/core/streak.svg',
CHECKIN_STREAK_100: '/assets/icons/achievements/core/streak.svg',
CHECKIN_STREAK_180: '/assets/icons/achievements/core/streak.svg',
CHECKIN_STREAK_360: '/assets/icons/achievements/core/streak.svg',
ACCURACY_80: '/assets/icons/achievements/core/accuracy.svg',
ACCURACY_90: '/assets/icons/achievements/core/accuracy.svg',
DAILY_REVIEW_20: '/assets/icons/achievements/core/review-volume.svg',
DAILY_REVIEW_50: '/assets/icons/achievements/core/review-volume.svg',
}
const STATUS_ICON_MAP = {
1: '/assets/icons/achievements/state/achievement-unlocked.svg',
0: '/assets/icons/achievements/state/achievement-locked.svg',
}
const GROUP_ORDER = {
mastery: 1,
checkin: 2,
streak: 3,
accuracy: 4,
review: 5,
other: 9,
}
const GROUP_META = {
mastery: { title: '掌握进阶', subtitle: '从入门到精通', icon: '/assets/icons/achievements/core/mastery-advanced.svg' },
checkin: { title: '打卡里程', subtitle: '记录坚持的脚步', icon: '/assets/icons/achievements/core/checkin-30.svg' },
streak: { title: '连续坚持', subtitle: '不断线的努力', icon: '/assets/icons/achievements/core/streak.svg' },
accuracy: { title: '准确率', subtitle: '复习质量提升', icon: '/assets/icons/achievements/core/accuracy.svg' },
review: { title: '复习数量', subtitle: '积累复习量', icon: '/assets/icons/achievements/core/review-volume.svg' },
other: { title: '其他', subtitle: '更多成就', icon: '/assets/icons/achievements/core/total-100.svg' },
}
const GROUP_PROGRESS = {
mastery: [10, 50, 100, 500, 1000, 2000, 5000, 10000],
checkin: [7, 30, 100, 180, 360],
streak: [7, 30, 100, 180, 360],
accuracy: [80, 90],
review: [20, 50],
}
function getNextTarget(items, values) {
const unlockedMax = items.filter((item) => item.is_unlocked).reduce((max, item) => Math.max(max, item.target_value || 0), 0)
const nextTarget = values.find((value) => value > unlockedMax)
return nextTarget || null
}
function formatProgressText(key, items) {
const values = GROUP_PROGRESS[key] || []
const nextTarget = getNextTarget(items, values)
const best = items.reduce((max, item) => Math.max(max, item.progress_value || 0), 0)
if (nextTarget) {
return `距离下一档还差 ${Math.max(0, nextTarget - best)}`
}
if (items.some((item) => item.is_unlocked)) {
return '该分组已全部解锁'
}
return '继续坚持,解锁第一档'
}
Page({
data: {
total: 0,
unlocked: 0,
list: [],
sections: [],
loading: false,
error: '',
},
onShow() {
this.loadData()
},
getGroup(code) {
if (code.startsWith('MASTERED_')) return 'mastery'
if (code.startsWith('CHECKIN_STREAK_')) return 'streak'
if (code.startsWith('CHECKIN_')) return 'checkin'
if (code.startsWith('ACCURACY_')) return 'accuracy'
if (code.startsWith('DAILY_REVIEW_')) return 'review'
return 'other'
},
decorateList(list) {
return (list || []).map((item) => ({
...item,
group: this.getGroup(item.achievement_code),
icon: ACHIEVEMENT_ICON_MAP[item.achievement_code] || GROUP_META[this.getGroup(item.achievement_code)].icon,
stateIcon: STATUS_ICON_MAP[item.is_unlocked] || STATUS_ICON_MAP[0],
}))
},
buildSections(list) {
const buckets = { unlocked: {}, locked: {} }
list.forEach((item) => {
const state = item.is_unlocked ? 'unlocked' : 'locked'
const key = item.group || 'other'
if (!buckets[state][key]) buckets[state][key] = []
buckets[state][key].push(item)
})
const buildStateSections = (state) => Object.keys(buckets[state])
.sort((a, b) => (GROUP_ORDER[a] || 99) - (GROUP_ORDER[b] || 99))
.map((key) => {
const items = buckets[state][key].sort((a, b) => a.target_value - b.target_value)
return {
key: `${state}-${key}`,
state,
meta: GROUP_META[key] || GROUP_META.other,
progressText: formatProgressText(key, list.filter((item) => item.group === key)),
items,
}
})
return [
{ key: 'unlocked', title: '已解锁', sections: buildStateSections('unlocked') },
{ key: 'locked', title: '未解锁', sections: buildStateSections('locked') },
].filter((group) => group.sections.length)
},
loadData() {
this.setData({ loading: true, error: '' })
wx.request({
url: `${app.globalData.baseUrl}/api/v1/achievements`,
header: { 'X-OpenID': app.globalData.openid || '' },
success: (res) => {
const list = res.data?.data || []
const list = this.decorateList(res.data?.data || [])
const sections = this.buildSections(list)
this.setData({
list,
sections,
total: list.length,
unlocked: list.filter((item) => item.is_unlocked).length,
})
@ -28,4 +148,27 @@ Page({
complete: () => this.setData({ loading: false }),
})
},
refreshAchievements() {
this.setData({ loading: true, error: '' })
wx.request({
url: `${app.globalData.baseUrl}/api/v1/achievements/refresh`,
method: 'POST',
header: { 'X-OpenID': app.globalData.openid || '' },
success: (res) => {
const payload = res.data?.data || {}
const list = this.decorateList(payload.achievements || [])
const sections = this.buildSections(list)
this.setData({
sections,
total: list.length,
unlocked: list.filter((item) => item.is_unlocked).length,
})
if ((payload.just_unlocked_count || 0) > 0) {
wx.showToast({ title: `新解锁 ${payload.just_unlocked_count} 项成就`, icon: 'success' })
}
},
fail: () => this.setData({ error: '成就刷新失败' }),
complete: () => this.setData({ loading: false }),
})
},
})

View File

@ -12,7 +12,9 @@
<text class="hero-num">{{unlocked}}</text>
<text class="hero-label">已点亮勋章 / {{total}}</text>
</view>
<view class="hero-badge">🏆</view>
<view class="hero-badge">
<image class="hero-badge-img" src="/assets/icons/achievements/state/achievement-unlocked.svg" mode="aspectFit" />
</view>
</view>
</view>
@ -25,17 +27,44 @@
<text class="error-text">{{error}}</text>
</view>
<view wx:elif="{{!list.length}}" class="empty-state">
<view wx:elif="{{!sections.length}}" class="empty-state">
<text class="empty-title">暂无成就</text>
<text class="empty-subtitle">继续复习和打卡后会逐渐解锁</text>
</view>
<view wx:else class="grid">
<block wx:for="{{list}}" wx:key="id">
<view class="item {{item.is_unlocked ? 'accent-primary' : 'accent-secondary'}}">
<view class="item-icon">🏅</view>
<text class="item-title">{{item.achievement_name}}</text>
<text class="item-sub">{{item.progress_value}} / {{item.target_value}}</text>
<view wx:else>
<block wx:for="{{sections}}" wx:key="key">
<view class="state-card {{item.key}}">
<view class="state-head">
<text class="state-title">{{item.title}}</text>
<text class="state-subtitle">{{item.sections.length}} 个分组</text>
</view>
<block wx:for="{{item.sections}}" wx:key="key">
<view class="section-card">
<view class="section-head">
<view class="section-title-wrap">
<image class="section-icon" src="{{item.meta.icon}}" mode="aspectFit" />
<view>
<text class="section-title">{{item.meta.title}}</text>
<text class="section-subtitle">{{item.meta.subtitle}}</text>
</view>
</view>
<text class="section-count">{{item.items.length}} 项</text>
</view>
<text class="section-progress">{{item.progressText}}</text>
<view class="grid">
<block wx:for="{{item.items}}" wx:key="id">
<view class="item {{item.is_unlocked ? 'accent-primary' : 'accent-secondary'}}">
<view class="item-icon">
<image class="item-icon-img" src="{{item.icon}}" mode="aspectFit" />
</view>
<text class="item-title">{{item.achievement_name}}</text>
<text class="item-sub">{{item.progress_value}} / {{item.target_value}}</text>
</view>
</block>
</view>
</view>
</block>
</view>
</block>
</view>

View File

@ -1 +1 @@
page{background:#0c0e12;color:#f6f6fc}.page{min-height:100vh;background:#0c0e12;box-sizing:border-box}.topbar{display:flex;justify-content:space-between;align-items:center;padding:24rpx 32rpx;background:rgba(12,14,18,.8);backdrop-filter:blur(24px);border-bottom:1rpx solid rgba(255,255,255,.08);box-shadow:0 1rpx 20rpx rgba(0,0,0,.08)}.brand{display:flex;align-items:center;gap:18rpx}.avatar-wrap{width:64rpx;height:64rpx;border-radius:9999rpx;overflow:hidden;border:1rpx solid rgba(255,255,255,.1);flex-shrink:0}.avatar{width:100%;height:100%}.brand-text{font-size:38rpx;font-weight:800;color:#c19cff}.icon-btn{width:72rpx;height:72rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center}.content{height:calc(100vh - 120rpx);padding:48rpx 40rpx 32rpx;box-sizing:border-box}.heading{margin-bottom:28rpx}.title{display:block;font-size:52rpx;font-weight:800;line-height:1.1}.subtitle{display:block;margin-top:10rpx;color:#aaabb0;font-size:26rpx}.hero-card{position:relative;overflow:hidden;padding:32rpx;border-radius:32rpx;background:rgba(29,32,37,.6);backdrop-filter:blur(24rpx);border:1rpx solid rgba(255,255,255,.08);box-shadow:0 20rpx 40rpx rgba(0,0,0,.08);margin-bottom:24rpx}.hero-glow{position:absolute;right:-40rpx;top:-40rpx;width:160rpx;height:160rpx;border-radius:9999rpx;background:rgba(255,64,129,.12);filter:blur(30rpx)}.hero-row{display:flex;justify-content:space-between;align-items:center;position:relative;z-index:1}.hero-num{display:block;font-size:72rpx;font-weight:800;color:#fff}.hero-label{display:block;margin-top:8rpx;font-size:24rpx;color:#aaabb0}.hero-badge{width:112rpx;height:112rpx;border-radius:9999rpx;background:linear-gradient(135deg,#ff4081,#00e5ff);display:flex;align-items:center;justify-content:center;font-size:44rpx;box-shadow:0 0 20rpx rgba(255,64,129,.2)}.grid{display:grid;grid-template-columns:1fr 1fr;gap:18rpx}.item{position:relative;overflow:hidden;padding:28rpx;border-radius:28rpx;background:rgba(29,32,37,.56);backdrop-filter:blur(24rpx);border:1rpx solid rgba(255,255,255,.06);box-shadow:0 20rpx 40rpx rgba(0,0,0,.06);min-height:220rpx}.accent-tertiary{box-shadow:0 20rpx 40rpx rgba(255,64,129,.06)}.accent-secondary{box-shadow:0 20rpx 40rpx rgba(0,229,255,.06)}.accent-primary{box-shadow:0 20rpx 40rpx rgba(127,0,255,.06)}.item-icon{width:76rpx;height:76rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;margin-bottom:24rpx;font-size:32rpx}.item-title{display:block;font-size:30rpx;font-weight:700;margin-bottom:8rpx}.item-sub{display:block;font-size:22rpx;color:#aaabb0}
page{background:#0c0e12;color:#f6f6fc}.page{min-height:100vh;background:#0c0e12;box-sizing:border-box}.topbar{display:flex;justify-content:space-between;align-items:center;padding:24rpx 32rpx;background:rgba(12,14,18,.8);backdrop-filter:blur(24px);border-bottom:1rpx solid rgba(255,255,255,.08);box-shadow:0 1rpx 20rpx rgba(0,0,0,.08)}.brand{display:flex;align-items:center;gap:18rpx}.avatar-wrap{width:64rpx;height:64rpx;border-radius:9999rpx;overflow:hidden;border:1rpx solid rgba(255,255,255,.1);flex-shrink:0}.avatar{width:100%;height:100%}.brand-text{font-size:38rpx;font-weight:800;color:#c19cff}.icon-btn{width:72rpx;height:72rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center}.content{height:calc(100vh - 120rpx);padding:48rpx 40rpx 32rpx;box-sizing:border-box}.heading{margin-bottom:28rpx}.title{display:block;font-size:52rpx;font-weight:800;line-height:1.1}.subtitle{display:block;margin-top:10rpx;color:#aaabb0;font-size:26rpx}.hero-card{position:relative;overflow:hidden;padding:32rpx;border-radius:32rpx;background:rgba(29,32,37,.6);backdrop-filter:blur(24rpx);border:1rpx solid rgba(255,255,255,.08);box-shadow:0 20rpx 40rpx rgba(0,0,0,.08);margin-bottom:24rpx}.hero-glow{position:absolute;right:-40rpx;top:-40rpx;width:160rpx;height:160rpx;border-radius:9999rpx;background:rgba(255,64,129,.12);filter:blur(30rpx)}.hero-row{display:flex;justify-content:space-between;align-items:center;position:relative;z-index:1}.hero-num{display:block;font-size:72rpx;font-weight:800;color:#fff}.hero-label{display:block;margin-top:8rpx;font-size:24rpx;color:#aaabb0}.hero-badge{width:112rpx;height:112rpx;border-radius:9999rpx;background:linear-gradient(135deg,#ff4081,#00e5ff);display:flex;align-items:center;justify-content:center;box-shadow:0 0 20rpx rgba(255,64,129,.2);overflow:hidden}.hero-badge-img{width:64rpx;height:64rpx}.state-card{margin-bottom:28rpx}.state-head{display:flex;justify-content:space-between;align-items:center;margin:0 6rpx 18rpx}.state-title{font-size:36rpx;font-weight:800}.state-subtitle{font-size:22rpx;color:#aaabb0}.section-card{margin-bottom:20rpx;padding:28rpx;border-radius:30rpx;background:rgba(29,32,37,.45);border:1rpx solid rgba(255,255,255,.07);backdrop-filter:blur(24rpx)}.section-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14rpx}.section-title-wrap{display:flex;align-items:center;gap:16rpx}.section-icon{width:52rpx;height:52rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);padding:8rpx;box-sizing:border-box}.section-title{display:block;font-size:32rpx;font-weight:800;line-height:1.2}.section-subtitle{display:block;margin-top:4rpx;font-size:22rpx;color:#aaabb0}.section-count{font-size:22rpx;color:#c19cff;background:rgba(193,156,255,.12);padding:8rpx 14rpx;border-radius:9999rpx}.section-progress{display:block;margin:0 0 18rpx 68rpx;font-size:22rpx;color:#8fe3ff}.grid{display:grid;grid-template-columns:1fr 1fr;gap:18rpx}.item{position:relative;overflow:hidden;padding:28rpx;border-radius:28rpx;background:rgba(29,32,37,.56);backdrop-filter:blur(24rpx);border:1rpx solid rgba(255,255,255,.06);box-shadow:0 20rpx 40rpx rgba(0,0,0,.06);min-height:220rpx}.accent-tertiary{box-shadow:0 20rpx 40rpx rgba(255,64,129,.06)}.accent-secondary{box-shadow:0 20rpx 40rpx rgba(0,229,255,.06)}.accent-primary{box-shadow:0 20rpx 40rpx rgba(127,0,255,.06)}.item-icon{width:76rpx;height:76rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;margin-bottom:24rpx;overflow:hidden}.item-icon-img{width:44rpx;height:44rpx}.item-title{display:block;font-size:30rpx;font-weight:700;margin-bottom:8rpx}.item-sub{display:block;font-size:22rpx;color:#aaabb0}

View File

@ -2,15 +2,61 @@ const app = getApp()
Page({
data: {
entryId: null,
entryType: 'word',
enText: '',
zhText: '',
exampleText: '',
loading: false,
error: '',
isEditMode: false,
},
setType(e) {
this.setData({ entryType: e.currentTarget.dataset.type })
onLoad(options) {
const entryId = options.id ? String(options.id) : null
this.setData({ entryId, isEditMode: !!entryId })
if (entryId) this.loadEntry(entryId)
},
loadEntry(entryId) {
if (!app.globalData.openid) {
this.setData({ error: '请先登录' })
return
}
this.setData({ loading: true, error: '' })
wx.request({
url: `${app.globalData.baseUrl}/api/v1/entries/${entryId}`,
header: { 'X-OpenID': app.globalData.openid },
success: (res) => {
const entry = res.data?.data
if (!entry) {
this.setData({ error: '词条不存在' })
return
}
this.setData({
entryType: entry.entry_type || 'word',
enText: entry.en_text || '',
zhText: entry.zh_text || '',
exampleText: entry.example_text || '',
})
},
fail: () => this.setData({ error: '词条加载失败' }),
complete: () => this.setData({ loading: false }),
})
},
switchType(e) {
const entryType = e.currentTarget.dataset.type
if (this.data.isEditMode && entryType !== this.data.entryType) {
wx.showModal({
title: '切换类型',
content: '切换类型后,当前内容仍会保留,但字段含义会变成新的类型。是否继续?',
confirmText: '继续切换',
cancelText: '取消',
success: (res) => {
if (res.confirm) this.setData({ entryType })
},
})
return
}
this.setData({ entryType })
},
onEnInput(e) {
this.setData({ enText: e.detail.value })
@ -29,28 +75,41 @@ Page({
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
const method = this.data.isEditMode ? 'PUT' : 'POST'
const url = this.data.isEditMode
? `${app.globalData.baseUrl}/api/v1/entries/${this.data.entryId}`
: `${app.globalData.baseUrl}/api/v1/entries`
this.setData({ loading: true, error: '' })
wx.request({
url: `${app.globalData.baseUrl}/api/v1/entries`,
method: 'POST',
url,
method,
header: { 'Content-Type': 'application/json', 'X-OpenID': app.globalData.openid },
data: {
entry_type: this.data.entryType,
en_text: this.data.enText,
zh_text: this.data.zhText,
example_text: this.data.exampleText,
source_type: 'manual',
upload_date: new Date().toISOString().slice(0, 10),
},
data: this.data.isEditMode
? {
entry_type: this.data.entryType,
en_text: this.data.enText,
zh_text: this.data.zhText,
example_text: this.data.exampleText,
}
: {
entry_type: this.data.entryType,
en_text: this.data.enText,
zh_text: this.data.zhText,
example_text: this.data.exampleText,
source_type: 'manual',
upload_date: new Date().toISOString().slice(0, 10),
},
success: () => {
wx.showToast({ title: '保存成功', icon: 'success' })
this.setData({
enText: '',
zhText: '',
exampleText: '',
})
wx.showToast({ title: this.data.isEditMode ? '更新成功' : '保存成功', icon: 'success' })
if (!this.data.isEditMode) {
this.setData({
enText: '',
zhText: '',
exampleText: '',
})
}
},
fail: () => this.setData({ error: '保存失败' }),
fail: () => this.setData({ error: this.data.isEditMode ? '更新失败' : '保存失败' }),
complete: () => this.setData({ loading: false }),
})
},

View File

@ -1,39 +1,65 @@
<view class="page">
<view class="ambient-glow"></view>
<view class="bg-orb orb1"></view>
<view class="bg-orb orb2"></view>
<view class="topbar">
<button class="close-btn" bindtap="goBack">✕</button>
<view class="spacer"></view>
<view class="back-chip" bindtap="goBack">←</view>
<view class="mode-chip {{isEditMode ? 'mode-chip-edit' : 'mode-chip-add'}}">{{isEditMode ? '编辑模式' : '新增模式'}}</view>
</view>
<scroll-view scroll-y class="content">
<view class="header">
<text class="title">添加新内容</text>
<text class="subtitle">构建你的专属知识库</text>
<view class="hero-card {{entryType === 'sentence' ? 'hero-pink' : 'hero-cyan'}}">
<view class="hero-copy">
<text class="eyebrow">{{isEditMode ? '更新词条' : '创建词条'}}</text>
<text class="title">{{isEditMode ? '编辑内容' : '添加新内容'}}</text>
<text class="subtitle">{{entryType === 'word' ? '记录单词、释义和例句' : '记录句子、翻译和语境说明'}}</text>
</view>
<view class="hero-badge">{{entryType === 'sentence' ? '句' : '词'}}</view>
</view>
<view class="segmented">
<view class="seg {{entryType === 'word' ? 'active' : ''}}" data-type="word" bindtap="switchType">单词</view>
<view class="seg {{entryType === 'sentence' ? 'active' : ''}}" data-type="sentence" bindtap="switchType">句子</view>
<view class="segment-shell">
<view class="segmented">
<view class="seg {{entryType === 'word' ? 'active' : ''}}" data-type="word" bindtap="switchType">单词</view>
<view class="seg {{entryType === 'sentence' ? 'active' : ''}}" data-type="sentence" bindtap="switchType">句子</view>
</view>
</view>
<view class="tip-card" wx:if="{{isEditMode}}">
<text class="tip-title">编辑提示</text>
<text class="tip-text">切换类型会改变字段含义,但不会自动清空你输入的内容。</text>
</view>
<view class="form">
<view class="field">
<text class="label">英文内容</text>
<view class="input-shell"><input class="input" placeholder="输入英文内容..." bindinput="onEnInput" value="{{enText}}" /></view>
<text class="label">{{entryType === 'word' ? '单词' : '句子'}}</text>
<view class="input-shell">
<input
class="input"
placeholder="{{entryType === 'word' ? '输入单词...' : '输入完整句子...'}}"
bindinput="onEnInput"
value="{{enText}}"
/>
</view>
</view>
<view class="field">
<text class="label">中文释义</text>
<view class="input-shell"><input class="input small" placeholder="输入对应的中文含义..." bindinput="onZhInput" value="{{zhText}}" /></view>
<text class="label">{{entryType === 'word' ? '中文释义' : '中文翻译'}}</text>
<view class="input-shell">
<input class="input small" placeholder="{{entryType === 'word' ? '输入对应的中文含义...' : '输入句子的中文翻译...'}}" bindinput="onZhInput" value="{{zhText}}" />
</view>
</view>
<view class="field">
<view class="field" wx:if="{{entryType === 'word'}}">
<text class="label">例句 <text class="muted">(可选)</text></text>
<view class="input-shell textarea-shell"><textarea class="textarea" placeholder="添加包含该词的典型例句..." bindinput="onExampleInput" value="{{exampleText}}" /></view>
</view>
<view class="field" wx:else>
<text class="label">补充说明 <text class="muted">(可选)</text></text>
<view class="input-shell textarea-shell"><textarea class="textarea" placeholder="可填写语境、来源或记忆提示..." bindinput="onExampleInput" value="{{exampleText}}" /></view>
</view>
</view>
<view class="action-bar">
<button class="cancel-btn" bindtap="goBack">取消</button>
<button class="save-btn" bindtap="save">保存</button>
<button class="save-btn" bindtap="save">{{isEditMode ? '更新' : '保存'}}</button>
</view>
</scroll-view>
</view>

View File

@ -1,153 +1,274 @@
page {
background: #0c0e12;
color: #f6f6fc
color: #f6f6fc;
}
.page {
min-height: 100vh;
background: #0c0e12;
position: relative;
overflow-x: hidden;
box-sizing: border-box
overflow: hidden;
box-sizing: border-box;
}
.ambient-glow {
.bg-orb {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 80vw;
max-width: 800px;
height: 400px;
background: radial-gradient(circle, rgba(193, 156, 255, .08) 0%, rgba(12, 14, 18, 0) 70%);
border-radius: 9999rpx;
filter: blur(110rpx);
pointer-events: none;
z-index: 0
z-index: 0;
}
.orb1 {
width: 48vw;
height: 48vw;
top: -12vw;
right: -14vw;
background: rgba(193, 156, 255, 0.12);
}
.orb2 {
width: 56vw;
height: 56vw;
left: -18vw;
bottom: -16vw;
background: rgba(0, 227, 253, 0.08);
}
.topbar {
position: relative;
z-index: 10;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx
padding: 24rpx 32rpx 8rpx;
}
.close-btn {
width: 72rpx;
height: 72rpx;
.back-chip,
.mode-chip {
height: 68rpx;
padding: 0 24rpx;
border-radius: 9999rpx;
background: #000;
color: #aaabb0;
display: flex;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1rpx solid rgba(255, 255, 255, .1)
font-size: 24rpx;
font-weight: 700;
border: 1rpx solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
}
.spacer {
width: 72rpx;
height: 72rpx
.back-chip {
width: 68rpx;
padding: 0;
font-size: 30rpx;
}
.mode-chip-add {
color: #00e3fd;
}
.mode-chip-edit {
color: #c19cff;
}
.content {
position: relative;
z-index: 10;
z-index: 1;
height: calc(100vh - 120rpx);
padding: 16rpx 32rpx 48rpx;
box-sizing: border-box
padding: 16rpx 32rpx 40rpx;
box-sizing: border-box;
}
.header {
margin-bottom: 40rpx
.hero-card {
position: relative;
overflow: hidden;
border-radius: 34rpx;
padding: 34rpx;
border: 1rpx solid rgba(255, 255, 255, 0.08);
background: rgba(29, 32, 37, 0.72);
box-shadow: 0 18rpx 60rpx rgba(0, 0, 0, 0.18);
margin-bottom: 28rpx;
}
.hero-card::after {
content: '';
position: absolute;
inset: auto -40rpx -40rpx auto;
width: 180rpx;
height: 180rpx;
border-radius: 9999rpx;
filter: blur(24rpx);
opacity: 0.4;
}
.hero-cyan::after {
background: rgba(0, 227, 253, 0.45);
}
.hero-pink::after {
background: rgba(255, 108, 149, 0.45);
}
.hero-copy {
position: relative;
z-index: 1;
max-width: 520rpx;
}
.eyebrow {
display: block;
color: #aaabb0;
font-size: 22rpx;
letter-spacing: 2rpx;
margin-bottom: 8rpx;
}
.title {
display: block;
font-size: 56rpx;
font-size: 54rpx;
line-height: 1.1;
font-weight: 800;
line-height: 1.1
}
.subtitle {
display: block;
margin-top: 10rpx;
margin-top: 12rpx;
color: #aaabb0;
font-size: 26rpx
font-size: 25rpx;
line-height: 1.6;
}
.hero-badge {
position: absolute;
right: 30rpx;
top: 30rpx;
z-index: 1;
width: 84rpx;
height: 84rpx;
border-radius: 9999rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 800;
color: #f6f6fc;
background: rgba(255, 255, 255, 0.06);
border: 1rpx solid rgba(255, 255, 255, 0.08);
}
.segment-shell {
display: flex;
justify-content: flex-start;
margin-bottom: 18rpx;
}
.segmented {
display: flex;
width: fit-content;
padding: 8rpx;
background: #000;
border: 1rpx solid rgba(255, 255, 255, .1);
background: rgba(23, 26, 31, 0.85);
border: 1rpx solid rgba(255, 255, 255, 0.08);
border-radius: 9999rpx;
margin-bottom: 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, .2)
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.14);
}
.seg {
padding: 18rpx 32rpx;
padding: 18rpx 34rpx;
border-radius: 9999rpx;
color: #aaabb0;
font-size: 24rpx
font-size: 24rpx;
font-weight: 600;
}
.active {
background: #1d2025;
color: #c19cff;
font-weight: 700
box-shadow: 0 0 24rpx rgba(193, 156, 255, 0.18);
}
.tip-card {
margin-bottom: 26rpx;
padding: 22rpx 26rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.04);
border: 1rpx solid rgba(255, 255, 255, 0.06);
}
.tip-title {
display: block;
font-size: 24rpx;
font-weight: 700;
color: #f6f6fc;
}
.tip-text {
display: block;
margin-top: 8rpx;
color: #aaabb0;
font-size: 22rpx;
line-height: 1.6;
}
.form {
display: flex;
flex-direction: column;
gap: 28rpx
gap: 26rpx;
}
.field {
display: flex;
flex-direction: column
flex-direction: column;
}
.label {
font-size: 22rpx;
color: #aaabb0;
font-weight: 700;
margin: 0 0 12rpx 4rpx
margin: 0 0 12rpx 4rpx;
}
.muted {
color: rgba(170, 171, 176, .5);
font-weight: 400
color: rgba(170, 171, 176, 0.55);
font-weight: 400;
}
.input-shell {
min-height: 92rpx;
border-radius: 24rpx;
background: #000;
border: 1rpx solid rgba(255, 255, 255, .1);
background: rgba(255, 255, 255, 0.04);
border: 1rpx solid rgba(255, 255, 255, 0.08);
box-sizing: border-box;
display: flex;
align-items: center
align-items: center;
backdrop-filter: blur(16rpx);
}
.input-shell:focus-within {
border-color: rgba(193, 156, 255, 0.45);
box-shadow: 0 0 0 1rpx rgba(193, 156, 255, 0.12);
}
.input {
width: 100%;
padding: 28rpx 26rpx;
height: 100%;
min-height: 92rpx;
padding: 0 26rpx;
font-size: 30rpx;
line-height: 92rpx;
color: #f6f6fc;
box-sizing: border-box
box-sizing: border-box;
caret-color: #c19cff;
}
.small {
font-size: 28rpx
font-size: 28rpx;
line-height: 92rpx;
}
.textarea-shell {
background: #111318
background: rgba(17, 19, 24, 0.95);
align-items: flex-start;
}
.textarea {
@ -155,33 +276,56 @@ page {
min-height: 220rpx;
padding: 24rpx 26rpx;
font-size: 28rpx;
line-height: 1.6;
color: #f6f6fc;
box-sizing: border-box
box-sizing: border-box;
caret-color: #c19cff;
}
.input::placeholder,
.textarea::placeholder {
color: rgba(170, 171, 176, 0.72);
}
.input::-webkit-input-placeholder,
.textarea::-webkit-input-placeholder {
color: rgba(170, 171, 176, 0.72);
}
.input::-moz-placeholder,
.textarea::-moz-placeholder {
color: rgba(170, 171, 176, 0.72);
}
.input:-ms-input-placeholder,
.textarea:-ms-input-placeholder {
color: rgba(170, 171, 176, 0.72);
}
.action-bar {
display: flex;
justify-content: flex-end;
gap: 18rpx;
padding-top: 28rpx;
margin-top: 36rpx;
border-top: 1rpx solid rgba(255, 255, 255, .05)
}
.cancel-btn,
.save-btn {
flex: 1;
border-radius: 9999rpx;
padding: 22rpx 34rpx;
font-weight: 700
font-weight: 700;
font-size: 26rpx;
}
.cancel-btn {
background: #000;
background: rgba(255, 255, 255, 0.04);
color: #f6f6fc;
border: 1rpx solid rgba(255, 255, 255, .1)
border: 1rpx solid rgba(255, 255, 255, 0.08);
}
.save-btn {
background: linear-gradient(90deg, #c19cff, #9146ff);
color: #fff
color: #fff;
box-shadow: 0 14rpx 30rpx rgba(145, 70, 255, 0.24);
}

143
me/me.js
View File

@ -3,11 +3,15 @@ const app = getApp()
Page({
data: {
name: '',
nickname: '',
days: 0,
badges: 0,
openid: '',
avatarUrl: '',
loading: false,
error: '',
showNicknameModal: false,
nicknameDraft: '',
},
onShow() {
this.loadProfile()
@ -19,9 +23,141 @@ Page({
this.setData({ loading: false, error: '登录失败,请重试' })
return
}
this.setData({ openid: app.globalData.openid, name: '考研搭子' })
this.loadAchievements()
this.setData({ loading: false })
wx.request({
url: `${app.globalData.baseUrl}/api/v1/auth/profile`,
method: 'GET',
data: { openid: app.globalData.openid },
success: (res) => {
const profile = res.data?.data || {}
const avatarUrl = profile.avatar_url || wx.getStorageSync(`avatarUrl:${app.globalData.openid}`) || ''
const nickname = profile.nickname || '考研搭子'
const normalizedAvatar = avatarUrl.startsWith('http') ? avatarUrl : avatarUrl ? `${app.globalData.baseUrl}${avatarUrl}` : ''
this.setData({
openid: app.globalData.openid,
name: nickname,
nickname,
avatarUrl: normalizedAvatar,
nicknameDraft: nickname,
})
},
fail: () => {
const storedAvatarUrl = wx.getStorageSync(`avatarUrl:${app.globalData.openid}`) || ''
const normalizedAvatar = storedAvatarUrl.startsWith('http') ? storedAvatarUrl : storedAvatarUrl ? `${app.globalData.baseUrl}${storedAvatarUrl}` : ''
this.setData({ openid: app.globalData.openid, name: '考研搭子', nickname: '考研搭子', avatarUrl: normalizedAvatar, nicknameDraft: '考研搭子' })
},
complete: () => {
this.loadAchievements()
this.setData({ loading: false })
},
})
},
openNicknameModal() {
this.setData({ showNicknameModal: true, nicknameDraft: this.data.nickname || this.data.name || '考研搭子' })
},
onAvatarError() {
this.setData({ avatarUrl: '' })
},
closeNicknameModal() {
this.setData({ showNicknameModal: false, nicknameDraft: this.data.nickname || this.data.name || '考研搭子' })
},
onNicknameInput(e) {
this.setData({ nicknameDraft: e.detail.value })
},
saveNickname() {
if (!app.globalData.openid) return
const nickname = (this.data.nicknameDraft || '').trim()
if (!nickname) {
wx.showToast({ title: '昵称不能为空', icon: 'none' })
return
}
wx.request({
url: `${app.globalData.baseUrl}/api/v1/auth/profile`,
method: 'PUT',
header: { 'Content-Type': 'application/json' },
data: { openid: app.globalData.openid, nickname },
success: (res) => {
const profile = res.data?.data || {}
const nextNickname = profile.nickname || nickname
this.setData({ nickname: nextNickname, name: nextNickname, nicknameDraft: nextNickname, showNicknameModal: false })
wx.showToast({ title: '昵称已保存', icon: 'success' })
},
fail: () => wx.showToast({ title: '保存失败', icon: 'none' }),
})
},
chooseAvatar() {
if (!app.globalData.openid) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const localPath = res.tempFilePaths?.[0] || ''
if (!localPath) return
wx.getImageInfo({
src: localPath,
success: (info) => {
const size = 320
const side = Math.min(info.width, info.height)
const x = Math.max(0, Math.floor((info.width - side) / 2))
const y = Math.max(0, Math.floor((info.height - side) / 2))
const ctx = wx.createCanvasContext('avatar-canvas', this)
ctx.clearRect(0, 0, size, size)
ctx.save()
ctx.beginPath()
ctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI)
ctx.clip()
ctx.drawImage(localPath, x, y, side, side, 0, 0, size, size)
ctx.draw(false, () => {
wx.canvasToTempFilePath({
canvasId: 'avatar-canvas',
width: size,
height: size,
destWidth: size,
destHeight: size,
quality: 0.85,
success: (canvasRes) => {
const uploadPath = canvasRes.tempFilePath || localPath
wx.uploadFile({
url: `${app.globalData.baseUrl}/api/v1/auth/avatar?openid=${encodeURIComponent(app.globalData.openid)}`,
filePath: uploadPath,
name: 'file',
success: (uploadRes) => {
try {
const payload = JSON.parse(uploadRes.data)
const avatarUrl = payload?.data?.avatar_url || ''
if (!avatarUrl) throw new Error('missing avatar_url')
const fullUrl = avatarUrl.startsWith('http') ? avatarUrl : `${app.globalData.baseUrl}${avatarUrl}`
wx.setStorageSync(`avatarUrl:${app.globalData.openid}`, fullUrl)
this.setData({ avatarUrl: fullUrl })
wx.request({
url: `${app.globalData.baseUrl}/api/v1/auth/profile`,
method: 'GET',
data: { openid: app.globalData.openid },
success: (profileRes) => {
const profile = profileRes.data?.data || {}
const nextNickname = profile.nickname || '考研搭子'
this.setData({ name: nextNickname, avatarUrl: fullUrl })
},
})
wx.showToast({ title: '头像已上传', icon: 'success' })
} catch (err) {
wx.showToast({ title: '头像上传失败', icon: 'none' })
}
},
fail: () => wx.showToast({ title: '头像上传失败', icon: 'none' }),
})
},
fail: () => wx.showToast({ title: '头像处理失败', icon: 'none' }),
}, this)
})
},
fail: () => wx.showToast({ title: '头像处理失败', icon: 'none' }),
})
},
})
},
loadAchievements() {
if (!app.globalData.openid) {
@ -54,4 +190,5 @@ Page({
wx.navigateTo({ url: '/pages/review-config/review-config' })
},
goToMe() {},
noop() {},
})

View File

@ -1,5 +1,6 @@
<view class="page">
<scroll-view scroll-y class="content">
<canvas canvas-id="avatar-canvas" class="avatar-canvas"></canvas>
<view wx:if="{{loading}}" class="empty-state">
<text class="empty-title">登录中...</text>
<text class="empty-subtitle">正在获取你的登录态</text>
@ -11,13 +12,16 @@
</view>
<view wx:else class="hero-section">
<view class="hero-avatar-wrap">
<view class="hero-avatar-wrap" bindtap="chooseAvatar">
<view class="hero-avatar-border">
<image class="hero-avatar" src="https://picsum.photos/241" mode="aspectFill" />
<image class="hero-avatar" src="{{avatarUrl || '/assets/images/default-avatar.png'}}" mode="aspectFill" binderror="onAvatarError" />
<view class="avatar-mask">
<text class="avatar-mask-text">点击更换</text>
</view>
</view>
</view>
<view class="hero-info">
<text class="hero-name">{{name}}</text>
<text class="hero-name" bindtap="openNicknameModal">{{name}}</text>
<view class="hero-badge">
<view class="pulse-dot"></view>
<text class="badge-text">累计学习 {{days}} 天</text>
@ -30,7 +34,7 @@
<text class="empty-subtitle">完成复习和打卡后就会逐渐点亮</text>
</view>
<view wx:if="{{openid && badges > 0}}" class="grid-2">
<view wx:if="{{openid}}" class="grid-2">
<view class="glass-card accent-tertiary" bindtap="goTo" data-url="/pages/achievements/achievements">
<view class="card-glow"></view>
<view class="card-icon icon-tertiary">🏅</view>
@ -77,6 +81,20 @@
</view>
</scroll-view>
<view wx:if="{{showNicknameModal}}" class="modal-mask" bindtap="closeNicknameModal">
<view class="modal-card" catchtap="noop">
<view class="modal-header">
<text class="modal-title">修改昵称</text>
<text class="modal-close" bindtap="closeNicknameModal">×</text>
</view>
<input class="modal-input" value="{{nicknameDraft}}" placeholder="请输入昵称" maxlength="100" focus="true" bindinput="onNicknameInput" />
<view class="modal-actions">
<button class="modal-btn ghost" bindtap="closeNicknameModal">取消</button>
<button class="modal-btn primary" bindtap="saveNickname">保存</button>
</view>
</view>
</view>
<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="goToReview"><text class="nav-icon">📖</text><text class="nav-label">复习</text></view>

File diff suppressed because one or more lines are too long

View File

@ -1,27 +1,52 @@
const app = getApp()
const groupByType = (list) => ({
word: list.filter((item) => item.entry_type !== 'sentence'),
sentence: list.filter((item) => item.entry_type === 'sentence'),
})
Page({
data: {
reviewList: [],
masteredList: [],
reviewWords: [],
reviewSentences: [],
masteredWords: [],
masteredSentences: [],
reviewWordCollapsed: false,
reviewSentenceCollapsed: false,
masteredWordCollapsed: false,
masteredSentenceCollapsed: false,
loading: false,
error: '',
},
onShow() {
this.loadData()
},
toggleSection(e) {
const key = e.currentTarget.dataset.key
this.setData({ [key]: !this.data[key] })
},
loadData() {
this.setData({ loading: true, error: '' })
wx.request({
url: `${app.globalData.baseUrl}/api/v1/progress/review`,
header: { 'X-OpenID': app.globalData.openid || '' },
success: (res) => this.setData({ reviewList: res.data?.data || [] }),
success: (res) => {
const reviewList = res.data?.data || []
const grouped = groupByType(reviewList)
this.setData({ reviewList, reviewWords: grouped.word, reviewSentences: grouped.sentence })
},
fail: () => this.setData({ error: '待复习数据加载失败' }),
})
wx.request({
url: `${app.globalData.baseUrl}/api/v1/progress/mastered`,
header: { 'X-OpenID': app.globalData.openid || '' },
success: (res) => this.setData({ masteredList: res.data?.data || [] }),
success: (res) => {
const masteredList = res.data?.data || []
const grouped = groupByType(masteredList)
this.setData({ masteredList, masteredWords: grouped.word, masteredSentences: grouped.sentence })
},
fail: () => this.setData({ error: '已掌握数据加载失败' }),
complete: () => this.setData({ loading: false }),
})

View File

@ -9,8 +9,12 @@
</view>
<view class="tabs">
<view class="tab tab-active">待复习</view>
<view class="tab">已掌握</view>
<view class="tab tab-active">
<text class="tab-label">待复习</text>
</view>
<view class="tab">
<text class="tab-label">已掌握</text>
</view>
</view>
<view wx:if="{{loading}}" class="empty-state">
@ -23,50 +27,136 @@
</view>
<view wx:else>
<view class="grid">
<block wx:for="{{reviewList}}" wx:key="id">
<view class="card card-pink">
<view class="card-glow card-glow-pink"></view>
<view class="card-head">
<view>
<text class="word word-large">{{item.en_text}}</text>
<text class="desc desc-spaced">{{item.zh_text}}</text>
</view>
<view class="badge badge-pink">🔥</view>
</view>
<view class="progress-meta">
<text class="progress-label">神经元连接强度</text>
<text class="progress-value progress-pink">{{item.mastery_score}}%</text>
</view>
<view class="progress-track">
<view class="progress-fill progress-fill-pink" style="width: {{item.mastery_score}}%;"></view>
</view>
<view class="action-btn">移至已掌握</view>
<view class="section">
<view class="section-head" bindtap="toggleSection" data-key="reviewWordCollapsed">
<view class="section-title-wrap">
<text class="section-pill section-pill-pink">待复习</text>
<text class="section-title">单词</text>
</view>
</block>
<block wx:for="{{masteredList}}" wx:key="id">
<view class="card card-cyan">
<view class="card-glow card-glow-cyan"></view>
<view class="card-head">
<view>
<text class="word word-large">{{item.en_text}}</text>
<text class="desc desc-spaced">{{item.zh_text}}</text>
</view>
<view class="badge badge-cyan">✓</view>
</view>
<view class="progress-meta">
<text class="progress-label">神经元连接强度</text>
<text class="progress-value progress-cyan">{{item.mastery_score}}%</text>
</view>
<view class="progress-track">
<view class="progress-fill progress-fill-cyan" style="width: {{item.mastery_score}}%;"></view>
</view>
<view class="action-btn">移至已掌握</view>
<view class="section-toggle-chip {{reviewWordCollapsed ? 'collapsed' : 'expanded'}}">
<text class="section-toggle-text">{{reviewWordCollapsed ? '展开' : '收起'}}</text>
</view>
</block>
</view>
<view wx:if="{{!reviewWordCollapsed}}" class="grid">
<block wx:for="{{reviewWords}}" wx:key="id">
<view class="card card-pink">
<view class="card-glow card-glow-pink"></view>
<view class="card-head">
<view>
<text class="word word-large">{{item.en_text}}</text>
<text class="desc desc-spaced">{{item.zh_text}}</text>
</view>
<view class="badge badge-pink">🔥</view>
</view>
<view class="progress-meta">
<text class="progress-label">神经元连接强度</text>
<text class="progress-value progress-pink">{{item.mastery_score}}%</text>
</view>
<view class="progress-track">
<view class="progress-fill progress-fill-pink" style="width: {{item.mastery_score}}%;"></view>
</view>
</view>
</block>
</view>
</view>
<view class="section">
<view class="section-head" bindtap="toggleSection" data-key="reviewSentenceCollapsed">
<view class="section-title-wrap">
<text class="section-pill section-pill-cyan">待复习</text>
<text class="section-title">句子</text>
</view>
<view class="section-toggle-chip {{reviewSentenceCollapsed ? 'collapsed' : 'expanded'}}">
<text class="section-toggle-text">{{reviewSentenceCollapsed ? '展开' : '收起'}}</text>
</view>
</view>
<view wx:if="{{!reviewSentenceCollapsed}}" class="grid">
<block wx:for="{{reviewSentences}}" wx:key="id">
<view class="card card-cyan">
<view class="card-glow card-glow-cyan"></view>
<view class="card-head">
<view>
<text class="word word-large">{{item.en_text}}</text>
<text class="desc desc-spaced">{{item.zh_text}}</text>
</view>
<view class="badge badge-cyan">📝</view>
</view>
<view class="progress-meta">
<text class="progress-label">神经元连接强度</text>
<text class="progress-value progress-cyan">{{item.mastery_score}}%</text>
</view>
<view class="progress-track">
<view class="progress-fill progress-fill-cyan" style="width: {{item.mastery_score}}%;"></view>
</view>
</view>
</block>
</view>
</view>
<view class="section">
<view class="section-head" bindtap="toggleSection" data-key="masteredWordCollapsed">
<view class="section-title-wrap">
<text class="section-pill section-pill-cyan">已掌握</text>
<text class="section-title">单词</text>
</view>
<view class="section-toggle-chip {{masteredWordCollapsed ? 'collapsed' : 'expanded'}}">
<text class="section-toggle-text">{{masteredWordCollapsed ? '展开' : '收起'}}</text>
</view>
</view>
<view wx:if="{{!masteredWordCollapsed}}" class="grid">
<block wx:for="{{masteredWords}}" wx:key="id">
<view class="card card-cyan">
<view class="card-glow card-glow-cyan"></view>
<view class="card-head">
<view>
<text class="word word-large">{{item.en_text}}</text>
<text class="desc desc-spaced">{{item.zh_text}}</text>
</view>
<view class="badge badge-cyan">✓</view>
</view>
<view class="progress-meta">
<text class="progress-label">神经元连接强度</text>
<text class="progress-value progress-cyan">{{item.mastery_score}}%</text>
</view>
<view class="progress-track">
<view class="progress-fill progress-fill-cyan" style="width: {{item.mastery_score}}%;"></view>
</view>
</view>
</block>
</view>
</view>
<view class="section">
<view class="section-head" bindtap="toggleSection" data-key="masteredSentenceCollapsed">
<view class="section-title-wrap">
<text class="section-pill section-pill-pink">已掌握</text>
<text class="section-title">句子</text>
</view>
<view class="section-toggle-chip {{masteredSentenceCollapsed ? 'collapsed' : 'expanded'}}">
<text class="section-toggle-text">{{masteredSentenceCollapsed ? '展开' : '收起'}}</text>
</view>
</view>
<view wx:if="{{!masteredSentenceCollapsed}}" class="grid">
<block wx:for="{{masteredSentences}}" wx:key="id">
<view class="card card-pink">
<view class="card-glow card-glow-pink"></view>
<view class="card-head">
<view>
<text class="word word-large">{{item.en_text}}</text>
<text class="desc desc-spaced">{{item.zh_text}}</text>
</view>
<view class="badge badge-pink">✓</view>
</view>
<view class="progress-meta">
<text class="progress-label">神经元连接强度</text>
<text class="progress-value progress-pink">{{item.mastery_score}}%</text>
</view>
<view class="progress-track">
<view class="progress-fill progress-fill-pink" style="width: {{item.mastery_score}}%;"></view>
</view>
</view>
</block>
</view>
</view>
<view wx:if="{{!reviewList.length && !masteredList.length}}" class="empty-state small">

View File

@ -1 +1 @@
page{background:#0c0e12;color:#f6f6fc}.page{min-height:100vh;background:#0c0e12;box-sizing:border-box;position:relative;overflow:hidden}.bg-orb{position:absolute;border-radius:9999rpx;filter:blur(120rpx);pointer-events:none;z-index:0}.bg-orb-right{width:40vw;height:40vw;top:-10vw;right:-10vw;background:rgba(193,156,255,.08)}.bg-orb-left{width:50vw;height:50vw;left:-15vw;bottom:-15vw;background:rgba(0,227,253,.06)}.content{height:100vh;padding:40rpx 32rpx;box-sizing:border-box;position:relative;z-index:1}.header{margin-bottom:28rpx}.title{display:block;font-size:56rpx;line-height:1.1;font-weight:800;letter-spacing:-1rpx}.subtitle{display:block;margin-top:10rpx;color:#aaabb0;font-size:24rpx;line-height:1.5}.tabs{display:flex;gap:16rpx;padding:6rpx;border:1rpx solid rgba(255,255,255,.05);background:rgba(23,26,31,.7);border-radius:9999rpx;width:max-content;box-shadow:0 8rpx 30rpx rgba(0,0,0,.12);margin-bottom:34rpx}.tab{padding:18rpx 34rpx;border-radius:9999rpx;color:#aaabb0;font-size:24rpx;font-weight:600}.tab-active{background:#1d2025;color:#c19cff;box-shadow:0 0 24rpx rgba(193,156,255,.22)}.grid{display:flex;flex-direction:column;gap:24rpx}.card{position:relative;overflow:hidden;padding:30rpx;background:rgba(29,32,37,.56);border:1rpx solid rgba(255,255,255,.08);border-radius:32rpx;box-shadow:0 16rpx 60rpx rgba(0,0,0,.12)}.card-glow{position:absolute;right:-40rpx;top:-40rpx;width:180rpx;height:180rpx;border-radius:9999rpx;filter:blur(30rpx);opacity:.35}.card-glow-pink{background:rgba(255,108,149,.5)}.card-glow-cyan{background:rgba(0,227,253,.45)}.card-head{display:flex;justify-content:space-between;align-items:flex-start;gap:20rpx;margin-bottom:18rpx}.word{display:block;font-weight:800;line-height:1.1}.word-large{font-size:48rpx;letter-spacing:-1rpx}.desc{display:block;color:#aaabb0;font-size:22rpx}.desc-spaced{margin-top:10rpx}.badge{width:80rpx;height:80rpx;border-radius:9999rpx;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.04);border:1rpx solid rgba(255,255,255,.05);flex-shrink:0;font-size:28rpx}.badge-pink{color:#ff6c95}.badge-cyan{color:#00e3fd}.progress-meta{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:14rpx}.progress-label{font-size:20rpx;color:#aaabb0;letter-spacing:1rpx}.progress-value{font-size:28rpx;font-weight:800}.progress-pink{color:#ff6c95}.progress-cyan{color:#00e3fd}.progress-track{width:100%;height:8rpx;background:#23262c;border-radius:9999rpx;overflow:hidden;box-shadow:inset 0 0 0 1rpx rgba(255,255,255,.03);margin-bottom:20rpx}.progress-fill{height:100%;border-radius:9999rpx}.progress-fill-pink{background:linear-gradient(90deg,#ff769b,#ff6c95);box-shadow:0 0 20rpx rgba(255,108,149,.5)}.progress-fill-cyan{background:linear-gradient(90deg,#c19cff,#00e3fd);box-shadow:0 0 20rpx rgba(0,227,253,.45)}.action-btn{width:100%;padding:22rpx 0;border-radius:9999rpx;background:#1d2025;border:1rpx solid rgba(255,255,255,.05);color:#aaabb0;font-size:24rpx;font-weight:600;text-align:center}.empty-state{padding:60rpx 20rpx;text-align:center;color:#aaabb0}.empty-state.small{padding:40rpx 20rpx}.empty-title{display:block;font-size:28rpx;font-weight:700;color:#f6f6fc}.empty-subtitle{display:block;margin-top:10rpx;font-size:22rpx;color:#aaabb0}.error-banner{padding:24rpx 28rpx;border-radius:20rpx;background:rgba(167,1,56,.2);border:1rpx solid rgba(255,110,132,.35)}.error-text{color:#ffb2b9;font-size:24rpx}.cyan{color:#00e5ff}.purple{color:#c19cff}.pink{color:#ff4081}
page{background:#0c0e12;color:#f6f6fc}.page{min-height:100vh;background:#0c0e12;box-sizing:border-box;position:relative;overflow:hidden}.bg-orb{position:absolute;border-radius:9999rpx;filter:blur(120rpx);pointer-events:none;z-index:0}.bg-orb-right{width:40vw;height:40vw;top:-10vw;right:-10vw;background:rgba(193,156,255,.08)}.bg-orb-left{width:50vw;height:50vw;left:-15vw;bottom:-15vw;background:rgba(0,227,253,.06)}.content{height:100vh;padding:40rpx 32rpx;box-sizing:border-box;position:relative;z-index:1}.header{margin-bottom:28rpx}.title{display:block;font-size:56rpx;line-height:1.1;font-weight:800;letter-spacing:-1rpx}.subtitle{display:block;margin-top:10rpx;color:#aaabb0;font-size:24rpx;line-height:1.5}.tabs{display:flex;gap:16rpx;padding:8rpx;border:1rpx solid rgba(255,255,255,.05);background:rgba(23,26,31,.72);border-radius:9999rpx;width:max-content;box-shadow:0 8rpx 30rpx rgba(0,0,0,.12);margin-bottom:34rpx}.tab{min-width:150rpx;padding:16rpx 30rpx;border-radius:9999rpx;color:#aaabb0;font-size:24rpx;font-weight:700;display:flex;align-items:center;justify-content:center;transition:all .2s ease}.tab-active{background:linear-gradient(180deg,rgba(193,156,255,.18),rgba(193,156,255,.08));color:#d8c7ff;box-shadow:0 0 0 1rpx rgba(193,156,255,.12) inset,0 10rpx 26rpx rgba(193,156,255,.08)}.tab-label{line-height:1}.section{margin-top:34rpx}.section-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:18rpx;padding:14rpx 4rpx}.section-title-wrap{display:flex;align-items:center;gap:14rpx;flex-wrap:wrap}.section-pill{padding:8rpx 18rpx;border-radius:9999rpx;font-size:20rpx;font-weight:700;letter-spacing:1rpx;line-height:1}.section-pill-pink{background:rgba(255,108,149,.12);color:#ff98b0;border:1rpx solid rgba(255,108,149,.12)}.section-pill-cyan{background:rgba(0,227,253,.1);color:#7defff;border:1rpx solid rgba(0,227,253,.1)}.section-title{font-size:30rpx;font-weight:800;color:#f6f6fc;letter-spacing:-.5rpx}.section-toggle-chip{min-width:114rpx;padding:12rpx 22rpx;border-radius:9999rpx;display:flex;align-items:center;justify-content:center;border:1rpx solid rgba(255,255,255,.06);background:rgba(255,255,255,.04)}.section-toggle-chip.expanded{background:linear-gradient(180deg,rgba(193,156,255,.16),rgba(193,156,255,.08));border-color:rgba(193,156,255,.14)}.section-toggle-chip.collapsed{background:rgba(255,255,255,.04)}.section-toggle-text{font-size:22rpx;font-weight:700;color:#d8c7ff;line-height:1}.grid{display:flex;flex-direction:column;gap:24rpx}.card{position:relative;overflow:hidden;padding:30rpx;background:rgba(29,32,37,.56);border:1rpx solid rgba(255,255,255,.08);border-radius:32rpx;box-shadow:0 16rpx 60rpx rgba(0,0,0,.12)}.card-glow{position:absolute;right:-40rpx;top:-40rpx;width:180rpx;height:180rpx;border-radius:9999rpx;filter:blur(30rpx);opacity:.35}.card-glow-pink{background:rgba(255,108,149,.5)}.card-glow-cyan{background:rgba(0,227,253,.45)}.card-head{display:flex;justify-content:space-between;align-items:flex-start;gap:20rpx;margin-bottom:18rpx}.word{display:block;font-weight:800;line-height:1.1}.word-large{font-size:48rpx;letter-spacing:-1rpx}.desc{display:block;color:#aaabb0;font-size:22rpx}.desc-spaced{margin-top:10rpx}.badge{width:80rpx;height:80rpx;border-radius:9999rpx;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.04);border:1rpx solid rgba(255,255,255,.05);flex-shrink:0;font-size:28rpx}.badge-pink{color:#ff6c95}.badge-cyan{color:#00e3fd}.progress-meta{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:14rpx}.progress-label{font-size:20rpx;color:#aaabb0;letter-spacing:1rpx}.progress-value{font-size:28rpx;font-weight:800}.progress-pink{color:#ff6c95}.progress-cyan{color:#00e3fd}.progress-track{width:100%;height:8rpx;background:#23262c;border-radius:9999rpx;overflow:hidden;box-shadow:inset 0 0 0 1rpx rgba(255,255,255,.03);margin-bottom:20rpx}.progress-fill{height:100%;border-radius:9999rpx}.progress-fill-pink{background:linear-gradient(90deg,#ff769b,#ff6c95);box-shadow:0 0 20rpx rgba(255,108,149,.5)}.progress-fill-cyan{background:linear-gradient(90deg,#c19cff,#00e3fd);box-shadow:0 0 20rpx rgba(0,227,253,.45)}.action-btn{width:100%;padding:22rpx 0;border-radius:9999rpx;background:#1d2025;border:1rpx solid rgba(255,255,255,.05);color:#aaabb0;font-size:24rpx;font-weight:600;text-align:center}.empty-state{padding:60rpx 20rpx;text-align:center;color:#aaabb0}.empty-state.small{padding:40rpx 20rpx}.empty-title{display:block;font-size:28rpx;font-weight:700;color:#f6f6fc}.empty-subtitle{display:block;margin-top:10rpx;font-size:22rpx;color:#aaabb0}.error-banner{padding:24rpx 28rpx;border-radius:20rpx;background:rgba(167,1,56,.2);border:1rpx solid rgba(255,110,132,.35)}.error-text{color:#ffb2b9;font-size:24rpx}.cyan{color:#00e5ff}.purple{color:#c19cff}.pink{color:#ff4081}

View File

@ -2,7 +2,7 @@ const app = getApp()
Page({
data: {
timeRanges: ['今日', '本周', '本月', '自定义区间'],
timeRanges: ['今日', '本周', '本月'],
activeTimeRange: 0,
contentTypes: ['单词', '句子'],
activeContentType: 0,
@ -59,17 +59,13 @@ Page({
this.persistPatch({ questionCount })
},
startReview() {
const scopeTypeMap = ['today', 'week', 'month', 'custom']
const scopeTypeMap = ['today', 'week', 'month']
const contentTypeMap = ['word', 'sentence']
const questionModeMap = ['A', 'B', 'C']
const scope_type = scopeTypeMap[this.data.activeTimeRange]
const content_type = contentTypeMap[this.data.activeContentType]
const question_mode = questionModeMap[this.data.activeMode]
const today = new Date()
const yyyy = today.getFullYear()
const mm = String(today.getMonth() + 1).padStart(2, '0')
const dd = String(today.getDate()).padStart(2, '0')
const scope_config = scope_type === 'custom' ? { start_date: `${yyyy}-${mm}-${dd}`, end_date: `${yyyy}-${mm}-${dd}` } : {}
const scope_config = {}
this.setData({ loading: true, error: '' })
wx.request({
url: `${app.globalData.baseUrl}/api/v1/review/generate`,

View File

@ -18,8 +18,12 @@ Page({
loadResult() {
const sessionId = app.globalData.reviewSessionMeta?.id
const fallbackSession = app.globalData.reviewSession
if (!sessionId && !fallbackSession?.completed) {
this.setData({ error: '暂无复习结果' })
if (!sessionId) {
if (fallbackSession?.completed) {
this.setData({ error: '本地复习已完成,但结果会话信息已丢失' })
} else {
this.setData({ error: '暂无复习结果' })
}
return
}
this.setData({ loading: true, error: '' })

View File

@ -12,7 +12,12 @@
</view>
</view>
<view wx:if="{{!total}}" class="empty-state">
<view wx:if="{{error}}" class="empty-state">
<text class="empty-title">{{error}}</text>
<text class="empty-subtitle">可重新进入结果页,或再完成一次复习会话</text>
</view>
<view wx:elif="{{!total}}" class="empty-state">
<text class="empty-title">暂无复习结果</text>
<text class="empty-subtitle">请先完成一次复习会话</text>
</view>
@ -44,7 +49,7 @@
<view class="breakdown">
<view class="break-item">
<text class="break-num cyan">{{mastered}}</text>
<text class="break-label">新增已掌握</text>
<text class="break-label">已掌握</text>
</view>
<view class="break-item">
<text class="break-num pink">{{reviewNeeded}}</text>
@ -59,6 +64,7 @@
<text class="break-label">退步题目</text>
</view>
</view>
</view>
<view class="actions">

View File

@ -71,7 +71,10 @@ Page({
blank_prompt: item.blank_prompt || item.prompt,
}
}
return item
return {
...item,
entry_type: item.entry_type || 'word',
}
})
app.globalData.reviewSession = {
deck: questions,
@ -113,7 +116,7 @@ Page({
app.globalData.reviewSession.currentAnswer = answer
this.setData({ answer, showFeedback: false, revealAnswer: '', feedback: '', feedbackType: '', judged: false })
},
submitAnswer(showExplanation = false) {
submitAnswer(showExplanation = false, advanceAfterSubmit = false) {
const session = app.globalData.reviewSession
const current = session.deck[session.currentIndex]
if (!current || this.submitting || this.judged) return
@ -131,8 +134,7 @@ Page({
header: { 'Content-Type': 'application/json', 'X-OpenID': app.globalData.openid || '' },
data: {
session_id: app.globalData.reviewSessionMeta.id,
entry_id: current.entry_id,
question_mode: current.question_mode,
record_id: current.record_id,
user_answer: answer,
},
success: (res) => {
@ -147,7 +149,7 @@ Page({
revealAnswer: shouldShowAnswer ? `正确答案:${result.correct_answer}` : '',
showFeedback: true,
})
if (this.data.settings.autoNextQuestion) {
if (this.data.settings.autoNextQuestion || advanceAfterSubmit) {
setTimeout(() => this.nextQuestion(), shouldShowAnswer ? 1200 : 0)
}
},
@ -163,7 +165,7 @@ Page({
const session = app.globalData.reviewSession
if (!session.deck.length) return
if (!this.judged) {
wx.showToast({ title: '请先提交答案', icon: 'none' })
this.submitAnswer(true, true)
return
}
this.judged = false
@ -175,6 +177,10 @@ Page({
method: 'POST',
header: { 'X-OpenID': app.globalData.openid || '' },
success: () => wx.navigateTo({ url: '/pages/review-result/review-result' }),
fail: () => {
wx.showToast({ title: '提交结果失败,已本地完成', icon: 'none' })
wx.navigateTo({ url: '/pages/review-result/review-result' })
},
})
return
}

View File

@ -15,7 +15,9 @@
<view wx:else class="glass-card question-card ghost-border">
<view class="ambient-glow"></view>
<view class="mode-tag">{{questions[current-1].question_mode || 'C'}}</view>
<view class="mode-tag-wrap">
<text class="mode-tag">{{questions[current-1].entry_type === 'sentence' ? '句子' : '单词'}} · {{questions[current-1].question_mode || 'C'}}</text>
</view>
<view class="meaning-wrap" wx:if="{{meaning}}">
<text class="meaning">{{meaning}}</text>
</view>
@ -26,6 +28,7 @@
<view wx:else class="word-wrap">
<text class="word">{{word}}</text>
</view>
<view class="entry-type-hint" wx:if="{{questions[current-1].entry_type === 'sentence'}}">当前题目来自句子库,注意上下文表达。</view>
<view class="input-shell">
<input class="answer" value="{{answer}}" placeholder="输入答案..." bindinput="onAnswerInput" />

View File

@ -1 +1 @@
page{background:#0c0e12;color:#f6f6fc}.page{min-height:100vh;background:#0c0e12}.topbar{display:flex;justify-content:space-between;align-items:center;padding:24rpx 32rpx;background:rgba(12,14,18,.8);border-bottom:1rpx solid rgba(255,255,255,.08);backdrop-filter:blur(24px);box-shadow:0 1rpx 20rpx rgba(0,0,0,.08)}.topbar-left{display:flex;align-items:center;gap:18rpx}.avatar-shell{width:80rpx;height:80rpx;border-radius:9999rpx;overflow:hidden;border:2rpx solid rgba(193,156,255,.2);flex-shrink:0}.avatar-img{width:100%;height:100%}.brand-text{font-size:38rpx;font-weight:800;color:#c19cff}.topbar-icon{width:72rpx;height:72rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center}.content{height:calc(100vh - 120rpx);padding:32rpx;box-sizing:border-box}.progress-box{margin-bottom:28rpx}.progress-head{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:16rpx}.progress-label{font-size:24rpx;color:#aaabb0}.progress-count{font-size:28rpx;font-weight:700;color:#00e3fd}.progress-total{color:#aaabb0;font-size:22rpx;font-weight:400}.progress-bar{height:12rpx;background:#23262c;border-radius:9999rpx;overflow:hidden}.ghost-border{border:1rpx solid rgba(255,255,255,.1)}.progress-fill{height:100%;background:linear-gradient(90deg,#c19cff,#00e3fd)}.glass-card{background:rgba(29,32,37,.4);backdrop-filter:blur(24rpx)}.question-card{position:relative;overflow:hidden;border-radius:32rpx;padding:40rpx 32rpx;text-align:center;box-shadow:0 20rpx 40rpx rgba(0,0,0,.4);border:1rpx solid rgba(255,255,255,.1)}.ambient-glow{position:absolute;inset:0;background:linear-gradient(to bottom,rgba(193,156,255,.05),transparent);pointer-events:none}.meaning-wrap{margin-bottom:32rpx}.meaning{display:block;color:#aaabb0;font-size:28rpx;letter-spacing:4rpx}.word{display:block;font-size:56rpx;font-weight:800;letter-spacing:4rpx}.gap{color:#9146ff;opacity:.5}.input-shell{margin:26rpx auto 0;max-width:520rpx;background:#000;border:1rpx solid rgba(255,255,255,.08);border-radius:9999rpx}.answer{padding:22rpx 28rpx;text-align:center;color:#fff;font-size:32rpx}.feedback{margin-top:20rpx;color:#00e3fd}.actions-cluster{display:flex;flex-direction:row;gap:18rpx;justify-content:center;align-items:center;margin-top:24rpx}.secondary-btn,.primary-btn{flex:1;border-radius:9999rpx;padding:24rpx 0;font-weight:700}.secondary-btn{background:#23262c;color:#f6f6fc}.primary-btn{background:linear-gradient(90deg,#c19cff,#9146ff);color:#fff}.master-action{margin-top:34rpx;text-align:center}.master-text{color:#aaabb0;font-size:22rpx;border-bottom:1rpx solid transparent}.master-text.active{color:#00e3fd;border-color:#00e3fd}
page{background:#0c0e12;color:#f6f6fc}.page{min-height:100vh;background:#0c0e12}.topbar{display:flex;justify-content:space-between;align-items:center;padding:24rpx 32rpx;background:rgba(12,14,18,.8);border-bottom:1rpx solid rgba(255,255,255,.08);backdrop-filter:blur(24px);box-shadow:0 1rpx 20rpx rgba(0,0,0,.08)}.topbar-left{display:flex;align-items:center;gap:18rpx}.avatar-shell{width:80rpx;height:80rpx;border-radius:9999rpx;overflow:hidden;border:2rpx solid rgba(193,156,255,.2);flex-shrink:0}.avatar-img{width:100%;height:100%}.brand-text{font-size:38rpx;font-weight:800;color:#c19cff}.topbar-icon{width:72rpx;height:72rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center}.content{height:calc(100vh - 120rpx);padding:32rpx;box-sizing:border-box}.progress-box{margin-bottom:28rpx}.progress-head{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:16rpx}.progress-label{font-size:24rpx;color:#aaabb0}.progress-count{font-size:28rpx;font-weight:700;color:#00e3fd}.progress-total{color:#aaabb0;font-size:22rpx;font-weight:400}.progress-bar{height:12rpx;background:#23262c;border-radius:9999rpx;overflow:hidden}.ghost-border{border:1rpx solid rgba(255,255,255,.1)}.progress-fill{height:100%;background:linear-gradient(90deg,#c19cff,#00e3fd)}.glass-card{background:rgba(29,32,37,.4);backdrop-filter:blur(24rpx)}.question-card{position:relative;overflow:hidden;border-radius:32rpx;padding:40rpx 32rpx;text-align:center;box-shadow:0 20rpx 40rpx rgba(0,0,0,.4);border:1rpx solid rgba(255,255,255,.1)}.ambient-glow{position:absolute;inset:0;background:linear-gradient(to bottom,rgba(193,156,255,.05),transparent);pointer-events:none}.mode-tag-wrap{display:flex;justify-content:center;margin-bottom:20rpx}.mode-tag{display:inline-flex;align-items:center;gap:10rpx;padding:10rpx 22rpx;border-radius:9999rpx;background:rgba(193,156,255,.12);border:1rpx solid rgba(193,156,255,.18);color:#d8c7ff;font-size:22rpx;letter-spacing:2rpx}.meaning-wrap{margin-bottom:28rpx}.meaning{display:block;color:#aaabb0;font-size:26rpx;letter-spacing:2rpx}.cloze-wrap,.word-wrap{margin:14rpx 0 10rpx}.cloze,.word{display:block;font-size:58rpx;font-weight:800;letter-spacing:2rpx;line-height:1.25}.cloze{color:#f6f6fc}.word{color:#f6f6fc}.entry-type-hint{margin-top:14rpx;color:#8f9198;font-size:22rpx;line-height:1.6}.gap{color:#9146ff;opacity:.5}.input-shell{margin:26rpx auto 0;max-width:520rpx;background:rgba(255,255,255,.04);border:1rpx solid rgba(255,255,255,.08);border-radius:9999rpx}.answer{padding:22rpx 28rpx;text-align:center;color:#f6f6fc;font-size:32rpx;caret-color:#c19cff}.answer::placeholder{color:rgba(170,171,176,.72)}.answer::-webkit-input-placeholder{color:rgba(170,171,176,.72)}.answer::-moz-placeholder{color:rgba(170,171,176,.72)}.answer:-ms-input-placeholder{color:rgba(170,171,176,.72)}.feedback-box{margin-top:22rpx;padding:20rpx 24rpx;border-radius:24rpx;background:rgba(255,255,255,.04);border:1rpx solid rgba(255,255,255,.06)}.feedback{display:block;color:#00e3fd}.reveal-answer{display:block;margin-top:10rpx;color:#f6f6fc;font-weight:700}.actions-cluster{display:flex;flex-direction:row;gap:18rpx;justify-content:center;align-items:center;margin-top:24rpx}.secondary-btn,.primary-btn{flex:1;border-radius:9999rpx;padding:24rpx 0;font-weight:700}.secondary-btn{background:#23262c;color:#f6f6fc}.primary-btn{background:linear-gradient(90deg,#c19cff,#9146ff);color:#fff}.master-action{margin-top:34rpx;text-align:center}.master-text{color:#aaabb0;font-size:22rpx;border-bottom:1rpx solid transparent}.master-text.active{color:#00e3fd;border-color:#00e3fd}

View File

@ -29,6 +29,10 @@ Page({
goAdd() {
wx.navigateTo({ url: '/pages/entry-edit/entry-edit' })
},
goDetail(e) {
const { id } = e.currentTarget.dataset
wx.navigateTo({ url: `/pages/entry-detail/entry-detail?id=${id}` })
},
goBatch() {
wx.navigateTo({ url: '/pages/batch-upload/batch-upload' })
},

View File

@ -35,16 +35,17 @@
<view wx:else class="card-grid">
<block wx:for="{{list}}" wx:key="id">
<view class="word-card cyan">
<view class="word-card {{item.entry_type === 'sentence' ? 'pink' : 'cyan'}}" data-id="{{item.id}}" bindtap="goDetail">
<view class="card-top">
<view>
<text class="word-title">{{item.en_text}}</text>
<text class="phonetic">{{item.entry_type}}</text>
<text class="phonetic">{{item.entry_type === 'sentence' ? '句子' : '单词'}}</text>
</view>
<view class="status-dot done">{{item.status}}</view>
</view>
<text class="meaning">{{item.zh_text}}</text>
<text class="example" wx:if="{{item.example_text}}">{{item.example_text}}</text>
<text class="meaning">{{item.entry_type === 'sentence' ? item.zh_text : item.zh_text}}</text>
<text class="example" wx:if="{{item.entry_type === 'word' && item.example_text}}">例句:{{item.example_text}}</text>
<text class="example" wx:elif="{{item.entry_type === 'sentence' && item.example_text}}">说明:{{item.example_text}}</text>
</view>
</block>
</view>