feat: worksheet发布功能开发
This commit is contained in:
@@ -3,6 +3,12 @@ import { downloadPrint } from '../utils/downloadPrint';
|
||||
import { BaseDrawService } from '../core/draw/baseDraw';
|
||||
import tracker from '../utils/tracker';
|
||||
import { defaultShareConfig } from '../config/config';
|
||||
import {
|
||||
buildWorksheetPreviewCloudPath,
|
||||
isDebugPublishEnabled,
|
||||
type DebugPublishConfirmDetail,
|
||||
type DebugPublishMeta,
|
||||
} from '../utils/debugPublish';
|
||||
|
||||
/**
|
||||
* Canvas 相关的页面实例属性
|
||||
@@ -17,6 +23,8 @@ export interface PageCanvasInstance {
|
||||
setData(data: any, callback?: () => void): void;
|
||||
route: string;
|
||||
getShareOptions(): ShareOptions;
|
||||
selectComponent(selector: string): any;
|
||||
getPublishMeta?(): DebugPublishMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,6 +42,10 @@ export interface CanvasDataState {
|
||||
data: any;
|
||||
setData(data: any, callback?: () => void): void;
|
||||
route: string;
|
||||
isDevEnv?: boolean;
|
||||
debugPublishVisible?: boolean;
|
||||
debugPublishLoading?: boolean;
|
||||
debugPublishMeta?: DebugPublishMeta | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,6 +287,159 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
syncDebugPublishEnv(this: PageCanvasInstance) {
|
||||
this.setData({ isDevEnv: isDebugPublishEnabled() });
|
||||
},
|
||||
|
||||
onOpenDebugPublish(this: PageCanvasInstance) {
|
||||
if (!isDebugPublishEnabled()) return;
|
||||
if (typeof this.getPublishMeta !== 'function') {
|
||||
wx.showToast({
|
||||
title: '页面未实现发布元数据',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const meta = this.getPublishMeta();
|
||||
this.setData({
|
||||
isDevEnv: true,
|
||||
debugPublishVisible: true,
|
||||
debugPublishMeta: meta,
|
||||
});
|
||||
} catch (error) {
|
||||
wx.showToast({
|
||||
title:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: '准备发布数据失败',
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onCloseDebugPublish(this: PageCanvasInstance) {
|
||||
this.setData({ debugPublishVisible: false });
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认发布
|
||||
*/
|
||||
async onConfirmDebugPublish(
|
||||
this: PageCanvasInstance,
|
||||
e: WechatMiniprogram.CustomEvent<DebugPublishConfirmDetail>,
|
||||
) {
|
||||
const previewCard = this.selectComponent('#previewCard');
|
||||
const debugPublishTools =
|
||||
this.selectComponent('#debugPublishTools');
|
||||
const baseMeta =
|
||||
this.data.debugPublishMeta ||
|
||||
(typeof this.getPublishMeta === 'function'
|
||||
? this.getPublishMeta()
|
||||
: null);
|
||||
|
||||
if (!previewCard?.exportToTempFile) {
|
||||
wx.showToast({
|
||||
title: '预览组件不可用',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!debugPublishTools?.processImage) {
|
||||
wx.showToast({
|
||||
title: '发布工具不可用',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!baseMeta) {
|
||||
wx.showToast({
|
||||
title: '缺少发布元数据',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const detail = e.detail;
|
||||
const publishMeta: DebugPublishMeta = {
|
||||
...baseMeta,
|
||||
title: detail.meta.title,
|
||||
subtitle: detail.meta.subtitle,
|
||||
tags: detail.meta.tags,
|
||||
status: detail.meta.status,
|
||||
};
|
||||
|
||||
this.setData({ debugPublishLoading: true });
|
||||
wx.showLoading({ title: '发布中...' });
|
||||
|
||||
try {
|
||||
const sourcePath = await previewCard.exportToTempFile({
|
||||
fileType: 'jpg',
|
||||
quality: 1,
|
||||
});
|
||||
|
||||
console.log('sourcePath', sourcePath);
|
||||
|
||||
const processed = await debugPublishTools.processImage({
|
||||
sourcePath,
|
||||
settings: detail.settings,
|
||||
});
|
||||
console.log('processed', processed);
|
||||
|
||||
const cloudPath = buildWorksheetPreviewCloudPath(
|
||||
publishMeta.category,
|
||||
publishMeta.id,
|
||||
);
|
||||
console.log('cloudPath', cloudPath);
|
||||
const uploadRes = await wx.cloud.uploadFile({
|
||||
cloudPath,
|
||||
filePath: processed.tempFilePath,
|
||||
});
|
||||
console.log('uploadRes', uploadRes);
|
||||
|
||||
const cloudCall = (await wx.cloud.callFunction({
|
||||
name: 'worksheetsPublish',
|
||||
data: {
|
||||
...publishMeta,
|
||||
previewImg: uploadRes.fileID,
|
||||
},
|
||||
})) as {
|
||||
result?: { success?: boolean; message?: string };
|
||||
};
|
||||
console.log('cloudCall', cloudCall);
|
||||
if (!cloudCall.result?.success) {
|
||||
throw new Error(
|
||||
cloudCall.result?.message || '云端写入失败',
|
||||
);
|
||||
}
|
||||
console.log('success');
|
||||
this.setData({
|
||||
debugPublishVisible: false,
|
||||
debugPublishMeta: {
|
||||
...publishMeta,
|
||||
previewImg: uploadRes.fileID,
|
||||
},
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: `发布成功 ${Math.round(processed.size / 1024)}KB`,
|
||||
icon: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('onConfirmDebugPublish error', error);
|
||||
wx.showToast({
|
||||
title: error instanceof Error ? error.message : '发布失败',
|
||||
icon: 'none',
|
||||
});
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
this.setData({ debugPublishLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化页面信息
|
||||
*
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"styleIsolation": "isolated",
|
||||
"usingComponents": {
|
||||
"van-popup": "@vant/weapp/popup/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
.debug-publish-tools {
|
||||
.debug-publish-tools__fab {
|
||||
position: fixed;
|
||||
right: 24rpx;
|
||||
bottom: calc(136rpx + env(safe-area-inset-bottom));
|
||||
z-index: 40;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
padding: 20rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(50, 46, 37, 0.92);
|
||||
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.debug-publish-tools__fab--hover {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.debug-publish-tools__fab-icon {
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.debug-publish-tools__fab-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.debug-publish-tools__popup {
|
||||
width: 680rpx;
|
||||
max-height: 82vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.debug-publish-tools__content {
|
||||
max-height: 82vh;
|
||||
padding: 56rpx 32rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.debug-publish-tools__title {
|
||||
display: block;
|
||||
margin-bottom: 28rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
color: #322e25;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.debug-publish-tools__section + .debug-publish-tools__section {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.debug-publish-tools__section-title {
|
||||
display: block;
|
||||
margin-bottom: 18rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #7c766a;
|
||||
}
|
||||
|
||||
.debug-publish-tools__meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.debug-publish-tools__meta-item,
|
||||
.debug-publish-tools__path-box,
|
||||
.debug-publish-tools__field {
|
||||
padding: 18rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #f7f1e6;
|
||||
}
|
||||
|
||||
.debug-publish-tools__path-box {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.debug-publish-tools__meta-label,
|
||||
.debug-publish-tools__field-label {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #8a8478;
|
||||
}
|
||||
|
||||
.debug-publish-tools__meta-value,
|
||||
.debug-publish-tools__path {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #322e25;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.debug-publish-tools__field + .debug-publish-tools__field {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.debug-publish-tools__input {
|
||||
width: 100%;
|
||||
margin-top: 12rpx;
|
||||
min-height: 44rpx;
|
||||
font-size: 26rpx;
|
||||
color: #322e25;
|
||||
}
|
||||
|
||||
.debug-publish-tools__chip-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
|
||||
.debug-publish-tools__chip-row--wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-publish-tools__chip {
|
||||
min-width: 120rpx;
|
||||
padding: 16rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ebe3d4;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #605b50;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.debug-publish-tools__chip--active {
|
||||
background: #f7ce00;
|
||||
color: #453900;
|
||||
}
|
||||
|
||||
.debug-publish-tools__hint {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
color: #8a8478;
|
||||
}
|
||||
|
||||
.debug-publish-tools__actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.debug-publish-tools__btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.debug-publish-tools__btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.debug-publish-tools__btn--ghost {
|
||||
background: #f0eadf;
|
||||
color: #605b50;
|
||||
}
|
||||
|
||||
.debug-publish-tools__btn--primary {
|
||||
background: linear-gradient(135deg, #f7ee47 0%, #f0ca08 100%);
|
||||
color: #453900;
|
||||
}
|
||||
|
||||
.debug-publish-tools__processor {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.debug-publish-tools__processor-canvas {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
import {
|
||||
DEBUG_PUBLISH_DEFAULT_QUALITY,
|
||||
DEBUG_PUBLISH_DEFAULT_WIDTH,
|
||||
DEBUG_PUBLISH_MAX_SIZE_KB,
|
||||
clampPublishQuality,
|
||||
clampPublishWidth,
|
||||
getCropRatios,
|
||||
normalizeTagsInput,
|
||||
type DebugCropMode,
|
||||
type DebugProcessImageParams,
|
||||
type DebugProcessImageResult,
|
||||
type DebugPublishConfirmDetail,
|
||||
type DebugPublishMeta,
|
||||
} from '../../utils/debugPublish';
|
||||
|
||||
/**
|
||||
* Debug 发布工具组件。
|
||||
*
|
||||
* 组件只负责两件事:
|
||||
* 1. 展示调试发布的 UI(悬浮按钮 + 弹窗表单)
|
||||
* 2. 用隐藏 canvas 处理预览图的裁剪与压缩
|
||||
*
|
||||
* 真正的上传云存储、调用云函数写库仍由页面层处理,
|
||||
* 这样同一套组件可以复用到多个绘制页面。
|
||||
*/
|
||||
Component({
|
||||
properties: {
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
meta: {
|
||||
type: Object,
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
formTitle: '',
|
||||
formSubtitle: '',
|
||||
formTags: '',
|
||||
formStatus: 'draft',
|
||||
formCropMode: 'header-footer',
|
||||
formQuality: DEBUG_PUBLISH_DEFAULT_QUALITY,
|
||||
formWidth: DEBUG_PUBLISH_DEFAULT_WIDTH,
|
||||
maxSizeKB: DEBUG_PUBLISH_MAX_SIZE_KB,
|
||||
},
|
||||
|
||||
observers: {
|
||||
visible(visible: boolean) {
|
||||
if (visible) {
|
||||
this.syncFormState(
|
||||
(this.properties as any).meta as DebugPublishMeta | null,
|
||||
);
|
||||
}
|
||||
},
|
||||
meta(meta: DebugPublishMeta | null) {
|
||||
if ((this.properties as any).visible) {
|
||||
this.syncFormState(meta);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
ready() {
|
||||
this.initProcessorCanvas();
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** 打开弹窗时,用当前题型元数据重置表单。 */
|
||||
syncFormState(meta: DebugPublishMeta | null) {
|
||||
this.setData({
|
||||
formTitle: meta?.title ?? '',
|
||||
formSubtitle: meta?.subtitle ?? '',
|
||||
formTags: meta?.tags?.join(', ') ?? '',
|
||||
formStatus: meta?.status ?? 'draft',
|
||||
formCropMode: 'header-footer',
|
||||
formQuality: DEBUG_PUBLISH_DEFAULT_QUALITY,
|
||||
formWidth: DEBUG_PUBLISH_DEFAULT_WIDTH,
|
||||
});
|
||||
},
|
||||
|
||||
/** 点击悬浮发布按钮。挂载时机已由页面层控制,这里无需再二次判断 enabled。 */
|
||||
onOpen() {
|
||||
this.triggerEvent('open');
|
||||
},
|
||||
|
||||
/** 关闭发布弹窗。 */
|
||||
onClose() {
|
||||
this.triggerEvent('close');
|
||||
},
|
||||
|
||||
onTitleInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ formTitle: (e.detail.value ?? '').trim() });
|
||||
},
|
||||
|
||||
onSubtitleInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ formSubtitle: (e.detail.value ?? '').trim() });
|
||||
},
|
||||
|
||||
onTagsInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ formTags: e.detail.value ?? '' });
|
||||
},
|
||||
|
||||
onSelectStatus(e: WechatMiniprogram.TouchEvent) {
|
||||
const value = e.currentTarget.dataset.value as string;
|
||||
if (!value) return;
|
||||
this.setData({ formStatus: value });
|
||||
},
|
||||
|
||||
onSelectCropMode(e: WechatMiniprogram.TouchEvent) {
|
||||
const value = e.currentTarget.dataset.value as DebugCropMode;
|
||||
if (!value) return;
|
||||
this.setData({ formCropMode: value });
|
||||
},
|
||||
|
||||
onQualityChange(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({
|
||||
formQuality: clampPublishQuality(Number(e.detail.value)),
|
||||
});
|
||||
},
|
||||
|
||||
onWidthChange(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({
|
||||
formWidth: clampPublishWidth(Number(e.detail.value)),
|
||||
});
|
||||
},
|
||||
|
||||
/** 校验弹窗输入,并把结果回传给页面层。 */
|
||||
onConfirm() {
|
||||
const meta = (this.properties as any)
|
||||
.meta as DebugPublishMeta | null;
|
||||
if (!meta) {
|
||||
wx.showToast({
|
||||
title: '缺少发布元数据',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const title = this.data.formTitle.trim();
|
||||
const subtitle = this.data.formSubtitle.trim();
|
||||
if (!title || !subtitle) {
|
||||
wx.showToast({
|
||||
title: '标题和副标题不能为空',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const detail: DebugPublishConfirmDetail = {
|
||||
meta: {
|
||||
title,
|
||||
subtitle,
|
||||
tags: normalizeTagsInput(this.data.formTags),
|
||||
status: this.data.formStatus as DebugPublishMeta['status'],
|
||||
},
|
||||
settings: {
|
||||
cropMode: this.data.formCropMode as DebugCropMode,
|
||||
quality: clampPublishQuality(this.data.formQuality),
|
||||
width: clampPublishWidth(this.data.formWidth),
|
||||
maxSizeKB: this.data.maxSizeKB,
|
||||
},
|
||||
};
|
||||
|
||||
this.triggerEvent('confirm', detail);
|
||||
},
|
||||
|
||||
/** 初始化隐藏处理 canvas,后续所有裁剪压缩都在这里完成。 */
|
||||
initProcessorCanvas() {
|
||||
if ((this as any)._processorCanvasReadyPromise) return;
|
||||
|
||||
(this as any)._processorCanvasReadyPromise = new Promise<void>(
|
||||
(resolve, reject) => {
|
||||
this.createSelectorQuery()
|
||||
.select('#processorCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
if (!res[0] || !res[0].node) {
|
||||
reject(new Error('处理画布初始化失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
(this as any)._processorCanvas = res[0].node;
|
||||
(this as any)._processorCtx = (
|
||||
this as any
|
||||
)._processorCanvas.getContext('2d');
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
/** 确保隐藏 canvas 已经 ready。 */
|
||||
async ensureProcessorCanvasReady() {
|
||||
if (!(this as any)._processorCanvasReadyPromise) {
|
||||
this.initProcessorCanvas();
|
||||
}
|
||||
await (this as any)._processorCanvasReadyPromise;
|
||||
},
|
||||
|
||||
/** 用当前 canvas 上下文加载临时图片文件。 */
|
||||
loadImage(
|
||||
canvas: WechatMiniprogram.Canvas,
|
||||
src: string,
|
||||
): Promise<WechatMiniprogram.Image> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = canvas.createImage();
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = reject;
|
||||
image.src = src;
|
||||
});
|
||||
},
|
||||
|
||||
/** 将处理后的 canvas 导出成 jpg 临时文件。 */
|
||||
exportCanvasToTempFile(
|
||||
canvas: WechatMiniprogram.Canvas,
|
||||
width: number,
|
||||
height: number,
|
||||
quality: number,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.canvasToTempFilePath({
|
||||
canvas,
|
||||
fileType: 'jpg',
|
||||
quality,
|
||||
destWidth: width,
|
||||
destHeight: height,
|
||||
success: (res) => resolve(res.tempFilePath),
|
||||
fail: (err) => reject(err),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 读取导出文件大小,用于控制最大体积。
|
||||
* 开发者工具中 canvasToTempFilePath 可能返回 HTTP URL,
|
||||
* wx.getFileSystemManager().getFileInfo() 不支持 HTTP 路径,
|
||||
* 此时返回 -1 表示无法获取大小,调用方应跳过体积检测。
|
||||
*/
|
||||
getFileSize(filePath: string): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
wx.getFileSystemManager().getFileInfo({
|
||||
filePath,
|
||||
success: (res) => resolve(res.size),
|
||||
fail: () => resolve(-1),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 裁剪并压缩入口图。
|
||||
*
|
||||
* 流程:
|
||||
* 1. 加载 preview-card 导出的完整 A4 图片
|
||||
* 2. 按裁剪模式裁掉页眉 / 页脚
|
||||
* 3. 按目标宽度重绘到隐藏 canvas
|
||||
* 4. 若体积超出阈值,则逐步降低 jpg quality
|
||||
*/
|
||||
async processImage(
|
||||
params: DebugProcessImageParams,
|
||||
): Promise<DebugProcessImageResult> {
|
||||
await this.ensureProcessorCanvasReady();
|
||||
|
||||
const canvas = (this as any)
|
||||
._processorCanvas as WechatMiniprogram.Canvas;
|
||||
const ctx = (this as any)._processorCtx as RenderingContext;
|
||||
if (!canvas || !ctx) {
|
||||
throw new Error('处理画布不可用');
|
||||
}
|
||||
|
||||
const image = await this.loadImage(canvas, params.sourcePath);
|
||||
const { topRatio, bottomRatio } = getCropRatios(
|
||||
params.settings.cropMode,
|
||||
);
|
||||
const sourceWidth = image.width;
|
||||
const sourceHeight = image.height;
|
||||
const cropTop = Math.round(sourceHeight * topRatio);
|
||||
const cropBottom = Math.round(sourceHeight * bottomRatio);
|
||||
const extractHeight = sourceHeight - cropTop - cropBottom;
|
||||
|
||||
if (extractHeight <= 0) {
|
||||
throw new Error('裁剪区域无效');
|
||||
}
|
||||
|
||||
const targetWidth = clampPublishWidth(params.settings.width);
|
||||
const targetHeight = Math.round(
|
||||
(extractHeight * targetWidth) / sourceWidth,
|
||||
);
|
||||
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
ctx.clearRect(0, 0, targetWidth, targetHeight);
|
||||
ctx.drawImage(
|
||||
image,
|
||||
0,
|
||||
cropTop,
|
||||
sourceWidth,
|
||||
extractHeight,
|
||||
0,
|
||||
0,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
);
|
||||
|
||||
let currentQuality =
|
||||
clampPublishQuality(params.settings.quality) / 100;
|
||||
let outputPath = '';
|
||||
let outputSize = 0;
|
||||
|
||||
while (currentQuality >= 0.35) {
|
||||
outputPath = await this.exportCanvasToTempFile(
|
||||
canvas,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
currentQuality,
|
||||
);
|
||||
outputSize = await this.getFileSize(outputPath);
|
||||
|
||||
// -1 表示无法获取大小(开发者工具 HTTP 路径),直接跳过体积循环
|
||||
if (
|
||||
outputSize < 0 ||
|
||||
outputSize <= params.settings.maxSizeKB * 1024
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentQuality =
|
||||
Math.round((currentQuality - 0.05) * 100) / 100;
|
||||
}
|
||||
|
||||
return {
|
||||
tempFilePath: outputPath,
|
||||
size: outputSize,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
quality: Math.round(currentQuality * 100),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
<view class="debug-publish-tools">
|
||||
<view
|
||||
class="debug-publish-tools__fab"
|
||||
hover-class="debug-publish-tools__fab--hover"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
catchtap="onOpen">
|
||||
<text class="debug-publish-tools__fab-icon">📤</text>
|
||||
<text class="debug-publish-tools__fab-text">发布到云端</text>
|
||||
</view>
|
||||
|
||||
<van-popup
|
||||
show="{{visible}}"
|
||||
position="center"
|
||||
round
|
||||
closeable="{{!loading}}"
|
||||
close-on-click-overlay="{{!loading}}"
|
||||
bind:close="onClose"
|
||||
custom-class="debug-publish-tools__popup">
|
||||
<view class="debug-publish-tools__content">
|
||||
<text class="debug-publish-tools__title">Debug 发布</text>
|
||||
|
||||
<view wx:if="{{meta}}" class="debug-publish-tools__section">
|
||||
<text class="debug-publish-tools__section-title">基础信息</text>
|
||||
<view class="debug-publish-tools__meta-grid">
|
||||
<view class="debug-publish-tools__meta-item">
|
||||
<text class="debug-publish-tools__meta-label">ID</text>
|
||||
<text class="debug-publish-tools__meta-value">{{meta.id}}</text>
|
||||
</view>
|
||||
<view class="debug-publish-tools__meta-item">
|
||||
<text class="debug-publish-tools__meta-label">分类</text>
|
||||
<text class="debug-publish-tools__meta-value">{{meta.category}}</text>
|
||||
</view>
|
||||
<view class="debug-publish-tools__meta-item">
|
||||
<text class="debug-publish-tools__meta-label">年龄</text>
|
||||
<text class="debug-publish-tools__meta-value">
|
||||
{{meta.ageMin}}-{{meta.ageMax}} 岁
|
||||
</text>
|
||||
</view>
|
||||
<view class="debug-publish-tools__meta-item">
|
||||
<text class="debug-publish-tools__meta-label">难度</text>
|
||||
<text class="debug-publish-tools__meta-value">{{meta.difficulty}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="debug-publish-tools__path-box">
|
||||
<text class="debug-publish-tools__meta-label">路径</text>
|
||||
<text class="debug-publish-tools__path">{{meta.path}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="debug-publish-tools__section">
|
||||
<text class="debug-publish-tools__section-title">资料信息</text>
|
||||
<view class="debug-publish-tools__field">
|
||||
<text class="debug-publish-tools__field-label">标题</text>
|
||||
<input
|
||||
class="debug-publish-tools__input"
|
||||
value="{{formTitle}}"
|
||||
maxlength="20"
|
||||
bindinput="onTitleInput" />
|
||||
</view>
|
||||
<view class="debug-publish-tools__field">
|
||||
<text class="debug-publish-tools__field-label">副标题</text>
|
||||
<input
|
||||
class="debug-publish-tools__input"
|
||||
value="{{formSubtitle}}"
|
||||
maxlength="40"
|
||||
bindinput="onSubtitleInput" />
|
||||
</view>
|
||||
<view class="debug-publish-tools__field">
|
||||
<text class="debug-publish-tools__field-label">标签</text>
|
||||
<input
|
||||
class="debug-publish-tools__input"
|
||||
value="{{formTags}}"
|
||||
placeholder="多个标签用逗号分隔"
|
||||
maxlength="80"
|
||||
bindinput="onTagsInput" />
|
||||
</view>
|
||||
<view class="debug-publish-tools__field">
|
||||
<text class="debug-publish-tools__field-label">状态</text>
|
||||
<view class="debug-publish-tools__chip-row">
|
||||
<view
|
||||
class="debug-publish-tools__chip {{formStatus === 'draft' ? 'debug-publish-tools__chip--active' : ''}}"
|
||||
data-value="draft"
|
||||
bindtap="onSelectStatus">
|
||||
draft
|
||||
</view>
|
||||
<view
|
||||
class="debug-publish-tools__chip {{formStatus === 'active' ? 'debug-publish-tools__chip--active' : ''}}"
|
||||
data-value="active"
|
||||
bindtap="onSelectStatus">
|
||||
active
|
||||
</view>
|
||||
<view
|
||||
class="debug-publish-tools__chip {{formStatus === 'hidden' ? 'debug-publish-tools__chip--active' : ''}}"
|
||||
data-value="hidden"
|
||||
bindtap="onSelectStatus">
|
||||
hidden
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="debug-publish-tools__section">
|
||||
<text class="debug-publish-tools__section-title">图片处理</text>
|
||||
<view class="debug-publish-tools__field">
|
||||
<text class="debug-publish-tools__field-label">裁剪模式</text>
|
||||
<view class="debug-publish-tools__chip-row debug-publish-tools__chip-row--wrap">
|
||||
<view
|
||||
class="debug-publish-tools__chip {{formCropMode === 'header-footer' ? 'debug-publish-tools__chip--active' : ''}}"
|
||||
data-value="header-footer"
|
||||
bindtap="onSelectCropMode">
|
||||
裁剪头尾
|
||||
</view>
|
||||
<view
|
||||
class="debug-publish-tools__chip {{formCropMode === 'header-only' ? 'debug-publish-tools__chip--active' : ''}}"
|
||||
data-value="header-only"
|
||||
bindtap="onSelectCropMode">
|
||||
仅裁头部
|
||||
</view>
|
||||
<view
|
||||
class="debug-publish-tools__chip {{formCropMode === 'none' ? 'debug-publish-tools__chip--active' : ''}}"
|
||||
data-value="none"
|
||||
bindtap="onSelectCropMode">
|
||||
不裁剪
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="debug-publish-tools__field">
|
||||
<text class="debug-publish-tools__field-label">
|
||||
压缩质量 {{formQuality}}
|
||||
</text>
|
||||
<slider
|
||||
min="40"
|
||||
max="95"
|
||||
step="5"
|
||||
value="{{formQuality}}"
|
||||
activeColor="#f7ce00"
|
||||
backgroundColor="#ece4d2"
|
||||
bindchange="onQualityChange" />
|
||||
</view>
|
||||
<view class="debug-publish-tools__field">
|
||||
<text class="debug-publish-tools__field-label">
|
||||
输出宽度 {{formWidth}}px
|
||||
</text>
|
||||
<slider
|
||||
min="400"
|
||||
max="1000"
|
||||
step="20"
|
||||
value="{{formWidth}}"
|
||||
activeColor="#f7ce00"
|
||||
backgroundColor="#ece4d2"
|
||||
bindchange="onWidthChange" />
|
||||
</view>
|
||||
<text class="debug-publish-tools__hint">
|
||||
默认按文档规则裁掉页眉和页脚,并尽量压缩到 {{maxSizeKB}}KB 内。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="debug-publish-tools__actions">
|
||||
<button
|
||||
class="debug-publish-tools__btn debug-publish-tools__btn--ghost"
|
||||
disabled="{{loading}}"
|
||||
bindtap="onClose">
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="debug-publish-tools__btn debug-publish-tools__btn--primary"
|
||||
loading="{{loading}}"
|
||||
disabled="{{loading}}"
|
||||
bindtap="onConfirm">
|
||||
确认发布
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</van-popup>
|
||||
|
||||
<view class="debug-publish-tools__processor">
|
||||
<canvas
|
||||
id="processorCanvas"
|
||||
type="2d"
|
||||
class="debug-publish-tools__processor-canvas" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,8 +1,119 @@
|
||||
/**
|
||||
* 桥接文件 - 保持向后兼容
|
||||
* 实际数据已迁移到 core/data/worksheets.ts
|
||||
*/
|
||||
export {
|
||||
FOCUS_FUNCTION_TYPES,
|
||||
type FocusFunctionType,
|
||||
} from '../core/data/worksheets';
|
||||
export interface FocusFunctionType {
|
||||
id: string;
|
||||
page?: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
icon: string;
|
||||
mode?: string;
|
||||
img?: string;
|
||||
}
|
||||
|
||||
export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [
|
||||
{
|
||||
id: 'color-shape-match',
|
||||
page: 'focusDraw',
|
||||
title: '根据颜色画图形',
|
||||
desc: '根据颜色画出对应图形',
|
||||
icon: '🎯',
|
||||
img: '/assets/focusEntrance/color-shape-match.png',
|
||||
},
|
||||
{
|
||||
id: 'shape-symbol',
|
||||
page: 'focusDraw',
|
||||
title: '图形符号配对',
|
||||
desc: '根据图形画对应符号',
|
||||
icon: '🔗',
|
||||
img: '/assets/focusEntrance/shape-symbol.png',
|
||||
},
|
||||
{
|
||||
id: 'shape-recognition',
|
||||
page: 'focusDraw',
|
||||
title: '识别形状',
|
||||
desc: '识别形状,涂一涂',
|
||||
icon: '🔍',
|
||||
img: '/assets/focusEntrance/shape-recognition.png',
|
||||
},
|
||||
{
|
||||
id: 'position-coloring',
|
||||
page: 'focusDraw',
|
||||
title: '方位涂涂乐',
|
||||
desc: '观察位置,在方格中涂色',
|
||||
icon: '📍',
|
||||
img: '/assets/focusEntrance/position-coloring.png',
|
||||
},
|
||||
{
|
||||
id: 'color-pattern',
|
||||
page: 'focusDraw',
|
||||
title: '颜色找规律',
|
||||
desc: '观察颜色规律,在空白图形中涂色',
|
||||
icon: '🎨',
|
||||
img: '/assets/focusEntrance/color-pattern.png',
|
||||
},
|
||||
{
|
||||
id: 'match-connect',
|
||||
page: 'focusDraw',
|
||||
title: '连连看',
|
||||
desc: '根据物品连一连',
|
||||
icon: '🔗',
|
||||
img: '/assets/focusEntrance/match-connect.png',
|
||||
},
|
||||
{
|
||||
id: 'line-recognition',
|
||||
page: 'focusDraw',
|
||||
title: '线条识别',
|
||||
desc: '认识不同线条,画出颜色对应的线条',
|
||||
icon: '📏',
|
||||
img: '/assets/focusEntrance/line-recognition.png',
|
||||
},
|
||||
{
|
||||
id: 'grid-reasoning',
|
||||
page: 'focusDraw',
|
||||
title: '方格推理',
|
||||
desc: '推理出合并方格并连线',
|
||||
icon: '🧩',
|
||||
img: '/assets/focusEntrance/grid-reasoning.png',
|
||||
},
|
||||
{
|
||||
id: 'code-connect',
|
||||
page: 'focusDraw',
|
||||
title: '译码连线',
|
||||
desc: '按数字顺序将数字对应颜色连线',
|
||||
icon: '🔢',
|
||||
img: '/assets/focusEntrance/code-connect.png',
|
||||
},
|
||||
{
|
||||
id: 'dot-connect',
|
||||
page: 'focusDraw',
|
||||
title: '数字点连线',
|
||||
desc: '按数字顺序连点成图',
|
||||
icon: '🔗',
|
||||
img: '/assets/focusEntrance/dot-connect.png',
|
||||
},
|
||||
{
|
||||
id: 'grid-drawing-3x3',
|
||||
page: 'focusDraw',
|
||||
title: '格子仿画 3×3',
|
||||
desc: '简单有趣,培养专注力',
|
||||
icon: '🎨',
|
||||
mode: '3x3',
|
||||
img: '/assets/focusEntrance/grid-drawing-3x3.png',
|
||||
},
|
||||
{
|
||||
id: 'grid-drawing-5x5',
|
||||
page: 'focusDraw',
|
||||
title: '格子仿画 5×5',
|
||||
desc: '创意挑战,提升观察力',
|
||||
icon: '🎨',
|
||||
mode: '5x5',
|
||||
img: '/assets/focusEntrance/grid-drawing-5x5.png',
|
||||
},
|
||||
{
|
||||
id: 'grid-drawing-7x7',
|
||||
page: 'focusDraw',
|
||||
title: '格子仿画 7×7',
|
||||
desc: '大师挑战,锻炼耐心',
|
||||
icon: '🎨',
|
||||
mode: '7x7',
|
||||
img: '/assets/focusEntrance/grid-drawing-7x7.png',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,217 @@
|
||||
/**
|
||||
* 桥接文件 - 保持向后兼容
|
||||
* 实际数据已迁移到 core/data/worksheets.ts
|
||||
*/
|
||||
export { MATH_FUNCTION_TYPES, type MathFunctionType } from '../core/data/worksheets';
|
||||
export interface MathFunctionType {
|
||||
id: string;
|
||||
page?: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
icon?: string;
|
||||
img?: string;
|
||||
}
|
||||
|
||||
export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
|
||||
{
|
||||
id: 'number-find',
|
||||
page: 'mathDraw',
|
||||
title: '找数字,涂一涂',
|
||||
desc: '在数字方阵中找出目标数字并涂色',
|
||||
img: '/assets/mathEntrance/number-find.png',
|
||||
},
|
||||
{
|
||||
id: 'number-write',
|
||||
page: 'mathDraw',
|
||||
title: '看数字,写一写',
|
||||
desc: '按笔画顺序练习书写数字',
|
||||
img: '/assets/mathEntrance/number-write.png',
|
||||
},
|
||||
{
|
||||
id: 'number-coloring',
|
||||
page: 'mathDraw',
|
||||
title: '按数字,涂颜色',
|
||||
desc: '按指定数字给对应圆圈涂色',
|
||||
icon: '🎨',
|
||||
img: '/assets/mathEntrance/number-coloring.png',
|
||||
},
|
||||
{
|
||||
id: 'counting-matching',
|
||||
page: 'mathDraw',
|
||||
title: '数一数,连一连',
|
||||
desc: '连线配对数字和对应数量图形',
|
||||
icon: '🔗',
|
||||
img: '/assets/mathEntrance/count-match.png',
|
||||
},
|
||||
{
|
||||
id: 'number-object-match',
|
||||
page: 'mathDraw',
|
||||
title: '数物连线',
|
||||
desc: '连线相同数量的物品和数字',
|
||||
icon: '🔗',
|
||||
img: '/assets/mathEntrance/number-object-match.png',
|
||||
},
|
||||
{
|
||||
id: 'number-object-fill',
|
||||
page: 'mathDraw',
|
||||
title: '数物填写',
|
||||
desc: '数物品数量,填写对应数字',
|
||||
icon: '✏️',
|
||||
img: '/assets/mathEntrance/number-object-fill.png',
|
||||
},
|
||||
{
|
||||
id: 'counting-select',
|
||||
page: 'mathDraw',
|
||||
title: '数一数,选一选',
|
||||
desc: '数出物品数量,圈出正确答案',
|
||||
icon: '✓',
|
||||
img: '/assets/mathEntrance/counting-select.png',
|
||||
},
|
||||
{
|
||||
id: 'counting-fill',
|
||||
page: 'mathDraw',
|
||||
title: '数一数,填一填',
|
||||
desc: '数出物品数量,填写数字',
|
||||
icon: '✏️',
|
||||
img: '/assets/mathEntrance/counting-fill.png',
|
||||
},
|
||||
{
|
||||
id: 'compare',
|
||||
page: 'mathDraw',
|
||||
title: '数一数,比大小',
|
||||
desc: '比较数量,填入 ><=',
|
||||
icon: '⚖️',
|
||||
img: '/assets/mathEntrance/compare.png',
|
||||
},
|
||||
{
|
||||
id: 'number-sort',
|
||||
page: 'mathDraw',
|
||||
title: '数字排序',
|
||||
desc: '写出正确的数字顺序',
|
||||
icon: '🔢',
|
||||
img: '/assets/mathEntrance/number-sort.png',
|
||||
},
|
||||
{
|
||||
id: 'missing-number',
|
||||
page: 'mathDraw',
|
||||
title: '填上缺少的数字',
|
||||
desc: '在数列中找出并填写缺失数字',
|
||||
icon: '❓',
|
||||
img: '/assets/mathEntrance/missing-number.png',
|
||||
},
|
||||
{
|
||||
id: 'number-decompose',
|
||||
page: 'mathDraw',
|
||||
title: '10以内数的分与合',
|
||||
desc: '把数字分一分,合一合',
|
||||
icon: '🔢',
|
||||
img: '/assets/mathEntrance/number-decompose.png',
|
||||
},
|
||||
{
|
||||
id: 'number-decompose-20',
|
||||
page: 'mathDraw',
|
||||
title: '20以内数的分与合',
|
||||
desc: '把数字分一分,合一合',
|
||||
icon: '🔢',
|
||||
img: '/assets/mathEntrance/number-decompose-20.png',
|
||||
},
|
||||
{
|
||||
id: 'one-digit-addition',
|
||||
page: 'mathDraw',
|
||||
title: '一位数加法',
|
||||
desc: '通过圆点学习一位数加法运算',
|
||||
icon: '➕',
|
||||
img: '/assets/mathEntrance/one-digit-addition.png',
|
||||
},
|
||||
{
|
||||
id: 'addition-5',
|
||||
page: 'mathDraw',
|
||||
title: '5以内加法',
|
||||
desc: '图形化展示 5 以内加法',
|
||||
icon: '➕',
|
||||
img: '/assets/mathEntrance/addition-5.png',
|
||||
},
|
||||
{
|
||||
id: 'addition-10',
|
||||
page: 'mathDraw',
|
||||
title: '10以内加法',
|
||||
desc: '图形化展示 10 以内加法',
|
||||
icon: '➕',
|
||||
img: '/assets/mathEntrance/addition-10.png',
|
||||
},
|
||||
{
|
||||
id: 'subtraction-10',
|
||||
page: 'mathDraw',
|
||||
title: '10以内减法',
|
||||
desc: '图形化展示 10 以内减法',
|
||||
icon: '➖',
|
||||
img: '/assets/mathEntrance/subtraction-10.png',
|
||||
},
|
||||
{
|
||||
id: 'addition-subtraction-10',
|
||||
page: 'mathDraw',
|
||||
title: '10以内加减法',
|
||||
desc: '加减法混合运算',
|
||||
icon: '±',
|
||||
img: '/assets/mathEntrance/addition-subtraction-10.png',
|
||||
},
|
||||
{
|
||||
id: 'make-ten',
|
||||
page: 'mathDraw',
|
||||
title: '凑十法练习',
|
||||
desc: '20 以内进位加法',
|
||||
icon: '➕',
|
||||
img: '/assets/mathEntrance/make-ten.png',
|
||||
},
|
||||
{
|
||||
id: 'break-ten',
|
||||
page: 'mathDraw',
|
||||
title: '破十法练习',
|
||||
desc: '20 以内退位减法',
|
||||
icon: '➖',
|
||||
img: '/assets/mathEntrance/break-ten.png',
|
||||
},
|
||||
{
|
||||
id: 'flat-ten',
|
||||
page: 'mathDraw',
|
||||
title: '平十法练习',
|
||||
desc: '20 以内退位减法',
|
||||
icon: '➖',
|
||||
img: '/assets/mathEntrance/flat-ten.png',
|
||||
},
|
||||
{
|
||||
id: 'borrow-ten',
|
||||
page: 'mathDraw',
|
||||
title: '借十法练习',
|
||||
desc: '20 以上退位减法',
|
||||
icon: '➖',
|
||||
img: '/assets/mathEntrance/borrow-ten.png',
|
||||
},
|
||||
{
|
||||
id: 'practice-addition',
|
||||
page: 'mathDraw',
|
||||
title: '加法运算',
|
||||
desc: '10/20/50/100 以内加法',
|
||||
icon: '➕',
|
||||
img: '/assets/mathEntrance/practice-addition.png',
|
||||
},
|
||||
{
|
||||
id: 'practice-subtraction',
|
||||
page: 'mathDraw',
|
||||
title: '减法运算',
|
||||
desc: '10/20/50/100 以内减法',
|
||||
icon: '➖',
|
||||
img: '/assets/mathEntrance/practice-subtraction.png',
|
||||
},
|
||||
{
|
||||
id: 'practice-mixed',
|
||||
page: 'mathDraw',
|
||||
title: '混合运算',
|
||||
desc: '10/20/50/100 以内加减法混合',
|
||||
icon: '±',
|
||||
img: '/assets/mathEntrance/practice-mixed.png',
|
||||
},
|
||||
{
|
||||
id: 'multiplication-table',
|
||||
page: 'mathDraw',
|
||||
title: '九九乘法表',
|
||||
desc: '学习九九乘法口诀',
|
||||
icon: '✖️',
|
||||
img: '/assets/mathEntrance/multiplication-table.png',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,545 +0,0 @@
|
||||
import type {
|
||||
GeneratorType,
|
||||
LayoutConfig,
|
||||
TemplateType,
|
||||
WorksheetDefinition,
|
||||
WorksheetType,
|
||||
} from '../models/worksheet';
|
||||
import { difficultyToStars } from './difficulty';
|
||||
|
||||
/**
|
||||
* MathFunctionType - 保持向后兼容(mathPageMixin / mathIndex)
|
||||
*/
|
||||
export interface MathFunctionType {
|
||||
id: string;
|
||||
page?: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
icon?: string;
|
||||
img?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FocusFunctionType - 保持向后兼容(focusPageMixin / focusIndex)
|
||||
*/
|
||||
export interface FocusFunctionType {
|
||||
id: string;
|
||||
page?: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
icon: string;
|
||||
mode?: string;
|
||||
img?: string;
|
||||
}
|
||||
|
||||
interface SheetEngine {
|
||||
subcategory: string;
|
||||
template: TemplateType;
|
||||
generator: GeneratorType;
|
||||
generatorConfig?: Record<string, unknown>;
|
||||
layoutConfig?: LayoutConfig;
|
||||
tags?: string[];
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
function legacyPath(ui: WorksheetType): string | null {
|
||||
if (!ui.page) return null;
|
||||
if (ui.subpackage) {
|
||||
return `/${ui.subpackage}/${ui.page}/${ui.page}`;
|
||||
}
|
||||
return `/pages/${ui.page}/${ui.page}`;
|
||||
}
|
||||
|
||||
function sheet(ui: WorksheetType, engine: SheetEngine): WorksheetDefinition {
|
||||
const ageMin = ui.ageRange?.[0] ?? 3;
|
||||
const ageMax = ui.ageRange?.[1] ?? 8;
|
||||
return {
|
||||
...ui,
|
||||
subcategory: engine.subcategory,
|
||||
ageMin,
|
||||
ageMax,
|
||||
difficultyLevel: difficultyToStars(ui.difficulty),
|
||||
previewImage: ui.img ?? '',
|
||||
tags: engine.tags?.length ? engine.tags : [engine.subcategory],
|
||||
isNew: false,
|
||||
isHot: false,
|
||||
sortOrder: engine.sortOrder,
|
||||
downloadCount: 0,
|
||||
status: 'active',
|
||||
template: engine.template,
|
||||
generator: engine.generator,
|
||||
generatorConfig: engine.generatorConfig ?? {},
|
||||
layoutConfig: engine.layoutConfig ?? { showBorder: true },
|
||||
userConfigurable: null,
|
||||
legacyPage: legacyPath(ui),
|
||||
};
|
||||
}
|
||||
|
||||
const L = {
|
||||
gridEx: { showBorder: true, fontSize: 18 } satisfies LayoutConfig,
|
||||
card: { showBorder: true, columns: 2, fontSize: 16 } satisfies LayoutConfig,
|
||||
trace: {
|
||||
showBorder: true,
|
||||
showInstruction: true,
|
||||
fontSize: 20,
|
||||
} satisfies LayoutConfig,
|
||||
};
|
||||
|
||||
// 现有功能清单 §二:数感启蒙 26 种 + §三 13 种 + §四 §五 语文 2
|
||||
// prettier-ignore
|
||||
export const ALL_WORKSHEETS: WorksheetDefinition[] = [
|
||||
sheet(
|
||||
{ id: 'number-find', title: '找数字,涂一涂', desc: '在数字方阵中找出目标数字并涂色', category: 'math', page: 'mathDraw', subpackage: 'mathPages', img: '/assets/mathEntrance/number-find.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '🔍 1 2 3' },
|
||||
{ subcategory: 'number-sense', template: 'grid-coloring', generator: 'counting', sortOrder: 1, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'number-find' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-write', title: '看数字,写一写', desc: '按笔画顺序练习书写数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', img: '/assets/mathEntrance/number-write.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '1 2 3' },
|
||||
{ subcategory: 'number-sense', template: 'tracing-writing', generator: 'counting', sortOrder: 2, layoutConfig: { ...L.trace }, generatorConfig: { functionId: 'number-write' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-coloring', title: '按数字,涂颜色', desc: '按指定数字给对应圆圈涂色', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🎨', img: '/assets/mathEntrance/number-coloring.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '🎨' },
|
||||
{ subcategory: 'number-sense', template: 'grid-coloring', generator: 'counting', sortOrder: 3, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'number-coloring' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'counting-matching', title: '数一数,连一连', desc: '连线配对数字和对应数量图形', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/count-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '3↔🍎🍎🍎' },
|
||||
{ subcategory: 'counting', template: 'match-connect', generator: 'counting', sortOrder: 4, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'counting-matching' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-object-match', title: '数物连线', desc: '连线相同数量的物品和数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/number-object-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '🔗' },
|
||||
{ subcategory: 'counting', template: 'match-connect', generator: 'counting', sortOrder: 5, generatorConfig: { functionId: 'number-object-match' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-object-fill', title: '数物填写', desc: '数物品数量,填写对应数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/number-object-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 6, generatorConfig: { functionId: 'number-object-fill' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'counting-select', title: '数一数,选一选', desc: '数出物品数量,圈出正确答案', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✓', img: '/assets/mathEntrance/counting-select.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✓' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 7, generatorConfig: { functionId: 'counting-select' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'counting-fill', title: '数一数,填一填', desc: '数出物品数量,填写数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/counting-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 8, generatorConfig: { functionId: 'counting-fill' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'compare', title: '数一数,比大小', desc: '比较数量,填入 ><=', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '⚖️', img: '/assets/mathEntrance/compare.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3 ○ 5' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'comparison', sortOrder: 9, generatorConfig: { functionId: 'compare' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-sort', title: '数字排序', desc: '写出正确的数字顺序', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-sort.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1→2→3' },
|
||||
{ subcategory: 'sequence', template: 'sequence-pattern', generator: 'number-sequence', sortOrder: 10, generatorConfig: { functionId: 'number-sort' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'missing-number', title: '填上缺少的数字', desc: '在数列中找出并填写缺失数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '❓', img: '/assets/mathEntrance/missing-number.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1_3_5' },
|
||||
{ subcategory: 'sequence', template: 'sequence-pattern', generator: 'number-sequence', sortOrder: 11, generatorConfig: { functionId: 'missing-number' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-decompose', title: '10以内数的分与合', desc: '把数字分一分,合一合', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose.png', ageRange: [4, 6], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '8=?+?' },
|
||||
{ subcategory: 'decompose', template: 'grid-exercise', generator: 'number-decompose', sortOrder: 12, generatorConfig: { max: 10 } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-decompose-20', title: '20以内数的分与合', desc: '把数字分一分,合一合', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose-20.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#fff3e0', previewText: '15=?+?' },
|
||||
{ subcategory: 'decompose', template: 'grid-exercise', generator: 'number-decompose', sortOrder: 13, generatorConfig: { max: 20 } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'one-digit-addition', title: '一位数加法', desc: '通过圆点学习一位数加法运算', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/one-digit-addition.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '2+3=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 14, generatorConfig: { operators: ['+'], maxNumber: 9, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'addition-5', title: '5以内加法', desc: '图形化展示 5 以内加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-5.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '2+1=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 15, generatorConfig: { operators: ['+'], maxNumber: 5, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'addition-10', title: '10以内加法', desc: '图形化展示 10 以内加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3+5=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 16, generatorConfig: { operators: ['+'], maxNumber: 10, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'subtraction-10', title: '10以内减法', desc: '图形化展示 10 以内减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/subtraction-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '8-3=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 17, generatorConfig: { operators: ['-'], maxNumber: 10, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'addition-subtraction-10', title: '10以内加减法', desc: '加减法混合运算', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/addition-subtraction-10.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '±' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 18, generatorConfig: { operators: ['+', '-'], maxNumber: 10, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'make-ten', title: '凑十法练习', desc: '20 以内进位加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/make-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '7+?=10' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 19, generatorConfig: { method: 'make-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'break-ten', title: '破十法练习', desc: '20 以内退位减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/break-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '15-8=?' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 20, generatorConfig: { method: 'break-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'flat-ten', title: '平十法练习', desc: '20 以内退位减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/flat-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '14-6=?' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 21, generatorConfig: { method: 'flat-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'borrow-ten', title: '借十法练习', desc: '20 以上退位减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/borrow-ten.png', ageRange: [6, 8], difficulty: 'advanced', previewBg: '#e3f2fd', previewText: '32-8=?' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 22, generatorConfig: { method: 'borrow-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'practice-addition', title: '加法运算', desc: '10/20/50/100 以内加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/practice-addition.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '25+18=?' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 23, generatorConfig: { operators: ['+'], preset: 'vertical' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'practice-subtraction', title: '减法运算', desc: '10/20/50/100 以内减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/practice-subtraction.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '43-17=?' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 24, generatorConfig: { operators: ['-'], preset: 'vertical' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'practice-mixed', title: '混合运算', desc: '10/20/50/100 以内加减法混合', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/practice-mixed.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '±' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 25, generatorConfig: { operators: ['+', '-'], preset: 'vertical' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'multiplication-table', title: '九九乘法表', desc: '学习九九乘法口诀', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✖️', img: '/assets/mathEntrance/multiplication-table.png', ageRange: [6, 8], difficulty: 'intermediate', previewBg: '#f3e5f5', previewText: '3×4=12' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 26, generatorConfig: { operators: ['×'], mode: 'multiplication-table' } },
|
||||
),
|
||||
|
||||
sheet(
|
||||
{ id: 'color-shape-match', title: '根据颜色画图形', desc: '根据颜色画出对应图形', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎯', img: '/assets/focusEntrance/color-shape-match.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#f3e5f5', previewText: '🎯' },
|
||||
{ subcategory: 'focus-visual', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 27, generatorConfig: { functionId: 'color-shape-match' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'shape-symbol', title: '图形符号配对', desc: '根据图形画对应符号', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/shape-symbol.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8eaf6', previewText: '△→♠' },
|
||||
{ subcategory: 'focus-visual', template: 'match-connect', generator: 'shape-grid', sortOrder: 28, generatorConfig: { functionId: 'shape-symbol' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'shape-recognition', title: '识别形状', desc: '识别形状,涂一涂', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔍', img: '/assets/focusEntrance/shape-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '△ ○ □' },
|
||||
{ subcategory: 'focus-visual', template: 'grid-coloring', generator: 'shape-grid', sortOrder: 29, generatorConfig: { functionId: 'shape-recognition' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'position-coloring', title: '方位涂涂乐', desc: '观察位置,在方格中涂色', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '📍', img: '/assets/focusEntrance/position-coloring.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8f5e9', previewText: '📍' },
|
||||
{ subcategory: 'focus-grid', template: 'grid-coloring', generator: 'shape-grid', sortOrder: 30, generatorConfig: { functionId: 'position-coloring' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'color-pattern', title: '颜色找规律', desc: '观察颜色规律,在空白图形中涂色', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', img: '/assets/focusEntrance/color-pattern.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff8e1', previewText: '🎨' },
|
||||
{ subcategory: 'focus-grid', template: 'grid-coloring', generator: 'color-pattern', sortOrder: 31, generatorConfig: { functionId: 'color-pattern' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'match-connect', title: '连连看', desc: '根据物品连一连', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/match-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e0f7fa', previewText: '🔗' },
|
||||
{ subcategory: 'focus-connect', template: 'match-connect', generator: 'shape-grid', sortOrder: 32, generatorConfig: { functionId: 'match-connect' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'line-recognition', title: '线条识别', desc: '认识不同线条,画出颜色对应的线条', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '📏', img: '/assets/focusEntrance/line-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#f1f8e9', previewText: '〰️' },
|
||||
{ subcategory: 'focus-visual', template: 'tracing-writing', generator: 'shape-grid', sortOrder: 33, generatorConfig: { functionId: 'line-recognition' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-reasoning', title: '方格推理', desc: '推理出合并方格并连线', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🧩', img: '/assets/focusEntrance/grid-reasoning.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e8eaf6', previewText: '🧩' },
|
||||
{ subcategory: 'focus-logic', template: 'special-graphic', generator: 'shape-grid', sortOrder: 34, generatorConfig: { functionId: 'grid-reasoning' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'code-connect', title: '译码连线', desc: '按数字顺序将数字对应颜色连线', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔢', img: '/assets/focusEntrance/code-connect.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#fff3e0', previewText: '🔢' },
|
||||
{ subcategory: 'focus-connect', template: 'match-connect', generator: 'dot-connect', sortOrder: 35, generatorConfig: { functionId: 'code-connect' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'dot-connect', title: '数字点连线', desc: '按数字顺序连点成图', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/dot-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '1→2→3' },
|
||||
{ subcategory: 'focus-connect', template: 'match-connect', generator: 'dot-connect', sortOrder: 36, generatorConfig: { functionId: 'dot-connect' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-drawing-3x3', title: '格子仿画 3×3', desc: '简单有趣,培养专注力', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', mode: '3x3', img: '/assets/focusEntrance/grid-drawing-3x3.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 37, generatorConfig: { grid: '3x3' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-drawing-5x5', title: '格子仿画 5×5', desc: '创意挑战,提升观察力', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', mode: '5x5', img: '/assets/focusEntrance/grid-drawing-5x5.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 38, generatorConfig: { grid: '5x5' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-drawing-7x7', title: '格子仿画 7×7', desc: '大师挑战,锻炼耐心', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', mode: '7x7', img: '/assets/focusEntrance/grid-drawing-7x7.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 39, generatorConfig: { grid: '7x7' } },
|
||||
),
|
||||
|
||||
sheet(
|
||||
{ id: 'word-recognition', title: '识字卡', desc: '输入或选字生成涂色识字卡(grid/find 模板)', category: 'chinese', page: 'index', subpackage: '', icon: '📖', img: '', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '大 小' },
|
||||
{ subcategory: 'literacy', template: 'card-layout', generator: 'custom', sortOrder: 40, generatorConfig: { functionId: 'word-recognition' }, layoutConfig: { ...L.card } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'copybook', title: '练字帖', desc: '选字生成田字格笔顺练字帖', category: 'chinese', page: 'copyBook', subpackage: '', icon: '✏️', img: '', ageRange: [4, 7], difficulty: 'basic', previewBg: '#fff8e1', previewText: '横竖撇' },
|
||||
{ subcategory: 'copybook', template: 'tracing-writing', generator: 'character-tracing', sortOrder: 41, generatorConfig: { functionId: 'copybook' }, layoutConfig: { ...L.trace } },
|
||||
),
|
||||
|
||||
sheet(
|
||||
{
|
||||
id: 'letter-tracing-single',
|
||||
title: '看图描红',
|
||||
desc: '单字母配图、例句与六行四线三格描红',
|
||||
category: 'english',
|
||||
page: 'letterTracing',
|
||||
subpackage: 'englishPages',
|
||||
icon: '🔠',
|
||||
img: '',
|
||||
ageRange: [4, 7],
|
||||
difficulty: 'beginner',
|
||||
previewBg: '#e8f4ff',
|
||||
previewText: 'Aa',
|
||||
},
|
||||
{
|
||||
subcategory: 'letter-tracing',
|
||||
template: 'tracing-writing',
|
||||
generator: 'letter-tracing',
|
||||
sortOrder: 42,
|
||||
layoutConfig: {
|
||||
...L.trace,
|
||||
instructionText: '看一看,读一读,再描一描',
|
||||
},
|
||||
generatorConfig: {
|
||||
mode: 'letter-tracing-single',
|
||||
letterCase: 'upper',
|
||||
selectedLetter: 'A',
|
||||
repetitions: 5,
|
||||
fadePattern: 'gradient',
|
||||
},
|
||||
},
|
||||
),
|
||||
sheet(
|
||||
{
|
||||
id: 'letter-tracing-two-column',
|
||||
title: '两列练习',
|
||||
desc: '左 A–M、右 N–Z,每组包含大小写',
|
||||
category: 'english',
|
||||
page: 'letterTracing',
|
||||
subpackage: 'englishPages',
|
||||
icon: '🔤',
|
||||
img: '',
|
||||
ageRange: [4, 7],
|
||||
difficulty: 'beginner',
|
||||
previewBg: '#e8f4ff',
|
||||
previewText: 'A|N',
|
||||
},
|
||||
{
|
||||
subcategory: 'letter-tracing',
|
||||
template: 'tracing-writing',
|
||||
generator: 'letter-tracing',
|
||||
sortOrder: 43,
|
||||
layoutConfig: {
|
||||
...L.trace,
|
||||
columns: 2,
|
||||
instructionText: '左 A–M、右 N–Z,大小写配对描红',
|
||||
},
|
||||
generatorConfig: {
|
||||
mode: 'letter-tracing-two-column',
|
||||
letterCase: 'both',
|
||||
repetitions: 4,
|
||||
fadePattern: 'gradient',
|
||||
},
|
||||
},
|
||||
),
|
||||
sheet(
|
||||
{
|
||||
id: 'letter-tracing-upper-lower',
|
||||
title: '字母总览',
|
||||
desc: 'Uppercase / Lowercase 分区,每区四行(7+7+6+6)',
|
||||
category: 'english',
|
||||
page: 'letterTracing',
|
||||
subpackage: 'englishPages',
|
||||
icon: '🔡',
|
||||
img: '',
|
||||
ageRange: [4, 7],
|
||||
difficulty: 'beginner',
|
||||
previewBg: '#e8f4ff',
|
||||
previewText: 'Aa Bb',
|
||||
},
|
||||
{
|
||||
subcategory: 'letter-tracing',
|
||||
template: 'tracing-writing',
|
||||
generator: 'letter-tracing',
|
||||
sortOrder: 44,
|
||||
layoutConfig: {
|
||||
...L.trace,
|
||||
columns: 13,
|
||||
instructionText: '认读全部大写与小写字母',
|
||||
},
|
||||
generatorConfig: {
|
||||
mode: 'letter-tracing-upper-lower',
|
||||
letterCase: 'both',
|
||||
repetitions: 1,
|
||||
fadePattern: 'first-only',
|
||||
},
|
||||
},
|
||||
),
|
||||
sheet(
|
||||
{
|
||||
id: 'letter-tracing-case-pairing',
|
||||
title: '大小写练习',
|
||||
desc: '半组字母左大写右小写,每行四个',
|
||||
category: 'english',
|
||||
page: 'letterTracing',
|
||||
subpackage: 'englishPages',
|
||||
icon: '✏️',
|
||||
img: '',
|
||||
ageRange: [4, 7],
|
||||
difficulty: 'beginner',
|
||||
previewBg: '#e8f4ff',
|
||||
previewText: 'A→Z',
|
||||
},
|
||||
{
|
||||
subcategory: 'letter-tracing',
|
||||
template: 'tracing-writing',
|
||||
generator: 'letter-tracing',
|
||||
sortOrder: 45,
|
||||
layoutConfig: {
|
||||
...L.trace,
|
||||
instructionText: '左栏大写、右栏小写,逐行对应描红',
|
||||
},
|
||||
generatorConfig: {
|
||||
mode: 'letter-tracing-case-pairing',
|
||||
letterCase: 'both',
|
||||
alphabetHalf: 'A-M',
|
||||
repetitions: 4,
|
||||
fadePattern: 'gradient',
|
||||
},
|
||||
},
|
||||
),
|
||||
sheet(
|
||||
{
|
||||
id: 'letter-tracing-three',
|
||||
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: 'letter-tracing-three',
|
||||
letterCase: 'both',
|
||||
repetitions: 5,
|
||||
fadePattern: 'gradient',
|
||||
tripleLetters: ['A', 'B', 'C'],
|
||||
},
|
||||
},
|
||||
),
|
||||
sheet(
|
||||
{
|
||||
id: 'letter-tracing-half',
|
||||
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: 'letter-tracing-half',
|
||||
letterCase: 'both',
|
||||
alphabetHalf: 'A-M',
|
||||
repetitions: 5,
|
||||
fadePattern: 'first-only',
|
||||
},
|
||||
},
|
||||
),
|
||||
sheet(
|
||||
{
|
||||
id: 'letter-tracing-daily-checkin',
|
||||
title: '每日打卡',
|
||||
desc: '四宫格每日字母打卡,闭合四线三格练习',
|
||||
category: 'english',
|
||||
page: 'letterTracing',
|
||||
subpackage: 'englishPages',
|
||||
icon: '🗓️',
|
||||
img: '',
|
||||
ageRange: [4, 7],
|
||||
difficulty: 'basic',
|
||||
previewBg: '#e8f4ff',
|
||||
previewText: 'ABCD',
|
||||
},
|
||||
{
|
||||
subcategory: 'letter-tracing',
|
||||
template: 'tracing-writing',
|
||||
generator: 'letter-tracing',
|
||||
sortOrder: 48,
|
||||
layoutConfig: {
|
||||
...L.trace,
|
||||
instructionText: '四宫格每日打卡字母描红',
|
||||
},
|
||||
generatorConfig: {
|
||||
mode: 'letter-tracing-daily-checkin',
|
||||
letterCase: 'both',
|
||||
repetitions: 5,
|
||||
fadePattern: 'first-only',
|
||||
dailyLetters: ['A', 'B', 'C', 'D'],
|
||||
},
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
export const MATH_FUNCTION_TYPES: MathFunctionType[] = ALL_WORKSHEETS.filter(
|
||||
(w) => w.category === 'math',
|
||||
)
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((w) => ({
|
||||
id: w.id,
|
||||
page: w.page,
|
||||
title: w.title,
|
||||
desc: w.desc,
|
||||
icon: w.icon,
|
||||
img: w.img,
|
||||
}));
|
||||
|
||||
export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = ALL_WORKSHEETS.filter(
|
||||
(w) => w.category === 'focus',
|
||||
)
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((w) => ({
|
||||
id: w.id,
|
||||
page: w.page,
|
||||
title: w.title,
|
||||
desc: w.desc,
|
||||
icon: w.icon ?? '・',
|
||||
mode: w.mode,
|
||||
img: w.img,
|
||||
}));
|
||||
|
||||
export function getWorksheetsByCategory(
|
||||
category: string,
|
||||
): WorksheetDefinition[] {
|
||||
if (category === 'all') return ALL_WORKSHEETS;
|
||||
return ALL_WORKSHEETS.filter((w) => w.category === category);
|
||||
}
|
||||
|
||||
export function getWorksheetsByAge<T extends WorksheetType>(
|
||||
worksheets: readonly T[],
|
||||
age: number,
|
||||
): T[] {
|
||||
return worksheets.filter((w) => {
|
||||
if (!w.ageRange) return true;
|
||||
return age >= w.ageRange[0] && age <= w.ageRange[1];
|
||||
});
|
||||
}
|
||||
|
||||
export function getWorksheetPath(worksheet: WorksheetType): string {
|
||||
if (!worksheet.page) return '';
|
||||
if (worksheet.subpackage) {
|
||||
return `/${worksheet.subpackage}/${worksheet.page}/${worksheet.page}?id=${worksheet.id}`;
|
||||
}
|
||||
return `/pages/${worksheet.page}/${worksheet.page}`;
|
||||
}
|
||||
|
||||
export function getWorksheetById(id: string): WorksheetDefinition | undefined {
|
||||
return ALL_WORKSHEETS.find((w) => w.id === id);
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
export type CategoryId =
|
||||
| 'math'
|
||||
| 'chinese'
|
||||
| 'english'
|
||||
| 'puzzle'
|
||||
| 'craft'
|
||||
| 'pinyin';
|
||||
|
||||
/** Tab / 首页用的分类展示模型 */
|
||||
export interface CategoryType {
|
||||
id: string;
|
||||
|
||||
@@ -1,106 +1,26 @@
|
||||
import { CategoryId } from './category';
|
||||
/**
|
||||
* 题型与模板引擎相关模型:对齐技术架构 §5.4 与云开发 worksheets 集合 §3.3
|
||||
*/
|
||||
|
||||
/** 小程序现状:专注力单独成类;云端 category 枚举无 focus 时可映射为 puzzle + subcategory */
|
||||
export type WorksheetCategory =
|
||||
| 'math'
|
||||
| 'focus'
|
||||
| 'chinese'
|
||||
| 'english'
|
||||
| 'puzzle'
|
||||
| 'craft';
|
||||
|
||||
/** 云 worksheets.category 严格枚举(§3.3) */
|
||||
export type WorksheetCloudCategory =
|
||||
| 'math'
|
||||
| 'chinese'
|
||||
| 'english'
|
||||
| 'puzzle'
|
||||
| 'craft';
|
||||
|
||||
export type DifficultyLevel =
|
||||
| 'beginner'
|
||||
| 'basic'
|
||||
| 'intermediate'
|
||||
| 'advanced';
|
||||
|
||||
export type WorksheetStatus = 'active' | 'draft' | 'hidden';
|
||||
|
||||
/** 排版模板(技术架构 §5.4) */
|
||||
export type TemplateType =
|
||||
| 'grid-exercise'
|
||||
| 'match-connect'
|
||||
| 'grid-coloring'
|
||||
| 'card-layout'
|
||||
| 'tracing-writing'
|
||||
| 'full-page-asset'
|
||||
| 'sequence-pattern'
|
||||
| 'special-graphic';
|
||||
|
||||
/** 数据生成器(技术架构 §5.4) */
|
||||
export type GeneratorType =
|
||||
| 'arithmetic'
|
||||
| 'number-sequence'
|
||||
| 'number-decompose'
|
||||
| 'counting'
|
||||
| 'comparison'
|
||||
| 'shape-grid'
|
||||
| 'color-pattern'
|
||||
| 'character-tracing'
|
||||
| 'letter-tracing'
|
||||
| 'pinyin-tracing'
|
||||
| 'static-asset'
|
||||
| 'maze'
|
||||
| 'dot-connect'
|
||||
| 'clock'
|
||||
| 'custom';
|
||||
|
||||
export interface LayoutConfig {
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
fontSize?: number;
|
||||
showBorder?: boolean;
|
||||
showTitle?: boolean;
|
||||
padding?: number;
|
||||
itemSpacing?: number;
|
||||
showInstruction?: boolean;
|
||||
instructionText?: string;
|
||||
}
|
||||
|
||||
export interface UserConfigField {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'select' | 'slider' | 'switch';
|
||||
options?: { label: string; value: unknown }[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
defaultValue: unknown;
|
||||
}
|
||||
|
||||
/** 与架构文档 WorksheetConfig 一致,供云端下发 / 本地内置 */
|
||||
export interface WorksheetConfig {
|
||||
id: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
category: WorksheetCloudCategory;
|
||||
subcategory: string;
|
||||
subtitle: string;
|
||||
category: CategoryId;
|
||||
subcategory?: string;
|
||||
ageMin: number;
|
||||
ageMax: number;
|
||||
difficulty: 1 | 2 | 3 | 4;
|
||||
previewImage: string;
|
||||
previewImg: string;
|
||||
tags: string[];
|
||||
isNew: boolean;
|
||||
isHot: boolean;
|
||||
sortOrder: number;
|
||||
status: WorksheetStatus;
|
||||
template: TemplateType;
|
||||
generator: GeneratorType;
|
||||
generatorConfig: Record<string, unknown>;
|
||||
layoutConfig: LayoutConfig;
|
||||
userConfigurable?: UserConfigField[] | null;
|
||||
legacyPage?: string | null;
|
||||
downloadCount: number;
|
||||
sortOrder?: number;
|
||||
status: 'active' | 'draft' | 'hidden';
|
||||
downloads: number;
|
||||
likes: number;
|
||||
}
|
||||
|
||||
/** 云集合 `worksheets` 文档(§3.3,含数据库字段) */
|
||||
@@ -109,79 +29,3 @@ export interface WorksheetRecord extends WorksheetConfig {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/** 列表卡片 / 导航用(兼容存量页面) */
|
||||
export interface WorksheetType {
|
||||
id: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
category: WorksheetCategory;
|
||||
page?: string;
|
||||
subpackage?: string;
|
||||
icon?: string;
|
||||
img?: string;
|
||||
ageRange?: [number, number];
|
||||
difficulty?: DifficultyLevel;
|
||||
mode?: string;
|
||||
previewBg?: string;
|
||||
previewText?: string;
|
||||
}
|
||||
|
||||
/** 内置兜底 + 模板引擎字段(本地 core/data/worksheets) */
|
||||
export interface WorksheetDefinition extends WorksheetType {
|
||||
subcategory: string;
|
||||
ageMin: number;
|
||||
ageMax: number;
|
||||
/** 1–4,对应云 worksheets.difficulty */
|
||||
difficultyLevel: 1 | 2 | 3 | 4;
|
||||
previewImage: string;
|
||||
tags: string[];
|
||||
isNew: boolean;
|
||||
isHot: boolean;
|
||||
sortOrder: number;
|
||||
downloadCount: number;
|
||||
status: WorksheetStatus;
|
||||
template: TemplateType;
|
||||
generator: GeneratorType;
|
||||
generatorConfig: Record<string, unknown>;
|
||||
layoutConfig: LayoutConfig;
|
||||
userConfigurable?: UserConfigField[] | null;
|
||||
legacyPage?: string | null;
|
||||
}
|
||||
|
||||
/** 写入云库时的 category:focus 归为 puzzle,子类区分 */
|
||||
export function toCloudCategory(
|
||||
category: WorksheetCategory,
|
||||
): WorksheetCloudCategory {
|
||||
if (category === 'focus') return 'puzzle';
|
||||
return category;
|
||||
}
|
||||
|
||||
/** 由内置定义生成云侧 worksheets 文档形状(不含 _id/时间,供同步层补全) */
|
||||
export function worksheetDefinitionToConfig(
|
||||
def: WorksheetDefinition,
|
||||
): Omit<WorksheetRecord, '_id' | 'createdAt' | 'updatedAt'> {
|
||||
return {
|
||||
id: def.id,
|
||||
title: def.title,
|
||||
desc: def.desc,
|
||||
category: toCloudCategory(def.category),
|
||||
subcategory: def.subcategory,
|
||||
ageMin: def.ageMin,
|
||||
ageMax: def.ageMax,
|
||||
difficulty: def.difficultyLevel,
|
||||
previewImage: def.previewImage,
|
||||
tags: def.tags,
|
||||
isNew: def.isNew,
|
||||
isHot: def.isHot,
|
||||
sortOrder: def.sortOrder,
|
||||
downloadCount: def.downloadCount,
|
||||
status: def.status,
|
||||
template: def.template,
|
||||
generator: def.generator,
|
||||
generatorConfig: def.generatorConfig,
|
||||
layoutConfig: def.layoutConfig,
|
||||
userConfigurable: def.userConfigurable ?? null,
|
||||
legacyPage: def.legacyPage ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ export function generateLetterTracing(
|
||||
};
|
||||
}
|
||||
|
||||
/** 与 WorksheetDefinition.generatorConfig 合并后的运行时配置 */
|
||||
/** 与页面内模式配置合并后的运行时配置 */
|
||||
export function mergeLetterTracingConfig(
|
||||
base: Partial<LetterTracingGeneratorConfig>,
|
||||
overrides: Partial<LetterTracingGeneratorConfig>,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { LetterTracingMode } from './generators/letter-tracing-generator';
|
||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||
|
||||
interface ModeDefinition {
|
||||
id: LetterTracingMode;
|
||||
icon: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
difficulty: 1 | 2 | 3 | 4;
|
||||
tags: string[];
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
const MODES = [
|
||||
{
|
||||
id: 'letter-tracing-single',
|
||||
icon: 'start-a',
|
||||
title: '默认字帖',
|
||||
subtitle: '配图、例句与描红',
|
||||
difficulty: 1,
|
||||
tags: ['字母描红', '英语启蒙', '看图描红'],
|
||||
sortOrder: 42,
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-upper-lower',
|
||||
icon: 'draw-o',
|
||||
title: '基础描红',
|
||||
subtitle: 'Uppercase / Lowercase 总览',
|
||||
difficulty: 1,
|
||||
tags: ['字母描红', '英语启蒙', '字母总览'],
|
||||
sortOrder: 44,
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-case-pairing',
|
||||
icon: 'font-size',
|
||||
title: '大小写对照',
|
||||
subtitle: '半组字母左大写右小写',
|
||||
difficulty: 1,
|
||||
tags: ['字母描红', '英语启蒙', '大小写练习'],
|
||||
sortOrder: 45,
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-two-column',
|
||||
icon: 'two-columns',
|
||||
title: '两列描红',
|
||||
subtitle: '左 A–M、右 N–Z 配对描红',
|
||||
difficulty: 1,
|
||||
tags: ['字母描红', '英语启蒙', '两列练习'],
|
||||
sortOrder: 43,
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-half',
|
||||
icon: 'square-half',
|
||||
title: '13字母半表',
|
||||
subtitle: '每行一个字母,13 字母半表',
|
||||
difficulty: 2,
|
||||
tags: ['字母描红', '英语启蒙', '单字母逐行'],
|
||||
sortOrder: 47,
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-three',
|
||||
icon: 'ABC-list',
|
||||
title: '三字母精练',
|
||||
subtitle: '每页聚焦 3 个字母深度书写',
|
||||
difficulty: 2,
|
||||
tags: ['字母描红', '英语启蒙', '三字母精练'],
|
||||
sortOrder: 46,
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-daily-checkin',
|
||||
icon: 'draw-o',
|
||||
title: '每日打卡',
|
||||
subtitle: '四宫格每日字母打卡练习',
|
||||
difficulty: 2,
|
||||
tags: ['字母描红', '英语启蒙', '每日打卡'],
|
||||
sortOrder: 48,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<ModeDefinition>;
|
||||
|
||||
type Mode = (typeof MODES)[number];
|
||||
|
||||
const MODE_BY_ID = Object.fromEntries(
|
||||
MODES.map((m) => [m.id, m]),
|
||||
) as Record<string, Mode>;
|
||||
|
||||
/** 页面渲染用:模式选择器列表 */
|
||||
export const LETTER_TRACING_MODE_OPTIONS = MODES;
|
||||
|
||||
/** 页面 pageInfoLookup 用 */
|
||||
export function getModeInfo(id: string) {
|
||||
const m = MODE_BY_ID[id];
|
||||
return m ? { title: m.title, desc: m.subtitle } : undefined;
|
||||
}
|
||||
|
||||
/** 判断 id 是否有效 */
|
||||
export function isValidMode(id: string): boolean {
|
||||
return id in MODE_BY_ID;
|
||||
}
|
||||
|
||||
/** 发布用:从当前 mode 生成 DebugPublishMeta */
|
||||
export function getPublishMetaByMode(id: string): DebugPublishMeta | null {
|
||||
const m = MODE_BY_ID[id];
|
||||
if (!m) return null;
|
||||
return {
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
subtitle: m.subtitle,
|
||||
category: 'english',
|
||||
subcategory: 'letter-tracing',
|
||||
path: `/englishPages/letterTracing/letterTracing?id=${m.id}`,
|
||||
ageMin: 4,
|
||||
ageMax: 7,
|
||||
grade: inferGradeFromAge(4, 7),
|
||||
difficulty: m.difficulty,
|
||||
previewImg: '',
|
||||
tags: [...m.tags],
|
||||
isNew: false,
|
||||
isHot: false,
|
||||
sortOrder: m.sortOrder,
|
||||
status: 'draft',
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import LetterTracingDraw from './draw/letterTracingDraw';
|
||||
import { createPage, type CanvasDataState } from '../../base/pageMixin';
|
||||
import { defaultShareConfig } from '../../config/config';
|
||||
import { getWorksheetById } from '../../core/data/worksheets';
|
||||
import {
|
||||
generateLetterTracing,
|
||||
mergeLetterTracingConfig,
|
||||
@@ -9,6 +8,12 @@ import {
|
||||
type LetterTracingGeneratorConfig,
|
||||
type LetterTracingMode,
|
||||
} from './generators/letter-tracing-generator';
|
||||
import {
|
||||
getModeInfo,
|
||||
getPublishMetaByMode,
|
||||
isValidMode,
|
||||
LETTER_TRACING_MODE_OPTIONS,
|
||||
} from './letterTracing.config';
|
||||
import { LETTERS_UPPER, LETTERS_PAIRS } from '../shared/data/alphabet';
|
||||
import {
|
||||
DEFAULT_LETTER_PROFILE,
|
||||
@@ -16,75 +21,24 @@ import {
|
||||
type FontProfile,
|
||||
} from '../shared/data/fontProfiles';
|
||||
import { loadLetterFont } from '../shared/draw/drawTools';
|
||||
|
||||
/** 练习模式定义(顺序与产品文档一致),同时用于模式选择器和页面元信息 */
|
||||
const MODES = [
|
||||
{
|
||||
id: 'letter-tracing-single',
|
||||
icon: 'start-a',
|
||||
label: '默认字帖',
|
||||
desc: '配图、例句与描红',
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-upper-lower',
|
||||
icon: 'draw-o',
|
||||
// label: '字母总览',
|
||||
label: '基础描红',
|
||||
desc: 'Uppercase / Lowercase 总览',
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-case-pairing',
|
||||
icon: 'font-size',
|
||||
label: '大小写对照',
|
||||
desc: '半组字母左大写右小写',
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-two-column',
|
||||
icon: 'two-columns',
|
||||
label: '两列描红',
|
||||
desc: '左 A–M、右 N–Z 配对描红',
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-half',
|
||||
icon: 'square-half',
|
||||
label: '13字母半表',
|
||||
desc: '每行一个字母,13 字母半表',
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-three',
|
||||
icon: 'ABC-list',
|
||||
label: '三字母精练',
|
||||
desc: '每页聚焦 3 个字母深度书写',
|
||||
},
|
||||
{
|
||||
id: 'letter-tracing-daily-checkin',
|
||||
icon: 'draw-o',
|
||||
label: '每日打卡',
|
||||
desc: '四宫格每日字母打卡练习',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
|
||||
string,
|
||||
(typeof MODES)[number]
|
||||
>;
|
||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||
|
||||
/** 三字母精练分组:26 字母每 3 个一组 */
|
||||
const TRIPLE_GROUPS: { label: string; letters: string[] }[] = [];
|
||||
const TRIPLE_GROUPS: { title: string; letters: string[] }[] = [];
|
||||
for (let i = 0; i < LETTERS_UPPER.length; i += 3) {
|
||||
const group = LETTERS_UPPER.slice(i, i + 3);
|
||||
const label =
|
||||
const title =
|
||||
group.length > 1 ? `${group[0]}–${group[group.length - 1]}` : group[0];
|
||||
TRIPLE_GROUPS.push({ label, letters: [...group] });
|
||||
TRIPLE_GROUPS.push({ title, letters: [...group] });
|
||||
}
|
||||
|
||||
/** 每日打卡分组:4 个字母一组 */
|
||||
const DAILY_GROUPS: { label: string; letters: string[] }[] = [];
|
||||
const DAILY_GROUPS: { title: string; letters: string[] }[] = [];
|
||||
for (let i = 0; i < LETTERS_UPPER.length; i += 4) {
|
||||
const group = LETTERS_UPPER.slice(i, i + 4);
|
||||
const label =
|
||||
const title =
|
||||
group.length > 1 ? `${group[0]}–${group[group.length - 1]}` : group[0];
|
||||
DAILY_GROUPS.push({ label, letters: [...group] });
|
||||
DAILY_GROUPS.push({ title, letters: [...group] });
|
||||
}
|
||||
|
||||
function getDailyGroupIndex(letters?: string[]): number {
|
||||
@@ -93,11 +47,7 @@ function getDailyGroupIndex(letters?: string[]): number {
|
||||
return idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
/** 根据 worksheetId 查找页面元信息 */
|
||||
function pageInfoLookup(id: string) {
|
||||
const m = MODE_MAP[id];
|
||||
return m ? { title: m.label, desc: m.desc } : undefined;
|
||||
}
|
||||
const pageInfoLookup = getModeInfo;
|
||||
|
||||
type PageData = CanvasDataState & {
|
||||
worksheetId: string;
|
||||
@@ -110,13 +60,17 @@ type PageData = CanvasDataState & {
|
||||
showNextLetter: boolean;
|
||||
showTripleGroupPicker: boolean;
|
||||
showDailyGroupPicker: boolean;
|
||||
tripleGroups: { label: string; letters: string[] }[];
|
||||
tripleGroups: { title: string; letters: string[] }[];
|
||||
selectedTripleGroupIdx: number;
|
||||
dailyGroups: { label: string; letters: string[] }[];
|
||||
dailyGroups: { title: string; letters: string[] }[];
|
||||
selectedDailyGroupIdx: number;
|
||||
letterGrid: string[];
|
||||
modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>;
|
||||
modeOptions: typeof LETTER_TRACING_MODE_OPTIONS;
|
||||
isPreviewFavorite: boolean;
|
||||
isDevEnv: boolean;
|
||||
debugPublishVisible: boolean;
|
||||
debugPublishLoading: boolean;
|
||||
debugPublishMeta: DebugPublishMeta | null;
|
||||
};
|
||||
|
||||
createPage(
|
||||
@@ -148,8 +102,12 @@ createPage(
|
||||
dailyGroups: DAILY_GROUPS,
|
||||
selectedDailyGroupIdx: 0,
|
||||
letterGrid: LETTERS_PAIRS,
|
||||
modeOptions: [...MODES],
|
||||
modeOptions: LETTER_TRACING_MODE_OPTIONS,
|
||||
isPreviewFavorite: false,
|
||||
isDevEnv: false,
|
||||
debugPublishVisible: false,
|
||||
debugPublishLoading: false,
|
||||
debugPublishMeta: null,
|
||||
} as unknown as PageData,
|
||||
|
||||
/** 页面加载:从路由参数获取 worksheetId、letter、case、font 并应用,同时预加载字体 */
|
||||
@@ -164,9 +122,10 @@ createPage(
|
||||
}
|
||||
loadLetterFont(this.fontProfile).catch(() => {});
|
||||
const worksheetId =
|
||||
options.id && MODE_MAP[options.id]
|
||||
options.id && isValidMode(options.id)
|
||||
? options.id
|
||||
: 'letter-tracing-single';
|
||||
this.syncDebugPublishEnv();
|
||||
this.applyWorksheet(worksheetId);
|
||||
|
||||
const updates: Partial<PageData> = {};
|
||||
@@ -216,11 +175,11 @@ createPage(
|
||||
});
|
||||
},
|
||||
|
||||
/** 构建运行时生成器配置:合并 worksheet 预设 + 当前页面状态 */
|
||||
/** 构建运行时生成器配置:合并页面模式预设 + 当前页面状态 */
|
||||
buildRuntimeConfig(): LetterTracingGeneratorConfig {
|
||||
const def = getWorksheetById(this.data.worksheetId);
|
||||
const base = (def?.generatorConfig ??
|
||||
{}) as Partial<LetterTracingGeneratorConfig>;
|
||||
const base: Partial<LetterTracingGeneratorConfig> = {
|
||||
mode: this.data.traceMode,
|
||||
};
|
||||
const letterCase =
|
||||
this.data.traceMode === 'letter-tracing-single'
|
||||
? (base.letterCase ?? 'upper')
|
||||
@@ -321,6 +280,14 @@ createPage(
|
||||
});
|
||||
},
|
||||
|
||||
getPublishMeta(): DebugPublishMeta {
|
||||
const meta = getPublishMetaByMode(this.data.worksheetId);
|
||||
if (!meta) {
|
||||
throw new Error('当前题型配置不存在');
|
||||
}
|
||||
return meta;
|
||||
},
|
||||
|
||||
/** 用户在三字母分组选择器中选择了一组 */
|
||||
onSelectTripleGroup(e: WechatMiniprogram.TouchEvent) {
|
||||
const idx = Number(e.currentTarget.dataset.idx);
|
||||
@@ -338,9 +305,9 @@ createPage(
|
||||
},
|
||||
|
||||
/**
|
||||
* 应用指定的 worksheet 配置到页面状态
|
||||
* 根据 worksheet 的 generatorConfig 决定 UI 控件的显隐(字母选择器、大小写切换等)
|
||||
* @param worksheetId - 要应用的 worksheet ID
|
||||
* 应用指定的页面模式配置到页面状态
|
||||
* 根据当前模式的运行时默认参数决定 UI 控件的显隐(字母选择器、大小写切换等)
|
||||
* @param worksheetId - 要应用的模式 ID
|
||||
* @param options.preserveLetter - 是否保留当前选中的字母(模式切换时使用)
|
||||
* @param options.redraw - 是否立即重绘 Canvas
|
||||
*/
|
||||
@@ -351,11 +318,12 @@ createPage(
|
||||
redraw?: boolean;
|
||||
},
|
||||
) {
|
||||
const def = getWorksheetById(worksheetId);
|
||||
console.log('def', def);
|
||||
const base = (def?.generatorConfig ??
|
||||
{}) as Partial<LetterTracingGeneratorConfig>;
|
||||
const merged = mergeLetterTracingConfig(base, {});
|
||||
if (!isValidMode(worksheetId)) return;
|
||||
|
||||
const merged = mergeLetterTracingConfig(
|
||||
{ mode: worksheetId as LetterTracingMode },
|
||||
{},
|
||||
);
|
||||
|
||||
const traceMode = merged.mode;
|
||||
const showLetterPicker = traceMode === 'letter-tracing-single';
|
||||
@@ -397,10 +365,7 @@ createPage(
|
||||
selectedDailyGroupIdx,
|
||||
},
|
||||
() => {
|
||||
this.initPageInfo(
|
||||
worksheetId,
|
||||
MODE_MAP[worksheetId]?.label ?? '字母描红',
|
||||
);
|
||||
this.initPageInfo(worksheetId, '字母描红');
|
||||
|
||||
if (this.drawService) {
|
||||
this.drawService.options.title = this.data.pageTitle;
|
||||
|
||||
@@ -57,14 +57,14 @@
|
||||
<view class="lt-chip-row lt-chip-row--wrap lt-chip-row--triple">
|
||||
<view
|
||||
wx:for="{{tripleGroups}}"
|
||||
wx:key="label"
|
||||
wx:key="title"
|
||||
class="lt-chip lt-chip--triple {{selectedTripleGroupIdx === index ? 'lt-chip--active' : ''}}"
|
||||
hover-class="lt-chip--pressed"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
data-idx="{{index}}"
|
||||
bind:tap="onSelectTripleGroup">
|
||||
{{item.label}}
|
||||
{{item.title}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -74,14 +74,14 @@
|
||||
<view class="lt-chip-row lt-chip-row--wrap lt-chip-row--triple">
|
||||
<view
|
||||
wx:for="{{dailyGroups}}"
|
||||
wx:key="label"
|
||||
wx:key="title"
|
||||
class="lt-chip lt-chip--triple {{selectedDailyGroupIdx === index ? 'lt-chip--active' : ''}}"
|
||||
hover-class="lt-chip--pressed"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
data-idx="{{index}}"
|
||||
bind:tap="onSelectDailyGroup">
|
||||
{{item.label}}
|
||||
{{item.title}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -119,7 +119,7 @@
|
||||
size="42rpx"
|
||||
color="{{worksheetId === item.id ? '#453900' : '#605b50'}}"
|
||||
custom-class="lt-mode-card__icon" />
|
||||
<text class="lt-mode-card__label">{{item.label}}</text>
|
||||
<text class="lt-mode-card__label">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -133,6 +133,16 @@
|
||||
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"
|
||||
|
||||
@@ -4,14 +4,19 @@ export type CategoryItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
icon: string;
|
||||
img?: string;
|
||||
icon?: string;
|
||||
/** 无图时在列表中用 icon 占位 */
|
||||
previewImg?: string;
|
||||
ageBand: string;
|
||||
difficulty: string;
|
||||
ageMin?: number;
|
||||
ageMax?: number;
|
||||
difficulty: 1 | 2 | 3 | 4;
|
||||
difficultyLabel: string;
|
||||
path: string;
|
||||
available: boolean;
|
||||
likes: number;
|
||||
downloads: number;
|
||||
/** 列表底部展示的日期(静态数据用 id 派生稳定值) */
|
||||
date: string;
|
||||
};
|
||||
|
||||
@@ -27,23 +32,17 @@ export type CategoryDataset = {
|
||||
categories: CategoryGroup[];
|
||||
};
|
||||
|
||||
const DIFFICULTY_MAP: Record<string, string> = {
|
||||
beginner: '入门',
|
||||
basic: '基础',
|
||||
intermediate: '进阶',
|
||||
advanced: '挑战',
|
||||
const DIFFICULTY_LABELS: Record<CategoryItem['difficulty'], string> = {
|
||||
1: '入门',
|
||||
2: '基础',
|
||||
3: '进阶',
|
||||
4: '挑战',
|
||||
};
|
||||
|
||||
function d(key: string): string {
|
||||
return DIFFICULTY_MAP[key] ?? key;
|
||||
}
|
||||
|
||||
function age(min: number, max: number): string {
|
||||
return `${min}-${max}岁`;
|
||||
}
|
||||
|
||||
const TODAY = '2026-4-22';
|
||||
|
||||
/** 基于 id 生成稳定的伪随机统计数 */
|
||||
function statsFromId(id: string): { likes: number; downloads: number } {
|
||||
let h = 0;
|
||||
@@ -53,80 +52,649 @@ function statsFromId(id: string): { likes: number; downloads: number } {
|
||||
return { likes, downloads };
|
||||
}
|
||||
|
||||
type ItemInput = Omit<CategoryItem, 'likes' | 'downloads' | 'date'>;
|
||||
function dateFromId(id: string): string {
|
||||
let h = 0;
|
||||
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) | 0;
|
||||
const days = Math.abs(h % 500);
|
||||
const d = new Date('2024-01-01');
|
||||
d.setDate(d.getDate() + days);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
type ItemInput = Omit<
|
||||
CategoryItem,
|
||||
'likes' | 'downloads' | 'date' | 'difficultyLabel'
|
||||
>;
|
||||
|
||||
function item(input: ItemInput): CategoryItem {
|
||||
const { likes, downloads } = statsFromId(input.id);
|
||||
return { ...input, likes, downloads, date: TODAY };
|
||||
return {
|
||||
...input,
|
||||
likes,
|
||||
downloads,
|
||||
date: dateFromId(input.id),
|
||||
difficultyLabel: DIFFICULTY_LABELS[input.difficulty],
|
||||
};
|
||||
}
|
||||
|
||||
const CATEGORY_ITEMS_BY_ID: Record<string, CategoryItem[]> = {
|
||||
math: [
|
||||
item({ id: 'number-find', title: '找数字,涂一涂', subtitle: '在数字方阵中找出目标数字并涂色', icon: '🔍', img: '/assets/entrancePicture/math/number-find.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-find', available: true }),
|
||||
item({ id: 'number-write', title: '看数字,写一写', subtitle: '按笔画顺序练习书写数字', icon: '✏️', img: '/assets/entrancePicture/math/number-write.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-write', available: true }),
|
||||
item({ id: 'number-coloring', title: '按数字,涂颜色', subtitle: '按指定数字给对应圆圈涂色', icon: '🎨', img: '/assets/entrancePicture/math/number-coloring.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-coloring', available: true }),
|
||||
item({ id: 'counting-matching', title: '数一数,连一连', subtitle: '连线配对数字和对应数量图形', icon: '🔗', img: '/assets/entrancePicture/math/count-match.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=counting-matching', available: true }),
|
||||
item({ id: 'number-object-match', title: '数物连线', subtitle: '连线相同数量的物品和数字', icon: '🔗', img: '/assets/entrancePicture/math/number-object-match.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-object-match', available: true }),
|
||||
item({ id: 'number-object-fill', title: '数物填写', subtitle: '数物品数量,填写对应数字', icon: '✏️', img: '/assets/entrancePicture/math/number-object-fill.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-object-fill', available: true }),
|
||||
item({ id: 'counting-select', title: '数一数,选一选', subtitle: '数出物品数量,圈出正确答案', icon: '✓', img: '/assets/entrancePicture/math/counting-select.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=counting-select', available: true }),
|
||||
item({ id: 'counting-fill', title: '数一数,填一填', subtitle: '数出物品数量,填写数字', icon: '✏️', img: '/assets/entrancePicture/math/counting-fill.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=counting-fill', available: true }),
|
||||
item({ id: 'compare', title: '数一数,比大小', subtitle: '比较数量,填入 ><=', icon: '⚖️', img: '/assets/entrancePicture/math/compare.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=compare', available: true }),
|
||||
item({ id: 'number-sort', title: '数字排序', subtitle: '写出正确的数字顺序', icon: '🔢', img: '/assets/entrancePicture/math/number-sort.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=number-sort', available: true }),
|
||||
item({ id: 'missing-number', title: '填上缺少的数字', subtitle: '在数列中找出并填写缺失数字', icon: '❓', img: '/assets/entrancePicture/math/missing-number.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=missing-number', available: true }),
|
||||
item({ id: 'number-decompose', title: '10以内数的分与合', subtitle: '把数字分一分,合一合', icon: '🔢', img: '/assets/entrancePicture/math/number-decompose.png', ageBand: age(4, 6), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-decompose', available: true }),
|
||||
item({ id: 'number-decompose-20', title: '20以内数的分与合', subtitle: '把数字分一分,合一合', icon: '🔢', img: '/assets/entrancePicture/math/number-decompose-20.png', ageBand: age(5, 7), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=number-decompose-20', available: true }),
|
||||
item({ id: 'one-digit-addition', title: '一位数加法', subtitle: '通过圆点学习一位数加法运算', icon: '➕', img: '/assets/entrancePicture/math/one-digit-addition.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=one-digit-addition', available: true }),
|
||||
item({ id: 'addition-5', title: '5以内加法', subtitle: '图形化展示 5 以内加法', icon: '➕', img: '/assets/entrancePicture/math/addition-5.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=addition-5', available: true }),
|
||||
item({ id: 'addition-10', title: '10以内加法', subtitle: '图形化展示 10 以内加法', icon: '➕', img: '/assets/entrancePicture/math/addition-10.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=addition-10', available: true }),
|
||||
item({ id: 'subtraction-10', title: '10以内减法', subtitle: '图形化展示 10 以内减法', icon: '➖', img: '/assets/entrancePicture/math/subtraction-10.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=subtraction-10', available: true }),
|
||||
item({ id: 'addition-subtraction-10', title: '10以内加减法', subtitle: '加减法混合运算', icon: '±', img: '/assets/entrancePicture/math/addition-subtraction-10.png', ageBand: age(5, 7), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=addition-subtraction-10', available: true }),
|
||||
item({ id: 'make-ten', title: '凑十法练习', subtitle: '20 以内进位加法', icon: '➕', img: '/assets/entrancePicture/math/make-ten.png', ageBand: age(5, 7), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=make-ten', available: true }),
|
||||
item({ id: 'break-ten', title: '破十法练习', subtitle: '20 以内退位减法', icon: '➖', img: '/assets/entrancePicture/math/break-ten.png', ageBand: age(5, 7), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=break-ten', available: true }),
|
||||
item({ id: 'flat-ten', title: '平十法练习', subtitle: '20 以内退位减法', icon: '➖', img: '/assets/entrancePicture/math/flat-ten.png', ageBand: age(5, 7), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=flat-ten', available: true }),
|
||||
item({ id: 'borrow-ten', title: '借十法练习', subtitle: '20 以上退位减法', icon: '➖', img: '/assets/entrancePicture/math/borrow-ten.png', ageBand: age(6, 8), difficulty: d('advanced'), path: '/mathPages/mathDraw/mathDraw?id=borrow-ten', available: true }),
|
||||
item({ id: 'practice-addition', title: '加法运算', subtitle: '10/20/50/100 以内加法', icon: '➕', img: '/assets/entrancePicture/math/practice-addition.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=practice-addition', available: true }),
|
||||
item({ id: 'practice-subtraction', title: '减法运算', subtitle: '10/20/50/100 以内减法', icon: '➖', img: '/assets/entrancePicture/math/practice-subtraction.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=practice-subtraction', available: true }),
|
||||
item({ id: 'practice-mixed', title: '混合运算', subtitle: '10/20/50/100 以内加减法混合', icon: '±', img: '/assets/entrancePicture/math/practice-mixed.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=practice-mixed', available: true }),
|
||||
item({ id: 'multiplication-table', title: '九九乘法表', subtitle: '学习九九乘法口诀', icon: '✖️', img: '/assets/entrancePicture/math/multiplication-table.png', ageBand: age(6, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=multiplication-table', available: true }),
|
||||
item({
|
||||
id: 'number-find',
|
||||
title: '找数字,涂一涂',
|
||||
subtitle: '在数字方阵中找出目标数字并涂色',
|
||||
icon: '🔍',
|
||||
previewImg: '/assets/entrancePicture/math/number-find.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-find',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'number-write',
|
||||
title: '看数字,写一写',
|
||||
subtitle: '按笔画顺序练习书写数字',
|
||||
icon: '✏️',
|
||||
previewImg: '/assets/entrancePicture/math/number-write.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-write',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'number-coloring',
|
||||
title: '按数字,涂颜色',
|
||||
subtitle: '按指定数字给对应圆圈涂色',
|
||||
icon: '🎨',
|
||||
previewImg: '/assets/entrancePicture/math/number-coloring.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-coloring',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'counting-matching',
|
||||
title: '数一数,连一连',
|
||||
subtitle: '连线配对数字和对应数量图形',
|
||||
icon: '🔗',
|
||||
previewImg: '/assets/entrancePicture/math/count-match.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=counting-matching',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'number-object-match',
|
||||
title: '数物连线',
|
||||
subtitle: '连线相同数量的物品和数字',
|
||||
icon: '🔗',
|
||||
previewImg: '/assets/entrancePicture/math/number-object-match.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-object-match',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'number-object-fill',
|
||||
title: '数物填写',
|
||||
subtitle: '数物品数量,填写对应数字',
|
||||
icon: '✏️',
|
||||
previewImg: '/assets/entrancePicture/math/number-object-fill.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-object-fill',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'counting-select',
|
||||
title: '数一数,选一选',
|
||||
subtitle: '数出物品数量,圈出正确答案',
|
||||
icon: '✓',
|
||||
previewImg: '/assets/entrancePicture/math/counting-select.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=counting-select',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'counting-fill',
|
||||
title: '数一数,填一填',
|
||||
subtitle: '数出物品数量,填写数字',
|
||||
icon: '✏️',
|
||||
previewImg: '/assets/entrancePicture/math/counting-fill.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=counting-fill',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'compare',
|
||||
title: '数一数,比大小',
|
||||
subtitle: '比较数量,填入 ><=',
|
||||
icon: '⚖️',
|
||||
previewImg: '/assets/entrancePicture/math/compare.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=compare',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'number-sort',
|
||||
title: '数字排序',
|
||||
subtitle: '写出正确的数字顺序',
|
||||
icon: '🔢',
|
||||
previewImg: '/assets/entrancePicture/math/number-sort.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-sort',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'missing-number',
|
||||
title: '填上缺少的数字',
|
||||
subtitle: '在数列中找出并填写缺失数字',
|
||||
icon: '❓',
|
||||
previewImg: '/assets/entrancePicture/math/missing-number.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=missing-number',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'number-decompose',
|
||||
title: '10以内数的分与合',
|
||||
subtitle: '把数字分一分,合一合',
|
||||
icon: '🔢',
|
||||
previewImg: '/assets/entrancePicture/math/number-decompose.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 1,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-decompose',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'number-decompose-20',
|
||||
title: '20以内数的分与合',
|
||||
subtitle: '把数字分一分,合一合',
|
||||
icon: '🔢',
|
||||
previewImg: '/assets/entrancePicture/math/number-decompose-20.png',
|
||||
ageBand: age(5, 7),
|
||||
difficulty: 2,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=number-decompose-20',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'one-digit-addition',
|
||||
title: '一位数加法',
|
||||
subtitle: '通过圆点学习一位数加法运算',
|
||||
icon: '➕',
|
||||
previewImg: '/assets/entrancePicture/math/one-digit-addition.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=one-digit-addition',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'addition-5',
|
||||
title: '5以内加法',
|
||||
subtitle: '图形化展示 5 以内加法',
|
||||
icon: '➕',
|
||||
previewImg: '/assets/entrancePicture/math/addition-5.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition-5',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'addition-10',
|
||||
title: '10以内加法',
|
||||
subtitle: '图形化展示 10 以内加法',
|
||||
icon: '➕',
|
||||
previewImg: '/assets/entrancePicture/math/addition-10.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition-10',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'subtraction-10',
|
||||
title: '10以内减法',
|
||||
subtitle: '图形化展示 10 以内减法',
|
||||
icon: '➖',
|
||||
previewImg: '/assets/entrancePicture/math/subtraction-10.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=subtraction-10',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'addition-subtraction-10',
|
||||
title: '10以内加减法',
|
||||
subtitle: '加减法混合运算',
|
||||
icon: '±',
|
||||
previewImg:
|
||||
'/assets/entrancePicture/math/addition-subtraction-10.png',
|
||||
ageBand: age(5, 7),
|
||||
difficulty: 2,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=addition-subtraction-10',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'make-ten',
|
||||
title: '凑十法练习',
|
||||
subtitle: '20 以内进位加法',
|
||||
icon: '➕',
|
||||
previewImg: '/assets/entrancePicture/math/make-ten.png',
|
||||
ageBand: age(5, 7),
|
||||
difficulty: 3,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=make-ten',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'break-ten',
|
||||
title: '破十法练习',
|
||||
subtitle: '20 以内退位减法',
|
||||
icon: '➖',
|
||||
previewImg: '/assets/entrancePicture/math/break-ten.png',
|
||||
ageBand: age(5, 7),
|
||||
difficulty: 3,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=break-ten',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'flat-ten',
|
||||
title: '平十法练习',
|
||||
subtitle: '20 以内退位减法',
|
||||
icon: '➖',
|
||||
previewImg: '/assets/entrancePicture/math/flat-ten.png',
|
||||
ageBand: age(5, 7),
|
||||
difficulty: 3,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=flat-ten',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'borrow-ten',
|
||||
title: '借十法练习',
|
||||
subtitle: '20 以上退位减法',
|
||||
icon: '➖',
|
||||
previewImg: '/assets/entrancePicture/math/borrow-ten.png',
|
||||
ageBand: age(6, 8),
|
||||
difficulty: 4,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=borrow-ten',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'practice-addition',
|
||||
title: '加法运算',
|
||||
subtitle: '10/20/50/100 以内加法',
|
||||
icon: '➕',
|
||||
previewImg: '/assets/entrancePicture/math/practice-addition.png',
|
||||
ageBand: age(5, 8),
|
||||
difficulty: 3,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-addition',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'practice-subtraction',
|
||||
title: '减法运算',
|
||||
subtitle: '10/20/50/100 以内减法',
|
||||
icon: '➖',
|
||||
previewImg: '/assets/entrancePicture/math/practice-subtraction.png',
|
||||
ageBand: age(5, 8),
|
||||
difficulty: 3,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-subtraction',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'practice-mixed',
|
||||
title: '混合运算',
|
||||
subtitle: '10/20/50/100 以内加减法混合',
|
||||
icon: '±',
|
||||
previewImg: '/assets/entrancePicture/math/practice-mixed.png',
|
||||
ageBand: age(5, 8),
|
||||
difficulty: 3,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=practice-mixed',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'multiplication-table',
|
||||
title: '九九乘法表',
|
||||
subtitle: '学习九九乘法口诀',
|
||||
icon: '✖️',
|
||||
previewImg: '/assets/entrancePicture/math/multiplication-table.png',
|
||||
ageBand: age(6, 8),
|
||||
difficulty: 3,
|
||||
path: '/mathPages/mathDraw/mathDraw?id=multiplication-table',
|
||||
available: true,
|
||||
}),
|
||||
],
|
||||
pinyin: [
|
||||
item({ id: 'pinyin-initials', title: '声母描红', subtitle: '23个声母认读与书写练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('basic'), path: '', available: false }),
|
||||
item({ id: 'pinyin-finals', title: '韵母描红', subtitle: '24个韵母认读与书写练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('basic'), path: '', available: false }),
|
||||
item({ id: 'pinyin-overall', title: '整体认读音节', subtitle: '16个整体认读音节练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('intermediate'), path: '', available: false }),
|
||||
item({
|
||||
id: 'pinyin-initials',
|
||||
title: '声母描红',
|
||||
subtitle: '23个声母认读与书写练习',
|
||||
icon: '🔤',
|
||||
ageBand: age(5, 7),
|
||||
difficulty: 2,
|
||||
path: '',
|
||||
available: false,
|
||||
}),
|
||||
item({
|
||||
id: 'pinyin-finals',
|
||||
title: '韵母描红',
|
||||
subtitle: '24个韵母认读与书写练习',
|
||||
icon: '🔤',
|
||||
ageBand: age(5, 7),
|
||||
difficulty: 2,
|
||||
path: '',
|
||||
available: false,
|
||||
}),
|
||||
item({
|
||||
id: 'pinyin-overall',
|
||||
title: '整体认读音节',
|
||||
subtitle: '16个整体认读音节练习',
|
||||
icon: '🔤',
|
||||
ageBand: age(5, 7),
|
||||
difficulty: 3,
|
||||
path: '',
|
||||
available: false,
|
||||
}),
|
||||
],
|
||||
puzzle: [
|
||||
item({ id: 'color-shape-match', title: '根据颜色画图形', subtitle: '根据颜色画出对应图形', icon: '🎯', img: '/assets/entrancePicture/focus/color-shape-match.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=color-shape-match', available: true }),
|
||||
item({ id: 'shape-symbol', title: '图形符号配对', subtitle: '根据图形画对应符号', icon: '🔗', img: '/assets/entrancePicture/focus/shape-symbol.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=shape-symbol', available: true }),
|
||||
item({ id: 'shape-recognition', title: '识别形状', subtitle: '识别形状,涂一涂', icon: '🔍', img: '/assets/entrancePicture/focus/shape-recognition.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=shape-recognition', available: true }),
|
||||
item({ id: 'position-coloring', title: '方位涂涂乐', subtitle: '观察位置,在方格中涂色', icon: '📍', img: '/assets/entrancePicture/focus/position-coloring.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=position-coloring', available: true }),
|
||||
item({ id: 'color-pattern', title: '颜色找规律', subtitle: '观察颜色规律,在空白图形中涂色', icon: '🎨', img: '/assets/entrancePicture/focus/color-pattern.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=color-pattern', available: true }),
|
||||
item({ id: 'match-connect', title: '连连看', subtitle: '根据物品连一连', icon: '🔗', img: '/assets/entrancePicture/focus/match-connect.png', ageBand: age(3, 6), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=match-connect', available: true }),
|
||||
item({ id: 'line-recognition', title: '线条识别', subtitle: '认识不同线条,画出颜色对应的线条', icon: '📏', img: '/assets/entrancePicture/focus/line-recognition.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=line-recognition', available: true }),
|
||||
item({ id: 'grid-reasoning', title: '方格推理', subtitle: '推理出合并方格并连线', icon: '🧩', img: '/assets/entrancePicture/focus/grid-reasoning.png', ageBand: age(5, 7), difficulty: d('intermediate'), path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning', available: true }),
|
||||
item({ id: 'code-connect', title: '译码连线', subtitle: '按数字顺序将数字对应颜色连线', icon: '🔢', img: '/assets/entrancePicture/focus/code-connect.png', ageBand: age(5, 7), difficulty: d('intermediate'), path: '/focusPages/focusDraw/focusDraw?id=code-connect', available: true }),
|
||||
item({ id: 'dot-connect', title: '数字点连线', subtitle: '按数字顺序连点成图', icon: '🔗', img: '/assets/entrancePicture/focus/dot-connect.png', ageBand: age(3, 6), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=dot-connect', available: true }),
|
||||
item({ id: 'grid-drawing-3x3', title: '格子仿画 3×3', subtitle: '简单有趣,培养专注力', icon: '🎨', img: '/assets/entrancePicture/focus/grid-drawing-3x3.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-3x3', available: true }),
|
||||
item({ id: 'grid-drawing-5x5', title: '格子仿画 5×5', subtitle: '创意挑战,提升观察力', icon: '🎨', img: '/assets/entrancePicture/focus/grid-drawing-5x5.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-5x5', available: true }),
|
||||
item({ id: 'grid-drawing-7x7', title: '格子仿画 7×7', subtitle: '大师挑战,锻炼耐心', icon: '🎨', img: '/assets/entrancePicture/focus/grid-drawing-7x7.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-7x7', available: true }),
|
||||
item({
|
||||
id: 'color-shape-match',
|
||||
title: '根据颜色画图形',
|
||||
subtitle: '根据颜色画出对应图形',
|
||||
icon: '🎯',
|
||||
previewImg: '/assets/entrancePicture/focus/color-shape-match.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=color-shape-match',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'shape-symbol',
|
||||
title: '图形符号配对',
|
||||
subtitle: '根据图形画对应符号',
|
||||
icon: '🔗',
|
||||
previewImg: '/assets/entrancePicture/focus/shape-symbol.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=shape-symbol',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'shape-recognition',
|
||||
title: '识别形状',
|
||||
subtitle: '识别形状,涂一涂',
|
||||
icon: '🔍',
|
||||
previewImg: '/assets/entrancePicture/focus/shape-recognition.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=shape-recognition',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'position-coloring',
|
||||
title: '方位涂涂乐',
|
||||
subtitle: '观察位置,在方格中涂色',
|
||||
icon: '📍',
|
||||
previewImg: '/assets/entrancePicture/focus/position-coloring.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=position-coloring',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'color-pattern',
|
||||
title: '颜色找规律',
|
||||
subtitle: '观察颜色规律,在空白图形中涂色',
|
||||
icon: '🎨',
|
||||
previewImg: '/assets/entrancePicture/focus/color-pattern.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=color-pattern',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'match-connect',
|
||||
title: '连连看',
|
||||
subtitle: '根据物品连一连',
|
||||
icon: '🔗',
|
||||
previewImg: '/assets/entrancePicture/focus/match-connect.png',
|
||||
ageBand: age(3, 6),
|
||||
difficulty: 1,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'line-recognition',
|
||||
title: '线条识别',
|
||||
subtitle: '认识不同线条,画出颜色对应的线条',
|
||||
icon: '📏',
|
||||
previewImg: '/assets/entrancePicture/focus/line-recognition.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=line-recognition',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'grid-reasoning',
|
||||
title: '方格推理',
|
||||
subtitle: '推理出合并方格并连线',
|
||||
icon: '🧩',
|
||||
previewImg: '/assets/entrancePicture/focus/grid-reasoning.png',
|
||||
ageBand: age(5, 7),
|
||||
difficulty: 3,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'code-connect',
|
||||
title: '译码连线',
|
||||
subtitle: '按数字顺序将数字对应颜色连线',
|
||||
icon: '🔢',
|
||||
previewImg: '/assets/entrancePicture/focus/code-connect.png',
|
||||
ageBand: age(5, 7),
|
||||
difficulty: 3,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=code-connect',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'dot-connect',
|
||||
title: '数字点连线',
|
||||
subtitle: '按数字顺序连点成图',
|
||||
icon: '🔗',
|
||||
previewImg: '/assets/entrancePicture/focus/dot-connect.png',
|
||||
ageBand: age(3, 6),
|
||||
difficulty: 1,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=dot-connect',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'grid-drawing-3x3',
|
||||
title: '格子仿画 3×3',
|
||||
subtitle: '简单有趣,培养专注力',
|
||||
icon: '🎨',
|
||||
previewImg: '/assets/entrancePicture/focus/grid-drawing-3x3.png',
|
||||
ageBand: age(3, 5),
|
||||
difficulty: 1,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-3x3',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'grid-drawing-5x5',
|
||||
title: '格子仿画 5×5',
|
||||
subtitle: '创意挑战,提升观察力',
|
||||
icon: '🎨',
|
||||
previewImg: '/assets/entrancePicture/focus/grid-drawing-5x5.png',
|
||||
ageBand: age(4, 6),
|
||||
difficulty: 2,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-5x5',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'grid-drawing-7x7',
|
||||
title: '格子仿画 7×7',
|
||||
subtitle: '大师挑战,锻炼耐心',
|
||||
icon: '🎨',
|
||||
previewImg: '/assets/entrancePicture/focus/grid-drawing-7x7.png',
|
||||
ageBand: age(5, 8),
|
||||
difficulty: 3,
|
||||
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-7x7',
|
||||
available: true,
|
||||
}),
|
||||
],
|
||||
chinese: [
|
||||
item({ id: 'word-recognition', title: '识字卡', subtitle: '输入或选字生成涂色识字卡', icon: '📖', ageBand: age(3, 6), difficulty: d('beginner'), path: '/pages/index/index', available: true }),
|
||||
item({ id: 'copybook', title: '练字帖', subtitle: '选字生成田字格笔顺练字帖', icon: '✏️', ageBand: age(4, 7), difficulty: d('basic'), path: '/pages/copyBook/copyBook', available: true }),
|
||||
item({
|
||||
id: 'word-recognition',
|
||||
title: '识字卡',
|
||||
subtitle: '输入或选字生成涂色识字卡',
|
||||
icon: '📖',
|
||||
ageBand: age(3, 6),
|
||||
difficulty: 1,
|
||||
path: '/pages/index/index',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'copybook',
|
||||
title: '练字帖',
|
||||
subtitle: '选字生成田字格笔顺练字帖',
|
||||
icon: '✏️',
|
||||
ageBand: age(4, 7),
|
||||
difficulty: 2,
|
||||
path: '/pages/copyBook/copyBook',
|
||||
available: true,
|
||||
}),
|
||||
],
|
||||
english: [
|
||||
item({ id: 'letter-tracing-single', title: '看图描红', subtitle: '单字母配图、例句与六行四线三格描红', icon: '🔠', img: '/assets/entrancePicture/english/letter-tracing-single.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', available: true }),
|
||||
item({ id: 'letter-tracing-single2', title: '看图描红', subtitle: '单字母配图、例句与六行四线三格描红', icon: '🔠', img: '/assets/entrancePicture/english/letter-tracing-single2.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', available: true }),
|
||||
item({ id: 'letter-tracing-upper-lower', title: '字母总览', subtitle: 'Uppercase / Lowercase 分区总览', icon: '🔡', img: '/assets/entrancePicture/english/letter-tracing-upper-lower.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-upper-lower', available: true }),
|
||||
item({ id: 'letter-tracing-case-pairing', title: '大小写练习', subtitle: '半组字母左大写右小写,逐行对照描红', icon: '✏️', img: '/assets/entrancePicture/english/letter-tracing-case-pairing.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-case-pairing', available: true }),
|
||||
item({ id: 'letter-tracing-two-column', title: '两列练习', subtitle: '左 A-M、右 N-Z,大小写配对描红', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-two-column.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column', available: true }),
|
||||
item({ id: 'letter-tracing-half', title: '单字母逐行', subtitle: '13 字母半表逐字练习', icon: '📝', img: '/assets/entrancePicture/english/letter-tracing-half.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-half', available: true }),
|
||||
item({ id: 'letter-tracing-three', title: '三字母精练', subtitle: '每页 3 个字母,大写 + 小写深度书写', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-three.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-three', available: true }),
|
||||
item({ id: 'letter-tracing-daily-checkin', title: '每日打卡', subtitle: '四宫格每日字母打卡练习', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-daily-checkin.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-daily-checkin', available: true }),
|
||||
item({
|
||||
id: 'letter-tracing-single',
|
||||
title: '看图描红',
|
||||
subtitle: '单字母配图、例句与六行四线三格描红',
|
||||
icon: '🔠',
|
||||
previewImg:
|
||||
'/assets/entrancePicture/english/letter-tracing-single.jpg',
|
||||
ageBand: age(4, 7),
|
||||
difficulty: 1,
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'letter-tracing-single2',
|
||||
title: '看图描红',
|
||||
subtitle: '单字母配图、例句与六行四线三格描红',
|
||||
icon: '🔠',
|
||||
previewImg:
|
||||
'/assets/entrancePicture/english/letter-tracing-single2.jpg',
|
||||
ageBand: age(4, 7),
|
||||
difficulty: 1,
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'letter-tracing-upper-lower',
|
||||
title: '字母总览',
|
||||
subtitle: 'Uppercase / Lowercase 分区总览',
|
||||
icon: '🔡',
|
||||
previewImg:
|
||||
'/assets/entrancePicture/english/letter-tracing-upper-lower.jpg',
|
||||
ageBand: age(4, 7),
|
||||
difficulty: 1,
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-upper-lower',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'letter-tracing-case-pairing',
|
||||
title: '大小写练习',
|
||||
subtitle: '半组字母左大写右小写,逐行对照描红',
|
||||
icon: '✏️',
|
||||
previewImg:
|
||||
'/assets/entrancePicture/english/letter-tracing-case-pairing.jpg',
|
||||
ageBand: age(4, 7),
|
||||
difficulty: 1,
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-case-pairing',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'letter-tracing-two-column',
|
||||
title: '两列练习',
|
||||
subtitle: '左 A-M、右 N-Z,大小写配对描红',
|
||||
icon: '🔤',
|
||||
previewImg:
|
||||
'/assets/entrancePicture/english/letter-tracing-two-column.jpg',
|
||||
ageBand: age(4, 7),
|
||||
difficulty: 1,
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'letter-tracing-half',
|
||||
title: '单字母逐行',
|
||||
subtitle: '13 字母半表逐字练习',
|
||||
icon: '📝',
|
||||
previewImg:
|
||||
'/assets/entrancePicture/english/letter-tracing-half.jpg',
|
||||
ageBand: age(4, 7),
|
||||
difficulty: 2,
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-half',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'letter-tracing-three',
|
||||
title: '三字母精练',
|
||||
subtitle: '每页 3 个字母,大写 + 小写深度书写',
|
||||
icon: '🔤',
|
||||
previewImg:
|
||||
'/assets/entrancePicture/english/letter-tracing-three.jpg',
|
||||
ageBand: age(4, 7),
|
||||
difficulty: 2,
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-three',
|
||||
available: true,
|
||||
}),
|
||||
item({
|
||||
id: 'letter-tracing-daily-checkin',
|
||||
title: '每日打卡',
|
||||
subtitle: '四宫格每日字母打卡练习',
|
||||
icon: '🔤',
|
||||
previewImg:
|
||||
'/assets/entrancePicture/english/letter-tracing-daily-checkin.jpg',
|
||||
ageBand: age(4, 7),
|
||||
difficulty: 2,
|
||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-daily-checkin',
|
||||
available: true,
|
||||
}),
|
||||
],
|
||||
craft: [
|
||||
item({ id: 'craft-coloring', title: '涂色卡', subtitle: '动物、交通、节日主题涂色', icon: '🎨', ageBand: age(3, 6), difficulty: d('beginner'), path: '', available: false }),
|
||||
item({ id: 'craft-origami', title: '折纸模板', subtitle: '打印后即可折叠的趣味模板', icon: '🪭', ageBand: age(4, 7), difficulty: d('basic'), path: '', available: false }),
|
||||
item({ id: 'craft-stickers', title: '贴纸打印', subtitle: '奖励贴纸与装饰贴纸', icon: '⭐', ageBand: age(3, 8), difficulty: d('beginner'), path: '', available: false }),
|
||||
item({
|
||||
id: 'craft-coloring',
|
||||
title: '涂色卡',
|
||||
subtitle: '动物、交通、节日主题涂色',
|
||||
icon: '🎨',
|
||||
ageBand: age(3, 6),
|
||||
difficulty: 1,
|
||||
path: '',
|
||||
available: false,
|
||||
}),
|
||||
item({
|
||||
id: 'craft-origami',
|
||||
title: '折纸模板',
|
||||
subtitle: '打印后即可折叠的趣味模板',
|
||||
icon: '🪭',
|
||||
ageBand: age(4, 7),
|
||||
difficulty: 2,
|
||||
path: '',
|
||||
available: false,
|
||||
}),
|
||||
item({
|
||||
id: 'craft-stickers',
|
||||
title: '贴纸打印',
|
||||
subtitle: '奖励贴纸与装饰贴纸',
|
||||
icon: '⭐',
|
||||
ageBand: age(3, 8),
|
||||
difficulty: 1,
|
||||
path: '',
|
||||
available: false,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ type CategoryTab = {
|
||||
icon: string;
|
||||
};
|
||||
|
||||
const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(({ id, name, icon }) => ({
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
}));
|
||||
const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(
|
||||
({ id, name, icon }) => ({
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
}),
|
||||
);
|
||||
|
||||
function buildAllItems(): CategoryItem[] {
|
||||
const items: CategoryItem[] = [];
|
||||
|
||||
@@ -59,9 +59,9 @@
|
||||
bindtap="onTapItem">
|
||||
<view class="cat-card__thumb">
|
||||
<image
|
||||
wx:if="{{item.img}}"
|
||||
wx:if="{{item.previewImg}}"
|
||||
class="cat-card__thumb-img"
|
||||
src="{{item.img}}"
|
||||
src="{{item.previewImg}}"
|
||||
mode="aspectFill" />
|
||||
<text wx:else class="cat-card__thumb-icon"
|
||||
>{{item.icon}}</text
|
||||
@@ -81,7 +81,7 @@
|
||||
>{{item.ageBand}}</text
|
||||
>
|
||||
<text class="tag tag--diff"
|
||||
>{{item.difficulty}}</text
|
||||
>{{item.difficultyLabel}}</text
|
||||
>
|
||||
</view>
|
||||
<view class="cat-card__footer">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories';
|
||||
import { AGE_BANDS } from '../../core/data/difficulty';
|
||||
import { CategoryId } from '../../core/models/category';
|
||||
|
||||
export type HomeDisplayItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description?: string;
|
||||
category: 'math' | 'chinese' | 'english' | 'puzzle' | 'craft';
|
||||
category: CategoryId;
|
||||
ageBand: string;
|
||||
difficulty: '入门' | '基础' | '进阶' | '挑战';
|
||||
icon: string;
|
||||
@@ -44,9 +45,8 @@ export type HomeDisplayDataset = {
|
||||
sections: HomeDisplaySection[];
|
||||
};
|
||||
|
||||
const HOME_CATEGORY_TABS: HomeDisplayDataset['categoryTabs'] = CATEGORY_LIST_WITH_ALL.map(
|
||||
({ id, name }) => ({ id, name }),
|
||||
);
|
||||
const HOME_CATEGORY_TABS: HomeDisplayDataset['categoryTabs'] =
|
||||
CATEGORY_LIST_WITH_ALL.map(({ id, name }) => ({ id, name }));
|
||||
|
||||
const HOME_AGE_BANDS = AGE_BANDS.map((item, index) => ({
|
||||
key: item.key,
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import type {
|
||||
WorksheetCloudCategory,
|
||||
WorksheetStatus,
|
||||
} from '../core/models/worksheet';
|
||||
|
||||
/**
|
||||
* 调试发布时的裁剪模式。
|
||||
* - header-footer: 同 node-tools 入口图脚本,默认裁掉页眉和页脚
|
||||
* - header-only: 仅裁掉页眉,保留底部品牌区
|
||||
* - none: 不裁剪,直接压缩整张 A4 预览图
|
||||
*/
|
||||
export type DebugCropMode = 'header-footer' | 'header-only' | 'none';
|
||||
|
||||
/**
|
||||
* 发布到 worksheets 集合的核心元数据。
|
||||
* 这里对齐《小程序云开发方案》中的题型字段设计,供页面侧生成 payload 使用。
|
||||
*/
|
||||
export interface DebugPublishMeta {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
category: WorksheetCloudCategory;
|
||||
subcategory?: string;
|
||||
path: string;
|
||||
ageMin: number;
|
||||
ageMax: number;
|
||||
grade: number;
|
||||
difficulty: 1 | 2 | 3 | 4;
|
||||
previewImg?: string;
|
||||
tags: string[];
|
||||
isNew: boolean;
|
||||
isHot: boolean;
|
||||
sortOrder?: number;
|
||||
status: WorksheetStatus;
|
||||
}
|
||||
|
||||
/** 发布弹窗里可调的图片处理参数。 */
|
||||
export interface DebugPublishSettings {
|
||||
cropMode: DebugCropMode;
|
||||
quality: number;
|
||||
width: number;
|
||||
maxSizeKB: number;
|
||||
}
|
||||
|
||||
/** 弹窗确认后回传给页面的表单结果。 */
|
||||
export interface DebugPublishConfirmDetail {
|
||||
meta: Pick<DebugPublishMeta, 'title' | 'subtitle' | 'tags' | 'status'>;
|
||||
settings: DebugPublishSettings;
|
||||
}
|
||||
|
||||
/** 图片处理组件收到的输入参数。 */
|
||||
export interface DebugProcessImageParams {
|
||||
sourcePath: string;
|
||||
settings: DebugPublishSettings;
|
||||
}
|
||||
|
||||
/** 图片处理完成后返回给页面的信息。 */
|
||||
export interface DebugProcessImageResult {
|
||||
tempFilePath: string;
|
||||
size: number;
|
||||
width: number;
|
||||
height: number;
|
||||
quality: number;
|
||||
}
|
||||
|
||||
/** 默认压缩质量,与文档里的入口图建议值保持一致。 */
|
||||
export const DEBUG_PUBLISH_DEFAULT_QUALITY = 90;
|
||||
/** 默认入口图宽度,沿用 node-tools 脚本的 600px。 */
|
||||
export const DEBUG_PUBLISH_DEFAULT_WIDTH = 600;
|
||||
/** 默认体积上限,超过后会继续降质压缩。 */
|
||||
export const DEBUG_PUBLISH_MAX_SIZE_KB = 200;
|
||||
|
||||
/** 页眉裁剪比例:110 / 842,来源于现有入口图脚本。 */
|
||||
export const DEBUG_CROP_TOP_RATIO = 110 / 842;
|
||||
/** 页脚裁剪比例:52 / 842,来源于现有入口图脚本。 */
|
||||
export const DEBUG_CROP_BOTTOM_RATIO = 52 / 842;
|
||||
|
||||
/** 仅开发版开放 Debug 发布能力。 */
|
||||
export function isDebugPublishEnabled(): boolean {
|
||||
const accountInfo = wx.getAccountInfoSync();
|
||||
return accountInfo.miniProgram.envVersion === 'develop';
|
||||
}
|
||||
|
||||
/** 限制压缩质量范围,避免过高或过低。 */
|
||||
export function clampPublishQuality(value: number): number {
|
||||
return Math.min(95, Math.max(40, Math.round(value)));
|
||||
}
|
||||
|
||||
/** 限制输出宽度范围,防止导出过小或过大。 */
|
||||
export function clampPublishWidth(value: number): number {
|
||||
return Math.min(1000, Math.max(400, Math.round(value)));
|
||||
}
|
||||
|
||||
/** 根据裁剪模式计算顶部 / 底部裁剪比例。 */
|
||||
export function getCropRatios(mode: DebugCropMode): {
|
||||
topRatio: number;
|
||||
bottomRatio: number;
|
||||
} {
|
||||
if (mode === 'header-only') {
|
||||
return {
|
||||
topRatio: DEBUG_CROP_TOP_RATIO,
|
||||
bottomRatio: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === 'none') {
|
||||
return {
|
||||
topRatio: 0,
|
||||
bottomRatio: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
topRatio: DEBUG_CROP_TOP_RATIO,
|
||||
bottomRatio: DEBUG_CROP_BOTTOM_RATIO,
|
||||
};
|
||||
}
|
||||
|
||||
/** 统一构造预览图在云存储中的路径。 */
|
||||
export function buildWorksheetPreviewCloudPath(
|
||||
category: WorksheetCloudCategory,
|
||||
id: string,
|
||||
): string {
|
||||
return `assets/previews/${category}/${id}.jpg`;
|
||||
}
|
||||
|
||||
/** 将输入框中的标签文本拆成标签数组,兼容英文逗号、中文逗号、顿号和空白。 */
|
||||
export function normalizeTagsInput(raw: string): string[] {
|
||||
return raw
|
||||
.split(/[,,、\s]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 由年龄段推断年级值。
|
||||
* 当前先用“年龄中心点 -> 年级”的简单映射,后续如有更细分规则可独立替换。
|
||||
*/
|
||||
export function inferGradeFromAge(ageMin: number, ageMax: number): number {
|
||||
const centerAge = Math.round((ageMin + ageMax) / 2);
|
||||
const ageGradeMap: Record<number, number> = {
|
||||
2: -4,
|
||||
3: -3,
|
||||
4: -2,
|
||||
5: -1,
|
||||
6: 0,
|
||||
7: 1,
|
||||
8: 2,
|
||||
9: 3,
|
||||
10: 4,
|
||||
11: 5,
|
||||
12: 6,
|
||||
};
|
||||
|
||||
return ageGradeMap[centerAge] ?? 0;
|
||||
}
|
||||
Reference in New Issue
Block a user