feat: 完成绘制汉字基础功能
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"navigationBarTitleText": "练字",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "/ui/button/button",
|
||||
"van-icon": "@vant/weapp/icon/index",
|
||||
@@ -6,6 +6,10 @@ page {
|
||||
background-color: #f6f6f6;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.empty {
|
||||
height: 120rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@@ -144,4 +148,38 @@ page {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx dashed #dee2e6;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.canvas-content {
|
||||
max-width: 100%;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bottom-btn-box {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px -3.9rpx 5.2rpx rgba(0, 0, 0, 0.15);
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import WordDrawService from '../../service/wordDrawService';
|
||||
import { checkAndSaveImage } from '../../utils/saveImage';
|
||||
import { PAPER_SIZE } from '../../constants/colors';
|
||||
|
||||
Page({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
wordDrawService: null as WordDrawService | null,
|
||||
svgWords: {} as Record<string, string[]>,
|
||||
|
||||
data: {
|
||||
// 选中的汉字列表
|
||||
words: ['赢', '张', '政'] as string[],
|
||||
showSelectWordPopup: false,
|
||||
showColorPopup: false,
|
||||
currentKey: 0,
|
||||
currentColor: '',
|
||||
|
||||
// 田字格配置
|
||||
// rows: 10,
|
||||
// cols: 12,
|
||||
// rowsArray: [] as number[],
|
||||
// colsArray: [] as number[],
|
||||
boxWidth: 0,
|
||||
boxHeight: 0,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// this.initGrid();
|
||||
this.loadSvgWords();
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.setCanvasBoxSize();
|
||||
},
|
||||
|
||||
loadSvgWords() {
|
||||
const fs = wx.getFileSystemManager();
|
||||
fs.readFile({
|
||||
filePath: 'constants/svgWords.json', // JSON文件路径,相对于项目根目录
|
||||
encoding: 'utf8', // 重要:指定编码为utf8以读取文本内容
|
||||
success: (res) => {
|
||||
try {
|
||||
wx.showToast({ title: '加载中...', icon: 'loading' });
|
||||
const parsedData = JSON.parse(res.data as string);
|
||||
this.svgWords = parsedData;
|
||||
console.log('this.svgWords.length----:', Object.keys(this.svgWords).length)
|
||||
wx.hideToast();
|
||||
console.log('SVG words loaded successfully');
|
||||
const { words } = this.data as any;
|
||||
if (words && words.length > 0) {
|
||||
this.drawPracticeSheet().catch(console.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析JSON失败', e);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('读取文件失败', err);
|
||||
wx.hideToast();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 输入组件回调
|
||||
onConfirmInput(e: any) {
|
||||
const value: string = (e && e.detail && e.detail.value) || '';
|
||||
const text = (value || '').trim();
|
||||
if (!text) return;
|
||||
|
||||
if (!this.isChineseText(text)) {
|
||||
wx.showToast({ title: '请输入汉字', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const chars = this.splitToSingleChars(text);
|
||||
const { words } = this.data as any;
|
||||
const exists = new Set(words);
|
||||
const newWords = [...words];
|
||||
chars.forEach((ch) => {
|
||||
if (!exists.has(ch)) {
|
||||
newWords.push(ch);
|
||||
}
|
||||
});
|
||||
this.setData({ words: newWords }, () => this.drawPracticeSheet().catch(console.error));
|
||||
},
|
||||
|
||||
// 选择面板
|
||||
openSelectWordPopup() {
|
||||
this.setData({ showSelectWordPopup: true });
|
||||
},
|
||||
closeSelectWordPopup() {
|
||||
this.setData({ showSelectWordPopup: false });
|
||||
},
|
||||
onChangeWord(e: any) {
|
||||
const selectedWords = e.detail.selectedWords as string[];
|
||||
const newWords = [...this.data.words, ...selectedWords];
|
||||
this.setData({ words: newWords, showSelectWordPopup: false }, () => this.drawPracticeSheet().catch(console.error));
|
||||
},
|
||||
|
||||
// 删除与颜色
|
||||
deleteWordCard(e: any) {
|
||||
const { word } = e.detail;
|
||||
const { words } = this.data as any;
|
||||
const next = words.filter((w: string) => w !== word);
|
||||
this.setData({ words: next }, () => this.drawPracticeSheet().catch(console.error));
|
||||
},
|
||||
|
||||
// 校验 & 分割
|
||||
isChineseText(text: string): boolean {
|
||||
const chineseRegex = /^[\u4e00-\u9fff]+$/;
|
||||
return chineseRegex.test(text);
|
||||
},
|
||||
splitToSingleChars(text: string): string[] {
|
||||
const chineseRegex = /[\u4e00-\u9fff]/g;
|
||||
const matches = text.match(chineseRegex);
|
||||
return matches || [];
|
||||
},
|
||||
|
||||
// 田字格
|
||||
/* initGrid() {
|
||||
const { rows, cols } = this.data as any;
|
||||
const rowsArray = Array.from({ length: rows }, (_, i) => i);
|
||||
const colsArray = Array.from({ length: cols }, (_, i) => i);
|
||||
console.log('rowsArray----:', rowsArray)
|
||||
console.log('colsArray----:', colsArray)
|
||||
this.setData({ rowsArray, colsArray });
|
||||
}, */
|
||||
|
||||
setCanvasBoxSize() {
|
||||
const query = wx.createSelectorQuery();
|
||||
query
|
||||
.select('#canvasWrapper')
|
||||
.boundingClientRect((rect) => {
|
||||
if (rect) {
|
||||
const { width, height } = PAPER_SIZE['A4'];
|
||||
const boxWidth = rect.width;
|
||||
const boxHeight = boxWidth / (width / height);
|
||||
this.setData({ boxWidth, boxHeight });
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
},
|
||||
initCanvas() {
|
||||
// 然后初始化canvas
|
||||
wx.createSelectorQuery()
|
||||
.select('#canvasContent')
|
||||
.fields({
|
||||
node: true,
|
||||
size: true,
|
||||
})
|
||||
.exec((res) => {
|
||||
if (res[0] && res[0].node) {
|
||||
const canvas = res[0].node;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = ctx;
|
||||
this.wordDrawService = new WordDrawService(canvas, ctx, {
|
||||
appName: '涂鸦丫小程序',
|
||||
appHint: '练字|识字|打印',
|
||||
title: '田字格 练 字 贴',
|
||||
subTitle: '按笔画临摹练习'
|
||||
});
|
||||
this.drawPracticeSheet().catch(console.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 绘制练字贴
|
||||
async drawPracticeSheet() {
|
||||
if (!this.canvas || !this.wordDrawService) {
|
||||
this.initCanvas();
|
||||
}
|
||||
const { words } = this.data as any;
|
||||
if (!words || words.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建汉字笔画数据
|
||||
const wordsMap: Record<string, string[]> = {};
|
||||
console.log('this.svgWords----:', this.svgWords)
|
||||
words.forEach((word: string) => {
|
||||
if (this.svgWords[word]) {
|
||||
wordsMap[word] = this.svgWords[word];
|
||||
}
|
||||
});
|
||||
if (Object.keys(wordsMap).length === 0) {
|
||||
wx.showToast({ title: '暂不支持这些汉字', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.wordDrawService?.draw(wordsMap);
|
||||
},
|
||||
|
||||
// 下载打印练字贴
|
||||
exportToPrint() {
|
||||
const { words } = this.data as any;
|
||||
if (!this.canvas || words.length <= 0) {
|
||||
wx.showToast({ title: '请先生成练字贴', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
checkAndSaveImage(this.canvas);
|
||||
},
|
||||
|
||||
// 分享功能
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '涂鸦丫-练字|识字|打印',
|
||||
path: '/pages/copyBook/index',
|
||||
};
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '涂鸦丫-练字|识字|打印',
|
||||
query: '/pages/copyBook/index',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
</view>
|
||||
|
||||
<view class="word-card-box">
|
||||
<view class="word-cards-grid" wx:if="{{cardList.length > 0}}">
|
||||
<view class="word-cards-grid" wx:if="{{words.length > 0}}">
|
||||
<word-card
|
||||
disabledColor="{{true}}"
|
||||
wx:key="index"
|
||||
wx:for="{{cardList}}"
|
||||
wx:for-item="item"
|
||||
wx:for="{{words}}"
|
||||
wx:for-item="word"
|
||||
wx:for-index="index"
|
||||
word="{{item.word}}"
|
||||
color="{{item.color}}"
|
||||
word="{{word}}"
|
||||
bind:onDelete="deleteWordCard"
|
||||
bind:onColorTap="onColorTap" />
|
||||
</view>
|
||||
<view class="word-list-empty" wx:if="{{!cardList.length}}">
|
||||
<view class="word-list-empty" wx:if="{{!words.length}}">
|
||||
<view class="empty-icon">
|
||||
<text class="toy-icon toy-icon-translate"></text>
|
||||
</view>
|
||||
@@ -37,31 +37,46 @@
|
||||
|
||||
<view id="previewWrapper" class="wrapper">
|
||||
<text class="wrapper-title">预览练字田字格</text>
|
||||
<view class="tianzige" wx:if="{{rowHeaderWords.length > 0}}">
|
||||
<view
|
||||
class="tzg-row"
|
||||
wx:for="{{rowsArray}}"
|
||||
wx:for-index="rowIndex"
|
||||
wx:key="rowIndex">
|
||||
<view
|
||||
class="tzg-cell"
|
||||
wx:for="{{colsArray}}"
|
||||
wx:for-index="colIndex"
|
||||
wx:key="{{rowIndex}}-{{colIndex}}">
|
||||
<text wx:if="{{colIndex === 0}}" class="tzg-word"
|
||||
>{{rowHeaderWords[rowIndex]}}</text
|
||||
>
|
||||
<view class="mid-line h"></view>
|
||||
<view class="mid-line v"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
wx:if="{{words}}"
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
<view wx:else class="empty-tip">请选择或输入汉字后生成田字格</view>
|
||||
</view>
|
||||
<view wx:else class="empty-tip">请选择或输入汉字后生成田字格</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-btn-box">
|
||||
<toy-button
|
||||
openType="share"
|
||||
type="green"
|
||||
flat="{{true}}"
|
||||
bind:click="onShareAppMessage"
|
||||
width="220rpx"
|
||||
height="80rpx"
|
||||
icon="wechat"
|
||||
icon-class-prefix="toy-icon">
|
||||
分享
|
||||
</toy-button>
|
||||
<toy-button
|
||||
type="primary"
|
||||
flat="{{true}}"
|
||||
bind:click="exportToPrint"
|
||||
width="420rpx"
|
||||
height="80rpx"
|
||||
disabled="{{words.length <= 0}}"
|
||||
icon="download"
|
||||
icon-class-prefix="toy-icon">
|
||||
下载打印
|
||||
</toy-button>
|
||||
</view>
|
||||
<word-picker
|
||||
show="{{showSelectWordPopup}}"
|
||||
cardList="{{cardList}}"
|
||||
selectedWords="{{words}}"
|
||||
bind:onClose="closeSelectWordPopup"
|
||||
bind:onChange="onChangeWord" />
|
||||
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
@@ -1,91 +0,0 @@
|
||||
Page({
|
||||
data: {
|
||||
// 选中字卡列表,复用 index 页的结构 [{ word, color }]
|
||||
cardList: [] as { word: string; color: string }[],
|
||||
showSelectWordPopup: false,
|
||||
|
||||
// 田字格配置
|
||||
rows: 10,
|
||||
cols: 12,
|
||||
rowsArray: [] as number[],
|
||||
colsArray: [] as number[],
|
||||
rowHeaderWords: [] as string[],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initGrid();
|
||||
},
|
||||
|
||||
// 输入组件回调
|
||||
onConfirmInput(e: any) {
|
||||
const value: string = (e && e.detail && e.detail.value) || '';
|
||||
const text = (value || '').trim();
|
||||
if (!text) return;
|
||||
|
||||
if (!this.isChineseText(text)) {
|
||||
wx.showToast({ title: '请输入汉字', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const chars = this.splitToSingleChars(text);
|
||||
const { cardList } = this.data as any;
|
||||
const exists = new Set(cardList.map((c: any) => c.word));
|
||||
const newList = [...cardList];
|
||||
chars.forEach((ch) => {
|
||||
if (!exists.has(ch)) {
|
||||
newList.push({ word: ch, color: '#141414' });
|
||||
}
|
||||
});
|
||||
this.setData({ cardList: newList }, () => this.updateRowHeaderWords());
|
||||
},
|
||||
|
||||
// 选择面板
|
||||
openSelectWordPopup() {
|
||||
this.setData({ showSelectWordPopup: true });
|
||||
},
|
||||
closeSelectWordPopup() {
|
||||
this.setData({ showSelectWordPopup: false });
|
||||
},
|
||||
onChangeWord(e: any) {
|
||||
const { cardList } = e.detail || {};
|
||||
this.setData({ cardList }, () => this.updateRowHeaderWords());
|
||||
},
|
||||
|
||||
// 删除与颜色
|
||||
deleteWordCard(e: any) {
|
||||
const index = e.detail && e.detail.index;
|
||||
const { cardList } = this.data as any;
|
||||
const next = cardList.filter((_: any, i: number) => i !== index);
|
||||
this.setData({ cardList: next }, () => this.updateRowHeaderWords());
|
||||
},
|
||||
onColorTap() { },
|
||||
|
||||
// 校验 & 分割
|
||||
isChineseText(text: string): boolean {
|
||||
const chineseRegex = /^[\u4e00-\u9fff]+$/;
|
||||
return chineseRegex.test(text);
|
||||
},
|
||||
splitToSingleChars(text: string): string[] {
|
||||
const chineseRegex = /[\u4e00-\u9fff]/g;
|
||||
const matches = text.match(chineseRegex);
|
||||
return matches || [];
|
||||
},
|
||||
|
||||
// 田字格
|
||||
initGrid() {
|
||||
const { rows, cols } = this.data as any;
|
||||
const rowsArray = Array.from({ length: rows }, (_, i) => i);
|
||||
const colsArray = Array.from({ length: cols }, (_, i) => i);
|
||||
this.setData({ rowsArray, colsArray });
|
||||
},
|
||||
updateRowHeaderWords() {
|
||||
const { rows, cardList } = this.data as any;
|
||||
const rowHeaderWords: string[] = [];
|
||||
for (let i = 0; i < rows; i++) {
|
||||
rowHeaderWords.push((cardList[i] && cardList[i].word) || '');
|
||||
}
|
||||
this.setData({ rowHeaderWords });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ Page({
|
||||
|
||||
data: {
|
||||
cardList: [] as CardList,
|
||||
selectedWords: [] as string[],
|
||||
showColorPopup: false,
|
||||
currentKey: 0,
|
||||
currentColor: '',
|
||||
@@ -43,6 +44,7 @@ Page({
|
||||
{ color: '#FFFF00', word: '迎' },
|
||||
{ color: '#8A2BE2', word: '您' },
|
||||
],
|
||||
selectedWords: ['涂', '鸦', '丫', '欢', '迎', '您'],
|
||||
showIntroductionPopup: true,
|
||||
env: getApp().globalData.env || 'release',
|
||||
});
|
||||
@@ -69,8 +71,8 @@ Page({
|
||||
const boxWidth = rect.width;
|
||||
const boxHeight = boxWidth / (width / height);
|
||||
|
||||
this.boxHeight = boxHeight;
|
||||
this.boxWidth = boxWidth;
|
||||
console.log('boxWidth----:', boxWidth)
|
||||
console.log('boxHeight----:', boxHeight)
|
||||
|
||||
// 然后初始化canvas
|
||||
wx.createSelectorQuery()
|
||||
@@ -108,7 +110,10 @@ Page({
|
||||
callback: () => void = () => { },
|
||||
) {
|
||||
if (updatedCardList) {
|
||||
this.setData({ cardList: updatedCardList }, () => {
|
||||
this.setData({
|
||||
cardList: updatedCardList,
|
||||
selectedWords: updatedCardList.map((item) => item.word)
|
||||
}, () => {
|
||||
// 如果cardList有内容且canvas未初始化,先初始化canvas
|
||||
if (!this.canvas) {
|
||||
this.initCanvas();
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
bind:onChange="onChangeColor" />
|
||||
<word-picker
|
||||
show="{{showSelectWordPopup}}"
|
||||
cardList="{{cardList}}"
|
||||
selectedWords="{{cardList}}"
|
||||
bind:onClose="closeSelectWordPopup"
|
||||
bind:onChange="onChangeWord" />
|
||||
</view>
|
||||
|
||||
Reference in New Issue
Block a user