diff --git a/miniprogram/app.json b/miniprogram/app.json index f05f8e6..903dfab 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -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 } ], diff --git a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.config.ts b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.config.ts new file mode 100644 index 0000000..4b8b72b --- /dev/null +++ b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.config.ts @@ -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; + +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; + +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', + }; +} diff --git a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.json b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.json new file mode 100644 index 0000000..02843f8 --- /dev/null +++ b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.json @@ -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" + } +} diff --git a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.less b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.less new file mode 100644 index 0000000..05d6a83 --- /dev/null +++ b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.less @@ -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; + } +} \ No newline at end of file diff --git a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.ts b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.ts new file mode 100644 index 0000000..a3ddd86 --- /dev/null +++ b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.ts @@ -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, + maxRow: 0 as number, + maxCol: 0 as number, + _favoritedMap: {} as Record, + + 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, + }, +); diff --git a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.wxml b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.wxml new file mode 100644 index 0000000..0ea2ee1 --- /dev/null +++ b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.wxml @@ -0,0 +1,133 @@ + + + + + + + + 内容设置 + + + + 自定义文字 + 最多输入11个字 + + + + + + + + + + + + 已选文字 + 点击文字可删除 + + + + + {{item}} + + + + + + + + + + + + + + + + + {{item.icon}} + {{item.categoryName}} + + + ··· + 更多分类 + + + + + + + + + + + + + + + diff --git a/miniprogram/components3.0/word-picker/word-picker.wxml b/miniprogram/components3.0/word-picker/word-picker.wxml index 52eecc8..b469d3d 100644 --- a/miniprogram/components3.0/word-picker/word-picker.wxml +++ b/miniprogram/components3.0/word-picker/word-picker.wxml @@ -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"> 请选择文字 diff --git a/miniprogram/pages/category/category.ts b/miniprogram/pages/category/category.ts index cc2ea9f..99f24a0 100644 --- a/miniprogram/pages/category/category.ts +++ b/miniprogram/pages/category/category.ts @@ -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); diff --git a/miniprogram/pages/home/home.data.ts b/miniprogram/pages/home/home.data.ts index 9d0d3d1..924299a 100644 --- a/miniprogram/pages/home/home.data.ts +++ b/miniprogram/pages/home/home.data.ts @@ -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', diff --git a/miniprogram/pages/home/home.ts b/miniprogram/pages/home/home.ts index ac751aa..20fa496 100644 --- a/miniprogram/pages/home/home.ts +++ b/miniprogram/pages/home/home.ts @@ -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); diff --git a/miniprogram/supportPages/copyBook/copyBook.json b/miniprogram/supportPages/copyBook/copyBook.json deleted file mode 100644 index 75408e8..0000000 --- a/miniprogram/supportPages/copyBook/copyBook.json +++ /dev/null @@ -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" - } -} diff --git a/miniprogram/supportPages/copyBook/copyBook.less b/miniprogram/supportPages/copyBook/copyBook.less deleted file mode 100644 index 4dcfdc9..0000000 --- a/miniprogram/supportPages/copyBook/copyBook.less +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/miniprogram/supportPages/copyBook/copyBook.ts b/miniprogram/supportPages/copyBook/copyBook.ts deleted file mode 100644 index 0257017..0000000 --- a/miniprogram/supportPages/copyBook/copyBook.ts +++ /dev/null @@ -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, - 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((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 }); - }, -}); diff --git a/miniprogram/supportPages/copyBook/copyBook.wxml b/miniprogram/supportPages/copyBook/copyBook.wxml deleted file mode 100644 index 6a40653..0000000 --- a/miniprogram/supportPages/copyBook/copyBook.wxml +++ /dev/null @@ -1,99 +0,0 @@ - - - 自定义练字贴 - - - - - - - 选择文字 - - - 清空 - - - - - - - - - - - - - 预览练字田字格 - - - - - - - - - 分享 - - - 下载打印 - - - - - - - - diff --git a/miniprogram/supportPages/debug/debug.ts b/miniprogram/supportPages/debug/debug.ts index 5e9188a..367b449 100644 --- a/miniprogram/supportPages/debug/debug.ts +++ b/miniprogram/supportPages/debug/debug.ts @@ -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({ diff --git a/miniprogram/utils/index.ts b/miniprogram/utils/index.ts index 2a169cb..1e51d20 100644 --- a/miniprogram/utils/index.ts +++ b/miniprogram/utils/index.ts @@ -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().globalData.pendingCategoryTabId = trimmed; + } catch { + /* ignore */ + } +} + +/** 若无待处理 id 则返回 null;返回后清空,避免残留在全局 */ +export function takePendingCategoryTabId(): string | null { + try { + const app = getApp(); + 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; + } +} diff --git a/project.private.config.json b/project.private.config.json index d48f28b..8704609 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -24,22 +24,22 @@ "miniprogram": { "list": [ { - "name": "chinesePages/wordColoring/wordColoring", - "pathName": "chinesePages/wordColoring/wordColoring", + "name": "chinesePages/handwritingSheet/handwritingSheet", + "pathName": "chinesePages/handwritingSheet/handwritingSheet", "query": "", "scene": null, "launchMode": "default" }, { - "name": "pages/profile/profile", - "pathName": "pages/profile/profile", + "name": "chinesePages/wordColoring/wordColoring", + "pathName": "chinesePages/wordColoring/wordColoring", "query": "", "launchMode": "default", "scene": null }, { - "name": "supportPages/index/index", - "pathName": "supportPages/index/index", + "name": "pages/profile/profile", + "pathName": "pages/profile/profile", "query": "", "launchMode": "default", "scene": null diff --git a/skills/create-draw-page/SKILL.md b/skills/create-draw-page/SKILL.md new file mode 100644 index 0000000..6551550 --- /dev/null +++ b/skills/create-draw-page/SKILL.md @@ -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; +``` + +**必须导出的辅助函数**(参照 `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 + + + + + + + + + + + + + + + +``` + +### 3. 图标使用 + +图标**优先**使用 `toy-icon` 组件(路径 `../../toy/icon/icon`): + +```xml + +``` + +如果 `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` 中注册了新页面路径 diff --git a/stitch-ui/category/code.html b/stitch-ui/category/code.html index 9af575f..1b36a54 100644 --- a/stitch-ui/category/code.html +++ b/stitch-ui/category/code.html @@ -1,13 +1,19 @@ - - - -分类 - Doodle Mini - - - - + + + - + - + - -
-
-
-search - -
-
-
- -
- - - -
-
- -
-
-Colorful counting beads on a wooden frame, bright sunny lighting, educational -
-
-
-

