feat: 完成分龄页开发
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"pages": [
|
||||
"guide/guide",
|
||||
"debug/debug",
|
||||
"worksheetSync/worksheetSync",
|
||||
"categoryManage/categoryManage",
|
||||
"categoryContentManage/categoryContentManage",
|
||||
"homeContentManage/homeContentManage",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,13 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "47501281",
|
||||
"name": "loading",
|
||||
"font_class": "loading",
|
||||
"unicode": "e62e",
|
||||
"unicode_decimal": 58926
|
||||
},
|
||||
{
|
||||
"icon_id": "47465822",
|
||||
"name": "task-o",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -25,6 +25,7 @@ export interface PageCanvasInstance {
|
||||
getShareOptions(): ShareOptions;
|
||||
selectComponent(selector: string): any;
|
||||
getPublishMeta?(): DebugPublishMeta;
|
||||
getWorksheetStatsId?(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,10 +226,15 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
|
||||
* 导出打印
|
||||
*/
|
||||
async exportToPrint(this: PageCanvasInstance) {
|
||||
const worksheetId =
|
||||
typeof this.getWorksheetStatsId === 'function'
|
||||
? this.getWorksheetStatsId()
|
||||
: this.data.functionId;
|
||||
await downloadPrint(this.canvas, {
|
||||
errorToast: '请先生成内容',
|
||||
trackerName: this.data.pageTitle,
|
||||
trackerMode: this.data.currentMode,
|
||||
worksheetId,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -278,11 +284,16 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
|
||||
async onShareSuccess(this: PageCanvasInstance) {
|
||||
this.setData({ showShareDialog: false });
|
||||
if (this.canvas) {
|
||||
const worksheetId =
|
||||
typeof this.getWorksheetStatsId === 'function'
|
||||
? this.getWorksheetStatsId()
|
||||
: this.data.functionId;
|
||||
// 分享成功后下载(不限制次数)
|
||||
await downloadPrint(this.canvas, {
|
||||
errorToast: '请先生成内容',
|
||||
trackerName: this.data.pageTitle,
|
||||
trackerMode: this.data.currentMode,
|
||||
worksheetId,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@import '../../style/theme.less';
|
||||
@import '../../style/tags.less';
|
||||
|
||||
.worksheet-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
@@ -63,3 +66,80 @@
|
||||
background: #fff8e1;
|
||||
color: #f57f17;
|
||||
}
|
||||
|
||||
/* ── variant: track(对齐首页 home-track-card) ── */
|
||||
.worksheet-card--track {
|
||||
box-shadow: 0 6rpx 20rpx rgba(50, 46, 37, 0.06);
|
||||
}
|
||||
|
||||
.worksheet-card--track.worksheet-card--active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.worksheet-card__top {
|
||||
height: 270rpx;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
border-bottom: 1rpx solid rgba(50, 46, 37, 0.08);
|
||||
background: #f5edd8;
|
||||
}
|
||||
|
||||
.worksheet-card__cover {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.worksheet-card__icon {
|
||||
font-size: 72rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.worksheet-card__tags-track {
|
||||
position: absolute;
|
||||
right: 12rpx;
|
||||
bottom: 12rpx;
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.worksheet-card__body {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 20rpx @page-padding-inner-x 22rpx;
|
||||
background: #ffffff;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.worksheet-card__title-track {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: @text-title;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.worksheet-card__sub {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin-top: 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: @text-gray;
|
||||
line-height: 1.45;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -3,10 +3,18 @@ Component({
|
||||
styleIsolation: 'apply-shared',
|
||||
},
|
||||
properties: {
|
||||
variant: {
|
||||
type: String,
|
||||
value: 'classic',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
previewBg: {
|
||||
type: String,
|
||||
value: '#f5f5f5',
|
||||
@@ -19,6 +27,10 @@ Component({
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
ageBand: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
difficulty: {
|
||||
type: String,
|
||||
value: '',
|
||||
@@ -27,10 +39,25 @@ Component({
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
previewImg: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTap() {
|
||||
this.triggerEvent('tap');
|
||||
this.triggerEvent('tap', {
|
||||
title: this.data.title,
|
||||
path: this.data.path,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,21 +1,62 @@
|
||||
<view
|
||||
class="worksheet-card"
|
||||
class="worksheet-card worksheet-card--{{variant}}"
|
||||
hover-class="worksheet-card--active"
|
||||
hover-stay-time="100"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<view class="worksheet-card__preview" style="background-color: {{previewBg}}">
|
||||
<image
|
||||
wx:if="{{img}}"
|
||||
class="worksheet-card__img"
|
||||
src="{{img}}"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text wx:else class="worksheet-card__preview-text">{{previewText}}</text>
|
||||
</view>
|
||||
<view class="worksheet-card__title">{{title}}</view>
|
||||
<view class="worksheet-card__tags">
|
||||
<view wx:if="{{ageRange}}" class="worksheet-card__tag worksheet-card__tag--age">{{ageRange}}</view>
|
||||
<view wx:if="{{difficulty}}" class="worksheet-card__tag worksheet-card__tag--diff">{{difficulty}}</view>
|
||||
</view>
|
||||
bindtap="onTap">
|
||||
<block wx:if="{{variant === 'track'}}">
|
||||
<view class="worksheet-card__top">
|
||||
<image
|
||||
wx:if="{{previewImg || img}}"
|
||||
class="worksheet-card__cover"
|
||||
src="{{previewImg || img}}"
|
||||
mode="widthFix" />
|
||||
<text wx:else class="worksheet-card__icon">{{icon}}</text>
|
||||
|
||||
<view
|
||||
wx:if="{{ageBand || ageRange || difficulty}}"
|
||||
class="worksheet-card__tags-track">
|
||||
<text wx:if="{{ageBand || ageRange}}" class="tag tag--age"
|
||||
>{{ageBand || ageRange}}</text
|
||||
>
|
||||
<text wx:if="{{difficulty}}" class="tag tag--diff"
|
||||
>{{difficulty}}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="worksheet-card__body">
|
||||
<text class="worksheet-card__title-track">{{title}}</text>
|
||||
<text wx:if="{{subtitle}}" class="worksheet-card__sub"
|
||||
>{{subtitle}}</text
|
||||
>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<view
|
||||
class="worksheet-card__preview"
|
||||
style="background-color: {{previewBg}}">
|
||||
<image
|
||||
wx:if="{{img}}"
|
||||
class="worksheet-card__img"
|
||||
src="{{img}}"
|
||||
mode="aspectFill" />
|
||||
<text wx:else class="worksheet-card__preview-text"
|
||||
>{{previewText}}</text
|
||||
>
|
||||
</view>
|
||||
<view class="worksheet-card__title">{{title}}</view>
|
||||
<view class="worksheet-card__tags">
|
||||
<view
|
||||
wx:if="{{ageRange}}"
|
||||
class="worksheet-card__tag worksheet-card__tag--age"
|
||||
>{{ageRange}}</view
|
||||
>
|
||||
<view
|
||||
wx:if="{{difficulty}}"
|
||||
class="worksheet-card__tag worksheet-card__tag--diff"
|
||||
>{{difficulty}}</view
|
||||
>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -2,22 +2,27 @@ import type { LetterTracingMode } from './generators/letter-tracing-generator';
|
||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||
|
||||
interface ModeDefinition {
|
||||
/** 本地 worksheet 元数据(与云库 `worksheets` 文档 `_id` 对齐) */
|
||||
interface LetterTracingWorksheetDefinition {
|
||||
id: LetterTracingMode;
|
||||
icon: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
ageMin: number;
|
||||
ageMax: number;
|
||||
difficulty: 1 | 2 | 3 | 4;
|
||||
tags: string[];
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
const MODES = [
|
||||
export const LETTER_TRACING_WORKSHEET_DEFINITIONS = [
|
||||
{
|
||||
id: 'letter-tracing-single',
|
||||
icon: 'start-a',
|
||||
title: '默认字帖',
|
||||
subtitle: '配图、例句与描红',
|
||||
ageMin: 4,
|
||||
ageMax: 7,
|
||||
difficulty: 1,
|
||||
tags: ['字母', '描红', '看图描红'],
|
||||
sortOrder: 42,
|
||||
@@ -27,6 +32,8 @@ const MODES = [
|
||||
icon: 'draw-o',
|
||||
title: '基础描红',
|
||||
subtitle: 'Uppercase / Lowercase 总览',
|
||||
ageMin: 5,
|
||||
ageMax: 7,
|
||||
difficulty: 1,
|
||||
tags: ['字母', '描红', '字母总览'],
|
||||
sortOrder: 44,
|
||||
@@ -36,6 +43,8 @@ const MODES = [
|
||||
icon: 'font-size',
|
||||
title: '大小写对照',
|
||||
subtitle: '半组字母左大写右小写',
|
||||
ageMin: 5,
|
||||
ageMax: 7,
|
||||
difficulty: 1,
|
||||
tags: ['字母', '描红', '大小写练习'],
|
||||
sortOrder: 45,
|
||||
@@ -45,6 +54,8 @@ const MODES = [
|
||||
icon: 'two-columns',
|
||||
title: '两列描红',
|
||||
subtitle: '左 A–M、右 N–Z 配对描红',
|
||||
ageMin: 5,
|
||||
ageMax: 7,
|
||||
difficulty: 1,
|
||||
tags: ['字母', '描红', '两列练习'],
|
||||
sortOrder: 43,
|
||||
@@ -54,6 +65,8 @@ const MODES = [
|
||||
icon: 'square-half',
|
||||
title: '13字母半表',
|
||||
subtitle: '每行一个字母,13 字母半表',
|
||||
ageMin: 5,
|
||||
ageMax: 8,
|
||||
difficulty: 2,
|
||||
tags: ['字母', '描红', '半表练习'],
|
||||
sortOrder: 47,
|
||||
@@ -63,6 +76,8 @@ const MODES = [
|
||||
icon: 'ABC-list',
|
||||
title: '三字母精练',
|
||||
subtitle: '每页聚焦 3 个字母深度书写',
|
||||
ageMin: 5,
|
||||
ageMax: 7,
|
||||
difficulty: 2,
|
||||
tags: ['字母', '描红', '三字母精练'],
|
||||
sortOrder: 46,
|
||||
@@ -72,36 +87,37 @@ const MODES = [
|
||||
icon: 'task-o',
|
||||
title: '每日打卡',
|
||||
subtitle: '四宫格每日字母打卡练习',
|
||||
ageMin: 6,
|
||||
ageMax: 8,
|
||||
difficulty: 2,
|
||||
tags: ['字母', '描红', '每日打卡'],
|
||||
sortOrder: 48,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<ModeDefinition>;
|
||||
] as const satisfies ReadonlyArray<LetterTracingWorksheetDefinition>;
|
||||
|
||||
type Mode = (typeof MODES)[number];
|
||||
type LetterTracingWorksheetRow = (typeof LETTER_TRACING_WORKSHEET_DEFINITIONS)[number];
|
||||
|
||||
const MODE_BY_ID = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
|
||||
string,
|
||||
Mode
|
||||
>;
|
||||
const LETTER_TRACING_WORKSHEET_BY_ID = Object.fromEntries(
|
||||
LETTER_TRACING_WORKSHEET_DEFINITIONS.map((m) => [m.id, m]),
|
||||
) as Record<string, LetterTracingWorksheetRow>;
|
||||
|
||||
/** 页面渲染用:模式选择器列表 */
|
||||
export const LETTER_TRACING_MODE_OPTIONS = MODES;
|
||||
/** 页面渲染用:模式选择器列表(与 `LETTER_TRACING_WORKSHEET_DEFINITIONS` 同义) */
|
||||
export const LETTER_TRACING_MODE_OPTIONS = LETTER_TRACING_WORKSHEET_DEFINITIONS;
|
||||
|
||||
/** 页面 pageInfoLookup 用 */
|
||||
export function getModeInfo(id: string) {
|
||||
const m = MODE_BY_ID[id];
|
||||
const m = LETTER_TRACING_WORKSHEET_BY_ID[id];
|
||||
return m ? { title: m.title, desc: m.subtitle } : undefined;
|
||||
}
|
||||
|
||||
/** 判断 id 是否有效 */
|
||||
export function isValidMode(id: string): boolean {
|
||||
return id in MODE_BY_ID;
|
||||
return id in LETTER_TRACING_WORKSHEET_BY_ID;
|
||||
}
|
||||
|
||||
/** 发布用:从当前 mode 生成 DebugPublishMeta */
|
||||
export function getPublishMetaByMode(id: string): DebugPublishMeta | null {
|
||||
const m = MODE_BY_ID[id];
|
||||
const m = LETTER_TRACING_WORKSHEET_BY_ID[id];
|
||||
if (!m) return null;
|
||||
return {
|
||||
id: m.id,
|
||||
@@ -110,9 +126,9 @@ export function getPublishMetaByMode(id: string): DebugPublishMeta | null {
|
||||
category: 'english',
|
||||
subcategory: 'letter-tracing',
|
||||
path: `/englishPages/letterTracing/letterTracing?id=${m.id}`,
|
||||
ageMin: 4,
|
||||
ageMax: 7,
|
||||
grade: inferGradeFromAge(4, 7),
|
||||
ageMin: m.ageMin,
|
||||
ageMax: m.ageMax,
|
||||
grade: inferGradeFromAge(m.ageMin, m.ageMax),
|
||||
difficulty: m.difficulty,
|
||||
previewImg: '',
|
||||
tags: [...m.tags],
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '../shared/data/fontProfiles';
|
||||
import { loadLetterFont } from '../shared/draw/drawTools';
|
||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||
import { incrementWorksheetLikes } from '../../utils/worksheetStats';
|
||||
|
||||
/** 三字母精练分组:26 字母每 3 个一组 */
|
||||
const TRIPLE_GROUPS: { title: string; letters: string[] }[] = [];
|
||||
@@ -271,9 +272,12 @@ createPage(
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
onPreviewFavorite() {
|
||||
async onPreviewFavorite() {
|
||||
const next = !this.data.isPreviewFavorite;
|
||||
this.setData({ isPreviewFavorite: next });
|
||||
if (next) {
|
||||
incrementWorksheetLikes(this.data.worksheetId);
|
||||
}
|
||||
wx.showToast({
|
||||
title: next ? '收藏成功' : '已取消收藏',
|
||||
icon: 'none',
|
||||
@@ -354,6 +358,7 @@ createPage(
|
||||
worksheetId,
|
||||
functionId: worksheetId,
|
||||
traceMode,
|
||||
isPreviewFavorite: false,
|
||||
selectedLetter,
|
||||
selectedLetterPair: `${selectedLetter}${selectedLetter.toLowerCase()}`,
|
||||
letterCaseLower,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||
|
||||
interface FocusDrawDefinition {
|
||||
/** 本地 worksheet 元数据(与云库 `worksheets` 文档 `_id` 对齐) */
|
||||
interface FocusWorksheetDefinition {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
@@ -12,7 +13,7 @@ interface FocusDrawDefinition {
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
const FOCUS_DRAW_DEFINITIONS = [
|
||||
export const FOCUS_WORKSHEET_DEFINITIONS = [
|
||||
{
|
||||
id: 'color-shape-match',
|
||||
title: '根据颜色画图形',
|
||||
@@ -68,7 +69,7 @@ const FOCUS_DRAW_DEFINITIONS = [
|
||||
title: '线条识别',
|
||||
subtitle: '认识不同线条,画对应线条',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
ageMax: 6,
|
||||
difficulty: 1,
|
||||
tags: ['专注力', '线条', '识别', '控笔'],
|
||||
sortOrder: 206,
|
||||
@@ -97,7 +98,7 @@ const FOCUS_DRAW_DEFINITIONS = [
|
||||
id: 'dot-connect',
|
||||
title: '数字点连线',
|
||||
subtitle: '按数字顺序连点成图',
|
||||
ageMin: 3,
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
difficulty: 1,
|
||||
tags: ['专注力', '数字', '连线'],
|
||||
@@ -127,19 +128,19 @@ const FOCUS_DRAW_DEFINITIONS = [
|
||||
id: 'grid-drawing-7x7',
|
||||
title: '格子仿画 7×7',
|
||||
subtitle: '大师挑战,锻炼耐心',
|
||||
ageMin: 5,
|
||||
ageMin: 6,
|
||||
ageMax: 8,
|
||||
difficulty: 3,
|
||||
tags: ['专注力', '格子仿画', '耐心'],
|
||||
sortOrder: 212,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<FocusDrawDefinition>;
|
||||
] as const satisfies ReadonlyArray<FocusWorksheetDefinition>;
|
||||
|
||||
type FocusDrawDefinitionItem = (typeof FOCUS_DRAW_DEFINITIONS)[number];
|
||||
type FocusWorksheetDefinitionItem = (typeof FOCUS_WORKSHEET_DEFINITIONS)[number];
|
||||
|
||||
const FOCUS_DRAW_BY_ID = Object.fromEntries(
|
||||
FOCUS_DRAW_DEFINITIONS.map((item) => [item.id, item]),
|
||||
) as Record<string, FocusDrawDefinitionItem>;
|
||||
const FOCUS_WORKSHEET_BY_ID = Object.fromEntries(
|
||||
FOCUS_WORKSHEET_DEFINITIONS.map((item) => [item.id, item]),
|
||||
) as Record<string, FocusWorksheetDefinitionItem>;
|
||||
|
||||
function resolveFocusPublishId(
|
||||
routeId: string,
|
||||
@@ -150,7 +151,7 @@ function resolveFocusPublishId(
|
||||
return `grid-drawing-${mode || '3x3'}`;
|
||||
}
|
||||
|
||||
if (routeId in FOCUS_DRAW_BY_ID) {
|
||||
if (routeId in FOCUS_WORKSHEET_BY_ID) {
|
||||
return routeId;
|
||||
}
|
||||
|
||||
@@ -163,7 +164,7 @@ export function getPublishMetaByFocusState(
|
||||
mode?: string,
|
||||
): DebugPublishMeta | null {
|
||||
const id = resolveFocusPublishId(routeId, selectedTypeId, mode);
|
||||
const item = FOCUS_DRAW_BY_ID[id];
|
||||
const item = FOCUS_WORKSHEET_BY_ID[id];
|
||||
if (!item) return null;
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from './registry';
|
||||
import { getPublishMetaByFocusState } from './focusDraw.config';
|
||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||
import { incrementWorksheetLikes } from '../../utils/worksheetStats';
|
||||
|
||||
const TYPE_LIST = FOCUS_TYPE_CONFIGS.map((t) => ({
|
||||
id: t.id,
|
||||
@@ -120,6 +121,7 @@ createFocusPage({
|
||||
selectedTypeId: id,
|
||||
functionId: id,
|
||||
pageTitle: title,
|
||||
isPreviewFavorite: false,
|
||||
showActions: !!typeConfig.actions,
|
||||
currentActions: typeConfig.actions || [],
|
||||
actionsTitle: typeConfig.actionsTitle || '选择模式',
|
||||
@@ -156,6 +158,7 @@ createFocusPage({
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
pageTitle: title,
|
||||
isPreviewFavorite: false,
|
||||
});
|
||||
|
||||
this.initPageInfo(this.data.functionId, title);
|
||||
@@ -196,15 +199,28 @@ createFocusPage({
|
||||
this.onRandom();
|
||||
},
|
||||
|
||||
onPreviewFavorite() {
|
||||
async onPreviewFavorite() {
|
||||
const next = !this.data.isPreviewFavorite;
|
||||
this.setData({ isPreviewFavorite: next });
|
||||
if (next) {
|
||||
const id = this.getWorksheetStatsId();
|
||||
if (id) incrementWorksheetLikes(id);
|
||||
}
|
||||
wx.showToast({
|
||||
title: next ? '收藏成功' : '已取消收藏',
|
||||
icon: 'none',
|
||||
});
|
||||
},
|
||||
|
||||
getWorksheetStatsId(): string {
|
||||
const meta = getPublishMetaByFocusState(
|
||||
this.data.functionId,
|
||||
this.data.selectedTypeId,
|
||||
this.data.currentMode,
|
||||
);
|
||||
return meta?.id || this.data.functionId;
|
||||
},
|
||||
|
||||
getPublishMeta(): DebugPublishMeta {
|
||||
const meta = getPublishMetaByFocusState(
|
||||
this.data.functionId,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||
|
||||
interface MathDrawDefinition {
|
||||
/** 本地 worksheet 元数据(与云库 `worksheets` 文档 `_id` 对齐,用于发布与批量同步脚本) */
|
||||
interface MathWorksheetDefinition {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
@@ -12,7 +13,7 @@ interface MathDrawDefinition {
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
const MATH_DRAW_DEFINITIONS = [
|
||||
export const MATH_WORKSHEET_DEFINITIONS = [
|
||||
{
|
||||
id: 'number-find',
|
||||
title: '找数字,涂一涂',
|
||||
@@ -28,8 +29,8 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
id: 'number-write',
|
||||
title: '看数字,写一写',
|
||||
subtitle: '按笔画顺序书写数字',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
difficulty: 1,
|
||||
tags: ['数学', '数字书写', '描红'],
|
||||
sortOrder: 102,
|
||||
@@ -68,8 +69,8 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
id: 'number-object-fill',
|
||||
title: '数物填写',
|
||||
subtitle: '数出数量,填写对应数字',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
difficulty: 1,
|
||||
tags: ['数学', '数物对应', '填写'],
|
||||
sortOrder: 106,
|
||||
@@ -88,8 +89,8 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
id: 'counting-fill',
|
||||
title: '数一数,填一填',
|
||||
subtitle: '数出物品数量,填写数字',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
difficulty: 1,
|
||||
tags: ['数学', '计数', '填写'],
|
||||
sortOrder: 108,
|
||||
@@ -128,8 +129,8 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
id: 'number-decompose',
|
||||
title: '10以内数的分与合',
|
||||
subtitle: '把数字分一分,合一合',
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
ageMin: 5,
|
||||
ageMax: 7,
|
||||
difficulty: 1,
|
||||
tags: ['数学', '分与合', '10以内'],
|
||||
sortOrder: 112,
|
||||
@@ -158,8 +159,8 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
id: 'addition-5',
|
||||
title: '5以内加法',
|
||||
subtitle: '图形化展示5以内加法',
|
||||
ageMin: 3,
|
||||
ageMax: 5,
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
difficulty: 1,
|
||||
tags: ['数学', '加法', '5以内', '计算题'],
|
||||
sortOrder: 115,
|
||||
@@ -178,8 +179,8 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
id: 'subtraction-10',
|
||||
title: '10以内减法',
|
||||
subtitle: '图形化展示10以内减法',
|
||||
ageMin: 4,
|
||||
ageMax: 6,
|
||||
ageMin: 5,
|
||||
ageMax: 7,
|
||||
difficulty: 2,
|
||||
tags: ['数学', '减法', '10以内', '计算题'],
|
||||
sortOrder: 117,
|
||||
@@ -238,7 +239,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
id: 'practice-addition',
|
||||
title: '加法运算',
|
||||
subtitle: '10/20/50/100 以内加法',
|
||||
ageMin: 5,
|
||||
ageMin: 6,
|
||||
ageMax: 8,
|
||||
difficulty: 3,
|
||||
tags: ['数学', '口算', '加法', '计算题'],
|
||||
@@ -248,7 +249,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
id: 'practice-subtraction',
|
||||
title: '减法运算',
|
||||
subtitle: '10/20/50/100以内减法',
|
||||
ageMin: 5,
|
||||
ageMin: 6,
|
||||
ageMax: 8,
|
||||
difficulty: 3,
|
||||
tags: ['数学', '口算', '减法', '计算题'],
|
||||
@@ -258,7 +259,7 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
id: 'practice-mixed',
|
||||
title: '混合运算',
|
||||
subtitle: '10/20/50/100以内加减法混合',
|
||||
ageMin: 5,
|
||||
ageMin: 6,
|
||||
ageMax: 8,
|
||||
difficulty: 3,
|
||||
tags: ['数学', '口算', '加减法', '计算题'],
|
||||
@@ -268,19 +269,19 @@ const MATH_DRAW_DEFINITIONS = [
|
||||
id: 'multiplication-table',
|
||||
title: '九九乘法表',
|
||||
subtitle: '学习九九乘法口诀',
|
||||
ageMin: 6,
|
||||
ageMin: 7,
|
||||
ageMax: 8,
|
||||
difficulty: 3,
|
||||
tags: ['数学', '乘法', '九九乘法表'],
|
||||
sortOrder: 126,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MathDrawDefinition>;
|
||||
] as const satisfies ReadonlyArray<MathWorksheetDefinition>;
|
||||
|
||||
type MathDrawDefinitionItem = (typeof MATH_DRAW_DEFINITIONS)[number];
|
||||
type MathWorksheetDefinitionItem = (typeof MATH_WORKSHEET_DEFINITIONS)[number];
|
||||
|
||||
const MATH_DRAW_BY_ID = Object.fromEntries(
|
||||
MATH_DRAW_DEFINITIONS.map((item) => [item.id, item]),
|
||||
) as Record<string, MathDrawDefinitionItem>;
|
||||
const MATH_WORKSHEET_BY_ID = Object.fromEntries(
|
||||
MATH_WORKSHEET_DEFINITIONS.map((item) => [item.id, item]),
|
||||
) as Record<string, MathWorksheetDefinitionItem>;
|
||||
|
||||
function resolveMathPublishId(
|
||||
routeId: string,
|
||||
@@ -295,7 +296,7 @@ function resolveMathPublishId(
|
||||
return 'number-object-fill';
|
||||
}
|
||||
|
||||
if (routeId in MATH_DRAW_BY_ID) {
|
||||
if (routeId in MATH_WORKSHEET_BY_ID) {
|
||||
return routeId;
|
||||
}
|
||||
|
||||
@@ -308,7 +309,7 @@ export function getPublishMetaByMathState(
|
||||
mode?: string,
|
||||
): DebugPublishMeta | null {
|
||||
const id = resolveMathPublishId(routeId, selectedTypeId, mode);
|
||||
const item = MATH_DRAW_BY_ID[id];
|
||||
const item = MATH_WORKSHEET_BY_ID[id];
|
||||
if (!item) return null;
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from './registry';
|
||||
import { getPublishMetaByMathState } from './mathDraw.config';
|
||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||
import { incrementWorksheetLikes } from '../../utils/worksheetStats';
|
||||
|
||||
const TYPE_LIST = MATH_TYPE_CONFIGS.map((t) => ({
|
||||
id: t.id,
|
||||
@@ -130,6 +131,7 @@ createMathPage({
|
||||
selectedTypeId: id,
|
||||
functionId: id,
|
||||
pageTitle: title,
|
||||
isPreviewFavorite: false,
|
||||
showActions: !!typeConfig.actions,
|
||||
currentActions: typeConfig.actions || [],
|
||||
actionsTitle: typeConfig.actionsTitle || '选择模式',
|
||||
@@ -170,6 +172,7 @@ createMathPage({
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
pageTitle: title,
|
||||
isPreviewFavorite: false,
|
||||
});
|
||||
|
||||
this.initPageInfo(this.data.functionId, title);
|
||||
@@ -260,15 +263,28 @@ createMathPage({
|
||||
this.onRandom();
|
||||
},
|
||||
|
||||
onPreviewFavorite() {
|
||||
async onPreviewFavorite() {
|
||||
const next = !this.data.isPreviewFavorite;
|
||||
this.setData({ isPreviewFavorite: next });
|
||||
if (next) {
|
||||
const id = this.getWorksheetStatsId();
|
||||
if (id) incrementWorksheetLikes(id);
|
||||
}
|
||||
wx.showToast({
|
||||
title: next ? '收藏成功' : '已取消收藏',
|
||||
icon: 'none',
|
||||
});
|
||||
},
|
||||
|
||||
getWorksheetStatsId(): string {
|
||||
const meta = getPublishMetaByMathState(
|
||||
this.data.functionId,
|
||||
this.data.selectedTypeId,
|
||||
this.data.currentMode,
|
||||
);
|
||||
return meta?.id || this.data.functionId;
|
||||
},
|
||||
|
||||
getPublishMeta(): DebugPublishMeta {
|
||||
const meta = getPublishMetaByMathState(
|
||||
this.data.functionId,
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
import type { AgeBandKey } from '../../core/data/difficulty';
|
||||
|
||||
export type AbilityItem = {
|
||||
icon: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
};
|
||||
|
||||
/** 每周练习项:与云 / page-config 中 worksheet 字段对齐,便于跳转 */
|
||||
export type WeekExercise = {
|
||||
/** 与云数据库 worksheets._id、page-config category.items[].id 一致 */
|
||||
_id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
/** 小程序内路径,可含 query,如 /mathPages/mathDraw/mathDraw?id=xxx */
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type WeekPlan = {
|
||||
week: number;
|
||||
theme: string;
|
||||
exercises: WeekExercise[];
|
||||
};
|
||||
|
||||
export const AGE_TAB_SUB: Record<AgeBandKey, string> = {
|
||||
'3-4': '启蒙认知',
|
||||
'4-5': '基础练习',
|
||||
'5-6': '能力提升',
|
||||
'6-7': '幼小衔接',
|
||||
'7-8': '知识拓展',
|
||||
};
|
||||
|
||||
/** 各年龄段能力目标(初版,可由 AI Skill 更新) */
|
||||
export const AGE_ABILITIES: Record<AgeBandKey, AbilityItem[]> = {
|
||||
'3-4': [
|
||||
{ icon: '🔢', title: '数感', desc: '认读 1-5、点数对应' },
|
||||
{ icon: '✏️', title: '书写', desc: '涂鸦线条、简单描红' },
|
||||
{ icon: '🧩', title: '思维', desc: '找相同、简单配对' },
|
||||
],
|
||||
'4-5': [
|
||||
{ icon: '🔢', title: '数感', desc: '10 以内数数、比大小' },
|
||||
{ icon: '✏️', title: '书写', desc: '笔画模仿、图形描边' },
|
||||
{ icon: '🧩', title: '思维', desc: '规律排序、图形分类' },
|
||||
],
|
||||
'5-6': [
|
||||
{ icon: '🔢', title: '数感', desc: '10以内加减法、凑十法' },
|
||||
{ icon: '✏️', title: '书写', desc: '练字帖、拼音入门' },
|
||||
{ icon: '🧩', title: '思维', desc: '方格推理、格子仿画 5×5' },
|
||||
],
|
||||
'6-7': [
|
||||
{ icon: '🔢', title: '数感', desc: '20 以内运算、应用题启蒙' },
|
||||
{ icon: '✏️', title: '书写', desc: '常用字书写、词语积累' },
|
||||
{ icon: '🧩', title: '思维', desc: '逻辑填空、空间想象' },
|
||||
],
|
||||
'7-8': [
|
||||
{ icon: '🔢', title: '数感', desc: '乘除启蒙、巧算练习' },
|
||||
{ icon: '✏️', title: '书写', desc: '段落抄写、古诗诵读' },
|
||||
{ icon: '🧩', title: '思维', desc: '数独入门、综合推理' },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 4 周路线(初版):每项含 _id / subtitle / path,与线上 worksheet 或本地分类兜底一致。
|
||||
* AI Skill 生成时应从 .cache/page-config.json 的 category.items[] 复制 id→_id、title、subtitle、path。
|
||||
*/
|
||||
export const AGE_WEEK_PLANS: Record<AgeBandKey, WeekPlan[]> = {
|
||||
'3-4': [
|
||||
{
|
||||
week: 1,
|
||||
theme: '数感启蒙',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'number-find',
|
||||
title: '找数字,涂一涂',
|
||||
subtitle: '在数字方阵中找出目标数字并涂色',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-find',
|
||||
},
|
||||
{
|
||||
_id: 'addition-5',
|
||||
title: '5以内加法',
|
||||
subtitle: '图形化展示 5 以内加法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition-5',
|
||||
},
|
||||
{
|
||||
_id: 'counting-matching',
|
||||
title: '数一数,连一连',
|
||||
subtitle: '连线配对数字和对应数量图形',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=counting-matching',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 2,
|
||||
theme: '形状与连线',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'shape-recognition',
|
||||
title: '识别形状',
|
||||
subtitle: '识别形状,涂一涂',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=shape-recognition',
|
||||
},
|
||||
{
|
||||
_id: 'line-recognition',
|
||||
title: '线条识别',
|
||||
subtitle: '认识不同线条,画出颜色对应的线条',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=line-recognition',
|
||||
},
|
||||
{
|
||||
_id: 'dot-connect',
|
||||
title: '数字点连线',
|
||||
subtitle: '按数字顺序连点成图',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=dot-connect',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 3,
|
||||
theme: '趣味专注',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'color-pattern',
|
||||
title: '颜色找规律',
|
||||
subtitle: '观察颜色规律,在空白图形中涂色',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=color-pattern',
|
||||
},
|
||||
{
|
||||
_id: 'grid-drawing-3x3',
|
||||
title: '格子仿画 3×3',
|
||||
subtitle: '简单有趣,培养专注力',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-3x3',
|
||||
},
|
||||
{
|
||||
_id: 'match-connect',
|
||||
title: '连连看',
|
||||
subtitle: '根据物品连一连',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 4,
|
||||
theme: '综合练习',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'addition-10',
|
||||
title: '10以内加法',
|
||||
subtitle: '图形化展示 10 以内加法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition-10',
|
||||
},
|
||||
{
|
||||
_id: 'word-recognition',
|
||||
title: '识字卡',
|
||||
subtitle: '输入或选字生成涂色识字卡',
|
||||
path: '/pages/index/index',
|
||||
},
|
||||
{
|
||||
_id: 'number-coloring',
|
||||
title: '按数字,涂颜色',
|
||||
subtitle: '按指定数字给对应圆圈涂色',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-coloring',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'4-5': [
|
||||
{
|
||||
week: 1,
|
||||
theme: '数数与比较',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'counting-fill',
|
||||
title: '数一数,填一填',
|
||||
subtitle: '数出物品数量,填写数字',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=counting-fill',
|
||||
},
|
||||
{
|
||||
_id: 'compare',
|
||||
title: '数一数,比大小',
|
||||
subtitle: '比较数量,填入 ><=',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=compare',
|
||||
},
|
||||
{
|
||||
_id: 'number-object-match',
|
||||
title: '数物连线',
|
||||
subtitle: '连线相同数量的物品和数字',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-object-match',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 2,
|
||||
theme: '线条与形状',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'number-write',
|
||||
title: '看数字,写一写',
|
||||
subtitle: '按笔画顺序练习书写数字',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-write',
|
||||
},
|
||||
{
|
||||
_id: 'shape-symbol',
|
||||
title: '图形符号配对',
|
||||
subtitle: '根据图形画对应符号',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=shape-symbol',
|
||||
},
|
||||
{
|
||||
_id: 'counting-matching',
|
||||
title: '数一数,连一连',
|
||||
subtitle: '连线配对数字和对应数量图形',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=counting-matching',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 3,
|
||||
theme: '规律与分类',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'color-pattern',
|
||||
title: '颜色找规律',
|
||||
subtitle: '观察颜色规律,在空白图形中涂色',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=color-pattern',
|
||||
},
|
||||
{
|
||||
_id: 'number-sort',
|
||||
title: '数字排序',
|
||||
subtitle: '写出正确的数字顺序',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-sort',
|
||||
},
|
||||
{
|
||||
_id: 'color-shape-match',
|
||||
title: '根据颜色画图形',
|
||||
subtitle: '根据颜色画出对应图形',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=color-shape-match',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 4,
|
||||
theme: '综合提升',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'code-connect',
|
||||
title: '译码连线',
|
||||
subtitle: '按数字顺序将数字对应颜色连线',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=code-connect',
|
||||
},
|
||||
{
|
||||
_id: 'match-connect',
|
||||
title: '连连看',
|
||||
subtitle: '根据物品连一连',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
|
||||
},
|
||||
{
|
||||
_id: 'addition-10',
|
||||
title: '10以内加法',
|
||||
subtitle: '图形化展示 10 以内加法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition-10',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'5-6': [
|
||||
{
|
||||
week: 1,
|
||||
theme: '加减入门',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'addition-5',
|
||||
title: '5以内加法',
|
||||
subtitle: '图形化展示 5 以内加法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition-5',
|
||||
},
|
||||
{
|
||||
_id: 'addition-10',
|
||||
title: '10以内加法',
|
||||
subtitle: '图形化展示 10 以内加法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition-10',
|
||||
},
|
||||
{
|
||||
_id: 'subtraction-10',
|
||||
title: '10以内减法',
|
||||
subtitle: '图形化展示 10 以内减法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=subtraction-10',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 2,
|
||||
theme: '凑十与分解',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'make-ten',
|
||||
title: '凑十法练习',
|
||||
subtitle: '20 以内进位加法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=make-ten',
|
||||
},
|
||||
{
|
||||
_id: 'number-decompose',
|
||||
title: '10以内数的分与合',
|
||||
subtitle: '把数字分一分,合一合',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-decompose',
|
||||
},
|
||||
{
|
||||
_id: 'one-digit-addition',
|
||||
title: '一位数加法',
|
||||
subtitle: '通过圆点学习一位数加法运算',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=one-digit-addition',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 3,
|
||||
theme: '逻辑与推理',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'grid-reasoning',
|
||||
title: '方格推理',
|
||||
subtitle: '推理出合并方格并连线',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
|
||||
},
|
||||
{
|
||||
_id: 'grid-drawing-5x5',
|
||||
title: '格子仿画 5×5',
|
||||
subtitle: '创意挑战,提升观察力',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-5x5',
|
||||
},
|
||||
{
|
||||
_id: 'missing-number',
|
||||
title: '填上缺少的数字',
|
||||
subtitle: '在数列中找出并填写缺失数字',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=missing-number',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 4,
|
||||
theme: '综合应用',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'addition-subtraction-10',
|
||||
title: '10以内加减法',
|
||||
subtitle: '加减法混合运算',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition-subtraction-10',
|
||||
},
|
||||
{
|
||||
_id: 'practice-addition',
|
||||
title: '加法运算',
|
||||
subtitle: '10/20/50/100 以内加法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-addition',
|
||||
},
|
||||
{
|
||||
_id: 'copybook',
|
||||
title: '练字帖',
|
||||
subtitle: '选字生成田字格笔顺练字帖',
|
||||
path: '/pages/copyBook/copyBook',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'6-7': [
|
||||
{
|
||||
week: 1,
|
||||
theme: '20 以内运算',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'practice-addition',
|
||||
title: '加法运算',
|
||||
subtitle: '10/20/50/100 以内加法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-addition',
|
||||
},
|
||||
{
|
||||
_id: 'practice-subtraction',
|
||||
title: '减法运算',
|
||||
subtitle: '10/20/50/100 以内减法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-subtraction',
|
||||
},
|
||||
{
|
||||
_id: 'practice-mixed',
|
||||
title: '混合运算',
|
||||
subtitle: '10/20/50/100 以内加减法混合',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-mixed',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 2,
|
||||
theme: '识字与词语',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'copybook',
|
||||
title: '练字帖',
|
||||
subtitle: '选字生成田字格笔顺练字帖',
|
||||
path: '/pages/copyBook/copyBook',
|
||||
},
|
||||
{
|
||||
_id: 'match-connect',
|
||||
title: '连连看',
|
||||
subtitle: '根据物品连一连',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
|
||||
},
|
||||
{
|
||||
_id: 'word-recognition',
|
||||
title: '识字卡',
|
||||
subtitle: '输入或选字生成涂色识字卡',
|
||||
path: '/pages/index/index',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 3,
|
||||
theme: '逻辑与空间',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'grid-reasoning',
|
||||
title: '方格推理',
|
||||
subtitle: '推理出合并方格并连线',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
|
||||
},
|
||||
{
|
||||
_id: 'position-coloring',
|
||||
title: '方位涂涂乐',
|
||||
subtitle: '观察位置,在方格中涂色',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=position-coloring',
|
||||
},
|
||||
{
|
||||
_id: 'code-connect',
|
||||
title: '译码连线',
|
||||
subtitle: '按数字顺序将数字对应颜色连线',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=code-connect',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 4,
|
||||
theme: '综合提升',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'break-ten',
|
||||
title: '破十法练习',
|
||||
subtitle: '20 以内退位减法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=break-ten',
|
||||
},
|
||||
{
|
||||
_id: 'flat-ten',
|
||||
title: '平十法练习',
|
||||
subtitle: '20 以内退位减法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=flat-ten',
|
||||
},
|
||||
{
|
||||
_id: 'number-decompose-20',
|
||||
title: '20以内数的分与合',
|
||||
subtitle: '把数字分一分,合一合',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-decompose-20',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'7-8': [
|
||||
{
|
||||
week: 1,
|
||||
theme: '乘除启蒙',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'multiplication-table',
|
||||
title: '九九乘法表',
|
||||
subtitle: '学习九九乘法口诀',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=multiplication-table',
|
||||
},
|
||||
{
|
||||
_id: 'practice-addition',
|
||||
title: '加法运算',
|
||||
subtitle: '10/20/50/100 以内加法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-addition',
|
||||
},
|
||||
{
|
||||
_id: 'practice-subtraction',
|
||||
title: '减法运算',
|
||||
subtitle: '10/20/50/100 以内减法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-subtraction',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 2,
|
||||
theme: '巧算与策略',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'make-ten',
|
||||
title: '凑十法练习',
|
||||
subtitle: '20 以内进位加法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=make-ten',
|
||||
},
|
||||
{
|
||||
_id: 'borrow-ten',
|
||||
title: '借十法练习',
|
||||
subtitle: '20 以上退位减法',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=borrow-ten',
|
||||
},
|
||||
{
|
||||
_id: 'practice-mixed',
|
||||
title: '混合运算',
|
||||
subtitle: '10/20/50/100 以内加减法混合',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-mixed',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 3,
|
||||
theme: '综合推理',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'grid-reasoning',
|
||||
title: '方格推理',
|
||||
subtitle: '推理出合并方格并连线',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
|
||||
},
|
||||
{
|
||||
_id: 'code-connect',
|
||||
title: '译码连线',
|
||||
subtitle: '按数字顺序将数字对应颜色连线',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=code-connect',
|
||||
},
|
||||
{
|
||||
_id: 'dot-connect',
|
||||
title: '数字点连线',
|
||||
subtitle: '按数字顺序连点成图',
|
||||
path: '/focusPages/focusDraw/focusDraw?id=dot-connect',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 4,
|
||||
theme: '能力拓展',
|
||||
exercises: [
|
||||
{
|
||||
_id: 'letter-tracing-two-column',
|
||||
title: '两列练习',
|
||||
subtitle: '左 A-M、右 N-Z,大小写配对描红',
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column',
|
||||
},
|
||||
{
|
||||
_id: 'word-recognition',
|
||||
title: '识字卡',
|
||||
subtitle: '输入或选字生成涂色识字卡',
|
||||
path: '/pages/index/index',
|
||||
},
|
||||
{
|
||||
_id: 'practice-mixed',
|
||||
title: '混合运算',
|
||||
subtitle: '10/20/50/100 以内加减法混合',
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-mixed',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -5,6 +5,7 @@
|
||||
"backgroundColor": "#FEF6E7",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"nav-bar": "../../components3.0/nav-bar/nav-bar"
|
||||
"nav-bar": "../../components3.0/nav-bar/nav-bar",
|
||||
"worksheet-card": "../../components3.0/worksheet-card/worksheet-card"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
.age-page {
|
||||
min-height: 100vh;
|
||||
background: @bg-page;
|
||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||
padding-bottom: calc(10rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -289,14 +289,27 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.age-week-row__title {
|
||||
.age-week-row__meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.age-week-row__title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: @text-title;
|
||||
}
|
||||
|
||||
.age-week-row__subtitle {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
color: @text-gray;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.age-week-row__btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@@ -324,6 +337,39 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.age-rec-loading {
|
||||
margin-top: @section-gap-lg;
|
||||
padding: 48rpx @page-padding-x @section-gap-xs;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.age-rec-loading__icon {
|
||||
margin-right: 12rpx;
|
||||
font-size: 30rpx;
|
||||
color: @text-secondary;
|
||||
animation: age-rec-loading-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.age-rec-loading__text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: @text-secondary;
|
||||
}
|
||||
|
||||
@keyframes age-rec-loading-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.age-rec__head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -356,28 +402,5 @@
|
||||
|
||||
.age-rec__card {
|
||||
width: calc(50% - 16rpx);
|
||||
background: @bg-white;
|
||||
border-radius: @radius-lg;
|
||||
overflow: hidden;
|
||||
border: 1rpx solid rgba(50, 46, 37, 0.08);
|
||||
box-shadow: @shadow;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.age-rec__img {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
display: block;
|
||||
background: fade(@brand, 15%);
|
||||
}
|
||||
|
||||
.age-rec__body {
|
||||
padding: 20rpx 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.age-rec__card-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: @text-title;
|
||||
line-height: 1.35;
|
||||
}
|
||||
+118
-189
@@ -1,187 +1,101 @@
|
||||
import { AGE_BANDS, AgeBandKey } from '../../core/data/difficulty';
|
||||
import { parseMiniProgramUrl } from '../../utils/index';
|
||||
import {
|
||||
AGE_ABILITIES,
|
||||
AGE_TAB_SUB,
|
||||
AGE_WEEK_PLANS,
|
||||
type AbilityItem,
|
||||
type WeekPlan,
|
||||
} from './age.config';
|
||||
|
||||
type AbilityItem = {
|
||||
icon: string;
|
||||
const DEFAULT_AGE: AgeBandKey = '3-4';
|
||||
|
||||
const TAB_BAR_PATHS = new Set([
|
||||
'/pages/home/home',
|
||||
'/pages/category/category',
|
||||
'/pages/age/age',
|
||||
'/pages/favorites/favorites',
|
||||
'/pages/profile/profile',
|
||||
]);
|
||||
|
||||
function navigateByPath(path?: string, fallbackTitle?: string) {
|
||||
const p = String(path || '').trim();
|
||||
if (!p) {
|
||||
wx.showToast({
|
||||
title: fallbackTitle ? `${fallbackTitle} 即将上线` : '即将上线',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { path: basePath, query } = parseMiniProgramUrl(p);
|
||||
if (TAB_BAR_PATHS.has(basePath)) {
|
||||
if (Object.keys(query).length > 0) {
|
||||
console.warn(
|
||||
'[age navigateByPath] tabBar 不支持携带参数,已忽略 query:',
|
||||
basePath,
|
||||
query,
|
||||
);
|
||||
}
|
||||
wx.switchTab({ url: basePath });
|
||||
return;
|
||||
}
|
||||
wx.navigateTo({ url: p }).catch((err) => {
|
||||
console.error('navigateByPath error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
type RecommendedItemView = {
|
||||
title: string;
|
||||
desc: string;
|
||||
image: string;
|
||||
subtitle: string;
|
||||
ageBand: string;
|
||||
difficulty: string;
|
||||
path: string;
|
||||
worksheetId?: string;
|
||||
};
|
||||
const RECOMMENDED_FALLBACK: RecommendedItemView[] = [];
|
||||
|
||||
const DIFFICULTY_LABELS: Record<string, string> = {
|
||||
1: '入门',
|
||||
2: '基础',
|
||||
3: '进阶',
|
||||
4: '挑战',
|
||||
};
|
||||
|
||||
type WeekExercise = {
|
||||
title: string;
|
||||
};
|
||||
function toDifficultyLabel(value: unknown) {
|
||||
const text = String(value || '').trim();
|
||||
return DIFFICULTY_LABELS[text] || text;
|
||||
}
|
||||
|
||||
type WeekPlan = {
|
||||
week: number;
|
||||
theme: string;
|
||||
exercises: WeekExercise[];
|
||||
};
|
||||
|
||||
const AGE_TAB_SUB: Record<AgeBandKey, string> = {
|
||||
'3-4': '启蒙认知',
|
||||
'4-5': '基础练习',
|
||||
'5-6': '能力提升',
|
||||
'6-7': '幼小衔接',
|
||||
'7-8': '知识拓展',
|
||||
};
|
||||
|
||||
/** Stitch「为你推荐」占位图(需在小程序后台配置 download 合法域名含 lh3.googleusercontent.com) */
|
||||
const RECOMMENDED_FROM_STITCH: { title: string; image: string }[] = [
|
||||
{
|
||||
title: '10以内加减法',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuD-3lADhclTGODN9WtNh5v8bQPLysbllbKNkH40bAmVyE5d54PwfTBOiX9nER1hpE4rzHAfsmM_JhVnsE4EbdA914rYIb3S3e4r9QKFjUqYmj2XpE4MlYr_vLkG4oeMnvnHOqGC96nLGrPSOq-fpIddV7RSlYLDeNccBs0G2oUOy-6jIMujzTuEGk4nnxme5rpxs7rYjwcvVsqM5dlkpJthY42e4B3lSPH7ezx187WRSk--m4ZiWmAI52mMOiygzTSWoNormlvfvdM',
|
||||
},
|
||||
{
|
||||
title: '拼音描红',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBYS4O7PyflR5q8M6oXKwj_8QHzex3OL3YvdHBncutm7a_bjlZj83-8UBpGUQvJO20V0BnAdU3iKEoLlUqwpS60CiPhVyxYaHEAqv52P5YUhJdjf0hpLir1Li4rdI_-eqGkPTasYl-5kD2r8-RjUokKg_HiW5_TF72UbxFXHQh4BefYJZfl0Oc9-aTQvkbFL0C3tagi9Qv1jRsgQldMFU4M7cHt8KkxhO1QDF7-R85ZFORJM0RiqV2l-jn0c-UscKtuWtroiX6UUKk',
|
||||
},
|
||||
{
|
||||
title: '趣味找不同',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBSafbTyucs8JLpTgibRWwAJsHukrklTAuEnaAzax_Kr1RNCwHjqpQjta0Q-h47ClufzEmaHPgCIOlm_a2lL4gNlgBDyraCwzRU7aWCf_Fj_dM1cLGzROJmtqrdHKjQzTbCp0_Da8BvXi4a4H2X4RAYVPnLbZVmbgPXrTvpKgEI605kIMmr3n0--WojJQRsF4B_xCho80gABbIaMTHJHrs9xdEY6fZKg8ArQx7lRbJ6_EsLLB_C5a5crZ_begwruXKr3pmdU6KneC8',
|
||||
},
|
||||
{
|
||||
title: '走迷宫大挑战',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuAOZHI1Dz6ax4iFUb3QYL-EWp2euA_3kyUeCcu6V57BBCzarMwKnFzcpJxJn4X4woklWFB08Ii1w6zsk0H5eDaPdgUYmsvY0NFqvAhKN_IbZGZjZnclZlBHkLRcX64Nl767L2pg01FQu9Tp6nq2p3lAuYeEQ2fiIdXh4j-QHyikbmx4_aDlx5X5fC0fqMMxWykhwLNkDzl2Rs3ucXj7zYnUP-dNNY-aj_JvqkY4pqoXQDt9ZJcfSeL0dWes7nvD5U2C_JFhrk1bVkw',
|
||||
},
|
||||
{
|
||||
title: '数字点连线',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuD-3lADhclTGODN9WtNh5v8bQPLysbllbKNkH40bAmVyE5d54PwfTBOiX9nER1hpE4rzHAfsmM_JhVnsE4EbdA914rYIb3S3e4r9QKFjUqYmj2XpE4MlYr_vLkG4oeMnvnHOqGC96nLGrPSOq-fpIddV7RSlYLDeNccBs0G2oUOy-6jIMujzTuEGk4nnxme5rpxs7rYjwcvVsqM5dlkpJthY42e4B3lSPH7ezx187WRSk--m4ZiWmAI52mMOiygzTSWoNormlvfvdM',
|
||||
},
|
||||
{
|
||||
title: '方格推理高手',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBYS4O7PyflR5q8M6oXKwj_8QHzex3OL3YvdHBncutm7a_bjlZj83-8UBpGUQvJO20V0BnAdU3iKEoLlUqwpS60CiPhVyxYaHEAqv52P5YUhJdjf0hpLir1Li4rdI_-eqGkPTasYl-5kD2r8-RjUokKg_HiW5_TF72UbxFXHQh4BefYJZfl0Oc9-aTQvkbFL0C3tagi9Qv1jRsgQldMFU4M7cHt8KkxhO1QDF7-R85ZFORJM0RiqV2l-jn0c-UscKtuWtroiX6UUKk',
|
||||
},
|
||||
];
|
||||
|
||||
/** 各年龄段能力目标(Mock) */
|
||||
const MOCK_ABILITY: Record<AgeBandKey, AbilityItem[]> = {
|
||||
'3-4': [
|
||||
{
|
||||
icon: '🔢',
|
||||
title: '数感',
|
||||
desc: '认读 1-5、点数对应',
|
||||
},
|
||||
{
|
||||
icon: '✏️',
|
||||
title: '书写',
|
||||
desc: '涂鸦线条、简单描红',
|
||||
},
|
||||
{
|
||||
icon: '🧩',
|
||||
title: '思维',
|
||||
desc: '找相同、简单配对',
|
||||
},
|
||||
],
|
||||
'4-5': [
|
||||
{
|
||||
icon: '🔢',
|
||||
title: '数感',
|
||||
desc: '10 以内数数、比大小',
|
||||
},
|
||||
{
|
||||
icon: '✏️',
|
||||
title: '书写',
|
||||
desc: '笔画模仿、图形描边',
|
||||
},
|
||||
{
|
||||
icon: '🧩',
|
||||
title: '思维',
|
||||
desc: '规律排序、图形分类',
|
||||
},
|
||||
],
|
||||
'5-6': [
|
||||
{
|
||||
icon: '🔢',
|
||||
title: '数感',
|
||||
desc: '10以内加减法、凑十法',
|
||||
},
|
||||
{
|
||||
icon: '✏️',
|
||||
title: '书写',
|
||||
desc: '练字帖、拼音入门',
|
||||
},
|
||||
{
|
||||
icon: '🧩',
|
||||
title: '思维',
|
||||
desc: '方格推理、格子仿画 5×5',
|
||||
},
|
||||
],
|
||||
'6-7': [
|
||||
{
|
||||
icon: '🔢',
|
||||
title: '数感',
|
||||
desc: '20 以内运算、应用题启蒙',
|
||||
},
|
||||
{
|
||||
icon: '✏️',
|
||||
title: '书写',
|
||||
desc: '常用字书写、词语积累',
|
||||
},
|
||||
{
|
||||
icon: '🧩',
|
||||
title: '思维',
|
||||
desc: '逻辑填空、空间想象',
|
||||
},
|
||||
],
|
||||
'7-8': [
|
||||
{
|
||||
icon: '🔢',
|
||||
title: '数感',
|
||||
desc: '乘除启蒙、巧算练习',
|
||||
},
|
||||
{
|
||||
icon: '✏️',
|
||||
title: '书写',
|
||||
desc: '段落抄写、古诗诵读',
|
||||
},
|
||||
{
|
||||
icon: '🧩',
|
||||
title: '思维',
|
||||
desc: '数独入门、综合推理',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** 4 周路线(各年龄段共用结构,内容 Mock) */
|
||||
const MOCK_WEEKS: WeekPlan[] = [
|
||||
{
|
||||
week: 1,
|
||||
theme: '数感启蒙',
|
||||
exercises: [
|
||||
{ title: '找数字涂一涂' },
|
||||
{ title: '5 以内加法' },
|
||||
{ title: '数数连一连' },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 2,
|
||||
theme: '形状与连线',
|
||||
exercises: [
|
||||
{ title: '识别形状' },
|
||||
{ title: '线条识别' },
|
||||
{ title: '数字点连线' },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 3,
|
||||
theme: '趣味专注',
|
||||
exercises: [
|
||||
{ title: '颜色找规律' },
|
||||
{ title: '方格推理' },
|
||||
{ title: '格子仿画' },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 4,
|
||||
theme: '综合练习',
|
||||
exercises: [
|
||||
{ title: '10以内加减法' },
|
||||
{ title: '识字卡' },
|
||||
{ title: '连连看' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_AGE: AgeBandKey = '5-6';
|
||||
async function fetchAgeRecommended(
|
||||
ageKey: AgeBandKey,
|
||||
): Promise<RecommendedItemView[]> {
|
||||
try {
|
||||
const res = await wx.cloud.callFunction({
|
||||
name: 'ageRecommendedQuery',
|
||||
data: { ageKey },
|
||||
});
|
||||
const payload = (res && (res.result as any)) || null;
|
||||
if (!payload?.success) return [];
|
||||
const items = (payload.data?.items || []) as any[];
|
||||
return items
|
||||
.map((it) => ({
|
||||
title: String(it.title || ''),
|
||||
image: String(it.previewImg || ''),
|
||||
subtitle: String(it.subtitle || ''),
|
||||
ageBand: String(it.ageBand || it.ageRange || ''),
|
||||
difficulty: toDifficultyLabel(
|
||||
it.difficultyLabel || it.difficulty || '',
|
||||
),
|
||||
path: String(it.path || ''),
|
||||
worksheetId: String(it.worksheetId || ''),
|
||||
}))
|
||||
.filter((it) => it.title);
|
||||
} catch (e) {
|
||||
console.warn('[age] fetchAgeRecommended failed:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -190,24 +104,34 @@ Page({
|
||||
rangeText: b.label.replace(/\s*岁\s*$/, ''),
|
||||
subLabel: AGE_TAB_SUB[b.key],
|
||||
})),
|
||||
activeAgeKey: DEFAULT_AGE,
|
||||
activeAgeKey: DEFAULT_AGE as AgeBandKey,
|
||||
goalTitle: '',
|
||||
abilityItems: [] as AbilityItem[],
|
||||
weekPlans: MOCK_WEEKS,
|
||||
recommendedItems: RECOMMENDED_FROM_STITCH,
|
||||
weekPlans: [] as WeekPlan[],
|
||||
recommendedItems: RECOMMENDED_FALLBACK,
|
||||
recommendedLoading: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.applyAge(DEFAULT_AGE);
|
||||
},
|
||||
|
||||
applyAge(key: AgeBandKey) {
|
||||
async applyAge(key: AgeBandKey) {
|
||||
const band = AGE_BANDS.find((b) => b.key === key);
|
||||
const goalTitle = band ? `${band.label}能力目标` : '能力目标';
|
||||
this.setData({
|
||||
activeAgeKey: key,
|
||||
goalTitle,
|
||||
abilityItems: MOCK_ABILITY[key],
|
||||
abilityItems: AGE_ABILITIES[key] || [],
|
||||
weekPlans: AGE_WEEK_PLANS[key] || [],
|
||||
recommendedLoading: true,
|
||||
});
|
||||
|
||||
const recommended = await fetchAgeRecommended(key);
|
||||
this.setData({
|
||||
recommendedItems:
|
||||
recommended.length > 0 ? recommended : RECOMMENDED_FALLBACK,
|
||||
recommendedLoading: false,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -218,18 +142,23 @@ Page({
|
||||
},
|
||||
|
||||
onPracticeTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const path = e.currentTarget.dataset.path as string | undefined;
|
||||
const title = e.currentTarget.dataset.title as string | undefined;
|
||||
if (!title) return;
|
||||
wx.showToast({ title: `即将前往:${title}`, icon: 'none' });
|
||||
navigateByPath(path, title);
|
||||
},
|
||||
|
||||
onRecommendMore() {
|
||||
wx.switchTab({ url: '/pages/home/home' });
|
||||
},
|
||||
|
||||
onRecommendTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const title = e.currentTarget.dataset.title as string | undefined;
|
||||
if (!title) return;
|
||||
wx.showToast({ title: `去看看:${title}`, icon: 'none' });
|
||||
onTapCard(e: WechatMiniprogram.CustomEvent) {
|
||||
const detail = (e.detail || {}) as { title?: string; path?: string };
|
||||
const path =
|
||||
detail.path ||
|
||||
(e.currentTarget.dataset.path as string | undefined);
|
||||
const title =
|
||||
detail.title ||
|
||||
(e.currentTarget.dataset.title as string | undefined);
|
||||
navigateByPath(path, title);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -59,12 +59,22 @@
|
||||
<view
|
||||
wx:for="{{item.exercises}}"
|
||||
wx:for-item="ex"
|
||||
wx:key="title"
|
||||
wx:key="_id"
|
||||
class="age-week-row">
|
||||
<text class="age-week-row__title">{{ex.title}}</text>
|
||||
<view class="age-week-row__meta">
|
||||
<text class="age-week-row__title"
|
||||
>{{ex.title}}</text
|
||||
>
|
||||
<text
|
||||
wx:if="{{ex.subtitle}}"
|
||||
class="age-week-row__subtitle"
|
||||
>{{ex.subtitle}}</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="age-week-row__btn"
|
||||
data-title="{{ex.title}}"
|
||||
data-path="{{ex.path}}"
|
||||
catchtap="onPracticeTap">
|
||||
<text>去练习</text>
|
||||
<text
|
||||
@@ -78,7 +88,14 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="age-rec">
|
||||
<view wx:if="{{recommendedLoading}}" class="age-rec-loading">
|
||||
<text
|
||||
class="toy-icon toy-icon-loading age-rec-loading__icon"
|
||||
aria-hidden="true"></text>
|
||||
<text class="age-rec-loading__text">推荐加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:else class="age-rec">
|
||||
<view class="age-rec__head">
|
||||
<text class="age-rec__title">为你推荐</text>
|
||||
<text class="age-rec__more" bindtap="onRecommendMore"
|
||||
@@ -86,20 +103,21 @@
|
||||
>
|
||||
</view>
|
||||
<view class="age-rec__grid">
|
||||
<view
|
||||
<worksheet-card
|
||||
wx:for="{{recommendedItems}}"
|
||||
wx:key="title"
|
||||
class="age-rec__card"
|
||||
variant="track"
|
||||
title="{{item.title}}"
|
||||
preview-img="{{item.image}}"
|
||||
subtitle="{{item.subtitle}}"
|
||||
age-band="{{item.ageBand}}"
|
||||
difficulty="{{item.difficulty}}"
|
||||
path="{{item.path}}"
|
||||
data-title="{{item.title}}"
|
||||
bindtap="onRecommendTap">
|
||||
<image
|
||||
class="age-rec__img"
|
||||
src="{{item.image}}"
|
||||
mode="aspectFill" />
|
||||
<view class="age-rec__body">
|
||||
<text class="age-rec__card-title">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
data-path="{{item.path}}"
|
||||
bindtap="onTapCard"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"usingComponents": {
|
||||
"nav-bar": "/components3.0/nav-bar/nav-bar",
|
||||
"category-tabs": "/components3.0/category-tabs/category-tabs",
|
||||
"worksheet-card": "/components3.0/worksheet-card/worksheet-card",
|
||||
"van-icon": "@vant/weapp/icon/index",
|
||||
"toy-icon": "../../toy/icon/icon"
|
||||
}
|
||||
|
||||
@@ -225,79 +225,6 @@
|
||||
.home-track-card {
|
||||
width: calc((750rpx - 2 * @page-padding-x - 2 * @home-track-gap) / 2.2);
|
||||
flex-shrink: 0;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 6rpx 20rpx rgba(50, 46, 37, 0.06);
|
||||
}
|
||||
|
||||
.home-track-card__top {
|
||||
/* 与 supportPages/mathIndex .function-item-img-wrapper 一致 */
|
||||
height: 270rpx;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
border-bottom: 1rpx solid rgba(50, 46, 37, 0.08);
|
||||
background: #f5edd8;
|
||||
}
|
||||
|
||||
.home-track-card__cover {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.home-track-card__icon {
|
||||
font-size: 72rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-track-card__tags {
|
||||
position: absolute;
|
||||
right: 12rpx;
|
||||
bottom: 12rpx;
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.home-track-card__body {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 20rpx @page-padding-inner-x 22rpx;
|
||||
background: #ffffff;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.home-track-card__title {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: @text-title;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.home-track-card__sub {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin-top: 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: @text-gray;
|
||||
line-height: 1.45;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.home-age-scroll {
|
||||
|
||||
@@ -254,9 +254,14 @@ Page({
|
||||
navigateByPath(path, title);
|
||||
},
|
||||
|
||||
onTapTrackCard(e: WechatMiniprogram.TouchEvent) {
|
||||
const path = e.currentTarget.dataset.path as string | undefined;
|
||||
const title = e.currentTarget.dataset.title as string | undefined;
|
||||
onTapCard(e: WechatMiniprogram.CustomEvent) {
|
||||
const detail = (e.detail || {}) as { title?: string; path?: string };
|
||||
const path =
|
||||
detail.path ||
|
||||
(e.currentTarget.dataset.path as string | undefined);
|
||||
const title =
|
||||
detail.title ||
|
||||
(e.currentTarget.dataset.title as string | undefined);
|
||||
navigateByPath(path, title);
|
||||
},
|
||||
|
||||
|
||||
@@ -186,40 +186,22 @@
|
||||
<!-- <text class="home-section-panel__sub">{{item.subtitle}}</text> -->
|
||||
<scroll-view class="home-track-scroll" scroll-x enable-flex>
|
||||
<view class="home-track-row">
|
||||
<view
|
||||
<worksheet-card
|
||||
wx:for="{{item.items}}"
|
||||
wx:key="id"
|
||||
class="home-track-card"
|
||||
variant="track"
|
||||
title="{{item.title}}"
|
||||
subtitle="{{item.subtitle}}"
|
||||
preview-img="{{item.previewImg}}"
|
||||
icon="{{item.icon}}"
|
||||
age-band="{{item.ageBand}}"
|
||||
difficulty="{{item.difficulty}}"
|
||||
path="{{item.path}}"
|
||||
data-title="{{item.title}}"
|
||||
data-path="{{item.path}}"
|
||||
bindtap="onTapTrackCard">
|
||||
<view class="home-track-card__top">
|
||||
<image
|
||||
wx:if="{{item.previewImg}}"
|
||||
class="home-track-card__cover"
|
||||
src="{{item.previewImg}}"
|
||||
mode="widthFix" />
|
||||
<text wx:else class="home-track-card__icon"
|
||||
>{{item.icon}}</text
|
||||
>
|
||||
<view class="home-track-card__tags">
|
||||
<text class="tag tag--age"
|
||||
>{{item.ageBand}}</text
|
||||
>
|
||||
<text class="tag tag--diff"
|
||||
>{{item.difficulty}}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<view class="home-track-card__body">
|
||||
<text class="home-track-card__title"
|
||||
>{{item.title}}</text
|
||||
>
|
||||
<text class="home-track-card__sub"
|
||||
>{{item.subtitle}}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
bindtap="onTapCard"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -14,6 +14,13 @@ const DEBUG_ENTRIES: DebugEntry[] = [
|
||||
icon: '🗂️',
|
||||
path: '/supportPages/categoryManage/categoryManage',
|
||||
},
|
||||
{
|
||||
id: 'worksheet-sync',
|
||||
title: 'Worksheet 同步',
|
||||
subtitle: '按本地配置批量同步年龄/标签,并重建 home+category 配置。',
|
||||
icon: '🔄',
|
||||
path: '/supportPages/worksheetSync/worksheetSync',
|
||||
},
|
||||
{
|
||||
id: 'category-content',
|
||||
title: '分类页内容管理',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "Worksheet 同步",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#F8F0E0",
|
||||
"backgroundColor": "#F8F0E0",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"toy-button": "/toy/button-v2/button"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
@import '../../style/theme.less';
|
||||
|
||||
page {
|
||||
min-height: 100%;
|
||||
background: @bg-header;
|
||||
}
|
||||
|
||||
.ws-sync-page {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
padding-bottom: 320rpx;
|
||||
background: @bg-header;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ws-sync-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx;
|
||||
background: @bg-white;
|
||||
border-radius: @radius-xl;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
.ws-sync-summary__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ws-sync-summary__title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: @text-title;
|
||||
}
|
||||
|
||||
.ws-sync-summary__desc {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
color: @text-secondary;
|
||||
}
|
||||
|
||||
.ws-sync-summary__badge {
|
||||
flex-shrink: 0;
|
||||
margin-left: 20rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #605b50;
|
||||
background: fade(#8a8478, 15%);
|
||||
}
|
||||
|
||||
.ws-sync-summary__badge--ready {
|
||||
color: #4a7a12;
|
||||
background: fade(#93d333, 18%);
|
||||
}
|
||||
|
||||
.ws-sync-page__stats {
|
||||
display: flex;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.ws-sync-stat {
|
||||
flex: 1;
|
||||
padding: 24rpx 16rpx;
|
||||
background: @bg-white;
|
||||
border-radius: @radius-xl;
|
||||
text-align: center;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
.ws-sync-stat + .ws-sync-stat {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.ws-sync-stat__num {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
color: @text-title;
|
||||
}
|
||||
|
||||
.ws-sync-stat__label {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: @text-secondary;
|
||||
}
|
||||
|
||||
.ws-sync-page__notice {
|
||||
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;
|
||||
}
|
||||
|
||||
.ws-sync-page__notice--error {
|
||||
color: #8a6d00;
|
||||
background: fade(#ffb703, 18%);
|
||||
}
|
||||
|
||||
.ws-sync-result {
|
||||
margin-top: 24rpx;
|
||||
padding: 28rpx;
|
||||
background: @bg-white;
|
||||
border-radius: @radius-xl;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
.ws-sync-result__title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: @text-title;
|
||||
}
|
||||
|
||||
.ws-sync-result__body {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
color: @text-secondary;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ws-sync-page__build {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
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,315 @@
|
||||
type CloudFunctionResult<T> = {
|
||||
success?: boolean;
|
||||
message?: string;
|
||||
data?: T;
|
||||
dryRun?: boolean;
|
||||
total?: number;
|
||||
failed?: number;
|
||||
results?: Array<{ id: string | null; ok: boolean; error?: string }>;
|
||||
};
|
||||
|
||||
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 || {};
|
||||
}
|
||||
|
||||
type LocalDef = {
|
||||
id: string;
|
||||
ageMin: number;
|
||||
ageMax: number;
|
||||
tags: readonly string[];
|
||||
};
|
||||
|
||||
function inferGradeFromAge(ageMin: number, ageMax: number): number {
|
||||
const centerAge = Math.round((ageMin + ageMax) / 2);
|
||||
const ageGradeMap: Record<number, number> = {
|
||||
2: -4,
|
||||
3: -3,
|
||||
4: -2,
|
||||
5: -1,
|
||||
6: 0,
|
||||
7: 1,
|
||||
8: 2,
|
||||
9: 3,
|
||||
10: 4,
|
||||
11: 5,
|
||||
12: 6,
|
||||
};
|
||||
|
||||
return ageGradeMap[centerAge] ?? 0;
|
||||
}
|
||||
|
||||
type ConfigModule = Record<string, readonly LocalDef[] | unknown>;
|
||||
|
||||
type ConfigSource = {
|
||||
label: string;
|
||||
modulePath: string;
|
||||
exportName: string;
|
||||
};
|
||||
|
||||
const CONFIG_SOURCES: ConfigSource[] = [
|
||||
{
|
||||
label: 'math',
|
||||
modulePath: '../../mathPages/mathDraw/mathDraw.config',
|
||||
exportName: 'MATH_WORKSHEET_DEFINITIONS',
|
||||
},
|
||||
{
|
||||
label: 'focus',
|
||||
modulePath: '../../focusPages/focusDraw/focusDraw.config',
|
||||
exportName: 'FOCUS_WORKSHEET_DEFINITIONS',
|
||||
},
|
||||
{
|
||||
label: 'letterTracing',
|
||||
modulePath: '../../englishPages/letterTracing/letterTracing.config',
|
||||
exportName: 'LETTER_TRACING_WORKSHEET_DEFINITIONS',
|
||||
},
|
||||
];
|
||||
|
||||
function isLocalDef(value: unknown): value is LocalDef {
|
||||
const row = value as Partial<LocalDef>;
|
||||
return (
|
||||
!!row &&
|
||||
typeof row.id === 'string' &&
|
||||
typeof row.ageMin === 'number' &&
|
||||
typeof row.ageMax === 'number' &&
|
||||
Array.isArray(row.tags)
|
||||
);
|
||||
}
|
||||
|
||||
async function loadConfigSource(source: ConfigSource): Promise<LocalDef[]> {
|
||||
const mod = (await require.async(source.modulePath)) as ConfigModule;
|
||||
const rows = mod[source.exportName];
|
||||
|
||||
if (!Array.isArray(rows) || !rows.every(isLocalDef)) {
|
||||
throw new Error(`${source.label} 配置格式不正确`);
|
||||
}
|
||||
|
||||
return [...rows];
|
||||
}
|
||||
|
||||
async function collectAll(): Promise<LocalDef[]> {
|
||||
const groups = await Promise.all(CONFIG_SOURCES.map(loadConfigSource));
|
||||
return groups.flat();
|
||||
}
|
||||
|
||||
function assertUniqueIds(rows: LocalDef[]) {
|
||||
const set = new Set<string>();
|
||||
for (const r of rows) {
|
||||
if (set.has(r.id)) {
|
||||
throw new Error(`本地配置存在重复 id:${r.id}`);
|
||||
}
|
||||
set.add(r.id);
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeResult(
|
||||
title: string,
|
||||
result: CloudFunctionResult<unknown>,
|
||||
): string {
|
||||
if (!result.success) {
|
||||
return `${title}失败:${result.message || '未知错误'}`;
|
||||
}
|
||||
const total = result.total ?? 0;
|
||||
const failed = result.failed ?? 0;
|
||||
const ok = total - failed;
|
||||
const firstError = result.results?.find((r) => !r.ok)?.error;
|
||||
return `${title}完成:成功 ${ok}/${total}${
|
||||
failed ? `,失败 ${failed}(如:${firstError || '未知原因'})` : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
configLoading: true,
|
||||
configReady: false,
|
||||
configError: '',
|
||||
configTotal: 0,
|
||||
configSourceCount: CONFIG_SOURCES.length,
|
||||
loadingAges: false,
|
||||
loadingTags: false,
|
||||
loadingRebuild: false,
|
||||
statusText: '配置加载中,请稍候',
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
void this.loadConfigs();
|
||||
},
|
||||
|
||||
async onPullDownRefresh() {
|
||||
await this.loadConfigs();
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
async loadConfigs() {
|
||||
this.setData({
|
||||
configLoading: true,
|
||||
configReady: false,
|
||||
configError: '',
|
||||
statusText: '配置加载中,请稍候',
|
||||
});
|
||||
|
||||
try {
|
||||
const rows = await collectAll();
|
||||
assertUniqueIds(rows);
|
||||
this.setData({
|
||||
configLoading: false,
|
||||
configReady: true,
|
||||
configTotal: rows.length,
|
||||
statusText: `配置已加载:${rows.length} 个 worksheet,可执行同步`,
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : '配置加载失败';
|
||||
this.setData({
|
||||
configLoading: false,
|
||||
configReady: false,
|
||||
configError: msg,
|
||||
statusText: msg,
|
||||
});
|
||||
wx.showToast({ title: msg, icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
async sync(mode: 'ages' | 'tags') {
|
||||
if (!this.data.configReady) {
|
||||
wx.showToast({ title: '配置未加载完成', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = await collectAll();
|
||||
assertUniqueIds(rows);
|
||||
|
||||
const patches =
|
||||
mode === 'ages'
|
||||
? rows.map((r) => ({
|
||||
id: r.id,
|
||||
ageMin: r.ageMin,
|
||||
ageMax: r.ageMax,
|
||||
grade: inferGradeFromAge(r.ageMin, r.ageMax),
|
||||
}))
|
||||
: rows.map((r) => ({
|
||||
id: r.id,
|
||||
tags: [...r.tags],
|
||||
}));
|
||||
|
||||
wx.showLoading({ title: '同步中...' });
|
||||
const startedAt = Date.now();
|
||||
|
||||
try {
|
||||
const patchRes = await callCloudFunction('worksheetsBatchPatch', {
|
||||
patches,
|
||||
dryRun: false,
|
||||
});
|
||||
|
||||
if (!patchRes.success) {
|
||||
throw new Error(patchRes.message || '同步失败');
|
||||
}
|
||||
|
||||
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
||||
|
||||
const text = [
|
||||
summarizeResult(
|
||||
mode === 'ages'
|
||||
? '同步年龄(worksheetsBatchPatch)'
|
||||
: '同步标签(worksheetsBatchPatch)',
|
||||
patchRes,
|
||||
),
|
||||
'page-config 未自动重建,如需刷新前台展示请点击“重建配置”。',
|
||||
`耗时:${elapsed}s`,
|
||||
].join('\n');
|
||||
|
||||
this.setData({ statusText: text });
|
||||
wx.showToast({ title: '已完成', icon: 'success' });
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : '同步失败';
|
||||
this.setData({ statusText: msg });
|
||||
wx.showToast({ title: msg, icon: 'none' });
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
async rebuildPageConfig() {
|
||||
wx.showLoading({ title: '重建中...' });
|
||||
const startedAt = Date.now();
|
||||
|
||||
try {
|
||||
const rebuildRes = await callCloudFunction('pageContentBuild', {
|
||||
page: 'all',
|
||||
});
|
||||
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
||||
|
||||
const text = [
|
||||
summarizeResult(
|
||||
'重建 page-config(pageContentBuild: all)',
|
||||
rebuildRes,
|
||||
),
|
||||
`耗时:${elapsed}s`,
|
||||
].join('\n');
|
||||
|
||||
this.setData({ statusText: text });
|
||||
if (!rebuildRes.success) {
|
||||
throw new Error(rebuildRes.message || '重建失败');
|
||||
}
|
||||
wx.showToast({ title: '已重建', icon: 'success' });
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : '重建失败';
|
||||
this.setData({ statusText: msg });
|
||||
wx.showToast({ title: msg, icon: 'none' });
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
async onSyncAges() {
|
||||
if (
|
||||
!this.data.configReady ||
|
||||
this.data.loadingAges ||
|
||||
this.data.loadingTags ||
|
||||
this.data.loadingRebuild
|
||||
)
|
||||
return;
|
||||
this.setData({ loadingAges: true });
|
||||
try {
|
||||
await this.sync('ages');
|
||||
} finally {
|
||||
this.setData({ loadingAges: false });
|
||||
}
|
||||
},
|
||||
|
||||
async onSyncTags() {
|
||||
if (
|
||||
!this.data.configReady ||
|
||||
this.data.loadingAges ||
|
||||
this.data.loadingTags ||
|
||||
this.data.loadingRebuild
|
||||
)
|
||||
return;
|
||||
this.setData({ loadingTags: true });
|
||||
try {
|
||||
await this.sync('tags');
|
||||
} finally {
|
||||
this.setData({ loadingTags: false });
|
||||
}
|
||||
},
|
||||
|
||||
async onRebuildConfig() {
|
||||
if (
|
||||
this.data.loadingAges ||
|
||||
this.data.loadingTags ||
|
||||
this.data.loadingRebuild
|
||||
)
|
||||
return;
|
||||
this.setData({ loadingRebuild: true });
|
||||
try {
|
||||
await this.rebuildPageConfig();
|
||||
} finally {
|
||||
this.setData({ loadingRebuild: false });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
<view class="ws-sync-page">
|
||||
<view class="ws-sync-summary">
|
||||
<view class="ws-sync-summary__info">
|
||||
<text class="ws-sync-summary__title">Worksheet 批量同步</text>
|
||||
<text class="ws-sync-summary__desc">
|
||||
同步本地配置到 worksheets,必要时单独重建 page-config 数据。
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="ws-sync-summary__badge {{configReady ? 'ws-sync-summary__badge--ready' : ''}}">
|
||||
<text
|
||||
>{{configReady ? '可同步' : configLoading ? '加载中' :
|
||||
'需处理'}}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="ws-sync-page__stats">
|
||||
<view class="ws-sync-stat">
|
||||
<text class="ws-sync-stat__num">{{configTotal}}</text>
|
||||
<text class="ws-sync-stat__label">配置项</text>
|
||||
</view>
|
||||
<view class="ws-sync-stat">
|
||||
<text class="ws-sync-stat__num">{{configSourceCount}}</text>
|
||||
<text class="ws-sync-stat__label">分包</text>
|
||||
</view>
|
||||
<view class="ws-sync-stat">
|
||||
<text class="ws-sync-stat__num"
|
||||
>{{configReady ? 'Ready' : '--'}}</text
|
||||
>
|
||||
<text class="ws-sync-stat__label">状态</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{configLoading}}" class="ws-sync-page__notice">
|
||||
正在异步加载 math / focus / letterTracing 配置...
|
||||
</view>
|
||||
|
||||
<view
|
||||
wx:elif="{{configError}}"
|
||||
class="ws-sync-page__notice ws-sync-page__notice--error">
|
||||
{{configError}}
|
||||
</view>
|
||||
|
||||
<view class="ws-sync-result">
|
||||
<text class="ws-sync-result__title">最近一次结果</text>
|
||||
<text class="ws-sync-result__body">{{statusText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="ws-sync-page__build">
|
||||
<toy-button
|
||||
type="primary"
|
||||
width="100%"
|
||||
loading="{{loadingAges}}"
|
||||
disabled="{{!configReady || loadingAges || loadingTags || loadingRebuild}}"
|
||||
bindtap="onSyncAges">
|
||||
{{loadingAges ? '同步中...' : '同步年龄'}}
|
||||
</toy-button>
|
||||
<toy-button
|
||||
width="100%"
|
||||
loading="{{loadingTags}}"
|
||||
disabled="{{!configReady || loadingAges || loadingTags || loadingRebuild}}"
|
||||
bindtap="onSyncTags">
|
||||
{{loadingTags ? '同步中...' : '同步标签'}}
|
||||
</toy-button>
|
||||
<toy-button
|
||||
width="100%"
|
||||
loading="{{loadingRebuild}}"
|
||||
disabled="{{loadingAges || loadingTags || loadingRebuild}}"
|
||||
bindtap="onRebuildConfig">
|
||||
{{loadingRebuild ? '重建中...' : '重建配置'}}
|
||||
</toy-button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { checkAndSaveImage } from './saveImage';
|
||||
import tracker from './tracker';
|
||||
import { incrementWorksheetDownloads } from './worksheetStats';
|
||||
|
||||
// 存储键名
|
||||
const STORAGE_KEY_DOWNLOAD_COUNT = 'downloadCount';
|
||||
@@ -195,8 +196,9 @@ function doDownload(
|
||||
errorToast?: string;
|
||||
trackerName?: string;
|
||||
trackerMode?: string;
|
||||
worksheetId?: string;
|
||||
},
|
||||
) {
|
||||
): Promise<boolean> {
|
||||
// 上报下载埋点
|
||||
if (options.trackerName) {
|
||||
if (options.trackerMode) {
|
||||
@@ -206,11 +208,15 @@ function doDownload(
|
||||
}
|
||||
}
|
||||
|
||||
// 增加下载次数
|
||||
incrementDownloadCount();
|
||||
|
||||
// 执行下载(checkAndSaveImage 内部已有成功提示,这里不需要额外处理)
|
||||
checkAndSaveImage(canvas);
|
||||
return checkAndSaveImage(canvas).then((saved) => {
|
||||
if (saved) {
|
||||
incrementDownloadCount();
|
||||
if (options.worksheetId) {
|
||||
incrementWorksheetDownloads(options.worksheetId);
|
||||
}
|
||||
}
|
||||
return saved;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,6 +227,7 @@ function doDownload(
|
||||
* @param options.errorToast 错误提示(可选,默认:'请先生成内容')
|
||||
* @param options.trackerName 埋点名称(可选)
|
||||
* @param options.trackerMode 埋点模式(可选)
|
||||
* @param options.worksheetId worksheet ID(可选,保存成功后更新 downloads)
|
||||
*/
|
||||
export async function downloadPrint(
|
||||
canvas: Canvas | null,
|
||||
@@ -229,20 +236,20 @@ export async function downloadPrint(
|
||||
errorToast?: string;
|
||||
trackerName?: string;
|
||||
trackerMode?: string;
|
||||
worksheetId?: string;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
// 检查 canvas 是否存在
|
||||
if (!canvas) {
|
||||
wx.showToast({
|
||||
title: options.errorToast || '请先生成内容',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isDevBypassLimits()) {
|
||||
doDownload(canvas, options);
|
||||
return;
|
||||
return doDownload(canvas, options);
|
||||
}
|
||||
|
||||
// 获取今日下载次数
|
||||
@@ -251,21 +258,19 @@ export async function downloadPrint(
|
||||
|
||||
// 如果已观看广告,直接下载
|
||||
if (hasWatchedAd) {
|
||||
doDownload(canvas, options);
|
||||
return;
|
||||
return doDownload(canvas, options);
|
||||
}
|
||||
|
||||
// 如果未超过免费次数,直接下载
|
||||
if (downloadCount < FREE_DOWNLOAD_LIMIT) {
|
||||
doDownload(canvas, options);
|
||||
return;
|
||||
return doDownload(canvas, options);
|
||||
}
|
||||
|
||||
// 超过免费次数,需要观看广告
|
||||
const watched = await showVideoAd();
|
||||
if (watched) {
|
||||
// 观看完成,可以下载
|
||||
doDownload(canvas, options);
|
||||
return doDownload(canvas, options);
|
||||
}
|
||||
/* else {
|
||||
// 未观看完成,提示用户
|
||||
@@ -275,4 +280,5 @@ export async function downloadPrint(
|
||||
duration: 2000,
|
||||
});
|
||||
} */
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,63 +3,75 @@ import { A4_EXPORT_SIZE_150DPI } from '../constants/colors';
|
||||
/**
|
||||
* 导出为固定打印分辨率(A4 150 DPI),避免高 DPR 设备导出过大图导致打印慢、文件大
|
||||
*/
|
||||
const doSaveImage = (canvas: Canvas) => {
|
||||
const { width: destWidth, height: destHeight } = A4_EXPORT_SIZE_150DPI;
|
||||
wx.canvasToTempFilePath({
|
||||
canvas,
|
||||
fileType: 'jpg',
|
||||
quality: 0.9,
|
||||
destWidth,
|
||||
destHeight,
|
||||
success: (res) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
// 下载成功提示(下载次数记录在 downloadPrint.ts 中处理)
|
||||
wx.showToast({
|
||||
title: '保存成功,请在相册中查看',
|
||||
icon: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
// fail: () => wx.showToast({
|
||||
// title: '保存涂色卡失败,请重试!',
|
||||
// icon: 'none',
|
||||
// duration: 2000,
|
||||
// })
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({
|
||||
title: '生成涂色卡失败,请重试!',
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
const doSaveImage = (canvas: Canvas): Promise<boolean> =>
|
||||
new Promise((resolve) => {
|
||||
const { width: destWidth, height: destHeight } = A4_EXPORT_SIZE_150DPI;
|
||||
wx.canvasToTempFilePath({
|
||||
canvas,
|
||||
fileType: 'jpg',
|
||||
quality: 0.9,
|
||||
destWidth,
|
||||
destHeight,
|
||||
success: (res) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '保存成功,请在相册中查看',
|
||||
icon: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
resolve(true);
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({
|
||||
title: '保存涂色卡失败,请重试!',
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({
|
||||
title: '生成涂色卡失败,请重试!',
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const requestPhotoPermission = (canvas: Canvas) => {
|
||||
wx.authorize({
|
||||
scope: 'scope.writePhotosAlbum',
|
||||
success: () => doSaveImage(canvas),
|
||||
fail: () =>
|
||||
wx.showToast({
|
||||
title: '请在“设置-添加到相册”中开启相册权限以保存涂色卡',
|
||||
icon: 'none',
|
||||
duration: 3000,
|
||||
}),
|
||||
const requestPhotoPermission = (canvas: Canvas): Promise<boolean> =>
|
||||
new Promise((resolve) => {
|
||||
wx.authorize({
|
||||
scope: 'scope.writePhotosAlbum',
|
||||
success: () => {
|
||||
doSaveImage(canvas).then(resolve);
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({
|
||||
title: '请在“设置-添加到相册”中开启相册权限以保存涂色卡',
|
||||
icon: 'none',
|
||||
duration: 3000,
|
||||
});
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const checkAndSaveImage = (canvas: Canvas) => {
|
||||
wx.getSetting({
|
||||
success: (res) => {
|
||||
if (!res.authSetting['scope.writePhotosAlbum']) {
|
||||
requestPhotoPermission(canvas);
|
||||
} else {
|
||||
doSaveImage(canvas);
|
||||
}
|
||||
},
|
||||
export const checkAndSaveImage = (canvas: Canvas): Promise<boolean> =>
|
||||
new Promise((resolve) => {
|
||||
wx.getSetting({
|
||||
success: (res) => {
|
||||
if (!res.authSetting['scope.writePhotosAlbum']) {
|
||||
requestPhotoPermission(canvas).then(resolve);
|
||||
} else {
|
||||
doSaveImage(canvas).then(resolve);
|
||||
}
|
||||
},
|
||||
fail: () => resolve(false),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
type WorksheetStatsField = 'likes' | 'downloads';
|
||||
|
||||
export async function updateWorksheetStats(
|
||||
id: string,
|
||||
field: WorksheetStatsField,
|
||||
): Promise<boolean> {
|
||||
const worksheetId = String(id || '').trim();
|
||||
if (!worksheetId) return false;
|
||||
|
||||
try {
|
||||
const { result } = await wx.cloud.callFunction({
|
||||
name: 'worksheetsStatsUpdate',
|
||||
data: {
|
||||
id: worksheetId,
|
||||
field,
|
||||
},
|
||||
});
|
||||
return !!(result as { success?: boolean } | undefined)?.success;
|
||||
} catch (error) {
|
||||
console.error('update worksheet stats failed', field, worksheetId, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function incrementWorksheetLikes(id: string): Promise<boolean> {
|
||||
return updateWorksheetStats(id, 'likes');
|
||||
}
|
||||
|
||||
export function incrementWorksheetDownloads(id: string): Promise<boolean> {
|
||||
return updateWorksheetStats(id, 'downloads');
|
||||
}
|
||||
Reference in New Issue
Block a user