feat:开发分类页内容管理
This commit is contained in:
@@ -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
|
||||||
|
: '生成分类页数据失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "page-content-build",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"wx-server-sdk": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "worksheets-query",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"wx-server-sdk": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 : '更新状态失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "worksheets-update-status",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"wx-server-sdk": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"guide/guide",
|
"guide/guide",
|
||||||
"debug/debug",
|
"debug/debug",
|
||||||
"categoryManage/categoryManage",
|
"categoryManage/categoryManage",
|
||||||
|
"categoryContentManage/categoryContentManage",
|
||||||
"index/index",
|
"index/index",
|
||||||
"mathIndex/mathIndex",
|
"mathIndex/mathIndex",
|
||||||
"focusIndex/focusIndex",
|
"focusIndex/focusIndex",
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "分类页内容管理",
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarBackgroundColor": "#F8F0E0",
|
||||||
|
"backgroundColor": "#F8F0E0",
|
||||||
|
"enablePullDownRefresh": true,
|
||||||
|
"usingComponents": {
|
||||||
|
"toy-button": "/toy/button-v2/button"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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<T> = {
|
||||||
|
success?: boolean;
|
||||||
|
message?: string;
|
||||||
|
data?: T;
|
||||||
|
stats?: Stats;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DIFFICULTY_LABELS: Record<number, string> = {
|
||||||
|
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<string, unknown>): 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<T>(
|
||||||
|
name: string,
|
||||||
|
data?: Record<string, unknown>,
|
||||||
|
): Promise<CloudFunctionResult<T>> {
|
||||||
|
const response = (await wx.cloud.callFunction({
|
||||||
|
name,
|
||||||
|
data: data || {},
|
||||||
|
})) as { result?: CloudFunctionResult<T> };
|
||||||
|
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<WorksheetItem[]>(
|
||||||
|
'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<string, unknown>),
|
||||||
|
);
|
||||||
|
|
||||||
|
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<boolean>((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<boolean>((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<string, number>;
|
||||||
|
}>('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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<view class="ccm-page">
|
||||||
|
<!-- Category Selector -->
|
||||||
|
<scroll-view class="ccm-page__categories" scroll-x enable-flex>
|
||||||
|
<view
|
||||||
|
wx:for="{{categories}}"
|
||||||
|
wx:key="id"
|
||||||
|
class="ccm-pill {{activeCategoryId === item.id ? 'ccm-pill--active' : ''}}"
|
||||||
|
data-id="{{item.id}}"
|
||||||
|
bindtap="onCategoryTap">
|
||||||
|
<text class="ccm-pill__icon">{{item.icon}}</text>
|
||||||
|
<text class="ccm-pill__text">{{item.name}}</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<view class="ccm-page__stats">
|
||||||
|
<view class="ccm-stat">
|
||||||
|
<text class="ccm-stat__num">{{stats.total}}</text>
|
||||||
|
<text class="ccm-stat__label">总数</text>
|
||||||
|
</view>
|
||||||
|
<view class="ccm-stat">
|
||||||
|
<text class="ccm-stat__num">{{stats.draft}}</text>
|
||||||
|
<text class="ccm-stat__label">草稿</text>
|
||||||
|
</view>
|
||||||
|
<view class="ccm-stat">
|
||||||
|
<text class="ccm-stat__num">{{stats.active}}</text>
|
||||||
|
<text class="ccm-stat__label">线上</text>
|
||||||
|
</view>
|
||||||
|
<view class="ccm-stat">
|
||||||
|
<text class="ccm-stat__num">{{stats.hidden}}</text>
|
||||||
|
<text class="ccm-stat__label">已隐藏</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<view wx:if="{{loading}}" class="ccm-page__loading">
|
||||||
|
正在加载 worksheet 数据...
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Empty -->
|
||||||
|
<view wx:elif="{{!worksheets.length}}" class="ccm-page__empty">
|
||||||
|
当前分类下暂无 worksheet
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Worksheet List -->
|
||||||
|
<view wx:else class="ccm-page__list">
|
||||||
|
<view
|
||||||
|
wx:for="{{worksheets}}"
|
||||||
|
wx:key="_id"
|
||||||
|
class="ws-card ws-card--{{item.status}}">
|
||||||
|
<view class="ws-card__header">
|
||||||
|
<image
|
||||||
|
wx:if="{{item.previewImg}}"
|
||||||
|
class="ws-card__img"
|
||||||
|
src="{{item.previewImg}}"
|
||||||
|
mode="aspectFill" />
|
||||||
|
<view wx:else class="ws-card__img ws-card__img--placeholder">
|
||||||
|
<text>无图</text>
|
||||||
|
</view>
|
||||||
|
<view class="ws-card__info">
|
||||||
|
<text class="ws-card__title">{{item.title}}</text>
|
||||||
|
<text class="ws-card__subtitle">{{item.subtitle}}</text>
|
||||||
|
<view class="ws-card__meta">
|
||||||
|
<text class="ws-card__tag">{{item.ageBand}}</text>
|
||||||
|
<text class="ws-card__tag">{{item.difficultyLabel}}</text>
|
||||||
|
</view>
|
||||||
|
<text class="ws-card__id">{{item._id}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="ws-card__footer">
|
||||||
|
<text class="ws-card__badge ws-card__badge--{{item.status}}">
|
||||||
|
{{item.statusText}}
|
||||||
|
</text>
|
||||||
|
<text class="ws-card__sort">排序 {{item.sortOrder}}</text>
|
||||||
|
<toy-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
data-id="{{item._id}}"
|
||||||
|
data-target="{{item.actionTarget}}"
|
||||||
|
disabled="{{updatingId === item._id}}"
|
||||||
|
bindtap="onStatusTap">
|
||||||
|
{{updatingId === item._id ? '处理中...' : item.actionText}}
|
||||||
|
</toy-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Build Button -->
|
||||||
|
<view class="ccm-page__build">
|
||||||
|
<toy-button
|
||||||
|
type="primary"
|
||||||
|
loading="{{building}}"
|
||||||
|
disabled="{{building}}"
|
||||||
|
bindtap="onBuildTap">
|
||||||
|
{{building ? '生成中...' : '更新分类页数据'}}
|
||||||
|
</toy-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
@@ -14,6 +14,13 @@ const DEBUG_ENTRIES: DebugEntry[] = [
|
|||||||
icon: '🗂️',
|
icon: '🗂️',
|
||||||
path: '/supportPages/categoryManage/categoryManage',
|
path: '/supportPages/categoryManage/categoryManage',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'category-content',
|
||||||
|
title: '分类页内容管理',
|
||||||
|
subtitle: '管理各分类下 worksheet 状态,生成分类页数据。',
|
||||||
|
icon: '📋',
|
||||||
|
path: '/supportPages/categoryContentManage/categoryContentManage',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
|
|||||||
Reference in New Issue
Block a user