diff --git a/miniprogram/core/data/worksheets.ts b/miniprogram/core/data/worksheets.ts index ca014b4..09e0205 100644 --- a/miniprogram/core/data/worksheets.ts +++ b/miniprogram/core/data/worksheets.ts @@ -325,7 +325,7 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [ { id: 'letter-tracing-upper-lower', title: '字母总览', - desc: 'Uppercase / Lowercase 分区,9+9+8 行', + desc: 'Uppercase / Lowercase 分区,每区四行(7+7+6+6)', category: 'english', page: 'letterTracing', subpackage: 'englishPages', @@ -387,6 +387,72 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [ }, }, ), + sheet( + { + id: 'letter-tracing-triple', + title: '三字母精练', + desc: '每页聚焦 3 个字母,大写 + 小写深度书写', + category: 'english', + page: 'letterTracing', + subpackage: 'englishPages', + icon: '🔤', + img: '', + ageRange: [4, 7], + difficulty: 'basic', + previewBg: '#e8f4ff', + previewText: 'ABC', + }, + { + subcategory: 'letter-tracing', + template: 'tracing-writing', + generator: 'letter-tracing', + sortOrder: 46, + layoutConfig: { + ...L.trace, + instructionText: '每页 3 个字母,逐字精练', + }, + generatorConfig: { + mode: 'triple-focus', + letterCase: 'both', + repetitions: 5, + fadePattern: 'gradient', + tripleLetters: ['A', 'B', 'C'], + }, + }, + ), + sheet( + { + id: 'letter-tracing-single-line', + title: '单字母逐行', + desc: '每行一个字母,13 字母半表逐字练习', + category: 'english', + page: 'letterTracing', + subpackage: 'englishPages', + icon: '📝', + img: '', + ageRange: [4, 7], + difficulty: 'basic', + previewBg: '#e8f4ff', + previewText: 'A–M', + }, + { + subcategory: 'letter-tracing', + template: 'tracing-writing', + generator: 'letter-tracing', + sortOrder: 47, + layoutConfig: { + ...L.trace, + instructionText: '每行一个字母,逐字反复练习', + }, + generatorConfig: { + mode: 'single-line', + letterCase: 'both', + alphabetHalf: 'A-M', + repetitions: 5, + fadePattern: 'first-only', + }, + }, + ), ]; export const MATH_FUNCTION_TYPES: MathFunctionType[] = ALL_WORKSHEETS.filter( diff --git a/miniprogram/englishPages/letterTracing/letterTracing.ts b/miniprogram/englishPages/letterTracing/letterTracing.ts index 8692385..1f36cbd 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.ts +++ b/miniprogram/englishPages/letterTracing/letterTracing.ts @@ -10,41 +10,58 @@ import { type LetterTracingMode, } from '../shared/generators/letter-tracing-generator'; import { LETTERS_UPPER } from '../shared/data/alphabet'; +import { + DEFAULT_LETTER_PROFILE, + getFontProfile, + type FontProfile, +} from '../shared/data/fontProfiles'; import { loadLetterFont } from '../shared/draw/drawTools'; -/** 各 worksheetId 对应的页面标题和描述 */ -const PAGE_META: Record = { - 'letter-tracing-single': { - title: '看图描红', - desc: '配图、例句与六行描红', +/** 练习模式定义(顺序与产品文档一致),同时用于模式选择器和页面元信息 */ +const MODES = [ + { + id: 'letter-tracing-single', + label: '看图识字', + desc: '配图、例句与描红', }, - 'letter-tracing-full': { - title: '分栏对照', - desc: '半组字母左大写右小写', - }, - 'letter-tracing-upper-lower': { - title: '字母总览', + { + id: 'letter-tracing-upper-lower', + label: '字母总览', desc: 'Uppercase / Lowercase 总览', }, - 'letter-tracing-two-column': { - title: '大小写配对', + { + id: 'letter-tracing-two-column', + label: '大小写配对', desc: '左 A–M、右 N–Z 配对描红', }, -}; + { + id: 'letter-tracing-full', + label: '分栏对照', + desc: '半组字母左大写右小写', + }, + { + id: 'letter-tracing-triple', + label: '三字母精练', + desc: '每页聚焦 3 个字母深度书写', + }, + { + id: 'letter-tracing-single-line', + label: '单字母逐行', + desc: '每行一个字母,13 字母半表', + }, +] as const; + +const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record< + string, + (typeof MODES)[number] +>; /** 根据 worksheetId 查找页面元信息 */ function pageInfoLookup(id: string) { - return PAGE_META[id]; + const m = MODE_MAP[id]; + return m ? { title: m.label, desc: m.desc } : undefined; } -/** 模式选择器的选项列表 */ -const MODE_OPTIONS = [ - { id: 'letter-tracing-single', label: '看图描红' }, - { id: 'letter-tracing-full', label: '分栏对照' }, - { id: 'letter-tracing-upper-lower', label: '字母总览' }, - { id: 'letter-tracing-two-column', label: '大小写配对' }, -] as const; - type PageData = CanvasDataState & { worksheetId: string; traceMode: LetterTracingMode; @@ -54,7 +71,7 @@ type PageData = CanvasDataState & { showCaseToggle: boolean; showNextLetter: boolean; letterGrid: string[]; - modeOptions: ReadonlyArray<{ id: string; label: string }>; + modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>; }; createPage( @@ -64,6 +81,7 @@ createPage( boxHeight: 0, boxWidth: 0, drawService: null as LetterTracingDraw | null, + fontProfile: DEFAULT_LETTER_PROFILE as FontProfile, data: { pageTitle: '字母描红', @@ -78,18 +96,22 @@ createPage( showCaseToggle: false, showNextLetter: true, letterGrid: LETTERS_UPPER, - modeOptions: [...MODE_OPTIONS], + modeOptions: [...MODES], } as unknown as PageData, - /** 页面加载:从路由参数获取 worksheetId、letter、case 并应用,同时预加载字体 */ + /** 页面加载:从路由参数获取 worksheetId、letter、case、font 并应用,同时预加载字体 */ onLoad(options: { id?: string; letter?: string; case?: 'upper' | 'lower'; + font?: string; }) { - loadLetterFont().catch(() => {}); + if (options.font) { + this.fontProfile = getFontProfile(options.font); + } + loadLetterFont(this.fontProfile).catch(() => {}); const worksheetId = - options.id && PAGE_META[options.id] + options.id && MODE_MAP[options.id] ? options.id : 'letter-tracing-single'; this.applyWorksheet(worksheetId); @@ -149,10 +171,13 @@ createPage( : this.data.letterCaseLower ? 'lower' : 'upper'; + const needsHalf = + this.data.traceMode === 'column-compare' || + this.data.traceMode === 'single-line'; return mergeLetterTracingConfig(base, { selectedLetter: this.data.selectedLetter, letterCase, - ...(this.data.traceMode === 'column-compare' + ...(needsHalf ? { alphabetHalf: this.data.letterCaseLower ? 'N-Z' @@ -166,7 +191,7 @@ createPage( async drawCanvas() { if (!this.ctx || !this.drawService) return; try { - await loadLetterFont(); + await loadLetterFont(this.fontProfile); const config = this.buildRuntimeConfig(); const data = generateLetterTracing(config); console.log('drawCanvas data', data); @@ -228,14 +253,16 @@ createPage( const merged = mergeLetterTracingConfig(base, {}); const traceMode = merged.mode; - const showLetterPicker = traceMode === 'picture-tracing'; - const showCaseToggle = traceMode === 'column-compare'; + const showLetterPicker = + traceMode === 'picture-tracing' || traceMode === 'triple-focus'; + const showCaseToggle = + traceMode === 'column-compare' || traceMode === 'single-line'; const showNextLetter = traceMode === 'picture-tracing'; const selectedLetter = options?.preserveLetter ? (this.data.selectedLetter ?? 'A') : normalizeLetter(merged.selectedLetter ?? ''); const letterCaseLower = - traceMode === 'column-compare' + traceMode === 'column-compare' || traceMode === 'single-line' ? merged.alphabetHalf === 'N-Z' : traceMode === 'picture-tracing' ? merged.letterCase === 'lower' @@ -255,7 +282,7 @@ createPage( () => { this.initPageInfo( worksheetId, - PAGE_META[worksheetId]?.title ?? '字母描红', + MODE_MAP[worksheetId]?.label ?? '字母描红', ); if (this.drawService) { diff --git a/miniprogram/englishPages/letterTracing/字母描红产品文档.md b/miniprogram/englishPages/letterTracing/字母描红产品文档.md index 8756557..91e6c2e 100644 --- a/miniprogram/englishPages/letterTracing/字母描红产品文档.md +++ b/miniprogram/englishPages/letterTracing/字母描红产品文档.md @@ -1,20 +1,84 @@ ## 练习类型 -1. 大小写配对 — 26 字母双栏,每组大写+小写并排 -2. 分栏对照 — 半组字母,左栏大写右栏小写 -3. 字母总览 — 上下分区展示全部大小写 -4. 看图描红 — 单字母教学配图 + 描红练习 +共 6 种练习模式,从单字母精练到全表总览逐步递进。 -### 类型描述 +--- -1. 一页展示 26 个字母,每个包含大小写,分左右两栏,左栏A-M,右栏N-Z,每栏的一行展示 4 组字母,每一组都是一个大写和小写字母的组合。同一行是相同的字母 -2. 一页展示 A-M 或N-Z的字母,分左右两栏,左栏每行展示 4 个大小字母,右栏展示 4 个对应的小写字母。同一行是相同的字母 -3. 一页分上下两块区域展示,上面展示大写字母,下面展示小写字母,上面是Uppercase Letters 标题加三行,前面两行展示 9 个字母,最后一行展示剩余的 8 个字母。下面区域title是 lowercase Letters,行的展示逻辑和上面一致 -4. 一页分为上下两部分,上部分有分左右两部分,左边展示超大字体的大小写字母,下面展示一段方面记忆的短句,例如:“L is for Lion”,上部分右边展示对应的Lion 的图片。下部分展示 6 行字母描红,前三行大写,后三行小写。 +### 1. 看图识字(picture-tracing)—— 单字母教学 + 配图 + 描红 + +> 每页聚焦一个字母,适合初学者逐字认读和书写。 + +页面分为上下两部分: + +- **上部 · 教学区**:左侧展示超大字体的大小写字母及助记短句(如 "L is for Lion"),右侧展示对应单词的配图。中间包含"Highlight It"字母识别小游戏和"Trace It"笔顺演示区。 +- **下部 · 描红区**:4 行四线三格,每行重复书写该字母的大小写组合,透明度逐渐递减形成倒三角描红引导。 + +### 2. 字母总览(overview)—— 26 字母全表速览 + +> 一页纵览全部大小写字母,适合复习或张贴展示。 + +页面分上下两大区域: + +- **上区 · Uppercase Letters**:4 行四线三格,按 7-7-6-6 排列全部大写字母。 +- **下区 · lowercase letters**:布局同上,展示对应的小写字母。 + +所有字母以浅红色描红字体呈现,可打印后用铅笔覆写练习。 + +### 3. 大小写配对(case-pairing)—— 左右分栏 · 大小写成对练习 + +> 以 Aa 为一组进行配对描红,强化大小写对应关系。 + +页面分左右两栏(左栏 A–M,右栏 N–Z),每行展示 4 组大小写配对字母。同一行的字母互相对应,每组内包含示范字母(实色)和渐淡描红格,引导从临摹到独立书写。 + +### 4. 分栏对照(column-compare)—— 半表字母 · 左大写右小写对照 + +> 半组字母(A–M 或 N–Z)逐行展开,左大写右小写一一对照。 + +四线三格横跨整页宽度,每行分为左右两半:左半栏写大写字母,右半栏写对应的小写字母。第一个字母为黑色示范,后续为浅红色描红。可切换 A–M / N–Z 两个半表。 + +### 5. 三字母精练(triple-focus)—— 每页 3 个字母 · 深度书写 + +> 每页聚焦 3 个字母,兼顾专注度和练习量,适合进阶阶段。 + +页面纵向分为 3 个等高区域,每个区域展示一个字母: + +- 每区包含一行大写描红和一行小写描红(四线三格),每行重复书写该字母若干次,透明度逐渐递减。 +- 字母组可按 ABC/DEF/… 顺序自动分组,也可由用户自选 3 个字母。 + +### 6. 单字母逐行(single-line)—— 每行一个字母 · 13 字母半表 + +> 每页 13 个字母(A–M 或 N–Z),每个字母独占一行,专注逐字练习。 + +页面纵向排列 13 行四线三格,每行对应一个字母。行内先展示一个黑色示范字母,后接若干浅红色描红字母。可切换 A–M / N–Z 半表。布局紧凑,适合大量反复练习。 + +--- + +## 扩展思考:更多可补充的练习类型 + +以下为后续可扩展的练习模式,供产品迭代参考: + +### 书写方向类 + +- **笔顺分步练习**:将每个字母的笔画拆解为 2–4 步,每步用不同颜色高亮当前笔画,配合箭头指示书写方向,逐步引导完成整个字母。 +- **起笔点标注**:在四线三格中标注每个字母的起笔位置(圆点)和运笔路径(虚线箭头),帮助建立正确的书写习惯。 + +### 认知强化类 + +- **大小写连线**:左列随机排列大写字母,右列随机排列对应的小写字母,练习者用线条将配对字母连起来。 +- **缺失字母填空**:展示部分字母表序列(如 A B _ D _ F),要求填入缺失字母,巩固字母顺序记忆。 +- **字母迷宫 / 字母涂色**:在迷宫或涂色图案中隐藏目标字母,通过寻找和涂色加深印象。 + +### 综合应用类 + +- **字母+单词抄写**:每行先展示一个字母及对应单词(如 A — Apple),后面是该单词的描红格,从字母过渡到单词书写。 +- **字母排序练习**:给出一组乱序字母,要求按字母表顺序重新抄写排列。 +- **听写预备格**:纯空白四线三格(无任何预设字母),配合家长/老师口述使用,适合听写测试场景。 + +--- ## 生成要求 -字母行统一使用 四线三格,颜色为偏黑色,其中最上边和最下边的线条为实线,中间的两个线条为虚线。 +字母行统一使用四线三格,颜色为偏黑色,其中最上边和最下边的线条为实线,中间的两个线条为虚线。 ## 字母书写顺序资料 diff --git a/miniprogram/englishPages/shared/data/fontProfiles.ts b/miniprogram/englishPages/shared/data/fontProfiles.ts index 2b37659..ef01b63 100644 --- a/miniprogram/englishPages/shared/data/fontProfiles.ts +++ b/miniprogram/englishPages/shared/data/fontProfiles.ts @@ -33,6 +33,8 @@ export interface LetterOverride { /** 一套完整的字体渲染配置 */ export interface FontProfile { + /** 唯一标识,同时用作 wx.loadFontFace 的 family 和按名称查找的 key */ + name: string; /** 字体 URL */ url: string; /** 各分类的默认渲染参数 */ @@ -41,6 +43,11 @@ export interface FontProfile { letterOverrides?: Record; } +/** 构建 CSS font-family 串(含回退字体) */ +export function fontFamilyOf(profile: FontProfile): string { + return `${profile.name}, Roboto, sans-serif`; +} + // ── 字母分类集合 ── const TALL_LOWERCASE = new Set(['b', 'd', 'h', 'k', 'l']); const DESCENDERS = new Set(['g', 'p', 'q', 'y']); @@ -88,88 +95,121 @@ export function getLetterMetrics( /** ZhiyongWrite 手写字体 */ export const ZHIYONG_WRITE: FontProfile = { + name: 'ZhiyongWrite', url: 'https://cdn.joeyone.cn/doodle/fonts/ZhiyongWrite.ttf', + // categories: { + // uppercase: { scale: 0.93, baselineOffset: 0 }, + // tallLower: { scale: 0.95, baselineOffset: 0 }, + // descender: { scale: 1.12, baselineOffset: 0.18 }, + // lowercase: { scale: 0.83, baselineOffset: 0.03 }, + // }, + // letterOverrides: { + // i: { scale: 0.78, baselineOffset: 0.02 }, + // f: { scale: 1.37, baselineOffset: 0.34 }, + // t: { scale: 0.85, baselineOffset: 0 }, + // j: { scale: 1, baselineOffset: 0.15 }, + // }, categories: { - uppercase: { scale: 0.93, baselineOffset: 0 }, - tallLower: { scale: 0.95, baselineOffset: 0 }, - descender: { scale: 1.12, baselineOffset: 0.18 }, - lowercase: { scale: 0.83, baselineOffset: 0.03 }, + uppercase: { scale: 0.95, baselineOffset: 0 }, + tallLower: { scale: 0.85, baselineOffset: 0 }, + descender: { scale: 1, baselineOffset: 0.12 }, + lowercase: { scale: 0.75, baselineOffset: 0 }, }, letterOverrides: { - i: { scale: 0.78, baselineOffset: 0.02 }, - f: { scale: 1.37, baselineOffset: 0.34 }, + i: { scale: 0.8, baselineOffset: 0 }, + f: { scale: 1, baselineOffset: 0.05 }, t: { scale: 0.85, baselineOffset: 0 }, - j: { scale: 1, baselineOffset: 0.15 }, + j: { scale: 0.85, baselineOffset: 0.1 }, }, }; /** Handlee Regular — Google 手写风格 */ export const HANDLEE_REGULAR: FontProfile = { + name: 'HandleeRegular', url: 'https://cdn.joeyone.cn/doodle/fonts/Handlee-Regular.ttf', + // categories: { + // uppercase: { scale: 0.9, baselineOffset: 0 }, + // tallLower: { scale: 0.73, baselineOffset: 0.03 }, + // descender: { scale: 0.8, baselineOffset: 0.05 }, + // lowercase: { scale: 0.73, baselineOffset: 0.02 }, + // }, + // letterOverrides: { + // i: { scale: 0.78, baselineOffset: 0.02 }, + // f: { scale: 0.82, baselineOffset: 0 }, + // t: { scale: 0.77, baselineOffset: 0.04 }, + // j: { scale: 0.85, baselineOffset: 0.08 }, + // }, categories: { uppercase: { scale: 0.9, baselineOffset: 0 }, - tallLower: { scale: 0.73, baselineOffset: 0.03 }, - descender: { scale: 0.8, baselineOffset: 0.05 }, - lowercase: { scale: 0.73, baselineOffset: 0.02 }, + tallLower: { scale: 0.68, baselineOffset: 0 }, + descender: { scale: 0.68, baselineOffset: 0 }, + lowercase: { scale: 0.68, baselineOffset: 0 }, }, letterOverrides: { - i: { scale: 0.78, baselineOffset: 0.02 }, - f: { scale: 0.82, baselineOffset: 0 }, - t: { scale: 0.77, baselineOffset: 0.04 }, - j: { scale: 0.85, baselineOffset: 0.08 }, + i: { scale: 0.68, baselineOffset: 0 }, + f: { scale: 0.68, baselineOffset: 0 }, + t: { scale: 0.68, baselineOffset: 0 }, + j: { scale: 0.68, baselineOffset: 0 }, }, }; /** Print Clearly Regular */ export const PRINT_CLEARLY_REGULAR: FontProfile = { + name: 'PrintClearlyRegular', url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_regular_tt.ttf', categories: { - uppercase: { scale: 0.95, baselineOffset: 0 }, - tallLower: { scale: 0.95, baselineOffset: 0 }, - descender: { scale: 1.05, baselineOffset: 0 }, - lowercase: { scale: 0.85, baselineOffset: 0 }, + uppercase: { scale: 1, baselineOffset: 0 }, + tallLower: { scale: 1, baselineOffset: 0 }, + descender: { scale: 1, baselineOffset: 0 }, + lowercase: { scale: 1, baselineOffset: 0 }, }, letterOverrides: { - f: { scale: 1.35, baselineOffset: 0 }, - t: { scale: 0.85, baselineOffset: 0 }, - j: { scale: 1.15, baselineOffset: 0 }, + i: { scale: 1, baselineOffset: 0 }, + f: { scale: 1, baselineOffset: 0 }, + t: { scale: 1, baselineOffset: 0 }, + j: { scale: 1, baselineOffset: 0 }, }, }; /** Print Clearly Bold */ export const PRINT_CLEARLY_BOLD: FontProfile = { + name: 'PrintClearlyBold', url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_bold_tt.ttf', categories: { - uppercase: { scale: 0.95, baselineOffset: 0 }, - tallLower: { scale: 0.95, baselineOffset: 0 }, - descender: { scale: 1.05, baselineOffset: 0 }, - lowercase: { scale: 0.85, baselineOffset: 0 }, + uppercase: { scale: 1, baselineOffset: 0 }, + tallLower: { scale: 1, baselineOffset: 0 }, + descender: { scale: 1, baselineOffset: 0 }, + lowercase: { scale: 1, baselineOffset: 0 }, }, letterOverrides: { - f: { scale: 1.35, baselineOffset: 0 }, - t: { scale: 0.85, baselineOffset: 0 }, - j: { scale: 1.15, baselineOffset: 0 }, + i: { scale: 1, baselineOffset: 0 }, + f: { scale: 1, baselineOffset: 0 }, + t: { scale: 1, baselineOffset: 0 }, + j: { scale: 1, baselineOffset: 0 }, }, }; /** Print Clearly Dashed — 虚线描红字体 */ export const PRINT_CLEARLY_DASHED: FontProfile = { + name: 'PrintClearlyDashed', url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_dashed_tt.ttf', categories: { - uppercase: { scale: 0.95, baselineOffset: 0 }, - tallLower: { scale: 0.95, baselineOffset: 0 }, - descender: { scale: 1.05, baselineOffset: 0 }, - lowercase: { scale: 0.85, baselineOffset: 0 }, + uppercase: { scale: 1, baselineOffset: 0 }, + tallLower: { scale: 1, baselineOffset: 0 }, + descender: { scale: 1, baselineOffset: 0 }, + lowercase: { scale: 1, baselineOffset: 0 }, }, letterOverrides: { - f: { scale: 1.35, baselineOffset: 0 }, - t: { scale: 0.85, baselineOffset: 0 }, - j: { scale: 1.15, baselineOffset: 0 }, + i: { scale: 1, baselineOffset: 0 }, + f: { scale: 1, baselineOffset: 0 }, + t: { scale: 1, baselineOffset: 0 }, + j: { scale: 1, baselineOffset: 0 }, }, }; /** Num 数字字体 */ export const NUM_FONT: FontProfile = { + name: 'NumFont', url: 'https://cdn.joeyone.cn/doodle/fonts/num.woff2', categories: { uppercase: { scale: 0.95, baselineOffset: 0 }, @@ -184,6 +224,25 @@ export const NUM_FONT: FontProfile = { }, }; -/** 当前激活的字体配置(切换字体只需修改这里) */ -// export const ACTIVE_FONT_PROFILE = ZHIYONG_WRITE; -export const ACTIVE_FONT_PROFILE = HANDLEE_REGULAR; +// ── 字体注册表:按 name 索引所有可用字体 ── + +const FONT_PROFILES: ReadonlyArray = [ + ZHIYONG_WRITE, + HANDLEE_REGULAR, + PRINT_CLEARLY_REGULAR, + PRINT_CLEARLY_BOLD, + PRINT_CLEARLY_DASHED, + NUM_FONT, +]; + +const _profileByName = new Map( + FONT_PROFILES.map((p) => [p.name, p]), +); + +/** 默认字母字体 */ +export const DEFAULT_LETTER_PROFILE = PRINT_CLEARLY_REGULAR; + +/** 按名称查找字体配置,找不到返回默认 */ +export function getFontProfile(name: string): FontProfile { + return _profileByName.get(name) ?? DEFAULT_LETTER_PROFILE; +} diff --git a/miniprogram/englishPages/shared/draw/casePairingDraw.ts b/miniprogram/englishPages/shared/draw/casePairingDraw.ts index 613a9f3..5cb4d05 100644 --- a/miniprogram/englishPages/shared/draw/casePairingDraw.ts +++ b/miniprogram/englishPages/shared/draw/casePairingDraw.ts @@ -5,8 +5,7 @@ import type { } from '../generators/letter-tracing-generator'; import { drawFourLineGrid } from './drawTools'; -const LETTER_FACE = - 'Helvetica, Arial, "Segoe UI", Roboto, sans-serif'; +const LETTER_FACE = 'Helvetica, Arial, "Segoe UI", Roboto, sans-serif'; type CasePairingData = Extract; diff --git a/miniprogram/englishPages/shared/draw/columnCompareDraw.ts b/miniprogram/englishPages/shared/draw/columnCompareDraw.ts index 69a8b04..b27d8b1 100644 --- a/miniprogram/englishPages/shared/draw/columnCompareDraw.ts +++ b/miniprogram/englishPages/shared/draw/columnCompareDraw.ts @@ -3,28 +3,32 @@ import type { LetterTracingData, TracingRow, } from '../generators/letter-tracing-generator'; +import { fontFamilyOf } from '../data/fontProfiles'; import { calcLetterFontSizes, drawFourLineGrid, drawLetterInFourLineGrid, FOUR_LINE_GRID_H, - TOY_FONT_LETTER, type LetterFontSizes, } from './drawTools'; - -const LETTER_FACE = `${TOY_FONT_LETTER}, Roboto, sans-serif`; const COLOR_BLACK = '#1a1a1a'; const COLOR_LIGHT_RED = 'rgba(252, 46, 0, 0.3)'; -type ColumnCompareData = Extract< - LetterTracingData, - { mode: 'column-compare' } ->; +/** 左右大写/小写两栏之间的间距(相对行宽 + 下限,避免过窄屏过小) */ +const COLUMN_GUTTER_RATIO = 0.02; +const COLUMN_GUTTER_MIN = 14; + +/** + * 单栏内字母排版区域占栏宽比例(<1 则同栏字母更紧凑、字间距更小) + */ +const INTRA_COLUMN_LETTER_BAND_RATIO = 1; + +type ColumnCompareData = Extract; /** * 分栏对照绘制: * 四线三格横跨整页宽度,字母分左右两侧书写—— - * 左半写大写(repetitions 个),右半写小写(repetitions 个), + * 左栏大写、右栏小写(各 repetitions 个),中间留间距;同栏内字母略收紧。 * 每侧第一个字母黑色,后续浅红色。 */ export default class ColumnCompareDraw extends BaseDrawService { @@ -52,7 +56,14 @@ export default class ColumnCompareDraw extends BaseDrawService { ); const y0 = this.currentY + rowGap; - const halfW = lineW / 2; + + const columnGutter = Math.max( + COLUMN_GUTTER_MIN, + Math.round(lineW * COLUMN_GUTTER_RATIO), + ); + const halfContentW = (lineW - columnGutter) / 2; + const leftX = M; + const rightX = M + halfContentW + columnGutter; for (let i = 0; i < maxR; i++) { const y = y0 + i * (FOUR_LINE_GRID_H + rowGap); @@ -60,10 +71,22 @@ export default class ColumnCompareDraw extends BaseDrawService { drawFourLineGrid(this.ctx, M, y, lineW, FOUR_LINE_GRID_H); if (i < leftRows.length) { - this.drawLettersInHalf(leftRows[i], M, y, halfW); + this.drawLettersInHalf( + leftRows[i], + leftX, + y, + halfContentW, + INTRA_COLUMN_LETTER_BAND_RATIO, + ); } if (i < rightRows.length) { - this.drawLettersInHalf(rightRows[i], M + halfW, y, halfW); + this.drawLettersInHalf( + rightRows[i], + rightX, + y, + halfContentW, + INTRA_COLUMN_LETTER_BAND_RATIO, + ); } } } @@ -76,16 +99,19 @@ export default class ColumnCompareDraw extends BaseDrawService { row: TracingRow, x: number, y: number, - halfW: number, + columnW: number, + letterBandRatio: number, ) { const n = row.cells.length; if (n === 0) return; - const slotW = halfW / n; + const bandW = columnW * letterBandRatio; + const inset = (columnW - bandW) / 2; + const slotW = bandW / n; for (let i = 0; i < n; i++) { const cell = row.cells[i]; const color = i === 0 ? COLOR_BLACK : COLOR_LIGHT_RED; - const cx = x + slotW * i + slotW / 2; + const cx = x + inset + slotW * i + slotW / 2; drawLetterInFourLineGrid( this.ctx, @@ -94,7 +120,7 @@ export default class ColumnCompareDraw extends BaseDrawService { y, FOUR_LINE_GRID_H, this.letterSizes, - { fontFamily: LETTER_FACE, color }, + { fontFamily: fontFamilyOf(this.letterSizes._profile), color }, ); } } diff --git a/miniprogram/englishPages/shared/draw/drawTools.ts b/miniprogram/englishPages/shared/draw/drawTools.ts index efc9d72..4cdc78e 100644 --- a/miniprogram/englishPages/shared/draw/drawTools.ts +++ b/miniprogram/englishPages/shared/draw/drawTools.ts @@ -1,9 +1,10 @@ /** - * 绘制工具集合(后续可继续扩展其他通用绘制方法) + * 四线三格 & 字母绘制工具集 */ import { - ACTIVE_FONT_PROFILE, + DEFAULT_LETTER_PROFILE, + fontFamilyOf, getLetterMetrics, type FontProfile, } from '../data/fontProfiles'; @@ -11,47 +12,47 @@ import { // ── 四线三格统一高度(所有页面共用) ── export const FOUR_LINE_GRID_H = 43; -// ── 英语字母字体集合 ── -export const TOY_FONT_LETTER = 'ToyLetterFont'; +// ── 字体加载(支持多字体,按 URL 去重) ── -let _fontLoaded = false; -export function loadLetterFont(): Promise { - if (_fontLoaded) return Promise.resolve(); +const _loadedUrls = new Set(); + +/** + * 加载字体到 Canvas native 渲染管线。 + * 同一 URL 只加载一次;不同 FontProfile 可安全并发调用。 + */ +export function loadLetterFont( + profile: FontProfile = DEFAULT_LETTER_PROFILE, +): Promise { + if (_loadedUrls.has(profile.url)) return Promise.resolve(); return new Promise((resolve, reject) => { wx.loadFontFace({ - family: TOY_FONT_LETTER, - source: `url("${ACTIVE_FONT_PROFILE.url}")`, + family: profile.name, + source: `url("${profile.url}")`, scopes: ['native'], success: () => { - _fontLoaded = true; + _loadedUrls.add(profile.url); resolve(); }, fail: (err) => { - console.error('loadFontFace failed', err); + console.error(`loadLetterFont [${profile.name}] failed`, err); reject(err); }, }); }); } -// ── 向后兼容:LetterFontSizes 类型保留,供外部已有调用使用 ── +// ── LetterFontSizes:持有 FontProfile 引用,运行时按字母查表 ── export type LetterFontSizes = { - /** 字体配置(新机制) */ _profile: FontProfile; - /** gridH(用于运行时计算) */ _gridH: number; }; -/** - * 根据四线三格总高度,构建 LetterFontSizes。 - * 内部持有 FontProfile 引用,按字母查表计算。 - */ -export function calcLetterFontSizes(gridH: number): LetterFontSizes { - return { - _profile: ACTIVE_FONT_PROFILE, - _gridH: gridH, - }; +export function calcLetterFontSizes( + gridH: number, + profile: FontProfile = DEFAULT_LETTER_PROFILE, +): LetterFontSizes { + return { _profile: profile, _gridH: gridH }; } /** 根据字母和字体配置,计算对应 fontSize */ @@ -97,7 +98,7 @@ export function drawLetterInFourLineGrid( }, ) { const fontFamily = - options?.fontFamily ?? `${TOY_FONT_LETTER}, Roboto, sans-serif`; + options?.fontFamily ?? fontFamilyOf(sizes._profile); const color = options?.color ?? '#1a1a1a'; const bold = options?.bold ?? false; diff --git a/miniprogram/englishPages/shared/draw/overviewDraw.ts b/miniprogram/englishPages/shared/draw/overviewDraw.ts index 3a2a196..108ea51 100644 --- a/miniprogram/englishPages/shared/draw/overviewDraw.ts +++ b/miniprogram/englishPages/shared/draw/overviewDraw.ts @@ -1,87 +1,133 @@ import { BaseDrawService } from '../../../core/draw/baseDraw'; import type { LetterTracingData } from '../generators/letter-tracing-generator'; -import { drawFourLineGrid } from './drawTools'; - -const LETTER_FACE = - 'Helvetica, Arial, "Segoe UI", Roboto, sans-serif'; +import { + PRINT_CLEARLY_BOLD, + PRINT_CLEARLY_DASHED, + fontFamilyOf, +} from '../data/fontProfiles'; +import { + calcLetterFontSizes, + drawFourLineGrid, + drawLetterInFourLineGrid, + FOUR_LINE_GRID_H, + loadLetterFont, + type LetterFontSizes, +} from './drawTools'; type OverviewData = Extract; +/** 页面左右边距(margin,px),用于排版时留白 */ +const M = 24; +/** 上下两大区(Uppercase / lowercase)之间的间距 */ +const SECTION_GAP = 25; +/** 标题字号 */ +const TITLE_SIZE = 28; +/** 标题与第一行四线三格的间距 */ +const TITLE_GAP = 25; +/** 同一区域内相邻四线三格行之间的间距 */ +const ROW_GAP = 24; +const TITLE_COLOR = '#000'; + +/** 浅红色描红(不透明 hex,避免打印时半透明发灰) */ +const TRACE_LIGHT_RED = '#FF8E8E'; + +const DASHED_PROFILE = PRINT_CLEARLY_DASHED; +const TITLE_PROFILE = PRINT_CLEARLY_BOLD; +const LETTER_FACE = fontFamilyOf(DASHED_PROFILE); +const TITLE_FACE = fontFamilyOf(TITLE_PROFILE); + +/** + * 字母总览:上区大写、下区小写;每区标题 + 4 行四线三格(7+7+6+6)。 + * 列宽按 7 列均分;后两行 6 字左对齐,与前两行逐列对齐(占前 6 列)。 + */ export default class OverviewDraw extends BaseDrawService { + private letterSizes: LetterFontSizes = calcLetterFontSizes( + FOUR_LINE_GRID_H, + DASHED_PROFILE, + ); + async draw(data: OverviewData) { this.prepareDraw(); + await Promise.allSettled([ + loadLetterFont(DASHED_PROFILE), + loadLetterFont(TITLE_PROFILE), + ]); await this.drawHeaderAndDivider(); - this.drawContent(data.upperRows, data.lowerRows); - this.drawPrintFooter(); + this.drawOverviewBody(data.upperRows, data.lowerRows); + await this.drawPrintFooter(); } - private drawContent(upperRows: string[][], lowerRows: string[][]) { - const ctx = this.ctx; - const M = 24; - let y = this.currentY + 6; - const innerW = this.canvasWidth - M * 2; + private drawOverviewBody( + upperRows: string[][], + lowerRows: string[][], + ): void { + const lineW = this.canvasWidth - M * 2; + const slotW = lineW / 7; - const drawTitle = (t: string) => { - ctx.fillStyle = '#111'; - ctx.font = 'bold 17px sans-serif'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - ctx.fillText(t, M, y); - y += 24; - }; + let y = this.currentY + SECTION_GAP - 7; - const drawBand = (rows: string[][]) => { - for (const line of rows) { - const n = line.length; - const gap = 4; - const cellW = (innerW - (n - 1) * gap) / n; - const cellH = 30; - for (let i = 0; i < n; i++) { - const cx = M + i * (cellW + gap); - drawFourLineGrid(ctx, cx, y, cellW, cellH); - this.drawCharInCell( - line[i], - cx, - y, - cellW, - cellH, - 0.9, - true, - ); - } - y += cellH + 8; - } - }; - - drawTitle('Uppercase Letters'); - drawBand(upperRows); - y += 6; - this.drawDashedDivider(y, M, '#bbb'); - y += 12; - drawTitle('lowercase Letters'); - drawBand(lowerRows); + y = this.drawLetterBlock( + 'Uppercase Letters', + upperRows, + y, + lineW, + slotW, + ); + y += SECTION_GAP + 7; + this.drawLetterBlock('lowercase letters', lowerRows, y, lineW, slotW); } - private drawCharInCell( - ch: string, - x: number, - y: number, - w: number, - h: number, - opacity: number, - bold: boolean, - ) { + /** + * 绘制一个分区:居中标题 + 4 行四线三格与字母。 + * @returns 该分区最后一行四线三格底边 Y + */ + private drawLetterBlock( + title: string, + rows: string[][], + startY: number, + lineW: number, + slotW: number, + ): number { const ctx = this.ctx; - if (opacity <= 0) return; + const centerX = M + lineW / 2; + ctx.save(); - ctx.globalAlpha = opacity; - ctx.fillStyle = bold ? '#1a1a1a' : '#4a4a4a'; + ctx.font = `bold ${TITLE_SIZE}px ${TITLE_FACE}`; + ctx.fillStyle = TITLE_COLOR; ctx.textAlign = 'center'; - ctx.textBaseline = 'alphabetic'; - const baselineY = y + h * 0.72; - const fontSize = Math.min(h * 0.72, w * 0.85); - ctx.font = `${bold ? 'bold ' : ''}${fontSize}px ${LETTER_FACE}`; - ctx.fillText(ch, x + w / 2, baselineY); + ctx.textBaseline = 'top'; + ctx.fillText(title, centerX, startY); ctx.restore(); + + let y = startY + TITLE_SIZE + TITLE_GAP; + + for (let r = 0; r < rows.length; r++) { + const chars = rows[r]; + drawFourLineGrid(ctx, M, y, lineW, FOUR_LINE_GRID_H); + + /** 与 7 字行共用列宽 slotW,后两行 6 字左对齐(占前 6 列),与上行逐列对齐 */ + const offsetX = M; + for (let i = 0; i < chars.length; i++) { + const cx = offsetX + slotW * (i + 0.5); + drawLetterInFourLineGrid( + ctx, + chars[i], + cx, + y, + FOUR_LINE_GRID_H, + this.letterSizes, + { + fontFamily: LETTER_FACE, + color: TRACE_LIGHT_RED, + }, + ); + } + y += FOUR_LINE_GRID_H; + if (r < rows.length - 1) { + y += ROW_GAP; + } + } + + return y; } } diff --git a/miniprogram/englishPages/shared/draw/pictureTracingDraw.ts b/miniprogram/englishPages/shared/draw/pictureTracingDraw.ts index 696d5f6..e6a0941 100644 --- a/miniprogram/englishPages/shared/draw/pictureTracingDraw.ts +++ b/miniprogram/englishPages/shared/draw/pictureTracingDraw.ts @@ -4,29 +4,29 @@ import type { LetterTracingData, HighlightGrid, } from '../generators/letter-tracing-generator'; +import { fontFamilyOf } from '../data/fontProfiles'; import { calcLetterFontSizes, drawFourLineGrid, drawLetterInFourLineGrid, FOUR_LINE_GRID_H, getLetterFontSize, - TOY_FONT_LETTER, type LetterFontSizes, } from './drawTools'; const LINE_INK_ALPHA = 'rgba(50, 46, 37, 1)'; const ACCENT_RED = 'rgba(252, 46, 0, 1)'; -//'Helvetica, Arial, "Segoe UI", Roboto, sans-serif'; -const LETTER_FACE = `${TOY_FONT_LETTER}, Roboto, sans-serif`; -// const TITLE_EMOJI_FONT = -// "16px 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif"; const TITLE_TEXT_FONT = '14px sans-serif'; const TITLE_ICON_GAP = 4; type PictureData = Extract; export default class PictureTracingDraw extends BaseDrawService { - private letterSizes: LetterFontSizes = calcLetterFontSizes(FOUR_LINE_GRID_H); + private letterSizes: LetterFontSizes = + calcLetterFontSizes(FOUR_LINE_GRID_H); + private get letterFace(): string { + return fontFamilyOf(this.letterSizes._profile); + } async draw(data: PictureData) { this.prepareDraw(); @@ -68,7 +68,7 @@ export default class PictureTracingDraw extends BaseDrawService { dh, ); } catch { - ctx.font = `bold 60px ${LETTER_FACE}`; + ctx.font = `bold 60px ${this.letterFace}`; ctx.fillStyle = '#111'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; @@ -195,11 +195,11 @@ export default class PictureTracingDraw extends BaseDrawService { const pairGap = 3; const upperFs = getLetterFontSize(upper, sizes); - ctx.font = `${upperFs}px ${LETTER_FACE}`; + ctx.font = `${upperFs}px ${this.letterFace}`; const upperW = ctx.measureText(upper).width; const lowerFs = getLetterFontSize(lower, sizes); - ctx.font = `${lowerFs}px ${LETTER_FACE}`; + ctx.font = `${lowerFs}px ${this.letterFace}`; const lowerW = ctx.measureText(lower).width; const totalPairW = upperW + pairGap + lowerW; @@ -213,7 +213,7 @@ export default class PictureTracingDraw extends BaseDrawService { y, FOUR_LINE_GRID_H, sizes, - { fontFamily: LETTER_FACE, color }, + { fontFamily: this.letterFace, color }, ); drawLetterInFourLineGrid( ctx, @@ -222,7 +222,7 @@ export default class PictureTracingDraw extends BaseDrawService { y, FOUR_LINE_GRID_H, sizes, - { fontFamily: LETTER_FACE, color }, + { fontFamily: this.letterFace, color }, ); ctx.restore(); @@ -235,7 +235,7 @@ export default class PictureTracingDraw extends BaseDrawService { y: number, ) { const ctx = this.ctx; - ctx.font = `36px ${LETTER_FACE}`; + ctx.font = `36px ${this.letterFace}`; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; @@ -286,7 +286,7 @@ export default class PictureTracingDraw extends BaseDrawService { ctx.setLineDash([]); ctx.strokeRect(cx, cy, cellSize, cellSize); - ctx.font = `18px ${LETTER_FACE}`; + ctx.font = `18px ${this.letterFace}`; ctx.fillStyle = '#000'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; @@ -380,7 +380,7 @@ export default class PictureTracingDraw extends BaseDrawService { ); } catch { const fs = Math.min(imgH * 0.6, imgW * 0.18); - ctx.font = `bold ${fs}px ${LETTER_FACE}`; + ctx.font = `bold ${fs}px ${this.letterFace}`; ctx.fillStyle = '#222'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; diff --git a/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts b/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts index 795af27..8642901 100644 --- a/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts +++ b/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts @@ -1,15 +1,19 @@ import { ALPHABET_BY_LETTER, LETTERS_UPPER } from '../data/alphabet'; -/** 字母描红的布局模式(与产品文档四类练习一致) */ +/** 字母描红的布局模式(与产品文档六类练习一致) */ export type LetterTracingMode = - /** 4 看图描红:教学区 + 6 行描红(前三行大写、后三行小写) */ + /** 1 看图识字:教学区 + 描红行(前三行大写、后三行小写) */ | 'picture-tracing' - /** 1 大小写配对:左栏 A–M、右栏 N–Z,每行最多 4 组 Aa */ + /** 2 字母总览:Uppercase / Lowercase 分区,每区四行 7+7+6+6 */ + | 'overview' + /** 3 大小写配对:左栏 A–M、右栏 N–Z,每行最多 4 组 Aa */ | 'case-pairing' - /** 2 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */ + /** 4 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */ | 'column-compare' - /** 3 字母总览:Uppercase / Lowercase 分区,每区 9+9+8 */ - | 'overview'; + /** 5 三字母精练:每页聚焦 3 个字母,各含大写行 + 小写行 */ + | 'triple-focus' + /** 6 单字母逐行:每行一个字母,13 字母半表(A–M 或 N–Z) */ + | 'single-line'; /** 字母大小写模式(部分模式不使用) */ export type LetterCaseMode = 'upper' | 'lower' | 'both'; @@ -24,8 +28,10 @@ export interface LetterTracingGeneratorConfig { selectedLetter?: string; repetitions: number; fadePattern: 'gradient' | 'first-only'; - /** 分栏对照:练习 A–M 或 N–Z */ + /** 分栏对照 / 单字母逐行:练习 A–M 或 N–Z */ alphabetHalf?: AlphabetHalf; + /** 三字母精练:当前页聚焦的 3 个字母(如 ['A','B','C']) */ + tripleLetters?: [string, string, string]; } /** 单个描红格子的数据 */ @@ -184,9 +190,14 @@ function chunk(arr: T[], size: number): T[][] { return out; } -/** 总览区 26 字母拆成 9 + 9 + 8 三行 */ +/** 总览区 26 字母拆成 7 + 7 + 6 + 6 四行(前两行 7 个、后两行 6 个,与四线三格排版一致) */ function overviewRowsFrom(letters: string[]): string[][] { - return [letters.slice(0, 9), letters.slice(9, 18), letters.slice(18, 26)]; + return [ + letters.slice(0, 7), + letters.slice(7, 14), + letters.slice(14, 20), + letters.slice(20, 26), + ]; } /** @@ -194,7 +205,7 @@ function overviewRowsFrom(letters: string[]): string[][] { * - picture-tracing:教学区 + 6 行描红 * - case-pairing:左 A–M / 右 N–Z,每行最多 4 组大小写配对 * - column-compare:半表,左大写右小写,每行 4 个 - * - overview:Uppercase / Lowercase Letters,9+9+8 + * - overview:Uppercase / Lowercase Letters,每区四行 7+7+6+6 */ export function generateLetterTracing( config: LetterTracingGeneratorConfig, @@ -295,5 +306,7 @@ export function mergeLetterTracingConfig( alphabetHalf: (overrides.alphabetHalf ?? base.alphabetHalf ?? 'A-M') as AlphabetHalf, + tripleLetters: overrides.tripleLetters ?? + base.tripleLetters ?? ['A', 'B', 'C'], }; } diff --git a/miniprogram/pages/home/home.data.ts b/miniprogram/pages/home/home.data.ts index b02e3a9..a33e210 100644 --- a/miniprogram/pages/home/home.data.ts +++ b/miniprogram/pages/home/home.data.ts @@ -286,7 +286,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = { icon: '🔠', path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', available: true, - source: 'letter-tracing', + source: 'letter-tracing-single', }, { id: 'target-english-2', diff --git a/miniprogram/utils/downloadPrint.ts b/miniprogram/utils/downloadPrint.ts index 2193ea2..41a02ad 100644 --- a/miniprogram/utils/downloadPrint.ts +++ b/miniprogram/utils/downloadPrint.ts @@ -14,6 +14,25 @@ const STORAGE_KEY_WATCHED_AD_TODAY = 'watchedAdToday'; // 免费下载次数限制 const FREE_DOWNLOAD_LIMIT = 5; +/** + * 开发调试环境:本地 develop 包或开发者工具(含桌面模拟器) + * 此环境下不弹广告、不限制下载次数 + */ +function isDevBypassLimits(): boolean { + try { + const env = wx.getAccountInfoSync().miniProgram.envVersion || 'release'; + if (env === 'develop') { + return true; + } + if (wx.getSystemInfoSync().platform === 'devtools') { + return true; + } + } catch { + // ignore + } + return false; +} + // 激励视频广告单元ID const AD_UNIT_ID = 'adunit-eff5626f8b68c2e0'; @@ -34,7 +53,7 @@ function getTodayDateString(): string { export function getTodayDownloadCount(): number { const today = getTodayDateString(); const downloadDate = wx.getStorageSync(STORAGE_KEY_DOWNLOAD_DATE); - + // 如果不是今天,重置计数 if (downloadDate !== today) { wx.setStorageSync(STORAGE_KEY_DOWNLOAD_COUNT, 0); @@ -42,7 +61,7 @@ export function getTodayDownloadCount(): number { wx.setStorageSync(STORAGE_KEY_WATCHED_AD_TODAY, false); return 0; } - + return wx.getStorageSync(STORAGE_KEY_DOWNLOAD_COUNT) || 0; } @@ -80,19 +99,19 @@ function createVideoAd() { if (!wx.createRewardedVideoAd) { return null; } - + const videoAd = wx.createRewardedVideoAd({ adUnitId: AD_UNIT_ID, }); - + videoAd.onLoad(() => { console.log('激励视频广告加载成功'); }); - + videoAd.onError((err) => { console.error('激励视频广告加载失败', err); }); - + return videoAd; } @@ -102,7 +121,7 @@ function createVideoAd() { function showVideoAd(): Promise { return new Promise((resolve) => { const videoAd = createVideoAd(); - + if (!videoAd) { wx.showToast({ title: '当前环境不支持激励广告', @@ -111,18 +130,18 @@ function showVideoAd(): Promise { resolve(false); return; } - + // 显示 loading wx.showLoading({ title: '加载中...', mask: true, }); - + // 监听广告关闭事件 videoAd.onClose((res) => { // 隐藏 loading(如果还在显示) wx.hideLoading(); - + if (res && res.isEnded) { // 用户完整观看了广告 markWatchedAdToday(); @@ -132,7 +151,7 @@ function showVideoAd(): Promise { resolve(false); } }); - + // 显示广告 videoAd .show() @@ -186,10 +205,10 @@ function doDownload( tracker.reportDownload(options.trackerName); } } - + // 增加下载次数 incrementDownloadCount(); - + // 执行下载(checkAndSaveImage 内部已有成功提示,这里不需要额外处理) checkAndSaveImage(canvas); } @@ -220,23 +239,28 @@ export async function downloadPrint( }); return; } - + + if (isDevBypassLimits()) { + doDownload(canvas, options); + return; + } + // 获取今日下载次数 const downloadCount = getTodayDownloadCount(); const hasWatchedAd = hasWatchedAdToday(); - + // 如果已观看广告,直接下载 if (hasWatchedAd) { doDownload(canvas, options); return; } - + // 如果未超过免费次数,直接下载 if (downloadCount < FREE_DOWNLOAD_LIMIT) { doDownload(canvas, options); return; } - + // 超过免费次数,需要观看广告 const watched = await showVideoAd(); if (watched) { diff --git a/project.private.config.json b/project.private.config.json index 1c1a1c6..cdfe7d1 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -26,10 +26,17 @@ { "name": "englishPages/letterTracing/letterTracing", "pathName": "englishPages/letterTracing/letterTracing", - "query": "id=letter-tracing-full", + "query": "id=letter-tracing-upper-lower", "scene": null, "launchMode": "default" }, + { + "name": "englishPages/letterTracing/letterTracing", + "pathName": "englishPages/letterTracing/letterTracing", + "query": "id=letter-tracing-full", + "launchMode": "default", + "scene": null + }, { "name": "englishPages/letterTracing/letterTracing", "pathName": "englishPages/letterTracing/letterTracing",