feat: 新增幼儿测字表
This commit is contained in:
@@ -54,7 +54,8 @@
|
||||
"pages": [
|
||||
"wordColoring/wordColoring",
|
||||
"handwritingSheet/handwritingSheet",
|
||||
"penControlSheet/penControlSheet"
|
||||
"penControlSheet/penControlSheet",
|
||||
"wordTestSheet/wordTestSheet"
|
||||
],
|
||||
"independent": false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { WORDS } from '../../../core/data/words';
|
||||
|
||||
export const WORD_TEST_GROUP_SIZE = 50;
|
||||
|
||||
export interface WordTestCategory {
|
||||
id: string;
|
||||
icon: string;
|
||||
name: string;
|
||||
parentName?: string;
|
||||
words: string[];
|
||||
}
|
||||
|
||||
export interface WordTestGroup {
|
||||
index: number;
|
||||
label: string;
|
||||
words: string[];
|
||||
}
|
||||
|
||||
const EXCLUDED_CATEGORY_IDS = new Set([4, 5]);
|
||||
|
||||
/** 默认分类:一年级上册 */
|
||||
export const DEFAULT_WORD_TEST_CATEGORY_ID = '26-一年级上册';
|
||||
|
||||
export function buildWordTestCategories(): WordTestCategory[] {
|
||||
const result: WordTestCategory[] = [];
|
||||
|
||||
for (const cat of WORDS) {
|
||||
if (EXCLUDED_CATEGORY_IDS.has(cat.categoryId)) continue;
|
||||
|
||||
if (cat.sections?.length) {
|
||||
for (const section of cat.sections) {
|
||||
result.push({
|
||||
id: `${cat.categoryId}-${section.sectionName}`,
|
||||
icon: cat.icon,
|
||||
name: section.sectionName,
|
||||
parentName: cat.categoryName,
|
||||
words: section.words,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cat.words?.length) {
|
||||
result.push({
|
||||
id: String(cat.categoryId),
|
||||
icon: cat.icon,
|
||||
name: cat.categoryName,
|
||||
words: cat.words,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function buildWordTestGroups(words: string[]): WordTestGroup[] {
|
||||
if (!words.length) return [];
|
||||
|
||||
const groups: WordTestGroup[] = [];
|
||||
for (let i = 0; i < words.length; i += WORD_TEST_GROUP_SIZE) {
|
||||
const index = Math.floor(i / WORD_TEST_GROUP_SIZE) + 1;
|
||||
groups.push({
|
||||
index,
|
||||
label: `第${index}组`,
|
||||
words: words.slice(i, i + WORD_TEST_GROUP_SIZE),
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
export function findWordTestCategory(
|
||||
categories: WordTestCategory[],
|
||||
id: string,
|
||||
): WordTestCategory | undefined {
|
||||
return categories.find((cat) => cat.id === id);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||
|
||||
const COLS = 5;
|
||||
const ROWS = 10;
|
||||
|
||||
const LAYOUT = {
|
||||
topGap: 10,
|
||||
leftMargin: 36,
|
||||
rightMargin: 36,
|
||||
bottomMargin: 40,
|
||||
instructionGap: 18,
|
||||
gridTopGap: 12,
|
||||
instructionFont: 'bold 16px "KaiTi", "STKaiti", "Microsoft Yahei", serif',
|
||||
instructionBoxSize: 12,
|
||||
instructionCheckSize: 14,
|
||||
instructionSymbolGap: 3,
|
||||
rowGap: 10,
|
||||
charBoxGap: 12,
|
||||
boxSize: 14,
|
||||
} as const;
|
||||
|
||||
export interface WordTestSheetDrawData {
|
||||
words: string[];
|
||||
pageNumber: number;
|
||||
}
|
||||
|
||||
export default class WordTestDrawService extends BaseDrawService {
|
||||
constructor(
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, unknown>,
|
||||
) {
|
||||
super(canvas, ctx, {
|
||||
title: '测字表',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
async draw(data: WordTestSheetDrawData) {
|
||||
this.prepareDraw();
|
||||
await this.drawHeaderAndDivider();
|
||||
this.drawInstruction();
|
||||
this.drawWordGrid(data.words);
|
||||
this.drawPageNumber(data.pageNumber);
|
||||
this.drawPrintFooter();
|
||||
}
|
||||
|
||||
private getContentRect() {
|
||||
const contentTop = this.currentY;
|
||||
const contentWidth =
|
||||
this.canvasWidth - LAYOUT.leftMargin - LAYOUT.rightMargin;
|
||||
const contentHeight =
|
||||
this.canvasHeight - contentTop - LAYOUT.bottomMargin;
|
||||
|
||||
return {
|
||||
top: contentTop,
|
||||
left: LAYOUT.leftMargin,
|
||||
width: contentWidth,
|
||||
height: contentHeight,
|
||||
};
|
||||
}
|
||||
|
||||
private drawInstruction() {
|
||||
const { ctx, canvasWidth } = this;
|
||||
const y = this.currentY + LAYOUT.instructionGap;
|
||||
const {
|
||||
instructionFont,
|
||||
instructionBoxSize,
|
||||
instructionCheckSize,
|
||||
instructionSymbolGap,
|
||||
} = LAYOUT;
|
||||
|
||||
const prefix = '认识的字';
|
||||
const middle = '里打';
|
||||
|
||||
ctx.save();
|
||||
ctx.font = instructionFont;
|
||||
ctx.fillStyle = '#322E25';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'left';
|
||||
|
||||
const prefixW = ctx.measureText(prefix).width;
|
||||
const middleW = ctx.measureText(middle).width;
|
||||
const totalW =
|
||||
prefixW +
|
||||
instructionSymbolGap +
|
||||
instructionBoxSize +
|
||||
instructionSymbolGap +
|
||||
middleW +
|
||||
instructionSymbolGap +
|
||||
instructionCheckSize;
|
||||
|
||||
let x = (canvasWidth - totalW) / 2;
|
||||
|
||||
ctx.fillText(prefix, x, y);
|
||||
x += prefixW + instructionSymbolGap;
|
||||
|
||||
this.drawBox(
|
||||
ctx,
|
||||
x,
|
||||
y - instructionBoxSize / 2,
|
||||
instructionBoxSize,
|
||||
instructionBoxSize,
|
||||
);
|
||||
x += instructionBoxSize + instructionSymbolGap;
|
||||
|
||||
ctx.fillText(middle, x, y);
|
||||
x += middleW + instructionSymbolGap;
|
||||
|
||||
this.drawCheckMark(ctx, x, y, instructionCheckSize);
|
||||
|
||||
const metrics = ctx.measureText(prefix);
|
||||
const instructionFontSize = 16;
|
||||
const halfH = Math.max(
|
||||
metrics.actualBoundingBoxAscent ?? instructionFontSize * 0.5,
|
||||
metrics.actualBoundingBoxDescent ?? instructionFontSize * 0.5,
|
||||
);
|
||||
const contentStartY = y + halfH + LAYOUT.gridTopGap;
|
||||
|
||||
ctx.restore();
|
||||
|
||||
this.currentY = contentStartY;
|
||||
}
|
||||
|
||||
/** 绘制对勾(替代文字 V) */
|
||||
private drawCheckMark(
|
||||
ctx: RenderingContext,
|
||||
leftX: number,
|
||||
centerY: number,
|
||||
size: number,
|
||||
) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = '#322E25';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(leftX + size * 0.12, centerY + size * 0.02);
|
||||
ctx.lineTo(leftX + size * 0.38, centerY + size * 0.32);
|
||||
ctx.lineTo(leftX + size * 0.88, centerY - size * 0.34);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
private getCharVerticalMetrics(
|
||||
ctx: RenderingContext,
|
||||
char: string,
|
||||
fontSize: number,
|
||||
) {
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
const metrics = ctx.measureText(char);
|
||||
const ascent = metrics.actualBoundingBoxAscent ?? fontSize * 0.82;
|
||||
const descent = metrics.actualBoundingBoxDescent ?? fontSize * 0.18;
|
||||
return { ascent, descent };
|
||||
}
|
||||
|
||||
/** 以 centerY 为视觉中心,绘制汉字与右侧方框 */
|
||||
private drawCharWithBox(
|
||||
ctx: RenderingContext,
|
||||
char: string,
|
||||
startX: number,
|
||||
centerY: number,
|
||||
fontSize: number,
|
||||
) {
|
||||
const { charBoxGap, boxSize } = LAYOUT;
|
||||
const { ascent, descent } = this.getCharVerticalMetrics(
|
||||
ctx,
|
||||
char,
|
||||
fontSize,
|
||||
);
|
||||
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
const textY = centerY + (ascent - descent) / 2;
|
||||
ctx.fillText(char, startX, textY);
|
||||
|
||||
const boxX = startX + ctx.measureText(char).width + charBoxGap;
|
||||
const boxY = centerY - boxSize / 2;
|
||||
this.drawBox(ctx, boxX, boxY, boxSize, boxSize);
|
||||
}
|
||||
|
||||
private drawWordGrid(words: string[]) {
|
||||
const { ctx } = this;
|
||||
const content = this.getContentRect();
|
||||
const colWidth = content.width / COLS;
|
||||
const rowHeight = content.height / ROWS;
|
||||
const fontSize = 20;
|
||||
|
||||
ctx.save();
|
||||
ctx.font = `${fontSize}px "KaiTi", "STKaiti", "Microsoft Yahei", serif`;
|
||||
ctx.fillStyle = '#322E25';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
|
||||
for (let row = 0; row < ROWS; row++) {
|
||||
for (let col = 0; col < COLS; col++) {
|
||||
const index = row * COLS + col;
|
||||
const char = words[index];
|
||||
if (!char) continue;
|
||||
|
||||
const cellLeft = content.left + col * colWidth;
|
||||
const cellCenterY =
|
||||
content.top + row * rowHeight + rowHeight / 2;
|
||||
|
||||
const charWidth = ctx.measureText(char).width;
|
||||
const groupWidth =
|
||||
charWidth + LAYOUT.charBoxGap + LAYOUT.boxSize;
|
||||
const startX = cellLeft + (colWidth - groupWidth) / 2;
|
||||
|
||||
this.drawCharWithBox(ctx, char, startX, cellCenterY, fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
private drawPageNumber(pageNumber: number) {
|
||||
const { ctx, canvasWidth, canvasHeight } = this;
|
||||
|
||||
ctx.save();
|
||||
ctx.font = '12px "Microsoft Yahei", sans-serif';
|
||||
ctx.fillStyle = '#7C766A';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(String(pageNumber), canvasWidth / 2, canvasHeight - 28);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||
import { WORD_TEST_WORKSHEET_DEFINITIONS } from '../../config/worksheets/wordTest';
|
||||
|
||||
type WordTestWorksheetRow = (typeof WORD_TEST_WORKSHEET_DEFINITIONS)[number];
|
||||
|
||||
const WORKSHEET_BY_ID = Object.fromEntries(
|
||||
WORD_TEST_WORKSHEET_DEFINITIONS.map((m) => [m.id, m]),
|
||||
) as Record<string, WordTestWorksheetRow>;
|
||||
|
||||
export const WORD_TEST_WORKSHEET_ID = WORD_TEST_WORKSHEET_DEFINITIONS[0].id;
|
||||
|
||||
export function getModeInfo(id: string) {
|
||||
const m = WORKSHEET_BY_ID[id];
|
||||
return m ? { title: m.title, desc: m.subtitle } : undefined;
|
||||
}
|
||||
|
||||
export function isValidMode(id: string): boolean {
|
||||
return id in WORKSHEET_BY_ID;
|
||||
}
|
||||
|
||||
export function getPublishMetaByMode(id: string): DebugPublishMeta | null {
|
||||
const m = WORKSHEET_BY_ID[id];
|
||||
if (!m) return null;
|
||||
return {
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
subtitle: m.subtitle,
|
||||
category: 'chinese',
|
||||
subcategory: 'word-test',
|
||||
path: `/chinesePages/wordTestSheet/wordTestSheet?id=${m.id}`,
|
||||
ageMin: m.ageMin,
|
||||
ageMax: m.ageMax,
|
||||
grade: inferGradeFromAge(m.ageMin, m.ageMax),
|
||||
difficulty: m.difficulty,
|
||||
previewImg: '',
|
||||
tags: [...m.tags],
|
||||
isNew: true,
|
||||
isHot: false,
|
||||
sortOrder: m.sortOrder,
|
||||
status: 'draft',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "测字表",
|
||||
"navigationBarBackgroundColor": "#F8F0E0",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#FEF6E7",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"nav-bar": "../../components3.0/nav-bar/nav-bar",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
||||
"debug-publish-tools": "../../components3.0/debug-publish-tools/debug-publish-tools",
|
||||
"toy-icon": "../../toy/icon/icon",
|
||||
"preview-card": "../../components3.0/preview-card/preview-card"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
@import '../../style/theme.less';
|
||||
|
||||
page {
|
||||
background-color: @bg-page;
|
||||
}
|
||||
|
||||
.wt-page {
|
||||
min-height: 100vh;
|
||||
padding: 0 @page-padding-x;
|
||||
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wt-main {
|
||||
padding-top: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.wt-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.wt-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.wt-section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #6d3b00;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
.wt-category-grid,
|
||||
.wt-group-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.wt-category-card,
|
||||
.wt-group-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 28rpx 12rpx;
|
||||
border-radius: @radius-lg;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border: 4rpx solid rgba(179, 172, 159, 0.1);
|
||||
box-shadow: @shadow;
|
||||
transition: transform 0.12s, box-shadow 0.12s, border-color 0.12s,
|
||||
background 0.12s;
|
||||
}
|
||||
|
||||
.wt-group-card {
|
||||
padding: 24rpx 12rpx;
|
||||
}
|
||||
|
||||
.wt-category-card--active,
|
||||
.wt-group-card--active {
|
||||
background: #ffffff;
|
||||
border-color: @brand;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
.wt-category-card--pressed,
|
||||
.wt-group-card--pressed {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.wt-category-card__icon {
|
||||
font-size: 36rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.wt-category-card__name,
|
||||
.wt-group-card__name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: @text-title;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wt-category-card__cat {
|
||||
font-size: 20rpx;
|
||||
color: @text-secondary;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.wt-category-card__check,
|
||||
.wt-group-card__check {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 18rpx;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #9cd343;
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import WordTestDrawService from './draw/wordTestDrawService';
|
||||
import {
|
||||
buildWordTestCategories,
|
||||
buildWordTestGroups,
|
||||
DEFAULT_WORD_TEST_CATEGORY_ID,
|
||||
findWordTestCategory,
|
||||
type WordTestCategory,
|
||||
type WordTestGroup,
|
||||
} from './data/wordTestCategories';
|
||||
import {
|
||||
getModeInfo,
|
||||
getPublishMetaByMode,
|
||||
isValidMode,
|
||||
WORD_TEST_WORKSHEET_ID,
|
||||
} from './wordTestSheet.config';
|
||||
import { createPage, type CanvasDataState } from '../../base/pageMixin';
|
||||
import { defaultShareConfig } from '../../config/config';
|
||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||
import {
|
||||
addFavorite,
|
||||
removeFavorite,
|
||||
batchCheckFavorited,
|
||||
} from '../../utils/favorites';
|
||||
|
||||
const pageInfoLookup = getModeInfo;
|
||||
|
||||
function buildSelectedMap(id: string | null): Record<string, boolean> {
|
||||
if (!id) return {};
|
||||
return { [id]: true };
|
||||
}
|
||||
|
||||
type PageData = CanvasDataState & {
|
||||
worksheetId: string;
|
||||
categoryList: WordTestCategory[];
|
||||
groupList: WordTestGroup[];
|
||||
selectedCategoryId: string;
|
||||
selectedGroupIndex: number;
|
||||
selectedCategoryMap: Record<string, boolean>;
|
||||
selectedGroupMap: Record<string, boolean>;
|
||||
isPreviewFavorite: boolean;
|
||||
isDevEnv: boolean;
|
||||
debugPublishVisible: boolean;
|
||||
debugPublishLoading: boolean;
|
||||
debugPublishMeta: DebugPublishMeta | null;
|
||||
};
|
||||
|
||||
createPage(
|
||||
{
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as WordTestDrawService | null,
|
||||
_favoritedMap: {} as Record<string, boolean>,
|
||||
|
||||
data: {
|
||||
pageTitle: '测字表',
|
||||
functionId: WORD_TEST_WORKSHEET_ID,
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
worksheetId: WORD_TEST_WORKSHEET_ID,
|
||||
categoryList: [] as WordTestCategory[],
|
||||
groupList: [] as WordTestGroup[],
|
||||
selectedCategoryId: DEFAULT_WORD_TEST_CATEGORY_ID,
|
||||
selectedGroupIndex: 1,
|
||||
selectedCategoryMap: buildSelectedMap(
|
||||
DEFAULT_WORD_TEST_CATEGORY_ID,
|
||||
),
|
||||
selectedGroupMap: buildSelectedMap('1'),
|
||||
isPreviewFavorite: false,
|
||||
isDevEnv: false,
|
||||
debugPublishVisible: false,
|
||||
debugPublishLoading: false,
|
||||
debugPublishMeta: null,
|
||||
} as unknown as PageData,
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
this.syncDebugPublishEnv();
|
||||
|
||||
const worksheetId =
|
||||
options.id && isValidMode(options.id)
|
||||
? options.id
|
||||
: WORD_TEST_WORKSHEET_ID;
|
||||
|
||||
const categoryList = buildWordTestCategories();
|
||||
const defaultCategory =
|
||||
findWordTestCategory(
|
||||
categoryList,
|
||||
DEFAULT_WORD_TEST_CATEGORY_ID,
|
||||
) || categoryList[0];
|
||||
const selectedCategoryId = defaultCategory?.id || '';
|
||||
const groupList = buildWordTestGroups(defaultCategory?.words || []);
|
||||
|
||||
this.setData({
|
||||
worksheetId,
|
||||
functionId: worksheetId,
|
||||
categoryList,
|
||||
groupList,
|
||||
selectedCategoryId,
|
||||
selectedGroupIndex: 1,
|
||||
selectedCategoryMap: buildSelectedMap(selectedCategoryId),
|
||||
selectedGroupMap: buildSelectedMap('1'),
|
||||
});
|
||||
|
||||
this.initPageInfo(worksheetId, '测字表');
|
||||
this.loadFavoritedMap();
|
||||
},
|
||||
|
||||
onCanvasReady(e: WechatMiniprogram.CustomEvent) {
|
||||
const category = this.getSelectedCategory();
|
||||
this.initCanvasFromComponent(e.detail, {
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
opts?: Record<string, unknown>,
|
||||
) => new WordTestDrawService(canvas, ctx, opts),
|
||||
drawServiceOptions: {
|
||||
title: this.getSheetTitle(category),
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
getSelectedCategory(): WordTestCategory | undefined {
|
||||
return findWordTestCategory(
|
||||
this.data.categoryList,
|
||||
this.data.selectedCategoryId,
|
||||
);
|
||||
},
|
||||
|
||||
getSelectedGroup(): WordTestGroup | undefined {
|
||||
return (this.data.groupList as WordTestGroup[]).find(
|
||||
(group: WordTestGroup) =>
|
||||
group.index === this.data.selectedGroupIndex,
|
||||
);
|
||||
},
|
||||
|
||||
getSheetTitle(category?: WordTestCategory): string {
|
||||
if (!category) return '测字表';
|
||||
return `${category.name}(测字表)`;
|
||||
},
|
||||
|
||||
async drawCanvas() {
|
||||
if (!this.drawService) return;
|
||||
|
||||
const group = this.getSelectedGroup();
|
||||
if (!group || group.words.length === 0) {
|
||||
this.setData({ hasContent: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const category = this.getSelectedCategory();
|
||||
const title = this.getSheetTitle(category);
|
||||
|
||||
try {
|
||||
(this.drawService as WordTestDrawService).options.title = title;
|
||||
await (this.drawService as WordTestDrawService).draw({
|
||||
words: group.words,
|
||||
pageNumber: group.index,
|
||||
});
|
||||
this.setData({ hasContent: true });
|
||||
} catch (e) {
|
||||
console.error('wordTestSheet draw failed', e);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
onSelectCategory(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id as string;
|
||||
if (!id || id === this.data.selectedCategoryId) return;
|
||||
|
||||
const category = findWordTestCategory(this.data.categoryList, id);
|
||||
if (!category) return;
|
||||
|
||||
const groupList = buildWordTestGroups(category.words);
|
||||
|
||||
this.setData(
|
||||
{
|
||||
selectedCategoryId: id,
|
||||
selectedCategoryMap: buildSelectedMap(id),
|
||||
groupList,
|
||||
selectedGroupIndex: 1,
|
||||
selectedGroupMap: buildSelectedMap('1'),
|
||||
},
|
||||
() => this.drawCanvas(),
|
||||
);
|
||||
},
|
||||
|
||||
onSelectGroup(e: WechatMiniprogram.TouchEvent) {
|
||||
const index = Number(e.currentTarget.dataset.index);
|
||||
if (!index || index === this.data.selectedGroupIndex) return;
|
||||
|
||||
this.setData(
|
||||
{
|
||||
selectedGroupIndex: index,
|
||||
selectedGroupMap: buildSelectedMap(String(index)),
|
||||
},
|
||||
() => this.drawCanvas(),
|
||||
);
|
||||
},
|
||||
|
||||
onPreviewRefresh() {
|
||||
const groupList = this.data.groupList as WordTestGroup[];
|
||||
if (!groupList.length) {
|
||||
this.drawCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIdx = groupList.findIndex(
|
||||
(group) => group.index === this.data.selectedGroupIndex,
|
||||
);
|
||||
const nextIdx =
|
||||
currentIdx < 0 ? 0 : (currentIdx + 1) % groupList.length;
|
||||
const nextGroup = groupList[nextIdx];
|
||||
|
||||
this.setData(
|
||||
{
|
||||
selectedGroupIndex: nextGroup.index,
|
||||
selectedGroupMap: buildSelectedMap(String(nextGroup.index)),
|
||||
},
|
||||
() => this.drawCanvas(),
|
||||
);
|
||||
},
|
||||
|
||||
async onPreviewFavorite() {
|
||||
const next = !this.data.isPreviewFavorite;
|
||||
this.setData({ isPreviewFavorite: next });
|
||||
const id = this.data.worksheetId;
|
||||
if (id) {
|
||||
this._favoritedMap[id] = next;
|
||||
if (next) {
|
||||
addFavorite(id);
|
||||
} else {
|
||||
removeFavorite(id);
|
||||
}
|
||||
}
|
||||
wx.showToast({
|
||||
title: next ? '收藏成功' : '已取消收藏',
|
||||
icon: 'none',
|
||||
});
|
||||
},
|
||||
|
||||
async loadFavoritedMap() {
|
||||
const ids = [WORD_TEST_WORKSHEET_ID];
|
||||
this._favoritedMap = await batchCheckFavorited(ids);
|
||||
if (this._favoritedMap[this.data.worksheetId]) {
|
||||
this.setData({ isPreviewFavorite: true });
|
||||
}
|
||||
},
|
||||
|
||||
getPublishMeta(): DebugPublishMeta {
|
||||
const meta = getPublishMetaByMode(this.data.worksheetId);
|
||||
if (!meta) {
|
||||
throw new Error('当前题型配置不存在');
|
||||
}
|
||||
return meta;
|
||||
},
|
||||
},
|
||||
{
|
||||
shareConfig: defaultShareConfig,
|
||||
pageInfoLookup,
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,83 @@
|
||||
<nav-bar title="测字表" />
|
||||
|
||||
<view class="wt-page">
|
||||
<view class="wt-main">
|
||||
<preview-card
|
||||
id="previewCard"
|
||||
showRefresh="{{true}}"
|
||||
showFavorite="{{true}}"
|
||||
favorited="{{isPreviewFavorite}}"
|
||||
bind:canvas-ready="onCanvasReady"
|
||||
bind:refresh="onPreviewRefresh"
|
||||
bind:favorite="onPreviewFavorite" />
|
||||
|
||||
<view wx:if="{{groupList.length > 0}}" class="wt-section">
|
||||
<view class="wt-section-header">
|
||||
<text class="wt-section-title">选择组</text>
|
||||
</view>
|
||||
<view class="wt-group-grid">
|
||||
<view
|
||||
wx:for="{{groupList}}"
|
||||
wx:key="index"
|
||||
class="wt-group-card {{selectedGroupMap[item.index] ? 'wt-group-card--active' : ''}}"
|
||||
data-index="{{item.index}}"
|
||||
hover-class="wt-group-card--pressed"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
bindtap="onSelectGroup">
|
||||
<view
|
||||
wx:if="{{selectedGroupMap[item.index]}}"
|
||||
class="wt-group-card__check">
|
||||
<toy-icon name="check" size="20rpx" color="#fff" />
|
||||
</view>
|
||||
<text class="wt-group-card__name">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="wt-section">
|
||||
<view class="wt-section-header">
|
||||
<text class="wt-section-title">汉字分类</text>
|
||||
</view>
|
||||
<view class="wt-category-grid">
|
||||
<view
|
||||
wx:for="{{categoryList}}"
|
||||
wx:key="id"
|
||||
class="wt-category-card {{selectedCategoryMap[item.id] ? 'wt-category-card--active' : ''}}"
|
||||
data-id="{{item.id}}"
|
||||
hover-class="wt-category-card--pressed"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
bindtap="onSelectCategory">
|
||||
<text class="wt-category-card__icon">{{item.icon}}</text>
|
||||
<text class="wt-category-card__name">{{item.name}}</text>
|
||||
<text
|
||||
wx:if="{{item.parentName}}"
|
||||
class="wt-category-card__cat"
|
||||
>{{item.parentName}}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<preview-footer-actions
|
||||
disabled="{{!hasContent}}"
|
||||
bind:primary="exportToPrint"
|
||||
bind:secondary="onShare" />
|
||||
|
||||
<debug-publish-tools
|
||||
wx:if="{{isDevEnv && hasContent}}"
|
||||
id="debugPublishTools"
|
||||
visible="{{debugPublishVisible}}"
|
||||
loading="{{debugPublishLoading}}"
|
||||
meta="{{debugPublishMeta}}"
|
||||
bind:open="onOpenDebugPublish"
|
||||
bind:close="onCloseDebugPublish"
|
||||
bind:confirm="onConfirmDebugPublish" />
|
||||
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
@@ -7,10 +7,10 @@
|
||||
// };
|
||||
|
||||
/** 当前应用版本号;profile 页和首页公告共用 */
|
||||
export const APP_VERSION = '3.0.1';
|
||||
export const APP_VERSION = '3.2.0';
|
||||
|
||||
/** 当前版本的更新摘要,用于首页公告;改版本时一起改 */
|
||||
export const APP_VERSION_NOTE = '新增控笔练习';
|
||||
export const APP_VERSION_NOTE = '新增幼儿识字测字表';
|
||||
|
||||
export const defaultPrintConfig: PrintConfig = {
|
||||
// header: 'LogoImage',
|
||||
@@ -31,11 +31,11 @@ export const defaultShareConfig = {
|
||||
* - 当日第 1 次满批:弹分享窗,须分享后才能继续下载
|
||||
* - 当日第 2 次及以后满批:直接播放激励广告解锁
|
||||
*/
|
||||
export const downloadFreeBatchSize = 4;
|
||||
export const downloadFreeBatchSize = 3;
|
||||
|
||||
/**
|
||||
* develop 包下载限制开关
|
||||
* - develop:为 true 时启用下载配额/分享/广告(便于联调);为 false 时不限制
|
||||
* - 体验版 / 正式版:始终启用,不受此开关影响
|
||||
*/
|
||||
export const enableDownloadLimitInDevelop = false;
|
||||
export const enableDownloadLimitInDevelop = true;
|
||||
|
||||
@@ -5,3 +5,4 @@ export { LETTER_TRACING_WORKSHEET_DEFINITIONS } from './letterTracing';
|
||||
export { PINYIN_DICTATION_WORKSHEET_DEFINITIONS } from './pinyin';
|
||||
export { PEN_CONTROL_WORKSHEET_DEFINITIONS } from './penControl';
|
||||
export { WORD_COLORING_WORKSHEET_DEFINITIONS } from './wordColoring';
|
||||
export { WORD_TEST_WORKSHEET_DEFINITIONS } from './wordTest';
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { WorksheetDefinition } from './types';
|
||||
|
||||
export const WORD_TEST_WORKSHEET_DEFINITIONS = [
|
||||
{
|
||||
id: 'word-test-sheet',
|
||||
icon: 'todo-list-o',
|
||||
title: '测字表',
|
||||
subtitle: '按分类测识字,勾选认识的字',
|
||||
ageMin: 5,
|
||||
ageMax: 8,
|
||||
difficulty: 1,
|
||||
tags: ['测字', '识字', '汉字', '一年级'],
|
||||
sortOrder: 38,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<WorksheetDefinition>;
|
||||
+100
-36
@@ -48,9 +48,7 @@ export const WORDS = [
|
||||
'中', '个', '工', '王', '车', '儿', '女', '子', '门', '马', '牛', '羊', '米', '衣', '白',
|
||||
'田', '石', '雨', '电', '云', '花', '草', '叶', '果', '鸟', '虫', '鱼', '头', '目', '耳',
|
||||
'足', '心', '力', '立', '正',
|
||||
// 扩充(常见基础字)
|
||||
'刀', '又', '三', '四', '五', '六', '七', '八', '九', '十', '口', '门', '心', '耳', '目',
|
||||
'牙', '鼻', '口', '眉', '田', '米', '木', '竹', '石', '土', '火', '水', '金'
|
||||
'刀', '又', '三', '四', '五', '六', '七', '八', '九', '十', '牙', '鼻', '眉', '竹', '金',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -62,10 +60,8 @@ export const WORDS = [
|
||||
'灯', '家', '房', '床', '吃', '喝', '坐', '走', '跑', '看', '听', '说', '笑', '哭', '爱',
|
||||
'好', '有', '来', '去', '开', '关', '里', '外', '多', '少', '红', '黄', '蓝', '绿', '白',
|
||||
'黑', '是', '不', '我', '你',
|
||||
// 扩充(贴近生活)
|
||||
'他', '她', '它', '们', '在', '和', '把', '给', '用', '做', '玩', '买', '卖', '学', '读',
|
||||
'写', '问', '答', '听', '看', '吃', '喝', '睡', '起', '穿', '洗', '擦', '开', '关', '拿',
|
||||
'放'
|
||||
'写', '问', '答', '睡', '起', '穿', '洗', '擦', '拿', '放',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -77,17 +73,19 @@ export const WORDS = [
|
||||
'南', '西', '北', '飞', '跳', '游', '唱', '画', '洗', '买', '卖', '问', '学', '读', '写',
|
||||
'早', '晚', '明', '亮', '高', '长', '圆', '方', '快', '乐', '热', '冷', '轻', '重', '新',
|
||||
'旧', '和', '同', '会', '要',
|
||||
// 扩充(自然与常见动作)
|
||||
'雨', '雷', '电', '云', '雾', '露', '霜', '星', '月', '阳', '阴', '晴', '风', '雪', '冰',
|
||||
'草', '花', '叶', '果', '根', '看', '听', '说', '读', '写', '跑', '跳', '走', '爬', '抓',
|
||||
'推', '拉', '抱', '笑', '哭'
|
||||
'雨', '雷', '电', '云', '雾', '露', '霜', '月', '阳', '阴', '晴', '冰',
|
||||
'草', '花', '叶', '果', '根', '跑', '走', '爬', '抓', '推', '拉', '抱', '笑', '哭',
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryId: 3,
|
||||
icon: '🎨',
|
||||
categoryName: '颜色',
|
||||
words: ['红', '蓝', '绿', '黄', '黑', '白', '紫', '橙', '粉', '棕', '灰'],
|
||||
words: [
|
||||
'红', '蓝', '绿', '黄', '黑', '白', '紫', '橙', '粉', '棕', '灰',
|
||||
'青', '翠', '碧', '丹', '金', '银', '铜', '朱', '墨', '绛', '褐',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 4,
|
||||
@@ -123,61 +121,96 @@ export const WORDS = [
|
||||
categoryId: 6,
|
||||
icon: '☀️',
|
||||
categoryName: '日字旁',
|
||||
words: ['日', '明', '早', '时', '晴', '春', '星', '晨', '晚', '晒', '照'],
|
||||
words: [
|
||||
'日', '明', '早', '时', '晴', '春', '星', '晨', '晚', '晒', '照',
|
||||
'旧', '昏', '旦', '旭', '暗', '显', '映', '晌', '昼', '晕', '昌', '易', '昂', '晶', '暄',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 7,
|
||||
icon: '💧',
|
||||
categoryName: '三点水',
|
||||
words: ['水', '江', '河', '流', '沙', '洗', '海', '汗', '汽', '澡', '泡', '湖', '泉'],
|
||||
words: [
|
||||
'水', '江', '河', '湖', '海', '泉', '汗', '汽', '沙', '洗', '波', '流', '游', '洪', '浴',
|
||||
'浪', '池', '泡', '澡', '油', '洋', '溪', '泥', '深', '浅', '泪', '洁', '温', '漂', '混',
|
||||
'涨', '滴', '港', '渔', '涛', '润', '消', '清', '渡', '洞',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 8,
|
||||
icon: '🐦',
|
||||
categoryName: '鸟字边',
|
||||
words: ['鸟', '鸡', '鸭', '鹅', '鸣', '鸽', '鸦', '鹊', '鹤'],
|
||||
categoryName: '鸟字旁',
|
||||
words: [
|
||||
'鸟', '鸡', '鸭', '鹅', '鸣', '鸽', '鸦', '鹊', '鹤', '鸳', '鸯', '鹰', '鹏', '鹦',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 9,
|
||||
icon: '🐾',
|
||||
categoryName: '反犬旁',
|
||||
words: ['狗', '猫', '猴', '狮', '狼', '猪', '狠', '独', '犯'],
|
||||
words: [
|
||||
'狗', '猫', '猴', '狮', '狼', '猪', '狠', '独', '犯',
|
||||
'猜', '猩', '猛', '猿', '猾', '猬', '猎', '狐', '狂', '猢',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 10,
|
||||
icon: '🪱',
|
||||
categoryName: '虫字旁',
|
||||
words: ['虫', '蚁', '蝶', '蜻', '蛙', '蛇', '蜘', '蜂', '蚊', '蛾'],
|
||||
words: [
|
||||
'虫', '蚁', '蚂', '蚊', '蚕', '蛹', '蛙', '蛾', '蛇', '蝶',
|
||||
'蜻', '蜓', '蜘', '蛛', '蜂', '蝉', '螃', '蟹', '螺', '蝌',
|
||||
'蝇', '螳', '蟋', '蟀',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 11,
|
||||
icon: '🌧️',
|
||||
categoryName: '雨字头',
|
||||
words: ['雨', '雪', '雷', '露', '雾', '雹', '霜', '雯', '霖'],
|
||||
words: [
|
||||
'雨', '雪', '雷', '露', '雾', '雹', '霜', '震', '零', '需', '霞', '霓', '霁',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 12,
|
||||
icon: '🌲',
|
||||
categoryName: '木字旁',
|
||||
words: ['木', '林', '树', '桃', '森', '松', '桥', '枝', '板', '柜', '杯', '校'],
|
||||
words: [
|
||||
'木', '林', '树', '桃', '森', '松', '桥', '枝', '板', '柜', '杯', '校',
|
||||
'柳', '柏', '杨', '枫', '桐', '桂', '榕', '杉',
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryId: 13,
|
||||
icon: '👄',
|
||||
categoryName: '口字旁',
|
||||
words: ['口', '吃', '叫', '唱', '听', '吹', '叶', '和', '问', '品', '吐', '吸'],
|
||||
words: [
|
||||
'口', '吃', '叫', '唱', '听', '吹', '叶', '和', '问', '品', '吐', '吸', '嘴', '咽', '喉',
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryId: 14,
|
||||
icon: '🧍',
|
||||
categoryName: '单人旁',
|
||||
words: ['你', '他', '们', '作', '休', '住', '伙', '伴', '体', '位', '信', '化'],
|
||||
words: [
|
||||
'你', '他', '们', '作', '休', '住', '伙', '伴', '体', '位', '信', '化',
|
||||
'保', '什', '但', '代', '使', '俩', '便', '仰', '传', '伤', '伯', '佳',
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryId: 15,
|
||||
icon: '❤️',
|
||||
categoryName: '竖心旁',
|
||||
words: ['心', '情', '快', '怕', '惊', '忙', '怀', '爱', '想', '念'],
|
||||
words: [
|
||||
'心', '情', '快', '怕', '惊', '忙', '怀', '爱', '想', '念',
|
||||
'意', '愿', '态', '怪', '性', '悟', '惜', '懒', '慌',
|
||||
],
|
||||
},
|
||||
|
||||
// 新增适龄分类
|
||||
@@ -186,7 +219,7 @@ export const WORDS = [
|
||||
icon: '🧭',
|
||||
categoryName: '方位方向',
|
||||
words: [
|
||||
'上', '下', '左', '右', '前', '后', '里', '外', '东', '南', '西', '北', '中', '近', '远'
|
||||
'上', '下', '左', '右', '前', '后', '里', '外', '东', '南', '西', '北', '中', '近', '远',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -195,7 +228,7 @@ export const WORDS = [
|
||||
categoryName: '身体部位',
|
||||
words: [
|
||||
'头', '脸', '目', '眼', '眉', '鼻', '口', '牙', '舌', '耳', '手', '指', '掌', '臂',
|
||||
'足', '腿', '心'
|
||||
'足', '腿', '心', '发', '肩', '背', '肚', '腰', '皮', '骨', '血', '身',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -204,14 +237,22 @@ export const WORDS = [
|
||||
categoryName: '常见动物',
|
||||
words: [
|
||||
'狗', '猫', '马', '牛', '羊', '鸡', '鸭', '鹅', '鱼', '兔', '猪', '熊', '虎', '鹿', '猴',
|
||||
'狼', '狮', '虎', '豹', '象',
|
||||
'狼', '狮', '豹', '象', '鸟', '龟', '蛙', '蛇', '鼠', '狐', '狸', '蝶', '蜂', '蚁', '蚕',
|
||||
'蝉', '蛛', '雀', '鸽', '鹰', '虾',
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryId: 19,
|
||||
icon: '🌼',
|
||||
categoryName: '常见植物',
|
||||
words: ['花', '草', '树', '叶', '果', '根', '竹', '松', '柳', '桃', '梅', '荷', '菊'],
|
||||
words: [
|
||||
'花', '草', '树', '叶', '果', '根', '竹', '松', '柳', '桃', '梅', '荷', '菊',
|
||||
'兰', '桂', '杏', '槐', '柏', '杨', '榕', '杉', '枫', '桐', '瓜', '豆',
|
||||
'蔬', '菜', '麦', '稻', '谷', '苹', '李', '梨', '橙', '柚', '柿', '葡',
|
||||
'萄', '西', '荔', '枝', '椰', '蕉', '菠', '萝', '辣', '椒', '茄', '芹',
|
||||
'卜', '葱', '姜', '蒜', '薯', '蘑', '菇', '笋', '苔', '蔓', '藕',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 20,
|
||||
@@ -219,40 +260,65 @@ export const WORDS = [
|
||||
categoryName: '食物饮品',
|
||||
words: [
|
||||
'米', '饭', '面', '菜', '果', '肉', '蛋', '奶', '糖', '盐', '油', '水', '茶', '汤',
|
||||
'粥', '饼'
|
||||
'粥', '饼', '薯', '菇', '竹', '笋', '苔', '蔓', '荷', '藕',
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryId: 21,
|
||||
icon: '🚗',
|
||||
categoryName: '交通出行',
|
||||
words: ['车', '船', '飞', '机', '站', '路', '桥', '铁', '轨', '轮'],
|
||||
words: [
|
||||
'车', '船', '飞', '机', '站', '路', '桥', '铁', '轨', '轮',
|
||||
'汽', '地', '高', '速', '动', '单', '双',
|
||||
'驾', '乘', '骑', '停', '票', '码', '行', '驶', '程', '过',
|
||||
'隧', '道', '转', '步', '街', '灯', '红', '绿', '黄',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 22,
|
||||
icon: '📚',
|
||||
categoryName: '校园与文具',
|
||||
words: ['书', '本', '笔', '尺', '刀', '纸', '包', '课', '桌', '椅', '图', '画', '作', '业'],
|
||||
words: [
|
||||
'书', '本', '笔', '尺', '刀', '纸', '包', '课', '桌', '椅', '图', '画', '作', '业',
|
||||
'教', '室', '校', '园', '板', '黑', '白', '擦', '讲', '台', '钟', '铃',
|
||||
'练', '习', '册', '橡', '皮', '胶', '垫', '盒', '袋', '卷',
|
||||
'订', '夹', '筒', '水', '彩', '颜', '料', '文', '具', '铅',
|
||||
'钢', '毛', '红', '蓝', '字', '帖', '典', '母', '材', '布', '贴', '考', '试', '印', '章', '机',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 23,
|
||||
icon: '⏰',
|
||||
categoryName: '时间与季节',
|
||||
words: [
|
||||
'日', '月', '年', '时', '分', '秒', '早', '晚', '今', '明', '昨', '春', '夏', '秋', '冬'
|
||||
'日', '月', '年', '岁', '时', '分', '秒', '晨', '午', '晚', '早', '暮', '夜', '昼',
|
||||
'今', '明', '昨', '春', '夏', '秋', '冬', '季', '节', '阳', '阴',
|
||||
'钟', '点', '刻', '旬', '期', '当', '初', '终', '始', '末', '再', '曾', '旧', '新',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 24,
|
||||
icon: '🟠',
|
||||
categoryName: '形状与图形',
|
||||
words: ['点', '线', '面', '圆', '方', '角', '弧', '长', '宽', '高'],
|
||||
words: [
|
||||
'点', '线', '面', '角', '边', '体', '圆', '方', '长', '宽', '高', '深', '矩', '形',
|
||||
'弧', '棱', '柱', '锥', '球', '扇', '环', '扁', '厚', '薄',
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
categoryId: 25,
|
||||
icon: '📏',
|
||||
categoryName: '量词常用',
|
||||
words: ['个', '只', '匹', '条', '朵', '棵', '片', '张', '本', '杯', '块', '双'],
|
||||
categoryName: '常用量词',
|
||||
words: [
|
||||
'个', '只', '匹', '条', '朵', '棵', '片', '张', '本', '杯', '块', '双',
|
||||
'头', '位', '节', '根', '座', '群', '瓶', '颗', '段', '辆', '架',
|
||||
'支', '面', '粒', '把', '首', '封', '页', '箱', '队',
|
||||
],
|
||||
|
||||
},
|
||||
];
|
||||
|
||||
@@ -263,9 +329,7 @@ export function getCategoryTabIndex(categoryId: number): number {
|
||||
}
|
||||
|
||||
/** 汇总分类下全部汉字(含一二年级 sections) */
|
||||
export function collectCategoryWords(
|
||||
cat: (typeof WORDS)[number],
|
||||
): string[] {
|
||||
export function collectCategoryWords(cat: (typeof WORDS)[number]): string[] {
|
||||
if (cat.words?.length) {
|
||||
return cat.words;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,37 @@ import drawHeader from './drawHeader';
|
||||
const LINE_COLOR = '#BCBAB2'; // 打印友好的深灰线色,避免渐变色
|
||||
const LINE_WIDTH = 3;
|
||||
|
||||
/** 品牌水印模式:A 右下角醒目;B 页面中央大号旋转浅水印(默认) */
|
||||
export type BrandWatermarkMode = 'A' | 'B';
|
||||
|
||||
export interface DrawPrintFooterOptions {
|
||||
/** 默认 B */
|
||||
mode?: BrandWatermarkMode;
|
||||
/** 品牌文案,默认「涂鸦丫小程序」 */
|
||||
text?: string;
|
||||
opacity?: number;
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
const BRAND_TEXT = '涂鸦丫小程序';
|
||||
const BRAND_COLOR = '#7C766A';
|
||||
|
||||
/** 方案 A:右下角单行品牌水印 */
|
||||
const FOOTER_MODE_A = {
|
||||
fontSize: 20,
|
||||
opacity: 0.12,
|
||||
marginRight: 24,
|
||||
marginBottom: 20,
|
||||
} as const;
|
||||
|
||||
/** 方案 B:页面中央大号旋转浅水印,单行居中 */
|
||||
const FOOTER_MODE_B = {
|
||||
fontSize: 32,
|
||||
opacity: 0.12,
|
||||
rotationDeg: -25,
|
||||
centerYRatio: 0.52,
|
||||
} as const;
|
||||
|
||||
export class BaseDrawService {
|
||||
canvas: WechatMiniprogram.Canvas;
|
||||
ctx: RenderingContext;
|
||||
@@ -174,24 +205,69 @@ export class BaseDrawService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 页脚:页面底部居中绘制品牌文案「涂鸦丫小程序」(粗体)。
|
||||
* 品牌水印页脚(默认「涂鸦丫小程序」单行)。
|
||||
* - A:右下角水印,适合有底部页码的页面
|
||||
* - B:页面中央大号旋转浅水印(默认)
|
||||
* 请在整页正文绘制完成后调用,避免被内容覆盖。
|
||||
*/
|
||||
async drawPrintFooter(): Promise<void> {
|
||||
drawPrintFooter(options?: DrawPrintFooterOptions): void {
|
||||
const mode = options?.mode ?? 'A';
|
||||
const text = options?.text ?? BRAND_TEXT;
|
||||
|
||||
if (mode === 'A') {
|
||||
this.drawPrintFooterModeA(text, options);
|
||||
} else {
|
||||
this.drawPrintFooterModeB(text, options);
|
||||
}
|
||||
}
|
||||
|
||||
/** 方案 A:右下角单行品牌水印 */
|
||||
private drawPrintFooterModeA(
|
||||
text: string,
|
||||
options?: DrawPrintFooterOptions,
|
||||
): void {
|
||||
const { ctx, canvasWidth, canvasHeight } = this;
|
||||
const text = '涂鸦丫小程序';
|
||||
const bottomMargin = 12;
|
||||
const fontSize = 13;
|
||||
const fontSize = options?.fontSize ?? FOOTER_MODE_A.fontSize;
|
||||
const opacity = options?.opacity ?? FOOTER_MODE_A.opacity;
|
||||
const { marginRight, marginBottom } = FOOTER_MODE_A;
|
||||
const font = `bold ${fontSize}px "Microsoft Yahei", sans-serif`;
|
||||
const textColor = '#7C766A';
|
||||
|
||||
ctx.save();
|
||||
ctx.font = font;
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.fillStyle = BRAND_COLOR;
|
||||
ctx.globalAlpha = opacity;
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.fillText(
|
||||
text,
|
||||
canvasWidth - marginRight,
|
||||
canvasHeight - marginBottom,
|
||||
);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/** 方案 B:页面中央大号旋转浅水印,单行居中 */
|
||||
private drawPrintFooterModeB(
|
||||
text: string,
|
||||
options?: DrawPrintFooterOptions,
|
||||
): void {
|
||||
const { ctx, canvasWidth, canvasHeight } = this;
|
||||
const fontSize = options?.fontSize ?? FOOTER_MODE_B.fontSize;
|
||||
const opacity = options?.opacity ?? FOOTER_MODE_B.opacity;
|
||||
const { rotationDeg, centerYRatio } = FOOTER_MODE_B;
|
||||
const font = `bold ${fontSize}px "Microsoft Yahei", sans-serif`;
|
||||
const centerX = canvasWidth / 2;
|
||||
const centerY = canvasHeight * centerYRatio;
|
||||
|
||||
ctx.save();
|
||||
ctx.font = font;
|
||||
ctx.fillStyle = BRAND_COLOR;
|
||||
ctx.globalAlpha = opacity;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
const y = canvasHeight - bottomMargin - fontSize / 2;
|
||||
ctx.fillText(text, canvasWidth / 2, y);
|
||||
ctx.translate(centerX, centerY);
|
||||
ctx.rotate((rotationDeg * Math.PI) / 180);
|
||||
ctx.fillText(text, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,4 +13,10 @@ export const UNRELEASED_PAGE_ENTRIES: UnreleasedPageEntry[] = [
|
||||
subtitle: '控笔练习',
|
||||
path: '/chinesePages/penControlSheet/penControlSheet',
|
||||
},
|
||||
{
|
||||
id: 'word-test-sheet',
|
||||
title: '测字表',
|
||||
subtitle: '测字表',
|
||||
path: '/chinesePages/wordTestSheet/wordTestSheet',
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user