feat: 分类页骨架屏优化
This commit is contained in:
@@ -14,7 +14,6 @@
|
|||||||
"guide/guide",
|
"guide/guide",
|
||||||
"settings/settings",
|
"settings/settings",
|
||||||
"debug/debug",
|
"debug/debug",
|
||||||
"worksheetSync/worksheetSync",
|
|
||||||
"categoryManage/categoryManage",
|
"categoryManage/categoryManage",
|
||||||
"categoryContentManage/categoryContentManage",
|
"categoryContentManage/categoryContentManage",
|
||||||
"homeContentManage/homeContentManage",
|
"homeContentManage/homeContentManage",
|
||||||
@@ -42,6 +41,12 @@
|
|||||||
"name": "focusPages",
|
"name": "focusPages",
|
||||||
"pages": ["focusDraw/focusDraw", "shape/shape"],
|
"pages": ["focusDraw/focusDraw", "shape/shape"],
|
||||||
"independent": false
|
"independent": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "chinesePages",
|
||||||
|
"name": "chinesePages",
|
||||||
|
"pages": ["wordColoring/wordColoring"],
|
||||||
|
"independent": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"preloadRule": {
|
"preloadRule": {
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||||
|
import type { TemplateType } from '../../service/drawServiceFactory';
|
||||||
|
|
||||||
|
/** 本地 worksheet 元数据(与云库 `worksheets` 文档 `_id` 对齐) */
|
||||||
|
interface WordColoringWorksheetDefinition {
|
||||||
|
id: string;
|
||||||
|
templateType: TemplateType;
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
ageMin: number;
|
||||||
|
ageMax: number;
|
||||||
|
difficulty: 1 | 2 | 3 | 4;
|
||||||
|
tags: string[];
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WORD_COLORING_WORKSHEET_DEFINITIONS = [
|
||||||
|
{
|
||||||
|
id: 'word-coloring-grid',
|
||||||
|
templateType: 'grid' as TemplateType,
|
||||||
|
icon: 'grid',
|
||||||
|
title: '网格涂色',
|
||||||
|
subtitle: '田字格涂色识字练习',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['识字', '涂色', '田字格'],
|
||||||
|
sortOrder: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'word-coloring-find',
|
||||||
|
templateType: 'find' as TemplateType,
|
||||||
|
icon: 'search',
|
||||||
|
title: '找字涂色',
|
||||||
|
subtitle: '在字海中找到目标字并涂色',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 7,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['识字', '涂色', '找字'],
|
||||||
|
sortOrder: 31,
|
||||||
|
},
|
||||||
|
] as const satisfies ReadonlyArray<WordColoringWorksheetDefinition>;
|
||||||
|
|
||||||
|
type WordColoringWorksheetRow =
|
||||||
|
(typeof WORD_COLORING_WORKSHEET_DEFINITIONS)[number];
|
||||||
|
|
||||||
|
const WORD_COLORING_WORKSHEET_BY_ID = Object.fromEntries(
|
||||||
|
WORD_COLORING_WORKSHEET_DEFINITIONS.map((m) => [m.id, m]),
|
||||||
|
) as Record<string, WordColoringWorksheetRow>;
|
||||||
|
|
||||||
|
/** 页面渲染用:模式选择器列表 */
|
||||||
|
export const WORD_COLORING_MODE_OPTIONS = WORD_COLORING_WORKSHEET_DEFINITIONS;
|
||||||
|
|
||||||
|
/** 页面 pageInfoLookup 用 */
|
||||||
|
export function getModeInfo(id: string) {
|
||||||
|
const m = WORD_COLORING_WORKSHEET_BY_ID[id];
|
||||||
|
return m ? { title: m.title, desc: m.subtitle } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断 id 是否有效 */
|
||||||
|
export function isValidMode(id: string): boolean {
|
||||||
|
return id in WORD_COLORING_WORKSHEET_BY_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据 id 获取 templateType */
|
||||||
|
export function getTemplateType(id: string): TemplateType {
|
||||||
|
const m = WORD_COLORING_WORKSHEET_BY_ID[id];
|
||||||
|
return m?.templateType || 'grid';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发布用:从当前 mode 生成 DebugPublishMeta */
|
||||||
|
export function getPublishMetaByMode(id: string): DebugPublishMeta | null {
|
||||||
|
const m = WORD_COLORING_WORKSHEET_BY_ID[id];
|
||||||
|
if (!m) return null;
|
||||||
|
return {
|
||||||
|
id: m.id,
|
||||||
|
title: m.title,
|
||||||
|
subtitle: m.subtitle,
|
||||||
|
category: 'chinese',
|
||||||
|
subcategory: 'word-coloring',
|
||||||
|
path: `/chinesePages/wordColoring/wordColoring?id=${m.id}`,
|
||||||
|
ageMin: m.ageMin,
|
||||||
|
ageMax: m.ageMax,
|
||||||
|
grade: inferGradeFromAge(m.ageMin, m.ageMax),
|
||||||
|
difficulty: m.difficulty,
|
||||||
|
previewImg: '',
|
||||||
|
tags: [...m.tags],
|
||||||
|
isNew: false,
|
||||||
|
isHot: false,
|
||||||
|
sortOrder: m.sortOrder,
|
||||||
|
status: 'draft',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "涂色识字",
|
||||||
|
"navigationBarBackgroundColor": "#F8F0E0",
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"backgroundColor": "#FEF6E7",
|
||||||
|
"enablePullDownRefresh": false,
|
||||||
|
"usingComponents": {
|
||||||
|
"nav-bar": "../../components3.0/nav-bar/nav-bar",
|
||||||
|
"preview-card": "../../components3.0/preview-card/preview-card",
|
||||||
|
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
||||||
|
"debug-publish-tools": "../../components3.0/debug-publish-tools/debug-publish-tools",
|
||||||
|
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||||
|
"draw-ad": "../../components/draw-ad/draw-ad",
|
||||||
|
"toy-icon": "../../toy/icon/icon",
|
||||||
|
"toy-button": "../../toy/button-v2/button",
|
||||||
|
"word-input": "../../components/word-input/word-input",
|
||||||
|
"word-card": "../../components/word-card/word-card",
|
||||||
|
"color-picker": "../../components/color-picker/color-picker",
|
||||||
|
"word-picker": "../../components/word-picker/word-picker",
|
||||||
|
"empty-state": "../../components/empty-state/empty-state"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
@import '../../style/theme.less';
|
||||||
|
|
||||||
|
.wc-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: @bg-page;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-main {
|
||||||
|
padding: @page-padding-x;
|
||||||
|
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-section {
|
||||||
|
margin-top: @section-gap-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-section-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @text-secondary;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入行 */
|
||||||
|
.wc-input-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文字卡片网格 */
|
||||||
|
.wc-card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮行 */
|
||||||
|
.wc-action-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.wc-empty {
|
||||||
|
padding: 48rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模板选择网格(2列) */
|
||||||
|
.wc-mode-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-mode-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 32rpx 16rpx;
|
||||||
|
border-radius: @radius-lg;
|
||||||
|
background: @bg-header;
|
||||||
|
box-shadow: @shadow;
|
||||||
|
transition: transform 0.12s, box-shadow 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-mode-card--active {
|
||||||
|
background: @brand;
|
||||||
|
box-shadow: @shadow, 0 0 0 4rpx fade(@brand, 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-mode-card--pressed {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-mode-card__icon {
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-mode-card__label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @text-title;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-mode-card--active .wc-mode-card__label {
|
||||||
|
color: @text-selected-btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-mode-card__sub {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: @text-secondary;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-mode-card--active .wc-mode-card__sub {
|
||||||
|
color: fade(@text-selected-btn, 75%);
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import { WATER_COLORS } from '../../constants/colors';
|
||||||
|
import { WORDS } from '../../constants/words';
|
||||||
|
import { DrawServiceFactory, type IDrawService } from '../../service/drawServiceFactory';
|
||||||
|
import { createPage, type CanvasDataState } from '../../base/pageMixin';
|
||||||
|
import { defaultShareConfig } from '../../config/config';
|
||||||
|
import {
|
||||||
|
getModeInfo,
|
||||||
|
getPublishMetaByMode,
|
||||||
|
getTemplateType,
|
||||||
|
isValidMode,
|
||||||
|
WORD_COLORING_MODE_OPTIONS,
|
||||||
|
WORD_COLORING_WORKSHEET_DEFINITIONS,
|
||||||
|
} from './wordColoring.config';
|
||||||
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
import { addFavorite, removeFavorite, batchCheckFavorited } from '../../utils/favorites';
|
||||||
|
|
||||||
|
type CardItem = { color: string; word: string };
|
||||||
|
|
||||||
|
const COLOR_MAP: Record<string, string> = {
|
||||||
|
红: '#FF0000', 蓝: '#0000FF', 绿: '#00FF00', 黄: '#FFFF00',
|
||||||
|
黑: '#000000', 白: '#FFFFFF', 紫: '#800080', 橙: '#FF7F00',
|
||||||
|
粉: '#FF69B4', 棕: '#A52A2A', 灰: '#808080',
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageInfoLookup = getModeInfo;
|
||||||
|
|
||||||
|
// PLACEHOLDER_TS_PART2
|
||||||
|
|
||||||
|
type PageData = CanvasDataState & {
|
||||||
|
worksheetId: string;
|
||||||
|
cardList: CardItem[];
|
||||||
|
selectedWords: string[];
|
||||||
|
showColorPopup: boolean;
|
||||||
|
currentKey: number;
|
||||||
|
currentColor: string;
|
||||||
|
showSelectWordPopup: boolean;
|
||||||
|
modeOptions: typeof WORD_COLORING_MODE_OPTIONS;
|
||||||
|
isPreviewFavorite: boolean;
|
||||||
|
isDevEnv: boolean;
|
||||||
|
debugPublishVisible: boolean;
|
||||||
|
debugPublishLoading: boolean;
|
||||||
|
debugPublishMeta: DebugPublishMeta | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
createPage(
|
||||||
|
{
|
||||||
|
canvas: null as Canvas | null,
|
||||||
|
ctx: null as RenderingContext | null,
|
||||||
|
boxHeight: 0,
|
||||||
|
boxWidth: 0,
|
||||||
|
drawService: null as IDrawService | null,
|
||||||
|
_favoritedMap: {} as Record<string, boolean>,
|
||||||
|
|
||||||
|
data: {
|
||||||
|
pageTitle: '涂色识字',
|
||||||
|
functionId: '',
|
||||||
|
hasContent: false,
|
||||||
|
showShareDialog: false,
|
||||||
|
worksheetId: 'word-coloring-grid',
|
||||||
|
cardList: [] as CardItem[],
|
||||||
|
selectedWords: [] as string[],
|
||||||
|
showColorPopup: false,
|
||||||
|
currentKey: 0,
|
||||||
|
currentColor: '',
|
||||||
|
showSelectWordPopup: false,
|
||||||
|
modeOptions: WORD_COLORING_MODE_OPTIONS,
|
||||||
|
isPreviewFavorite: false,
|
||||||
|
isDevEnv: false,
|
||||||
|
debugPublishVisible: false,
|
||||||
|
debugPublishLoading: false,
|
||||||
|
debugPublishMeta: null,
|
||||||
|
} as unknown as PageData,
|
||||||
|
|
||||||
|
onLoad(options: { id?: string }) {
|
||||||
|
const worksheetId =
|
||||||
|
options.id && isValidMode(options.id)
|
||||||
|
? options.id
|
||||||
|
: 'word-coloring-grid';
|
||||||
|
|
||||||
|
this.syncDebugPublishEnv();
|
||||||
|
this.setData({
|
||||||
|
worksheetId,
|
||||||
|
functionId: worksheetId,
|
||||||
|
});
|
||||||
|
this.initPageInfo(worksheetId, '涂色识字');
|
||||||
|
this.loadFavoritedMap();
|
||||||
|
},
|
||||||
|
|
||||||
|
onCanvasReady(e: WechatMiniprogram.CustomEvent) {
|
||||||
|
const worksheetId = this.data.worksheetId;
|
||||||
|
const templateType = getTemplateType(worksheetId);
|
||||||
|
|
||||||
|
this.initCanvasFromComponent(e.detail, {
|
||||||
|
createDrawService: (
|
||||||
|
canvas: Canvas,
|
||||||
|
ctx: RenderingContext,
|
||||||
|
options?: Record<string, any>,
|
||||||
|
) => DrawServiceFactory.create(templateType, canvas, ctx, options),
|
||||||
|
drawServiceOptions: {
|
||||||
|
title: this.data.pageTitle,
|
||||||
|
},
|
||||||
|
onCanvasReady: () => {
|
||||||
|
if (this.data.cardList.length > 0) {
|
||||||
|
this.drawCanvas();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// PLACEHOLDER_TS_PART3
|
||||||
|
|
||||||
|
/** 执行 Canvas 绘制 */
|
||||||
|
async drawCanvas() {
|
||||||
|
if (!this.drawService) return;
|
||||||
|
const cardList = this.data.cardList;
|
||||||
|
if (cardList.length === 0) {
|
||||||
|
this.setData({ hasContent: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.drawService.draw(cardList);
|
||||||
|
this.setData({ hasContent: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('绘制失败:', e);
|
||||||
|
this.setData({ hasContent: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 切换模板类型 */
|
||||||
|
onSelectMode(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
const id = e.currentTarget.dataset.id as string;
|
||||||
|
if (!id || id === this.data.worksheetId || !isValidMode(id)) return;
|
||||||
|
|
||||||
|
const templateType = getTemplateType(id);
|
||||||
|
this.setData({
|
||||||
|
worksheetId: id,
|
||||||
|
functionId: id,
|
||||||
|
isPreviewFavorite: !!this._favoritedMap[id],
|
||||||
|
});
|
||||||
|
this.initPageInfo(id, '涂色识字');
|
||||||
|
|
||||||
|
if (this.canvas && this.ctx) {
|
||||||
|
this.drawService = DrawServiceFactory.create(
|
||||||
|
templateType,
|
||||||
|
this.canvas,
|
||||||
|
this.ctx,
|
||||||
|
{ title: this.data.pageTitle },
|
||||||
|
);
|
||||||
|
if (this.data.cardList.length > 0) {
|
||||||
|
this.drawCanvas();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** preview-card 换一批 */
|
||||||
|
onPreviewRefresh() {
|
||||||
|
this.refreshCardList();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 随机生成文字卡片 */
|
||||||
|
refreshCardList() {
|
||||||
|
const allWords = WORDS.slice(0, 2).reduce(
|
||||||
|
(acc, cat) => acc.concat(cat.words),
|
||||||
|
[] as string[],
|
||||||
|
);
|
||||||
|
const selected: string[] = [];
|
||||||
|
while (selected.length < 6 && allWords.length > 0) {
|
||||||
|
const idx = Math.floor(Math.random() * allWords.length);
|
||||||
|
const w = allWords[idx];
|
||||||
|
if (!selected.includes(w)) selected.push(w);
|
||||||
|
}
|
||||||
|
const colors = [...WATER_COLORS.basic12].sort(() => Math.random() - 0.5);
|
||||||
|
const cardList = selected.map((word, i) => ({
|
||||||
|
word,
|
||||||
|
color: COLOR_MAP[word] || colors[i % colors.length].hex,
|
||||||
|
}));
|
||||||
|
this.setData({ cardList, selectedWords: selected }, () => {
|
||||||
|
this.drawCanvas();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 清空文字卡片 */
|
||||||
|
clearCardList() {
|
||||||
|
this.setData({ cardList: [], selectedWords: [], hasContent: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
// PLACEHOLDER_TS_PART4
|
||||||
|
|
||||||
|
/** 输入确认:添加文字卡片 */
|
||||||
|
onConfirmInput(e: WechatMiniprogram.Input) {
|
||||||
|
const { cardList } = this.data;
|
||||||
|
const { value } = e.detail;
|
||||||
|
const characters = value.split('');
|
||||||
|
const remainingSlots = 6 - cardList.length;
|
||||||
|
if (remainingSlots <= 0) {
|
||||||
|
wx.showToast({ title: '最多只能添加6个字哦', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedColors = cardList.map((item) => item.color);
|
||||||
|
const usedWords = cardList.map((item) => item.word);
|
||||||
|
const allColors = WATER_COLORS.basic12.map((c) => c.hex);
|
||||||
|
const available = allColors.filter((c) => !usedColors.includes(c));
|
||||||
|
const shuffled = [...available].sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
const newChars: string[] = [];
|
||||||
|
for (const char of characters) {
|
||||||
|
if (newChars.length >= remainingSlots) break;
|
||||||
|
if (!usedWords.includes(char) && !newChars.includes(char)) {
|
||||||
|
newChars.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCards = newChars.map((char, i) => {
|
||||||
|
const mapped = COLOR_MAP[char];
|
||||||
|
const color =
|
||||||
|
mapped && !usedColors.includes(mapped)
|
||||||
|
? mapped
|
||||||
|
: shuffled[i % shuffled.length];
|
||||||
|
return { word: char, color };
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = [...cardList, ...newCards];
|
||||||
|
this.setData(
|
||||||
|
{ cardList: updated, selectedWords: updated.map((c) => c.word) },
|
||||||
|
() => this.drawCanvas(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 删除单个文字卡片 */
|
||||||
|
deleteWordCard(e: WechatMiniprogram.CustomEvent) {
|
||||||
|
const { key } = e.detail;
|
||||||
|
const updated = this.data.cardList.filter((_, i) => i !== key);
|
||||||
|
this.setData(
|
||||||
|
{ cardList: updated, selectedWords: updated.map((c) => c.word) },
|
||||||
|
() => this.drawCanvas(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 颜色选择相关 */
|
||||||
|
onColorTap(e: WechatMiniprogram.CustomEvent) {
|
||||||
|
const { key, color } = e.detail;
|
||||||
|
this.setData({ showColorPopup: true, currentKey: key, currentColor: color });
|
||||||
|
},
|
||||||
|
|
||||||
|
onCloseColorPopup() {
|
||||||
|
this.setData({ showColorPopup: false, currentKey: 0, currentColor: '' });
|
||||||
|
},
|
||||||
|
|
||||||
|
onChangeColor(e: WechatMiniprogram.CustomEvent) {
|
||||||
|
const { color } = e.detail;
|
||||||
|
const { currentKey, cardList } = this.data;
|
||||||
|
const updated = [...cardList];
|
||||||
|
updated[currentKey] = { ...updated[currentKey], color };
|
||||||
|
this.setData({ showColorPopup: false, cardList: updated }, () => {
|
||||||
|
this.drawCanvas();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 文字选择弹窗 */
|
||||||
|
openSelectWordPopup() {
|
||||||
|
this.setData({ showSelectWordPopup: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
closeSelectWordPopup() {
|
||||||
|
this.setData({ showSelectWordPopup: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
onChangeWord(e: WechatMiniprogram.CustomEvent) {
|
||||||
|
const selectedWords = e.detail.selectedWords as string[];
|
||||||
|
const colors = [...WATER_COLORS.basic12];
|
||||||
|
const cardList = selectedWords.map((word, i) => ({
|
||||||
|
word,
|
||||||
|
color: COLOR_MAP[word] || colors[i % colors.length].hex,
|
||||||
|
}));
|
||||||
|
this.setData({ showSelectWordPopup: false, cardList, selectedWords }, () => {
|
||||||
|
this.drawCanvas();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 收藏 */
|
||||||
|
async onPreviewFavorite() {
|
||||||
|
const next = !this.data.isPreviewFavorite;
|
||||||
|
this.setData({ isPreviewFavorite: next });
|
||||||
|
const id = this.data.worksheetId;
|
||||||
|
if (id) {
|
||||||
|
this._favoritedMap[id] = next;
|
||||||
|
if (next) addFavorite(id);
|
||||||
|
else removeFavorite(id);
|
||||||
|
}
|
||||||
|
wx.showToast({ title: next ? '收藏成功' : '已取消收藏', icon: 'none' });
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadFavoritedMap() {
|
||||||
|
const ids = WORD_COLORING_WORKSHEET_DEFINITIONS.map((d) => d.id);
|
||||||
|
this._favoritedMap = await batchCheckFavorited(ids);
|
||||||
|
if (this._favoritedMap[this.data.worksheetId]) {
|
||||||
|
this.setData({ isPreviewFavorite: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getPublishMeta(): DebugPublishMeta {
|
||||||
|
const meta = getPublishMetaByMode(this.data.worksheetId);
|
||||||
|
if (!meta) throw new Error('当前题型配置不存在');
|
||||||
|
return meta;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shareConfig: defaultShareConfig,
|
||||||
|
pageInfoLookup,
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<nav-bar title="涂色识字" />
|
||||||
|
|
||||||
|
<view class="wc-page">
|
||||||
|
<view class="wc-main">
|
||||||
|
<preview-card
|
||||||
|
id="previewCard"
|
||||||
|
showRefresh="{{true}}"
|
||||||
|
showFavorite="{{true}}"
|
||||||
|
favorited="{{isPreviewFavorite}}"
|
||||||
|
bind:canvas-ready="onCanvasReady"
|
||||||
|
bind:refresh="onPreviewRefresh"
|
||||||
|
bind:favorite="onPreviewFavorite" />
|
||||||
|
|
||||||
|
<!-- 文字输入区域 -->
|
||||||
|
<view class="wc-section">
|
||||||
|
<text class="wc-section-title">添加文字</text>
|
||||||
|
<view class="wc-input-row">
|
||||||
|
<word-input bind:onConfirm="onConfirmInput" width="380rpx" />
|
||||||
|
<toy-button
|
||||||
|
type="green"
|
||||||
|
size="small"
|
||||||
|
bind:click="openSelectWordPopup"
|
||||||
|
width="180rpx">
|
||||||
|
选择文字
|
||||||
|
</toy-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 文字卡片 -->
|
||||||
|
<view wx:if="{{cardList.length > 0}}" class="wc-section">
|
||||||
|
<view class="wc-card-grid">
|
||||||
|
<word-card
|
||||||
|
wx:for="{{cardList}}"
|
||||||
|
wx:key="index"
|
||||||
|
key="{{index}}"
|
||||||
|
word="{{item.word}}"
|
||||||
|
color="{{item.color}}"
|
||||||
|
bind:onDelete="deleteWordCard"
|
||||||
|
bind:onColorTap="onColorTap" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view wx:elif="{{!hasContent}}" class="wc-empty">
|
||||||
|
<empty-state
|
||||||
|
title="还没有添加文字"
|
||||||
|
desc="输入文字或点击选择文字,开始创建涂鸦卡"
|
||||||
|
hint="提示:最多可以同时添加 6 个文字" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<view class="wc-section">
|
||||||
|
<view class="wc-action-row">
|
||||||
|
<toy-button
|
||||||
|
type="primary"
|
||||||
|
bind:click="refreshCardList"
|
||||||
|
width="380rpx"
|
||||||
|
icon="refresh"
|
||||||
|
icon-class-prefix="toy-icon">
|
||||||
|
随机生成
|
||||||
|
</toy-button>
|
||||||
|
<toy-button
|
||||||
|
disabled="{{cardList.length <= 0}}"
|
||||||
|
type="white"
|
||||||
|
bind:click="clearCardList"
|
||||||
|
width="180rpx"
|
||||||
|
icon="clear"
|
||||||
|
icon-class-prefix="toy-icon">
|
||||||
|
清空
|
||||||
|
</toy-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 模板选择(2列卡片网格) -->
|
||||||
|
<view class="wc-section">
|
||||||
|
<text class="wc-section-title">选择模板</text>
|
||||||
|
<view class="wc-mode-grid">
|
||||||
|
<view
|
||||||
|
wx:for="{{modeOptions}}"
|
||||||
|
wx:key="id"
|
||||||
|
class="wc-mode-card {{worksheetId === item.id ? 'wc-mode-card--active' : ''}}"
|
||||||
|
data-id="{{item.id}}"
|
||||||
|
hover-class="wc-mode-card--pressed"
|
||||||
|
hover-start-time="0"
|
||||||
|
hover-stay-time="70"
|
||||||
|
bindtap="onSelectMode">
|
||||||
|
<toy-icon
|
||||||
|
name="{{item.icon}}"
|
||||||
|
size="42rpx"
|
||||||
|
color="{{worksheetId === item.id ? '#453900' : '#605b50'}}"
|
||||||
|
custom-class="wc-mode-card__icon" />
|
||||||
|
<text class="wc-mode-card__label">{{item.title}}</text>
|
||||||
|
<text class="wc-mode-card__sub">{{item.subtitle}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<draw-ad type="mathDraw"></draw-ad>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<preview-footer-actions
|
||||||
|
disabled="{{!hasContent}}"
|
||||||
|
bind:primary="exportToPrint"
|
||||||
|
bind:secondary="onShare" />
|
||||||
|
|
||||||
|
<debug-publish-tools
|
||||||
|
wx:if="{{isDevEnv && hasContent}}"
|
||||||
|
id="debugPublishTools"
|
||||||
|
visible="{{debugPublishVisible}}"
|
||||||
|
loading="{{debugPublishLoading}}"
|
||||||
|
meta="{{debugPublishMeta}}"
|
||||||
|
bind:open="onOpenDebugPublish"
|
||||||
|
bind:close="onCloseDebugPublish"
|
||||||
|
bind:confirm="onConfirmDebugPublish" />
|
||||||
|
|
||||||
|
<share-guide-popup
|
||||||
|
show="{{showShareDialog}}"
|
||||||
|
bind:onClose="onCloseShareDialog"
|
||||||
|
bind:onShareSuccess="onShareSuccess" />
|
||||||
|
|
||||||
|
<color-picker
|
||||||
|
show="{{showColorPopup}}"
|
||||||
|
currentColor="{{currentColor}}"
|
||||||
|
bind:onClose="onCloseColorPopup"
|
||||||
|
bind:onChange="onChangeColor" />
|
||||||
|
|
||||||
|
<word-picker
|
||||||
|
show="{{showSelectWordPopup}}"
|
||||||
|
cardList="{{cardList}}"
|
||||||
|
bind:onClose="closeSelectWordPopup"
|
||||||
|
bind:onChange="onChangeWord" />
|
||||||
@@ -70,9 +70,10 @@
|
|||||||
|
|
||||||
.cat-sidebar__item {
|
.cat-sidebar__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6rpx;
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
padding: 24rpx 12rpx;
|
padding: 24rpx 12rpx;
|
||||||
margin: 4rpx 12rpx;
|
margin: 4rpx 12rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cat-sidebar__icon {
|
.cat-sidebar__icon {
|
||||||
font-size: 36rpx;
|
font-size: 32rpx;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,35 +264,47 @@
|
|||||||
color: @text-gray;
|
color: @text-gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Skeleton ──
|
// ── Skeleton shimmer ──
|
||||||
@keyframes skeleton-pulse {
|
@skeleton-base: #e8e2d5;
|
||||||
0% {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
@keyframes skeleton-shimmer {
|
||||||
opacity: 0.3;
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 0.6;
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cat-card--skeleton {
|
.cat-card--skeleton {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
transparent 25%,
|
||||||
|
rgba(255, 255, 255, 0.7) 50%,
|
||||||
|
transparent 75%);
|
||||||
|
animation: skeleton-shimmer 1.2s linear infinite;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.cat-card__thumb {
|
.cat-card__thumb {
|
||||||
background: #e8e2d5;
|
background: @skeleton-base;
|
||||||
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cat-card__title,
|
.cat-card__title,
|
||||||
.cat-card__subtitle {
|
.cat-card__subtitle {
|
||||||
background: #e8e2d5;
|
background: @skeleton-base;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cat-card__title {
|
.cat-card__title {
|
||||||
|
|||||||
@@ -24,13 +24,18 @@ const DIFFICULTY_LABELS: Record<number, string> = {
|
|||||||
4: '挑战',
|
4: '挑战',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(
|
const DYNAMIC_TABS: CategoryTab[] = [
|
||||||
({ id, name, icon }) => ({
|
{ id: 'hot', name: '热门推荐', icon: '🔥' },
|
||||||
id,
|
{ id: 'top-downloads', name: '下载最多', icon: '📥' },
|
||||||
name,
|
];
|
||||||
icon,
|
|
||||||
}),
|
const SIDEBAR_TABS: CategoryTab[] = [
|
||||||
);
|
{ id: 'all', name: '全部', icon: '📋' },
|
||||||
|
...DYNAMIC_TABS,
|
||||||
|
...CATEGORY_LIST_WITH_ALL.filter((c) => c.id !== 'all').map(
|
||||||
|
({ id, name, icon }) => ({ id, name, icon }),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
const TAB_BAR_PATHS = new Set([
|
const TAB_BAR_PATHS = new Set([
|
||||||
'/pages/home/home',
|
'/pages/home/home',
|
||||||
@@ -119,9 +124,23 @@ function buildAllItems(): CategoryItem[] {
|
|||||||
return sortWorksheets(items);
|
return sortWorksheets(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DYNAMIC_LIMIT = 12;
|
||||||
|
|
||||||
|
function buildHotItems(): CategoryItem[] {
|
||||||
|
const all = buildAllItems();
|
||||||
|
return [...all].sort((a, b) => (b.likes || 0) - (a.likes || 0)).slice(0, DYNAMIC_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTopDownloadsItems(): CategoryItem[] {
|
||||||
|
const all = buildAllItems();
|
||||||
|
return [...all].sort((a, b) => (b.downloads || 0) - (a.downloads || 0)).slice(0, DYNAMIC_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
function getItemsByCategory(categoryId: string): CategoryItem[] {
|
function getItemsByCategory(categoryId: string): CategoryItem[] {
|
||||||
if (!_categoryData) return [];
|
if (!_categoryData) return [];
|
||||||
if (categoryId === 'all') return buildAllItems();
|
if (categoryId === 'all') return buildAllItems();
|
||||||
|
if (categoryId === 'hot') return buildHotItems();
|
||||||
|
if (categoryId === 'top-downloads') return buildTopDownloadsItems();
|
||||||
const group = _categoryData.categories.find((c) => c.id === categoryId);
|
const group = _categoryData.categories.find((c) => c.id === categoryId);
|
||||||
if (!group) return [];
|
if (!group) return [];
|
||||||
return group.items.map((item) => ({ ...item }));
|
return group.items.map((item) => ({ ...item }));
|
||||||
|
|||||||
@@ -37,7 +37,9 @@
|
|||||||
class="cat-sidebar__item {{activeCategoryId === item.id ? 'cat-sidebar__item--active' : ''}}"
|
class="cat-sidebar__item {{activeCategoryId === item.id ? 'cat-sidebar__item--active' : ''}}"
|
||||||
data-id="{{item.id}}"
|
data-id="{{item.id}}"
|
||||||
bindtap="onTapCategory">
|
bindtap="onTapCategory">
|
||||||
<!-- <text class="cat-sidebar__icon">{{item.icon}}</text> -->
|
<text wx:if="{{item.icon}}" class="cat-sidebar__icon"
|
||||||
|
>{{item.icon}}</text
|
||||||
|
>
|
||||||
<text class="cat-sidebar__name">{{item.name}}</text>
|
<text class="cat-sidebar__name">{{item.name}}</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Page({
|
|||||||
data: {
|
data: {
|
||||||
userName: '微信用户',
|
userName: '微信用户',
|
||||||
avatarUrl: '',
|
avatarUrl: '',
|
||||||
|
isDevEnv: false,
|
||||||
printCount: 0,
|
printCount: 0,
|
||||||
favoriteCount: 0,
|
favoriteCount: 0,
|
||||||
version: '3.0.0',
|
version: '3.0.0',
|
||||||
@@ -23,6 +24,11 @@ Page({
|
|||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
this.syncUserInfoFromApp();
|
this.syncUserInfoFromApp();
|
||||||
|
const isDevEnv =
|
||||||
|
wx.getAccountInfoSync().miniProgram.envVersion !== 'release';
|
||||||
|
this.setData({
|
||||||
|
isDevEnv,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
|
|||||||
+2
-5
@@ -4,10 +4,7 @@
|
|||||||
"compileType": "miniprogram",
|
"compileType": "miniprogram",
|
||||||
"cloudfunctionRoot": "cloudfunctions/",
|
"cloudfunctionRoot": "cloudfunctions/",
|
||||||
"setting": {
|
"setting": {
|
||||||
"useCompilerPlugins": [
|
"useCompilerPlugins": ["typescript", "less"],
|
||||||
"typescript",
|
|
||||||
"less"
|
|
||||||
],
|
|
||||||
"babelSetting": {
|
"babelSetting": {
|
||||||
"ignore": [],
|
"ignore": [],
|
||||||
"disablePlugins": [],
|
"disablePlugins": [],
|
||||||
@@ -67,4 +64,4 @@
|
|||||||
"cloudfunctionTemplateRoot": "cloudfunctionTemplate/",
|
"cloudfunctionTemplateRoot": "cloudfunctionTemplate/",
|
||||||
"appid": "wx4353d20418f2fa37",
|
"appid": "wx4353d20418f2fa37",
|
||||||
"projectname": "doodle-mini"
|
"projectname": "doodle-mini"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,19 @@
|
|||||||
"miniprogram": {
|
"miniprogram": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"name": "pages/profile/profile",
|
"name": "chinesePages/wordColoring/wordColoring",
|
||||||
"pathName": "pages/profile/profile",
|
"pathName": "chinesePages/wordColoring/wordColoring",
|
||||||
"query": "",
|
"query": "",
|
||||||
"scene": null,
|
"scene": null,
|
||||||
"launchMode": "default"
|
"launchMode": "default"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "pages/profile/profile",
|
||||||
|
"pathName": "pages/profile/profile",
|
||||||
|
"query": "",
|
||||||
|
"launchMode": "default",
|
||||||
|
"scene": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "supportPages/index/index",
|
"name": "supportPages/index/index",
|
||||||
"pathName": "supportPages/index/index",
|
"pathName": "supportPages/index/index",
|
||||||
|
|||||||
Reference in New Issue
Block a user