From c95bd601e86655d7634c878e2c9dd2a8076dd444 Mon Sep 17 00:00:00 2001 From: joey Date: Wed, 20 Aug 2025 10:49:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=9B=BE=E5=BD=A2?= =?UTF-8?q?=E6=B6=82=E8=89=B2=E7=BB=98=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/app.ts | 20 ++-- .../components/shape-picker/shape-picker.ts | 34 +++++- .../components/shape-picker/shape-picker.wxml | 7 +- miniprogram/config/config.ts | 6 + miniprogram/constants/shapes.ts | 3 +- miniprogram/pages/debug/debug.ts | 14 +-- miniprogram/pages/debug/debug.wxml | 4 +- miniprogram/pages/shape/index.less | 2 +- miniprogram/pages/shape/index.ts | 107 ++++++++++-------- miniprogram/pages/shape/index.wxml | 4 +- miniprogram/service/drawShape.ts | 21 +++- miniprogram/service/shapeDrawService.ts | 102 +++++++---------- 12 files changed, 178 insertions(+), 146 deletions(-) diff --git a/miniprogram/app.ts b/miniprogram/app.ts index 3c21a06..51bdcdd 100644 --- a/miniprogram/app.ts +++ b/miniprogram/app.ts @@ -1,21 +1,20 @@ +import config from './config/config'; + +const defaultPrintConfig: PrintConfig = config.printHeader as PrintConfig; + + // app.ts App({ globalData: { env: 'release', - printConfig: { - header: 'wechat', - appName: '涂鸦丫小程序' - } + printConfig: defaultPrintConfig }, onLaunch() { const accountInfo = wx.getAccountInfoSync(); const env = accountInfo.miniProgram.envVersion || 'release'; this.globalData.env = env; if (env !== 'release') { - const printConfig = wx.getStorageSync('printConfig') || { - header: 'wechat', - appName: '涂鸦丫小程序' - }; + const printConfig = wx.getStorageSync('printConfig') || defaultPrintConfig; this.globalData.printConfig = printConfig; } }, @@ -29,10 +28,7 @@ App({ }, getPrintConfig(): PrintConfig { - return this.globalData.printConfig || { - header: 'wechat', - appName: '涂鸦丫小程序' - }; + return this.globalData.printConfig || defaultPrintConfig; }, setPrintConfig(config: PrintConfig) { diff --git a/miniprogram/components/shape-picker/shape-picker.ts b/miniprogram/components/shape-picker/shape-picker.ts index 28c2ee2..7538df5 100644 --- a/miniprogram/components/shape-picker/shape-picker.ts +++ b/miniprogram/components/shape-picker/shape-picker.ts @@ -10,9 +10,9 @@ Component({ type: Boolean, value: false, }, - singleMode: { - type: Boolean, - value: false, + selectedShapeIndex: { + type: Number, + value: -1, }, }, /** @@ -20,6 +20,7 @@ Component({ */ data: { shapes: [] as (ShapeCard & { svgDataUrl: string })[], + singleMode: false, totalSelected: 0, }, lifetimes: { @@ -36,8 +37,26 @@ Component({ this.setData({ shapes: shapesWithDataUrl }); + } + }, + observers: { + selectedShapeIndex(newVal: number) { + this.setData({ + singleMode: newVal !== -1 + }); }, }, + // pageLifetimes: { + // show: function () { + // console.log('show singleMode', this.data.singleMode); + // }, + // hide: function () { + // console.log('hide singleMode', this.data.singleMode); + // }, + // resize: function (size) { + // console.log('resize', size); + // } + // }, /** * 组件的方法列表 */ @@ -89,9 +108,14 @@ Component({ }, onChange() { - const { shapes } = this.data; + const { shapes, selectedShapeIndex, singleMode } = this.data; const selectedShapes = shapes.filter(item => item.checked); - this.triggerEvent('onChange', { shapes: selectedShapes, singleMode: this.data.singleMode }); + + this.triggerEvent('onChange', { + shapes: selectedShapes, + singleMode, + selectedShapeIndex + }); }, }, }); diff --git a/miniprogram/components/shape-picker/shape-picker.wxml b/miniprogram/components/shape-picker/shape-picker.wxml index 91a20ba..d7b0f09 100644 --- a/miniprogram/components/shape-picker/shape-picker.wxml +++ b/miniprogram/components/shape-picker/shape-picker.wxml @@ -3,9 +3,10 @@ bind:close="onClose" custom-class="shape-picker-popup"> - 选择形状{{ singleMode ? '' : '(多选)' }} + + 选择形状{{selectedShapeIndex}} {{ singleMode ? '(单选)' : + '(多选)' }} + ', - svg: '', + svg: '', fillColor: '#ff0000', drawFunction: 'drawTriangle', }, diff --git a/miniprogram/pages/debug/debug.ts b/miniprogram/pages/debug/debug.ts index 6105386..2df77b3 100644 --- a/miniprogram/pages/debug/debug.ts +++ b/miniprogram/pages/debug/debug.ts @@ -1,17 +1,15 @@ +import config from '../../config/config'; + +const defaultPrintConfig: PrintConfig = config.printHeader as PrintConfig; + Page({ data: { - printConfig: { - header: 'wechat', - appName: '涂鸦丫小程序' - }, + printConfig: defaultPrintConfig, enableDebug: false }, onLoad() { - const printConfig = getApp().getPrintConfig() || { - header: 'wechat', - appName: '涂鸦丫小程序' - }; + const printConfig = getApp().getPrintConfig() || defaultPrintConfig; const enableDebug = wx.getStorageSync('enableDebug') || false; this.setData({ printConfig, enableDebug }); }, diff --git a/miniprogram/pages/debug/debug.wxml b/miniprogram/pages/debug/debug.wxml index 7ec3159..8144add 100644 --- a/miniprogram/pages/debug/debug.wxml +++ b/miniprogram/pages/debug/debug.wxml @@ -8,7 +8,7 @@ value="{{printConfig.header}}" bind:change="onChangeHeader"> @@ -29,7 +29,7 @@ item.hex); + let newShapeList: ShapeCard[] = []; - // 记录已被使用的颜色(只考虑当前 shapeList 中的 fillColor) - const usedColors = prevShapeList.map(item => item.fillColor).filter(Boolean); + 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 - const newShapeList: ShapeCard[] = []; + // 记录已被使用的颜色(只考虑当前 shapeList 中的 fillColor) + const usedColors = prevShapeList.map(item => item.fillColor).filter(Boolean); - // 记录已分配的颜色,避免重复 - const assignedColors = [...usedColors]; + // 记录已分配的颜色,避免重复 + const assignedColors = [...usedColors]; - // 先将 shapes 转为 Map,方便查找 - const prevShapeMap = new Map(prevShapeList.map(item => [item.id, item])); + // 先将 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]]; + // 随机打乱剩余可用颜色 + 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; } - 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 + }); + } + }); } - // 计算未被占用的颜色 - 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, - currentShapeId: '', + selectedShapeIndex: -1, shapeList: newShapeList }, () => { // 绘制canvas @@ -216,7 +230,6 @@ Page({ }, onColorTap(e: any) { const { index, fillColor } = e.detail; - console.log('shape Page onColorTap', index, fillColor); this.setData({ showColorPopup: true, currentColor: fillColor, diff --git a/miniprogram/pages/shape/index.wxml b/miniprogram/pages/shape/index.wxml index a0e98a4..0a68fa7 100644 --- a/miniprogram/pages/shape/index.wxml +++ b/miniprogram/pages/shape/index.wxml @@ -34,6 +34,7 @@ - 预览打印效果 + 预览打印效果 {{currentShapeId}} + + // 以(x, y)为中心,size为半高,按SVG比例映射四个点 + // SVG中心(50,50),顶点(50,15),右(70,50),下(50,85),左(30,50) + // 计算比例 + // 顶点: (0, -0.7*size) + // 右: (0.4*size, 0) + // 下: (0, 0.7*size) + // 左: (-0.4*size, 0) + const topY = y - size * 0.7; + const rightX = x + size * 0.4; + const bottomY = y + size * 0.7; + const leftX = x - size * 0.4; + ctx.beginPath(); - ctx.moveTo(x, y - size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x - size, y); + ctx.moveTo(x, topY); // 顶点 + ctx.lineTo(rightX, y); // 右 + ctx.lineTo(x, bottomY); // 下 + ctx.lineTo(leftX, y); // 左 ctx.closePath(); if (fillColor !== 'transparent') { ctx.fillStyle = fillColor; diff --git a/miniprogram/service/shapeDrawService.ts b/miniprogram/service/shapeDrawService.ts index 12bd704..aae4b56 100644 --- a/miniprogram/service/shapeDrawService.ts +++ b/miniprogram/service/shapeDrawService.ts @@ -226,90 +226,70 @@ class ShapeDrawService { const { canvas, ctx, shapes } = this; if (shapes.length <= 0) return; - const minY = this.headerType === 'minimal' ? 532 : 622; + // 固定可渲染的总行数 + const ROW_COUNT = 6; + // 每行之间的垂直间距 + const VERTICAL_GAP = 120; + // 图形大小 const shapeSize = 180; - const minGap = 40; // 图形之间最小间距 - const bottomMargin = 20; - // 计算内容区起始Y坐标,保证在532或622以下 - const contentTop = minY + 20; // 距离minY线之后20px开始绘制 - const contentHeight = canvas.height - contentTop - bottomMargin; + // 左右边距 + const leftMargin = 160; + const rightMargin = 160; + // 以 drawLegend 画完后的分割线作为基准,内容区顶部间距 50 + const legendBottomY = this.currentY; + const topGap = 60; + const contentTop = legendBottomY + topGap; - // 左右各留100像素边距 - const contentLeft = 100; - const contentWidth = canvas.width - shapeSize; + // 可用宽度 + const contentWidth = canvas.width - leftMargin - rightMargin; - // 计算一共要绘制多少个图形 - const repeatCount = 8; - const totalShapes = shapes.length * repeatCount; + // 每行最多能放多少个图形(考虑最小间距40) + const minGap = 40; + const maxPerRow = Math.floor((contentWidth + minGap) / (shapeSize + minGap)); + + // 总共要绘制多少个图形(每行都填满) + const totalShapes = maxPerRow * ROW_COUNT; // 生成所有要绘制的图形(打乱顺序,保证随机性) let allShapes: ShapeCard[] = []; - for (let i = 0; i < repeatCount; i++) { + for (let i = 0; i < Math.ceil(totalShapes / shapes.length); i++) { allShapes = allShapes.concat(shapes); } + allShapes = allShapes.slice(0, totalShapes); // 洗牌算法彻底打乱 for (let i = allShapes.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [allShapes[i], allShapes[j]] = [allShapes[j], allShapes[i]]; } - // 计算每行最多能放多少个图形(考虑最小间距和边距) - const maxPerRow = Math.floor((contentWidth + minGap) / (shapeSize + minGap)); - const rowCount = Math.ceil(totalShapes / maxPerRow); - - // 计算实际间距(保证左右边距为80,且间距不小于minGap) - const actualPerRow = Math.min(maxPerRow, totalShapes); - const actualGap = actualPerRow > 1 - ? Math.max(minGap, (contentWidth - actualPerRow * shapeSize) / (actualPerRow - 1)) - : 0; - - // 计算每行的Y坐标 - 固定上边距,不使用垂直居中 - const totalRows = rowCount; - let startY = contentTop; // 直接使用contentTop,不进行垂直居中计算 - // 生成所有图形的位置 let positions: { x: number, y: number }[] = []; - let shapeIdx = 0; - - const canvasHeight = this.canvas.height; - - for (let row = 0; row < totalRows; row++) { - // 本行实际要放多少个图形 - const shapesInThisRow = Math.min(actualPerRow, totalShapes - shapeIdx); + for (let row = 0; row < ROW_COUNT; row++) { + // 本行起始Y + const y = contentTop + row * (shapeSize + VERTICAL_GAP) + shapeSize / 2; // 本行实际间距 - const gap = shapesInThisRow > 1 - ? Math.max(minGap, (contentWidth - shapesInThisRow * shapeSize) / (shapesInThisRow - 1)) + const gap = maxPerRow > 1 + ? Math.max(minGap, (contentWidth - maxPerRow * shapeSize) / (maxPerRow - 1)) : 0; - // 本行起始X - let startX = contentLeft; - // 居中对齐 - if (shapesInThisRow > 1) { - const rowWidth = shapesInThisRow * shapeSize + (shapesInThisRow - 1) * gap; - startX = contentLeft + (contentWidth - rowWidth) / 2; + // 本行起始X(水平居中) + let startX = leftMargin; + if (maxPerRow > 1) { + const rowWidth = maxPerRow * shapeSize + (maxPerRow - 1) * gap; + startX = leftMargin + (contentWidth - rowWidth) / 2; } else { - startX = contentLeft + (contentWidth - shapeSize) / 2; + startX = leftMargin + (contentWidth - shapeSize) / 2; } - - // 计算当前行的Y坐标 - const currentRowY = startY + row * (shapeSize + minGap) + shapeSize / 2; - - // 检查当前行是否会超出下边界 - if (currentRowY + shapeSize / 2 > canvasHeight - bottomMargin) { - break; // 停止生成更多行 - } - - for (let col = 0; col < shapesInThisRow; col++) { + for (let col = 0; col < maxPerRow; col++) { positions.push({ x: startX + col * (shapeSize + gap) + shapeSize / 2, - y: currentRowY + y: y }); - shapeIdx++; - if (shapeIdx >= totalShapes) break; } - - if (shapeIdx >= totalShapes) break; } + // 修正:只保留前 totalShapes 个位置,防止多出一行 + positions = positions.slice(0, totalShapes); + // 再次彻底打乱位置顺序,保证排列无规律 for (let i = positions.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); @@ -317,14 +297,14 @@ class ShapeDrawService { } // 记录实际绘制的图形数量 - console.log(`绘制图形:计划${totalShapes}个,实际${positions.length}个,边界限制:距离下边${bottomMargin}px`); + console.log(`绘制图形:计划${totalShapes}个,实际${positions.length}个;每行${maxPerRow}个,共${ROW_COUNT}行;边距 左右=${leftMargin}/${rightMargin},顶部=${topGap}(基于legend底线)`); // 绘制所有图形 positions.forEach((pos, index) => { const shape = allShapes[index]; - // 随机旋转角度 - const rotation = (Math.random() - 0.5) * 0.5; // ±0.25弧度 + // 随机旋转角度,范围为±90度(即最大180度) + const rotation = (Math.random() - 0.5) * Math.PI; // ±90度 = ±π/2 弧度,Math.PI为180度 ctx.save(); ctx.translate(pos.x, pos.y);