修复我的页面头像登录功能
This commit is contained in:
parent
862b4fbe3b
commit
4204761837
@ -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 }),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 }),
|
||||
})
|
||||
},
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
143
me/me.js
@ -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() {},
|
||||
})
|
||||
|
||||
26
me/me.wxml
26
me/me.wxml
@ -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
@ -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 }),
|
||||
})
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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: '' })
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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' })
|
||||
},
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user