feat: 首页跳转到分类页指定分类优化
This commit is contained in:
@@ -18,8 +18,7 @@
|
|||||||
"categoryContentManage/categoryContentManage",
|
"categoryContentManage/categoryContentManage",
|
||||||
"homeContentManage/homeContentManage",
|
"homeContentManage/homeContentManage",
|
||||||
"mathIndex/mathIndex",
|
"mathIndex/mathIndex",
|
||||||
"focusIndex/focusIndex",
|
"focusIndex/focusIndex"
|
||||||
"copyBook/copyBook"
|
|
||||||
],
|
],
|
||||||
"independent": false
|
"independent": false
|
||||||
},
|
},
|
||||||
@@ -44,7 +43,10 @@
|
|||||||
{
|
{
|
||||||
"root": "chinesePages",
|
"root": "chinesePages",
|
||||||
"name": "chinesePages",
|
"name": "chinesePages",
|
||||||
"pages": ["wordColoring/wordColoring"],
|
"pages": [
|
||||||
|
"wordColoring/wordColoring",
|
||||||
|
"handwritingSheet/handwritingSheet"
|
||||||
|
],
|
||||||
"independent": false
|
"independent": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||||
|
|
||||||
|
interface HandwritingSheetWorksheetDefinition {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
ageMin: number;
|
||||||
|
ageMax: number;
|
||||||
|
difficulty: 1 | 2 | 3 | 4;
|
||||||
|
tags: string[];
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HANDWRITING_SHEET_WORKSHEET_DEFINITIONS = [
|
||||||
|
{
|
||||||
|
id: 'handwriting-sheet',
|
||||||
|
title: '自定义练字帖',
|
||||||
|
subtitle: '自定义田字格练字帖',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 8,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['练字', '字帖', '田字格', '汉字'],
|
||||||
|
sortOrder: 35,
|
||||||
|
},
|
||||||
|
] as const satisfies ReadonlyArray<HandwritingSheetWorksheetDefinition>;
|
||||||
|
|
||||||
|
type HandwritingSheetWorksheetRow =
|
||||||
|
(typeof HANDWRITING_SHEET_WORKSHEET_DEFINITIONS)[number];
|
||||||
|
|
||||||
|
const HANDWRITING_SHEET_WORKSHEET_BY_ID = Object.fromEntries(
|
||||||
|
HANDWRITING_SHEET_WORKSHEET_DEFINITIONS.map((item) => [item.id, item]),
|
||||||
|
) as Record<string, HandwritingSheetWorksheetRow>;
|
||||||
|
|
||||||
|
export const HANDWRITING_SHEET_WORKSHEET_ID =
|
||||||
|
HANDWRITING_SHEET_WORKSHEET_DEFINITIONS[0].id;
|
||||||
|
|
||||||
|
export function getModeInfo(id: string) {
|
||||||
|
const item = HANDWRITING_SHEET_WORKSHEET_BY_ID[id];
|
||||||
|
return item ? { title: item.title, desc: item.subtitle } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidMode(id: string): boolean {
|
||||||
|
return id in HANDWRITING_SHEET_WORKSHEET_BY_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPublishMetaByMode(id: string): DebugPublishMeta | null {
|
||||||
|
const item = HANDWRITING_SHEET_WORKSHEET_BY_ID[id];
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
subtitle: item.subtitle,
|
||||||
|
category: 'chinese',
|
||||||
|
subcategory: 'handwriting-sheet',
|
||||||
|
path: `/chinesePages/handwritingSheet/handwritingSheet?id=${item.id}`,
|
||||||
|
ageMin: item.ageMin,
|
||||||
|
ageMax: item.ageMax,
|
||||||
|
grade: inferGradeFromAge(item.ageMin, item.ageMax),
|
||||||
|
difficulty: item.difficulty,
|
||||||
|
previewImg: '',
|
||||||
|
tags: [...item.tags],
|
||||||
|
isNew: false,
|
||||||
|
isHot: false,
|
||||||
|
sortOrder: item.sortOrder,
|
||||||
|
status: 'draft',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "自定义练字贴",
|
||||||
|
"navigationBarBackgroundColor": "#F8F0E0",
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"backgroundColor": "#FEF6E7",
|
||||||
|
"enablePullDownRefresh": false,
|
||||||
|
"usingComponents": {
|
||||||
|
"nav-bar": "../../components3.0/nav-bar/nav-bar",
|
||||||
|
"preview-card": "../../components3.0/preview-card/preview-card",
|
||||||
|
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
||||||
|
"debug-publish-tools": "../../components3.0/debug-publish-tools/debug-publish-tools",
|
||||||
|
"word-picker": "../../components3.0/word-picker/word-picker",
|
||||||
|
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||||
|
"toy-icon": "../../toy/icon/icon"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
@import '../../style/theme.less';
|
||||||
|
|
||||||
|
.cb-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: @bg-page;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-main {
|
||||||
|
padding: @page-padding-x;
|
||||||
|
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-section {
|
||||||
|
margin-top: @section-gap-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-section-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @text-title;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-content-panel {
|
||||||
|
background: rgba(240, 231, 214, 0.5);
|
||||||
|
border-radius: @radius-lg;
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-field {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-field-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 4rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-field-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: @text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-field-hint {
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(96, 91, 80, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-text-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 96rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2rpx solid rgba(179, 172, 159, 0.3);
|
||||||
|
border-radius: @radius-lg;
|
||||||
|
padding: 0 88rpx 0 28rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: @text-title;
|
||||||
|
box-shadow: @shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-input-clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 20rpx;
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-word-scroll {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-word-list {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-word-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
min-width: 88rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
padding-left: 20rpx;
|
||||||
|
padding-right: 12rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2rpx solid rgba(179, 172, 159, 0.2);
|
||||||
|
border-radius: 36rpx;
|
||||||
|
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-word-chip__text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @text-title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-word-chip__delete-hit {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
margin-right: -6rpx;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-word-add {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-word-add-btn {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx dashed rgba(179, 172, 159, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-category-scroll {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-category-list {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-category-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 20rpx 28rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2rpx solid rgba(179, 172, 159, 0.2);
|
||||||
|
border-radius: 36rpx;
|
||||||
|
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||||
|
transition: transform 0.12s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-category-tag-icon {
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-category-tag-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6c5a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-category-tag--more {
|
||||||
|
.cb-category-tag-icon {
|
||||||
|
font-size: 24rpx;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
color: #6c5a00;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
import WordDrawService from '../../service/wordDrawService';
|
||||||
|
import { downloadPrint } from '../../utils/downloadPrint';
|
||||||
|
import { getWordsSvgData } from '../../utils/getWordsSvgJson';
|
||||||
|
import { WORDS } from '../../core/data/words';
|
||||||
|
import { CharacterItem } from '../../types/characterType';
|
||||||
|
import tracker from '../../utils/tracker';
|
||||||
|
import { createPage } from '../../base/pageMixin';
|
||||||
|
import { defaultShareConfig } from '../../config/config';
|
||||||
|
import {
|
||||||
|
addFavorite,
|
||||||
|
removeFavorite,
|
||||||
|
batchCheckFavorited,
|
||||||
|
} from '../../utils/favorites';
|
||||||
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
import {
|
||||||
|
HANDWRITING_SHEET_WORKSHEET_DEFINITIONS,
|
||||||
|
HANDWRITING_SHEET_WORKSHEET_ID,
|
||||||
|
getModeInfo,
|
||||||
|
getPublishMetaByMode,
|
||||||
|
} from './handwritingSheet.config';
|
||||||
|
|
||||||
|
const MAX_WORDS = 11;
|
||||||
|
const RANDOM_WORD_COUNT = 8;
|
||||||
|
const DEFAULT_WORDS = '东西南北日月山河风雨';
|
||||||
|
const CATEGORY_TAGS = WORDS.slice(0, 3).map((cat) => ({
|
||||||
|
categoryId: cat.categoryId,
|
||||||
|
icon: cat.icon,
|
||||||
|
categoryName: cat.categoryName,
|
||||||
|
}));
|
||||||
|
|
||||||
|
createPage(
|
||||||
|
{
|
||||||
|
canvas: null as Canvas | null,
|
||||||
|
ctx: null as RenderingContext | null,
|
||||||
|
wordDrawService: null as WordDrawService | null,
|
||||||
|
svgWords: {} as Record<string, string[]>,
|
||||||
|
maxRow: 0 as number,
|
||||||
|
maxCol: 0 as number,
|
||||||
|
_favoritedMap: {} as Record<string, boolean>,
|
||||||
|
|
||||||
|
data: {
|
||||||
|
pageTitle: HANDWRITING_SHEET_WORKSHEET_DEFINITIONS[0].title,
|
||||||
|
functionId: HANDWRITING_SHEET_WORKSHEET_ID,
|
||||||
|
hasContent: false,
|
||||||
|
words: [] as string[],
|
||||||
|
inputWords: [] as string[],
|
||||||
|
selectedWords: [] as string[],
|
||||||
|
inputValue: '',
|
||||||
|
showSelectWordPopup: false,
|
||||||
|
pickerCurrentTab: 0,
|
||||||
|
categoryTags: CATEGORY_TAGS,
|
||||||
|
showShareDialog: false,
|
||||||
|
isPreviewFavorite: false,
|
||||||
|
isDevEnv: false,
|
||||||
|
debugPublishVisible: false,
|
||||||
|
debugPublishLoading: false,
|
||||||
|
debugPublishMeta: null as DebugPublishMeta | null,
|
||||||
|
},
|
||||||
|
|
||||||
|
async onLoad() {
|
||||||
|
this.syncDebugPublishEnv();
|
||||||
|
this.loadFavoritedMap();
|
||||||
|
this.initPageInfo(
|
||||||
|
HANDWRITING_SHEET_WORKSHEET_ID,
|
||||||
|
HANDWRITING_SHEET_WORKSHEET_DEFINITIONS[0].title,
|
||||||
|
);
|
||||||
|
await this.loadSvgWords();
|
||||||
|
this.initDefaultWords();
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadSvgWords() {
|
||||||
|
try {
|
||||||
|
wx.showToast({ title: '加载字体中...', icon: 'loading' });
|
||||||
|
this.svgWords = await getWordsSvgData();
|
||||||
|
wx.hideToast();
|
||||||
|
|
||||||
|
if (this.data.words.length > 0) {
|
||||||
|
this.renderPracticeContent().catch(console.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载SVG汉字数据失败:', error);
|
||||||
|
wx.hideToast();
|
||||||
|
wx.showModal({
|
||||||
|
title: '加载失败',
|
||||||
|
content: '无法加载汉字数据,请检查网络连接后重试',
|
||||||
|
showCancel: true,
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmText: '重试',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
this.loadSvgWords();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onCanvasReady(e: WechatMiniprogram.CustomEvent) {
|
||||||
|
const { canvas, ctx } = e.detail;
|
||||||
|
if (!canvas || !ctx) return;
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.wordDrawService = new WordDrawService(canvas, ctx);
|
||||||
|
|
||||||
|
await this.wordDrawService.drawLayout();
|
||||||
|
|
||||||
|
const { maxRow, maxCol } = this.wordDrawService.getMaxGridLayout();
|
||||||
|
this.maxRow = maxRow;
|
||||||
|
this.maxCol = maxCol;
|
||||||
|
|
||||||
|
if (this.data.words.length > 0) {
|
||||||
|
await this.renderPracticeContent();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onPreviewRefresh() {
|
||||||
|
this.refreshWords();
|
||||||
|
},
|
||||||
|
|
||||||
|
async onPreviewFavorite() {
|
||||||
|
const next = !this.data.isPreviewFavorite;
|
||||||
|
this.setData({ isPreviewFavorite: next });
|
||||||
|
this._favoritedMap[HANDWRITING_SHEET_WORKSHEET_ID] = next;
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
await addFavorite(HANDWRITING_SHEET_WORKSHEET_ID);
|
||||||
|
} else {
|
||||||
|
await removeFavorite(HANDWRITING_SHEET_WORKSHEET_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
wx.showToast({
|
||||||
|
title: next ? '收藏成功' : '已取消收藏',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadFavoritedMap() {
|
||||||
|
this._favoritedMap = await batchCheckFavorited([
|
||||||
|
HANDWRITING_SHEET_WORKSHEET_ID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (this._favoritedMap[HANDWRITING_SHEET_WORKSHEET_ID]) {
|
||||||
|
this.setData({ isPreviewFavorite: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputChange(e: WechatMiniprogram.Input) {
|
||||||
|
const value = e.detail.value as string;
|
||||||
|
this.syncWordsFromInput(value, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputConfirm(e: WechatMiniprogram.Input) {
|
||||||
|
const value = e.detail.value as string;
|
||||||
|
this.syncWordsFromInput(value, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
onClearInput() {
|
||||||
|
this.clearWords();
|
||||||
|
},
|
||||||
|
|
||||||
|
syncWordsFromInput(value: string, showLimitToast: boolean) {
|
||||||
|
const selectedWords = this.data.selectedWords as string[];
|
||||||
|
const maxInputCount = Math.max(0, MAX_WORDS - selectedWords.length);
|
||||||
|
const nextInputWords = this.splitToSingleChars(value).slice(
|
||||||
|
0,
|
||||||
|
maxInputCount,
|
||||||
|
);
|
||||||
|
const nextWords = [...nextInputWords, ...selectedWords];
|
||||||
|
|
||||||
|
if (
|
||||||
|
showLimitToast &&
|
||||||
|
nextInputWords.length < this.splitToSingleChars(value).length
|
||||||
|
) {
|
||||||
|
wx.showToast({
|
||||||
|
title: `最多输入 ${maxInputCount} 个字`,
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setData(
|
||||||
|
{
|
||||||
|
inputValue: nextInputWords.join(''),
|
||||||
|
inputWords: nextInputWords,
|
||||||
|
words: nextWords,
|
||||||
|
},
|
||||||
|
() => this.renderPracticeContent().catch(console.error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
openSelectWordPopup() {
|
||||||
|
this.setData({ showSelectWordPopup: true, pickerCurrentTab: 0 });
|
||||||
|
},
|
||||||
|
|
||||||
|
closeSelectWordPopup() {
|
||||||
|
this.setData({ showSelectWordPopup: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
onCategoryTap(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
const categoryId = Number(e.currentTarget.dataset.categoryId);
|
||||||
|
this.setData({
|
||||||
|
showSelectWordPopup: true,
|
||||||
|
pickerCurrentTab: categoryId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onChangeWord(e: WechatMiniprogram.CustomEvent) {
|
||||||
|
const nextSelectedWords = e.detail.selectedWords as string[];
|
||||||
|
const inputWords = this.data.inputWords as string[];
|
||||||
|
|
||||||
|
if (inputWords.length + nextSelectedWords.length > MAX_WORDS) {
|
||||||
|
wx.showToast({
|
||||||
|
title: `最多只能添加 ${MAX_WORDS} 个字`,
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setData(
|
||||||
|
{
|
||||||
|
selectedWords: nextSelectedWords,
|
||||||
|
words: [...inputWords, ...nextSelectedWords],
|
||||||
|
showSelectWordPopup: false,
|
||||||
|
},
|
||||||
|
() => this.renderPracticeContent().catch(console.error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteWordChip(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
const index = Number(e.currentTarget.dataset.index);
|
||||||
|
if (Number.isNaN(index)) return;
|
||||||
|
|
||||||
|
const inputWords = [...(this.data.inputWords as string[])];
|
||||||
|
const selectedWords = [...(this.data.selectedWords as string[])];
|
||||||
|
|
||||||
|
if (index < inputWords.length) {
|
||||||
|
inputWords.splice(index, 1);
|
||||||
|
this.setData(
|
||||||
|
{
|
||||||
|
inputWords,
|
||||||
|
inputValue: inputWords.join(''),
|
||||||
|
words: [...inputWords, ...selectedWords],
|
||||||
|
},
|
||||||
|
() => this.renderPracticeContent().catch(console.error),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIndex = index - inputWords.length;
|
||||||
|
if (selectedIndex < 0 || selectedIndex >= selectedWords.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
selectedWords.splice(selectedIndex, 1);
|
||||||
|
this.setData(
|
||||||
|
{
|
||||||
|
selectedWords,
|
||||||
|
words: [...inputWords, ...selectedWords],
|
||||||
|
},
|
||||||
|
() => this.renderPracticeContent().catch(console.error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearWords() {
|
||||||
|
this.setData(
|
||||||
|
{
|
||||||
|
words: [],
|
||||||
|
inputWords: [],
|
||||||
|
selectedWords: [],
|
||||||
|
inputValue: '',
|
||||||
|
showSelectWordPopup: false,
|
||||||
|
},
|
||||||
|
() => this.renderPracticeContent().catch(console.error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
initDefaultWords() {
|
||||||
|
const defaults = this.getSupportedWords(
|
||||||
|
this.splitToSingleChars(DEFAULT_WORDS),
|
||||||
|
).slice(0, MAX_WORDS);
|
||||||
|
|
||||||
|
if (defaults.length === 0) {
|
||||||
|
this.refreshWords();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setData(
|
||||||
|
{
|
||||||
|
words: defaults,
|
||||||
|
inputWords: defaults,
|
||||||
|
selectedWords: [],
|
||||||
|
inputValue: defaults.join(''),
|
||||||
|
},
|
||||||
|
() => this.renderPracticeContent().catch(console.error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshWords() {
|
||||||
|
const candidates = WORDS.slice(0, 3).reduce(
|
||||||
|
(acc: string[], category) => acc.concat(category.words),
|
||||||
|
[] as string[],
|
||||||
|
);
|
||||||
|
const shuffled = Array.from(new Set(candidates)).sort(
|
||||||
|
() => Math.random() - 0.5,
|
||||||
|
);
|
||||||
|
const supported = this.getSupportedWords(shuffled).slice(
|
||||||
|
0,
|
||||||
|
RANDOM_WORD_COUNT,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (supported.length === 0) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '随机到的字暂不支持,重试一下',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setData(
|
||||||
|
{
|
||||||
|
words: supported,
|
||||||
|
inputWords: supported,
|
||||||
|
selectedWords: [],
|
||||||
|
inputValue: supported.join(''),
|
||||||
|
},
|
||||||
|
() => this.renderPracticeContent().catch(console.error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
splitToSingleChars(text: string): string[] {
|
||||||
|
const matches = text.match(/[\u4e00-\u9fff]/g);
|
||||||
|
return matches || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async renderPracticeContent() {
|
||||||
|
if (!this.canvas || !this.wordDrawService) return;
|
||||||
|
|
||||||
|
const words = this.data.words as string[];
|
||||||
|
if (words.length === 0) {
|
||||||
|
await this.wordDrawService.drawContentEmpty();
|
||||||
|
this.setData({ hasContent: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportedWords = this.getSupportedWords(words);
|
||||||
|
if (supportedWords.length === 0) {
|
||||||
|
wx.showToast({ title: '暂不支持这些汉字', icon: 'none' });
|
||||||
|
await this.wordDrawService.drawContentEmpty();
|
||||||
|
this.setData({ hasContent: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxRow = this.maxRow;
|
||||||
|
let maxCol = this.maxCol;
|
||||||
|
if (!maxRow || !maxCol) {
|
||||||
|
const layout = this.wordDrawService.getMaxGridLayout();
|
||||||
|
this.maxRow = layout.maxRow;
|
||||||
|
this.maxCol = layout.maxCol;
|
||||||
|
maxRow = layout.maxRow;
|
||||||
|
maxCol = layout.maxCol;
|
||||||
|
}
|
||||||
|
|
||||||
|
const characterData = this.processCharacterLayout(
|
||||||
|
supportedWords,
|
||||||
|
maxRow,
|
||||||
|
maxCol,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.wordDrawService.drawPracticeContent(characterData);
|
||||||
|
this.setData({ hasContent: characterData.length > 0 });
|
||||||
|
},
|
||||||
|
|
||||||
|
getSupportedWords(words: string[]): string[] {
|
||||||
|
return words.filter((word) => this.svgWords[word]);
|
||||||
|
},
|
||||||
|
|
||||||
|
processCharacterLayout(
|
||||||
|
words: string[],
|
||||||
|
maxRow: number,
|
||||||
|
maxCol: number,
|
||||||
|
): CharacterItem[] {
|
||||||
|
let rowIndex = 0;
|
||||||
|
const characterData: CharacterItem[] = [];
|
||||||
|
|
||||||
|
words.forEach((word: string) => {
|
||||||
|
const strokes = this.svgWords[word];
|
||||||
|
const strokeCount = strokes.length;
|
||||||
|
const totalCells = 1 + strokeCount;
|
||||||
|
const totalRows = Math.ceil(totalCells / maxCol);
|
||||||
|
rowIndex += totalRows;
|
||||||
|
|
||||||
|
if (rowIndex <= maxRow) {
|
||||||
|
characterData.push({ character: word, strokes });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return characterData;
|
||||||
|
},
|
||||||
|
|
||||||
|
async exportToPrint() {
|
||||||
|
await downloadPrint(this.canvas, {
|
||||||
|
errorToast: '请先生成练字贴',
|
||||||
|
trackerName: '练字贴',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onShare() {},
|
||||||
|
|
||||||
|
onShareAppMessage() {
|
||||||
|
tracker.reportShare('练字贴');
|
||||||
|
return {
|
||||||
|
...defaultShareConfig,
|
||||||
|
path: `/chinesePages/handwritingSheet/handwritingSheet?id=${HANDWRITING_SHEET_WORKSHEET_ID}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareTimeline() {
|
||||||
|
tracker.reportShare('练字贴');
|
||||||
|
return {
|
||||||
|
...defaultShareConfig,
|
||||||
|
query: `id=${HANDWRITING_SHEET_WORKSHEET_ID}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onCloseShareDialog() {
|
||||||
|
this.setData({ showShareDialog: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareSuccess() {
|
||||||
|
this.setData({ showShareDialog: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
getPublishMeta(): DebugPublishMeta {
|
||||||
|
const meta = getPublishMetaByMode(HANDWRITING_SHEET_WORKSHEET_ID);
|
||||||
|
if (!meta) {
|
||||||
|
throw new Error('当前题型配置不存在');
|
||||||
|
}
|
||||||
|
return meta;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shareConfig: defaultShareConfig,
|
||||||
|
pageInfoLookup: getModeInfo,
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<nav-bar title="自定义练字贴" />
|
||||||
|
|
||||||
|
<view class="cb-page">
|
||||||
|
<view class="cb-main">
|
||||||
|
<preview-card
|
||||||
|
id="previewCard"
|
||||||
|
showRefresh="{{true}}"
|
||||||
|
showFavorite="{{true}}"
|
||||||
|
favorited="{{isPreviewFavorite}}"
|
||||||
|
bind:canvas-ready="onCanvasReady"
|
||||||
|
bind:refresh="onPreviewRefresh"
|
||||||
|
bind:favorite="onPreviewFavorite" />
|
||||||
|
|
||||||
|
<view class="cb-section">
|
||||||
|
<text class="cb-section-title">内容设置</text>
|
||||||
|
<view class="cb-content-panel">
|
||||||
|
<view class="cb-field">
|
||||||
|
<view class="cb-field-header">
|
||||||
|
<text class="cb-field-label">自定义文字</text>
|
||||||
|
<text class="cb-field-hint">最多输入11个字</text>
|
||||||
|
</view>
|
||||||
|
<view class="cb-input-wrapper">
|
||||||
|
<input
|
||||||
|
class="cb-text-input"
|
||||||
|
type="text"
|
||||||
|
maxlength="20"
|
||||||
|
placeholder="输入想要练习的汉字"
|
||||||
|
value="{{inputValue}}"
|
||||||
|
bindinput="onInputChange"
|
||||||
|
bindconfirm="onInputConfirm" />
|
||||||
|
<view
|
||||||
|
wx:if="{{inputValue.length > 0}}"
|
||||||
|
class="cb-input-clear"
|
||||||
|
bindtap="onClearInput">
|
||||||
|
<toy-icon
|
||||||
|
name="close-bold"
|
||||||
|
size="36rpx"
|
||||||
|
color="#605b50" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="cb-field">
|
||||||
|
<view class="cb-field-header">
|
||||||
|
<text class="cb-field-label">已选文字</text>
|
||||||
|
<text class="cb-field-hint">点击文字可删除</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-x class="cb-word-scroll">
|
||||||
|
<view class="cb-word-list">
|
||||||
|
<view
|
||||||
|
wx:for="{{words}}"
|
||||||
|
wx:key="index"
|
||||||
|
class="cb-word-chip">
|
||||||
|
<text class="cb-word-chip__text">{{item}}</text>
|
||||||
|
<view
|
||||||
|
class="cb-word-chip__delete-hit"
|
||||||
|
data-index="{{index}}"
|
||||||
|
catchtap="deleteWordChip">
|
||||||
|
<toy-icon
|
||||||
|
name="close-bold"
|
||||||
|
size="24rpx"
|
||||||
|
color="rgba(96,91,80,0.5)" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="cb-word-add">
|
||||||
|
<view
|
||||||
|
class="cb-word-add-btn"
|
||||||
|
bindtap="openSelectWordPopup">
|
||||||
|
<toy-icon
|
||||||
|
name="plus"
|
||||||
|
size="36rpx"
|
||||||
|
color="rgba(96,91,80,0.4)" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-x class="cb-category-scroll">
|
||||||
|
<view class="cb-category-list">
|
||||||
|
<view
|
||||||
|
wx:for="{{categoryTags}}"
|
||||||
|
wx:key="categoryId"
|
||||||
|
class="cb-category-tag"
|
||||||
|
data-category-id="{{item.categoryId}}"
|
||||||
|
bindtap="onCategoryTap">
|
||||||
|
<text class="cb-category-tag-icon"
|
||||||
|
>{{item.icon}}</text
|
||||||
|
>
|
||||||
|
<text class="cb-category-tag-text"
|
||||||
|
>{{item.categoryName}}</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="cb-category-tag cb-category-tag--more"
|
||||||
|
bindtap="openSelectWordPopup">
|
||||||
|
<text class="cb-category-tag-icon">···</text>
|
||||||
|
<text class="cb-category-tag-text">更多分类</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<preview-footer-actions
|
||||||
|
disabled="{{!hasContent}}"
|
||||||
|
bind:primary="exportToPrint"
|
||||||
|
bind:secondary="onShare" />
|
||||||
|
|
||||||
|
<debug-publish-tools
|
||||||
|
wx:if="{{isDevEnv && hasContent}}"
|
||||||
|
id="debugPublishTools"
|
||||||
|
visible="{{debugPublishVisible}}"
|
||||||
|
loading="{{debugPublishLoading}}"
|
||||||
|
meta="{{debugPublishMeta}}"
|
||||||
|
bind:open="onOpenDebugPublish"
|
||||||
|
bind:close="onCloseDebugPublish"
|
||||||
|
bind:confirm="onConfirmDebugPublish" />
|
||||||
|
|
||||||
|
<word-picker
|
||||||
|
show="{{showSelectWordPopup}}"
|
||||||
|
selectedWords="{{selectedWords}}"
|
||||||
|
currentTab="{{pickerCurrentTab}}"
|
||||||
|
max="11"
|
||||||
|
bind:onClose="closeSelectWordPopup"
|
||||||
|
bind:onChange="onChangeWord" />
|
||||||
|
|
||||||
|
<share-guide-popup
|
||||||
|
show="{{showShareDialog}}"
|
||||||
|
bind:onClose="onCloseShareDialog"
|
||||||
|
bind:onShareSuccess="onShareSuccess" />
|
||||||
@@ -6,9 +6,8 @@
|
|||||||
round
|
round
|
||||||
closeable="{{true}}"
|
closeable="{{true}}"
|
||||||
close-on-click-overlay="{{true}}"
|
close-on-click-overlay="{{true}}"
|
||||||
safe-area-inset-bottom="{{false}}"
|
safe-area-inset-bottom="{{true}}"
|
||||||
position="bottom"
|
position="bottom"
|
||||||
custom-style="height:78%"
|
|
||||||
custom-class="word-picker">
|
custom-class="word-picker">
|
||||||
<view class="word-picker-content">
|
<view class="word-picker-content">
|
||||||
<view class="word-picker-title">请选择文字</view>
|
<view class="word-picker-title">请选择文字</view>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories';
|
import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories';
|
||||||
|
import { takePendingCategoryTabId } from '../../utils/index';
|
||||||
import { CATEGORY_DATA, type CategoryItem } from './category.data';
|
import { CATEGORY_DATA, type CategoryItem } from './category.data';
|
||||||
|
|
||||||
type CategoryTab = {
|
type CategoryTab = {
|
||||||
@@ -190,10 +191,10 @@ Page({
|
|||||||
onShow() {
|
onShow() {
|
||||||
_categoryData = null;
|
_categoryData = null;
|
||||||
this.setData({ displayItems: SKELETON_ITEMS, loading: true });
|
this.setData({ displayItems: SKELETON_ITEMS, loading: true });
|
||||||
void this.loadWorksheets();
|
void this.loadWorksheets(takePendingCategoryTabId() ?? undefined);
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadWorksheets() {
|
async loadWorksheets(overrideCategoryId?: string | null) {
|
||||||
try {
|
try {
|
||||||
const res = await wx.cloud.callFunction({
|
const res = await wx.cloud.callFunction({
|
||||||
name: 'worksheetsQuery',
|
name: 'worksheetsQuery',
|
||||||
@@ -207,7 +208,11 @@ Page({
|
|||||||
_categoryData = CATEGORY_DATA;
|
_categoryData = CATEGORY_DATA;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeId = this.data.activeCategoryId || this._defaultCategoryId;
|
const activeId =
|
||||||
|
overrideCategoryId != null &&
|
||||||
|
String(overrideCategoryId).trim() !== ''
|
||||||
|
? String(overrideCategoryId).trim()
|
||||||
|
: this.data.activeCategoryId || this._defaultCategoryId;
|
||||||
const items = getItemsByCategory(activeId);
|
const items = getItemsByCategory(activeId);
|
||||||
const filtered = filterByKeyword(items, this.data.searchKeyword);
|
const filtered = filterByKeyword(items, this.data.searchKeyword);
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
|
|||||||
id: 'math',
|
id: 'math',
|
||||||
title: '数学启蒙',
|
title: '数学启蒙',
|
||||||
subtitle: '数字认知、计数练习、加减运算、时钟与形状',
|
subtitle: '数字认知、计数练习、加减运算、时钟与形状',
|
||||||
morePath: '/pages/mathIndex/mathIndex',
|
morePath: '/pages/category/category?id=math',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'target-math-1',
|
id: 'target-math-1',
|
||||||
@@ -221,7 +221,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
|
|||||||
id: 'chinese',
|
id: 'chinese',
|
||||||
title: '语文启蒙',
|
title: '语文启蒙',
|
||||||
subtitle: '识字、练字、拼音、表达训练',
|
subtitle: '识字、练字、拼音、表达训练',
|
||||||
morePath: '/pages/index/index',
|
morePath: '/pages/category/category?id=chinese',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'target-chinese-1',
|
id: 'target-chinese-1',
|
||||||
@@ -277,7 +277,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
|
|||||||
id: 'english',
|
id: 'english',
|
||||||
title: '英语启蒙',
|
title: '英语启蒙',
|
||||||
subtitle: '字母学习、闪卡、描红与自然拼读',
|
subtitle: '字母学习、闪卡、描红与自然拼读',
|
||||||
morePath: '/englishPages/letterTracing/letterTracing',
|
morePath: '/pages/category/category?id=english',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'target-english-1',
|
id: 'target-english-1',
|
||||||
@@ -321,7 +321,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
|
|||||||
id: 'puzzle',
|
id: 'puzzle',
|
||||||
title: '益智游戏',
|
title: '益智游戏',
|
||||||
subtitle: '迷宫、找不同、连线、数独与控笔',
|
subtitle: '迷宫、找不同、连线、数独与控笔',
|
||||||
morePath: '/pages/focusIndex/focusIndex',
|
morePath: '/pages/category/category?id=puzzle',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'target-puzzle-1',
|
id: 'target-puzzle-1',
|
||||||
@@ -519,7 +519,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
id: 'math',
|
id: 'math',
|
||||||
title: '数感启蒙',
|
title: '数感启蒙',
|
||||||
subtitle: '优先展示现有高价值、高覆盖的数感与运算内容',
|
subtitle: '优先展示现有高价值、高覆盖的数感与运算内容',
|
||||||
morePath: '/pages/mathIndex/mathIndex',
|
morePath: '/pages/category/category?id=math',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'current-math-1',
|
id: 'current-math-1',
|
||||||
@@ -581,7 +581,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '7-8岁',
|
ageBand: '7-8岁',
|
||||||
difficulty: '挑战',
|
difficulty: '挑战',
|
||||||
icon: '✖️',
|
icon: '✖️',
|
||||||
previewImg: '/assets/entrancePicture/math/multiplication-table.png',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/math/multiplication-table.png',
|
||||||
path: '/mathPages/mathDraw/mathDraw?id=multiplication-table',
|
path: '/mathPages/mathDraw/mathDraw?id=multiplication-table',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'multiplicationTable',
|
source: 'multiplicationTable',
|
||||||
@@ -590,9 +591,9 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'chinese',
|
id: 'chinese',
|
||||||
title: '趣味识字',
|
title: '识字练字',
|
||||||
subtitle: '用现有识字与练字能力先覆盖核心语文场景',
|
subtitle: '用现有识字与练字能力先覆盖核心语文场景',
|
||||||
morePath: '/pages/index/index',
|
morePath: '/pages/category/category?id=chinese',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'current-chinese-1',
|
id: 'current-chinese-1',
|
||||||
@@ -636,7 +637,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
id: 'puzzle',
|
id: 'puzzle',
|
||||||
title: '益智游戏',
|
title: '益智游戏',
|
||||||
subtitle: '从现有专注力模块中抽取最符合产品重设计的内容',
|
subtitle: '从现有专注力模块中抽取最符合产品重设计的内容',
|
||||||
morePath: '/pages/focusIndex/focusIndex',
|
morePath: '/pages/category/category?id=puzzle',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'current-puzzle-1',
|
id: 'current-puzzle-1',
|
||||||
@@ -659,7 +660,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '4-6岁',
|
ageBand: '4-6岁',
|
||||||
difficulty: '基础',
|
difficulty: '基础',
|
||||||
icon: '🧵',
|
icon: '🧵',
|
||||||
previewImg: '/assets/entrancePicture/focus/match-connect.png',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/focus/match-connect.png',
|
||||||
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
|
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'match-connect',
|
source: 'match-connect',
|
||||||
@@ -672,7 +674,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '4-6岁',
|
ageBand: '4-6岁',
|
||||||
difficulty: '基础',
|
difficulty: '基础',
|
||||||
icon: '🌈',
|
icon: '🌈',
|
||||||
previewImg: '/assets/entrancePicture/focus/color-pattern.png',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/focus/color-pattern.png',
|
||||||
path: '/focusPages/focusDraw/focusDraw?id=color-pattern',
|
path: '/focusPages/focusDraw/focusDraw?id=color-pattern',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'color-pattern',
|
source: 'color-pattern',
|
||||||
@@ -685,7 +688,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '5-7岁',
|
ageBand: '5-7岁',
|
||||||
difficulty: '进阶',
|
difficulty: '进阶',
|
||||||
icon: '🧠',
|
icon: '🧠',
|
||||||
previewImg: '/assets/entrancePicture/focus/grid-reasoning.png',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/focus/grid-reasoning.png',
|
||||||
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
|
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'grid-reasoning',
|
source: 'grid-reasoning',
|
||||||
@@ -698,7 +702,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '4-7岁',
|
ageBand: '4-7岁',
|
||||||
difficulty: '进阶',
|
difficulty: '进阶',
|
||||||
icon: '🟨',
|
icon: '🟨',
|
||||||
previewImg: '/assets/entrancePicture/focus/grid-drawing-5x5.png',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/focus/grid-drawing-5x5.png',
|
||||||
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing',
|
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'grid-drawing',
|
source: 'grid-drawing',
|
||||||
@@ -709,7 +714,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
id: 'english',
|
id: 'english',
|
||||||
title: '英语启蒙',
|
title: '英语启蒙',
|
||||||
subtitle: '多种描红版式,认读与书写循序渐进',
|
subtitle: '多种描红版式,认读与书写循序渐进',
|
||||||
morePath: '/englishPages/letterTracing/letterTracing',
|
morePath: '/pages/category/category?id=english',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'current-english-1',
|
id: 'current-english-1',
|
||||||
@@ -719,7 +724,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '4-7岁',
|
ageBand: '4-7岁',
|
||||||
difficulty: '入门',
|
difficulty: '入门',
|
||||||
icon: '',
|
icon: '',
|
||||||
previewImg: '/assets/entrancePicture/english/letter-tracing-single.jpg',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/english/letter-tracing-single.jpg',
|
||||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single',
|
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'letter-tracing-single',
|
source: 'letter-tracing-single',
|
||||||
@@ -732,7 +738,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '4-7岁',
|
ageBand: '4-7岁',
|
||||||
difficulty: '入门',
|
difficulty: '入门',
|
||||||
icon: '',
|
icon: '',
|
||||||
previewImg: '/assets/entrancePicture/english/letter-tracing-upper-lower.jpg',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/english/letter-tracing-upper-lower.jpg',
|
||||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-upper-lower',
|
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-upper-lower',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'letter-tracing-upper-lower',
|
source: 'letter-tracing-upper-lower',
|
||||||
@@ -745,7 +752,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '4-7岁',
|
ageBand: '4-7岁',
|
||||||
difficulty: '入门',
|
difficulty: '入门',
|
||||||
icon: '',
|
icon: '',
|
||||||
previewImg: '/assets/entrancePicture/english/letter-tracing-case-pairing.jpg',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/english/letter-tracing-case-pairing.jpg',
|
||||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-case-pairing',
|
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-case-pairing',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'letter-tracing-case-pairing',
|
source: 'letter-tracing-case-pairing',
|
||||||
@@ -758,7 +766,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '4-7岁',
|
ageBand: '4-7岁',
|
||||||
difficulty: '入门',
|
difficulty: '入门',
|
||||||
icon: '',
|
icon: '',
|
||||||
previewImg: '/assets/entrancePicture/english/letter-tracing-two-column.jpg',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/english/letter-tracing-two-column.jpg',
|
||||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column',
|
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'letter-tracing-two-column',
|
source: 'letter-tracing-two-column',
|
||||||
@@ -771,7 +780,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '4-7岁',
|
ageBand: '4-7岁',
|
||||||
difficulty: '入门',
|
difficulty: '入门',
|
||||||
icon: '',
|
icon: '',
|
||||||
previewImg: '/assets/entrancePicture/english/letter-tracing-half.jpg',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/english/letter-tracing-half.jpg',
|
||||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-half',
|
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-half',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'letter-tracing-half',
|
source: 'letter-tracing-half',
|
||||||
@@ -784,7 +794,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
ageBand: '4-7岁',
|
ageBand: '4-7岁',
|
||||||
difficulty: '入门',
|
difficulty: '入门',
|
||||||
icon: '',
|
icon: '',
|
||||||
previewImg: '/assets/entrancePicture/english/letter-tracing-three.jpg',
|
previewImg:
|
||||||
|
'/assets/entrancePicture/english/letter-tracing-three.jpg',
|
||||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-three',
|
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-three',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'letter-tracing-three',
|
source: 'letter-tracing-three',
|
||||||
@@ -795,7 +806,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
|
|||||||
id: 'craft',
|
id: 'craft',
|
||||||
title: '创意手工',
|
title: '创意手工',
|
||||||
subtitle: '暂用最接近创意表达的现有能力承接展示位',
|
subtitle: '暂用最接近创意表达的现有能力承接展示位',
|
||||||
morePath: '/pages/index/index',
|
morePath: '/pages/category/category?id=craft',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'current-craft-1',
|
id: 'current-craft-1',
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { NAV_INNER_PX } from '../../utils/navMetrics';
|
import { NAV_INNER_PX } from '../../utils/navMetrics';
|
||||||
import { parseMiniProgramUrl } from '../../utils/index';
|
import {
|
||||||
|
parseMiniProgramUrl,
|
||||||
|
stashPendingCategoryTabId,
|
||||||
|
} from '../../utils/index';
|
||||||
import {
|
import {
|
||||||
HOME_CURRENT_CAPABILITY_DATA,
|
HOME_CURRENT_CAPABILITY_DATA,
|
||||||
type HomeDisplayItem,
|
type HomeDisplayItem,
|
||||||
@@ -78,8 +81,13 @@ function navigateByPath(path?: string, fallbackTitle?: string) {
|
|||||||
const { path: basePath, query } = parseMiniProgramUrl(path);
|
const { path: basePath, query } = parseMiniProgramUrl(path);
|
||||||
|
|
||||||
if (TAB_BAR_PATHS.has(basePath)) {
|
if (TAB_BAR_PATHS.has(basePath)) {
|
||||||
// tabBar 页面不支持携带 query/hash,统一跳到 basePath
|
if (
|
||||||
if (Object.keys(query).length > 0) {
|
basePath === '/pages/category/category' &&
|
||||||
|
query.id &&
|
||||||
|
String(query.id).trim() !== ''
|
||||||
|
) {
|
||||||
|
stashPendingCategoryTabId(String(query.id).trim());
|
||||||
|
} else if (Object.keys(query).length > 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'[navigateByPath] tabBar 不支持携带参数,已忽略 query:',
|
'[navigateByPath] tabBar 不支持携带参数,已忽略 query:',
|
||||||
basePath,
|
basePath,
|
||||||
@@ -245,7 +253,7 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onTapHotMore() {
|
onTapHotMore() {
|
||||||
wx.showToast({ title: '热门内容整理中', icon: 'none' });
|
navigateByPath('/pages/category/category?id=hot');
|
||||||
},
|
},
|
||||||
|
|
||||||
onTapTrackMore(e: WechatMiniprogram.TouchEvent) {
|
onTapTrackMore(e: WechatMiniprogram.TouchEvent) {
|
||||||
@@ -257,8 +265,7 @@ Page({
|
|||||||
onTapCard(e: WechatMiniprogram.CustomEvent) {
|
onTapCard(e: WechatMiniprogram.CustomEvent) {
|
||||||
const detail = (e.detail || {}) as { title?: string; path?: string };
|
const detail = (e.detail || {}) as { title?: string; path?: string };
|
||||||
const path =
|
const path =
|
||||||
detail.path ||
|
detail.path || (e.currentTarget.dataset.path as string | undefined);
|
||||||
(e.currentTarget.dataset.path as string | undefined);
|
|
||||||
const title =
|
const title =
|
||||||
detail.title ||
|
detail.title ||
|
||||||
(e.currentTarget.dataset.title as string | undefined);
|
(e.currentTarget.dataset.title as string | undefined);
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"navigationBarTitleText": "练字",
|
|
||||||
"navigationBarTextStyle": "black",
|
|
||||||
"navigationBarBackgroundColor": "#FFD719",
|
|
||||||
"homeButton": true,
|
|
||||||
"backgroundColor": "#F6F6F6",
|
|
||||||
"enablePullDownRefresh": false,
|
|
||||||
"usingComponents": {
|
|
||||||
"toy-button": "/toy/button-v2/button",
|
|
||||||
"van-dialog": "@vant/weapp/dialog/index",
|
|
||||||
"word-input": "/components/word-input/word-input",
|
|
||||||
"word-card": "/components/word-card/word-card",
|
|
||||||
"word-picker": "/components3.0/word-picker/word-picker",
|
|
||||||
"empty-state": "/components/empty-state/empty-state",
|
|
||||||
"share-guide-popup": "/components/share-guide-popup/share-guide-popup",
|
|
||||||
"add-to-miniprogram-tip": "/components/add-to-miniprogram-tip/add-to-miniprogram-tip"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
page {
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container {
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
padding: 0 24rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
height: 120rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 36rpx 24rpx;
|
|
||||||
box-shadow: 0 6rpx 8rpx rgba(0, 0, 0, 0.15);
|
|
||||||
margin: 30rpx 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.wrapper-title {
|
|
||||||
font-size: 32rpx;
|
|
||||||
color: #141414;
|
|
||||||
margin-bottom: 36rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.word-card-box {
|
|
||||||
padding: 34rpx 0;
|
|
||||||
|
|
||||||
.word-cards-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 40rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.word-input-box {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons-box {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20rpx;
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tianzige {
|
|
||||||
display: grid;
|
|
||||||
grid-auto-rows: min-content;
|
|
||||||
row-gap: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tzg-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(12, 1fr);
|
|
||||||
column-gap: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tzg-cell {
|
|
||||||
position: relative;
|
|
||||||
background: #fff;
|
|
||||||
border: 4rpx solid #333;
|
|
||||||
height: 120rpx;
|
|
||||||
border-radius: 6rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tzg-word {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
font-size: 72rpx;
|
|
||||||
color: #222;
|
|
||||||
font-family: 'Kaiti', 'KaiTi', 'STKaiti', 'FZKai-Z03S', serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mid-line {
|
|
||||||
position: absolute;
|
|
||||||
background: repeating-linear-gradient(to right,
|
|
||||||
rgba(0, 0, 0, 0.25),
|
|
||||||
rgba(0, 0, 0, 0.25) 2rpx,
|
|
||||||
transparent 2rpx,
|
|
||||||
transparent 6rpx);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mid-line.h {
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 50%;
|
|
||||||
height: 2rpx;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mid-line.v {
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
width: 2rpx;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-tip {
|
|
||||||
text-align: center;
|
|
||||||
color: #999;
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 400rpx;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
border: 2rpx dashed #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas-content {
|
|
||||||
max-width: 100%;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-btn-box {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 120rpx;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #fff;
|
|
||||||
box-shadow: 0px -3.9rpx 5.2rpx rgba(0, 0, 0, 0.15);
|
|
||||||
padding: 0 24rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 16rpx 16rpx 0 0;
|
|
||||||
}
|
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
import WordDrawService from '../../service/wordDrawService';
|
|
||||||
import { downloadPrint } from '../../utils/downloadPrint';
|
|
||||||
import { PAPER_SIZE } from '../../constants/colors';
|
|
||||||
import { getWordsSvgData } from '../../utils/getWordsSvgJson';
|
|
||||||
import { WORDS } from '../../core/data/words';
|
|
||||||
import { CharacterItem } from '../../types/characterType';
|
|
||||||
import tracker from '../../utils/tracker';
|
|
||||||
import { defaultShareConfig } from '../../config/config';
|
|
||||||
|
|
||||||
Page({
|
|
||||||
canvas: null as Canvas | null,
|
|
||||||
ctx: null as RenderingContext | null,
|
|
||||||
wordDrawService: null as WordDrawService | null,
|
|
||||||
svgWords: {} as Record<string, string[]>,
|
|
||||||
maxRow: 0 as number,
|
|
||||||
maxCol: 0 as number,
|
|
||||||
|
|
||||||
data: {
|
|
||||||
// 最终的汉字列表(合并输入和选择的汉字)
|
|
||||||
words: [] as string[],
|
|
||||||
// 来自文本框输入的汉字(每次输入替换)
|
|
||||||
inputWords: [] as string[],
|
|
||||||
// 来自弹窗选择的汉字(追加)
|
|
||||||
selectedWords: [] as string[],
|
|
||||||
// 输入框的值(用于清空)
|
|
||||||
inputValue: '',
|
|
||||||
showSelectWordPopup: false,
|
|
||||||
showColorPopup: false,
|
|
||||||
currentKey: 0,
|
|
||||||
currentColor: '',
|
|
||||||
boxWidth: 0,
|
|
||||||
boxHeight: 0,
|
|
||||||
showShareDialog: false, // 显示分享引导弹窗
|
|
||||||
},
|
|
||||||
|
|
||||||
async onLoad() {
|
|
||||||
await this.loadSvgWords();
|
|
||||||
const hasShowPracticeDemo =
|
|
||||||
wx.getStorageSync('hasShowPracticeDemo') || false;
|
|
||||||
if (!hasShowPracticeDemo) {
|
|
||||||
const demoWords = ['好', '好', '学', '习', '天', '天', '向', '上'];
|
|
||||||
this.setData(
|
|
||||||
{
|
|
||||||
words: demoWords,
|
|
||||||
inputWords: demoWords,
|
|
||||||
selectedWords: [],
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
wx.setStorageSync('hasShowPracticeDemo', true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await this.setCanvasBoxSize();
|
|
||||||
await this.initCanvas();
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadSvgWords() {
|
|
||||||
try {
|
|
||||||
wx.showToast({ title: '加载字体中...', icon: 'loading' });
|
|
||||||
// 使用带缓存的SVG汉字数据获取函数
|
|
||||||
const svgWordsData = await getWordsSvgData();
|
|
||||||
|
|
||||||
this.svgWords = svgWordsData;
|
|
||||||
wx.hideToast();
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'SVG汉字数据加载成功,共',
|
|
||||||
Object.keys(this.svgWords).length,
|
|
||||||
'个汉字',
|
|
||||||
);
|
|
||||||
|
|
||||||
const { words } = this.data as any;
|
|
||||||
if (words && words.length > 0) {
|
|
||||||
this.renderPracticeContent().catch(console.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载SVG汉字数据失败:', error);
|
|
||||||
wx.hideToast();
|
|
||||||
|
|
||||||
// 显示错误提示
|
|
||||||
wx.showModal({
|
|
||||||
title: '加载失败',
|
|
||||||
content: '无法加载汉字数据,请检查网络连接后重试',
|
|
||||||
showCancel: true,
|
|
||||||
cancelText: '取消',
|
|
||||||
confirmText: '重试',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
// 用户点击重试,重新加载
|
|
||||||
this.loadSvgWords();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 输入组件回调(所见即所得:替换文本框输入的汉字)
|
|
||||||
onConfirmInput(e: any) {
|
|
||||||
const value: string = (e && e.detail && e.detail.value) || '';
|
|
||||||
const text = (value || '').trim();
|
|
||||||
|
|
||||||
// 如果文本框为空,清空输入汉字
|
|
||||||
if (!text) {
|
|
||||||
const { selectedWords } = this.data as any;
|
|
||||||
const updatedWords = [...selectedWords];
|
|
||||||
this.setData(
|
|
||||||
{
|
|
||||||
inputWords: [],
|
|
||||||
words: updatedWords,
|
|
||||||
inputValue: '', // 同步清空输入框值
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.renderPracticeContent().catch(console.error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isChineseText(text)) {
|
|
||||||
wx.showToast({ title: '请输入汉字', icon: 'none' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 解析输入的汉字
|
|
||||||
const newInputChars = this.splitToSingleChars(text);
|
|
||||||
if (newInputChars.length === 0) {
|
|
||||||
wx.showToast({ title: '请输入有效汉字', icon: 'none' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { selectedWords } = this.data as any;
|
|
||||||
|
|
||||||
// 2. 检查是否超过限制(输入汉字 + 已选择的汉字)
|
|
||||||
if (newInputChars.length + selectedWords.length > 11) {
|
|
||||||
wx.showToast({
|
|
||||||
title: `最多只能添加 11 个字,当前已选择 ${selectedWords.length} 个,输入了 ${newInputChars.length} 个`,
|
|
||||||
icon: 'none',
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 替换输入汉字(不是追加),合并已选择的汉字
|
|
||||||
const updatedWords = [...newInputChars, ...selectedWords];
|
|
||||||
|
|
||||||
// 4. 更新数据并提示(同步更新 inputValue 保持一致)
|
|
||||||
this.setData(
|
|
||||||
{
|
|
||||||
inputWords: newInputChars,
|
|
||||||
words: updatedWords,
|
|
||||||
inputValue: text, // 同步输入框值
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.renderPracticeContent().catch(console.error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 5. 显示添加结果提示
|
|
||||||
wx.showToast({
|
|
||||||
title: `已更新输入汉字(${newInputChars.length} 个)`,
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// 选择面板
|
|
||||||
openSelectWordPopup() {
|
|
||||||
this.setData({ showSelectWordPopup: true });
|
|
||||||
},
|
|
||||||
closeSelectWordPopup() {
|
|
||||||
this.setData({ showSelectWordPopup: false });
|
|
||||||
},
|
|
||||||
onChangeWord(e: any) {
|
|
||||||
const newSelectedWords = e.detail.selectedWords as string[];
|
|
||||||
const { inputWords } = this.data as any;
|
|
||||||
|
|
||||||
// 检查是否超过限制
|
|
||||||
if (inputWords.length + newSelectedWords.length > 11) {
|
|
||||||
wx.showToast({
|
|
||||||
title: `最多只能添加 11 个字,当前输入 ${inputWords.length} 个,选择了 ${newSelectedWords.length} 个`,
|
|
||||||
icon: 'none',
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并输入汉字和选择的汉字(求并集)
|
|
||||||
const updatedWords = [...inputWords, ...newSelectedWords];
|
|
||||||
this.setData(
|
|
||||||
{
|
|
||||||
selectedWords: newSelectedWords,
|
|
||||||
words: updatedWords,
|
|
||||||
showSelectWordPopup: false,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (updatedWords && updatedWords.length > 0) {
|
|
||||||
this.renderPracticeContent().catch(console.error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
// 删除(需要判断删除的是输入汉字还是选择的汉字)
|
|
||||||
deleteWordCard(e: any) {
|
|
||||||
const { key } = e.detail;
|
|
||||||
const { inputWords, selectedWords } = this.data as any;
|
|
||||||
|
|
||||||
// 判断删除的是输入汉字还是选择的汉字
|
|
||||||
if (key < inputWords.length) {
|
|
||||||
// 删除的是输入汉字
|
|
||||||
const nextInputWords = inputWords.filter(
|
|
||||||
(_: string, index: number) => index !== key,
|
|
||||||
);
|
|
||||||
const nextWords = [...nextInputWords, ...selectedWords];
|
|
||||||
this.setData(
|
|
||||||
{
|
|
||||||
inputWords: nextInputWords,
|
|
||||||
words: nextWords,
|
|
||||||
},
|
|
||||||
() => this.renderPracticeContent().catch(console.error),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 删除的是选择的汉字
|
|
||||||
const selectedIndex = key - inputWords.length;
|
|
||||||
const nextSelectedWords = selectedWords.filter(
|
|
||||||
(_: string, index: number) => index !== selectedIndex,
|
|
||||||
);
|
|
||||||
const nextWords = [...inputWords, ...nextSelectedWords];
|
|
||||||
this.setData(
|
|
||||||
{
|
|
||||||
selectedWords: nextSelectedWords,
|
|
||||||
words: nextWords,
|
|
||||||
},
|
|
||||||
() => this.renderPracticeContent().catch(console.error),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 清空(清空所有来源的汉字:文本框、预览、弹窗选择)
|
|
||||||
clearWords() {
|
|
||||||
// 清空所有汉字数据
|
|
||||||
this.setData(
|
|
||||||
{
|
|
||||||
words: [],
|
|
||||||
inputWords: [],
|
|
||||||
selectedWords: [],
|
|
||||||
inputValue: '', // 清空输入框
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.renderPracticeContent().catch(console.error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 关闭弹窗(如果打开)
|
|
||||||
if (this.data.showSelectWordPopup) {
|
|
||||||
this.setData({ showSelectWordPopup: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 随机生成(取基础分类,过滤为可用SVG字,最多11个)
|
|
||||||
refreshWords() {
|
|
||||||
// 选取较简单的几个分类(如 0:基础独体字,1:生活高频字,2:自然与动作)
|
|
||||||
const candidate = WORDS.slice(0, 3).reduce(
|
|
||||||
(acc: string[], c: any) => acc.concat(c.words),
|
|
||||||
[] as string[],
|
|
||||||
);
|
|
||||||
// 去重
|
|
||||||
const unique = Array.from(new Set(candidate));
|
|
||||||
// 打乱
|
|
||||||
const shuffled = unique.sort(() => Math.random() - 0.5);
|
|
||||||
// 先取前 11 个
|
|
||||||
const picked = shuffled.slice(0, 10);
|
|
||||||
// 过滤为支持的SVG字
|
|
||||||
const supported = this.getSupportedWords(picked);
|
|
||||||
if (supported.length === 0) {
|
|
||||||
wx.showToast({
|
|
||||||
title: '随机到的字暂不支持,重试一下',
|
|
||||||
icon: 'none',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setData({ words: supported }, () =>
|
|
||||||
this.renderPracticeContent().catch(console.error),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
// 校验 & 分割
|
|
||||||
isChineseText(text: string): boolean {
|
|
||||||
const chineseRegex = /^[\u4e00-\u9fff]+$/;
|
|
||||||
return chineseRegex.test(text);
|
|
||||||
},
|
|
||||||
splitToSingleChars(text: string): string[] {
|
|
||||||
const chineseRegex = /[\u4e00-\u9fff]/g;
|
|
||||||
const matches = text.match(chineseRegex);
|
|
||||||
return matches || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
setCanvasBoxSize() {
|
|
||||||
const query = wx.createSelectorQuery();
|
|
||||||
query
|
|
||||||
.select('#canvasWrapper')
|
|
||||||
.boundingClientRect((rect) => {
|
|
||||||
if (rect) {
|
|
||||||
const { width, height } = PAPER_SIZE['A4'];
|
|
||||||
const boxWidth = rect.width;
|
|
||||||
const boxHeight = boxWidth / (width / height);
|
|
||||||
this.setData({ boxWidth, boxHeight });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.exec();
|
|
||||||
},
|
|
||||||
async initCanvas() {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
// 然后初始化canvas
|
|
||||||
wx.createSelectorQuery()
|
|
||||||
.select('#canvasContent')
|
|
||||||
.fields({
|
|
||||||
node: true,
|
|
||||||
size: true,
|
|
||||||
})
|
|
||||||
.exec(async (res) => {
|
|
||||||
if (res[0] && res[0].node) {
|
|
||||||
const canvas = res[0].node;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
this.canvas = canvas;
|
|
||||||
this.ctx = ctx;
|
|
||||||
this.wordDrawService = new WordDrawService(
|
|
||||||
canvas,
|
|
||||||
ctx,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 初始化时只绘制页眉和布局
|
|
||||||
await this.wordDrawService.drawLayout();
|
|
||||||
|
|
||||||
// 绘制完成后计算并缓存最大行列数(与实际绘制一致)
|
|
||||||
const { maxRow, maxCol } =
|
|
||||||
this.wordDrawService.getMaxGridLayout();
|
|
||||||
this.maxRow = maxRow;
|
|
||||||
this.maxCol = maxCol;
|
|
||||||
|
|
||||||
// 如果有汉字数据,绘制练字内容
|
|
||||||
const { words } = this.data as any;
|
|
||||||
if (words && words.length > 0) {
|
|
||||||
await this.renderPracticeContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// 绘制练字内容(只绘制田字格和汉字内容)
|
|
||||||
async renderPracticeContent() {
|
|
||||||
if (!this.canvas || !this.wordDrawService) {
|
|
||||||
await this.initCanvas();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { words } = this.data as any;
|
|
||||||
|
|
||||||
if (!words || words.length === 0) {
|
|
||||||
await this.wordDrawService.drawContentEmpty();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查汉字是否支持
|
|
||||||
const supportedWords = this.getSupportedWords(words);
|
|
||||||
if (supportedWords.length === 0) {
|
|
||||||
wx.showToast({ title: '暂不支持这些汉字', icon: 'none' });
|
|
||||||
await this.wordDrawService.drawContentEmpty();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用初始化阶段缓存的最大行列数;若未初始化则计算一次并缓存
|
|
||||||
let maxRow = this.maxRow;
|
|
||||||
let maxCol = this.maxCol;
|
|
||||||
if (!maxRow || !maxCol) {
|
|
||||||
const layout = this.wordDrawService!.getMaxGridLayout();
|
|
||||||
this.maxRow = layout.maxRow;
|
|
||||||
this.maxCol = layout.maxCol;
|
|
||||||
maxRow = layout.maxRow;
|
|
||||||
maxCol = layout.maxCol;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理布局和生成练字数据(基于实际最大行列数)
|
|
||||||
const characterData = this.processCharacterLayout(
|
|
||||||
supportedWords,
|
|
||||||
maxRow,
|
|
||||||
maxCol,
|
|
||||||
);
|
|
||||||
await this.wordDrawService.drawPracticeContent(characterData);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 检查汉字是否支持,返回支持的汉字列表 */
|
|
||||||
getSupportedWords(words: string[]): string[] {
|
|
||||||
return words.filter((word) => this.svgWords[word]);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 处理汉字布局,生成练字数据 */
|
|
||||||
processCharacterLayout(
|
|
||||||
words: string[],
|
|
||||||
maxRow: number,
|
|
||||||
maxCol: number,
|
|
||||||
): CharacterItem[] {
|
|
||||||
let rowIndex = 0;
|
|
||||||
const characterData: CharacterItem[] = [];
|
|
||||||
const boundaryWords: string[] = [];
|
|
||||||
|
|
||||||
// 按顺序处理每个汉字
|
|
||||||
words.forEach((word: string) => {
|
|
||||||
const strokes = this.svgWords[word];
|
|
||||||
const strokeCount = strokes.length;
|
|
||||||
const totalCells = 1 + strokeCount + 0; // 预览格 + 练习格 + 空白格
|
|
||||||
const totalRows = Math.ceil(totalCells / maxCol);
|
|
||||||
|
|
||||||
// 这里的意思是:把这一个汉字所占的行数(totalRows)累加到 rowIndex 上。
|
|
||||||
// 这样 rowIndex 就记录了当前已经排布了多少行,用于判断是否超出了最大允许行数(maxRow)。
|
|
||||||
rowIndex = rowIndex + totalRows;
|
|
||||||
|
|
||||||
if (rowIndex <= maxRow) {
|
|
||||||
characterData.push({ character: word, strokes });
|
|
||||||
} else {
|
|
||||||
boundaryWords.push(word);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// // 提示超出页面的汉字
|
|
||||||
// if (showToast && boundaryWords.length > 0) {
|
|
||||||
// wx.showToast({
|
|
||||||
// title: `练字贴已超过一页,部分汉字未包含在本次生成结果中`,
|
|
||||||
// icon: 'none'
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
return characterData;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 下载打印练字贴
|
|
||||||
async exportToPrint() {
|
|
||||||
await downloadPrint(this.canvas, {
|
|
||||||
errorToast: '请先生成练字贴',
|
|
||||||
trackerName: '练字贴',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// 分享功能
|
|
||||||
onShareAppMessage() {
|
|
||||||
// 上报分享埋点
|
|
||||||
tracker.reportShare('练字贴');
|
|
||||||
|
|
||||||
return {
|
|
||||||
...defaultShareConfig,
|
|
||||||
path: '/pages/copyBook/copyBook',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
onShareTimeline() {
|
|
||||||
// 上报分享埋点
|
|
||||||
tracker.reportShare('练字贴');
|
|
||||||
|
|
||||||
return {
|
|
||||||
...defaultShareConfig,
|
|
||||||
query: '/pages/copyBook/copyBook',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 分享成功回调(由组件触发) */
|
|
||||||
onShareSuccess() {
|
|
||||||
// 只关闭弹窗,不触发下载
|
|
||||||
this.setData({ showShareDialog: false });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<view class="page-container">
|
|
||||||
<view class="wrapper">
|
|
||||||
<text class="wrapper-title">自定义练字贴</text>
|
|
||||||
<view class="word-input-box">
|
|
||||||
<word-input value="{{inputValue}}" bind:onConfirm="onConfirmInput" style="width: 100%;"/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="action-buttons-box">
|
|
||||||
<toy-button
|
|
||||||
type="green"
|
|
||||||
bind:click="openSelectWordPopup"
|
|
||||||
height="80rpx"
|
|
||||||
width="410rpx"
|
|
||||||
icon="success"
|
|
||||||
icon-class-prefix="toy-icon">
|
|
||||||
选择文字
|
|
||||||
</toy-button>
|
|
||||||
<toy-button
|
|
||||||
disabled="{{words.length <= 0}}"
|
|
||||||
type="white"
|
|
||||||
bind:click="clearWords"
|
|
||||||
width="220rpx"
|
|
||||||
height="80rpx"
|
|
||||||
icon="clear"
|
|
||||||
icon-class-prefix="toy-icon">
|
|
||||||
清空
|
|
||||||
</toy-button>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="word-card-box">
|
|
||||||
<view class="word-cards-grid" wx:if="{{words.length > 0}}">
|
|
||||||
<word-card
|
|
||||||
disabledColor="{{true}}"
|
|
||||||
wx:key="index"
|
|
||||||
wx:for="{{words}}"
|
|
||||||
wx:for-item="word"
|
|
||||||
wx:for-index="index"
|
|
||||||
word="{{word}}"
|
|
||||||
key="{{index}}"
|
|
||||||
bind:onDelete="deleteWordCard"
|
|
||||||
bind:onColorTap="onColorTap" />
|
|
||||||
</view>
|
|
||||||
<empty-state
|
|
||||||
wx:if="{{!words.length}}"
|
|
||||||
title="还没有添加文字"
|
|
||||||
desc='请在上方输入文字或点击"选择文字"按钮,开始创建你的练字田字格吧!'
|
|
||||||
hint="提示:最多可以同时添加 11 个文字" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view id="previewWrapper" class="wrapper">
|
|
||||||
<text class="wrapper-title">预览练字田字格</text>
|
|
||||||
<view id="canvasWrapper" class="canvas-wrapper">
|
|
||||||
<canvas
|
|
||||||
type="2d"
|
|
||||||
id="canvasContent"
|
|
||||||
class="canvas-content"
|
|
||||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
|
||||||
</view>
|
|
||||||
<ad-custom unit-id="adunit-6f972b9b9bca0b9f"></ad-custom>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="bottom-btn-box">
|
|
||||||
<toy-button
|
|
||||||
openType="share"
|
|
||||||
type="green"
|
|
||||||
flat="{{true}}"
|
|
||||||
bind:click="onShareAppMessage"
|
|
||||||
width="220rpx"
|
|
||||||
height="80rpx"
|
|
||||||
icon="wechat"
|
|
||||||
icon-class-prefix="toy-icon">
|
|
||||||
分享
|
|
||||||
</toy-button>
|
|
||||||
<toy-button
|
|
||||||
type="primary"
|
|
||||||
flat="{{true}}"
|
|
||||||
bind:click="exportToPrint"
|
|
||||||
width="420rpx"
|
|
||||||
height="80rpx"
|
|
||||||
icon="download"
|
|
||||||
icon-class-prefix="toy-icon">
|
|
||||||
下载打印
|
|
||||||
</toy-button>
|
|
||||||
</view>
|
|
||||||
<word-picker
|
|
||||||
show="{{showSelectWordPopup}}"
|
|
||||||
selectedWords="{{selectedWords}}"
|
|
||||||
max="11"
|
|
||||||
bind:onClose="closeSelectWordPopup"
|
|
||||||
bind:onChange="onChangeWord" />
|
|
||||||
<share-guide-popup
|
|
||||||
show="{{showShareDialog}}"
|
|
||||||
bind:onClose="onCloseShareDialog"
|
|
||||||
bind:onShareSuccess="onShareSuccess" />
|
|
||||||
<add-to-miniprogram-tip />
|
|
||||||
|
|
||||||
<view class="empty"></view>
|
|
||||||
</view>
|
|
||||||
@@ -7,6 +7,13 @@ type DebugEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DEBUG_ENTRIES: DebugEntry[] = [
|
const DEBUG_ENTRIES: DebugEntry[] = [
|
||||||
|
{
|
||||||
|
id: 'home-content',
|
||||||
|
title: '首页内容管理',
|
||||||
|
subtitle: '编排首页推荐位、热门推荐和分类分区内容。',
|
||||||
|
icon: '🏠',
|
||||||
|
path: '/supportPages/homeContentManage/homeContentManage',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'category-manage',
|
id: 'category-manage',
|
||||||
title: '分类管理',
|
title: '分类管理',
|
||||||
@@ -28,13 +35,6 @@ const DEBUG_ENTRIES: DebugEntry[] = [
|
|||||||
icon: '📋',
|
icon: '📋',
|
||||||
path: '/supportPages/categoryContentManage/categoryContentManage',
|
path: '/supportPages/categoryContentManage/categoryContentManage',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'home-content',
|
|
||||||
title: '首页内容管理',
|
|
||||||
subtitle: '编排首页推荐位、热门推荐和分类分区内容。',
|
|
||||||
icon: '🏠',
|
|
||||||
path: '/supportPages/homeContentManage/homeContentManage',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
|
|||||||
@@ -75,3 +75,28 @@ export function parseMiniProgramUrl(input: string) {
|
|||||||
|
|
||||||
return { path: pathPart, query };
|
return { path: pathPart, query };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** tabBar 无法用 switchTab 带 query:用 globalData 传一次性意图,避免 storage 磁盘同步读写 */
|
||||||
|
export function stashPendingCategoryTabId(id: string) {
|
||||||
|
const trimmed = String(id ?? '').trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
try {
|
||||||
|
getApp<IAppOption>().globalData.pendingCategoryTabId = trimmed;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 若无待处理 id 则返回 null;返回后清空,避免残留在全局 */
|
||||||
|
export function takePendingCategoryTabId(): string | null {
|
||||||
|
try {
|
||||||
|
const app = getApp<IAppOption>();
|
||||||
|
const raw = app.globalData.pendingCategoryTabId;
|
||||||
|
if (raw == null || String(raw).trim() === '') return null;
|
||||||
|
const v = String(raw).trim();
|
||||||
|
delete app.globalData.pendingCategoryTabId;
|
||||||
|
return v;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,22 +24,22 @@
|
|||||||
"miniprogram": {
|
"miniprogram": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"name": "chinesePages/wordColoring/wordColoring",
|
"name": "chinesePages/handwritingSheet/handwritingSheet",
|
||||||
"pathName": "chinesePages/wordColoring/wordColoring",
|
"pathName": "chinesePages/handwritingSheet/handwritingSheet",
|
||||||
"query": "",
|
"query": "",
|
||||||
"scene": null,
|
"scene": null,
|
||||||
"launchMode": "default"
|
"launchMode": "default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pages/profile/profile",
|
"name": "chinesePages/wordColoring/wordColoring",
|
||||||
"pathName": "pages/profile/profile",
|
"pathName": "chinesePages/wordColoring/wordColoring",
|
||||||
"query": "",
|
"query": "",
|
||||||
"launchMode": "default",
|
"launchMode": "default",
|
||||||
"scene": null
|
"scene": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "supportPages/index/index",
|
"name": "pages/profile/profile",
|
||||||
"pathName": "supportPages/index/index",
|
"pathName": "pages/profile/profile",
|
||||||
"query": "",
|
"query": "",
|
||||||
"launchMode": "default",
|
"launchMode": "default",
|
||||||
"scene": null
|
"scene": null
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
---
|
||||||
|
name: create-draw-page
|
||||||
|
description: >-
|
||||||
|
基于现有 draw 页面模板创建新的绘制页(worksheet page)。
|
||||||
|
当用户说「基于现有的draw页面来开发新页面」「基于draw页面开发新页面」
|
||||||
|
「新建绘制页」「创建一个新的draw页面」「新增一个worksheet页」时触发。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 创建新的绘制页(Draw Page)
|
||||||
|
|
||||||
|
基于 `englishPages/letterTracing` 模板,快速搭建一个新的 Canvas 绘制页。
|
||||||
|
|
||||||
|
## 模板参考
|
||||||
|
|
||||||
|
以 `miniprogram/englishPages/letterTracing` 为标准模板,新页面的文件结构与代码风格应与之对齐。创建前先阅读模板页的以下文件:
|
||||||
|
|
||||||
|
- `letterTracing.ts` — 页面逻辑
|
||||||
|
- `letterTracing.wxml` — 页面模板
|
||||||
|
- `letterTracing.less` — 页面样式
|
||||||
|
- `letterTracing.json` — 组件注册
|
||||||
|
- `letterTracing.config.ts` — worksheet 元数据配置
|
||||||
|
|
||||||
|
## 新页面必备文件
|
||||||
|
|
||||||
|
假设新页面名为 `newPage`,需要创建以下文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
newPage/
|
||||||
|
├── newPage.ts # 页面逻辑(使用 createPage)
|
||||||
|
├── newPage.wxml # 页面模板
|
||||||
|
├── newPage.less # 页面样式
|
||||||
|
├── newPage.json # 组件注册
|
||||||
|
├── newPage.config.ts # worksheet 元数据
|
||||||
|
└── draw/ # 绘制服务
|
||||||
|
└── newPageDraw.ts # 继承 BaseDrawService
|
||||||
|
```
|
||||||
|
|
||||||
|
## 必备要求
|
||||||
|
|
||||||
|
### 1. xxx.config.ts — worksheet 元数据配置
|
||||||
|
|
||||||
|
每个绘制页**必须**有对应的 `xxx.config.ts` 文件。参考 `letterTracing.config.ts` 的结构,至少包含:
|
||||||
|
|
||||||
|
**worksheet 定义数组**(核心):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface XxxWorksheetDefinition {
|
||||||
|
id: string; // 唯一标识,格式:{pageName}-{variant}
|
||||||
|
icon: string; // 图标名(使用 toy-icon 支持的图标)
|
||||||
|
title: string; // 标题(简短,4-6 字)
|
||||||
|
subtitle: string; // 副标题(描述该模式的特点)
|
||||||
|
ageMin: number; // 最小适用年龄
|
||||||
|
ageMax: number; // 最大适用年龄
|
||||||
|
difficulty: 1 | 2 | 3 | 4; // 难度等级
|
||||||
|
tags: string[]; // 标签数组
|
||||||
|
sortOrder: number; // 排序权重
|
||||||
|
}
|
||||||
|
|
||||||
|
export const XXX_WORKSHEET_DEFINITIONS = [
|
||||||
|
{
|
||||||
|
id: 'xxx-default',
|
||||||
|
icon: 'icon-name',
|
||||||
|
title: '默认模式',
|
||||||
|
subtitle: '模式描述',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 7,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['标签1', '标签2'],
|
||||||
|
sortOrder: 50,
|
||||||
|
},
|
||||||
|
// ... 更多模式
|
||||||
|
] as const satisfies ReadonlyArray<XxxWorksheetDefinition>;
|
||||||
|
```
|
||||||
|
|
||||||
|
**必须导出的辅助函数**(参照 `letterTracing.config.ts`):
|
||||||
|
|
||||||
|
- `getModeInfo(id)` — 返回 `{ title, desc }` 用于 `pageInfoLookup`
|
||||||
|
- `isValidMode(id)` — 校验 id 是否合法
|
||||||
|
- `getPublishMetaByMode(id)` — 生成 `DebugPublishMeta`,使用 `inferGradeFromAge` 推断年级
|
||||||
|
- 模式选项常量(如 `XXX_MODE_OPTIONS`)
|
||||||
|
|
||||||
|
`getPublishMetaByMode` 中的 `category` 和 `subcategory` 需根据页面所属学科正确设置:
|
||||||
|
- `category`:`math` | `puzzle` | `pinyin` | `chinese` | `english` | `craft`
|
||||||
|
- `subcategory`:页面功能简称,如 `letter-tracing`、`word-coloring`
|
||||||
|
|
||||||
|
### 2. 必须使用的组件
|
||||||
|
|
||||||
|
每个绘制页**必须**在 `.json` 和 `.wxml` 中注册并使用以下组件:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"usingComponents": {
|
||||||
|
"nav-bar": "../../components3.0/nav-bar/nav-bar",
|
||||||
|
"preview-card": "../../components3.0/preview-card/preview-card",
|
||||||
|
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
||||||
|
"debug-publish-tools": "../../components3.0/debug-publish-tools/debug-publish-tools",
|
||||||
|
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||||
|
"toy-icon": "../../toy/icon/icon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 路径中的 `../../` 需根据新页面实际所在目录调整层级。
|
||||||
|
|
||||||
|
**wxml 中的基本布局结构**:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<nav-bar title="页面标题" />
|
||||||
|
|
||||||
|
<view class="page-wrapper">
|
||||||
|
<view class="page-main">
|
||||||
|
<preview-card
|
||||||
|
id="previewCard"
|
||||||
|
showRefresh="{{false}}"
|
||||||
|
showFavorite="{{true}}"
|
||||||
|
favorited="{{isPreviewFavorite}}"
|
||||||
|
bind:canvas-ready="onCanvasReady"
|
||||||
|
bind:favorite="onPreviewFavorite" />
|
||||||
|
|
||||||
|
<!-- 页面特有的交互区域放这里 -->
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<preview-footer-actions
|
||||||
|
disabled="{{!hasContent}}"
|
||||||
|
bind:primary="exportToPrint"
|
||||||
|
bind:secondary="onShare" />
|
||||||
|
|
||||||
|
<debug-publish-tools
|
||||||
|
wx:if="{{isDevEnv && hasContent}}"
|
||||||
|
id="debugPublishTools"
|
||||||
|
visible="{{debugPublishVisible}}"
|
||||||
|
loading="{{debugPublishLoading}}"
|
||||||
|
meta="{{debugPublishMeta}}"
|
||||||
|
bind:open="onOpenDebugPublish"
|
||||||
|
bind:close="onCloseDebugPublish"
|
||||||
|
bind:confirm="onConfirmDebugPublish" />
|
||||||
|
|
||||||
|
<share-guide-popup
|
||||||
|
show="{{showShareDialog}}"
|
||||||
|
bind:onClose="onCloseShareDialog"
|
||||||
|
bind:onShareSuccess="onShareSuccess" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 图标使用
|
||||||
|
|
||||||
|
图标**优先**使用 `toy-icon` 组件(路径 `../../toy/icon/icon`):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<toy-icon name="refresh" size="40rpx" color="#605b50" />
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 `toy-icon` 没有所需图标,可直接引入 iconfont:
|
||||||
|
|
||||||
|
```less
|
||||||
|
@import '../../assets/fonts/iconfont.less';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 使用 createPage 创建页面
|
||||||
|
|
||||||
|
页面**必须**使用 `base/pageMixin.ts` 中的 `createPage` 函数创建,不能直接调用 `Page()`。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createPage, type CanvasDataState } from '../../base/pageMixin';
|
||||||
|
|
||||||
|
createPage(
|
||||||
|
{
|
||||||
|
canvas: null as Canvas | null,
|
||||||
|
ctx: null as RenderingContext | null,
|
||||||
|
boxHeight: 0,
|
||||||
|
boxWidth: 0,
|
||||||
|
drawService: null as XxxDraw | null,
|
||||||
|
|
||||||
|
data: {
|
||||||
|
pageTitle: '页面标题',
|
||||||
|
functionId: '',
|
||||||
|
hasContent: false,
|
||||||
|
showShareDialog: false,
|
||||||
|
// ... 页面特有数据
|
||||||
|
isPreviewFavorite: false,
|
||||||
|
isDevEnv: false,
|
||||||
|
debugPublishVisible: false,
|
||||||
|
debugPublishLoading: false,
|
||||||
|
debugPublishMeta: null,
|
||||||
|
} as unknown as PageData,
|
||||||
|
|
||||||
|
onLoad(options: { id?: string }) {
|
||||||
|
// 1. syncDebugPublishEnv()
|
||||||
|
// 2. 解析路由参数、应用默认模式
|
||||||
|
// 3. 加载收藏状态
|
||||||
|
},
|
||||||
|
|
||||||
|
onCanvasReady(e: WechatMiniprogram.CustomEvent) {
|
||||||
|
this.initCanvasFromComponent(e.detail, {
|
||||||
|
createDrawService: (canvas, ctx, opts) =>
|
||||||
|
new XxxDraw(canvas, ctx, opts),
|
||||||
|
onCanvasReady: () => this.drawCanvas(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async drawCanvas() {
|
||||||
|
// 生成数据 → 调用 drawService 渲染
|
||||||
|
},
|
||||||
|
|
||||||
|
getPublishMeta() {
|
||||||
|
// 调用 config 中的 getPublishMetaByMode
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shareConfig: defaultShareConfig,
|
||||||
|
pageInfoLookup,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
`createPage` 自动混入的公共方法包括:
|
||||||
|
- `initCanvas` / `initCanvasFromComponent` — Canvas 初始化
|
||||||
|
- `exportToPrint` — 导出打印
|
||||||
|
- `onShareAppMessage` / `onShareTimeline` — 分享
|
||||||
|
- `onCloseShareDialog` / `onShareSuccess` — 分享弹窗
|
||||||
|
- `syncDebugPublishEnv` / `onOpenDebugPublish` / `onCloseDebugPublish` / `onConfirmDebugPublish` — 调试发布
|
||||||
|
- `initPageInfo` — 页面标题初始化
|
||||||
|
- `getShareOptions` — 分享路径
|
||||||
|
|
||||||
|
**不要重复实现**这些方法,直接使用即可。
|
||||||
|
|
||||||
|
### 5. 主题色与设计风格
|
||||||
|
|
||||||
|
保持与模板页一致的暖色调主题,除非用户另行指定 UI 稿:
|
||||||
|
|
||||||
|
```less
|
||||||
|
@import '../../style/theme.less';
|
||||||
|
|
||||||
|
page {
|
||||||
|
background-color: @bg-page;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
关键设计 token(来自 `theme.less`):
|
||||||
|
- 页面背景:`@bg-page`
|
||||||
|
- 卡片背景:`@bg-card`
|
||||||
|
- 品牌色/选中态:`@brand`
|
||||||
|
- 主标题色:`#6d3b00`
|
||||||
|
- 正文色:`@text-secondary`
|
||||||
|
- 选中文字色:`@text-selected-btn`
|
||||||
|
- 页面左右内边距:`@page-padding-x`
|
||||||
|
- 底部安全区留白:`calc(200rpx + env(safe-area-inset-bottom))`
|
||||||
|
- 圆角:`24rpx`(卡片)/ `32rpx`(按钮/chip)
|
||||||
|
|
||||||
|
## 开发检查清单
|
||||||
|
|
||||||
|
创建完成后逐项确认:
|
||||||
|
|
||||||
|
- [ ] 有 `xxx.config.ts`,包含 worksheet 定义数组和辅助函数
|
||||||
|
- [ ] `.json` 中注册了全部 5 个必备组件 + `toy-icon`
|
||||||
|
- [ ] `.wxml` 中包含 `nav-bar`、`preview-card`、`preview-footer-actions`、`debug-publish-tools`、`share-guide-popup`
|
||||||
|
- [ ] `.ts` 中使用 `createPage()` 创建页面
|
||||||
|
- [ ] `.less` 中引入 `theme.less` 并使用主题变量
|
||||||
|
- [ ] 有 `draw/` 目录放置绘制服务
|
||||||
|
- [ ] `onLoad` 中调用了 `syncDebugPublishEnv()`
|
||||||
|
- [ ] 实现了 `getPublishMeta()` 方法
|
||||||
|
- [ ] 在 `app.json` 中注册了新页面路径
|
||||||
+230
-170
@@ -1,13 +1,19 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html class="light" lang="zh-CN"><head>
|
<html class="light" lang="zh-CN">
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
<head>
|
||||||
<title>分类 - Doodle Mini</title>
|
<meta charset="utf-8" />
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800;900&display=swap" rel="stylesheet"/>
|
<title>分类 - Doodle Mini</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<script id="tailwind-config">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800;900&display=swap"
|
||||||
|
rel="stylesheet" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
||||||
|
rel="stylesheet" />
|
||||||
|
<script id="tailwind-config">
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
darkMode: "class",
|
darkMode: "class",
|
||||||
theme: {
|
theme: {
|
||||||
@@ -81,168 +87,222 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: 'Plus Jakarta Sans', sans-serif; }
|
body {
|
||||||
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||||
.icon-fill { font-variation-settings: 'FILL' 1; }
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-fill {
|
||||||
|
font-variation-settings: 'FILL' 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
min-height: max(884px, 100dvh);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-background text-on-surface h-screen flex flex-col overflow-hidden">
|
<body class="bg-background text-on-surface h-screen flex flex-col overflow-hidden">
|
||||||
<!-- TopAppBar equivalent for search -->
|
<!-- TopAppBar equivalent for search -->
|
||||||
<header class="bg-surface-container-low/80 backdrop-blur-xl z-50 px-4 py-3 flex-none sticky top-0">
|
<header class="bg-surface-container-low/80 backdrop-blur-xl z-50 px-4 py-3 flex-none sticky top-0">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="flex-1 bg-surface-container-highest rounded-full flex items-center px-4 py-2">
|
<div class="flex-1 bg-surface-container-highest rounded-full flex items-center px-4 py-2">
|
||||||
<span class="material-symbols-outlined text-outline mr-2">search</span>
|
<span class="material-symbols-outlined text-outline mr-2">search</span>
|
||||||
<input class="bg-transparent border-none focus:ring-0 text-body-lg text-on-surface placeholder-on-surface-variant w-full p-0" placeholder="搜索练习纸..." type="text"/>
|
<input
|
||||||
</div>
|
class="bg-transparent border-none focus:ring-0 text-body-lg text-on-surface placeholder-on-surface-variant w-full p-0"
|
||||||
</div>
|
placeholder="搜索练习纸..." type="text" />
|
||||||
</header>
|
</div>
|
||||||
<!-- Main Content Area -->
|
</div>
|
||||||
<main class="flex-1 flex overflow-hidden">
|
</header>
|
||||||
<!-- Left Sidebar (Categories) -->
|
<!-- Main Content Area -->
|
||||||
<aside class="w-20 md:w-40 bg-surface-container-low flex flex-col overflow-y-auto hide-scrollbar py-2 px-2 gap-1 flex-none rounded-r-[2rem]">
|
<main class="flex-1 flex overflow-hidden">
|
||||||
<button class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
<!-- Left Sidebar (Categories) -->
|
||||||
<span class="text-xs md:text-sm font-medium">全部</span>
|
<aside
|
||||||
</button>
|
class="w-20 md:w-40 bg-surface-container-low flex flex-col overflow-y-auto hide-scrollbar py-2 px-2 gap-1 flex-none rounded-r-[2rem]">
|
||||||
<button class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full bg-primary-container text-on-primary-container font-bold shadow-sm scale-105 transition-all">
|
<button
|
||||||
<span class="text-xs md:text-sm">数感启蒙</span>
|
class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
||||||
</button>
|
<span class="text-xs md:text-sm font-medium">全部</span>
|
||||||
<button class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
</button>
|
||||||
<span class="text-xs md:text-sm font-medium">汉语拼音</span>
|
<button
|
||||||
</button>
|
class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full bg-primary-container text-on-primary-container font-bold shadow-sm scale-105 transition-all">
|
||||||
<button class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
<span class="text-xs md:text-sm">数感启蒙</span>
|
||||||
<span class="text-xs md:text-sm font-medium">专注力游戏</span>
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
||||||
<span class="text-xs md:text-sm font-medium">趣味识字</span>
|
<span class="text-xs md:text-sm font-medium">汉语拼音</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
<button
|
||||||
<span class="text-xs md:text-sm font-medium">英语启蒙</span>
|
class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
||||||
</button>
|
<span class="text-xs md:text-sm font-medium">专注力游戏</span>
|
||||||
<button class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
</button>
|
||||||
<span class="text-xs md:text-sm font-medium">创意手工</span>
|
<button
|
||||||
</button>
|
class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
||||||
</aside>
|
<span class="text-xs md:text-sm font-medium">趣味识字</span>
|
||||||
<!-- Right Content List -->
|
</button>
|
||||||
<section class="flex-1 overflow-y-auto p-2 md:p-4 bg-background">
|
<button
|
||||||
<div class="flex flex-col gap-2 pb-24">
|
class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
||||||
<!-- Card 1 -->
|
<span class="text-xs md:text-sm font-medium">英语启蒙</span>
|
||||||
<article class="bg-surface-container-lowest rounded-[20px] p-2 flex gap-3 shadow-sm border border-surface-container-high hover:shadow-md transition-shadow">
|
</button>
|
||||||
<div class="w-24 shrink-0 rounded-[20px] overflow-hidden bg-surface-container relative">
|
<button
|
||||||
<img alt="Colorful counting beads on a wooden frame, bright sunny lighting, educational" class="w-full h-full object-cover" data-alt="Colorful counting beads on a wooden frame, bright sunny lighting, educational" src="https://lh3.googleusercontent.com/aida-public/AB6AXuC9YDdXdSqQute8UWn7lNaZDuRi4z_BD5HYyM2GKRikRpUs7G-CuwDgAhVqH_6tqN7icRuAhrtmrxbGtrv4T65CdT9ItF68N-9SgNeU1mFQiliih1bt7Cc-nKeyJW2_bgZCg-FOZsl_1S_sD25qt7qBhPsmXT8X3ahauX3VpI3-84vfatabv2wk_TUNOeSt3SdbIpmCHvZR1RqKkyrvaAxPxKIO4uY5v0BhccLxE5L-L96DyM5JAcxxQKRXtnFY2qfvBWMqPT3LC_E"/>
|
class="flex flex-col md:flex-row items-center gap-1 md:gap-3 px-2 md:px-4 py-2 rounded-full text-on-surface-variant hover:bg-surface-container transition-colors">
|
||||||
</div>
|
<span class="text-xs md:text-sm font-medium">创意手工</span>
|
||||||
<div class="flex flex-col justify-between flex-1 overflow-hidden py-1">
|
</button>
|
||||||
<div>
|
</aside>
|
||||||
<h3 class="text-base font-bold text-on-surface tracking-tight mb-1 truncate">认识基础图形</h3>
|
<!-- Right Content List -->
|
||||||
<p class="text-on-surface-variant text-xs mb-2 truncate">通过趣味拼贴,让孩子初步感知圆形、方形与三角形的美妙世界。</p>
|
<section class="flex-1 overflow-y-auto p-2 md:p-4 bg-background">
|
||||||
<div class="flex gap-2 mb-1">
|
<div class="flex flex-col gap-2 pb-24">
|
||||||
<span class="px-2 py-0.5 bg-primary-container text-on-primary-container rounded-full text-[10px] font-medium whitespace-nowrap">3-6岁</span>
|
<!-- Card 1 -->
|
||||||
<span class="px-2 py-0.5 bg-surface-container-high rounded-full text-[10px] font-medium text-on-surface-variant whitespace-nowrap">入门</span>
|
<article
|
||||||
</div>
|
class="bg-surface-container-lowest rounded-[20px] p-2 flex gap-3 shadow-sm border border-surface-container-high hover:shadow-md transition-shadow">
|
||||||
</div>
|
<div class="w-24 shrink-0 rounded-[20px] overflow-hidden bg-surface-container relative">
|
||||||
<div class="flex items-center justify-between text-[10px] text-outline mt-1">
|
<img alt="Colorful counting beads on a wooden frame, bright sunny lighting, educational"
|
||||||
<div class="flex items-center gap-2">
|
class="w-full h-full object-cover"
|
||||||
<span class="flex items-center gap-0.5"><span class="material-symbols-outlined text-[14px]">favorite</span> 456</span>
|
data-alt="Colorful counting beads on a wooden frame, bright sunny lighting, educational"
|
||||||
<span class="flex items-center gap-0.5"><span class="material-symbols-outlined text-[14px]">download</span> 89</span>
|
src="https://lh3.googleusercontent.com/aida-public/AB6AXuC9YDdXdSqQute8UWn7lNaZDuRi4z_BD5HYyM2GKRikRpUs7G-CuwDgAhVqH_6tqN7icRuAhrtmrxbGtrv4T65CdT9ItF68N-9SgNeU1mFQiliih1bt7Cc-nKeyJW2_bgZCg-FOZsl_1S_sD25qt7qBhPsmXT8X3ahauX3VpI3-84vfatabv2wk_TUNOeSt3SdbIpmCHvZR1RqKkyrvaAxPxKIO4uY5v0BhccLxE5L-L96DyM5JAcxxQKRXtnFY2qfvBWMqPT3LC_E" />
|
||||||
</div>
|
</div>
|
||||||
<span>2024-05-20</span>
|
<div class="flex flex-col justify-between flex-1 overflow-hidden py-1">
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<h3 class="text-base font-bold text-on-surface tracking-tight mb-1 truncate">认识基础图形</h3>
|
||||||
</article>
|
<p class="text-on-surface-variant text-xs mb-2 truncate">通过趣味拼贴,让孩子初步感知圆形、方形与三角形的美妙世界。</p>
|
||||||
<!-- Card 2 -->
|
<div class="flex gap-2 mb-1">
|
||||||
<article class="bg-surface-container-lowest rounded-[20px] p-2 flex gap-3 shadow-sm border border-surface-container-high hover:shadow-md transition-shadow">
|
<span
|
||||||
<div class="w-24 shrink-0 rounded-[20px] overflow-hidden bg-surface-container relative">
|
class="px-2 py-0.5 bg-primary-container text-on-primary-container rounded-full text-[10px] font-medium whitespace-nowrap">3-6岁</span>
|
||||||
<img alt="Hand drawn numbers on colorful paper cutouts, messy desk, creative warm light" class="w-full h-full object-cover" data-alt="Hand drawn numbers on colorful paper cutouts, messy desk, creative warm light" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCyTKBE2-6CRjamB6TIUQBymqI1pU6sGFiSLL3meuf0rC8-B-37beMuXbFxJN4bhoA7mZqqYM9QzUAiu-iQmDBLhaayZkJGc0QfTTAAte3Mp7gNwGMy9UDJah-oiJcuY6UUTp0tOTeT-A1Nley0IwNVpeQ2ED1p-NwFwYPojmDcdsfbYRcj5GmnGzR_au03NR8AqFe4o1MtgjRrw4s3_U89bYBmBcpzBTo9BEMsltkwgKbcboX7j6nh2iyyGbCqwtJxSjgzoqbquhA"/>
|
<span
|
||||||
</div>
|
class="px-2 py-0.5 bg-surface-container-high rounded-full text-[10px] font-medium text-on-surface-variant whitespace-nowrap">入门</span>
|
||||||
<div class="flex flex-col justify-between flex-1 overflow-hidden py-1">
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<h3 class="text-base font-bold text-on-surface tracking-tight mb-1 truncate">数字连连看</h3>
|
<div class="flex items-center justify-between text-[10px] text-outline mt-1">
|
||||||
<p class="text-on-surface-variant text-xs mb-2 truncate">跟随小兔子走迷宫,在游戏中掌握1-10的数字顺序。</p>
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex gap-2 mb-1">
|
<span class="flex items-center gap-0.5"><span
|
||||||
<span class="px-2 py-0.5 bg-tertiary-container text-on-tertiary-container rounded-full text-[10px] font-medium whitespace-nowrap">4-5岁</span>
|
class="material-symbols-outlined text-[14px]">favorite</span> 456</span>
|
||||||
<span class="px-2 py-0.5 bg-surface-container-high rounded-full text-[10px] font-medium text-on-surface-variant whitespace-nowrap">进阶</span>
|
<span class="flex items-center gap-0.5"><span
|
||||||
</div>
|
class="material-symbols-outlined text-[14px]">download</span> 89</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-[10px] text-outline mt-1">
|
<span>2024-05-20</span>
|
||||||
<div class="flex items-center gap-2">
|
</div>
|
||||||
<span class="flex items-center gap-0.5"><span class="material-symbols-outlined text-[14px]">favorite</span> 230</span>
|
</div>
|
||||||
<span class="flex items-center gap-0.5"><span class="material-symbols-outlined text-[14px]">download</span> 45</span>
|
</article>
|
||||||
</div>
|
<!-- Card 2 -->
|
||||||
<span>2024-05-18</span>
|
<article
|
||||||
</div>
|
class="bg-surface-container-lowest rounded-[20px] p-2 flex gap-3 shadow-sm border border-surface-container-high hover:shadow-md transition-shadow">
|
||||||
</div>
|
<div class="w-24 shrink-0 rounded-[20px] overflow-hidden bg-surface-container relative">
|
||||||
</article>
|
<img alt="Hand drawn numbers on colorful paper cutouts, messy desk, creative warm light"
|
||||||
<!-- Card 3 -->
|
class="w-full h-full object-cover"
|
||||||
<article class="bg-surface-container-lowest rounded-[20px] p-2 flex gap-3 shadow-sm border border-surface-container-high hover:shadow-md transition-shadow">
|
data-alt="Hand drawn numbers on colorful paper cutouts, messy desk, creative warm light"
|
||||||
<div class="w-24 shrink-0 rounded-[20px] overflow-hidden bg-surface-container relative">
|
src="https://lh3.googleusercontent.com/aida-public/AB6AXuCyTKBE2-6CRjamB6TIUQBymqI1pU6sGFiSLL3meuf0rC8-B-37beMuXbFxJN4bhoA7mZqqYM9QzUAiu-iQmDBLhaayZkJGc0QfTTAAte3Mp7gNwGMy9UDJah-oiJcuY6UUTp0tOTeT-A1Nley0IwNVpeQ2ED1p-NwFwYPojmDcdsfbYRcj5GmnGzR_au03NR8AqFe4o1MtgjRrw4s3_U89bYBmBcpzBTo9BEMsltkwgKbcboX7j6nh2iyyGbCqwtJxSjgzoqbquhA" />
|
||||||
<img alt="Wooden abacus with colorful beads on a bright yellow desk, soft shadows" class="w-full h-full object-cover" data-alt="Wooden abacus with colorful beads on a bright yellow desk, soft shadows" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCZTJ8e802cJ_5FfgED6WRGmFayr9x84nGvf4F-Prhl0jI8Cl0jJAMVtNM2ccSAgCM3XTBTSrMUPL1Ez2W40lJGQsopbLz0Lkm6cNMbPpI0xluIUX5iWqB-BsMCoiOAFN0Zrn5_7wwT1Y14CqEt7gjjPdk9HgHWM5fNu2Fxn-vSvbAZdFGGAsanZQkQ_pjfjQX0vgLSwT13Zo0uCy50CskhRd-NB9bCG4NEby-b7lht5gHvBco21O8_IRh1Mal3jWmubIen-2KxGb4"/>
|
</div>
|
||||||
</div>
|
<div class="flex flex-col justify-between flex-1 overflow-hidden py-1">
|
||||||
<div class="flex flex-col justify-between flex-1 overflow-hidden py-1">
|
<div>
|
||||||
<div>
|
<h3 class="text-base font-bold text-on-surface tracking-tight mb-1 truncate">数字连连看</h3>
|
||||||
<h3 class="text-base font-bold text-on-surface tracking-tight mb-1 truncate">加法启蒙</h3>
|
<p class="text-on-surface-variant text-xs mb-2 truncate">跟随小兔子走迷宫,在游戏中掌握1-10的数字顺序。</p>
|
||||||
<p class="text-on-surface-variant text-xs mb-2 truncate">结合生动插画,帮助孩子轻松记忆基础加法运算。</p>
|
<div class="flex gap-2 mb-1">
|
||||||
<div class="flex gap-2 mb-1">
|
<span
|
||||||
<span class="px-2 py-0.5 bg-secondary-container text-on-secondary-container rounded-full text-[10px] font-medium whitespace-nowrap">5-7岁</span>
|
class="px-2 py-0.5 bg-tertiary-container text-on-tertiary-container rounded-full text-[10px] font-medium whitespace-nowrap">4-5岁</span>
|
||||||
<span class="px-2 py-0.5 bg-surface-container-high rounded-full text-[10px] font-medium text-on-surface-variant whitespace-nowrap">挑战</span>
|
<span
|
||||||
</div>
|
class="px-2 py-0.5 bg-surface-container-high rounded-full text-[10px] font-medium text-on-surface-variant whitespace-nowrap">进阶</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-[10px] text-outline mt-1">
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center justify-between text-[10px] text-outline mt-1">
|
||||||
<span class="flex items-center gap-0.5"><span class="material-symbols-outlined text-[14px]">favorite</span> 890</span>
|
<div class="flex items-center gap-2">
|
||||||
<span class="flex items-center gap-0.5"><span class="material-symbols-outlined text-[14px]">download</span> 156</span>
|
<span class="flex items-center gap-0.5"><span
|
||||||
</div>
|
class="material-symbols-outlined text-[14px]">favorite</span> 230</span>
|
||||||
<span>2024-05-15</span>
|
<span class="flex items-center gap-0.5"><span
|
||||||
</div>
|
class="material-symbols-outlined text-[14px]">download</span> 45</span>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
<span>2024-05-18</span>
|
||||||
<!-- Card 4 -->
|
</div>
|
||||||
<article class="bg-surface-container-lowest rounded-[20px] p-2 flex gap-3 shadow-sm border border-surface-container-high hover:shadow-md transition-shadow">
|
</div>
|
||||||
<div class="w-24 shrink-0 rounded-[20px] overflow-hidden bg-surface-container relative">
|
</article>
|
||||||
<img alt="Comic style colorful abstract shapes on white paper, playful child drawing" class="w-full h-full object-cover" data-alt="Comic style colorful abstract shapes on white paper, playful child drawing" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDNI9UXLOiyKRJscHCtr4bBERSio7GZGyntl-0IkJW-OHd-3hdwx2bL00mpzqAHVVu-O2944uDbQ8Zy1la27ucH2vIFOz01EEe5lCsV4aWSr7KUIQVqvMP8kSq0kPBiJV2EJCgZJp-YY3q4A9PPNRlq5i9qbj01ZJipPmG71aVYjRrz4JBt7J8V8Zl5OQH7kU7HgaT-NNVDCTXnZcmVQXCQtJwiEExpJkrAb7jPi4V1QpMDFIIpqdtD1cHVDOvO7418cr24yRaGPK8"/>
|
<!-- Card 3 -->
|
||||||
</div>
|
<article
|
||||||
<div class="flex flex-col justify-between flex-1 overflow-hidden py-1">
|
class="bg-surface-container-lowest rounded-[20px] p-2 flex gap-3 shadow-sm border border-surface-container-high hover:shadow-md transition-shadow">
|
||||||
<div>
|
<div class="w-24 shrink-0 rounded-[20px] overflow-hidden bg-surface-container relative">
|
||||||
<h3 class="text-base font-bold text-on-surface tracking-tight mb-1 truncate">找规律</h3>
|
<img alt="Wooden abacus with colorful beads on a bright yellow desk, soft shadows"
|
||||||
<p class="text-on-surface-variant text-xs mb-2 truncate">简单的剪贴纸工艺,培养孩子的动手能力与色彩感知。</p>
|
class="w-full h-full object-cover"
|
||||||
<div class="flex gap-2 mb-1">
|
data-alt="Wooden abacus with colorful beads on a bright yellow desk, soft shadows"
|
||||||
<span class="px-2 py-0.5 bg-primary-container text-on-primary-container rounded-full text-[10px] font-medium whitespace-nowrap">4-6岁</span>
|
src="https://lh3.googleusercontent.com/aida-public/AB6AXuCZTJ8e802cJ_5FfgED6WRGmFayr9x84nGvf4F-Prhl0jI8Cl0jJAMVtNM2ccSAgCM3XTBTSrMUPL1Ez2W40lJGQsopbLz0Lkm6cNMbPpI0xluIUX5iWqB-BsMCoiOAFN0Zrn5_7wwT1Y14CqEt7gjjPdk9HgHWM5fNu2Fxn-vSvbAZdFGGAsanZQkQ_pjfjQX0vgLSwT13Zo0uCy50CskhRd-NB9bCG4NEby-b7lht5gHvBco21O8_IRh1Mal3jWmubIen-2KxGb4" />
|
||||||
<span class="px-2 py-0.5 bg-surface-container-high rounded-full text-[10px] font-medium text-on-surface-variant whitespace-nowrap">逻辑</span>
|
</div>
|
||||||
</div>
|
<div class="flex flex-col justify-between flex-1 overflow-hidden py-1">
|
||||||
</div>
|
<div>
|
||||||
<div class="flex items-center justify-between text-[10px] text-outline mt-1">
|
<h3 class="text-base font-bold text-on-surface tracking-tight mb-1 truncate">加法启蒙</h3>
|
||||||
<div class="flex items-center gap-2">
|
<p class="text-on-surface-variant text-xs mb-2 truncate">结合生动插画,帮助孩子轻松记忆基础加法运算。</p>
|
||||||
<span class="flex items-center gap-0.5"><span class="material-symbols-outlined text-[14px]">favorite</span> 512</span>
|
<div class="flex gap-2 mb-1">
|
||||||
<span class="flex items-center gap-0.5"><span class="material-symbols-outlined text-[14px]">download</span> 110</span>
|
<span
|
||||||
</div>
|
class="px-2 py-0.5 bg-secondary-container text-on-secondary-container rounded-full text-[10px] font-medium whitespace-nowrap">5-7岁</span>
|
||||||
<span>2024-05-10</span>
|
<span
|
||||||
</div>
|
class="px-2 py-0.5 bg-surface-container-high rounded-full text-[10px] font-medium text-on-surface-variant whitespace-nowrap">挑战</span>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
</div>
|
<div class="flex items-center justify-between text-[10px] text-outline mt-1">
|
||||||
</section>
|
<div class="flex items-center gap-2">
|
||||||
</main>
|
<span class="flex items-center gap-0.5"><span
|
||||||
<!-- BottomNavBar -->
|
class="material-symbols-outlined text-[14px]">favorite</span> 890</span>
|
||||||
<nav class="bg-surface-container-low/90 backdrop-blur-xl pb-safe pt-2 px-6 flex justify-between items-center fixed bottom-0 w-full z-50 md:hidden">
|
<span class="flex items-center gap-0.5"><span
|
||||||
<button class="flex flex-col items-center p-2 text-primary">
|
class="material-symbols-outlined text-[14px]">download</span> 156</span>
|
||||||
<span class="material-symbols-outlined icon-fill">explore</span>
|
</div>
|
||||||
<span class="text-xs font-bold mt-1">发现</span>
|
<span>2024-05-15</span>
|
||||||
</button>
|
</div>
|
||||||
<button class="flex flex-col items-center p-2 text-on-surface-variant">
|
</div>
|
||||||
<span class="material-symbols-outlined">filter_list</span>
|
</article>
|
||||||
<span class="text-xs font-medium mt-1">分龄</span>
|
<!-- Card 4 -->
|
||||||
</button>
|
<article
|
||||||
<button class="flex flex-col items-center p-2 text-on-surface-variant">
|
class="bg-surface-container-lowest rounded-[20px] p-2 flex gap-3 shadow-sm border border-surface-container-high hover:shadow-md transition-shadow">
|
||||||
<span class="material-symbols-outlined">favorite</span>
|
<div class="w-24 shrink-0 rounded-[20px] overflow-hidden bg-surface-container relative">
|
||||||
<span class="text-xs font-medium mt-1">收藏</span>
|
<img alt="Comic style colorful abstract shapes on white paper, playful child drawing"
|
||||||
</button>
|
class="w-full h-full object-cover"
|
||||||
<button class="flex flex-col items-center p-2 text-on-surface-variant">
|
data-alt="Comic style colorful abstract shapes on white paper, playful child drawing"
|
||||||
<span class="material-symbols-outlined">person</span>
|
src="https://lh3.googleusercontent.com/aida-public/AB6AXuDNI9UXLOiyKRJscHCtr4bBERSio7GZGyntl-0IkJW-OHd-3hdwx2bL00mpzqAHVVu-O2944uDbQ8Zy1la27ucH2vIFOz01EEe5lCsV4aWSr7KUIQVqvMP8kSq0kPBiJV2EJCgZJp-YY3q4A9PPNRlq5i9qbj01ZJipPmG71aVYjRrz4JBt7J8V8Zl5OQH7kU7HgaT-NNVDCTXnZcmVQXCQtJwiEExpJkrAb7jPi4V1QpMDFIIpqdtD1cHVDOvO7418cr24yRaGPK8" />
|
||||||
<span class="text-xs font-medium mt-1">我的</span>
|
</div>
|
||||||
</button>
|
<div class="flex flex-col justify-between flex-1 overflow-hidden py-1">
|
||||||
</nav>
|
<div>
|
||||||
</body></html>
|
<h3 class="text-base font-bold text-on-surface tracking-tight mb-1 truncate">找规律</h3>
|
||||||
|
<p class="text-on-surface-variant text-xs mb-2 truncate">简单的剪贴纸工艺,培养孩子的动手能力与色彩感知。</p>
|
||||||
|
<div class="flex gap-2 mb-1">
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 bg-primary-container text-on-primary-container rounded-full text-[10px] font-medium whitespace-nowrap">4-6岁</span>
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 bg-surface-container-high rounded-full text-[10px] font-medium text-on-surface-variant whitespace-nowrap">逻辑</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-[10px] text-outline mt-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="flex items-center gap-0.5"><span
|
||||||
|
class="material-symbols-outlined text-[14px]">favorite</span> 512</span>
|
||||||
|
<span class="flex items-center gap-0.5"><span
|
||||||
|
class="material-symbols-outlined text-[14px]">download</span> 110</span>
|
||||||
|
</div>
|
||||||
|
<span>2024-05-10</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<!-- BottomNavBar -->
|
||||||
|
<nav
|
||||||
|
class="bg-surface-container-low/90 backdrop-blur-xl pb-safe pt-2 px-6 flex justify-between items-center fixed bottom-0 w-full z-50 md:hidden">
|
||||||
|
<button class="flex flex-col items-center p-2 text-primary">
|
||||||
|
<span class="material-symbols-outlined icon-fill">explore</span>
|
||||||
|
<span class="text-xs font-bold mt-1">发现</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center p-2 text-on-surface-variant">
|
||||||
|
<span class="material-symbols-outlined">filter_list</span>
|
||||||
|
<span class="text-xs font-medium mt-1">分龄</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center p-2 text-on-surface-variant">
|
||||||
|
<span class="material-symbols-outlined">favorite</span>
|
||||||
|
<span class="text-xs font-medium mt-1">收藏</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center p-2 text-on-surface-variant">
|
||||||
|
<span class="material-symbols-outlined">person</span>
|
||||||
|
<span class="text-xs font-medium mt-1">我的</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Vendored
+2
@@ -26,6 +26,8 @@ interface IAppOption {
|
|||||||
printConfig?: PrintConfig;
|
printConfig?: PrintConfig;
|
||||||
pageConfig?: PageConfigData;
|
pageConfig?: PageConfigData;
|
||||||
pageConfigReady?: boolean;
|
pageConfigReady?: boolean;
|
||||||
|
/** switchTab 无法带 query:首页→分类页的临时候选侧边栏 id,取出后清空 */
|
||||||
|
pendingCategoryTabId?: string;
|
||||||
};
|
};
|
||||||
userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback;
|
userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback;
|
||||||
getPageConfig(): Promise<PageConfigData | null>;
|
getPageConfig(): Promise<PageConfigData | null>;
|
||||||
|
|||||||
Reference in New Issue
Block a user