认识基础图形

-

通过趣味拼贴,让孩子初步感知圆形、方形与三角形的美妙世界。

-
-3-6岁 -入门 -
-
-
-
-favorite 456 -download 89 -
-2024-05-20 -
-
-
- -
-
-Hand drawn numbers on colorful paper cutouts, messy desk, creative warm light -
-
-
-

数字连连看

-

跟随小兔子走迷宫,在游戏中掌握1-10的数字顺序。

-
-4-5岁 -进阶 -
-
-
-
-favorite 230 -download 45 -
-2024-05-18 -
-
-
- -
-
-Wooden abacus with colorful beads on a bright yellow desk, soft shadows -
-
-
-

加法启蒙

-

结合生动插画,帮助孩子轻松记忆基础加法运算。

-
-5-7岁 -挑战 -
-
-
-
-favorite 890 -download 156 -
-2024-05-15 -
-
-
- -
-
-Comic style colorful abstract shapes on white paper, playful child drawing -
-
-
-

找规律

-

简单的剪贴纸工艺,培养孩子的动手能力与色彩感知。

-
-4-6岁 -逻辑 -
-
-
-
-favorite 512 -download 110 -
-2024-05-10 -
-
-
-
-
-
- - - \ No newline at end of file + +
+
+
+ search + +
+
+
+ +
+ + + +
+
+ +
+
+ Colorful counting beads on a wooden frame, bright sunny lighting, educational +
+
+
+

认识基础图形

+

通过趣味拼贴,让孩子初步感知圆形、方形与三角形的美妙世界。

+
+ 3-6岁 + 入门 +
+
+
+
+ favorite 456 + download 89 +
+ 2024-05-20 +
+
+
+ +
+
+ Hand drawn numbers on colorful paper cutouts, messy desk, creative warm light +
+
+
+

数字连连看

+

跟随小兔子走迷宫,在游戏中掌握1-10的数字顺序。

+
+ 4-5岁 + 进阶 +
+
+
+
+ favorite 230 + download 45 +
+ 2024-05-18 +
+
+
+ +
+
+ Wooden abacus with colorful beads on a bright yellow desk, soft shadows +
+
+
+

加法启蒙

+

结合生动插画,帮助孩子轻松记忆基础加法运算。

+
+ 5-7岁 + 挑战 +
+
+
+
+ favorite 890 + download 156 +
+ 2024-05-15 +
+
+
+ +
+
+ Comic style colorful abstract shapes on white paper, playful child drawing +
+
+
+

找规律

+

简单的剪贴纸工艺,培养孩子的动手能力与色彩感知。

+
+ 4-6岁 + 逻辑 +
+
+
+
+ favorite 512 + download 110 +
+ 2024-05-10 +
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/typings/index.d.ts b/typings/index.d.ts index 4bb4107..f15f8f5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -26,6 +26,8 @@ interface IAppOption { printConfig?: PrintConfig; pageConfig?: PageConfigData; pageConfigReady?: boolean; + /** switchTab 无法带 query:首页→分类页的临时候选侧边栏 id,取出后清空 */ + pendingCategoryTabId?: string; }; userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback; getPageConfig(): Promise;