feat: 开发练字页
This commit is contained in:
@@ -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": "练字"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">自定义练字</text>
|
||||
<view class="word-input-box">
|
||||
<word-input bind:onConfirm="onConfirmInput" width="410rpx" />
|
||||
<toy-button
|
||||
type="green"
|
||||
bind:click="openSelectWordPopup"
|
||||
width="200rpx">
|
||||
选择文字
|
||||
</toy-button>
|
||||
</view>
|
||||
|
||||
<view class="word-card-box">
|
||||
<view class="word-cards-grid" wx:if="{{cardList.length > 0}}">
|
||||
<word-card
|
||||
wx:key="index"
|
||||
wx:for="{{cardList}}"
|
||||
wx:for-item="item"
|
||||
wx:for-index="index"
|
||||
word="{{item.word}}"
|
||||
color="{{item.color}}"
|
||||
bind:onDelete="deleteWordCard"
|
||||
bind:onColorTap="onColorTap" />
|
||||
</view>
|
||||
<view class="word-list-empty" wx:if="{{!cardList.length}}">
|
||||
<view class="empty-icon">
|
||||
<text class="toy-icon toy-icon-translate"></text>
|
||||
</view>
|
||||
<view class="empty-title">还没有添加文字</view>
|
||||
<view class="empty-desc"
|
||||
>请在上方输入文字或点击"选择文字"按钮<br />开始创建你的练字田字格吧!</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<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>
|
||||
<view wx:else class="empty-tip">请选择或输入汉字后生成田字格</view>
|
||||
</view>
|
||||
|
||||
<word-picker
|
||||
show="{{showSelectWordPopup}}"
|
||||
cardList="{{cardList}}"
|
||||
bind:onClose="closeSelectWordPopup"
|
||||
bind:onChange="onChangeWord" />
|
||||
</view>
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -61,3 +61,28 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 预览:田字格 -->
|
||||
<view class="preview-section">
|
||||
<text class="section-title">预览练字田字格</text>
|
||||
<view wx:if="{{rowHeaderWords.length > 0}}" class="tianzige">
|
||||
<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>
|
||||
<view wx:else class="empty-tip">请选择或输入汉字后生成田字格</view>
|
||||
</view>
|
||||
|
||||
Reference in New Issue
Block a user