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",
|
||||
"debug/debug",
|
||||
"categoryManage/categoryManage",
|
||||
"categoryContentManage/categoryContentManage",
|
||||
"index/index",
|
||||
"mathIndex/mathIndex",
|
||||
"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: '🗂️',
|
||||
path: '/supportPages/categoryManage/categoryManage',
|
||||
},
|
||||
{
|
||||
id: 'category-content',
|
||||
title: '分类页内容管理',
|
||||
subtitle: '管理各分类下 worksheet 状态,生成分类页数据。',
|
||||
icon: '📋',
|
||||
path: '/supportPages/categoryContentManage/categoryContentManage',
|
||||
},
|
||||
];
|
||||
|
||||
Page({
|
||||
|
||||
Reference in New Issue
Block a user