feat:修改练习字帖生产逻辑

This commit is contained in:
R524809
2025-10-29 17:45:12 +08:00
parent 643a7e23e7
commit c4f27f2125
13 changed files with 141224 additions and 60061 deletions
+56 -53
View File
@@ -1,15 +1,15 @@
/* Logo 字体 */ /* Logo 字体 */
@font-face { @font-face {
font-family: "iconfont logo"; font-family: 'iconfont logo';
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); /* src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); */
} }
.logo { .logo {
font-family: "iconfont logo"; font-family: 'iconfont logo';
font-size: 160px; font-size: 160px;
font-style: normal; font-style: normal;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@@ -48,7 +48,6 @@
color: #666; color: #666;
} }
#tabs .active { #tabs .active {
border-bottom-color: #f00; border-bottom-color: #f00;
color: #222; color: #222;
@@ -119,9 +118,15 @@
font-size: 42px; font-size: 42px;
margin: 10px auto; margin: 10px auto;
color: #333; color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear; -webkit-transition:
-moz-transition: font-size 0.25s linear, width 0.25s linear; font-size 0.25s linear,
transition: font-size 0.25s linear, width 0.25s linear; width 0.25s linear;
-moz-transition:
font-size 0.25s linear,
width 0.25s linear;
transition:
font-size 0.25s linear,
width 0.25s linear;
} }
.icon_lists .icon:hover { .icon_lists .icon:hover {
@@ -215,35 +220,35 @@
margin: 1em 0; margin: 1em 0;
} }
.markdown>p, .markdown > p,
.markdown>blockquote, .markdown > blockquote,
.markdown>.highlight, .markdown > .highlight,
.markdown>ol, .markdown > ol,
.markdown>ul { .markdown > ul {
width: 80%; width: 80%;
} }
.markdown ul>li { .markdown ul > li {
list-style: circle; list-style: circle;
} }
.markdown>ul li, .markdown > ul li,
.markdown blockquote ul>li { .markdown blockquote ul > li {
margin-left: 20px; margin-left: 20px;
padding-left: 4px; padding-left: 4px;
} }
.markdown>ul li p, .markdown > ul li p,
.markdown>ol li p { .markdown > ol li p {
margin: 0.6em 0; margin: 0.6em 0;
} }
.markdown ol>li { .markdown ol > li {
list-style: decimal; list-style: decimal;
} }
.markdown>ol li, .markdown > ol li,
.markdown blockquote ol>li { .markdown blockquote ol > li {
margin-left: 20px; margin-left: 20px;
padding-left: 4px; padding-left: 4px;
} }
@@ -260,7 +265,7 @@
font-weight: 600; font-weight: 600;
} }
.markdown>table { .markdown > table {
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0px; border-spacing: 0px;
empty-cells: show; empty-cells: show;
@@ -269,21 +274,21 @@
margin-bottom: 24px; margin-bottom: 24px;
} }
.markdown>table th { .markdown > table th {
white-space: nowrap; white-space: nowrap;
color: #333; color: #333;
font-weight: 600; font-weight: 600;
} }
.markdown>table th, .markdown > table th,
.markdown>table td { .markdown > table td {
border: 1px solid #e9e9e9; border: 1px solid #e9e9e9;
padding: 8px 16px; padding: 8px 16px;
text-align: left; text-align: left;
} }
.markdown>table th { .markdown > table th {
background: #F7F7F7; background: #f7f7f7;
} }
.markdown blockquote { .markdown blockquote {
@@ -318,12 +323,11 @@
display: inline-block; display: inline-block;
} }
.markdown>br, .markdown > br,
.markdown>p>br { .markdown > p > br {
clear: both; clear: both;
} }
.hljs { .hljs {
display: block; display: block;
background: white; background: white;
@@ -399,8 +403,8 @@ https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javasc
* Based on dabblet (http://dabblet.com) * Based on dabblet (http://dabblet.com)
* @author Lea Verou * @author Lea Verou
*/ */
code[class*="language-"], code[class*='language-'],
pre[class*="language-"] { pre[class*='language-'] {
color: black; color: black;
background: none; background: none;
text-shadow: 0 1px white; text-shadow: 0 1px white;
@@ -422,46 +426,45 @@ pre[class*="language-"] {
hyphens: none; hyphens: none;
} }
pre[class*="language-"]::-moz-selection, pre[class*='language-']::-moz-selection,
pre[class*="language-"] ::-moz-selection, pre[class*='language-'] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*='language-']::-moz-selection,
code[class*="language-"] ::-moz-selection { code[class*='language-'] ::-moz-selection {
text-shadow: none; text-shadow: none;
background: #b3d4fc; background: #b3d4fc;
} }
pre[class*="language-"]::selection, pre[class*='language-']::selection,
pre[class*="language-"] ::selection, pre[class*='language-'] ::selection,
code[class*="language-"]::selection, code[class*='language-']::selection,
code[class*="language-"] ::selection { code[class*='language-'] ::selection {
text-shadow: none; text-shadow: none;
background: #b3d4fc; background: #b3d4fc;
} }
@media print { @media print {
code[class*='language-'],
code[class*="language-"], pre[class*='language-'] {
pre[class*="language-"] {
text-shadow: none; text-shadow: none;
} }
} }
/* Code blocks */ /* Code blocks */
pre[class*="language-"] { pre[class*='language-'] {
padding: 1em; padding: 1em;
margin: .5em 0; margin: 0.5em 0;
overflow: auto; overflow: auto;
} }
:not(pre)>code[class*="language-"], :not(pre) > code[class*='language-'],
pre[class*="language-"] { pre[class*='language-'] {
background: #f5f2f0; background: #f5f2f0;
} }
/* Inline code */ /* Inline code */
:not(pre)>code[class*="language-"] { :not(pre) > code[class*='language-'] {
padding: .1em; padding: 0.1em;
border-radius: .3em; border-radius: 0.3em;
white-space: normal; white-space: normal;
} }
@@ -477,7 +480,7 @@ pre[class*="language-"] {
} }
.namespace { .namespace {
opacity: .7; opacity: 0.7;
} }
.token.property, .token.property,
@@ -505,7 +508,7 @@ pre[class*="language-"] {
.language-css .token.string, .language-css .token.string,
.style .token.string { .style .token.string {
color: #9a6e3a; color: #9a6e3a;
background: hsla(0, 0%, 100%, .5); background: hsla(0, 0%, 100%, 0.5);
} }
.token.atrule, .token.atrule,
@@ -516,7 +519,7 @@ pre[class*="language-"] {
.token.function, .token.function,
.token.class-name { .token.class-name {
color: #DD4A68; color: #dd4a68;
} }
.token.regex, .token.regex,
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1,4 +1,3 @@
words=
{ {
"两": [ "两": [
"M5.4 8.64 L7.02 10.26 L27 8.1 L47.52 6.48 L44.82 4.86 L42.12 5.4 L39.96 5.94 L27 7.02 Z", "M5.4 8.64 L7.02 10.26 L27 8.1 L47.52 6.48 L44.82 4.86 L42.12 5.4 L39.96 5.94 L27 7.02 Z",
File diff suppressed because it is too large Load Diff
+90 -92
View File
@@ -2,6 +2,7 @@ import WordDrawService from '../../service/wordDrawService';
import { checkAndSaveImage } from '../../utils/saveImage'; import { checkAndSaveImage } from '../../utils/saveImage';
import { PAPER_SIZE } from '../../constants/colors'; import { PAPER_SIZE } from '../../constants/colors';
import { getWordsSvgData } from '../../utils/getWordsSvgJson'; import { getWordsSvgData } from '../../utils/getWordsSvgJson';
import { CharacterItem } from '../../types/characterType';
Page({ Page({
canvas: null as Canvas | null, canvas: null as Canvas | null,
@@ -33,16 +34,18 @@ Page({
async onReady() { async onReady() {
await this.setCanvasBoxSize(); await this.setCanvasBoxSize();
this.initCanvas() await this.initCanvas()
}, },
async loadSvgWords() { async loadSvgWords() {
try { try {
wx.showToast({ title: '加载字体中...', icon: 'loading' }); wx.showToast({ title: '加载字体中...', icon: 'loading' });
console.log("loadSvgWords");
// 使用带缓存的SVG汉字数据获取函数 // 使用带缓存的SVG汉字数据获取函数
const svgWordsData = await getWordsSvgData(); const svgWordsData = await getWordsSvgData();
console.log("svgWordsData", svgWordsData);
this.svgWords = svgWordsData; this.svgWords = svgWordsData;
wx.hideToast(); wx.hideToast();
@@ -50,7 +53,7 @@ Page({
const { words } = this.data as any; const { words } = this.data as any;
if (words && words.length > 0) { if (words && words.length > 0) {
this.drawPracticeSheet().catch(console.error); this.renderPracticeContent().catch(console.error);
} }
} catch (error) { } catch (error) {
console.error('加载SVG汉字数据失败:', error); console.error('加载SVG汉字数据失败:', error);
@@ -86,73 +89,37 @@ Page({
const { words } = this.data as any; const { words } = this.data as any;
// 1. 先检查原来已有的汉字是否超过11个 // 1. 解析新输入的汉字
if (words.length >= 11) {
wx.showToast({
title: '最多只能添加 11 个字哦!',
icon: 'none',
duration: 2000,
});
return;
}
// 2. 解析新输入的汉字
const newChars = this.splitToSingleChars(text); const newChars = this.splitToSingleChars(text);
if (newChars.length === 0) { if (newChars.length === 0) {
wx.showToast({ title: '请输入有效汉字', icon: 'none' }); wx.showToast({ title: '请输入有效汉字', icon: 'none' });
return; return;
} }
// 3. 保留原有汉字,追加新汉字(去重处理) // 2. 检查是否超过限制
const existingWords = new Set(words); // 用于快速查找重复 if (words.length + newChars.length > 11) {
const updatedWords: string[] = [...words]; // 保留原有汉字 wx.showToast({
let addedCount = 0; title: `最多只能添加 11 个字,当前已有 ${words.length} 个,输入了 ${newChars.length}`,
let skippedCount = 0; icon: 'none',
duration: 3000,
for (const char of newChars) { });
// 检查是否已达到11个字的限制 return;
if (updatedWords.length >= 11) {
break;
} }
// 检查是否已存在(去重) // 3. 直接追加新汉字
if (existingWords.has(char)) { const updatedWords = [...words, ...newChars];
skippedCount++;
continue;
}
// 添加新汉字
updatedWords.push(char);
existingWords.add(char);
addedCount++;
}
// 4. 更新数据并提示 // 4. 更新数据并提示
this.setData({ words: updatedWords }, () => { this.setData({ words: updatedWords }, () => {
this.drawPracticeSheet().catch(console.error); this.renderPracticeContent().catch(console.error);
}); });
// 5. 显示添加结果提示 // 5. 显示添加结果提示
if (addedCount > 0) {
let message = `成功添加 ${addedCount} 个汉字`;
if (skippedCount > 0) {
message += `,跳过 ${skippedCount} 个重复汉字`;
}
if (updatedWords.length >= 11) {
message += ',已达到最大限制';
}
wx.showToast({ wx.showToast({
title: message, title: `成功添加 ${newChars.length} 个汉字`,
icon: 'none', icon: 'none',
duration: 2000, duration: 2000,
}); });
} else if (skippedCount > 0) {
wx.showToast({
title: `所有汉字都已存在,跳过 ${skippedCount} 个重复汉字`,
icon: 'none',
duration: 2000,
});
}
}, },
// 选择面板 // 选择面板
@@ -165,7 +132,11 @@ Page({
onChangeWord(e: any) { onChangeWord(e: any) {
const selectedWords = e.detail.selectedWords as string[]; const selectedWords = e.detail.selectedWords as string[];
const newWords = [...this.data.words, ...selectedWords]; const newWords = [...this.data.words, ...selectedWords];
this.setData({ words: newWords, showSelectWordPopup: false }, () => this.drawPracticeSheet().catch(console.error)); this.setData({ words: newWords, showSelectWordPopup: false }, () => {
if (newWords && newWords.length > 0) {
this.renderPracticeContent().catch(console.error);
}
});
}, },
// 删除 // 删除
@@ -173,8 +144,7 @@ Page({
const { key } = e.detail; const { key } = e.detail;
const { words } = this.data as any; const { words } = this.data as any;
const next = words.filter((_: string, index: number) => index !== key); const next = words.filter((_: string, index: number) => index !== key);
console.log("-nextnext-----:", next); this.setData({ words: next }, () => this.renderPracticeContent().catch(console.error));
this.setData({ words: next }, () => this.drawPracticeSheet().catch(console.error));
}, },
// 校验 & 分割 // 校验 & 分割
@@ -188,6 +158,7 @@ Page({
return matches || []; return matches || [];
}, },
setCanvasBoxSize() { setCanvasBoxSize() {
const query = wx.createSelectorQuery(); const query = wx.createSelectorQuery();
query query
@@ -202,7 +173,8 @@ Page({
}) })
.exec(); .exec();
}, },
initCanvas() { async initCanvas() {
return new Promise<void>((resolve) => {
// 然后初始化canvas // 然后初始化canvas
wx.createSelectorQuery() wx.createSelectorQuery()
.select('#canvasContent') .select('#canvasContent')
@@ -210,7 +182,7 @@ Page({
node: true, node: true,
size: true, size: true,
}) })
.exec((res) => { .exec(async (res) => {
if (res[0] && res[0].node) { if (res[0] && res[0].node) {
const canvas = res[0].node; const canvas = res[0].node;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@@ -223,65 +195,91 @@ Page({
title: '田字格 练 字 贴', title: '田字格 练 字 贴',
subTitle: '按笔画临摹练习' subTitle: '按笔画临摹练习'
}); });
this.drawPracticeSheet().catch(console.error);
// 初始化时只绘制页眉和布局
await this.wordDrawService.drawLayout();
// 如果有汉字数据,绘制练字内容
const { words } = this.data as any;
if (words && words.length > 0) {
await this.renderPracticeContent();
}
resolve();
} }
} }
}); });
});
}, },
// 绘制练字 // 绘制练字内容(只绘制田字格和汉字内容)
async drawPracticeSheet() { async renderPracticeContent() {
if (!this.canvas || !this.wordDrawService) { if (!this.canvas || !this.wordDrawService) {
this.initCanvas(); await this.initCanvas();
return;
} }
const { words } = this.data as any; const { words } = this.data as any;
// 构建汉字笔画数据 console.log("renderPracticeContent words", words);
const wordsMap: Record<string, string[]> | null = this.checkWords(words);
await this.wordDrawService?.draw(wordsMap || {}); if (!words || words.length === 0) {
this.wordDrawService.drawContentEmpty();
return;
}
// 检查汉字是否支持
const supportedWords = this.getSupportedWords(words);
if (supportedWords.length === 0) {
wx.showToast({ title: '暂不支持这些汉字', icon: 'none' });
this.wordDrawService.drawContentEmpty();
return;
}
// 处理布局和生成练字数据
const characterData = this.processCharacterLayout(supportedWords);
this.wordDrawService.drawPracticeContent(characterData);
}, },
checkWords(words: string[]): Record<string, string[]> | null {
// 构建汉字笔画数据(已去重) /** 检查汉字是否支持,返回支持的汉字列表 */
const wordsMap: Record<string, string[]> = {}; getSupportedWords(words: string[]): string[] {
const supportedWords: string[] = []; return words.filter(word => this.svgWords[word]);
},
/** 处理汉字布局,生成练字数据 */
processCharacterLayout(words: string[]): CharacterItem[] {
const maxRow = 11, maxCol = 9;
let rowIndex = 0;
const characterData: CharacterItem[] = [];
const boundaryWords: string[] = [];
// 按顺序处理每个汉字 // 按顺序处理每个汉字
words.forEach((word: string) => { words.forEach((word: string) => {
if (this.svgWords[word]) { const strokes = this.svgWords[word];
wordsMap[word] = this.svgWords[word];
supportedWords.push(word);
}
});
if (supportedWords.length === 0) {
wx.showToast({ title: '暂不支持这些汉字', icon: 'none' });
return null;
}
const maxRow = 11, maxCol = 9;
let rowIndex = 0;
const characters = Object.keys(wordsMap);
const newWorksMap: Record<string, string[]> = {};
const boundaryWords: string[] = []
characters.forEach((char) => {
const strokes = wordsMap[char] || [];
const strokeCount = strokes.length; const strokeCount = strokes.length;
const totalCells = 1 + strokeCount + 2; const totalCells = 1 + strokeCount + 2; // 预览格 + 练习格 + 空白格
const totalRows = Math.ceil(totalCells / maxCol); const totalRows = Math.ceil(totalCells / maxCol);
rowIndex += totalRows; rowIndex += totalRows;
if (rowIndex <= maxRow) { if (rowIndex <= maxRow) {
newWorksMap[char] = wordsMap[char]; characterData.push({ character: word, strokes });
} else { } else {
boundaryWords.push(char) boundaryWords.push(word);
} }
}); });
// 提示超出页面的汉字
if (boundaryWords.length > 0) { if (boundaryWords.length > 0) {
wx.showToast({ title: `练字贴已超过一页,部分汉字未包含在本次生成结果中`, icon: 'none' }); wx.showToast({
title: `练字贴已超过一页,部分汉字未包含在本次生成结果中`,
icon: 'none'
});
} }
return newWorksMap;
return characterData;
}, },
// 下载打印练字贴 // 下载打印练字贴
exportToPrint() { exportToPrint() {
const { words } = this.data as any; const { words } = this.data as any;
+1
View File
@@ -20,6 +20,7 @@
wx:for-item="word" wx:for-item="word"
wx:for-index="index" wx:for-index="index"
word="{{word}}" word="{{word}}"
key="{{index}}"
bind:onDelete="deleteWordCard" bind:onDelete="deleteWordCard"
bind:onColorTap="onColorTap" /> bind:onColorTap="onColorTap" />
</view> </view>
+173
View File
@@ -120,6 +120,179 @@ page {
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
} }
// 模板选择区域
.template-section {
margin-top: 40rpx;
.template-title {
font-size: 28rpx;
font-weight: 600;
color: #141414;
margin-bottom: 24rpx;
text-align: center;
}
.template-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.template-item {
background: #FFFFFF;
border: 2rpx solid #E8E8E8;
border-radius: 16rpx;
padding: 16rpx;
cursor: pointer;
transition: all 0.2s ease-in-out;
position: relative;
&:active {
transform: scale(0.95);
}
&.active {
border-color: #93D333;
box-shadow: 0 0 0 4rpx rgba(147, 211, 51, 0.2);
&::after {
content: '✓';
position: absolute;
top: 12rpx;
right: 12rpx;
width: 24rpx;
height: 24rpx;
background: #93D333;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #FFFFFF;
font-size: 14rpx;
font-weight: bold;
}
}
.template-preview {
width: 100%;
height: 80rpx;
background: #F8F8F8;
border-radius: 12rpx;
margin-bottom: 12rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
.template-preview-content {
display: flex;
align-items: center;
justify-content: center;
gap: 6rpx;
width: 100%;
height: 100%;
.preview-word {
font-size: 20rpx;
font-weight: 600;
width: 24rpx;
height: 24rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6rpx;
background: rgba(255, 255, 255, 0.8);
}
// 不同模板的布局样式
&.grid-template {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4rpx;
}
&.circle-template {
position: relative;
.preview-word {
position: absolute;
&:nth-child(1) {
top: 12rpx;
left: 50%;
transform: translateX(-50%);
}
&:nth-child(2) {
right: 12rpx;
top: 50%;
transform: translateY(-50%);
}
&:nth-child(3) {
bottom: 12rpx;
left: 50%;
transform: translateX(-50%);
}
&:nth-child(4) {
left: 12rpx;
top: 50%;
transform: translateY(-50%);
}
}
}
&.random-template {
position: relative;
.preview-word {
position: absolute;
&:nth-child(1) {
top: 10rpx;
left: 16rpx;
}
&:nth-child(2) {
top: 24rpx;
right: 10rpx;
}
&:nth-child(3) {
bottom: 18rpx;
left: 10rpx;
}
&:nth-child(4) {
bottom: 10rpx;
right: 18rpx;
}
}
}
&.line-template {
flex-direction: row;
gap: 8rpx;
}
}
}
.template-name {
font-size: 22rpx;
color: #666666;
text-align: center;
font-weight: 500;
.template-item.active & {
color: #93D333;
font-weight: 600;
}
}
}
}
} }
+17
View File
@@ -18,6 +18,7 @@ Page({
currentColor: '', currentColor: '',
showIntroductionPopup: false, showIntroductionPopup: false,
showSelectWordPopup: false, // 选择文字弹窗 showSelectWordPopup: false, // 选择文字弹窗
selectedTemplate: 'grid', // 选中的模板类型
env: 'release', env: 'release',
isPCDevtool: false, isPCDevtool: false,
}, },
@@ -375,4 +376,20 @@ Page({
url: '/pages/debug/debug', url: '/pages/debug/debug',
}); });
}, },
// 模板选择事件
onSelectTemplate(e: WechatMiniprogram.TouchEvent) {
const { template } = e.currentTarget.dataset;
this.setData({
selectedTemplate: template
});
// 这里后续可以添加根据模板类型更新canvas绘制效果的逻辑
console.log('选择的模板类型:', template);
// 如果有文字卡片,可以重新绘制canvas
if (this.data.cardList.length > 0) {
this.drawCanvas();
}
},
}); });
+95
View File
@@ -59,6 +59,101 @@
清空 清空
</toy-button> </toy-button>
</view> </view>
<!-- 趣味模板选择区域 -->
<view class="template-section">
<view class="template-title">选择涂色模板</view>
<view class="template-grid">
<view
class="template-item {{selectedTemplate === 'grid' ? 'active' : ''}}"
data-template="grid"
bind:tap="onSelectTemplate">
<view class="template-preview">
<view class="template-preview-content grid-template">
<view class="preview-word" style="color: #ffd719"
>王</view
>
<view class="preview-word" style="color: #93d333"
>手</view
>
<view class="preview-word" style="color: #ff6b9d"
>哭</view
>
<view class="preview-word" style="color: #4ecdc4"
>少</view
>
</view>
</view>
<view class="template-name">网格排列</view>
</view>
<view
class="template-item {{selectedTemplate === 'circle' ? 'active' : ''}}"
data-template="circle"
bind:tap="onSelectTemplate">
<view class="template-preview">
<view class="template-preview-content circle-template">
<view class="preview-word" style="color: #ffd719"
>王</view
>
<view class="preview-word" style="color: #93d333"
>手</view
>
<view class="preview-word" style="color: #ff6b9d"
>哭</view
>
<view class="preview-word" style="color: #4ecdc4"
>少</view
>
</view>
</view>
<view class="template-name">圆形排列</view>
</view>
<view
class="template-item {{selectedTemplate === 'random' ? 'active' : ''}}"
data-template="random"
bind:tap="onSelectTemplate">
<view class="template-preview">
<view class="template-preview-content random-template">
<view class="preview-word" style="color: #ffd719"
>王</view
>
<view class="preview-word" style="color: #93d333"
>手</view
>
<view class="preview-word" style="color: #ff6b9d"
>哭</view
>
<view class="preview-word" style="color: #4ecdc4"
>少</view
>
</view>
</view>
<view class="template-name">随机分布</view>
</view>
<view
class="template-item {{selectedTemplate === 'line' ? 'active' : ''}}"
data-template="line"
bind:tap="onSelectTemplate">
<view class="template-preview">
<view class="template-preview-content line-template">
<view class="preview-word" style="color: #ffd719"
>王</view
>
<view class="preview-word" style="color: #93d333"
>手</view
>
<view class="preview-word" style="color: #ff6b9d"
>哭</view
>
<view class="preview-word" style="color: #4ecdc4"
>少</view
>
</view>
</view>
<view class="template-name">直线排列</view>
</view>
</view>
</view>
</view> </view>
<view id="previewWrapper" class="wrapper" wx:if="{{cardList.length > 0}}"> <view id="previewWrapper" class="wrapper" wx:if="{{cardList.length > 0}}">
<text class="wrapper-title">预览打印效果</text> <text class="wrapper-title">预览打印效果</text>
+43 -21
View File
@@ -1,5 +1,6 @@
import { PAPER_SIZE } from '../constants/colors'; import { PAPER_SIZE } from '../constants/colors';
import { getMiniCodeImage, getImage } from '../utils/index'; import { getMiniCodeImage, getImage } from '../utils/index';
import { CharacterItem } from '../types/characterType';
/** /**
* 仅支持 M/L/Z 的简单 SVG 路径解析与绘制 * 仅支持 M/L/Z 的简单 SVG 路径解析与绘制
@@ -246,10 +247,9 @@ class WordDrawService {
} }
/** /**
* 生成练字帖 * 绘制页眉和基础布局
* @param wordsMap 形如 { "其": ["M.. L.. Z", ...], ... },不超过 10 个汉字
*/ */
async draw(wordsMap: Record<string, string[]>) { async drawLayout() {
this.setPrintConfig(); this.setPrintConfig();
this.clear(); this.clear();
this.setPaper(); this.setPaper();
@@ -260,9 +260,35 @@ class WordDrawService {
} else { } else {
await this.drawMiniHeader(); await this.drawMiniHeader();
} }
}
// 页眉绘制完成后,再绘制内容 async drawContentEmpty() {
this.drawContent(wordsMap); this.clearContentArea();
this.drawContent(null);
}
/**
* 绘制田字格和练字内容
* @param wordsMap 形如 { "其": ["M.. L.. Z", ...], ... },不超过 10 个汉字
*/
drawPracticeContent(characters: CharacterItem[]) {
// 清空内容区域(页眉以下的部分)
this.clearContentArea();
// 绘制田字格和练字内容
this.drawContent(characters);
}
/**
* 清空内容区域(页眉以下的部分)
*/
private clearContentArea() {
const { ctx, canvas } = this;
const contentStartY = this.currentY;
// 清空页眉以下的所有内容
ctx.fillStyle = '#fff';
ctx.fillRect(0, contentStartY, canvas.width, canvas.height - contentStartY);
} }
async drawHeader() { async drawHeader() {
@@ -353,10 +379,8 @@ class WordDrawService {
* rowGap 作用解释: * rowGap 作用解释:
* rowGap(行间距)用于田字格竖直方向(每一行田字格之间的间隔)的初始最小值。它防止行与行的田字格上下贴得太紧,增加一定的可视呼吸空间。之后同样会结合剩余空间动态调整为实际间距 actualRowGap。 * rowGap(行间距)用于田字格竖直方向(每一行田字格之间的间隔)的初始最小值。它防止行与行的田字格上下贴得太紧,增加一定的可视呼吸空间。之后同样会结合剩余空间动态调整为实际间距 actualRowGap。
*/ */
drawContent(wordsMap: Record<string, string[]>) { drawContent(characterData: CharacterItem[] | null) {
const { canvas, ctx } = this; const { canvas, ctx } = this;
const characters = Object.keys(wordsMap).slice(0, 10);
// 布局参数 // 布局参数
const topGap = 50; // 与页眉分割线的距离 const topGap = 50; // 与页眉分割线的距离
@@ -398,11 +422,11 @@ class WordDrawService {
columnNumber columnNumber
}); });
if (characters.length > 0) { if (characterData && characterData.length > 0) {
// 第二阶段:绘制练字内容 // 第二阶段:绘制练字内容
this.drawPracticeContent({ this.drawPracticeContentInternal({
ctx, ctx,
wordsMap, characterData,
startX: leftMargin, startX: leftMargin,
startY: contentTop, startY: contentTop,
cellSize, cellSize,
@@ -464,9 +488,9 @@ class WordDrawService {
/** /**
* 绘制练字内容(参考 template.html 的实现) * 绘制练字内容(参考 template.html 的实现)
*/ */
drawPracticeContent({ drawPracticeContentInternal({
ctx, ctx,
wordsMap, characterData,
startX, startX,
startY, startY,
cellSize, cellSize,
@@ -475,7 +499,7 @@ class WordDrawService {
columnNumber columnNumber
}: { }: {
ctx: RenderingContext; ctx: RenderingContext;
wordsMap: Record<string, string[]>; characterData: CharacterItem[];
startX: number; startX: number;
startY: number; startY: number;
cellSize: number; cellSize: number;
@@ -483,19 +507,17 @@ class WordDrawService {
rowGap: number; rowGap: number;
columnNumber: number; columnNumber: number;
}) { }) {
const characters = Object.keys(wordsMap);
const reservedCells = 2; const reservedCells = 2;
let rowIndex = 0, columnIndex = 0; let rowIndex = 0, columnIndex = 0;
// console.log('开始绘制练字内容,汉字数量:', characters.length); // console.log('开始绘制练字内容,汉字数量:', characters.length);
characters.forEach((uniqueKey, charIndex) => { characterData.forEach((item, index) => {
const strokes = wordsMap[uniqueKey] || []; const character = item.character;
const strokes = item.strokes;
const strokeCount = strokes.length; const strokeCount = strokes.length;
// 从uniqueKey中提取原始汉字(去掉后缀)
const originalChar = uniqueKey.split('_')[0];
console.log(`绘制第 ${charIndex + 1} 个汉字 "${originalChar}",笔画数: ${strokeCount}`); console.log(`绘制第 ${index + 1} 个汉字 "${character}",笔画数: ${strokeCount}`);
// 1. 预览格:显示完整汉字(黑色) // 1. 预览格:显示完整汉字(黑色)
this.drawPreviewCell({ this.drawPreviewCell({
@@ -538,7 +560,7 @@ class WordDrawService {
} }
// 4. 强制下一个汉字换行到新行的第一个田字格 // 4. 强制下一个汉字换行到新行的第一个田字格
if (charIndex < characters.length - 1) { // 不是最后一个汉字 if (index < characterData.length - 1) { // 不是最后一个汉字
rowIndex = rowIndex + 1; rowIndex = rowIndex + 1;
columnIndex = 0; columnIndex = 0;
// console.log(`汉字 "${char}" 绘制完成,强制换行到新行,下一个汉字从 rowIndex: ${rowIndex}, columnIndex: ${columnIndex} 开始`); // console.log(`汉字 "${char}" 绘制完成,强制换行到新行,下一个汉字从 rowIndex: ${rowIndex}, columnIndex: ${columnIndex} 开始`);
+4
View File
@@ -0,0 +1,4 @@
export interface CharacterItem {
character: string;
strokes: string[];
}
+2 -1
View File
@@ -6,7 +6,8 @@ import { getJson } from './http';
const CACHE_CONFIG = { const CACHE_CONFIG = {
key: 'svgWordsData', // 缓存键名 key: 'svgWordsData', // 缓存键名
expireDays: 7, // 缓存过期天数 expireDays: 7, // 缓存过期天数
url: 'https://www.ice-sea.com/doodle/words-svg/words-svg-3000.json' // 远程数据地址 // url: 'https://cdn.joeyone.cn/words-svg-all.json' // 远程数据地址
url: 'https://cdn.joeyone.cn/words-svg-3000.json' // 远程数据地址
}; };
/** /**