From fa39938e5ae919eb43e0f8d012b158baba6a0d7e Mon Sep 17 00:00:00 2001 From: R524809 Date: Wed, 29 Apr 2026 18:17:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=BC=80=E5=8F=91=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E9=A1=B5=E5=86=85=E5=AE=B9=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cloudfunctions/pageContentBuild/index.js | 131 ++++++++++ cloudfunctions/pageContentBuild/package.json | 8 + cloudfunctions/worksheetsQuery/index.js | 62 +++++ cloudfunctions/worksheetsQuery/package.json | 8 + .../worksheetsUpdateStatus/index.js | 43 ++++ .../worksheetsUpdateStatus/package.json | 8 + miniprogram/app.json | 1 + .../categoryContentManage.json | 10 + .../categoryContentManage.less | 237 +++++++++++++++++ .../categoryContentManage.ts | 240 ++++++++++++++++++ .../categoryContentManage.wxml | 98 +++++++ miniprogram/supportPages/debug/debug.ts | 7 + 12 files changed, 853 insertions(+) create mode 100644 cloudfunctions/pageContentBuild/index.js create mode 100644 cloudfunctions/pageContentBuild/package.json create mode 100644 cloudfunctions/worksheetsQuery/index.js create mode 100644 cloudfunctions/worksheetsQuery/package.json create mode 100644 cloudfunctions/worksheetsUpdateStatus/index.js create mode 100644 cloudfunctions/worksheetsUpdateStatus/package.json create mode 100644 miniprogram/supportPages/categoryContentManage/categoryContentManage.json create mode 100644 miniprogram/supportPages/categoryContentManage/categoryContentManage.less create mode 100644 miniprogram/supportPages/categoryContentManage/categoryContentManage.ts create mode 100644 miniprogram/supportPages/categoryContentManage/categoryContentManage.wxml diff --git a/cloudfunctions/pageContentBuild/index.js b/cloudfunctions/pageContentBuild/index.js new file mode 100644 index 0000000..b6fadde --- /dev/null +++ b/cloudfunctions/pageContentBuild/index.js @@ -0,0 +1,131 @@ +const cloud = require('wx-server-sdk'); + +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); + +const CONFIG_PATH = 'content/page-config.json'; + +const DIFFICULTY_LABELS = { + 1: '入门', + 2: '基础', + 3: '进阶', + 4: '挑战', +}; + +function ageBand(min, max) { + return `${min}-${max}岁`; +} + +function toDisplayItem(ws) { + return { + id: ws._id, + title: ws.title || '', + subtitle: ws.subtitle || '', + previewImg: ws.previewImg || '', + ageBand: ageBand(ws.ageMin || 0, ws.ageMax || 0), + ageMin: ws.ageMin || 0, + ageMax: ws.ageMax || 0, + difficulty: ws.difficulty || 2, + difficultyLabel: DIFFICULTY_LABELS[ws.difficulty] || '基础', + path: ws.path || '', + available: true, + likes: ws.likes || 0, + downloads: ws.downloads || 0, + date: ws.updatedAt + ? new Date(ws.updatedAt).toISOString().slice(0, 10) + : new Date().toISOString().slice(0, 10), + }; +} + +async function readCurrentConfig() { + try { + const res = await cloud.downloadFile({ fileID: CONFIG_PATH }); + return JSON.parse(res.fileContent.toString('utf-8')); + } catch { + return { home: null, category: null, age: null, version: 0 }; + } +} + +async function buildCategoryData(db) { + // Get all categories + const { data: categories } = await db + .collection('categories') + .orderBy('sortOrder', 'asc') + .get(); + + // Get all active worksheets + const { data: worksheets } = await db + .collection('worksheets') + .where({ status: 'active' }) + .orderBy('sortOrder', 'asc') + .orderBy('updatedAt', 'desc') + .limit(100) + .get(); + + // Group by category + const grouped = {}; + for (const ws of worksheets) { + const cat = ws.category || 'unknown'; + if (!grouped[cat]) grouped[cat] = []; + grouped[cat].push(toDisplayItem(ws)); + } + + const categoryData = { + searchPlaceholder: '搜索练习纸...', + categories: categories.map((cat) => ({ + id: cat._id, + name: cat.name || '', + icon: cat.icon || '', + items: grouped[cat._id] || [], + })), + }; + + const stats = {}; + for (const cat of categoryData.categories) { + stats[cat.id] = cat.items.length; + } + + return { categoryData, stats }; +} + +exports.main = async (event) => { + try { + const db = cloud.database(); + const page = String(event.page || '').trim(); + + if (page !== 'category') { + throw new Error('当前仅支持 page=category'); + } + + const { categoryData, stats } = await buildCategoryData(db); + + // Read current config, merge, bump version + const config = await readCurrentConfig(); + config.category = categoryData; + config.version = (config.version || 0) + 1; + config.updatedAt = new Date().toISOString(); + + // Upload to cloud storage + const buffer = Buffer.from(JSON.stringify(config), 'utf-8'); + await cloud.uploadFile({ + cloudPath: CONFIG_PATH, + fileContent: buffer, + }); + + return { + success: true, + data: { + version: config.version, + updatedAt: config.updatedAt, + stats, + }, + }; + } catch (error) { + return { + success: false, + message: + error instanceof Error + ? error.message + : '生成分类页数据失败', + }; + } +}; diff --git a/cloudfunctions/pageContentBuild/package.json b/cloudfunctions/pageContentBuild/package.json new file mode 100644 index 0000000..fa6a552 --- /dev/null +++ b/cloudfunctions/pageContentBuild/package.json @@ -0,0 +1,8 @@ +{ + "name": "page-content-build", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "^3.0.4" + } +} diff --git a/cloudfunctions/worksheetsQuery/index.js b/cloudfunctions/worksheetsQuery/index.js new file mode 100644 index 0000000..489e1a5 --- /dev/null +++ b/cloudfunctions/worksheetsQuery/index.js @@ -0,0 +1,62 @@ +const cloud = require('wx-server-sdk'); + +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); + +const VALID_STATUS = new Set(['draft', 'active', 'hidden']); + +exports.main = async (event) => { + try { + const db = cloud.database(); + const _ = db.command; + const collection = db.collection('worksheets'); + + const category = String(event.category || '').trim(); + const status = String(event.status || '').trim(); + + // Build query condition + const where = {}; + if (category) where.category = category; + if (status && VALID_STATUS.has(status)) where.status = status; + + // Query worksheets (cloud DB limit is 100 per call) + const { data } = await collection + .where(where) + .orderBy('sortOrder', 'asc') + .orderBy('updatedAt', 'desc') + .limit(100) + .get(); + + // Compute stats for the category (all statuses) + let allData = data; + if (status) { + // If filtered by status, need a separate query for stats + const statsWhere = {}; + if (category) statsWhere.category = category; + const { data: allItems } = await collection + .where(statsWhere) + .limit(100) + .get(); + allData = allItems; + } + + const stats = { total: 0, draft: 0, active: 0, hidden: 0 }; + for (const item of allData) { + stats.total++; + if (item.status === 'draft') stats.draft++; + else if (item.status === 'active') stats.active++; + else if (item.status === 'hidden') stats.hidden++; + } + + return { + success: true, + data, + stats, + }; + } catch (error) { + return { + success: false, + message: + error instanceof Error ? error.message : '查询 worksheet 失败', + }; + } +}; diff --git a/cloudfunctions/worksheetsQuery/package.json b/cloudfunctions/worksheetsQuery/package.json new file mode 100644 index 0000000..b6be148 --- /dev/null +++ b/cloudfunctions/worksheetsQuery/package.json @@ -0,0 +1,8 @@ +{ + "name": "worksheets-query", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "^3.0.4" + } +} diff --git a/cloudfunctions/worksheetsUpdateStatus/index.js b/cloudfunctions/worksheetsUpdateStatus/index.js new file mode 100644 index 0000000..b17a50b --- /dev/null +++ b/cloudfunctions/worksheetsUpdateStatus/index.js @@ -0,0 +1,43 @@ +const cloud = require('wx-server-sdk'); + +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); + +const VALID_STATUS = new Set(['draft', 'active', 'hidden']); + +exports.main = async (event) => { + try { + const db = cloud.database(); + const collection = db.collection('worksheets'); + + const id = String(event.id || '').trim(); + const status = String(event.status || '').trim(); + + if (!id) throw new Error('worksheet id 不能为空'); + if (!VALID_STATUS.has(status)) throw new Error('状态不合法'); + + // Check worksheet exists + try { + await collection.doc(id).get(); + } catch { + throw new Error('worksheet 不存在'); + } + + await collection.doc(id).update({ + data: { + status, + updatedAt: db.serverDate(), + }, + }); + + return { + success: true, + data: { _id: id, status }, + }; + } catch (error) { + return { + success: false, + message: + error instanceof Error ? error.message : '更新状态失败', + }; + } +}; diff --git a/cloudfunctions/worksheetsUpdateStatus/package.json b/cloudfunctions/worksheetsUpdateStatus/package.json new file mode 100644 index 0000000..971177b --- /dev/null +++ b/cloudfunctions/worksheetsUpdateStatus/package.json @@ -0,0 +1,8 @@ +{ + "name": "worksheets-update-status", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "^3.0.4" + } +} diff --git a/miniprogram/app.json b/miniprogram/app.json index 9f40e28..f98250b 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -14,6 +14,7 @@ "guide/guide", "debug/debug", "categoryManage/categoryManage", + "categoryContentManage/categoryContentManage", "index/index", "mathIndex/mathIndex", "focusIndex/focusIndex", diff --git a/miniprogram/supportPages/categoryContentManage/categoryContentManage.json b/miniprogram/supportPages/categoryContentManage/categoryContentManage.json new file mode 100644 index 0000000..993863c --- /dev/null +++ b/miniprogram/supportPages/categoryContentManage/categoryContentManage.json @@ -0,0 +1,10 @@ +{ + "navigationBarTitleText": "分类页内容管理", + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#F8F0E0", + "backgroundColor": "#F8F0E0", + "enablePullDownRefresh": true, + "usingComponents": { + "toy-button": "/toy/button-v2/button" + } +} diff --git a/miniprogram/supportPages/categoryContentManage/categoryContentManage.less b/miniprogram/supportPages/categoryContentManage/categoryContentManage.less new file mode 100644 index 0000000..588fd3f --- /dev/null +++ b/miniprogram/supportPages/categoryContentManage/categoryContentManage.less @@ -0,0 +1,237 @@ +@import '../../style/theme.less'; + +page { + min-height: 100%; + background: @bg-header; +} + +.ccm-page { + min-height: 100vh; + padding: 24rpx; + padding-bottom: 160rpx; + background: @bg-header; + box-sizing: border-box; +} + +// ── Category Selector ── +.ccm-page__categories { + white-space: nowrap; + padding-bottom: 8rpx; +} + +.ccm-pill { + display: inline-flex; + align-items: center; + padding: 16rpx 28rpx; + margin-right: 16rpx; + background: @bg-white; + border-radius: 999rpx; + border: 2rpx solid transparent; + box-shadow: @shadow; +} + +.ccm-pill--active { + background: @brand-light; + border-color: @brand; +} + +.ccm-pill__icon { + font-size: 28rpx; + margin-right: 8rpx; +} + +.ccm-pill__text { + font-size: 26rpx; + font-weight: 600; + color: @text-title; + white-space: nowrap; +} + +// ── Stats ── +.ccm-page__stats { + display: flex; + margin-top: 24rpx; +} + +.ccm-stat { + flex: 1; + padding: 24rpx 16rpx; + background: @bg-white; + border-radius: @radius-xl; + text-align: center; + box-shadow: @shadow; +} + +.ccm-stat + .ccm-stat { + margin-left: 16rpx; +} + +.ccm-stat__num { + display: block; + font-size: 36rpx; + font-weight: 800; + color: @text-title; +} + +.ccm-stat__label { + display: block; + margin-top: 8rpx; + font-size: 22rpx; + color: @text-secondary; +} + +// ── Loading / Empty ── +.ccm-page__loading, +.ccm-page__empty { + margin-top: 24rpx; + padding: 24rpx 28rpx; + background: @bg-white; + border-radius: @radius; + font-size: 24rpx; + line-height: 1.6; + color: @text-secondary; + box-shadow: @shadow; +} + +// ── Worksheet List ── +.ccm-page__list { + margin-top: 24rpx; +} + +.ws-card { + padding: 28rpx; + margin-bottom: 20rpx; + background: @bg-white; + border-radius: @radius-xl; + box-shadow: @shadow; + border: 2rpx solid transparent; +} + +.ws-card--active { + border-color: fade(#93d333, 30%); +} + +.ws-card--draft { + border-color: fade(#ffb703, 40%); +} + +.ws-card--hidden { + border-color: fade(#8a8478, 25%); +} + +.ws-card__header { + display: flex; + align-items: flex-start; +} + +.ws-card__img { + width: 120rpx; + height: 120rpx; + border-radius: @radius; + flex-shrink: 0; + background: @bg-gray; +} + +.ws-card__img--placeholder { + display: flex; + align-items: center; + justify-content: center; + font-size: 22rpx; + color: @text-gray; +} + +.ws-card__info { + flex: 1; + min-width: 0; + margin-left: 20rpx; +} + +.ws-card__title { + display: block; + font-size: 28rpx; + font-weight: 700; + color: @text-title; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ws-card__subtitle { + display: block; + margin-top: 6rpx; + font-size: 22rpx; + color: @text-secondary; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ws-card__meta { + display: flex; + margin-top: 10rpx; + gap: 12rpx; +} + +.ws-card__tag { + padding: 4rpx 14rpx; + border-radius: 999rpx; + font-size: 20rpx; + color: @text-secondary; + background: @bg-gray; +} + +.ws-card__id { + display: block; + margin-top: 8rpx; + font-size: 20rpx; + color: @text-gray; +} + +.ws-card__footer { + display: flex; + align-items: center; + margin-top: 20rpx; + padding-top: 20rpx; + border-top: @border; +} + +.ws-card__badge { + padding: 6rpx 16rpx; + border-radius: 999rpx; + font-size: 22rpx; + font-weight: 700; +} + +.ws-card__badge--active { + background: fade(#93d333, 18%); + color: #4a7a12; +} + +.ws-card__badge--draft { + background: fade(#ffb703, 18%); + color: #8a6d00; +} + +.ws-card__badge--hidden { + background: fade(#8a8478, 15%); + color: #605b50; +} + +.ws-card__sort { + flex: 1; + margin-left: 16rpx; + font-size: 22rpx; + color: @text-gray; +} + +// ── Build Button ── +.ccm-page__build { + position: fixed; + left: 0; + right: 0; + bottom: 0; + padding: 24rpx 40rpx; + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); + background: @bg-header; + box-shadow: 0 -4rpx 16rpx rgba(50, 46, 37, 0.06); +} diff --git a/miniprogram/supportPages/categoryContentManage/categoryContentManage.ts b/miniprogram/supportPages/categoryContentManage/categoryContentManage.ts new file mode 100644 index 0000000..8e2b419 --- /dev/null +++ b/miniprogram/supportPages/categoryContentManage/categoryContentManage.ts @@ -0,0 +1,240 @@ +import { CATEGORY_LIST } from '../../core/data/categories'; + +type WorksheetItem = { + _id: string; + title: string; + subtitle: string; + category: string; + previewImg: string; + ageMin: number; + ageMax: number; + difficulty: 1 | 2 | 3 | 4; + tags: string[]; + status: 'draft' | 'active' | 'hidden'; + sortOrder: number; + updatedAt: string; + // display helpers + ageBand: string; + difficultyLabel: string; + statusText: string; + actionText: string; + actionTarget: string; +}; + +type Stats = { + total: number; + draft: number; + active: number; + hidden: number; +}; + +type CloudFunctionResult = { + success?: boolean; + message?: string; + data?: T; + stats?: Stats; +}; + +const DIFFICULTY_LABELS: Record = { + 1: '入门', + 2: '基础', + 3: '进阶', + 4: '挑战', +}; + +const STATUS_CONFIG: Record< + string, + { text: string; action: string; target: string } +> = { + draft: { text: '草稿', action: '激活', target: 'active' }, + active: { text: '线上', action: '下架', target: 'hidden' }, + hidden: { text: '已隐藏', action: '恢复', target: 'draft' }, +}; + +function formatWorksheet(raw: Record): WorksheetItem { + const status = String(raw.status || 'draft'); + const cfg = STATUS_CONFIG[status] || STATUS_CONFIG.draft; + const ageMin = Number(raw.ageMin) || 0; + const ageMax = Number(raw.ageMax) || 0; + const difficulty = (Number(raw.difficulty) || 2) as 1 | 2 | 3 | 4; + + return { + _id: String(raw._id || ''), + title: String(raw.title || ''), + subtitle: String(raw.subtitle || ''), + category: String(raw.category || ''), + previewImg: String(raw.previewImg || ''), + ageMin, + ageMax, + difficulty, + tags: Array.isArray(raw.tags) ? raw.tags.map(String) : [], + status: status as WorksheetItem['status'], + sortOrder: Number(raw.sortOrder) || 0, + updatedAt: raw.updatedAt ? String(raw.updatedAt) : '', + ageBand: `${ageMin}-${ageMax}岁`, + difficultyLabel: DIFFICULTY_LABELS[difficulty] || '基础', + statusText: cfg.text, + actionText: cfg.action, + actionTarget: cfg.target, + }; +} + +async function callCloudFunction( + name: string, + data?: Record, +): Promise> { + const response = (await wx.cloud.callFunction({ + name, + data: data || {}, + })) as { result?: CloudFunctionResult }; + return response.result || {}; +} + +Page({ + data: { + categories: CATEGORY_LIST, + activeCategoryId: CATEGORY_LIST[0]?.id || 'math', + worksheets: [] as WorksheetItem[], + stats: { total: 0, draft: 0, active: 0, hidden: 0 } as Stats, + loading: false, + updatingId: '', + building: false, + }, + + onLoad() { + void this.loadWorksheets(); + }, + + async onPullDownRefresh() { + await this.loadWorksheets(); + wx.stopPullDownRefresh(); + }, + + async loadWorksheets() { + this.setData({ loading: true }); + + try { + const result = await callCloudFunction( + 'worksheetsQuery', + { category: this.data.activeCategoryId }, + ); + + if (!result.success) { + throw new Error(result.message || '查询失败'); + } + + const worksheets = (result.data || []).map((raw) => + formatWorksheet(raw as unknown as Record), + ); + + this.setData({ + loading: false, + worksheets, + stats: result.stats || { + total: 0, + draft: 0, + active: 0, + hidden: 0, + }, + }); + } catch (error) { + this.setData({ loading: false }); + wx.showToast({ + title: error instanceof Error ? error.message : '查询失败', + icon: 'none', + }); + } + }, + + onCategoryTap(e: WechatMiniprogram.TouchEvent) { + const id = e.currentTarget.dataset.id as string; + if (!id || id === this.data.activeCategoryId) return; + this.setData({ activeCategoryId: id }); + void this.loadWorksheets(); + }, + + async onStatusTap(e: WechatMiniprogram.TouchEvent) { + const id = e.currentTarget.dataset.id as string; + const target = e.currentTarget.dataset.target as string; + if (!id || !target || this.data.updatingId) return; + + const ws = this.data.worksheets.find((w) => w._id === id); + if (!ws) return; + + const confirmed = await new Promise((resolve) => { + wx.showModal({ + title: `${ws.actionText}`, + content: `确认将「${ws.title}」${ws.actionText}吗?`, + success: (res) => resolve(!!res.confirm), + fail: () => resolve(false), + }); + }); + if (!confirmed) return; + + this.setData({ updatingId: id }); + + try { + const result = await callCloudFunction('worksheetsUpdateStatus', { + id, + status: target, + }); + + if (!result.success) { + throw new Error(result.message || '操作失败'); + } + + wx.showToast({ title: `${ws.actionText}成功`, icon: 'success' }); + await this.loadWorksheets(); + } catch (error) { + wx.showToast({ + title: + error instanceof Error ? error.message : '操作失败', + icon: 'none', + }); + } finally { + this.setData({ updatingId: '' }); + } + }, + + async onBuildTap() { + if (this.data.building) return; + + const confirmed = await new Promise((resolve) => { + wx.showModal({ + title: '更新分类页数据', + content: + '将根据当前线上 worksheet 重新生成分类页数据,确认继续?', + success: (res) => resolve(!!res.confirm), + fail: () => resolve(false), + }); + }); + if (!confirmed) return; + + this.setData({ building: true }); + + try { + const result = await callCloudFunction<{ + version: number; + stats: Record; + }>('pageContentBuild', { page: 'category' }); + + if (!result.success) { + throw new Error(result.message || '生成失败'); + } + + const version = result.data?.version || 0; + wx.showToast({ + title: `生成成功 v${version}`, + icon: 'success', + }); + } catch (error) { + wx.showToast({ + title: + error instanceof Error ? error.message : '生成失败', + icon: 'none', + }); + } finally { + this.setData({ building: false }); + } + }, +}); diff --git a/miniprogram/supportPages/categoryContentManage/categoryContentManage.wxml b/miniprogram/supportPages/categoryContentManage/categoryContentManage.wxml new file mode 100644 index 0000000..57c3d47 --- /dev/null +++ b/miniprogram/supportPages/categoryContentManage/categoryContentManage.wxml @@ -0,0 +1,98 @@ + + + + + {{item.icon}} + {{item.name}} + + + + + + + {{stats.total}} + 总数 + + + {{stats.draft}} + 草稿 + + + {{stats.active}} + 线上 + + + {{stats.hidden}} + 已隐藏 + + + + + + 正在加载 worksheet 数据... + + + + + 当前分类下暂无 worksheet + + + + + + + + + 无图 + + + {{item.title}} + {{item.subtitle}} + + {{item.ageBand}} + {{item.difficultyLabel}} + + {{item._id}} + + + + + {{item.statusText}} + + 排序 {{item.sortOrder}} + + {{updatingId === item._id ? '处理中...' : item.actionText}} + + + + + + + + + {{building ? '生成中...' : '更新分类页数据'}} + + + \ No newline at end of file diff --git a/miniprogram/supportPages/debug/debug.ts b/miniprogram/supportPages/debug/debug.ts index 88eec5c..258bfe3 100644 --- a/miniprogram/supportPages/debug/debug.ts +++ b/miniprogram/supportPages/debug/debug.ts @@ -14,6 +14,13 @@ const DEBUG_ENTRIES: DebugEntry[] = [ icon: '🗂️', path: '/supportPages/categoryManage/categoryManage', }, + { + id: 'category-content', + title: '分类页内容管理', + subtitle: '管理各分类下 worksheet 状态,生成分类页数据。', + icon: '📋', + path: '/supportPages/categoryContentManage/categoryContentManage', + }, ]; Page({