diff --git a/manage/batch-upload/batch-upload.js b/manage/batch-upload/batch-upload.js new file mode 100644 index 0000000..9702b18 --- /dev/null +++ b/manage/batch-upload/batch-upload.js @@ -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 }), + }) + }, + }) + }, +}) diff --git a/manage/batch-upload/batch-upload.json b/manage/batch-upload/batch-upload.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/manage/batch-upload/batch-upload.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/manage/batch-upload/batch-upload.wxml b/manage/batch-upload/batch-upload.wxml new file mode 100644 index 0000000..83e2505 --- /dev/null +++ b/manage/batch-upload/batch-upload.wxml @@ -0,0 +1,25 @@ + + + + + VocabMaster + + + + + 批量上传 + 先下载模板,按模板填写后直接上传单词或句子。 + + + + + + 已选择:{{fileName}} + {{error}} + + 总数:{{result.total}} + 成功:{{result.success}} + 失败:{{result.failed}} + + + diff --git a/manage/batch-upload/batch-upload.wxss b/manage/batch-upload/batch-upload.wxss new file mode 100644 index 0000000..813983c --- /dev/null +++ b/manage/batch-upload/batch-upload.wxss @@ -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)} diff --git a/manage/entry-detail/entry-detail.js b/manage/entry-detail/entry-detail.js new file mode 100644 index 0000000..942cb41 --- /dev/null +++ b/manage/entry-detail/entry-detail.js @@ -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() + }, +}) diff --git a/manage/entry-detail/entry-detail.json b/manage/entry-detail/entry-detail.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/manage/entry-detail/entry-detail.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/manage/entry-detail/entry-detail.wxml b/manage/entry-detail/entry-detail.wxml new file mode 100644 index 0000000..a2f3670 --- /dev/null +++ b/manage/entry-detail/entry-detail.wxml @@ -0,0 +1,60 @@ + + + + + + + + + {{entry.entry_type === 'sentence' ? '句子' : '单词'}} + + + 🗑 + + + + {{entry.en_text}} + {{entry.entry_type === 'sentence' ? '翻译:' : '释义:'}}{{entry.zh_text}} + + 来源:{{entry.source_type}} + 状态:{{entry.status}} + + + + + 词条详情 + 查看单词或句子的完整信息 + + + + 加载中... + + + + {{error}} + + + + + 类型 + {{entry.entry_type === 'sentence' ? '句子' : '单词'}} + + + 内容 + {{entry.en_text}} + + + {{entry.entry_type === 'sentence' ? '翻译' : '释义'}} + {{entry.zh_text}} + + + {{entry.entry_type === 'word' ? '例句' : '补充说明'}} + {{entry.example_text}} + + + 来源 + {{entry.source_type}} + + + + diff --git a/manage/entry-detail/entry-detail.wxss b/manage/entry-detail/entry-detail.wxss new file mode 100644 index 0000000..96222db --- /dev/null +++ b/manage/entry-detail/entry-detail.wxss @@ -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; +} diff --git a/manage/entry-edit/entry-edit.js b/manage/entry-edit/entry-edit.js new file mode 100644 index 0000000..6b9f5d5 --- /dev/null +++ b/manage/entry-edit/entry-edit.js @@ -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 }), + }) + }, +}) diff --git a/manage/entry-edit/entry-edit.json b/manage/entry-edit/entry-edit.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/manage/entry-edit/entry-edit.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/manage/entry-edit/entry-edit.wxml b/manage/entry-edit/entry-edit.wxml new file mode 100644 index 0000000..bee4636 --- /dev/null +++ b/manage/entry-edit/entry-edit.wxml @@ -0,0 +1,65 @@ + + + + + + + {{isEditMode ? '编辑模式' : '新增模式'}} + + + + + + {{isEditMode ? '更新词条' : '创建词条'}} + {{isEditMode ? '编辑内容' : '添加新内容'}} + {{entryType === 'word' ? '记录单词、释义和例句' : '记录句子、翻译和语境说明'}} + + {{entryType === 'sentence' ? '句' : '词'}} + + + + + 单词 + 句子 + + + + + 编辑提示 + 切换类型会改变字段含义,但不会自动清空你输入的内容。 + + + + + {{entryType === 'word' ? '单词' : '句子'}} + + + + + + {{entryType === 'word' ? '中文释义' : '中文翻译'}} + + + + + + 例句 (可选) +