feat:绘制图形
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
// 读取grade3.json文件
|
||||||
|
const data = JSON.parse(
|
||||||
|
fs.readFileSync('miniprogram/demo/grade3.json', 'utf8'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 提取所有汉字
|
||||||
|
const hanzi = Object.keys(data);
|
||||||
|
|
||||||
|
// 生成TypeScript代码
|
||||||
|
const output = `/**
|
||||||
|
* 从 grade3.json 中提取的所有汉字
|
||||||
|
* 总共 ${hanzi.length} 个汉字
|
||||||
|
*/
|
||||||
|
export const SUPPORT_HANZI: string[] = [
|
||||||
|
${hanzi.map((h) => `'${h}'`).join(', ')}
|
||||||
|
];
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
fs.writeFileSync('miniprogram/constants/copyWords.ts', output);
|
||||||
|
|
||||||
|
console.log(`Generated copyWords.ts with ${hanzi.length} hanzi`);
|
||||||
|
console.log('First 10:', hanzi.slice(0, 10));
|
||||||
|
console.log('Last 10:', hanzi.slice(-10));
|
||||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
|||||||
"pages/index/index",
|
"pages/index/index",
|
||||||
"pages/shape/index",
|
"pages/shape/index",
|
||||||
"pages/debug/debug",
|
"pages/debug/debug",
|
||||||
"demoPages/shapePrint/index"
|
"pages/wordDemo/index"
|
||||||
],
|
],
|
||||||
"window": {},
|
"window": {},
|
||||||
"style": "v2",
|
"style": "v2",
|
||||||
@@ -40,6 +40,12 @@
|
|||||||
"iconPath": "assets/tabBar/icon-shape.png",
|
"iconPath": "assets/tabBar/icon-shape.png",
|
||||||
"selectedIconPath": "assets/tabBar/icon-shape-active.png",
|
"selectedIconPath": "assets/tabBar/icon-shape-active.png",
|
||||||
"text": "图形"
|
"text": "图形"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/wordDemo/index",
|
||||||
|
"iconPath": "assets/tabBar/icon-word.png",
|
||||||
|
"selectedIconPath": "assets/tabBar/icon-word-active.png",
|
||||||
|
"text": "练字"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 969 B |
@@ -38,6 +38,6 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: -16rpx;
|
top: -16rpx;
|
||||||
right: -16rpx;
|
right: -16rpx;
|
||||||
color: rgba(255, 0, 0);
|
color: #ff0000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +77,18 @@
|
|||||||
width: 48rpx;
|
width: 48rpx;
|
||||||
height: 48rpx;
|
height: 48rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.checked-disabled {
|
||||||
|
border: 1px solid #c2c2c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-shape-checked-disabled {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2rpx;
|
||||||
|
right: -2rpx;
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ Component({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
|
selectedShapeList: { // 已选中的shapeList,用于初始化
|
||||||
|
type: Array,
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
selectedShapeIndex: {
|
selectedShapeIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
value: -1,
|
value: -1,
|
||||||
@@ -24,39 +28,72 @@ Component({
|
|||||||
totalSelected: 0,
|
totalSelected: 0,
|
||||||
},
|
},
|
||||||
lifetimes: {
|
lifetimes: {
|
||||||
attached() {
|
// attached() {
|
||||||
// 处理 SVG 数据,转换为 data URL
|
// // 处理 SVG 数据,转换为 data URL
|
||||||
const shapesWithDataUrl = SHAPES.map(shape => {
|
// const shapesWithDataUrl = SHAPES.map(shape => {
|
||||||
const completeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">${shape.svg}</svg>`;
|
// const completeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">${shape.svg}</svg>`;
|
||||||
const svgDataUri = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(completeSvg)}`;
|
// const svgDataUri = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(completeSvg)}`;
|
||||||
return {
|
// return {
|
||||||
...shape,
|
// ...shape,
|
||||||
svgDataUrl: svgDataUri
|
// svgDataUrl: svgDataUri
|
||||||
};
|
// };
|
||||||
});
|
// });
|
||||||
|
// this.setData({
|
||||||
|
// shapes: shapesWithDataUrl
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
observers: {
|
||||||
|
// selectedShapeIndex(index: number) {
|
||||||
|
// const { shapes } = this.data;
|
||||||
|
// const newShapes = shapes.map((item, i) => ({
|
||||||
|
// ...item,
|
||||||
|
// checked: i === index
|
||||||
|
// }));
|
||||||
|
// this.setData({
|
||||||
|
// singleMode: index !== -1,
|
||||||
|
// totalSelected: index !== -1 ? 1 : 0,
|
||||||
|
// shapes: newShapes
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
'selectedShapeList, selectedShapeIndex': function (selectedShapeList: ShapeCard[], index: number) {
|
||||||
|
let newShapes = [];
|
||||||
|
|
||||||
|
let checkedIds: string[] = selectedShapeList.map(item => item.id);
|
||||||
|
if (index !== -1) { // 单选模式
|
||||||
|
let singleShapeId = selectedShapeList[index].id;
|
||||||
|
|
||||||
|
newShapes = SHAPES.map(shape => {
|
||||||
|
const completeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">${shape.svg}</svg>`;
|
||||||
|
const svgDataUri = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(completeSvg)}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
svgDataUrl: svgDataUri,
|
||||||
|
checked: singleShapeId === shape.id,
|
||||||
|
checkedDisabled: singleShapeId !== shape.id && checkedIds.includes(shape.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else { // 多选模式
|
||||||
|
newShapes = SHAPES.map(shape => {
|
||||||
|
const completeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">${shape.svg}</svg>`;
|
||||||
|
const svgDataUri = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(completeSvg)}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
svgDataUrl: svgDataUri,
|
||||||
|
checked: checkedIds.includes(shape.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
shapes: shapesWithDataUrl
|
shapes: newShapes,
|
||||||
|
totalSelected: newShapes.filter(item => item.checked).length,
|
||||||
|
singleMode: index !== -1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
observers: {
|
|
||||||
selectedShapeIndex(newVal: number) {
|
|
||||||
this.setData({
|
|
||||||
singleMode: newVal !== -1
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// pageLifetimes: {
|
|
||||||
// show: function () {
|
|
||||||
// console.log('show singleMode', this.data.singleMode);
|
|
||||||
// },
|
|
||||||
// hide: function () {
|
|
||||||
// console.log('hide singleMode', this.data.singleMode);
|
|
||||||
// },
|
|
||||||
// resize: function (size) {
|
|
||||||
// console.log('resize', size);
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
/**
|
/**
|
||||||
* 组件的方法列表
|
* 组件的方法列表
|
||||||
*/
|
*/
|
||||||
@@ -70,6 +107,16 @@ Component({
|
|||||||
const { singleMode, shapes } = this.data;
|
const { singleMode, shapes } = this.data;
|
||||||
let newShapes = [];
|
let newShapes = [];
|
||||||
|
|
||||||
|
const selectedShape = shapes[index];
|
||||||
|
if (selectedShape.checkedDisabled) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '当前形状已选中,请勿重复选中',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (singleMode) {
|
if (singleMode) {
|
||||||
// 单选模式:currentShapeKey 不为空时,只能选中一个形状
|
// 单选模式:currentShapeKey 不为空时,只能选中一个形状
|
||||||
newShapes = shapes.map((item, i) => ({
|
newShapes = shapes.map((item, i) => ({
|
||||||
@@ -78,7 +125,7 @@ Component({
|
|||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
// 多选模式:currentShapeKey 为空时,支持多选,最多可选6个
|
// 多选模式:currentShapeKey 为空时,支持多选,最多可选6个
|
||||||
newShapes = this.data.shapes.map(item => ({ ...item }));
|
newShapes = shapes.map(item => ({ ...item }));
|
||||||
const targetIndex = index;
|
const targetIndex = index;
|
||||||
|
|
||||||
// 统计当前已选中的数量
|
// 统计当前已选中的数量
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
选择形状{{ singleMode ? '(单选)' : '(多选)' }}
|
选择形状{{ singleMode ? '(单选)' : '(多选)' }}
|
||||||
</text>
|
</text>
|
||||||
<view class="shape-options-wrapper">
|
<view class="shape-options-wrapper">
|
||||||
<view class="shape-options">
|
<view class="shape-options {{ singleMode ? 'single-mode' : '' }}">
|
||||||
<view
|
<view
|
||||||
wx:for="{{shapes}}"
|
wx:for="{{shapes}}"
|
||||||
wx:key="id"
|
wx:key="id"
|
||||||
class="shape-option {{item.checked ? 'selected' : '' }}"
|
class="shape-option {{item.checked ? 'selected' : '' }} {{ item.checkedDisabled ? 'checked-disabled' : '' }}"
|
||||||
bindtap="onSelectShape"
|
bindtap="onSelectShape"
|
||||||
data-index="{{index}}"
|
data-index="{{index}}"
|
||||||
data-shape="{{item.id}}">
|
data-shape="{{item.id}}">
|
||||||
@@ -25,6 +25,11 @@
|
|||||||
class="img-shape-selected"
|
class="img-shape-selected"
|
||||||
src="/assets/imgs/checked.png"
|
src="/assets/imgs/checked.png"
|
||||||
mode="aspectFit" />
|
mode="aspectFit" />
|
||||||
|
<image
|
||||||
|
wx:if="{{item.checkedDisabled}}"
|
||||||
|
class="img-shape-checked-disabled"
|
||||||
|
src="/assets/imgs/checked-disabled.png"
|
||||||
|
mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -15,6 +15,7 @@ interface ShapeCard {
|
|||||||
fillColor: string;
|
fillColor: string;
|
||||||
drawFunction?: string;
|
drawFunction?: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
|
checkedDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SHAPES: ShapeCard[] = [
|
const SHAPES: ShapeCard[] = [
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Executable
+59680
File diff suppressed because it is too large
Load Diff
Executable
+156
@@ -0,0 +1,156 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>田字格字帖生成器</title>
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
div,
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
li {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
width: 938px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
float: left;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
font-family: "楷体", "楷体_gb2312";
|
||||||
|
font-size: 58px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 85px;
|
||||||
|
background: url(images/bg12.jpg);
|
||||||
|
margin: 4px 0px 5px -2px;
|
||||||
|
color: #b8b8b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.f {
|
||||||
|
color: #000;
|
||||||
|
/**margin-left: -0px*/
|
||||||
|
}
|
||||||
|
|
||||||
|
li.svg {
|
||||||
|
line-height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li svg {
|
||||||
|
margin: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afterpage {
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
li {
|
||||||
|
background: url(images/bg12.jpg);
|
||||||
|
}
|
||||||
|
.afterpage {
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
.noprint {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div class="noprint">
|
||||||
|
<textarea id="to_print" style='margin: 0px; width: 930px; height: 175px'></textarea>
|
||||||
|
<div style='align-items:center; margin: 0px; width: 930px; height: 20px'>
|
||||||
|
<input type="submit" onclick='onclick_gen();' value="生成" />
|
||||||
|
<input type="submit" onclick='window.print();' value="打印" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul id="u_printf">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<script id="myword" charset="UTF-8" src="mwords.json"></script>
|
||||||
|
<script>
|
||||||
|
console.log(words["舨"]);
|
||||||
|
let template_li = '<li class="svg"><svg width="54" height="54" version="1.1" xmlns="http://www.w3.org/2000/svg">##paths##</svg></li>';
|
||||||
|
let empty_li1 = '<li class="f"> </li>';
|
||||||
|
let empty_li2 = '';
|
||||||
|
for (let i = 0; i < 11; i++) { empty_li2 += "<li> </li>" }
|
||||||
|
let first_paths = '<path d="##path_d##" style="fill:rgb(0,0,0);stroke:rgb(0,0,0);" stroke-width="1.5"></path>';
|
||||||
|
let second_paths = '<path d="##path_d##" style="fill:rgb(184,184,184);stroke:rgb(184,184,184);" stroke-width="1.6"></path>';
|
||||||
|
second_paths = '<path d="##path_d##" style="fill:rgb(184,184,184);stroke:#999;" stroke-width="1.6"></path>';
|
||||||
|
let d_key = '##path_d##';
|
||||||
|
let p_key = '##paths##';
|
||||||
|
u_printf.innerHTML = "";
|
||||||
|
let preview = (c) => {
|
||||||
|
|
||||||
|
let pre_li = "";
|
||||||
|
|
||||||
|
ds = words[c];
|
||||||
|
if (!ds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
len = ds.length;
|
||||||
|
f_path = "";
|
||||||
|
for (let j = 0; j < len; j++) {
|
||||||
|
f_path += first_paths.replace(d_key, ds[j]);
|
||||||
|
}
|
||||||
|
pre_li = template_li.replace(p_key, f_path);;
|
||||||
|
|
||||||
|
return pre_li;
|
||||||
|
};
|
||||||
|
let practice = (c) => {
|
||||||
|
ds = words[c];
|
||||||
|
if (!ds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
len = ds.length;
|
||||||
|
let bihua_li = "";
|
||||||
|
let s_path = "";
|
||||||
|
for (let bihua = 0; bihua < len; bihua++) {
|
||||||
|
s_path = "";
|
||||||
|
for (let i = 0; i <= bihua; i++) {
|
||||||
|
s_path += second_paths.replace(d_key, ds[i]);
|
||||||
|
}
|
||||||
|
bihua_li += template_li.replace(p_key, s_path);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 23 - len; i++) {
|
||||||
|
bihua_li += template_li.replace(p_key, s_path);
|
||||||
|
}
|
||||||
|
bihua_li += empty_li1;
|
||||||
|
bihua_li += empty_li2;
|
||||||
|
|
||||||
|
return bihua_li;
|
||||||
|
}
|
||||||
|
let print_char = (c) => {
|
||||||
|
return preview(c) + practice(c);
|
||||||
|
}
|
||||||
|
let print_sentence = (s) => {
|
||||||
|
sen_li = "";
|
||||||
|
for (let i in s) {
|
||||||
|
sen_li += print_char(s[i]);
|
||||||
|
}
|
||||||
|
return sen_li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onclick_gen = () => {
|
||||||
|
u_printf.innerHTML = "";
|
||||||
|
|
||||||
|
lis = print_sentence(to_print.value.replace(/ /g,"").trim());
|
||||||
|
u_printf.innerHTML = lis;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"navigationBarTitleText": "首页",
|
|
||||||
"navigationBarBackgroundColor": "#d2e7d8",
|
|
||||||
"homeButton": true,
|
|
||||||
"backgroundColor": "#e8eddb",
|
|
||||||
"enablePullDownRefresh": false
|
|
||||||
}
|
|
||||||
@@ -3,31 +3,197 @@ page {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 20rpx;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入区域样式 */
|
||||||
|
.input-section {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 80rpx;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
background-color: #07c160;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用区域样式 */
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
background-color: #ff4757;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已选择的汉字区域 */
|
||||||
|
.selected-section {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item.selected {
|
||||||
|
background-color: #07c160;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50rpx;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
position: relative;
|
||||||
|
min-width: 80rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: -10rpx;
|
||||||
|
right: -10rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: #ff4757;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 可选汉字区域 */
|
||||||
|
.available-section {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item.available {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
border-radius: 50rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item.available:active {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item.available.disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item.available.disabled:active {
|
||||||
|
background-color: #ccc;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 功能模块区域 */
|
||||||
|
.cards-section {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-view {
|
.scroll-view {
|
||||||
flex: 1;
|
height: 400rpx;
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
margin: 10px;
|
margin: 10rpx;
|
||||||
padding: 10px;
|
padding: 20rpx;
|
||||||
background-color: #fff;
|
background-color: #f8f9fa;
|
||||||
border-radius: 10px;
|
border-radius: 20rpx;
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||||
width: calc(33.33% - 20px);
|
width: calc(33.33% - 20rpx);
|
||||||
/* Adjust width for three cards per row */
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* Ensure padding and margin are included in the width */
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
/* Allow cards to sit next to each other */
|
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
/* Align cards to the top */
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
.card-title {
|
|
||||||
font-size: 18px;
|
.card:active {
|
||||||
font-weight: bold;
|
transform: scale(0.95);
|
||||||
color: #333;
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
|
inputWord: '', // 输入框中的汉字
|
||||||
|
wordList: [] as string[], // 选中的汉字列表
|
||||||
|
availableWords: [] as string[], // 从grade3.json中提取的前30个汉字
|
||||||
cards: [
|
cards: [
|
||||||
{
|
{
|
||||||
key: 0,
|
key: 0,
|
||||||
@@ -24,6 +27,113 @@ Page({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.loadGrade3Words();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载grade3.json中的前30个汉字
|
||||||
|
loadGrade3Words() {
|
||||||
|
const fs = wx.getFileSystemManager();
|
||||||
|
try {
|
||||||
|
const fileContent = fs.readFileSync('grade3.json', 'utf8') as string;
|
||||||
|
const grade3Data = JSON.parse(fileContent);
|
||||||
|
const words = Object.keys(grade3Data).slice(0, 30);
|
||||||
|
this.setData({
|
||||||
|
availableWords: words
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载grade3.json失败:', error);
|
||||||
|
// 如果文件读取失败,使用一些示例汉字
|
||||||
|
const fallbackWords = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '人', '口', '手', '足', '目', '耳', '鼻', '舌', '心', '肝', '脾', '肺', '肾', '胃', '肠', '胆', '膀', '胱', '皮', '毛'];
|
||||||
|
this.setData({
|
||||||
|
availableWords: fallbackWords
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 输入框输入事件
|
||||||
|
onInputChange(e: WechatMiniprogram.Input) {
|
||||||
|
this.setData({
|
||||||
|
inputWord: e.detail.value
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 生成按钮点击事件
|
||||||
|
onGenerateClick() {
|
||||||
|
const { inputWord, wordList } = this.data;
|
||||||
|
if (inputWord && inputWord.trim()) {
|
||||||
|
// 检查是否已经存在
|
||||||
|
if (!wordList.includes(inputWord.trim())) {
|
||||||
|
const newWordList = [...wordList, inputWord.trim()];
|
||||||
|
this.setData({
|
||||||
|
wordList: newWordList,
|
||||||
|
inputWord: '' // 清空输入框
|
||||||
|
});
|
||||||
|
wx.showToast({
|
||||||
|
title: '已添加到列表',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
wx.showToast({
|
||||||
|
title: '该汉字已存在',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请输入汉字',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择汉字点击事件
|
||||||
|
onWordSelect(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
const { word } = e.currentTarget.dataset;
|
||||||
|
const { wordList } = this.data;
|
||||||
|
|
||||||
|
if (!wordList.includes(word)) {
|
||||||
|
const newWordList = [...wordList, word];
|
||||||
|
this.setData({
|
||||||
|
wordList: newWordList
|
||||||
|
});
|
||||||
|
wx.showToast({
|
||||||
|
title: '已选择',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
wx.showToast({
|
||||||
|
title: '已选择过',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从列表中移除汉字
|
||||||
|
onRemoveWord(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
const { index } = e.currentTarget.dataset;
|
||||||
|
const { wordList } = this.data;
|
||||||
|
const newWordList = wordList.filter((_, i) => i !== index);
|
||||||
|
this.setData({
|
||||||
|
wordList: newWordList
|
||||||
|
});
|
||||||
|
wx.showToast({
|
||||||
|
title: '已移除',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空所有选中的汉字
|
||||||
|
onClearAll() {
|
||||||
|
this.setData({
|
||||||
|
wordList: []
|
||||||
|
});
|
||||||
|
wx.showToast({
|
||||||
|
title: '已清空',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
tapCard(e: WechatMiniprogram.TouchEvent) {
|
tapCard(e: WechatMiniprogram.TouchEvent) {
|
||||||
const { url } = e.currentTarget.dataset;
|
const { url } = e.currentTarget.dataset;
|
||||||
wx.navigateTo({ url });
|
wx.navigateTo({ url });
|
||||||
|
|||||||
@@ -1,12 +1,81 @@
|
|||||||
<!--index.wxml-->
|
<!--index.wxml-->
|
||||||
<scroll-view class="scroll-view" scroll-y type="list">
|
<view class="container">
|
||||||
<view
|
<!-- 输入区域 -->
|
||||||
class="card"
|
<view class="input-section">
|
||||||
wx:for="{{cards}}"
|
<view class="input-row">
|
||||||
wx:for-item="card"
|
<input
|
||||||
wx:key="*this"
|
class="word-input"
|
||||||
bindtap="tapCard"
|
placeholder="请输入汉字"
|
||||||
data-url="{{card.url}}">
|
value="{{inputWord}}"
|
||||||
<view class="card-title">{{card.title}}</view>
|
bindinput="onInputChange"
|
||||||
|
maxlength="10" />
|
||||||
|
<button class="generate-btn" bindtap="onGenerateClick">生成</button>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
|
||||||
|
<!-- 已选择的汉字列表 -->
|
||||||
|
<view class="selected-section" wx:if="{{wordList.length > 0}}">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title"
|
||||||
|
>已选择的汉字 ({{wordList.length}})</text
|
||||||
|
>
|
||||||
|
<button class="clear-btn" bindtap="onClearAll" size="mini">
|
||||||
|
清空
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<view class="word-list">
|
||||||
|
<view
|
||||||
|
class="word-item selected"
|
||||||
|
wx:for="{{wordList}}"
|
||||||
|
wx:key="*this"
|
||||||
|
wx:for-item="word"
|
||||||
|
wx:for-index="index">
|
||||||
|
<text class="word-text">{{word}}</text>
|
||||||
|
<text
|
||||||
|
class="remove-btn"
|
||||||
|
bindtap="onRemoveWord"
|
||||||
|
data-index="{{index}}"
|
||||||
|
>×</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 可选汉字区域 -->
|
||||||
|
<view class="available-section">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title"
|
||||||
|
>可选汉字 ({{availableWords.length}})</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<view class="word-grid">
|
||||||
|
<view
|
||||||
|
class="word-item available {{wordList.includes(word) ? 'disabled' : ''}}"
|
||||||
|
wx:for="{{availableWords}}"
|
||||||
|
wx:key="*this"
|
||||||
|
wx:for-item="word"
|
||||||
|
bindtap="onWordSelect"
|
||||||
|
data-word="{{word}}">
|
||||||
|
<text class="word-text">{{word}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 原有的卡片区域 -->
|
||||||
|
<view class="cards-section">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">功能模块</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="scroll-view" scroll-y type="list">
|
||||||
|
<view
|
||||||
|
class="card"
|
||||||
|
wx:for="{{cards}}"
|
||||||
|
wx:for-item="card"
|
||||||
|
wx:key="*this"
|
||||||
|
bindtap="tapCard"
|
||||||
|
data-url="{{card.url}}">
|
||||||
|
<view class="card-title">{{card.title}}</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ Page({
|
|||||||
wx.getStorageSync('hasShowIntroduction') || false;
|
wx.getStorageSync('hasShowIntroduction') || false;
|
||||||
|
|
||||||
// 判断是否在PC端开发工具上运行
|
// 判断是否在PC端开发工具上运行
|
||||||
// const systemInfo = wx.getDeviceInfo();
|
const systemInfo = wx.getDeviceInfo();
|
||||||
// if (systemInfo.platform === 'devtools') {
|
if (systemInfo.platform === 'devtools') {
|
||||||
// this.setData({
|
this.setData({
|
||||||
// isPCDevtool: true,
|
isPCDevtool: true,
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
if (!hasShowIntroduction) {
|
if (!hasShowIntroduction) {
|
||||||
this.setData({
|
this.setData({
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Page({
|
|||||||
boxHeight: 0,
|
boxHeight: 0,
|
||||||
boxWidth: 0,
|
boxWidth: 0,
|
||||||
shapeDrawService: null as ShapeDrawService | null,
|
shapeDrawService: null as ShapeDrawService | null,
|
||||||
|
timer: null as any,
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
shapeList: [] as ShapeCard[],
|
shapeList: [] as ShapeCard[],
|
||||||
@@ -93,6 +94,13 @@ Page({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleSelectShape() {
|
||||||
|
this.setData({
|
||||||
|
showSelectShapePopup: true,
|
||||||
|
selectedShapeIndex: -1
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
openSelectShapePopup(e: any) {
|
openSelectShapePopup(e: any) {
|
||||||
const { index } = e.detail;
|
const { index } = e.detail;
|
||||||
this.setData({
|
this.setData({
|
||||||
@@ -104,7 +112,6 @@ Page({
|
|||||||
closeSelectShapePopup() {
|
closeSelectShapePopup() {
|
||||||
this.setData({
|
this.setData({
|
||||||
showSelectShapePopup: false,
|
showSelectShapePopup: false,
|
||||||
selectedShapeIndex: -1
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -174,12 +181,11 @@ Page({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先关闭弹窗
|
||||||
this.setData({
|
this.setData({
|
||||||
showSelectShapePopup: false,
|
showSelectShapePopup: false,
|
||||||
selectedShapeIndex: -1,
|
shapeList: newShapeList,
|
||||||
shapeList: newShapeList
|
|
||||||
}, () => {
|
}, () => {
|
||||||
// 绘制canvas
|
|
||||||
this.drawCanvas();
|
this.drawCanvas();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -233,56 +239,29 @@ Page({
|
|||||||
this.setData({
|
this.setData({
|
||||||
showColorPopup: true,
|
showColorPopup: true,
|
||||||
currentColor: fillColor,
|
currentColor: fillColor,
|
||||||
currentIndex: index
|
selectedShapeIndex: index
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onCloseColorPopup() {
|
onCloseColorPopup() {
|
||||||
this.setData({
|
this.setData({
|
||||||
showColorPopup: false,
|
showColorPopup: false,
|
||||||
currentColor: '',
|
currentColor: '',
|
||||||
currentIndex: 0
|
selectedShapeIndex: 0
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onChangeColor(e: any) {
|
onChangeColor(e: any) {
|
||||||
const { color } = e.detail;
|
const { color } = e.detail;
|
||||||
const { currentIndex } = this.data;
|
const { selectedShapeIndex } = this.data;
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
showColorPopup: false,
|
showColorPopup: false,
|
||||||
[`shapeList[${currentIndex}].fillColor`]: color
|
[`shapeList[${selectedShapeIndex}].fillColor`]: color
|
||||||
}, () => {
|
}, () => {
|
||||||
// 绘制canvas
|
// 绘制canvas
|
||||||
this.drawCanvas();
|
this.drawCanvas();
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 测试ShapeDrawService
|
|
||||||
// testShapeDrawService() {
|
|
||||||
// if (!this.shapeDrawService) {
|
|
||||||
// wx.showToast({
|
|
||||||
// title: 'Canvas未初始化',
|
|
||||||
// icon: 'none'
|
|
||||||
// });
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 使用当前选中的图形进行测试
|
|
||||||
// const testShapes = this.data.shapeList.slice(0, 3); // 只测试前3个图形
|
|
||||||
// if (testShapes.length === 0) {
|
|
||||||
// wx.showToast({
|
|
||||||
// title: '请先选择图形',
|
|
||||||
// icon: 'none'
|
|
||||||
// });
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this.shapeDrawService.draw(testShapes);
|
|
||||||
// wx.showToast({
|
|
||||||
// title: '绘制完成',
|
|
||||||
// icon: 'success'
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
|
|
||||||
onShareAppMessage() {
|
onShareAppMessage() {
|
||||||
return {
|
return {
|
||||||
title: '涂鸦丫-涂色|识字|画画|打印',
|
title: '涂鸦丫-涂色|识字|画画|打印',
|
||||||
@@ -303,4 +282,10 @@ Page({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onUnload() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<view class="btn-area">
|
<view class="btn-area">
|
||||||
<toy-button
|
<toy-button
|
||||||
type="green"
|
type="green"
|
||||||
bind:click="openSelectShapePopup"
|
bind:click="handleSelectShape"
|
||||||
width="290rpx">
|
width="290rpx">
|
||||||
选择图形
|
选择图形
|
||||||
</toy-button>
|
</toy-button>
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<shape-picker
|
<shape-picker
|
||||||
|
selectedShapeList="{{shapeList}}"
|
||||||
show="{{showSelectShapePopup}}"
|
show="{{showSelectShapePopup}}"
|
||||||
selectedShapeIndex="{{selectedShapeIndex}}"
|
selectedShapeIndex="{{selectedShapeIndex}}"
|
||||||
bind:onClose="closeSelectShapePopup"
|
bind:onClose="closeSelectShapePopup"
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# 汉字选择页面 (wordDemo)
|
||||||
|
|
||||||
|
## 功能描述
|
||||||
|
|
||||||
|
这是一个汉字输入和选择的页面,主要包含以下功能:
|
||||||
|
|
||||||
|
### 上部区域 - 汉字输入
|
||||||
|
|
||||||
|
- 输入框:可以输入任意汉字(最多10个字符)
|
||||||
|
- 生成按钮:点击后将输入的汉字添加到已选择列表中
|
||||||
|
- **汉字校验**:自动校验输入内容是否为汉字,非汉字会提示错误
|
||||||
|
- **智能分割**:支持输入多个汉字,自动分割为单个汉字存储
|
||||||
|
|
||||||
|
### 中部区域 - 已选择的汉字
|
||||||
|
|
||||||
|
- 显示所有已选择的汉字
|
||||||
|
- 每个汉字都有删除按钮(×)
|
||||||
|
- 清空按钮:一键清空所有已选择的汉字
|
||||||
|
- 显示已选择汉字的数量
|
||||||
|
|
||||||
|
### 下部区域 - 可选汉字
|
||||||
|
|
||||||
|
- 从 `demo/grade3.json` 文件中提取前30个汉字
|
||||||
|
- 以6列网格形式展示
|
||||||
|
- 已选择的汉字会显示为禁用状态
|
||||||
|
- 点击未选择的汉字可以添加到列表中
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
|
### 🔍 **汉字校验**
|
||||||
|
|
||||||
|
- 使用Unicode范围 `\u4e00-\u9fff` 校验汉字
|
||||||
|
- 非汉字输入会显示Toast提示"请输入汉字"
|
||||||
|
- 确保 `wordList` 中只存储纯汉字
|
||||||
|
|
||||||
|
### ✂️ **智能分割**
|
||||||
|
|
||||||
|
- 支持输入多个汉字(如:"你好世界")
|
||||||
|
- 自动分割为单个汉字:["你", "好", "世", "界"]
|
||||||
|
- 过滤重复汉字,避免重复添加
|
||||||
|
- 显示实际添加的汉字数量
|
||||||
|
|
||||||
|
### 💾 **数据管理**
|
||||||
|
|
||||||
|
- `wordList` 中每个元素都是单个汉字
|
||||||
|
- 自动去重,避免重复存储
|
||||||
|
- 支持批量添加和单个删除
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
|
||||||
|
- `inputWord`: 输入框中的汉字
|
||||||
|
- `wordList`: 已选择的汉字列表(string[]类型,每个元素为单个汉字)
|
||||||
|
- `availableWords`: 从grade3.json中提取的前30个汉字
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
1. 在输入框中输入汉字(支持多个汉字)
|
||||||
|
2. 点击"生成"按钮,系统自动校验和分割
|
||||||
|
3. 点击下方网格中的汉字进行选择
|
||||||
|
4. 已选择的汉字会显示在上方,可以单独删除或一键清空
|
||||||
|
5. 所有选择的汉字都存储在 `this.data.wordList` 中
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 汉字校验
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
isChineseText(text: string): boolean {
|
||||||
|
const chineseRegex = /^[\u4e00-\u9fff]+$/;
|
||||||
|
return chineseRegex.test(text);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文本分割
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
splitToSingleChars(text: string): string[] {
|
||||||
|
const chineseRegex = /[\u4e00-\u9fff]/g;
|
||||||
|
const matches = text.match(chineseRegex);
|
||||||
|
return matches || [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
- `index.ts`: 页面逻辑文件
|
||||||
|
- `index.wxml`: 页面模板文件
|
||||||
|
- `index.less`: 页面样式文件
|
||||||
|
- `index.json`: 页面配置文件
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "汉字选择",
|
||||||
|
"navigationBarBackgroundColor": "#d2e7d8",
|
||||||
|
"backgroundColor": "#f5f5f5",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/**index.less**/
|
||||||
|
page {
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 20rpx;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入区域样式 */
|
||||||
|
.input-section {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 80rpx;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
background-color: #07c160;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用区域样式 */
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
background-color: #ff4757;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已选择的汉字区域 */
|
||||||
|
.selected-section {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item.selected {
|
||||||
|
background-color: #07c160;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50rpx;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
position: relative;
|
||||||
|
min-width: 80rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: -10rpx;
|
||||||
|
right: -10rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: #ff4757;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 可选汉字区域 */
|
||||||
|
.available-section {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item.available {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
border-radius: 50rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item.available:active {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item.available.disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item.available.disabled:active {
|
||||||
|
background-color: #ccc;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
Page({
|
||||||
|
data: {
|
||||||
|
inputWord: '', // 输入框中的汉字
|
||||||
|
wordList: [] as string[], // 选中的汉字列表
|
||||||
|
availableWords: [] as string[], // 从grade3.json中提取的前30个汉字
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.loadGrade3Words();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载grade3.json中的前30个汉字
|
||||||
|
loadGrade3Words() {
|
||||||
|
const fs = wx.getFileSystemManager();
|
||||||
|
try {
|
||||||
|
const fileContent = fs.readFileSync('demo/grade3.json', 'utf8') as string;
|
||||||
|
const grade3Data = JSON.parse(fileContent);
|
||||||
|
const words = Object.keys(grade3Data).slice(0, 30);
|
||||||
|
this.setData({
|
||||||
|
availableWords: words
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载grade3.json失败:', error);
|
||||||
|
// 如果文件读取失败,使用一些示例汉字
|
||||||
|
const fallbackWords = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '人', '口', '手', '足', '目', '耳', '鼻', '舌', '心', '肝', '脾', '肺', '肾', '胃', '肠', '胆', '膀', '胱', '皮', '毛'];
|
||||||
|
this.setData({
|
||||||
|
availableWords: fallbackWords
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 输入框输入事件
|
||||||
|
onInputChange(e: WechatMiniprogram.Input) {
|
||||||
|
this.setData({
|
||||||
|
inputWord: e.detail.value
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 生成按钮点击事件
|
||||||
|
onGenerateClick() {
|
||||||
|
const { inputWord, wordList } = this.data;
|
||||||
|
if (inputWord && inputWord.trim()) {
|
||||||
|
const inputText = inputWord.trim();
|
||||||
|
|
||||||
|
// 校验输入是否为汉字
|
||||||
|
if (!this.isChineseText(inputText)) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请输入汉字',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将输入文本分割为单个汉字
|
||||||
|
const singleChars = this.splitToSingleChars(inputText);
|
||||||
|
|
||||||
|
// 过滤掉已存在的汉字
|
||||||
|
const newChars = singleChars.filter(char => !wordList.includes(char));
|
||||||
|
|
||||||
|
if (newChars.length === 0) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '所有汉字都已存在',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到列表中
|
||||||
|
const newWordList = [...wordList, ...newChars];
|
||||||
|
this.setData({
|
||||||
|
wordList: newWordList,
|
||||||
|
inputWord: '' // 清空输入框
|
||||||
|
});
|
||||||
|
|
||||||
|
wx.showToast({
|
||||||
|
title: `已添加${newChars.length}个汉字`,
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请输入汉字',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 校验文本是否为汉字
|
||||||
|
isChineseText(text: string): boolean {
|
||||||
|
// 汉字Unicode范围:\u4e00-\u9fff
|
||||||
|
const chineseRegex = /^[\u4e00-\u9fff]+$/;
|
||||||
|
return chineseRegex.test(text);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 将文本分割为单个汉字
|
||||||
|
splitToSingleChars(text: string): string[] {
|
||||||
|
// 使用正则表达式匹配每个汉字
|
||||||
|
const chineseRegex = /[\u4e00-\u9fff]/g;
|
||||||
|
const matches = text.match(chineseRegex);
|
||||||
|
return matches || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择汉字点击事件
|
||||||
|
onWordSelect(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
const { word } = e.currentTarget.dataset;
|
||||||
|
const { wordList } = this.data;
|
||||||
|
|
||||||
|
if (!wordList.includes(word)) {
|
||||||
|
const newWordList = [...wordList, word];
|
||||||
|
this.setData({
|
||||||
|
wordList: newWordList
|
||||||
|
});
|
||||||
|
wx.showToast({
|
||||||
|
title: '已选择',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
wx.showToast({
|
||||||
|
title: '已选择过',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从列表中移除汉字
|
||||||
|
onRemoveWord(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
const { index } = e.currentTarget.dataset;
|
||||||
|
const { wordList } = this.data;
|
||||||
|
const newWordList = wordList.filter((_, i) => i !== index);
|
||||||
|
this.setData({
|
||||||
|
wordList: newWordList
|
||||||
|
});
|
||||||
|
wx.showToast({
|
||||||
|
title: '已移除',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空所有选中的汉字
|
||||||
|
onClearAll() {
|
||||||
|
this.setData({
|
||||||
|
wordList: []
|
||||||
|
});
|
||||||
|
wx.showToast({
|
||||||
|
title: '已清空',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<!--index.wxml-->
|
||||||
|
<view class="container">
|
||||||
|
<!-- 上部:输入区域 -->
|
||||||
|
<view class="input-section">
|
||||||
|
<view class="input-row">
|
||||||
|
<input
|
||||||
|
class="word-input"
|
||||||
|
placeholder="请输入汉字(支持多个汉字)"
|
||||||
|
value="{{inputWord}}"
|
||||||
|
bindinput="onInputChange"
|
||||||
|
maxlength="20" />
|
||||||
|
<button class="generate-btn" bindtap="onGenerateClick">生成</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 已选择的汉字列表 -->
|
||||||
|
<view class="selected-section" wx:if="{{wordList.length > 0}}">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title"
|
||||||
|
>已选择的汉字 ({{wordList.length}})</text
|
||||||
|
>
|
||||||
|
<button class="clear-btn" bindtap="onClearAll" size="mini">
|
||||||
|
清空
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<view class="word-list">
|
||||||
|
<view
|
||||||
|
class="word-item selected"
|
||||||
|
wx:for="{{wordList}}"
|
||||||
|
wx:key="*this"
|
||||||
|
wx:for-item="word"
|
||||||
|
wx:for-index="index">
|
||||||
|
<text class="word-text">{{word}}</text>
|
||||||
|
<text
|
||||||
|
class="remove-btn"
|
||||||
|
bindtap="onRemoveWord"
|
||||||
|
data-index="{{index}}"
|
||||||
|
>×</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 下部:可选汉字区域 -->
|
||||||
|
<view class="available-section">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title"
|
||||||
|
>可选汉字 ({{availableWords.length}})</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<view class="word-grid">
|
||||||
|
<view
|
||||||
|
class="word-item available {{wordList.includes(word) ? 'disabled' : ''}}"
|
||||||
|
wx:for="{{availableWords}}"
|
||||||
|
wx:key="*this"
|
||||||
|
wx:for-item="word"
|
||||||
|
bindtap="onWordSelect"
|
||||||
|
data-word="{{word}}">
|
||||||
|
<text class="word-text">{{word}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
import { PAPER_SIZE } from '../constants/colors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅支持 M/L/Z 的简单 SVG 路径解析与绘制
|
||||||
|
*/
|
||||||
|
function drawSvgPathCommands(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
pathD: string,
|
||||||
|
offsetX: number,
|
||||||
|
offsetY: number,
|
||||||
|
scale: number,
|
||||||
|
) {
|
||||||
|
const tokens = pathD
|
||||||
|
.replace(/,/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/);
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
let currentX = 0;
|
||||||
|
let currentY = 0;
|
||||||
|
while (i < tokens.length) {
|
||||||
|
const cmd = tokens[i++];
|
||||||
|
switch (cmd) {
|
||||||
|
case 'M': {
|
||||||
|
const x = parseFloat(tokens[i++]);
|
||||||
|
const y = parseFloat(tokens[i++]);
|
||||||
|
currentX = offsetX + x * scale;
|
||||||
|
currentY = offsetY + y * scale;
|
||||||
|
ctx.moveTo(currentX, currentY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'L': {
|
||||||
|
const x = parseFloat(tokens[i++]);
|
||||||
|
const y = parseFloat(tokens[i++]);
|
||||||
|
currentX = offsetX + x * scale;
|
||||||
|
currentY = offsetY + y * scale;
|
||||||
|
ctx.lineTo(currentX, currentY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'Z': {
|
||||||
|
ctx.closePath();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// 遇到数字(可能因为路径省略了连续的 L),回退一步并按 L 处理
|
||||||
|
if (!isNaN(parseFloat(cmd))) {
|
||||||
|
i--;
|
||||||
|
const x = parseFloat(tokens[i++]);
|
||||||
|
const y = parseFloat(tokens[i++]);
|
||||||
|
currentX = offsetX + x * scale;
|
||||||
|
currentY = offsetY + y * scale;
|
||||||
|
ctx.lineTo(currentX, currentY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 其他命令不支持,直接跳过
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawStrokes(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
strokes: string[],
|
||||||
|
offsetX: number,
|
||||||
|
offsetY: number,
|
||||||
|
size: number,
|
||||||
|
uptoInclusive: number,
|
||||||
|
fillStyle: string,
|
||||||
|
strokeStyle: string,
|
||||||
|
lineWidth: number,
|
||||||
|
) {
|
||||||
|
// 模板中使用 54x54 的视窗尺寸
|
||||||
|
const viewBoxSize = 54;
|
||||||
|
const scale = size / viewBoxSize;
|
||||||
|
|
||||||
|
for (let s = 0; s <= uptoInclusive && s < strokes.length; s++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
drawSvgPathCommands(ctx, strokes[s], offsetX, offsetY, scale);
|
||||||
|
ctx.fillStyle = fillStyle;
|
||||||
|
ctx.strokeStyle = strokeStyle;
|
||||||
|
ctx.lineWidth = lineWidth;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.closePath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTianZiGrid(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
size: number,
|
||||||
|
lineColor: string = '#e0e0e0',
|
||||||
|
boldColor: string = '#cccccc',
|
||||||
|
) {
|
||||||
|
// 外框
|
||||||
|
ctx.strokeStyle = boldColor;
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeRect(x - size / 2, y - size / 2, size, size);
|
||||||
|
|
||||||
|
// 中线
|
||||||
|
ctx.strokeStyle = lineColor;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
// 竖线
|
||||||
|
ctx.moveTo(x, y - size / 2);
|
||||||
|
ctx.lineTo(x, y + size / 2);
|
||||||
|
// 横线
|
||||||
|
ctx.moveTo(x - size / 2, y);
|
||||||
|
ctx.lineTo(x + size / 2, y);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
// 对角线(淡)
|
||||||
|
ctx.strokeStyle = '#eeeeee';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x - size / 2, y - size / 2);
|
||||||
|
ctx.lineTo(x + size / 2, y + size / 2);
|
||||||
|
ctx.moveTo(x + size / 2, y - size / 2);
|
||||||
|
ctx.lineTo(x - size / 2, y + size / 2);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.closePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
class WordDrawService {
|
||||||
|
headerType: PrintHeader = 'wechat';
|
||||||
|
canvas: WechatMiniprogram.Canvas;
|
||||||
|
ctx: RenderingContext;
|
||||||
|
options: Record<string, any>;
|
||||||
|
paperSize: PaperSize;
|
||||||
|
currentX: number;
|
||||||
|
currentY: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
canvas: Canvas,
|
||||||
|
ctx: RenderingContext,
|
||||||
|
options?: Record<string, any>,
|
||||||
|
) {
|
||||||
|
options = options || {};
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.paperSize = 'A4';
|
||||||
|
this.options = {
|
||||||
|
appName: '涂鸦丫小程序',
|
||||||
|
appHint: '练字|识字|打印',
|
||||||
|
title: '田字格 练 字 贴',
|
||||||
|
subTitle: '按笔画临摹练习',
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
this.currentX = 0;
|
||||||
|
this.currentY = 0;
|
||||||
|
this.setPrintConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
setPrintConfig() {
|
||||||
|
const printConfig = getApp().getPrintConfig();
|
||||||
|
this.headerType = printConfig.header;
|
||||||
|
this.options.appName = printConfig.appName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成练字帖
|
||||||
|
* @param wordsMap 形如 { "其": ["M.. L.. Z", ...], ... },不超过 10 个汉字
|
||||||
|
*/
|
||||||
|
draw(wordsMap: Record<string, string[]>) {
|
||||||
|
this.setPrintConfig();
|
||||||
|
this.clear();
|
||||||
|
this.setPaper();
|
||||||
|
if (this.headerType !== 'minimal') {
|
||||||
|
this.drawHeader();
|
||||||
|
} else {
|
||||||
|
this.drawMiniHeader();
|
||||||
|
}
|
||||||
|
this.drawContent(wordsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
async drawHeader() {
|
||||||
|
const { canvas, ctx } = this;
|
||||||
|
const { appName, appHint, title, subTitle } = this.options;
|
||||||
|
|
||||||
|
this.currentX = 80;
|
||||||
|
this.currentY = 80;
|
||||||
|
let titleX = this.currentX + 200 + 48;
|
||||||
|
const titleY = 80;
|
||||||
|
|
||||||
|
const logoX = 80;
|
||||||
|
const logoY = 60;
|
||||||
|
const logoWidth = 200;
|
||||||
|
const logoHeight = 200;
|
||||||
|
|
||||||
|
// 这里沿用小程序码/Logo策略,保持与其它服务一致
|
||||||
|
try {
|
||||||
|
const { getMiniCodeImage } = await import('../utils/index');
|
||||||
|
const image = await getMiniCodeImage(canvas);
|
||||||
|
ctx.drawImage(image, logoX, logoY, logoWidth, logoHeight);
|
||||||
|
} catch (_) {
|
||||||
|
// 忽略加载失败
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.font = 'bold 64px "Microsoft Yahei"';
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillText(appName, titleX, titleY);
|
||||||
|
|
||||||
|
ctx.font = '48px "Microsoft Yahei"';
|
||||||
|
ctx.fillStyle = '#666';
|
||||||
|
ctx.fillText(appHint, titleX, 186);
|
||||||
|
|
||||||
|
ctx.font = 'bold 64px "Microsoft Yahei"';
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.fillText(title, 1015, titleY);
|
||||||
|
|
||||||
|
ctx.font = '48px "Microsoft Yahei"';
|
||||||
|
ctx.fillStyle = '#666';
|
||||||
|
ctx.fillText(subTitle, 1015, 186);
|
||||||
|
|
||||||
|
this.currentY = 304;
|
||||||
|
this.drawLine(this.currentY);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawMiniHeader() {
|
||||||
|
const { canvas, ctx } = this;
|
||||||
|
const { appName, title } = this.options;
|
||||||
|
const titleY = 120;
|
||||||
|
|
||||||
|
ctx.font = 'bold 64px "Microsoft Yahei"';
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(appName + ' ' + title, canvas.width / 2, titleY);
|
||||||
|
this.currentY = 200;
|
||||||
|
this.drawLine(this.currentY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制正文内容:为每个汉字生成“预览 + 逐笔画临摹 + 空白格”的练习单元
|
||||||
|
*/
|
||||||
|
drawContent(wordsMap: Record<string, string[]>) {
|
||||||
|
const { canvas, ctx } = this;
|
||||||
|
const characters = Object.keys(wordsMap).slice(0, 10);
|
||||||
|
if (characters.length === 0) return;
|
||||||
|
|
||||||
|
// 布局参数
|
||||||
|
const topGap = 50; // 与页眉分割线的距离
|
||||||
|
const leftMargin = 120;
|
||||||
|
const rightMargin = 120;
|
||||||
|
const bottomMargin = 120;
|
||||||
|
const contentTop = this.currentY + topGap;
|
||||||
|
const contentWidth = canvas.width - leftMargin - rightMargin;
|
||||||
|
|
||||||
|
const cellSize = 140;
|
||||||
|
const minGap = 24;
|
||||||
|
const maxPerRow = Math.max(1, Math.floor((contentWidth + minGap) / (cellSize + minGap)));
|
||||||
|
const rowGap = 36;
|
||||||
|
|
||||||
|
// 为每个汉字生成一串单元:1个预览 + N个逐笔画 + 2个空白 (便于描摹)
|
||||||
|
const unitsPerCharBuilder = (strokeCount: number) => {
|
||||||
|
const preview = 1;
|
||||||
|
const practice = Math.min(strokeCount, 8); // 最多展示 8 步逐笔画,避免过长
|
||||||
|
const blanks = 2;
|
||||||
|
return preview + practice + blanks;
|
||||||
|
};
|
||||||
|
|
||||||
|
let cursorX = leftMargin;
|
||||||
|
let cursorY = contentTop + cellSize / 2;
|
||||||
|
let usedInRow = 0;
|
||||||
|
|
||||||
|
characters.forEach((char) => {
|
||||||
|
const strokes = wordsMap[char] || [];
|
||||||
|
const totalUnits = unitsPerCharBuilder(strokes.length);
|
||||||
|
let unitIndex = 0;
|
||||||
|
|
||||||
|
while (unitIndex < totalUnits) {
|
||||||
|
// 如果本行放不下,换行
|
||||||
|
if (usedInRow >= maxPerRow) {
|
||||||
|
cursorX = leftMargin;
|
||||||
|
cursorY += cellSize + rowGap;
|
||||||
|
usedInRow = 0;
|
||||||
|
// 边界检查
|
||||||
|
if (cursorY + cellSize / 2 > canvas.height - bottomMargin) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制田字格
|
||||||
|
drawTianZiGrid(ctx, cursorX + cellSize / 2, cursorY, cellSize);
|
||||||
|
|
||||||
|
// 单元类型:0 预览;1..practice 逐笔画;最后 blanks 空白
|
||||||
|
const practiceMax = Math.min(strokes.length, 8);
|
||||||
|
if (unitIndex === 0) {
|
||||||
|
// 预览:全部笔画使用较深颜色
|
||||||
|
drawStrokes(
|
||||||
|
ctx,
|
||||||
|
strokes,
|
||||||
|
cursorX + (cellSize - cellSize) / 2,
|
||||||
|
cursorY - cellSize / 2,
|
||||||
|
cellSize,
|
||||||
|
strokes.length - 1,
|
||||||
|
'rgb(0,0,0)',
|
||||||
|
'#000',
|
||||||
|
1.6,
|
||||||
|
);
|
||||||
|
} else if (unitIndex <= practiceMax) {
|
||||||
|
// 逐笔画:累进显示到第 k 画,使用灰色/描边
|
||||||
|
drawStrokes(
|
||||||
|
ctx,
|
||||||
|
strokes,
|
||||||
|
cursorX + (cellSize - cellSize) / 2,
|
||||||
|
cursorY - cellSize / 2,
|
||||||
|
cellSize,
|
||||||
|
unitIndex - 1,
|
||||||
|
'rgb(184,184,184)',
|
||||||
|
'#999',
|
||||||
|
1.6,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 空白格:不画笔画
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前进到下一个单元
|
||||||
|
cursorX += cellSize + minGap;
|
||||||
|
usedInRow += 1;
|
||||||
|
unitIndex += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
drawLine(linY: number) {
|
||||||
|
const { canvas, ctx } = this;
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(80, linY);
|
||||||
|
ctx.lineTo(canvas.width - 80, linY);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
setPaper() {
|
||||||
|
const { ctx, canvas } = this;
|
||||||
|
const { pixelRatio: dpr } = wx.getWindowInfo();
|
||||||
|
let { width, height } = PAPER_SIZE[this.paperSize];
|
||||||
|
width = width * dpr;
|
||||||
|
height = height * dpr;
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
const canvas = this.canvas;
|
||||||
|
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WordDrawService;
|
||||||
@@ -24,12 +24,19 @@
|
|||||||
"miniprogram": {
|
"miniprogram": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"name": "pages/shape/index",
|
"name": "pages/wordDemo/index",
|
||||||
"pathName": "pages/shape/index",
|
"pathName": "pages/wordDemo/index",
|
||||||
"query": "",
|
"query": "",
|
||||||
"scene": null,
|
"scene": null,
|
||||||
"launchMode": "default"
|
"launchMode": "default"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "pages/shape/index",
|
||||||
|
"pathName": "pages/shape/index",
|
||||||
|
"query": "",
|
||||||
|
"launchMode": "default",
|
||||||
|
"scene": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "测试图形绘制",
|
"name": "测试图形绘制",
|
||||||
"pathName": "demoPages/shapePrint/index",
|
"pathName": "demoPages/shapePrint/index",
|
||||||
|
|||||||
Reference in New Issue
Block a user