diff --git a/miniprogram/app.json b/miniprogram/app.json
index 711aeb2..2fc6fbf 100644
--- a/miniprogram/app.json
+++ b/miniprogram/app.json
@@ -4,6 +4,7 @@
"pages/shape/index",
"pages/debug/debug",
"pages/wordDemo/index",
+ "pages/copyBook/index",
"pages/buttonTest/buttonTest"
],
"window": {},
@@ -43,7 +44,7 @@
"text": "图形"
},
{
- "pagePath": "pages/wordDemo/index",
+ "pagePath": "pages/copyBook/index",
"iconPath": "assets/tabBar/icon-word.png",
"selectedIconPath": "assets/tabBar/icon-word-active.png",
"text": "练字"
diff --git a/miniprogram/pages/copyBook/index.json b/miniprogram/pages/copyBook/index.json
new file mode 100644
index 0000000..b5d9f55
--- /dev/null
+++ b/miniprogram/pages/copyBook/index.json
@@ -0,0 +1,10 @@
+{
+ "navigationBarTitleText": "练字",
+ "usingComponents": {
+ "toy-button": "/ui/button/button",
+ "van-icon": "@vant/weapp/icon/index",
+ "word-input": "/components/word-input/word-input",
+ "word-card": "/components/word-card/word-card",
+ "word-picker": "/components/word-picker/word-picker"
+ }
+}
diff --git a/miniprogram/pages/copyBook/index.less b/miniprogram/pages/copyBook/index.less
new file mode 100644
index 0000000..45e8d68
--- /dev/null
+++ b/miniprogram/pages/copyBook/index.less
@@ -0,0 +1,147 @@
+page {
+ background-color: #f6f6f6;
+}
+
+.page-container {
+ background-color: #f6f6f6;
+ padding: 0 24rpx;
+ box-sizing: border-box;
+}
+
+.wrapper {
+ background-color: #ffffff;
+ border-radius: 20rpx;
+ padding: 36rpx 24rpx;
+ box-shadow: 0 6rpx 8rpx rgba(0, 0, 0, 0.15);
+ margin: 30rpx 0;
+ display: flex;
+ flex-direction: column;
+
+ .wrapper-title {
+ font-size: 32rpx;
+ color: #141414;
+ margin-bottom: 36rpx;
+ font-weight: bold;
+ text-align: center;
+ }
+
+ .word-card-box {
+ padding: 34rpx 0;
+
+ .word-list-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 60rpx 40rpx;
+ text-align: center;
+
+ .empty-icon {
+ width: 120rpx;
+ height: 120rpx;
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 32rpx;
+ box-shadow: 0 4rpx 12rpx rgba(59, 130, 246, 0.15);
+
+ .toy-icon {
+ font-size: 48rpx;
+ color: #3b82f6;
+ }
+ }
+
+ .empty-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #374151;
+ margin-bottom: 16rpx;
+ line-height: 1.4;
+ }
+
+ .empty-desc {
+ font-size: 26rpx;
+ color: #6b7280;
+ line-height: 1.6;
+ margin-bottom: 32rpx;
+ max-width: 400rpx;
+ }
+ }
+
+ .word-cards-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 40rpx;
+ }
+ }
+
+ .word-input-box {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+}
+
+.tianzige {
+ display: grid;
+ grid-auto-rows: min-content;
+ row-gap: 12rpx;
+}
+
+.tzg-row {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ column-gap: 12rpx;
+}
+
+.tzg-cell {
+ position: relative;
+ background: #fff;
+ border: 4rpx solid #333;
+ height: 120rpx;
+ border-radius: 6rpx;
+ overflow: hidden;
+}
+
+.tzg-word {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 72rpx;
+ color: #222;
+ font-family: 'Kaiti', 'KaiTi', 'STKaiti', 'FZKai-Z03S', serif;
+}
+
+.mid-line {
+ position: absolute;
+ background: repeating-linear-gradient(to right,
+ rgba(0, 0, 0, 0.25),
+ rgba(0, 0, 0, 0.25) 2rpx,
+ transparent 2rpx,
+ transparent 6rpx);
+}
+
+.mid-line.h {
+ left: 0;
+ right: 0;
+ top: 50%;
+ height: 2rpx;
+ transform: translateY(-50%);
+}
+
+.mid-line.v {
+ top: 0;
+ bottom: 0;
+ left: 50%;
+ width: 2rpx;
+ transform: translateX(-50%);
+}
+
+.empty-tip {
+ text-align: center;
+ color: #999;
+ font-size: 28rpx;
+}
\ No newline at end of file
diff --git a/miniprogram/pages/copyBook/index.ts b/miniprogram/pages/copyBook/index.ts
new file mode 100644
index 0000000..11e9e4c
--- /dev/null
+++ b/miniprogram/pages/copyBook/index.ts
@@ -0,0 +1,91 @@
+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 });
+ },
+});
+
+
diff --git a/miniprogram/pages/copyBook/index.wxml b/miniprogram/pages/copyBook/index.wxml
new file mode 100644
index 0000000..0b372b6
--- /dev/null
+++ b/miniprogram/pages/copyBook/index.wxml
@@ -0,0 +1,67 @@
+
+
+ 自定义练字
+
+
+
+ 选择文字
+
+
+
+
+
+
+
+
+
+
+
+ 还没有添加文字
+ 请在上方输入文字或点击"选择文字"按钮
开始创建你的练字田字格吧!
+
+
+
+
+
+ 预览练字田字格
+
+
+
+ {{rowHeaderWords[rowIndex]}}
+
+
+
+
+
+ 请选择或输入汉字后生成田字格
+
+
+
+
diff --git a/miniprogram/pages/wordDemo/index.less b/miniprogram/pages/wordDemo/index.less
index d35ce8e..1e30e29 100644
--- a/miniprogram/pages/wordDemo/index.less
+++ b/miniprogram/pages/wordDemo/index.less
@@ -10,6 +10,84 @@ page {
overflow-y: auto;
}
+.preview-section {
+ margin-top: 24rpx;
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 24rpx;
+ box-shadow: 0 6rpx 8rpx rgba(0, 0, 0, 0.15);
+}
+
+.section-title {
+ display: block;
+ text-align: center;
+ font-size: 32rpx;
+ font-weight: bold;
+ margin-bottom: 24rpx;
+}
+
+.tianzige {
+ display: grid;
+ grid-auto-rows: min-content;
+ row-gap: 12rpx;
+}
+
+.tzg-row {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ column-gap: 12rpx;
+}
+
+.tzg-cell {
+ position: relative;
+ background: #fff;
+ border: 4rpx solid #333;
+ height: 120rpx;
+ border-radius: 6rpx;
+ overflow: hidden;
+}
+
+.tzg-word {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 72rpx;
+ color: #222;
+ font-family: 'Kaiti', 'KaiTi', 'STKaiti', 'FZKai-Z03S', serif;
+}
+
+.mid-line {
+ position: absolute;
+ background: repeating-linear-gradient(to right,
+ rgba(0, 0, 0, 0.25),
+ rgba(0, 0, 0, 0.25) 2rpx,
+ transparent 2rpx,
+ transparent 6rpx);
+}
+
+.mid-line.h {
+ left: 0;
+ right: 0;
+ top: 50%;
+ height: 2rpx;
+ transform: translateY(-50%);
+}
+
+.mid-line.v {
+ top: 0;
+ bottom: 0;
+ left: 50%;
+ width: 2rpx;
+ transform: translateX(-50%);
+}
+
+.empty-tip {
+ text-align: center;
+ color: #999;
+ font-size: 28rpx;
+}
+
/* 输入区域样式 */
.input-section {
background-color: #fff;
diff --git a/miniprogram/pages/wordDemo/index.ts b/miniprogram/pages/wordDemo/index.ts
index 55ac4dd..c6f53b7 100644
--- a/miniprogram/pages/wordDemo/index.ts
+++ b/miniprogram/pages/wordDemo/index.ts
@@ -3,10 +3,17 @@ Page({
inputWord: '', // 输入框中的汉字
wordList: [] as string[], // 选中的汉字列表
availableWords: [] as string[], // 从grade3.json中提取的前30个汉字
+ // 田字格配置
+ rows: 10,
+ cols: 12,
+ rowsArray: [] as number[],
+ colsArray: [] as number[],
+ rowHeaderWords: [] as string[],
},
onLoad() {
this.loadGrade3Words();
+ this.initGrid();
},
// 加载grade3.json中的前30个汉字
@@ -70,6 +77,8 @@ Page({
this.setData({
wordList: newWordList,
inputWord: '' // 清空输入框
+ }, () => {
+ this.updateRowHeaderWords();
});
wx.showToast({
@@ -128,6 +137,8 @@ Page({
const newWordList = wordList.filter((_, i) => i !== index);
this.setData({
wordList: newWordList
+ }, () => {
+ this.updateRowHeaderWords();
});
wx.showToast({
title: '已移除',
@@ -139,10 +150,30 @@ Page({
onClearAll() {
this.setData({
wordList: []
+ }, () => {
+ this.updateRowHeaderWords();
});
wx.showToast({
title: '已清空',
icon: 'success'
});
},
+
+ // 初始化田字格行列数组
+ 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, wordList } = this.data as any;
+ const rowHeaderWords: string[] = [];
+ for (let i = 0; i < rows; i++) {
+ rowHeaderWords.push(wordList[i] || '');
+ }
+ this.setData({ rowHeaderWords });
+ },
});
diff --git a/miniprogram/pages/wordDemo/index.wxml b/miniprogram/pages/wordDemo/index.wxml
index 4132e2d..2bf6a65 100644
--- a/miniprogram/pages/wordDemo/index.wxml
+++ b/miniprogram/pages/wordDemo/index.wxml
@@ -61,3 +61,28 @@
+
+
+
+ 预览练字田字格
+
+
+
+ {{rowHeaderWords[rowIndex]}}
+
+
+
+
+
+ 请选择或输入汉字后生成田字格
+