feat: 首页跳转到分类页指定分类优化

This commit is contained in:
R524809
2026-05-09 11:08:29 +08:00
parent c21ac9e935
commit debecd67c6
20 changed files with 1450 additions and 959 deletions
+5 -3
View File
@@ -18,8 +18,7 @@
"categoryContentManage/categoryContentManage",
"homeContentManage/homeContentManage",
"mathIndex/mathIndex",
"focusIndex/focusIndex",
"copyBook/copyBook"
"focusIndex/focusIndex"
],
"independent": false
},
@@ -44,7 +43,10 @@
{
"root": "chinesePages",
"name": "chinesePages",
"pages": ["wordColoring/wordColoring"],
"pages": [
"wordColoring/wordColoring",
"handwritingSheet/handwritingSheet"
],
"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
closeable="{{true}}"
close-on-click-overlay="{{true}}"
safe-area-inset-bottom="{{false}}"
safe-area-inset-bottom="{{true}}"
position="bottom"
custom-style="height:78%"
custom-class="word-picker">
<view class="word-picker-content">
<view class="word-picker-title">请选择文字</view>
+8 -3
View File
@@ -1,4 +1,5 @@
import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories';
import { takePendingCategoryTabId } from '../../utils/index';
import { CATEGORY_DATA, type CategoryItem } from './category.data';
type CategoryTab = {
@@ -190,10 +191,10 @@ Page({
onShow() {
_categoryData = null;
this.setData({ displayItems: SKELETON_ITEMS, loading: true });
void this.loadWorksheets();
void this.loadWorksheets(takePendingCategoryTabId() ?? undefined);
},
async loadWorksheets() {
async loadWorksheets(overrideCategoryId?: string | null) {
try {
const res = await wx.cloud.callFunction({
name: 'worksheetsQuery',
@@ -207,7 +208,11 @@ Page({
_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 filtered = filterByKeyword(items, this.data.searchKeyword);
+32 -21
View File
@@ -150,7 +150,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
id: 'math',
title: '数学启蒙',
subtitle: '数字认知、计数练习、加减运算、时钟与形状',
morePath: '/pages/mathIndex/mathIndex',
morePath: '/pages/category/category?id=math',
items: [
{
id: 'target-math-1',
@@ -221,7 +221,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
id: 'chinese',
title: '语文启蒙',
subtitle: '识字、练字、拼音、表达训练',
morePath: '/pages/index/index',
morePath: '/pages/category/category?id=chinese',
items: [
{
id: 'target-chinese-1',
@@ -277,7 +277,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
id: 'english',
title: '英语启蒙',
subtitle: '字母学习、闪卡、描红与自然拼读',
morePath: '/englishPages/letterTracing/letterTracing',
morePath: '/pages/category/category?id=english',
items: [
{
id: 'target-english-1',
@@ -321,7 +321,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
id: 'puzzle',
title: '益智游戏',
subtitle: '迷宫、找不同、连线、数独与控笔',
morePath: '/pages/focusIndex/focusIndex',
morePath: '/pages/category/category?id=puzzle',
items: [
{
id: 'target-puzzle-1',
@@ -519,7 +519,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
id: 'math',
title: '数感启蒙',
subtitle: '优先展示现有高价值、高覆盖的数感与运算内容',
morePath: '/pages/mathIndex/mathIndex',
morePath: '/pages/category/category?id=math',
items: [
{
id: 'current-math-1',
@@ -581,7 +581,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '7-8岁',
difficulty: '挑战',
icon: '✖️',
previewImg: '/assets/entrancePicture/math/multiplication-table.png',
previewImg:
'/assets/entrancePicture/math/multiplication-table.png',
path: '/mathPages/mathDraw/mathDraw?id=multiplication-table',
available: true,
source: 'multiplicationTable',
@@ -590,9 +591,9 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
},
{
id: 'chinese',
title: '趣味识字',
title: '识字练字',
subtitle: '用现有识字与练字能力先覆盖核心语文场景',
morePath: '/pages/index/index',
morePath: '/pages/category/category?id=chinese',
items: [
{
id: 'current-chinese-1',
@@ -636,7 +637,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
id: 'puzzle',
title: '益智游戏',
subtitle: '从现有专注力模块中抽取最符合产品重设计的内容',
morePath: '/pages/focusIndex/focusIndex',
morePath: '/pages/category/category?id=puzzle',
items: [
{
id: 'current-puzzle-1',
@@ -659,7 +660,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '4-6岁',
difficulty: '基础',
icon: '🧵',
previewImg: '/assets/entrancePicture/focus/match-connect.png',
previewImg:
'/assets/entrancePicture/focus/match-connect.png',
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
available: true,
source: 'match-connect',
@@ -672,7 +674,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '4-6岁',
difficulty: '基础',
icon: '🌈',
previewImg: '/assets/entrancePicture/focus/color-pattern.png',
previewImg:
'/assets/entrancePicture/focus/color-pattern.png',
path: '/focusPages/focusDraw/focusDraw?id=color-pattern',
available: true,
source: 'color-pattern',
@@ -685,7 +688,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '5-7岁',
difficulty: '进阶',
icon: '🧠',
previewImg: '/assets/entrancePicture/focus/grid-reasoning.png',
previewImg:
'/assets/entrancePicture/focus/grid-reasoning.png',
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
available: true,
source: 'grid-reasoning',
@@ -698,7 +702,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '4-7岁',
difficulty: '进阶',
icon: '🟨',
previewImg: '/assets/entrancePicture/focus/grid-drawing-5x5.png',
previewImg:
'/assets/entrancePicture/focus/grid-drawing-5x5.png',
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing',
available: true,
source: 'grid-drawing',
@@ -709,7 +714,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
id: 'english',
title: '英语启蒙',
subtitle: '多种描红版式,认读与书写循序渐进',
morePath: '/englishPages/letterTracing/letterTracing',
morePath: '/pages/category/category?id=english',
items: [
{
id: 'current-english-1',
@@ -719,7 +724,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '4-7岁',
difficulty: '入门',
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',
available: true,
source: 'letter-tracing-single',
@@ -732,7 +738,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '4-7岁',
difficulty: '入门',
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',
available: true,
source: 'letter-tracing-upper-lower',
@@ -745,7 +752,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '4-7岁',
difficulty: '入门',
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',
available: true,
source: 'letter-tracing-case-pairing',
@@ -758,7 +766,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '4-7岁',
difficulty: '入门',
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',
available: true,
source: 'letter-tracing-two-column',
@@ -771,7 +780,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '4-7岁',
difficulty: '入门',
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',
available: true,
source: 'letter-tracing-half',
@@ -784,7 +794,8 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
ageBand: '4-7岁',
difficulty: '入门',
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',
available: true,
source: 'letter-tracing-three',
@@ -795,7 +806,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = {
id: 'craft',
title: '创意手工',
subtitle: '暂用最接近创意表达的现有能力承接展示位',
morePath: '/pages/index/index',
morePath: '/pages/category/category?id=craft',
items: [
{
id: 'current-craft-1',
+13 -6
View File
@@ -1,5 +1,8 @@
import { NAV_INNER_PX } from '../../utils/navMetrics';
import { parseMiniProgramUrl } from '../../utils/index';
import {
parseMiniProgramUrl,
stashPendingCategoryTabId,
} from '../../utils/index';
import {
HOME_CURRENT_CAPABILITY_DATA,
type HomeDisplayItem,
@@ -78,8 +81,13 @@ function navigateByPath(path?: string, fallbackTitle?: string) {
const { path: basePath, query } = parseMiniProgramUrl(path);
if (TAB_BAR_PATHS.has(basePath)) {
// tabBar 页面不支持携带 query/hash,统一跳到 basePath
if (Object.keys(query).length > 0) {
if (
basePath === '/pages/category/category' &&
query.id &&
String(query.id).trim() !== ''
) {
stashPendingCategoryTabId(String(query.id).trim());
} else if (Object.keys(query).length > 0) {
console.warn(
'[navigateByPath] tabBar 不支持携带参数,已忽略 query:',
basePath,
@@ -245,7 +253,7 @@ Page({
},
onTapHotMore() {
wx.showToast({ title: '热门内容整理中', icon: 'none' });
navigateByPath('/pages/category/category?id=hot');
},
onTapTrackMore(e: WechatMiniprogram.TouchEvent) {
@@ -257,8 +265,7 @@ Page({
onTapCard(e: WechatMiniprogram.CustomEvent) {
const detail = (e.detail || {}) as { title?: string; path?: string };
const path =
detail.path ||
(e.currentTarget.dataset.path as string | undefined);
detail.path || (e.currentTarget.dataset.path as string | undefined);
const title =
detail.title ||
(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 -7
View File
@@ -7,6 +7,13 @@ type DebugEntry = {
};
const DEBUG_ENTRIES: DebugEntry[] = [
{
id: 'home-content',
title: '首页内容管理',
subtitle: '编排首页推荐位、热门推荐和分类分区内容。',
icon: '🏠',
path: '/supportPages/homeContentManage/homeContentManage',
},
{
id: 'category-manage',
title: '分类管理',
@@ -28,13 +35,6 @@ const DEBUG_ENTRIES: DebugEntry[] = [
icon: '📋',
path: '/supportPages/categoryContentManage/categoryContentManage',
},
{
id: 'home-content',
title: '首页内容管理',
subtitle: '编排首页推荐位、热门推荐和分类分区内容。',
icon: '🏠',
path: '/supportPages/homeContentManage/homeContentManage',
},
];
Page({
+25
View File
@@ -75,3 +75,28 @@ export function parseMiniProgramUrl(input: string) {
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;
}
}