feat:2.5.0初始化专注启蒙项目

This commit is contained in:
R524809
2025-12-15 17:00:06 +08:00
parent 62ba977a45
commit fefe437f0f
29 changed files with 3465 additions and 700 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"navigationBarTitleText": "涂鸦丫-专注力",
"navigationBarTitleText": "专注启蒙",
"navigationBarBackgroundColor": "#FFD719",
"homeButton": true,
"backgroundColor": "#F6F6F6",
+6 -1
View File
@@ -24,8 +24,13 @@ Page({
// 如果有page字段,跳转到对应页面
if (functionItem.page) {
let url = `/focusPages/${functionItem.page}/${functionItem.page}?id=${functionId}`;
// 如果有 mode 参数,添加到 URL 中
if (functionItem.mode) {
url += `&mode=${functionItem.mode}`;
}
wx.navigateTo({
url: `/focusPages/${functionItem.page}/${functionItem.page}?id=${functionId}`,
url,
});
} else {
wx.showToast({
+2 -2
View File
@@ -1,9 +1,9 @@
<view class="page-container">
<!-- 页面标题和描述 -->
<view class="math-page-header">
<view class="math-page-title">专注力训练</view>
<view class="math-page-title">专注启蒙</view>
<view class="math-page-subtitle">
选择功能,生成可打印的专注力训练涂鸦卡
选择功能,生成可打印的专注力、观察力启蒙涂鸦卡
</view>
</view>
+1 -1
View File
@@ -1,5 +1,5 @@
{
"navigationBarTitleText": "涂鸦丫-数学",
"navigationBarTitleText": "数感启蒙",
"navigationBarBackgroundColor": "#FFD719",
"homeButton": true,
"backgroundColor": "#F6F6F6",
-18
View File
@@ -1,18 +0,0 @@
{
"navigationBarTitleText": "涂鸦丫-认识图形",
"navigationBarBackgroundColor": "#FFD719",
"homeButton": true,
"backgroundColor": "#F6F6F6",
"enablePullDownRefresh": false,
"usingComponents": {
"van-divider": "@vant/weapp/divider/index",
"van-icon": "@vant/weapp/icon/index",
"van-popup": "@vant/weapp/popup/index",
"van-dialog": "@vant/weapp/dialog/index",
"toy-button": "../../ui/button/button",
"shape-card": "../../components/shape-card/shape-card",
"shape-picker": "../../components/shape-picker/shape-picker",
"color-picker": "../../components/color-picker/color-picker",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup"
}
}
-119
View File
@@ -1,119 +0,0 @@
page {
background-color: #f6f6f6;
}
.page-container {
background-color: #f6f6f6;
padding: 0 24rpx;
box-sizing: border-box;
.empty {
height: 120rpx;
}
.wrapper {
background-color: #fff;
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;
}
}
.btn-area {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 38rpx;
}
.shape-list {
margin: 38rpx 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 38rpx;
}
.shape-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;
}
}
}
.canvas-wrapper {
display: flex;
justify-content: center;
align-items: center;
min-height: 400rpx;
background: #f8f9fa;
border-radius: 12rpx;
border: 2rpx dashed #dee2e6;
}
.canvas-content {
max-width: 100%;
border-radius: 8rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.bottom-btn-box {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 120rpx;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
box-shadow: 0px -3.9rpx 5.2rpx rgba(0, 0, 0, 0.15);
padding: 0 24rpx;
box-sizing: border-box;
border-radius: 16rpx 16rpx 0 0;
}
-346
View File
@@ -1,346 +0,0 @@
import { SHAPES, ShapeCard } from '../../constants/shapes';
import { WATER_COLORS, PAPER_SIZE } from '../../constants/colors';
import ShapeDrawService from '../../service/shapeDrawService';
import { checkAndSaveImage } from '../../utils/saveImage';
import { shouldShowShareGuide } from '../../utils/shareGuide';
import tracker from '../../utils/tracker';
Page({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
shapeDrawService: null as ShapeDrawService | null,
timer: null as any,
data: {
shapeList: [] as ShapeCard[],
showSelectShapePopup: false,
selectedShapeIndex: -1,
showColorPopup: false,
currentIndex: 0,
currentColor: '',
isPCDevtool: false,
showShareDialog: false, // 显示分享引导弹窗
},
onLoad() {
// 判断是否在PC端开发工具上运行
// const systemInfo = wx.getDeviceInfo();
// if (systemInfo.platform === 'devtools') {
// this.setData({
// isPCDevtool: true,
// });
// }
this.refreshShapeCard();
},
onReady() {
const query = wx.createSelectorQuery();
query
.select('#canvasWrapper')
.boundingClientRect((rect) => {
if (rect) {
const { width, height } = PAPER_SIZE['A4'];
const boxWidth = rect.width;
const boxHeight = boxWidth / (width / height);
this.boxHeight = boxHeight;
this.boxWidth = boxWidth;
this.initCanvas(boxWidth, boxHeight);
}
})
.exec();
},
initCanvas(boxWidth: number, boxHeight: number) {
wx.createSelectorQuery()
.select('#canvasContent')
.fields({
node: true,
size: true,
})
.exec((res) => {
if (res[0] && res[0].node) {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
if (ctx) {
this.canvas = canvas;
this.ctx = ctx;
// const rect = res[0];
this.shapeDrawService = new ShapeDrawService(
canvas,
ctx,
);
// shapeDrawService.draw();
this.setData({ boxWidth, boxHeight });
this.drawCanvas();
}
}
});
},
/** 变更shapeList 并更新canvas */
drawCanvas(
updatedShapeList?: ShapeCard[],
callback: () => void = () => {},
) {
if (updatedShapeList) {
this.setData({ shapeList: updatedShapeList }, () => {
this.shapeDrawService?.draw(updatedShapeList!);
callback();
});
} else {
const shapeList = this.data.shapeList;
this.shapeDrawService?.draw(shapeList);
}
},
handleSelectShape() {
this.setData({
showSelectShapePopup: true,
selectedShapeIndex: -1,
});
},
openSelectShapePopup(e: any) {
const { index } = e.detail;
this.setData({
showSelectShapePopup: true,
selectedShapeIndex: index,
});
},
closeSelectShapePopup() {
this.setData({
showSelectShapePopup: false,
});
},
onChangeShape(e: any) {
const { shapes, singleMode, selectedShapeIndex } = e.detail;
const prevShapeList: ShapeCard[] = this.data.shapeList || [];
let newShapeList: ShapeCard[] = [];
if (singleMode) {
// 单选模式,只替换 selectedShapeIndex 对应的 shape,其他 shape 保持不变和原有颜色
newShapeList = prevShapeList.map((item, idx) => {
if (idx === selectedShapeIndex) {
// 替换为新选中的 shape,保留原有 fillColor
return {
...shapes[0],
fillColor: item.fillColor,
};
} else {
// 其他 shape 不变
return item;
}
});
} else {
// 多选模式,原有逻辑
// 获取所有可用颜色
const colorList = WATER_COLORS.basic12.map((item) => item.hex);
// 记录已被使用的颜色(只考虑当前 shapeList 中的 fillColor
const usedColors = prevShapeList
.map((item) => item.fillColor)
.filter(Boolean);
// 记录已分配的颜色,避免重复
const assignedColors = [...usedColors];
// 先将 shapes 转为 Map,方便查找
const prevShapeMap = new Map(
prevShapeList.map((item) => [item.id, item]),
);
// 随机打乱剩余可用颜色
function shuffle(arr: string[]) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
// 计算未被占用的颜色
let availableColors = colorList.filter(
(color) => !assignedColors.includes(color),
);
availableColors = shuffle(availableColors);
shapes.forEach((item: ShapeCard) => {
if (prevShapeMap.has(item.id)) {
// 已存在,保留原有 fillColor
newShapeList.push({
...item,
fillColor: prevShapeMap.get(item.id)!.fillColor,
});
} else {
// 新增,分配未被占用的颜色
const color = availableColors.shift() || colorList[0];
assignedColors.push(color);
newShapeList.push({
...item,
fillColor: color,
});
}
});
}
// 先关闭弹窗
this.setData(
{
showSelectShapePopup: false,
shapeList: newShapeList,
},
() => {
this.drawCanvas();
},
);
},
/**
* 随机生成6个图形
*/
refreshShapeCard() {
// 随机取6个图形
const shapesCopy = [...SHAPES];
for (let i = shapesCopy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shapesCopy[i], shapesCopy[j]] = [shapesCopy[j], shapesCopy[i]];
}
const selectedShapes = shapesCopy.slice(0, 6);
// 随机取6个颜色
const colorList = WATER_COLORS.basic12.map((item) => item.hex);
const colorsCopy = [...colorList];
for (let i = colorsCopy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[colorsCopy[i], colorsCopy[j]] = [colorsCopy[j], colorsCopy[i]];
}
const selectedColors = colorsCopy.slice(0, 6);
// 分配颜色到图形
const shapeList = selectedShapes.map((shape, idx) => ({
...shape,
fillColor: selectedColors[idx],
}));
// 更新数据
this.setData(
{
shapeList,
},
() => {
// 绘制canvas
this.drawCanvas();
},
);
},
onDelete(e: any) {
const { index } = e.detail;
const newShapeList = this.data.shapeList.filter((_, i) => i !== index);
this.setData(
{
shapeList: newShapeList,
},
() => {
// 绘制canvas
this.drawCanvas();
},
);
},
onColorTap(e: any) {
const { index, fillColor } = e.detail;
this.setData({
showColorPopup: true,
currentColor: fillColor,
selectedShapeIndex: index,
});
},
onCloseColorPopup() {
this.setData({
showColorPopup: false,
currentColor: '',
selectedShapeIndex: 0,
});
},
onChangeColor(e: any) {
const { color } = e.detail;
const { selectedShapeIndex } = this.data;
this.setData(
{
showColorPopup: false,
[`shapeList[${selectedShapeIndex}].fillColor`]: color,
},
() => {
// 绘制canvas
this.drawCanvas();
},
);
},
/** 下载打印 */
exportToPrint() {
if (!this.canvas || this.data.shapeList.length <= 0) {
return;
}
// 检查是否需要显示分享引导
if (shouldShowShareGuide()) {
this.setData({ showShareDialog: true });
return;
}
// 上报下载埋点
tracker.reportDownload('图形涂色');
// 直接下载
checkAndSaveImage(this.canvas);
},
/** 关闭分享引导弹窗 */
onCloseShareDialog() {
this.setData({ showShareDialog: false });
},
onShareAppMessage() {
// 上报分享埋点
tracker.reportShare('图形涂色');
return {
title: '涂鸦丫-涂色|识字|图形|打印',
path: '/pages/shape/index',
imageUrl:
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
};
},
onShareTimeline() {
// 上报分享埋点
tracker.reportShare('图形涂色');
return {
title: '涂鸦丫-涂色|识字|图形|打印',
query: '/pages/shape/index',
imageUrl:
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
};
},
/** 分享成功回调(由组件触发) */
onShareSuccess() {
// 只关闭弹窗,不触发下载
this.setData({ showShareDialog: false });
},
onUnload() {
if (this.timer) {
clearTimeout(this.timer);
}
},
});
-97
View File
@@ -1,97 +0,0 @@
<view class="page-container">
<view class="wrapper">
<text class="wrapper-title">自定义涂鸦卡</text>
<view class="btn-area">
<toy-button
type="green"
bind:click="handleSelectShape"
width="100%">
选择图形
</toy-button>
<toy-button
type="primary"
bind:click="refreshShapeCard"
width="100%"
icon="refresh"
icon-class-prefix="toy-icon">
随机生成
</toy-button>
</view>
<view class="shape-list" wx:if="{{shapeList.length > 0}}">
<shape-card
wx:for="{{ shapeList }}"
wx:for-item="item"
wx:for-index="index"
wx:key="{{index}}"
index="{{index}}"
svg="{{ item.svg }}"
fillColor="{{ item.fillColor }}"
bind:onShapeTap="openSelectShapePopup"
bind:onDelete="onDelete"
bind:onColorTap="onColorTap" />
</view>
<view class="shape-list-empty" wx:if="{{!shapeList.length}}">
<view class="empty-icon">
<text class="toy-icon toy-icon-guide"></text>
</view>
<view class="empty-title">还没有选择图形</view>
<view class="empty-desc"
>请点击上方“选择图形”或使用“随机生成”开始创建</view
>
</view>
</view>
<shape-picker
selectedShapeList="{{shapeList}}"
show="{{showSelectShapePopup}}"
selectedShapeIndex="{{selectedShapeIndex}}"
bind:onClose="closeSelectShapePopup"
bind:onChange="onChangeShape" />
<color-picker
show="{{showColorPopup}}"
currentColor="{{currentColor}}"
bind:onClose="onCloseColorPopup"
bind:onChange="onChangeColor" />
<view id="previewWrapper" class="wrapper">
<text class="wrapper-title">预览打印效果</text>
<view
wx:if="{{!isPCDevtool}}"
id="canvasWrapper"
class="canvas-wrapper">
<canvas
type="2d"
id="canvasContent"
class="canvas-content"
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
</view>
</view>
<view class="empty"></view>
</view>
<share-guide-popup
show="{{showShareDialog}}"
bind:onClose="onCloseShareDialog"
bind:onShareSuccess="onShareSuccess" />
<view class="bottom-btn-box">
<toy-button
openType="share"
type="green"
flat="{{true}}"
bind:click="onShareAppMessage"
width="220rpx"
height="80rpx"
icon="wechat"
icon-class-prefix="toy-icon">
分享
</toy-button>
<toy-button
type="primary"
flat="{{true}}"
bind:click="exportToPrint"
width="420rpx"
height="80rpx"
disabled="{{shapeList.length <= 0}}"
icon="download"
icon-class-prefix="toy-icon">
下载打印
</toy-button>
</view>