修复BUG1.0
This commit is contained in:
parent
1a03509b40
commit
ad510a1be6
54
manage/batch-upload/batch-upload.js
Normal file
54
manage/batch-upload/batch-upload.js
Normal file
@ -0,0 +1,54 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
fileName: '',
|
||||
loading: false,
|
||||
error: '',
|
||||
},
|
||||
downloadTemplate() {
|
||||
wx.downloadFile({
|
||||
url: `${app.globalData.baseUrl}/api/v1/entries/template`,
|
||||
success: (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
this.setData({ error: '模板下载失败' })
|
||||
return
|
||||
}
|
||||
wx.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fileType: 'xlsx',
|
||||
fail: () => this.setData({ error: '模板打开失败,请先保存到本地再上传' }),
|
||||
})
|
||||
},
|
||||
fail: () => this.setData({ error: '模板下载失败' }),
|
||||
})
|
||||
},
|
||||
chooseFile() {
|
||||
wx.chooseMessageFile({
|
||||
count: 1,
|
||||
type: 'file',
|
||||
extension: ['xlsx'],
|
||||
success: (res) => {
|
||||
const file = res.tempFiles[0]
|
||||
this.setData({ fileName: file.name, loading: true, error: '' })
|
||||
wx.uploadFile({
|
||||
url: `${app.globalData.baseUrl}/api/v1/entries/upload-file`,
|
||||
filePath: file.path,
|
||||
name: 'file',
|
||||
header: { 'X-OpenID': app.globalData.openid || '' },
|
||||
success: (uploadRes) => {
|
||||
try {
|
||||
const payload = JSON.parse(uploadRes.data)
|
||||
this.setData({ result: payload.data || null })
|
||||
} catch (e) {
|
||||
this.setData({ error: '上传结果解析失败' })
|
||||
}
|
||||
},
|
||||
fail: () => this.setData({ error: '文件上传失败' }),
|
||||
complete: () => this.setData({ loading: false }),
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
3
manage/batch-upload/batch-upload.json
Normal file
3
manage/batch-upload/batch-upload.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
25
manage/batch-upload/batch-upload.wxml
Normal file
25
manage/batch-upload/batch-upload.wxml
Normal file
@ -0,0 +1,25 @@
|
||||
<view class="page">
|
||||
<view class="topbar">
|
||||
<view class="brand">
|
||||
<view class="avatar-wrap"><image class="avatar" src="https://picsum.photos/280" mode="aspectFill" /></view>
|
||||
<text class="brand-text">VocabMaster</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="content">
|
||||
<view class="header">
|
||||
<text class="title">批量上传</text>
|
||||
<text class="subtitle">先下载模板,按模板填写后直接上传单词或句子。</text>
|
||||
</view>
|
||||
<view class="action-row">
|
||||
<button class="action-btn ghost-btn" bindtap="downloadTemplate">下载模板</button>
|
||||
<button class="action-btn primary-btn" bindtap="chooseFile">选择文件上传</button>
|
||||
</view>
|
||||
<view wx:if="{{fileName}}" class="info">已选择:{{fileName}}</view>
|
||||
<view wx:if="{{error}}" class="error">{{error}}</view>
|
||||
<view wx:if="{{result}}" class="result-card">
|
||||
<text>总数:{{result.total}}</text>
|
||||
<text>成功:{{result.success}}</text>
|
||||
<text>失败:{{result.failed}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
1
manage/batch-upload/batch-upload.wxss
Normal file
1
manage/batch-upload/batch-upload.wxss
Normal file
@ -0,0 +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:40rpx 32rpx 32rpx;box-sizing:border-box}.header{margin-bottom:28rpx}.title{display:block;font-size:52rpx;font-weight:800}.subtitle{display:block;margin-top:10rpx;color:#aaabb0;font-size:24rpx}.layout{display:grid;grid-template-columns:1fr;gap:24rpx}.instruction-card,.upload-card,.feedback-card,.step-card,.dropzone{background:rgba(29,32,37,.56);border:1rpx solid rgba(255,255,255,.08);border-radius:28rpx;box-sizing:border-box}.instruction-card{padding:24rpx}.section-title{display:block;font-size:28rpx;font-weight:700;margin-bottom:18rpx}.steps{display:flex;flex-direction:column;gap:18rpx}.step{display:flex;align-items:stretch;gap:16rpx}.step-dot{width:44rpx;height:44rpx;border-radius:9999rpx;background:#23262c;color:#aaabb0;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:20rpx;font-weight:700;margin-top:10rpx}.active-dot{background:#c19cff;color:#000}.step-card{flex:1;padding:18rpx;background:#171a1f}.low{background:#111318}.step-title{display:block;font-size:24rpx;font-weight:700;margin-bottom:6rpx}.step-desc{display:block;font-size:20rpx;color:#aaabb0}.download{display:block;margin-top:12rpx;font-size:20rpx;color:#00e5ff}.upload-card{display:flex;flex-direction:column;gap:18rpx}.dropzone{padding:40rpx 28rpx;min-height:360rpx;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:rgba(29,32,37,.4)}.upload-icon{width:112rpx;height:112rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;font-size:48rpx;color:#c19cff;margin-bottom:18rpx}.drop-title{display:block;font-size:34rpx;font-weight:800;margin-bottom:10rpx}.drop-desc{display:block;font-size:22rpx;color:#aaabb0;line-height:1.6;margin-bottom:24rpx}.action-row{display:flex;gap:20rpx}.action-btn{flex:1;border-radius:9999rpx;padding:22rpx 24rpx;font-size:28rpx;font-weight:700;line-height:1;text-align:center;box-sizing:border-box;border:1rpx solid transparent}.primary-btn{background:linear-gradient(90deg,#c19cff,#9146ff);color:#fff;box-shadow:0 16rpx 32rpx rgba(145,70,255,.22)}.ghost-btn{background:rgba(255,255,255,.04);color:#f6f6fc;border-color:rgba(255,255,255,.1)}.action-btn::after{border:none}.primary-btn:active{opacity:.92;transform:translateY(1rpx)}.ghost-btn:active{background:rgba(255,255,255,.08)}.info{margin-top:18rpx;padding:16rpx 20rpx;border-radius:20rpx;background:rgba(255,255,255,.04);color:#d7d7de;font-size:22rpx}.feedback-card{padding:24rpx;display:flex;flex-direction:column;gap:16rpx}.feedback-left{display:flex;align-items:center;gap:16rpx}.status-icon{width:56rpx;height:56rpx;border-radius:9999rpx;background:#111318;display:flex;align-items:center;justify-content:center;color:#00e5ff}.feedback-title{display:block;font-size:24rpx;font-weight:700}.feedback-file{display:block;font-size:20rpx;color:#aaabb0}.feedback-stats{display:flex;align-items:center;gap:24rpx;justify-content:flex-end}.stat{text-align:center}.stat-num{display:block;font-size:34rpx;font-weight:800}.good{color:#00e5ff}.bad{color:#aaabb0}.stat-label{display:block;font-size:18rpx;color:#aaabb0}.divider{width:1rpx;height:34rpx;background:rgba(255,255,255,.08)}
|
||||
70
manage/entry-detail/entry-detail.js
Normal file
70
manage/entry-detail/entry-detail.js
Normal file
@ -0,0 +1,70 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
entry: null,
|
||||
loading: false,
|
||||
error: '',
|
||||
},
|
||||
onLoad(options) {
|
||||
this.entryId = options.id
|
||||
this.loadEntry()
|
||||
},
|
||||
loadEntry() {
|
||||
if (!app.globalData.openid) {
|
||||
this.setData({ error: '????' })
|
||||
return
|
||||
}
|
||||
if (!this.entryId) {
|
||||
this.setData({ error: '????ID' })
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true, error: '' })
|
||||
wx.request({
|
||||
url: `${app.globalData.baseUrl}/api/v1/entries/${this.entryId}`,
|
||||
header: { 'X-OpenID': app.globalData.openid },
|
||||
success: (res) => {
|
||||
const entry = res.data?.data || null
|
||||
this.setData({ entry, error: entry ? '' : '?????' })
|
||||
},
|
||||
fail: () => this.setData({ error: '??????' }),
|
||||
complete: () => this.setData({ loading: false }),
|
||||
})
|
||||
},
|
||||
editEntry() {
|
||||
if (!this.data.entry?.id) return
|
||||
wx.navigateTo({ url: `/pages/manage/entry-edit/entry-edit?id=${this.data.entry.id}` })
|
||||
},
|
||||
deleteEntry() {
|
||||
if (!this.data.entry?.id) return
|
||||
wx.showModal({
|
||||
title: '????',
|
||||
content: '?????????????',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
wx.request({
|
||||
url: `${app.globalData.baseUrl}/api/v1/entries/${this.data.entry.id}`,
|
||||
method: 'DELETE',
|
||||
header: { 'X-OpenID': app.globalData.openid },
|
||||
success: () => {
|
||||
wx.showToast({ title: '???', icon: 'success' })
|
||||
const pages = getCurrentPages()
|
||||
const prevPage = pages[pages.length - 2]
|
||||
if (prevPage && typeof prevPage.loadList === 'function') {
|
||||
prevPage.loadList()
|
||||
}
|
||||
wx.navigateBack({ delta: 1 })
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
goBack() {
|
||||
const pages = getCurrentPages()
|
||||
const prevPage = pages[pages.length - 2]
|
||||
if (prevPage && typeof prevPage.loadList === 'function') {
|
||||
prevPage.loadList()
|
||||
}
|
||||
wx.navigateBack()
|
||||
},
|
||||
})
|
||||
3
manage/entry-detail/entry-detail.json
Normal file
3
manage/entry-detail/entry-detail.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
60
manage/entry-detail/entry-detail.wxml
Normal file
60
manage/entry-detail/entry-detail.wxml
Normal file
@ -0,0 +1,60 @@
|
||||
<view class="page">
|
||||
<view class="bg-orb orb1"></view>
|
||||
<view class="bg-orb orb2"></view>
|
||||
|
||||
<scroll-view scroll-y class="content">
|
||||
<view class="hero-card {{entry && entry.entry_type === 'sentence' ? 'pink' : 'cyan'}}" wx:if="{{entry}}">
|
||||
<view class="hero-top">
|
||||
<view class="hero-title-group">
|
||||
<view class="type-chip">{{entry.entry_type === 'sentence' ? '句子' : '单词'}}</view>
|
||||
<view class="hero-actions-vertical">
|
||||
<view class="mini-action" bindtap="editEntry" catchtap="editEntry">✎</view>
|
||||
<view class="mini-action danger" bindtap="deleteEntry" catchtap="deleteEntry">🗑</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="main-text">{{entry.en_text}}</text>
|
||||
<text class="translate">{{entry.entry_type === 'sentence' ? '翻译:' : '释义:'}}{{entry.zh_text}}</text>
|
||||
<view class="detail-badges">
|
||||
<view class="badge-pill">来源:{{entry.source_type}}</view>
|
||||
<view class="badge-pill">状态:{{entry.status}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="header">
|
||||
<text class="title">词条详情</text>
|
||||
<text class="subtitle">查看单词或句子的完整信息</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading}}" class="empty-state">
|
||||
<text class="empty-title">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{error}}" class="error-banner">
|
||||
<text class="error-text">{{error}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{entry}}" class="info-card">
|
||||
<view class="info-row">
|
||||
<text class="info-label">类型</text>
|
||||
<text class="info-value">{{entry.entry_type === 'sentence' ? '句子' : '单词'}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">内容</text>
|
||||
<text class="info-value">{{entry.en_text}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{entry.entry_type === 'sentence' ? '翻译' : '释义'}}</text>
|
||||
<text class="info-value">{{entry.zh_text}}</text>
|
||||
</view>
|
||||
<view class="info-row" wx:if="{{entry.example_text}}">
|
||||
<text class="info-label">{{entry.entry_type === 'word' ? '例句' : '补充说明'}}</text>
|
||||
<text class="info-value">{{entry.example_text}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">来源</text>
|
||||
<text class="info-value">{{entry.source_type}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
241
manage/entry-detail/entry-detail.wxss
Normal file
241
manage/entry-detail/entry-detail.wxss
Normal file
@ -0,0 +1,241 @@
|
||||
page {
|
||||
background: #0c0e12;
|
||||
color: #f6f6fc;
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #0c0e12;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bg-orb {
|
||||
position: absolute;
|
||||
border-radius: 9999rpx;
|
||||
filter: blur(110rpx);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.orb1 {
|
||||
width: 46vw;
|
||||
height: 46vw;
|
||||
top: -14vw;
|
||||
right: -14vw;
|
||||
background: rgba(193, 156, 255, 0.12);
|
||||
}
|
||||
|
||||
.orb2 {
|
||||
width: 54vw;
|
||||
height: 54vw;
|
||||
left: -18vw;
|
||||
bottom: -16vw;
|
||||
background: rgba(0, 227, 253, 0.08);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100vh;
|
||||
padding: 26rpx 32rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.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-card.cyan::after {
|
||||
background: rgba(0, 227, 253, 0.45);
|
||||
}
|
||||
|
||||
.hero-card.pink::after {
|
||||
background: rgba(255, 108, 149, 0.45);
|
||||
}
|
||||
|
||||
.hero-top {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.hero-title-group {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.type-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60rpx;
|
||||
padding: 0 22rpx;
|
||||
border-radius: 9999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #00e3fd;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.hero-actions-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.mini-action {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
color: #f6f6fc;
|
||||
}
|
||||
|
||||
.mini-action.danger {
|
||||
color: #ff7b9b;
|
||||
}
|
||||
|
||||
.main-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
font-size: 46rpx;
|
||||
line-height: 1.25;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.5rpx;
|
||||
}
|
||||
|
||||
.translate {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
color: #f6f6fc;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.detail-badges {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.badge-pill {
|
||||
padding: 12rpx 18rpx;
|
||||
border-radius: 9999rpx;
|
||||
font-size: 22rpx;
|
||||
color: #aaabb0;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 52rpx;
|
||||
line-height: 1.1;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: #aaabb0;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
padding: 28rpx;
|
||||
border-radius: 30rpx;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
margin-bottom: 26rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
gap: 18rpx;
|
||||
padding: 18rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 120rpx;
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
color: #aaabb0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #f6f6fc;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 60rpx 20rpx;
|
||||
text-align: center;
|
||||
color: #aaabb0;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #f6f6fc;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
padding: 24rpx 28rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(167, 1, 56, 0.2);
|
||||
border: 1rpx solid rgba(255, 110, 132, 0.35);
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #ffb2b9;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
116
manage/entry-edit/entry-edit.js
Normal file
116
manage/entry-edit/entry-edit.js
Normal file
@ -0,0 +1,116 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
entryId: null,
|
||||
entryType: 'word',
|
||||
enText: '',
|
||||
zhText: '',
|
||||
exampleText: '',
|
||||
loading: false,
|
||||
error: '',
|
||||
isEditMode: false,
|
||||
},
|
||||
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 })
|
||||
},
|
||||
onZhInput(e) {
|
||||
this.setData({ zhText: e.detail.value })
|
||||
},
|
||||
onExampleInput(e) {
|
||||
this.setData({ exampleText: e.detail.value })
|
||||
},
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
save() {
|
||||
if (!app.globalData.openid) {
|
||||
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,
|
||||
method,
|
||||
header: { 'Content-Type': 'application/json', 'X-OpenID': app.globalData.openid },
|
||||
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: this.data.isEditMode ? '更新成功' : '保存成功', icon: 'success' })
|
||||
if (!this.data.isEditMode) {
|
||||
this.setData({
|
||||
enText: '',
|
||||
zhText: '',
|
||||
exampleText: '',
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: () => this.setData({ error: this.data.isEditMode ? '更新失败' : '保存失败' }),
|
||||
complete: () => this.setData({ loading: false }),
|
||||
})
|
||||
},
|
||||
})
|
||||
3
manage/entry-edit/entry-edit.json
Normal file
3
manage/entry-edit/entry-edit.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
65
manage/entry-edit/entry-edit.wxml
Normal file
65
manage/entry-edit/entry-edit.wxml
Normal file
@ -0,0 +1,65 @@
|
||||
<view class="page">
|
||||
<view class="bg-orb orb1"></view>
|
||||
<view class="bg-orb orb2"></view>
|
||||
|
||||
<view class="topbar">
|
||||
<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="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="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">{{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">{{entryType === 'word' ? '中文释义' : '中文翻译'}}</text>
|
||||
<view class="input-shell">
|
||||
<input class="input small" placeholder="{{entryType === 'word' ? '输入对应的中文含义...' : '输入句子的中文翻译...'}}" bindinput="onZhInput" value="{{zhText}}" />
|
||||
</view>
|
||||
</view>
|
||||
<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">{{isEditMode ? '更新' : '保存'}}</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
331
manage/entry-edit/entry-edit.wxss
Normal file
331
manage/entry-edit/entry-edit.wxss
Normal file
@ -0,0 +1,331 @@
|
||||
page {
|
||||
background: #0c0e12;
|
||||
color: #f6f6fc;
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #0c0e12;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bg-orb {
|
||||
position: absolute;
|
||||
border-radius: 9999rpx;
|
||||
filter: blur(110rpx);
|
||||
pointer-events: none;
|
||||
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: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx 8rpx;
|
||||
}
|
||||
|
||||
.back-chip,
|
||||
.mode-chip {
|
||||
height: 68rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 9999rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.back-chip {
|
||||
width: 68rpx;
|
||||
padding: 0;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.mode-chip-add {
|
||||
color: #00e3fd;
|
||||
}
|
||||
|
||||
.mode-chip-edit {
|
||||
color: #c19cff;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: calc(100vh - 120rpx);
|
||||
padding: 16rpx 32rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.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: 54rpx;
|
||||
line-height: 1.1;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
color: #aaabb0;
|
||||
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: rgba(23, 26, 31, 0.85);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 9999rpx;
|
||||
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
.seg {
|
||||
padding: 18rpx 34rpx;
|
||||
border-radius: 9999rpx;
|
||||
color: #aaabb0;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: #1d2025;
|
||||
color: #c19cff;
|
||||
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: 26rpx;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 22rpx;
|
||||
color: #aaabb0;
|
||||
font-weight: 700;
|
||||
margin: 0 0 12rpx 4rpx;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: rgba(170, 171, 176, 0.55);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.input-shell {
|
||||
min-height: 92rpx;
|
||||
border-radius: 24rpx;
|
||||
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;
|
||||
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%;
|
||||
height: 100%;
|
||||
min-height: 92rpx;
|
||||
padding: 0 26rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 92rpx;
|
||||
color: #f6f6fc;
|
||||
box-sizing: border-box;
|
||||
caret-color: #c19cff;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 28rpx;
|
||||
line-height: 92rpx;
|
||||
}
|
||||
|
||||
.textarea-shell {
|
||||
background: rgba(17, 19, 24, 0.95);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
min-height: 220rpx;
|
||||
padding: 24rpx 26rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
color: #f6f6fc;
|
||||
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;
|
||||
gap: 18rpx;
|
||||
padding-top: 28rpx;
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.save-btn {
|
||||
flex: 1;
|
||||
border-radius: 9999rpx;
|
||||
padding: 22rpx 34rpx;
|
||||
font-weight: 700;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: #f6f6fc;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: linear-gradient(90deg, #c19cff, #9146ff);
|
||||
color: #fff;
|
||||
box-shadow: 0 14rpx 30rpx rgba(145, 70, 255, 0.24);
|
||||
}
|
||||
18
manage/logs/logs.js
Normal file
18
manage/logs/logs.js
Normal file
@ -0,0 +1,18 @@
|
||||
// logs.js
|
||||
const util = require('../../utils/util.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
logs: []
|
||||
},
|
||||
onLoad() {
|
||||
this.setData({
|
||||
logs: (wx.getStorageSync('logs') || []).map(log => {
|
||||
return {
|
||||
date: util.formatTime(new Date(log)),
|
||||
timeStamp: log
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
3
manage/logs/logs.json
Normal file
3
manage/logs/logs.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "日志"
|
||||
}
|
||||
6
manage/logs/logs.wxml
Normal file
6
manage/logs/logs.wxml
Normal file
@ -0,0 +1,6 @@
|
||||
<!--logs.wxml-->
|
||||
<scroll-view class="scrollarea" scroll-y type="list">
|
||||
<block wx:for="{{logs}}" wx:key="timeStamp" wx:for-item="log">
|
||||
<view class="log-item">{{index + 1}}. {{log.date}}</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
16
manage/logs/logs.wxss
Normal file
16
manage/logs/logs.wxss
Normal file
@ -0,0 +1,16 @@
|
||||
page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.scrollarea {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.log-item {
|
||||
margin-top: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.log-item:last-child {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
45
manage/vocab-manage/vocab-manage.js
Normal file
45
manage/vocab-manage/vocab-manage.js
Normal file
@ -0,0 +1,45 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
list: [],
|
||||
loading: false,
|
||||
error: '',
|
||||
},
|
||||
onShow() {
|
||||
this.loadList()
|
||||
},
|
||||
loadList() {
|
||||
if (!app.globalData.openid) {
|
||||
this.setData({ error: '请先登录' })
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true, error: '' })
|
||||
wx.request({
|
||||
url: `${app.globalData.baseUrl}/api/v1/entries`,
|
||||
header: { 'X-OpenID': app.globalData.openid },
|
||||
success: (res) => {
|
||||
const payload = res.data?.data || []
|
||||
this.setData({ list: payload })
|
||||
},
|
||||
fail: () => this.setData({ error: '词库加载失败' }),
|
||||
complete: () => this.setData({ loading: false }),
|
||||
})
|
||||
},
|
||||
goAdd() {
|
||||
wx.navigateTo({ url: '/pages/manage/entry-edit/entry-edit' })
|
||||
},
|
||||
goDetail(e) {
|
||||
const { id } = e.currentTarget.dataset
|
||||
wx.navigateTo({ url: `/pages/manage/entry-detail/entry-detail?id=${id}` })
|
||||
},
|
||||
goBatch() {
|
||||
wx.navigateTo({ url: '/pages/manage/batch-upload/batch-upload' })
|
||||
},
|
||||
goProgress() {
|
||||
wx.navigateTo({ url: '/pages/progress/progress-list/progress-list' })
|
||||
},
|
||||
goSettings() {
|
||||
wx.navigateTo({ url: '/pages/settings/settings' })
|
||||
},
|
||||
})
|
||||
3
manage/vocab-manage/vocab-manage.json
Normal file
3
manage/vocab-manage/vocab-manage.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
55
manage/vocab-manage/vocab-manage.wxml
Normal file
55
manage/vocab-manage/vocab-manage.wxml
Normal file
@ -0,0 +1,55 @@
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="content">
|
||||
<view class="header">
|
||||
<text class="title">词库管理</text>
|
||||
<text class="subtitle">管理并策划你的个人词汇集合。</text>
|
||||
</view>
|
||||
|
||||
<!-- <view class="toolbar">
|
||||
<button class="primary-btn" bindtap="goAdd">新增</button>
|
||||
<button class="secondary-btn" bindtap="goBatch">批量上传</button>
|
||||
<button class="secondary-btn" bindtap="goProgress">进度</button>
|
||||
<button class="secondary-btn" bindtap="goSettings">设置</button>
|
||||
</view> -->
|
||||
|
||||
<view class="search-row">
|
||||
<view class="search-box">
|
||||
<text class="search-icon">🔎</text>
|
||||
<text class="search-placeholder">搜索单词、短语或标签...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading}}" class="empty-state">
|
||||
<text class="empty-title">加载中...</text>
|
||||
<text class="empty-subtitle">正在同步词库数据</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{error}}" class="error-banner">
|
||||
<text class="error-text">{{error}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{!list.length}}" class="empty-state">
|
||||
<text class="empty-title">暂无词条</text>
|
||||
<text class="empty-subtitle">点击“新增”或“批量上传”开始构建词库</text>
|
||||
</view>
|
||||
|
||||
<view wx:else class="card-grid">
|
||||
<block wx:for="{{list}}" wx:key="id">
|
||||
<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 === 'sentence' ? '句子' : '单词'}}</text>
|
||||
</view>
|
||||
<view class="status-dot done">{{item.status}}</view>
|
||||
</view>
|
||||
<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>
|
||||
</scroll-view>
|
||||
|
||||
<view class="fab" bindtap="goAdd">+</view>
|
||||
</view>
|
||||
1
manage/vocab-manage/vocab-manage.wxss
Normal file
1
manage/vocab-manage/vocab-manage.wxss
Normal file
@ -0,0 +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:40rpx 32rpx 32rpx;box-sizing:border-box}.header{margin-bottom:28rpx}.title{display:block;font-size:52rpx;font-weight:800}.subtitle{display:block;margin-top:10rpx;color:#aaabb0;font-size:26rpx}.search-row{display:flex;flex-direction:column;gap:18rpx;margin-bottom:28rpx}.search-box{display:flex;align-items:center;gap:12rpx;padding:22rpx 26rpx;border-radius:9999rpx;background:#000;border:1rpx solid rgba(255,255,255,.1);color:#aaabb0}.search-placeholder{font-size:24rpx}.filters{display:flex;gap:14rpx;overflow-x:auto}.filter-btn{padding:18rpx 22rpx;border-radius:9999rpx;background:#23262c;color:#f6f6fc;font-size:22rpx;white-space:nowrap}.dim{color:#aaabb0;margin:0 4rpx}.card-grid{display:grid;grid-template-columns:1fr;gap:18rpx}.word-card{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)}.cyan{box-shadow:0 20rpx 40rpx rgba(0,229,255,.05)}.purple{box-shadow:0 20rpx 40rpx rgba(127,0,255,.05)}.pink{box-shadow:0 20rpx 40rpx rgba(255,64,129,.05)}.wide{min-height:280rpx}.card-top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:18rpx}.word-title{display:block;font-size:38rpx;font-weight:800}.phonetic{display:block;margin-top:8rpx;font-size:22rpx;color:#aaabb0}.status-dot{width:56rpx;height:56rpx;border-radius:9999rpx;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.06)}.done{color:#00e5ff}.learn{color:#c19cff}.review{color:#ff4081}.meaning{display:block;font-size:26rpx;color:#f6f6fc;line-height:1.6}.split{display:grid;grid-template-columns:1fr 1fr;gap:18rpx;align-items:start}.example{font-size:22rpx;color:#aaabb0;font-style:italic;border-left:4rpx solid rgba(193,156,255,.3);padding-left:14rpx;line-height:1.6}.progress-block{margin-top:22rpx;padding-top:18rpx;border-top:1rpx solid rgba(255,255,255,.05)}.progress-meta{display:flex;justify-content:space-between;font-size:20rpx;color:#aaabb0;margin-bottom:10rpx}.accent-cyan{color:#00e5ff}.accent-purple{color:#c19cff}.accent-pink{color:#ff4081}.progress-bar{height:10rpx;border-radius:9999rpx;background:#111318;overflow:hidden}.fill{height:100%;border-radius:9999rpx}.cyan-fill{width:95%;background:#00e5ff}.purple-fill{width:60%;background:#c19cff}.pink-fill{width:30%;background:#ff4081}.fab{position:fixed;right:28rpx;bottom:130rpx;width:96rpx;height:96rpx;border-radius:9999rpx;background:linear-gradient(135deg,#c19cff,#00e5ff);display:flex;align-items:center;justify-content:center;color:#fff;font-size:48rpx;box-shadow:0 0 20rpx rgba(193,156,255,.2)}
|
||||
172
progress/achievements/achievements.js
Normal file
172
progress/achievements/achievements.js
Normal file
@ -0,0 +1,172 @@
|
||||
const app = getApp()
|
||||
|
||||
const ACHIEVEMENT_ICON_MAP = {
|
||||
MASTERED_100: '/assets/icons/achievements/core/100.svg',
|
||||
MASTERED_1000: '/assets/icons/achievements/core/1000.svg',
|
||||
MASTERED_3000: '/assets/icons/achievements/core/3000.svg',
|
||||
MASTERED_5000: '/assets/icons/achievements/core/5000.svg',
|
||||
MASTERED_7000: '/assets/icons/achievements/core/7000.svg',
|
||||
MASTERED_10000: '/assets/icons/achievements/core/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: [100, 1000, 3000, 5000, 7000, 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,
|
||||
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 = this.decorateList(res.data?.data || [])
|
||||
const sections = this.buildSections(list)
|
||||
this.setData({
|
||||
sections,
|
||||
total: list.length,
|
||||
unlocked: list.filter((item) => item.is_unlocked).length,
|
||||
})
|
||||
},
|
||||
fail: () => this.setData({ error: '成就数据加载失败' }),
|
||||
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 }),
|
||||
})
|
||||
},
|
||||
})
|
||||
3
progress/achievements/achievements.json
Normal file
3
progress/achievements/achievements.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
72
progress/achievements/achievements.wxml
Normal file
72
progress/achievements/achievements.wxml
Normal file
@ -0,0 +1,72 @@
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="content">
|
||||
<view class="heading">
|
||||
<text class="title">成就墙</text>
|
||||
<text class="subtitle">把你的每一次坚持都挂在墙上</text>
|
||||
</view>
|
||||
|
||||
<view class="hero-card">
|
||||
<view class="hero-glow"></view>
|
||||
<view class="hero-row">
|
||||
<view>
|
||||
<text class="hero-num">{{unlocked}}</text>
|
||||
<text class="hero-label">已点亮勋章 / {{total}}</text>
|
||||
</view>
|
||||
<view class="hero-badge">
|
||||
<image class="hero-badge-img" src="/assets/icons/achievements/state/achievement-unlocked.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading}}" class="empty-state">
|
||||
<text class="empty-title">加载中...</text>
|
||||
<text class="empty-subtitle">正在同步成就数据</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{error}}" class="error-banner">
|
||||
<text class="error-text">{{error}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{!sections.length}}" class="empty-state">
|
||||
<text class="empty-title">暂无成就</text>
|
||||
<text class="empty-subtitle">继续复习和打卡后会逐渐解锁</text>
|
||||
</view>
|
||||
|
||||
<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>
|
||||
</scroll-view>
|
||||
</view>
|
||||
287
progress/achievements/achievements.wxss
Normal file
287
progress/achievements/achievements.wxss
Normal file
@ -0,0 +1,287 @@
|
||||
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: 130rpx;
|
||||
height: 130rpx;
|
||||
border-radius: 9999rpx;
|
||||
background: rgba(255, 255, 255, .06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 24rpx;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.item-icon-img {
|
||||
width: 110rpx;
|
||||
height: 110rpx;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.item-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8rpx;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.item-sub {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #aaabb0;
|
||||
text-align: center
|
||||
}
|
||||
22
progress/checkin-detail/checkin-detail.js
Normal file
22
progress/checkin-detail/checkin-detail.js
Normal file
@ -0,0 +1,22 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
stats: null,
|
||||
loading: false,
|
||||
error: '',
|
||||
},
|
||||
onShow() {
|
||||
this.loadStats()
|
||||
},
|
||||
loadStats() {
|
||||
this.setData({ loading: true, error: '' })
|
||||
wx.request({
|
||||
url: `${app.globalData.baseUrl}/api/v1/checkin/stats`,
|
||||
header: { 'X-OpenID': app.globalData.openid || '' },
|
||||
success: (res) => this.setData({ stats: res.data?.data || null }),
|
||||
fail: () => this.setData({ error: '打卡数据加载失败' }),
|
||||
complete: () => this.setData({ loading: false }),
|
||||
})
|
||||
},
|
||||
})
|
||||
3
progress/checkin-detail/checkin-detail.json
Normal file
3
progress/checkin-detail/checkin-detail.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
42
progress/checkin-detail/checkin-detail.wxml
Normal file
42
progress/checkin-detail/checkin-detail.wxml
Normal file
@ -0,0 +1,42 @@
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="content">
|
||||
<view class="header">
|
||||
<text class="title">打卡详情</text>
|
||||
<text class="subtitle">查看近期的学习与坚持记录。</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!stats}}" class="empty-state">
|
||||
<text class="empty-title">暂无打卡数据</text>
|
||||
<text class="empty-subtitle">完成一次复习后即可自动生成打卡记录</text>
|
||||
</view>
|
||||
|
||||
<view class="card hero" wx:if="{{stats}}">
|
||||
<view class="hero-row">
|
||||
<view>
|
||||
<text class="hero-num">{{stats.streak_days}}</text>
|
||||
<text class="hero-label">连续打卡天数</text>
|
||||
</view>
|
||||
<view class="fire">🔥</view>
|
||||
</view>
|
||||
<view class="progress-bar"><view class="fill" style="width: {{stats.week_checkins / 7 * 100}}%;"></view></view>
|
||||
<view class="week-row">
|
||||
<text class="day">一</text><text class="day">二</text><text class="day">三</text><text class="day">四</text><text class="day active">五</text><text class="day">六</text><text class="day">日</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="timeline" wx:if="{{stats}}">
|
||||
<view class="entry">
|
||||
<view class="dot done"></view>
|
||||
<view class="entry-card"><text class="entry-title">总打卡次数</text><text class="entry-desc">{{stats.total_checkins}} 次</text></view>
|
||||
</view>
|
||||
<view class="entry">
|
||||
<view class="dot"></view>
|
||||
<view class="entry-card low"><text class="entry-title">本周打卡</text><text class="entry-desc">{{stats.week_checkins}} 次</text></view>
|
||||
</view>
|
||||
<view class="entry">
|
||||
<view class="dot"></view>
|
||||
<view class="entry-card low"><text class="entry-title">本月打卡</text><text class="entry-desc">{{stats.month_checkins}} 次</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
1
progress/checkin-detail/checkin-detail.wxss
Normal file
1
progress/checkin-detail/checkin-detail.wxss
Normal file
@ -0,0 +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:40rpx 32rpx;box-sizing:border-box}.header{margin-bottom:24rpx}.title{display:block;font-size:52rpx;font-weight:800}.subtitle{display:block;margin-top:10rpx;color:#aaabb0;font-size:24rpx}.card{background:rgba(29,32,37,.56);border:1rpx solid rgba(255,255,255,.08);border-radius:28rpx}.hero{padding:28rpx}.hero-row{display:flex;justify-content:space-between;align-items:center}.hero-num{display:block;font-size:72rpx;font-weight:800}.hero-label{display:block;margin-top:6rpx;color:#aaabb0;font-size:22rpx}.fire{width:76rpx;height:76rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;font-size:34rpx}.progress-bar{height:10rpx;margin-top:20rpx;border-radius:9999rpx;background:#23262c;overflow:hidden}.fill{width:71%;height:100%;background:linear-gradient(90deg,#c19cff,#00e5ff)}.week-row{display:flex;justify-content:space-between;margin-top:16rpx;color:#aaabb0;font-size:22rpx}.day.active{color:#00e5ff;font-weight:700}.timeline{margin-top:18rpx;display:flex;flex-direction:column;gap:14rpx}.entry{display:flex;gap:14rpx;align-items:stretch}.dot{width:18rpx;height:18rpx;margin-top:20rpx;border-radius:9999rpx;background:#23262c;flex-shrink:0}.dot.done{background:#00e5ff;box-shadow:0 0 12rpx rgba(0,229,255,.6)}.entry-card{flex:1;padding:20rpx;border-radius:22rpx;background:#171a1f;border:1rpx solid rgba(255,255,255,.06)}.low{background:#111318}.entry-title{display:block;font-size:26rpx;font-weight:700}.entry-desc{display:block;margin-top:6rpx;color:#aaabb0;font-size:22rpx}
|
||||
54
progress/progress-list/progress-list.js
Normal file
54
progress/progress-list/progress-list.js
Normal file
@ -0,0 +1,54 @@
|
||||
const app = getApp()
|
||||
|
||||
const groupByType = (list) => ({
|
||||
word: list.filter((item) => item.entry_type === 'word'),
|
||||
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) => {
|
||||
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) => {
|
||||
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 }),
|
||||
})
|
||||
},
|
||||
})
|
||||
3
progress/progress-list/progress-list.json
Normal file
3
progress/progress-list/progress-list.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
167
progress/progress-list/progress-list.wxml
Normal file
167
progress/progress-list/progress-list.wxml
Normal file
@ -0,0 +1,167 @@
|
||||
<view class="page">
|
||||
<view class="bg-orb bg-orb-right"></view>
|
||||
<view class="bg-orb bg-orb-left"></view>
|
||||
|
||||
<scroll-view scroll-y class="content">
|
||||
<view class="header">
|
||||
<text class="title">复习档案</text>
|
||||
<text class="subtitle">您的词汇神经元网络正在构建中。</text>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<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">
|
||||
<text class="empty-title">加载中...</text>
|
||||
<text class="empty-subtitle">正在同步进度数据</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{error}}" class="error-banner">
|
||||
<text class="error-text">{{error}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:else>
|
||||
<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>
|
||||
<view class="section-toggle-chip {{reviewWordCollapsed ? 'collapsed' : 'expanded'}}">
|
||||
<text class="section-toggle-text">{{reviewWordCollapsed ? '展开' : '收起'}}</text>
|
||||
</view>
|
||||
</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">
|
||||
<text class="empty-title">暂无进度内容</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
1
progress/progress-list/progress-list.wxss
Normal file
1
progress/progress-list/progress-list.wxss
Normal file
@ -0,0 +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: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}
|
||||
120
review/review-config/review-config.js
Normal file
120
review/review-config/review-config.js
Normal file
@ -0,0 +1,120 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
timeRanges: ['??', '??', '??'],
|
||||
activeTimeRange: 0,
|
||||
contentTypes: ['??', '??'],
|
||||
activeContentType: 0,
|
||||
modes: ['?? -> ????', '?? -> ????', '??????'],
|
||||
activeMode: 0,
|
||||
onlyReview: true,
|
||||
shuffle: false,
|
||||
reviewOrder: 'sequential',
|
||||
questionCount: 30,
|
||||
loading: false,
|
||||
error: '',
|
||||
},
|
||||
onLoad() {
|
||||
const plan = app.globalData.reviewPlan
|
||||
this.setData({
|
||||
activeTimeRange: plan.timeRangeIndex,
|
||||
activeContentType: plan.contentTypeIndex,
|
||||
activeMode: plan.modeIndex,
|
||||
onlyReview: plan.onlyReview,
|
||||
shuffle: plan.reviewOrder === 'shuffle' || plan.shuffle,
|
||||
reviewOrder: plan.reviewOrder || (plan.shuffle ? 'shuffle' : 'sequential'),
|
||||
questionCount: plan.questionCount,
|
||||
})
|
||||
},
|
||||
persistPatch(patch) {
|
||||
app.globalData.reviewPlan = { ...app.globalData.reviewPlan, ...patch }
|
||||
},
|
||||
setTimeRange(e) {
|
||||
const activeTimeRange = Number(e.currentTarget.dataset.index)
|
||||
this.setData({ activeTimeRange })
|
||||
this.persistPatch({ timeRangeIndex: activeTimeRange })
|
||||
},
|
||||
setContentType(e) {
|
||||
const activeContentType = Number(e.currentTarget.dataset.index)
|
||||
this.setData({ activeContentType })
|
||||
this.persistPatch({ contentTypeIndex: activeContentType })
|
||||
},
|
||||
setMode(e) {
|
||||
const activeMode = Number(e.currentTarget.dataset.index)
|
||||
this.setData({ activeMode })
|
||||
this.persistPatch({ modeIndex: activeMode })
|
||||
},
|
||||
toggleOnlyReview() {
|
||||
const onlyReview = !this.data.onlyReview
|
||||
this.setData({ onlyReview })
|
||||
this.persistPatch({ onlyReview })
|
||||
},
|
||||
toggleShuffle() {
|
||||
const shuffle = !this.data.shuffle
|
||||
this.setData({ shuffle, reviewOrder: shuffle ? 'shuffle' : 'sequential' })
|
||||
this.persistPatch({ shuffle, reviewOrder: shuffle ? 'shuffle' : 'sequential' })
|
||||
},
|
||||
onQuestionCountInput(e) {
|
||||
const questionCount = Number(e.detail.value || 0)
|
||||
this.setData({ questionCount })
|
||||
this.persistPatch({ questionCount })
|
||||
},
|
||||
startReview() {
|
||||
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 scope_config = {}
|
||||
this.setData({ loading: true, error: '' })
|
||||
app.ensureLogin().then((openid) => {
|
||||
if (!openid) {
|
||||
this.setData({ loading: false, error: '??????????' })
|
||||
return
|
||||
}
|
||||
wx.request({
|
||||
url: `${app.globalData.baseUrl}/api/v1/review/generate`,
|
||||
method: 'POST',
|
||||
header: { 'Content-Type': 'application/json', 'X-OpenID': openid },
|
||||
data: {
|
||||
scope_type,
|
||||
scope_config,
|
||||
content_type,
|
||||
question_mode,
|
||||
review_strategy: this.data.onlyReview ? 'review_only' : 'all',
|
||||
sort_mode: this.data.reviewOrder === 'shuffle' ? 'shuffle' : 'sequential',
|
||||
total_count: this.data.questionCount,
|
||||
},
|
||||
success: (res) => {
|
||||
const session = res.data?.data || null
|
||||
if (res.statusCode < 200 || res.statusCode >= 300 || !session?.id) {
|
||||
const message = res.data?.detail || res.data?.message || '????????'
|
||||
this.setData({ error: message })
|
||||
wx.showToast({ title: message, icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!session.record_ids?.length) {
|
||||
const message = '??????????????????????????'
|
||||
this.setData({ error: message })
|
||||
wx.showToast({ title: message, icon: 'none' })
|
||||
return
|
||||
}
|
||||
app.globalData.reviewSessionMeta = session
|
||||
wx.setStorageSync('reviewSessionMeta', session)
|
||||
wx.navigateTo({ url: '/pages/review/review-session/review-session' })
|
||||
},
|
||||
fail: () => this.setData({ error: '????????' }),
|
||||
complete: () => this.setData({ loading: false }),
|
||||
})
|
||||
})
|
||||
},
|
||||
goHome() {
|
||||
wx.reLaunch({ url: '/pages/index/index' })
|
||||
},
|
||||
goToReview() {},
|
||||
goToMe() {
|
||||
wx.navigateTo({ url: '/pages/me/me' })
|
||||
},
|
||||
})
|
||||
3
review/review-config/review-config.json
Normal file
3
review/review-config/review-config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "复习配置"
|
||||
}
|
||||
45
review/review-config/review-config.wxml
Normal file
45
review/review-config/review-config.wxml
Normal file
@ -0,0 +1,45 @@
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="content config-content">
|
||||
<view class="heading">
|
||||
<text class="title">配置复习计划</text>
|
||||
<text class="subtitle">定制你的复习范围、题型和题量。</text>
|
||||
</view>
|
||||
|
||||
<view class="config-card">
|
||||
<text class="section-title">时间范围</text>
|
||||
<view class="chip-row">
|
||||
<text wx:for="{{timeRanges}}" wx:key="index" data-index="{{index}}" bindtap="setTimeRange" class="chip {{index===activeTimeRange?'active':''}}">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="config-card">
|
||||
<text class="section-title">内容类型</text>
|
||||
<view class="two-grid">
|
||||
<view wx:for="{{contentTypes}}" wx:key="index" data-index="{{index}}" bindtap="setContentType" class="option-box {{index===activeContentType?'active':''}}">{{item}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="config-card">
|
||||
<text class="section-title">训练模式</text>
|
||||
<view class="mode-list">
|
||||
<view wx:for="{{modes}}" wx:key="index" data-index="{{index}}" bindtap="setMode" class="mode-item {{index===activeMode?'active':''}}">{{item}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="config-card">
|
||||
<text class="section-title">策略微调</text>
|
||||
<view class="switch-row" bindtap="toggleOnlyReview"><text>仅待复习</text><text class="switch {{onlyReview?'on':''}}">{{onlyReview ? 'ON' : 'OFF'}}</text></view>
|
||||
<view class="switch-row" bindtap="toggleShuffle"><text>随机打乱</text><text class="switch {{shuffle?'on':''}}">{{shuffle ? 'ON' : 'OFF'}}</text></view>
|
||||
<view class="range-row"><text>题量选择</text><text class="accent">{{questionCount}} 题</text></view>
|
||||
<slider min="5" max="100" step="5" value="{{questionCount}}" activeColor="#c19cff" backgroundColor="#23262c" bindchanging="onQuestionCountInput" bindchange="onQuestionCountInput" />
|
||||
</view>
|
||||
|
||||
<button class="start-btn" bindtap="startReview">开始复习</button>
|
||||
</scroll-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 active"><text class="nav-icon">📖</text><text class="nav-label">复习</text></view>
|
||||
<view class="nav-item" bindtap="goToMe"><text class="nav-icon">☺</text><text class="nav-label">我的</text></view>
|
||||
</view>
|
||||
</view>
|
||||
26
review/review-config/review-config.wxss
Normal file
26
review/review-config/review-config.wxss
Normal file
@ -0,0 +1,26 @@
|
||||
page { background:#0c0e12; color:#f6f6fc; }
|
||||
.page { min-height:100vh; background:#0c0e12; box-sizing:border-box; }
|
||||
.content { height:calc(100vh - 120rpx); padding:32rpx; box-sizing:border-box; }
|
||||
.config-content { padding-bottom:180rpx; }
|
||||
.heading { margin:16rpx 0 24rpx; }
|
||||
.title { display:block; font-size:52rpx; font-weight:800; }
|
||||
.subtitle { display:block; margin-top:10rpx; font-size:28rpx; color:#aaabb0; line-height:1.6; }
|
||||
.config-card { margin-top:24rpx; padding:28rpx; border-radius:28rpx; background:rgba(29,32,37,.65); border:1rpx solid rgba(255,255,255,.08); }
|
||||
.section-title { display:block; font-size:32rpx; font-weight:700; margin-bottom:20rpx; }
|
||||
.chip-row { display:flex; flex-wrap:wrap; gap:16rpx; }
|
||||
.chip { padding:18rpx 28rpx; border-radius:9999rpx; background:#23262c; color:#aaabb0; }
|
||||
.chip.active { background:#c19cff; color:#000; }
|
||||
.two-grid { display:grid; grid-template-columns:1fr 1fr; gap:18rpx; }
|
||||
.option-box, .mode-item { padding:24rpx; border-radius:24rpx; background:#111318; border:1rpx solid rgba(255,255,255,.08); text-align:center; color:#aaabb0; min-height:96rpx; display:flex; align-items:center; justify-content:center; }
|
||||
.option-box.active, .mode-item.active { color:#c19cff; border-color:#c19cff; }
|
||||
.mode-list { display:flex; flex-direction:column; gap:18rpx; }
|
||||
.switch-row, .range-row { display:flex; justify-content:space-between; align-items:center; padding:12rpx 0; }
|
||||
.switch { padding:6rpx 16rpx; border-radius:9999rpx; background:#23262c; color:#aaabb0; }
|
||||
.switch.on { background:#c19cff; color:#000; }
|
||||
.accent { color:#c19cff; font-weight:700; }
|
||||
.start-btn { margin:32rpx 0 48rpx; padding:28rpx 0; border-radius:9999rpx; background:linear-gradient(90deg,#c19cff,#9146ff); color:#fff; font-size:34rpx; font-weight:800; box-shadow:0 0 20rpx rgba(193,156,255,.28); }
|
||||
.bottom-nav { position:fixed; left:0; right:0; bottom:0; z-index:50; display:flex; justify-content:space-around; align-items:center; padding:20rpx 24rpx 40rpx; background:rgba(12,14,18,.7); backdrop-filter:blur(32rpx); border-top:1rpx solid rgba(255,255,255,.08); border-radius:32rpx 32rpx 0 0; box-shadow:0 -10rpx 40rpx rgba(0,0,0,.4); }
|
||||
.nav-item { display:flex; flex-direction:column; align-items:center; gap:6rpx; color:#aaabb0; }
|
||||
.nav-item.active { color:#00e3fd; transform:translateY(-6rpx); }
|
||||
.nav-icon { width:56rpx; height:56rpx; border-radius:9999rpx; background:rgba(255,255,255,.05); display:flex; align-items:center; justify-content:center; }
|
||||
.nav-label { font-size:20rpx; }
|
||||
80
review/review-result/review-result.js
Normal file
80
review/review-result/review-result.js
Normal file
@ -0,0 +1,80 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
accuracy: 0,
|
||||
timeUsed: '--:--',
|
||||
total: 0,
|
||||
mastered: 0,
|
||||
reviewNeeded: 0,
|
||||
improved: 0,
|
||||
regressed: 0,
|
||||
loading: false,
|
||||
error: '',
|
||||
clozeResults: [],
|
||||
},
|
||||
onLoad() {
|
||||
this.loadResult()
|
||||
},
|
||||
loadResult() {
|
||||
const sessionId = app.globalData.reviewSessionMeta?.id
|
||||
const fallbackSession = app.globalData.reviewSession
|
||||
if (!sessionId) {
|
||||
if (fallbackSession?.completed) {
|
||||
this.setData({ error: '本地复习已完成,但结果会话信息已丢失' })
|
||||
} else {
|
||||
this.setData({ error: '暂无复习结果' })
|
||||
}
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true, error: '' })
|
||||
wx.request({
|
||||
url: `${app.globalData.baseUrl}/api/v1/review-result?session_id=${sessionId}`,
|
||||
header: { 'X-OpenID': app.globalData.openid || '' },
|
||||
success: (res) => {
|
||||
const data = res.data?.data || {}
|
||||
const total = Number(data.total || 0)
|
||||
const correct = Number(data.correct || 0)
|
||||
const wrong = Number(data.wrong || Math.max(0, total - correct))
|
||||
const accuracy = total ? Math.round((correct / total) * 100) : 0
|
||||
const clozeResults = Array.isArray(data.cloze_results)
|
||||
? data.cloze_results.map((item) => ({
|
||||
record_id: item.record_id,
|
||||
entry_id: item.entry_id,
|
||||
question_mode: item.question_mode,
|
||||
user_answer: item.user_answer || '',
|
||||
prompt_text: item.cloze_question?.prompt_text || '',
|
||||
blanks: Array.isArray(item.blanks)
|
||||
? item.blanks.map((blank) => ({
|
||||
index: blank.index,
|
||||
user_answer: blank.user_answer || '',
|
||||
correct_answers: Array.isArray(blank.correct_answers) ? blank.correct_answers : [],
|
||||
is_correct: !!blank.is_correct,
|
||||
}))
|
||||
: [],
|
||||
is_correct: !!item.blanks?.every?.((blank) => blank.is_correct),
|
||||
}))
|
||||
: []
|
||||
this.setData({
|
||||
total,
|
||||
accuracy,
|
||||
mastered: Number(data.mastered ?? correct),
|
||||
reviewNeeded: Number(data.review_needed ?? wrong),
|
||||
improved: Number(data.improved ?? 0),
|
||||
regressed: Number(data.regressed ?? 0),
|
||||
timeUsed: data.time_used || '--:--',
|
||||
clozeResults,
|
||||
})
|
||||
},
|
||||
fail: () => this.setData({ error: '结果加载失败' }),
|
||||
complete: () => this.setData({ loading: false }),
|
||||
})
|
||||
},
|
||||
restart() {
|
||||
app.globalData.reviewSession = { deck: [], currentIndex: 0, correctCount: 0, wrongCount: 0, completed: false, currentAnswer: '' }
|
||||
wx.reLaunch({ url: '/pages/review/review-config/review-config' })
|
||||
},
|
||||
goHome() {
|
||||
wx.reLaunch({ url: '/pages/index/index' })
|
||||
},
|
||||
})
|
||||
3
review/review-result/review-result.json
Normal file
3
review/review-result/review-result.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
91
review/review-result/review-result.wxml
Normal file
91
review/review-result/review-result.wxml
Normal file
@ -0,0 +1,91 @@
|
||||
<view class="page">
|
||||
<view class="bg-orb orb1"></view>
|
||||
<view class="bg-orb orb2"></view>
|
||||
|
||||
<view class="hero">
|
||||
<view class="badge">
|
||||
<view class="badge-inner">🏆</view>
|
||||
</view>
|
||||
<view class="hero-copy">
|
||||
<text class="headline">复习结果</text>
|
||||
<text class="subtitle">本次复习已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<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>
|
||||
|
||||
<view wx:else class="grid">
|
||||
<view class="hero-card">
|
||||
<view class="hero-card-head">
|
||||
<view>
|
||||
<text class="metric-label">正确率</text>
|
||||
<view class="metric-row">
|
||||
<text class="metric-big">{{accuracy}}</text>
|
||||
<text class="metric-unit">%</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ring">✓</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mini-card">
|
||||
<view class="mini-head"><text class="mini-ico">⏱</text><text class="mini-label">耗时</text></view>
|
||||
<text class="mini-value">{{timeUsed}}</text>
|
||||
</view>
|
||||
|
||||
<view class="mini-card">
|
||||
<view class="mini-head"><text class="mini-ico">📚</text><text class="mini-label">总题数</text></view>
|
||||
<text class="mini-value">{{total}}</text>
|
||||
</view>
|
||||
|
||||
<view class="breakdown">
|
||||
<view class="break-item">
|
||||
<text class="break-num cyan">{{mastered}}</text>
|
||||
<text class="break-label">已掌握</text>
|
||||
</view>
|
||||
<view class="break-item">
|
||||
<text class="break-num pink">{{reviewNeeded}}</text>
|
||||
<text class="break-label">需加强</text>
|
||||
</view>
|
||||
<view class="break-item">
|
||||
<text class="break-num green">{{improved}}</text>
|
||||
<text class="break-label">进步题目</text>
|
||||
</view>
|
||||
<view class="break-item">
|
||||
<text class="break-num amber">{{regressed}}</text>
|
||||
<text class="break-label">退步题目</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{clozeResults.length}}" class="cloze-panel">
|
||||
<text class="panel-title">填空逐空反馈</text>
|
||||
<view wx:for="{{clozeResults}}" wx:for-item="item" wx:for-index="itemIndex" wx:key="record_id" class="cloze-result-card">
|
||||
<view class="cloze-result-head">
|
||||
<text class="cloze-result-index">题目 {{itemIndex + 1}}</text>
|
||||
<text class="cloze-result-status {{item.is_correct ? 'ok' : 'bad'}}">{{item.is_correct ? '正确' : '错误'}}</text>
|
||||
</view>
|
||||
<view wx:if="{{item.prompt_text}}" class="cloze-result-prompt">{{item.prompt_text}}</view>
|
||||
<view class="cloze-result-body">
|
||||
<view wx:for="{{item.blanks}}" wx:for-item="blank" wx:for-index="blankIndex" wx:key="index" class="cloze-blank-line">
|
||||
<text class="cloze-result-answer">空 {{blankIndex + 1}}:{{blank.user_answer || '(空)'}}</text>
|
||||
<text class="cloze-result-answer">答案:{{blank.correct_answers.join(' / ') || '—'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="actions">
|
||||
<button class="primary-btn" bindtap="restart">再来一组</button>
|
||||
<button class="secondary-btn" bindtap="goHome">返回首页</button>
|
||||
</view>
|
||||
</view>
|
||||
1
review/review-result/review-result.wxss
Normal file
1
review/review-result/review-result.wxss
Normal file
@ -0,0 +1 @@
|
||||
page{background:#0c0e12;color:#f6f6fc}.page{min-height:100vh;padding:48rpx 32rpx 60rpx;box-sizing:border-box;background:#0c0e12;position:relative;overflow:hidden}.bg-orb{position:absolute;border-radius:9999rpx;pointer-events:none;filter:blur(120rpx);opacity:.2;z-index:0}.orb1{top:18%;left:50%;width:520rpx;height:520rpx;transform:translate(-50%,-50%);background:#ff6c95}.orb2{bottom:18%;left:50%;width:380rpx;height:380rpx;transform:translate(-50%,50%);background:#00e5ff}.hero,.grid,.actions{position:relative;z-index:1}.hero{display:flex;flex-direction:column;align-items:center;gap:22rpx;margin:26rpx 0 44rpx}.badge{width:144rpx;height:144rpx;border-radius:9999rpx;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.06);box-shadow:0 0 30rpx rgba(255,108,149,.2)}.badge-inner{width:100%;height:100%;border-radius:9999rpx;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,rgba(255,108,149,.2),rgba(0,229,255,.2));font-size:56rpx}.headline{display:block;font-size:54rpx;font-weight:800;text-align:center}.subtitle{display:block;margin-top:8rpx;color:#00e5ff;letter-spacing:4rpx;text-transform:uppercase;text-align:center}.grid{display:grid;grid-template-columns:1fr 1fr;gap:18rpx}.hero-card{grid-column:1/-1;background:rgba(29,32,37,.7);backdrop-filter:blur(20rpx);border:1rpx solid rgba(255,255,255,.1);border-radius:28rpx;padding:32rpx;box-sizing:border-box;box-shadow:0 20rpx 40rpx rgba(0,0,0,.4);position:relative;overflow:hidden}.hero-card:after{content:'';position:absolute;top:-40rpx;right:-40rpx;width:160rpx;height:160rpx;border-radius:9999rpx;background:rgba(193,156,255,.1);filter:blur(48rpx)}.hero-card-head{display:flex;justify-content:space-between;align-items:center;position:relative;z-index:1}.metric-label{display:block;color:#aaabb0;font-size:20rpx;letter-spacing:4rpx;text-transform:uppercase}.metric-row{display:flex;align-items:baseline;gap:10rpx}.metric-big{font-size:88rpx;font-weight:800;color:#fff;line-height:1}.metric-unit{font-size:32rpx;color:#aaabb0;font-weight:700}.ring{width:124rpx;height:124rpx;border-radius:9999rpx;border:4rpx solid #23262c;display:flex;align-items:center;justify-content:center;color:#c19cff;font-size:52rpx;position:relative}.mini-card{background:rgba(29,32,37,.7);backdrop-filter:blur(20rpx);border:1rpx solid rgba(255,255,255,.1);border-radius:28rpx;padding:24rpx;box-sizing:border-box;box-shadow:0 20rpx 40rpx rgba(0,0,0,.4);display:flex;flex-direction:column;gap:18rpx}.mini-head{display:flex;align-items:center;gap:14rpx}.mini-ico{width:56rpx;height:56rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;font-size:28rpx}.mini-label{color:#aaabb0;font-size:20rpx;letter-spacing:4rpx;text-transform:uppercase}.mini-value{font-size:36rpx;font-weight:800}.breakdown{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;gap:0;border-radius:28rpx;overflow:hidden;border:1rpx solid rgba(255,255,255,.1)}.break-item{background:#1d2025;padding:30rpx;text-align:center}.break-num{display:block;font-size:54rpx;font-weight:800}.cyan{color:#00e5ff}.pink{color:#ff6c95}.break-label{display:block;margin-top:10rpx;color:#aaabb0;font-size:24rpx}.cloze-panel{grid-column:1/-1;margin-top:8rpx;padding:28rpx;border-radius:28rpx;background:rgba(29,32,37,.7);border:1rpx solid rgba(255,255,255,.1);box-shadow:0 20rpx 40rpx rgba(0,0,0,.28)}.panel-title{display:block;font-size:28rpx;font-weight:800;margin-bottom:18rpx;color:#f6f6fc}.cloze-result-card{padding:22rpx 20rpx;border-radius:22rpx;background:rgba(255,255,255,.04);border:1rpx solid rgba(255,255,255,.06);margin-bottom:16rpx}.cloze-result-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12rpx}.cloze-result-index{font-size:24rpx;color:#aaabb0}.cloze-result-status{font-size:22rpx;font-weight:700;padding:8rpx 16rpx;border-radius:9999rpx}.cloze-result-status.ok{background:rgba(0,229,255,.12);color:#00e5ff}.cloze-result-status.bad{background:rgba(255,108,149,.12);color:#ff6c95}.cloze-result-prompt{margin-bottom:14rpx;color:#d8c7ff;font-size:24rpx;line-height:1.6}.cloze-result-body{display:flex;flex-direction:column;gap:10rpx}.cloze-blank-line{display:flex;flex-direction:column;gap:6rpx;padding:14rpx 14rpx;border-radius:16rpx;background:rgba(255,255,255,.03)}.cloze-result-answer{font-size:24rpx;line-height:1.6;color:#e8e9ee}.actions{display:flex;gap:18rpx;margin-top:28rpx;position:relative;z-index:1}.primary-btn,.secondary-btn{flex:1;border-radius:9999rpx;padding:24rpx 0;font-weight:800}.primary-btn{background:linear-gradient(90deg,#c19cff,#9146ff);color:#fff}.secondary-btn{background:#23262c;color:#f6f6fc}
|
||||
443
review/review-session/review-session.js
Normal file
443
review/review-session/review-session.js
Normal file
@ -0,0 +1,443 @@
|
||||
const app = getApp()
|
||||
|
||||
const getReviewSettings = () => {
|
||||
const settings = app.globalData.userSettings || {}
|
||||
return {
|
||||
autoNextQuestion: Number(settings.auto_next_question || 0) === 1,
|
||||
showAnswerAfterSubmit: Number(settings.show_answer_after_submit || 0) === 1,
|
||||
blankCount: Number(settings.blank_count || 2),
|
||||
blankDifficulty: settings.blank_difficulty || 'normal',
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
progress: 0,
|
||||
current: 1,
|
||||
total: 0,
|
||||
word: '',
|
||||
meaning: '',
|
||||
answer: '',
|
||||
answers: [],
|
||||
clozeAnswers: [],
|
||||
feedback: '',
|
||||
revealAnswer: '',
|
||||
feedbackType: '',
|
||||
showFeedback: false,
|
||||
completed: false,
|
||||
masterHint: false,
|
||||
questions: [],
|
||||
loading: false,
|
||||
error: '',
|
||||
settings: getReviewSettings(),
|
||||
questionBadge: '',
|
||||
entryTypeHint: '',
|
||||
isCloze: false,
|
||||
currentClozeQuestion: null,
|
||||
currentClozeResults: [],
|
||||
showClozeFeedback: false,
|
||||
clozeSegments: [],
|
||||
clozeFeedbackSummary: '',
|
||||
activeClozeBlankIndex: 0,
|
||||
},
|
||||
submitting: false,
|
||||
judged: false,
|
||||
onLoad() {
|
||||
this.loadSettingsAndQuestions()
|
||||
},
|
||||
loadSettingsAndQuestions() {
|
||||
if (!app.globalData.openid) {
|
||||
this.setData({ error: '????' })
|
||||
return
|
||||
}
|
||||
wx.request({
|
||||
url: `${app.globalData.baseUrl}/api/v1/settings`,
|
||||
header: { 'X-OpenID': app.globalData.openid },
|
||||
success: (res) => {
|
||||
app.globalData.userSettings = res.data?.data || {}
|
||||
this.setData({ settings: getReviewSettings() })
|
||||
this.loadQuestions()
|
||||
},
|
||||
fail: () => {
|
||||
this.setData({ error: '??????' })
|
||||
this.loadQuestions()
|
||||
},
|
||||
})
|
||||
},
|
||||
buildClozeSegments(clozeQuestion) {
|
||||
if (!clozeQuestion || !Array.isArray(clozeQuestion.blanks)) return []
|
||||
const prompt = clozeQuestion.prompt_text || ''
|
||||
const blanks = clozeQuestion.blanks || []
|
||||
const segments = []
|
||||
const useUnderscorePrompt = prompt.includes('____')
|
||||
const workingPrompt = useUnderscorePrompt ? prompt : (clozeQuestion.original_text || prompt)
|
||||
|
||||
const pushWordBlank = (blankIndex, blank) => {
|
||||
const answerText = String(blank?.answer || '')
|
||||
const answerLength = Math.max(1, answerText.length || Number(blank?.hint?.length || 1))
|
||||
const blankWidth = Math.max(88, Math.min(320, 32 + answerLength * 28))
|
||||
segments.push({
|
||||
type: 'word-blank',
|
||||
blankIndex,
|
||||
index: `blank-${blankIndex}`,
|
||||
width: `${blankWidth}rpx`,
|
||||
minWidth: `${blankWidth}rpx`,
|
||||
maxLength: Math.max(1, answerLength),
|
||||
})
|
||||
}
|
||||
|
||||
if (useUnderscorePrompt) {
|
||||
let blankIndex = 0
|
||||
workingPrompt.split('____').forEach((text, index, parts) => {
|
||||
if (text) segments.push({ type: 'text', text, index: `text-${index}` })
|
||||
if (index < parts.length - 1) {
|
||||
pushWordBlank(blankIndex, blanks[blankIndex])
|
||||
blankIndex += 1
|
||||
}
|
||||
})
|
||||
return segments
|
||||
}
|
||||
|
||||
if (blanks.length === 1 && workingPrompt === (clozeQuestion.original_text || prompt)) {
|
||||
// Keep the original word context visible for single-word cloze questions.
|
||||
// The general start/end slicing below will render visible letters around the blank.
|
||||
}
|
||||
|
||||
let cursor = 0
|
||||
blanks.forEach((blank, idx) => {
|
||||
const start = Math.max(0, Number(blank.start || 0))
|
||||
const end = Math.max(start, Number(blank.end || start + 1))
|
||||
if (start > cursor) {
|
||||
segments.push({ type: 'text', text: workingPrompt.slice(cursor, start), index: `text-${idx}` })
|
||||
}
|
||||
pushWordBlank(idx, blank)
|
||||
cursor = end
|
||||
})
|
||||
if (cursor < workingPrompt.length) {
|
||||
segments.push({ type: 'text', text: workingPrompt.slice(cursor), index: `text-tail` })
|
||||
}
|
||||
return segments
|
||||
},
|
||||
loadQuestions() {
|
||||
const session = app.globalData.reviewSessionMeta || wx.getStorageSync('reviewSessionMeta')
|
||||
if (!session?.id) {
|
||||
this.setData({ error: '??????' })
|
||||
return
|
||||
}
|
||||
app.globalData.reviewSessionMeta = session
|
||||
this.setData({ loading: true, error: '' })
|
||||
wx.request({
|
||||
url: `${app.globalData.baseUrl}/api/v1/review/questions?session_id=${session.id}`,
|
||||
header: { 'X-OpenID': app.globalData.openid || '' },
|
||||
success: (res) => {
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
const message = res.data?.detail || res.data?.message || '??????'
|
||||
this.setData({ error: message, questions: [] })
|
||||
return
|
||||
}
|
||||
const questions = (res.data?.data || []).map((item) => {
|
||||
if (item.question_mode === 'C') {
|
||||
return {
|
||||
...item,
|
||||
blank_prompt: item.blank_prompt || item.cloze_question?.prompt_text || item.prompt,
|
||||
cloze_question: item.cloze_question || null,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
entry_type: item.entry_type || 'word',
|
||||
}
|
||||
})
|
||||
if (!questions.length) {
|
||||
this.setData({ error: '????????????????????', questions: [] })
|
||||
return
|
||||
}
|
||||
app.globalData.reviewSession = {
|
||||
deck: questions,
|
||||
currentIndex: 0,
|
||||
correctCount: 0,
|
||||
wrongCount: 0,
|
||||
completed: false,
|
||||
currentAnswer: '',
|
||||
currentAnswers: [],
|
||||
startedAt: Date.now(),
|
||||
}
|
||||
this.setData({ questions })
|
||||
this.syncSession()
|
||||
},
|
||||
fail: () => this.setData({ error: '??????' }),
|
||||
complete: () => this.setData({ loading: false }),
|
||||
})
|
||||
},
|
||||
syncSession() {
|
||||
const session = app.globalData.reviewSession
|
||||
const current = session.deck[session.currentIndex]
|
||||
const total = session.deck.length || 1
|
||||
const isCloze = current?.question_mode === 'C'
|
||||
const clozeQuestion = isCloze ? (current?.cloze_question || null) : null
|
||||
this.setData({
|
||||
progress: Math.round((session.currentIndex / total) * 100),
|
||||
current: session.currentIndex + 1,
|
||||
total,
|
||||
word: isCloze ? (current?.blank_prompt || clozeQuestion?.prompt_text || current?.prompt || '') : (current?.prompt || ''),
|
||||
meaning: isCloze ? (current?.answer_hint || '') : '',
|
||||
answer: session.currentAnswer || '',
|
||||
clozeAnswers: isCloze ? (Array.isArray(session.currentAnswers) ? session.currentAnswers : Array.from({ length: clozeQuestion?.blanks?.length || 0 }, () => '')) : [],
|
||||
activeClozeBlankIndex: 0,
|
||||
feedback: '',
|
||||
revealAnswer: '',
|
||||
feedbackType: '',
|
||||
showFeedback: false,
|
||||
completed: session.completed,
|
||||
masterHint: false,
|
||||
questionBadge: isCloze ? `?? ? ${current?.entry_type === 'sentence' ? '??' : '??'}` : `${current?.entry_type === 'sentence' ? '??' : '??'} ? ${current?.question_mode || 'A'}`,
|
||||
entryTypeHint: isCloze ? '??????????????????' : (current?.entry_type === 'sentence' ? '?????????????????' : ''),
|
||||
isCloze,
|
||||
currentClozeQuestion: clozeQuestion,
|
||||
currentClozeResults: session.lastClozeResults || [],
|
||||
showClozeFeedback: false,
|
||||
clozeSegments: isCloze ? this.buildClozeSegments(clozeQuestion) : [],
|
||||
clozeFeedbackSummary: '',
|
||||
})
|
||||
},
|
||||
onAnswerInput(e) {
|
||||
const answer = e.detail.value
|
||||
app.globalData.reviewSession.currentAnswer = answer
|
||||
this.setData({ answer, showFeedback: false, revealAnswer: '', feedback: '', feedbackType: '', judged: false, showClozeFeedback: false })
|
||||
},
|
||||
onClozeLetterInput(e) {
|
||||
const { index, letterIndex } = e.currentTarget.dataset
|
||||
const rawValue = e.detail.value || ''
|
||||
const cleaned = rawValue.slice(-1)
|
||||
const answers = [...(app.globalData.reviewSession.currentAnswers || [])]
|
||||
const current = answers[index] || ''
|
||||
const chars = current.split('')
|
||||
chars[letterIndex] = cleaned
|
||||
answers[index] = chars.join('').slice(0, Math.max(chars.length, letterIndex + 1))
|
||||
app.globalData.reviewSession.currentAnswers = answers
|
||||
this.setData({
|
||||
[`clozeAnswers[${index}]`]: answers[index],
|
||||
showFeedback: false,
|
||||
revealAnswer: '',
|
||||
feedback: '',
|
||||
feedbackType: '',
|
||||
judged: false,
|
||||
showClozeFeedback: false,
|
||||
}, () => {
|
||||
if (cleaned) this.focusNextLetterCell(Number(index), Number(letterIndex) + 1)
|
||||
})
|
||||
},
|
||||
focusNextLetterCell(blankIndex, letterIndex) {
|
||||
const selector = `#cloze-input-${blankIndex}-${letterIndex}`
|
||||
wx.nextTick(() => {
|
||||
const query = wx.createSelectorQuery().in(this)
|
||||
query.select(selector).fields({ node: true }).exec((res) => {
|
||||
if (!res || !res[0]) return
|
||||
const node = res[0].node
|
||||
if (node && typeof node.focus === 'function') node.focus()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleLetterKeydown(e) {
|
||||
const key = e?.detail?.key || e?.detail?.value || ''
|
||||
const { index, letterIndex } = e.currentTarget.dataset
|
||||
if (key === 'Backspace' || key === 'Delete') {
|
||||
const answers = [...(app.globalData.reviewSession.currentAnswers || [])]
|
||||
const current = answers[index] || ''
|
||||
const chars = current.split('')
|
||||
const pos = Number(letterIndex)
|
||||
if (chars[pos]) {
|
||||
chars[pos] = ''
|
||||
answers[index] = chars.join('')
|
||||
app.globalData.reviewSession.currentAnswers = answers
|
||||
this.setData({ [`clozeAnswers[${index}]`]: answers[index] })
|
||||
return
|
||||
}
|
||||
const prevIndex = Number(letterIndex) - 1
|
||||
if (prevIndex >= 0) this.focusNextLetterCell(Number(index), prevIndex)
|
||||
return
|
||||
}
|
||||
if (key === 'ArrowLeft') {
|
||||
const prevIndex = Number(letterIndex) - 1
|
||||
if (prevIndex >= 0) this.focusNextLetterCell(Number(index), prevIndex)
|
||||
return
|
||||
}
|
||||
if (key === 'ArrowRight' || key === ' ' || key === 'Space') {
|
||||
this.focusNextLetterCell(Number(index), Number(letterIndex) + 1)
|
||||
}
|
||||
},
|
||||
onClozeAnswerInput(e) {
|
||||
const { index } = e.currentTarget.dataset
|
||||
const rawValue = e.detail.value || ''
|
||||
const clozeQuestion = this.data.currentClozeQuestion
|
||||
const blank = clozeQuestion?.blanks?.[index]
|
||||
const maxLength = Math.max(1, Number(blank?.answer?.length || 1))
|
||||
const normalizedValue = rawValue.slice(-maxLength).slice(0, maxLength)
|
||||
const answers = [...(app.globalData.reviewSession.currentAnswers || [])]
|
||||
answers[index] = normalizedValue
|
||||
app.globalData.reviewSession.currentAnswers = answers
|
||||
const nextIndex = Number(index) + (normalizedValue.length >= maxLength ? 1 : 0)
|
||||
const prevIndex = Number(index) - 1
|
||||
this.setData({
|
||||
[`clozeAnswers[${index}]`]: normalizedValue,
|
||||
activeClozeBlankIndex: nextIndex,
|
||||
showFeedback: false,
|
||||
revealAnswer: '',
|
||||
feedback: '',
|
||||
feedbackType: '',
|
||||
judged: false,
|
||||
showClozeFeedback: false,
|
||||
}, () => {
|
||||
if (!normalizedValue && this.data.isCloze && rawValue === '') {
|
||||
this.focusNextClozeInput(Math.max(0, prevIndex))
|
||||
return
|
||||
}
|
||||
if (normalizedValue.length >= maxLength && this.data.isCloze) this.focusNextClozeInput(nextIndex)
|
||||
})
|
||||
},
|
||||
focusNextClozeInput(nextIndex) {
|
||||
const selector = `#cloze-input-${nextIndex}`
|
||||
wx.nextTick(() => {
|
||||
const query = wx.createSelectorQuery().in(this)
|
||||
query.select(selector).fields({ node: true, size: true }).exec((res) => {
|
||||
if (!res || !res[0]) return
|
||||
const node = res[0].node
|
||||
if (node && typeof node.focus === 'function') node.focus()
|
||||
})
|
||||
})
|
||||
},
|
||||
submitAnswer(showExplanation = false, advanceAfterSubmit = false) {
|
||||
const session = app.globalData.reviewSession
|
||||
const current = session.deck[session.currentIndex]
|
||||
if (!current || this.submitting || this.judged) return
|
||||
|
||||
const answer = (this.data.answer || '').trim()
|
||||
const clozeQuestion = current.cloze_question
|
||||
const payload = {
|
||||
session_id: app.globalData.reviewSessionMeta.id,
|
||||
record_id: current.record_id,
|
||||
user_answer: answer,
|
||||
}
|
||||
if (current.question_mode === 'C' && clozeQuestion?.blanks?.length) {
|
||||
const answers = (this.data.clozeAnswers || []).map((item) => (item || '').trim())
|
||||
while (answers.length < clozeQuestion.blanks.length) answers.push('')
|
||||
const isLetterCellMode = Array.isArray(this.data.clozeSegments) && this.data.clozeSegments.some((segment) => segment.type === 'letter-cell')
|
||||
const normalizedAnswers = isLetterCellMode
|
||||
? answers
|
||||
: answers
|
||||
if (!normalizedAnswers.some((item) => item)) {
|
||||
wx.showToast({ title: '??????', icon: 'none' })
|
||||
return
|
||||
}
|
||||
payload.user_answers = normalizedAnswers
|
||||
} else if (!answer) {
|
||||
wx.showToast({ title: '??????', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.submitting = true
|
||||
wx.request({
|
||||
url: `${app.globalData.baseUrl}/api/v1/review/submit`,
|
||||
method: 'POST',
|
||||
header: { 'Content-Type': 'application/json', 'X-OpenID': app.globalData.openid || '' },
|
||||
data: payload,
|
||||
success: (res) => {
|
||||
const result = res.data?.data || {}
|
||||
session.correctCount += result.is_correct ? 1 : 0
|
||||
session.wrongCount += result.is_correct ? 0 : 1
|
||||
this.judged = true
|
||||
const shouldShowAnswer = showExplanation || this.data.settings.showAnswerAfterSubmit
|
||||
const correctAnswerText = result.correct_answer_text || ''
|
||||
const clozeResults = Array.isArray(result.cloze_results) ? result.cloze_results : []
|
||||
const fallbackClozeResults = clozeResults.length
|
||||
? clozeResults
|
||||
: (this.data.currentClozeQuestion?.blanks || []).map((blank, idx) => ({
|
||||
index: typeof blank?.index === 'number' ? blank.index : idx,
|
||||
user_answer: '',
|
||||
correct_answers: [blank?.answer, ...(Array.isArray(blank?.accepted_answers) ? blank.accepted_answers : [])].filter(Boolean),
|
||||
correct_answer_text: blank?.answer || '',
|
||||
is_correct: false,
|
||||
}))
|
||||
const normalizedClozeResults = fallbackClozeResults.map((item, idx) => {
|
||||
const correctAnswers = Array.isArray(item.correct_answers)
|
||||
? item.correct_answers.filter(Boolean)
|
||||
: [item.correct_answer_text, ...(Array.isArray(item.accepted_answers) ? item.accepted_answers : [])].filter(Boolean)
|
||||
const standardAnswerText = correctAnswers.join(' / ') || item.correct_answer_text || ''
|
||||
return {
|
||||
index: item.index ?? idx,
|
||||
user_answer: item.user_answer ?? '',
|
||||
correct_answers: correctAnswers,
|
||||
correct_answer_text: item.correct_answer_text || standardAnswerText,
|
||||
standard_answer_text: standardAnswerText,
|
||||
is_correct: Boolean(item.is_correct),
|
||||
}
|
||||
})
|
||||
const derivedClozeResults = normalizedClozeResults.length
|
||||
? normalizedClozeResults
|
||||
: (correctAnswerText
|
||||
? [{ index: 0, user_answer: '', correct_answers: [correctAnswerText], correct_answer_text: correctAnswerText, standard_answer_text: correctAnswerText, is_correct: false }]
|
||||
: [])
|
||||
if (derivedClozeResults.length) {
|
||||
session.lastClozeResults = derivedClozeResults
|
||||
app.globalData.reviewSession.lastClozeResults = derivedClozeResults
|
||||
}
|
||||
const clozeFeedbackSummary = result.is_correct ? '????' : '????'
|
||||
this.setData({
|
||||
feedback: clozeFeedbackSummary,
|
||||
feedbackType: result.is_correct ? 'correct' : 'wrong',
|
||||
revealAnswer: '',
|
||||
showFeedback: true,
|
||||
showClozeFeedback: derivedClozeResults.length > 0,
|
||||
currentClozeResults: derivedClozeResults,
|
||||
clozeFeedbackSummary,
|
||||
})
|
||||
if (this.data.isCloze) {
|
||||
app.globalData.reviewSession.currentAnswers = []
|
||||
this.setData({ clozeAnswers: [] })
|
||||
}
|
||||
if (this.data.settings.autoNextQuestion || advanceAfterSubmit) {
|
||||
const delay = shouldShowAnswer ? 2500 : 0
|
||||
setTimeout(() => this.nextQuestion(), delay)
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
this.submitting = false
|
||||
},
|
||||
})
|
||||
},
|
||||
checkAnswer() {
|
||||
this.submitAnswer(true)
|
||||
},
|
||||
nextQuestion() {
|
||||
const session = app.globalData.reviewSession
|
||||
if (!session.deck.length) return
|
||||
if (!this.judged) {
|
||||
this.submitAnswer(true, true)
|
||||
return
|
||||
}
|
||||
this.judged = false
|
||||
if (session.currentIndex >= session.deck.length - 1) {
|
||||
session.completed = true
|
||||
session.endedAt = Date.now()
|
||||
wx.request({
|
||||
url: `${app.globalData.baseUrl}/api/v1/review/finish?session_id=${app.globalData.reviewSessionMeta.id}`,
|
||||
method: 'POST',
|
||||
header: { 'X-OpenID': app.globalData.openid || '' },
|
||||
success: () => wx.navigateTo({ url: '/pages/review/review-result/review-result' }),
|
||||
fail: () => {
|
||||
wx.showToast({ title: '????????????', icon: 'none' })
|
||||
wx.navigateTo({ url: '/pages/review/review-result/review-result' })
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
session.currentIndex += 1
|
||||
session.currentAnswer = ''
|
||||
session.currentAnswers = []
|
||||
this.syncSession()
|
||||
},
|
||||
markMastered() {
|
||||
this.setData({ masterHint: true })
|
||||
wx.showToast({ title: '???', icon: 'success' })
|
||||
},
|
||||
})
|
||||
3
review/review-session/review-session.json
Normal file
3
review/review-session/review-session.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
72
review/review-session/review-session.wxml
Normal file
72
review/review-session/review-session.wxml
Normal file
@ -0,0 +1,72 @@
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="content">
|
||||
<view class="progress-box">
|
||||
<view class="progress-head">
|
||||
<text class="progress-label">复习进度</text>
|
||||
<text class="progress-count">题号 {{current}} <text class="progress-total">/ {{total}}</text></text>
|
||||
</view>
|
||||
<view class="progress-bar ghost-border"><view class="progress-fill" style="width: {{progress}}%;"></view></view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!questions.length}}" class="empty-state">
|
||||
<text class="empty-title">暂无题目</text>
|
||||
<text class="empty-subtitle">{{error || '请先生成复习会话'}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:else class="glass-card question-card ghost-border">
|
||||
<view class="ambient-glow"></view>
|
||||
<view class="mode-tag-wrap">
|
||||
<text class="mode-tag">{{questionBadge}}</text>
|
||||
</view>
|
||||
<view class="meaning-wrap" wx:if="{{meaning}}">
|
||||
<text class="meaning">{{meaning}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{isCloze}}" class="cloze-wrap">
|
||||
<view wx:if="{{currentClozeQuestion && currentClozeQuestion.blanks.length}}" class="inline-cloze">
|
||||
<block wx:for="{{clozeSegments}}" wx:for-item="segment" wx:for-index="segmentIndex" wx:key="index">
|
||||
<text wx:if="{{segment.type === 'text'}}" class="cloze-segment-text">{{segment.text}}</text>
|
||||
<view wx:elif="{{segment.type === 'letter-cell'}}" class="cloze-letter-cell" style="width: {{segment.width}}; min-width: {{segment.minWidth}};">
|
||||
<input id="cloze-input-{{segment.blankIndex}}-{{segment.letterIndex}}" focus="{{activeClozeBlankIndex === segment.blankIndex && segment.letterIndex === 0}}" class="answer inline-cloze-answer letter-cell-input" data-index="{{segment.blankIndex}}" data-letter-index="{{segment.letterIndex}}" value="{{clozeAnswers[segment.blankIndex] ? clozeAnswers[segment.blankIndex].charAt(segment.letterIndex) : ''}}" placeholder="" maxlength="1" bindinput="onClozeLetterInput" bindkeydown="handleLetterKeydown" />
|
||||
<view class="cloze-letter-underline"></view>
|
||||
</view>
|
||||
<view wx:else class="cloze-blank-wrap" style="width: {{segment.width}}; min-width: {{segment.minWidth}};">
|
||||
<input id="cloze-input-{{segment.blankIndex}}" focus="{{activeClozeBlankIndex === segment.blankIndex}}" class="answer inline-cloze-answer" data-index="{{segment.blankIndex}}" value="{{clozeAnswers[segment.blankIndex] || ''}}" placeholder="" maxlength="{{segment.maxLength}}" bindinput="onClozeAnswerInput" />
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<text wx:else class="cloze">{{word}}</text>
|
||||
|
||||
</view>
|
||||
<view wx:else class="word-wrap">
|
||||
<text class="word">{{word}}</text>
|
||||
</view>
|
||||
<view class="entry-type-hint" wx:if="{{entryTypeHint}}">{{entryTypeHint}}</view>
|
||||
|
||||
<view class="input-shell" wx:if="{{!isCloze}}">
|
||||
<input class="answer" value="{{answer}}" placeholder="输入答案..." bindinput="onAnswerInput" />
|
||||
</view>
|
||||
<view wx:if="{{showFeedback}}" class="feedback-box {{feedbackType}}">
|
||||
<text class="feedback">{{clozeFeedbackSummary || feedback}}</text>
|
||||
<view wx:if="{{showClozeFeedback && currentClozeResults.length}}" class="cloze-feedback-list">
|
||||
<block wx:for="{{currentClozeResults}}" wx:for-item="blankResult" wx:for-index="blankIndex" wx:key="index">
|
||||
<view class="cloze-feedback-item">
|
||||
<text class="cloze-feedback-title">空 {{blankIndex + 1}}</text>
|
||||
<text class="cloze-feedback-text">你的答案:{{blankResult.user_answer || '(空)'}}</text>
|
||||
<text class="cloze-feedback-text">标准答案:{{blankResult.standard_answer_text || (blankResult.correct_answers && blankResult.correct_answers.length ? blankResult.correct_answers.join(' / ') : (blankResult.correct_answer_text || '(未返回)'))}}</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{questions.length}}" class="actions-cluster">
|
||||
<button class="secondary-btn" bindtap="checkAnswer">查看解析</button>
|
||||
<button class="primary-btn" bindtap="nextQuestion">下一题</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{questions.length}}" class="master-action" bindtap="markMastered">
|
||||
<text class="master-text {{masterHint ? 'active' : ''}}">标记已掌握</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
1
review/review-session/review-session.wxss
Normal file
1
review/review-session/review-session.wxss
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user