feat: 英文字母描红开发
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "英语字母描红",
|
||||
"navigationBarBackgroundColor": "#F8F0E0",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#FEF6E7",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"nav-bar": "../../components3.0/nav-bar/nav-bar",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
@import '../../style/theme3.less';
|
||||
|
||||
@lt-share-green: #9cd343;
|
||||
|
||||
page {
|
||||
background-color: @bg-page;
|
||||
}
|
||||
|
||||
.lt-page {
|
||||
min-height: 100vh;
|
||||
padding: 0 @page-padding-x;
|
||||
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.lt-main {
|
||||
padding-top: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @section-gap;
|
||||
}
|
||||
|
||||
/* ===== 预览卡 ===== */
|
||||
.lt-preview-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lt-preview-card {
|
||||
background: @bg-white;
|
||||
border-radius: @radius;
|
||||
box-shadow: @shadow;
|
||||
padding: 48rpx @page-padding-inner-x 32rpx;
|
||||
border: @border;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.lt-preview-kicker {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: @text-gray;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.lt-canvas-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400rpx;
|
||||
background: @bg-gray;
|
||||
border-radius: @radius-sm;
|
||||
border: 2rpx dashed rgba(50, 46, 37, 0.12);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lt-canvas {
|
||||
max-width: 100%;
|
||||
border-radius: @radius-sm;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
/* ===== 参数面板 ===== */
|
||||
.lt-params {
|
||||
background: @bg-header;
|
||||
border-radius: @radius;
|
||||
padding: 40rpx @page-padding-inner-x;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40rpx;
|
||||
border: @border;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
.lt-params-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.lt-params-emoji {
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.lt-params-title {
|
||||
font-size: @fs-section-head-title;
|
||||
font-weight: 700;
|
||||
color: @text-title;
|
||||
letter-spacing: @ls-nav-title;
|
||||
}
|
||||
|
||||
.lt-field-label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: @text-secondary;
|
||||
margin-bottom: 16rpx;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
/* ===== 练习类型:2×2 网格选择器 ===== */
|
||||
.lt-mode-seg {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.lt-mode-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx 16rpx;
|
||||
border-radius: @radius-sm;
|
||||
background: @bg-white;
|
||||
border: 2rpx solid rgba(50, 46, 37, 0.1);
|
||||
box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.04);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.lt-mode-item--active {
|
||||
background: @brand;
|
||||
border-color: rgba(255, 215, 9, 0.55);
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
.lt-mode-item__text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: @text-secondary;
|
||||
line-height: 1.35;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lt-mode-item--active .lt-mode-item__text {
|
||||
color: @text-selected-btn;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ===== 字母选择网格:7 列 ===== */
|
||||
.lt-letter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.lt-letter-cell {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: @radius-sm;
|
||||
background: @bg-white;
|
||||
border: 2rpx solid rgba(50, 46, 37, 0.1);
|
||||
box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.04);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.lt-letter-cell--active {
|
||||
background: @brand;
|
||||
border-color: rgba(255, 215, 9, 0.55);
|
||||
box-shadow: @shadow;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.lt-letter-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: @text-secondary;
|
||||
}
|
||||
|
||||
.lt-letter-cell--active .lt-letter-text {
|
||||
color: @text-selected-btn;
|
||||
}
|
||||
|
||||
/* ===== 大小写切换 ===== */
|
||||
.lt-chip-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.lt-chip {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 20rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: @text-secondary;
|
||||
background: @bg-white;
|
||||
border-radius: @radius-sm;
|
||||
border: 2rpx solid rgba(50, 46, 37, 0.1);
|
||||
box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.04);
|
||||
}
|
||||
|
||||
.lt-chip--active {
|
||||
background: @brand;
|
||||
color: @text-selected-btn;
|
||||
font-weight: 700;
|
||||
border-color: rgba(255, 215, 9, 0.55);
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
/* ===== 换一批 ===== */
|
||||
.lt-shuffle-btn {
|
||||
width: 100%;
|
||||
padding: 28rpx 24rpx;
|
||||
margin: 0;
|
||||
border-radius: @radius;
|
||||
border: 6rpx solid @bg-header;
|
||||
background: transparent;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: @text-secondary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lt-shuffle-btn:active {
|
||||
background: @bg-header;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* ===== 底栏 ===== */
|
||||
.lt-bottom-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx @page-padding-x;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-top: @border;
|
||||
box-shadow: 0 -8rpx 32rpx rgba(50, 46, 37, 0.06);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.lt-bottom-share,
|
||||
.lt-bottom-save {
|
||||
margin: 0;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
border-radius: @radius-sm;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lt-bottom-share {
|
||||
flex: 0 0 35%;
|
||||
width: 35%;
|
||||
background: @lt-share-green;
|
||||
color: @text-white;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
.lt-bottom-save {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: @brand;
|
||||
color: @text-selected-btn;
|
||||
font-weight: 800;
|
||||
box-shadow: @shadow-lg;
|
||||
}
|
||||
|
||||
.lt-bottom-save[disabled] {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.lt-bottom-icon {
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.lt-spacer-bottom {
|
||||
height: 24rpx;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import LetterTracingDraw from '../shared/service/letterTracingDraw';
|
||||
import { createPage, type CanvasDataState } from '../../base/pageMixin';
|
||||
import { defaultShareConfig } from '../../config/config';
|
||||
import { getWorksheetById } from '../../core/data/worksheets';
|
||||
import {
|
||||
generateLetterTracing,
|
||||
mergeLetterTracingConfig,
|
||||
type LetterTracingGeneratorConfig,
|
||||
type LetterTracingMode,
|
||||
} from '../../core/generators/letter-tracing-generator';
|
||||
import { LETTERS_UPPER } from '../../core/data/alphabet';
|
||||
|
||||
const PAGE_META: Record<string, { title: string; desc: string }> = {
|
||||
'letter-tracing-single': {
|
||||
title: '字母学习页',
|
||||
desc: '单字母配图与描红',
|
||||
},
|
||||
'letter-tracing-full': {
|
||||
title: '26 字母描红',
|
||||
desc: '整页字母书写练习',
|
||||
},
|
||||
'letter-tracing-upper-lower': {
|
||||
title: '大小写字母对照',
|
||||
desc: '一页认识 A-Z 与 a-z',
|
||||
},
|
||||
'letter-tracing-two-column': {
|
||||
title: '字母描红(两列)',
|
||||
desc: 'A–M / N–Z 两列练习',
|
||||
},
|
||||
};
|
||||
|
||||
function pageInfoLookup(id: string) {
|
||||
return PAGE_META[id];
|
||||
}
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{ id: 'letter-tracing-single', label: '单字母学习页' },
|
||||
{ id: 'letter-tracing-full', label: '整页描红' },
|
||||
{ id: 'letter-tracing-upper-lower', label: '大小写对照' },
|
||||
{ id: 'letter-tracing-two-column', label: '两列描红' },
|
||||
] as const;
|
||||
|
||||
type PageData = CanvasDataState & {
|
||||
worksheetId: string;
|
||||
traceMode: LetterTracingMode;
|
||||
selectedLetter: string;
|
||||
letterCaseLower: boolean;
|
||||
showLetterPicker: boolean;
|
||||
showCaseToggle: boolean;
|
||||
showNextLetter: boolean;
|
||||
letterGrid: string[];
|
||||
modeOptions: ReadonlyArray<{ id: string; label: string }>;
|
||||
};
|
||||
|
||||
createPage(
|
||||
{
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as LetterTracingDraw | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '字母描红',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
worksheetId: 'letter-tracing-single',
|
||||
traceMode: 'single-letter',
|
||||
selectedLetter: 'A',
|
||||
letterCaseLower: false,
|
||||
showLetterPicker: true,
|
||||
showCaseToggle: false,
|
||||
showNextLetter: true,
|
||||
letterGrid: LETTERS_UPPER,
|
||||
modeOptions: [...MODE_OPTIONS],
|
||||
} as unknown as PageData,
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const worksheetId =
|
||||
options.id && PAGE_META[options.id]
|
||||
? options.id
|
||||
: 'letter-tracing-single';
|
||||
const def = getWorksheetById(worksheetId);
|
||||
const base = (def?.generatorConfig ??
|
||||
{}) as Partial<LetterTracingGeneratorConfig>;
|
||||
const merged = mergeLetterTracingConfig(base, {});
|
||||
|
||||
const traceMode = merged.mode;
|
||||
const showLetterPicker = traceMode === 'single-letter';
|
||||
const showCaseToggle =
|
||||
traceMode === 'full-alphabet' || traceMode === 'two-column';
|
||||
const showNextLetter = traceMode === 'single-letter';
|
||||
|
||||
this.setData({
|
||||
worksheetId,
|
||||
functionId: worksheetId,
|
||||
traceMode,
|
||||
selectedLetter: normalizePick(merged.selectedLetter),
|
||||
letterCaseLower: merged.letterCase === 'lower',
|
||||
showLetterPicker,
|
||||
showCaseToggle,
|
||||
showNextLetter,
|
||||
});
|
||||
|
||||
this.initPageInfo(worksheetId, PAGE_META[worksheetId]?.title ?? '字母描红');
|
||||
},
|
||||
|
||||
onSelectMode(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id as string | undefined;
|
||||
if (!id || id === this.data.worksheetId) return;
|
||||
wx.redirectTo({
|
||||
url: `/englishPages/letterTracing/letterTracing?id=${id}`,
|
||||
});
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, unknown>,
|
||||
) => new LetterTracingDraw(canvas, ctx, options),
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: this.data.subTitle || '',
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
buildRuntimeConfig(): LetterTracingGeneratorConfig {
|
||||
const def = getWorksheetById(this.data.worksheetId);
|
||||
const base = (def?.generatorConfig ??
|
||||
{}) as Partial<LetterTracingGeneratorConfig>;
|
||||
const letterCase =
|
||||
this.data.traceMode === 'single-letter'
|
||||
? (base.letterCase ?? 'upper')
|
||||
: this.data.letterCaseLower
|
||||
? 'lower'
|
||||
: 'upper';
|
||||
return mergeLetterTracingConfig(base, {
|
||||
selectedLetter: this.data.selectedLetter,
|
||||
letterCase,
|
||||
});
|
||||
},
|
||||
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService) return;
|
||||
try {
|
||||
const config = this.buildRuntimeConfig();
|
||||
const data = generateLetterTracing(config);
|
||||
await (this.drawService as LetterTracingDraw).draw(data);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (e) {
|
||||
console.error('letterTracing draw failed', e);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
onSelectLetter(e: WechatMiniprogram.TouchEvent) {
|
||||
const letter = e.currentTarget.dataset.letter as string;
|
||||
if (!letter) return;
|
||||
this.setData({ selectedLetter: letter });
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
onSelectCaseUpper() {
|
||||
this.setData({ letterCaseLower: false });
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
onSelectCaseLower() {
|
||||
this.setData({ letterCaseLower: true });
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
onNextLetter() {
|
||||
const list = LETTERS_UPPER;
|
||||
const i = list.indexOf(this.data.selectedLetter);
|
||||
const next = list[(i + 1) % list.length];
|
||||
this.setData({ selectedLetter: next });
|
||||
this.drawCanvas();
|
||||
},
|
||||
},
|
||||
{
|
||||
shareConfig: defaultShareConfig,
|
||||
pageInfoLookup,
|
||||
},
|
||||
);
|
||||
|
||||
function normalizePick(c?: string): string {
|
||||
if (!c || c.length === 0) return 'A';
|
||||
return c.trim().toUpperCase().charAt(0);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<nav-bar title="英语字母描红" />
|
||||
|
||||
<view class="lt-page">
|
||||
<view class="lt-main">
|
||||
<view class="lt-preview-wrap">
|
||||
<view class="lt-preview-card">
|
||||
<text class="lt-preview-kicker">打印预览</text>
|
||||
<view id="canvasWrapper" class="lt-canvas-wrap">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="lt-canvas"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="lt-params">
|
||||
<view class="lt-params-head">
|
||||
<text class="lt-params-emoji">⚙️</text>
|
||||
<text class="lt-params-title">参数设置</text>
|
||||
</view>
|
||||
|
||||
<view class="param-block">
|
||||
<text class="lt-field-label">练习类型</text>
|
||||
<view class="lt-mode-seg">
|
||||
<view
|
||||
wx:for="{{modeOptions}}"
|
||||
wx:key="id"
|
||||
class="lt-mode-item {{worksheetId === item.id ? 'lt-mode-item--active' : ''}}"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="onSelectMode">
|
||||
<text class="lt-mode-item__text">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{showLetterPicker}}" class="param-block">
|
||||
<text class="lt-field-label">选择字母</text>
|
||||
<view class="lt-letter-grid">
|
||||
<view
|
||||
wx:for="{{letterGrid}}"
|
||||
wx:key="*this"
|
||||
class="lt-letter-cell {{selectedLetter === item ? 'lt-letter-cell--active' : ''}}"
|
||||
data-letter="{{item}}"
|
||||
bind:tap="onSelectLetter">
|
||||
<text class="lt-letter-text">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{showCaseToggle}}" class="param-block">
|
||||
<text class="lt-field-label">大小写</text>
|
||||
<view class="lt-chip-row">
|
||||
<view
|
||||
class="lt-chip {{!letterCaseLower ? 'lt-chip--active' : ''}}"
|
||||
bind:tap="onSelectCaseUpper">
|
||||
大写 A–Z
|
||||
</view>
|
||||
<view
|
||||
class="lt-chip {{letterCaseLower ? 'lt-chip--active' : ''}}"
|
||||
bind:tap="onSelectCaseLower">
|
||||
小写 a–z
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
wx:if="{{showNextLetter}}"
|
||||
hover-class="none"
|
||||
class="lt-shuffle-btn"
|
||||
bindtap="onNextLetter">
|
||||
🔄 换一批
|
||||
</button>
|
||||
|
||||
<draw-ad type="mathDraw"></draw-ad>
|
||||
<view class="lt-spacer-bottom"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="lt-bottom-bar">
|
||||
<button class="lt-bottom-share" hover-class="none" open-type="share">
|
||||
<text class="lt-bottom-icon">↗</text>
|
||||
分享
|
||||
</button>
|
||||
<button
|
||||
class="lt-bottom-save"
|
||||
hover-class="none"
|
||||
disabled="{{!hasContent}}"
|
||||
bindtap="exportToPrint">
|
||||
<text class="lt-bottom-icon">⬇</text>
|
||||
保存到相册
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
Reference in New Issue
Block a user