feat:V2.5.3 修改下载分享弹窗问题、新增连连看

This commit is contained in:
R524809
2025-12-17 18:00:22 +08:00
parent 4a3ad82d08
commit a8ed4a2cfc
11 changed files with 700 additions and 6 deletions
+2 -1
View File
@@ -29,7 +29,8 @@
"shape/shape",
"positionColoring/positionColoring",
"shapeSymbol/shapeSymbol",
"colorPattern/colorPattern"
"colorPattern/colorPattern",
"matchConnect/matchConnect"
],
"independent": false
}
+6
View File
@@ -11,6 +11,12 @@ App<IAppOption>({
onLaunch() {
const accountInfo = wx.getAccountInfoSync();
const env = accountInfo.miniProgram.envVersion || 'release';
/**
* 设置环境变量
* 开发环境:develop
* 体验环境:trial
* 正式环境:release
*/
this.globalData.env = env;
// 从 localStorage 读取 UUID,如果没有则生成并存储
+8
View File
@@ -40,6 +40,14 @@ export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [
desc: '观察颜色规律,在空白图形中涂上颜色',
icon: '🎨',
},
// 连连看
{
id: 'match-connect',
page: 'matchConnect',
title: '连连看',
desc: '快来根据物品连一连吧!',
icon: '🔗',
},
// 格子仿画
{
id: 'grid-drawing-3x3',
@@ -0,0 +1,12 @@
{
"navigationBarTitleText": "连连看",
"navigationBarBackgroundColor": "#FFD719",
"homeButton": true,
"backgroundColor": "#F6F6F6",
"enablePullDownRefresh": false,
"usingComponents": {
"toy-button": "../../ui/button/button",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons"
}
}
@@ -0,0 +1,97 @@
import MatchConnectDraw from '../shared/service/matchConnectDraw';
import { createFocusPage } from '../shared/common/focusPageMixin';
import { MatchConnectData } from '../shared/service/matchConnectDraw';
createFocusPage({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as MatchConnectDraw | null,
matchData: null as MatchConnectData | null,
data: {
pageTitle: '连连看',
functionId: '',
hasContent: false,
showShareDialog: false,
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'match-connect';
this.setData({
functionId,
});
this.initPageInfo(functionId, '连连看');
},
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new MatchConnectDraw(canvas, ctx, options);
},
drawServiceOptions: {
title: this.data.pageTitle,
subTitle: '快来根据物品连一连吧!',
functionId: this.data.functionId,
},
onCanvasReady: () => {
// 初始随机生成
this.onRandom();
},
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.matchData) {
return;
}
try {
await this.drawService.draw(this.matchData);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
// 从33张图片中随机选择5张不重复的图片
const allImageIndices = Array.from({ length: 33 }, (_, i) => i + 1);
const shuffled = [...allImageIndices].sort(() => Math.random() - 0.5);
const selectedImages = shuffled.slice(0, 5);
// 创建参考序列(顺序)
const referenceSequence = [...selectedImages];
// 创建6个框,每个框包含5张图片的索引(位置由绘制服务随机生成)
const boxes: Array<{
imageIndices: number[];
}> = [];
for (let boxIndex = 0; boxIndex < 6; boxIndex++) {
// 每个框使用相同的5张图片
boxes.push({
imageIndices: [...selectedImages],
});
}
this.matchData = {
referenceSequence,
boxes,
};
this.drawCanvas();
},
});
@@ -0,0 +1,37 @@
<view class="page-container">
<view class="wrapper">
<text class="wrapper-title">预览打印效果</text>
<!-- 预览打印效果 -->
<view id="canvasWrapper" class="canvas-wrapper">
<canvas
type="2d"
id="canvasContent"
class="canvas-content"
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
</view>
<!-- 随机生成按钮 -->
<view class="random-button-area">
<toy-button
class="random-button"
type="primary"
bind:click="onRandom"
width="100%"
height="80rpx"
icon="refresh"
icon-class-prefix="toy-icon">
随机生成
</toy-button>
</view>
</view>
<view class="empty"></view>
</view>
<math-bottom-buttons
disabled="{{!hasContent}}"
bind:share="onShareAppMessage"
bind:export="exportToPrint" />
<share-guide-popup
show="{{showShareDialog}}"
bind:onClose="onCloseShareDialog"
bind:onShareSuccess="onShareSuccess" />
@@ -0,0 +1 @@
@import '../../base/baseDrawPage.wxss';
@@ -0,0 +1,461 @@
/**
* 连连看绘制服务
*/
import { BaseDrawService } from '../../../service/baseDraw';
import { getImage } from '../../../utils/index';
/**
* 连连看数据
*/
export interface MatchConnectData {
/** 参考序列:5张图片的索引数组 */
referenceSequence: number[];
/** 6个框,每个框包含5张图片的索引(位置由绘制服务随机生成) */
boxes: Array<{
imageIndices: number[];
}>;
}
/**
* 连连看绘制服务
*/
class MatchConnectDraw extends BaseDrawService {
matchData: MatchConnectData | null = null;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
}
/**
* 绘制连连看内容
*/
async draw(matchData: MatchConnectData) {
if (!matchData || !matchData.referenceSequence || !matchData.boxes) {
return;
}
this.setPrintConfig();
this.matchData = matchData;
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
}
// 绘制内容区域
this.drawDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
matchData: this.matchData,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
/**
* 生成不重叠的图片位置
* @param count 图片数量
* @param contentWidth 内容区域宽度
* @param contentHeight 内容区域高度
* @param imageSize 图片大小
* @param imagePadding 图片padding
* @returns 图片位置数组,每个位置包含x和y(相对于内容区域的坐标)
*/
private generateNonOverlappingPositions(
count: number,
contentWidth: number,
contentHeight: number,
imageSize: number,
imagePadding: number,
): Array<{ x: number; y: number }> {
// 计算最大可放置位置(图片左上角的位置)
const maxX = contentWidth - imageSize;
const maxY = contentHeight - imageSize;
const positions: Array<{ x: number; y: number }> = [];
const placedRects: Array<{
left: number;
top: number;
right: number;
bottom: number;
}> = [];
/**
* 检查两个矩形是否重叠
*/
const isRectOverlap = (
rect1: {
left: number;
top: number;
right: number;
bottom: number;
},
rect2: {
left: number;
top: number;
right: number;
bottom: number;
},
): boolean => {
return !(
rect1.right < rect2.left ||
rect1.left > rect2.right ||
rect1.bottom < rect2.top ||
rect1.top > rect2.bottom
);
};
// 计算图片的实际占用大小(包含padding)
const occupiedSize = imageSize + imagePadding * 2;
// 生成候选位置网格,然后随机打乱
// 这样可以确保位置分布更均匀,同时保持随机性
const gridCols = Math.max(3, Math.floor(contentWidth / occupiedSize));
const gridRows = Math.max(2, Math.floor(contentHeight / occupiedSize));
const cellWidth = maxX / Math.max(1, gridCols - 1);
const cellHeight = maxY / Math.max(1, gridRows - 1);
// 生成所有可能的网格位置
const candidatePositions: Array<{ x: number; y: number }> = [];
for (let row = 0; row < gridRows; row++) {
for (let col = 0; col < gridCols; col++) {
const x = col * cellWidth;
const y = row * cellHeight;
candidatePositions.push({ x, y });
}
}
// 随机打乱候选位置
const shuffledCandidates = [...candidatePositions].sort(
() => Math.random() - 0.5,
);
// 从打乱的候选位置中选择不重叠的位置
for (let i = 0; i < count; i++) {
let found = false;
// 先尝试从网格候选位置中选择
for (const candidate of shuffledCandidates) {
const x = candidate.x;
const y = candidate.y;
// 计算当前图片的实际占用区域(包含padding)
const currentRect = {
left: x - imagePadding,
top: y - imagePadding,
right: x + imageSize + imagePadding,
bottom: y + imageSize + imagePadding,
};
// 确保不超出内容区域的范围
if (
currentRect.left < 0 ||
currentRect.top < 0 ||
currentRect.right > contentWidth ||
currentRect.bottom > contentHeight
) {
continue;
}
// 检查是否与已放置的图片重叠
let overlaps = false;
for (const placedRect of placedRects) {
if (isRectOverlap(currentRect, placedRect)) {
overlaps = true;
break;
}
}
if (!overlaps) {
// 存储实际占用区域
placedRects.push({
left: x - imagePadding,
top: y - imagePadding,
right: x + imageSize + imagePadding,
bottom: y + imageSize + imagePadding,
});
positions.push({ x, y });
found = true;
break;
}
}
// 如果网格位置都不行,尝试完全随机的位置
if (!found) {
let attempts = 0;
const maxAttempts = 1000;
while (attempts < maxAttempts) {
// 生成随机位置,添加一些随机偏移
const randomOffsetX =
(Math.random() - 0.5) * cellWidth * 0.5;
const randomOffsetY =
(Math.random() - 0.5) * cellHeight * 0.5;
const baseX = Math.random() * maxX;
const baseY = Math.random() * maxY;
const x = Math.max(
0,
Math.min(maxX, baseX + randomOffsetX),
);
const y = Math.max(
0,
Math.min(maxY, baseY + randomOffsetY),
);
// 计算当前图片的实际占用区域(包含padding)
const currentRect = {
left: x - imagePadding,
top: y - imagePadding,
right: x + imageSize + imagePadding,
bottom: y + imageSize + imagePadding,
};
// 确保不超出内容区域的范围
if (
currentRect.left < 0 ||
currentRect.top < 0 ||
currentRect.right > contentWidth ||
currentRect.bottom > contentHeight
) {
attempts++;
continue;
}
// 检查是否与已放置的图片重叠
let overlaps = false;
for (const placedRect of placedRects) {
if (isRectOverlap(currentRect, placedRect)) {
overlaps = true;
break;
}
}
if (!overlaps) {
// 存储实际占用区域
placedRects.push({
left: x - imagePadding,
top: y - imagePadding,
right: x + imageSize + imagePadding,
bottom: y + imageSize + imagePadding,
});
positions.push({ x, y });
found = true;
break;
}
attempts++;
}
}
// 最后的后备方案:使用网格布局,但添加随机偏移
if (!found) {
const gridCols = 3;
const gridRows = 2;
const gridCellWidth = maxX / Math.max(1, gridCols - 1);
const gridCellHeight = maxY / Math.max(1, gridRows - 1);
const baseX = (i % gridCols) * gridCellWidth;
const baseY = Math.floor(i / gridCols) * gridCellHeight;
// 添加随机偏移,但确保不重叠
const offsetX = (Math.random() - 0.5) * gridCellWidth * 0.3;
const offsetY = (Math.random() - 0.5) * gridCellHeight * 0.3;
const x = Math.max(0, Math.min(maxX, baseX + offsetX));
const y = Math.max(0, Math.min(maxY, baseY + offsetY));
positions.push({ x, y });
}
}
// 最后再次随机打乱位置顺序,增加随机性
return positions.sort(() => Math.random() - 0.5);
}
/**
* 绘制箭头
*/
private drawArrow(
ctx: RenderingContext,
x1: number,
y1: number,
x2: number,
y2: number,
) {
ctx.strokeStyle = '#FF69B4'; // 粉色
ctx.lineWidth = 2;
ctx.setLineDash([]); // 实线
// 绘制箭头线
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
// 绘制箭头头部
const angle = Math.atan2(y2 - y1, x2 - x1);
const arrowLength = 8;
const arrowAngle = Math.PI / 6; // 30度
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(
x2 - arrowLength * Math.cos(angle - arrowAngle),
y2 - arrowLength * Math.sin(angle - arrowAngle),
);
ctx.moveTo(x2, y2);
ctx.lineTo(
x2 - arrowLength * Math.cos(angle + arrowAngle),
y2 - arrowLength * Math.sin(angle + arrowAngle),
);
ctx.stroke();
}
/**
* 绘制内容区域
*/
private async drawContent(params: {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
matchData: MatchConnectData;
canvasWidth: number;
startY: number;
}): Promise<void> {
const { ctx, matchData, canvasWidth } = params;
let { startY } = params;
startY = startY + 25;
const margin = 40; // 左右边距
const imageSize = 50; // 图片大小
const arrowLength = 30; // 箭头长度
const arrowSpacing = 8; // 箭头左右间距
// 计算参考序列的总宽度
// 总宽度 = 所有图片宽度 + 所有箭头长度 + 所有箭头间距(左右各8px)
const arrowCount = matchData.referenceSequence.length - 1;
const referenceWidth =
matchData.referenceSequence.length * imageSize +
arrowCount * arrowLength +
arrowCount * arrowSpacing * 2; // 每个箭头左右各8px
// 绘制参考序列(上面部分)- 居中显示
const referenceStartX =
margin + (canvasWidth - margin * 2 - referenceWidth) / 2;
const referenceY = startY;
// 绘制参考序列的图片和箭头
let currentX = referenceStartX;
for (let i = 0; i < matchData.referenceSequence.length; i++) {
const imageIndex = matchData.referenceSequence[i];
// 绘制图片
try {
const imagePath = `/focusPages/shared/aeests/material/${imageIndex}.png`;
const image = await getImage(this.canvas, imagePath);
ctx.drawImage(
image,
currentX,
referenceY,
imageSize,
imageSize,
);
} catch (error) {
console.error(`加载参考图片失败: ${imageIndex}`, error);
}
// 如果不是最后一张,绘制箭头
if (i < matchData.referenceSequence.length - 1) {
// 箭头左边间距8px
const arrowStartX = currentX + imageSize + arrowSpacing;
const arrowEndX = arrowStartX + arrowLength;
const arrowY = referenceY + imageSize / 2;
this.drawArrow(ctx, arrowStartX, arrowY, arrowEndX, arrowY);
// 箭头右边间距8px,然后开始下一张图片
currentX = arrowEndX + arrowSpacing;
} else {
currentX += imageSize;
}
}
// 绘制虚线分割线
const dividerY = referenceY + imageSize + 30;
this.drawDashedDivider(dividerY, margin);
// 绘制下面的6个框(3行2列)
const boxStartY = dividerY + 30;
const boxWidth = (canvasWidth - margin * 2 - 20) / 2; // 减去列间距
const boxHeight = 165; // 框的高度
const boxSpacingX = 20; // 列间距
const boxSpacingY = 30; // 行间距
const boxPadding = 10; // 框内边距
const boxImageSize = 35; // 框内图片大小
const imagePadding = 20; // 每张图片的内边距(padding)
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 2; col++) {
const boxIndex = row * 2 + col;
const box = matchData.boxes[boxIndex];
if (!box || !box.imageIndices) continue;
const boxX = margin + col * (boxWidth + boxSpacingX);
const boxY = boxStartY + row * (boxHeight + boxSpacingY);
// 绘制框(圆角实线)
this.drawRoundedRect(boxX, boxY, boxWidth, boxHeight, {
isDashed: false,
radius: 16,
color: '#000',
lineWidth: 1,
});
// 计算框内可用的内容区域
const contentWidth = boxWidth - boxPadding * 2;
const contentHeight = boxHeight - boxPadding * 2;
// 为这个框的图片随机分配位置,确保图片+padding不重叠
const imagePositions = this.generateNonOverlappingPositions(
box.imageIndices.length,
contentWidth,
contentHeight,
boxImageSize,
imagePadding,
);
// 绘制框内的图片
for (let i = 0; i < box.imageIndices.length; i++) {
const imageIndex = box.imageIndices[i];
const position = imagePositions[i];
try {
const imagePath = `/focusPages/shared/aeests/material/${imageIndex}.png`;
const image = await getImage(this.canvas, imagePath);
// 计算图片位置
const imageX = boxX + boxPadding + position.x;
const imageY = boxY + boxPadding + position.y;
ctx.drawImage(
image,
imageX,
imageY,
boxImageSize,
boxImageSize,
);
} catch (error) {
console.error(`加载框内图片失败: ${imageIndex}`, error);
}
}
}
}
}
}
export default MatchConnectDraw;
+65
View File
@@ -217,4 +217,69 @@ export class BaseDrawService {
ctx.stroke();
}
}
/**
* 绘制圆角矩形框(逻辑像素,尺寸除以3)
* @param x 框的X坐标
* @param y 框的Y坐标
* @param width 框的宽度
* @param height 框的高度
* @param options 可选参数
* @param options.isDashed 是否为虚线,默认为false(实线)
* @param options.radius 圆角半径,默认为10
* @param options.color 线条颜色,默认为'#000'
* @param options.lineWidth 线条宽度,默认为1
*/
drawRoundedRect(
x: number,
y: number,
width: number,
height: number,
options?: {
isDashed?: boolean;
radius?: number;
color?: string;
lineWidth?: number;
},
): void {
const { ctx } = this;
const {
isDashed = false,
radius = 10,
color = '#000',
lineWidth = 1,
} = options || {};
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
// 设置虚线或实线
if (isDashed) {
ctx.setLineDash([4, 4]); // 虚线
} else {
ctx.setLineDash([]); // 实线
}
// 绘制圆角矩形
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(
x + width,
y + height,
x + width - radius,
y + height,
);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.stroke();
// 重置为实线(避免影响后续绘制)
ctx.setLineDash([]);
}
}
+2 -3
View File
@@ -61,10 +61,9 @@ export function recordShareSuccess(): void {
* @returns true表示需要显示分享引导,false表示不需要
*/
export function shouldShowShareGuide(): boolean {
// TODO: 开启分享引导
return false;
// 如果今天已经分享过,不需要显示引导
if (hasSharedToday()) {
const env = getApp().globalData.env;
if (env === 'develop' || hasSharedToday()) {
return false;
}
// 如果今天已经下载过,需要显示引导