feat:形状

This commit is contained in:
2025-08-07 17:36:04 +08:00
parent d3c1a2fd78
commit 738358ab76
17 changed files with 455 additions and 268 deletions
@@ -1,57 +0,0 @@
.shape-card {
position: relative;
width: 300rpx;
height: 300rpx;
border-radius: 16rpx;
// background: #f5f5f5;
background: #ffffff;
border: 1rpx solid rgba(242, 242, 242, 1);
box-shadow: 0rpx 16rpx 24rpx rgba(184, 210, 188, 0.2);
.shape-card-img-wrapper {
padding: 16rpx;
width: 100%;
height: 200rpx;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
.shape-card-img {
width: 180rpx;
height: 180rpx;
}
}
.shape-card-footer {
margin-inline: 32rpx;
padding-block: 16rpx;
display: flex;
justify-content: start;
align-items: center;
border-top: 1rpx solid #CCCCCC;
.shape-card-footer-color {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
border: 2rpx solid #141414;
margin-right: 34rpx;
}
.shape-card-footer-name {
font-size: 28rpx;
color: #141414;
}
}
.delete-icon {
position: absolute;
top: -18rpx;
right: -18rpx;
.delete-icon-inner {
color: rgba(255, 0, 0, 0.7);
}
}
}
@@ -1,7 +0,0 @@
{
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {
"van-icon": "@vant/weapp/icon/index"
}
}
@@ -1,58 +0,0 @@
.shape-card {
position: relative;
width: 240rpx;
height: 240rpx;
border-radius: 16rpx;
// background: #f5f5f5;
background: #ffffff;
border: 1rpx solid rgba(242, 242, 242, 1);
box-shadow: 0rpx 16rpx 24rpx rgba(184, 210, 188, 0.2);
.shape-card-img-wrapper {
padding: 16rpx;
width: 100%;
height: 120rpx;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
.shape-card-img {
width: 120rpx;
height: 120rpx;
color: skyblue;
}
}
.shape-card-footer {
margin-inline: 32rpx;
padding-block: 16rpx;
display: flex;
justify-content: start;
align-items: center;
border-top: 1rpx solid #CCCCCC;
.shape-card-footer-color {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
border: 2rpx solid #141414;
margin-right: 34rpx;
}
.shape-card-footer-name {
font-size: 28rpx;
color: #141414;
}
}
.delete-icon {
position: absolute;
top: -18rpx;
right: -18rpx;
.delete-icon-inner {
color: rgba(255, 0, 0, 0.7);
}
}
}
@@ -1,36 +0,0 @@
Component({
/**
* 组件的属性列表
*/
properties: {
key: {
type: Number,
value: 0,
},
img: {
type: String,
value: '',
},
name: {
type: String,
value: '',
},
color: {
type: String,
value: '',
},
},
/**
* 组件的方法列表
*/
methods: {
onDelete() {
const { key } = this.data;
this.triggerEvent('onDelete', { key });
},
onColorTap() {
const { key, color } = this.data;
this.triggerEvent('onColorTap', { key, color });
},
},
});
@@ -1,19 +0,0 @@
<view class="shape-card">
<view class="shape-card-img-wrapper">
<image src="{{ img }}" class="shape-card-img" />
</view>
<view class="shape-card-footer">
<view
class="shape-card-footer-color"
style="background-color: {{ color }}"
bind:tap="onColorTap"></view>
<view class="shape-card-footer-name">{{ name }}</view>
</view>
<view class="delete-icon" bindtap="onDelete">
<van-icon
class-prefix="toy-icon"
name="error-o"
custom-class="delete-icon-inner"
size="48rpx" />
</view>
</view>
@@ -1,57 +0,0 @@
.shape-card {
position: relative;
width: 300rpx;
height: 300rpx;
border-radius: 16rpx;
// background: #f5f5f5;
background: #ffffff;
border: 1rpx solid rgba(242, 242, 242, 1);
box-shadow: 0rpx 16rpx 24rpx rgba(184, 210, 188, 0.2);
.shape-card-img-wrapper {
padding: 16rpx;
width: 100%;
height: 200rpx;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
.shape-card-img {
width: 180rpx;
height: 180rpx;
}
}
.shape-card-footer {
margin-inline: 32rpx;
padding-block: 16rpx;
display: flex;
justify-content: start;
align-items: center;
border-top: 1rpx solid #CCCCCC;
.shape-card-footer-color {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
border: 2rpx solid #141414;
margin-right: 34rpx;
}
.shape-card-footer-name {
font-size: 28rpx;
color: #141414;
}
}
.delete-icon {
position: absolute;
top: -18rpx;
right: -18rpx;
.delete-icon-inner {
color: rgba(255, 0, 0, 0.7);
}
}
}
@@ -27,8 +27,8 @@
}
.shape-color {
width: 72rpx;
height: 72rpx;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
border: 1rpx solid #141414;
background-color: skyblue;
@@ -11,7 +11,7 @@ Component({
type: String,
value: '',
},
color: {
fillColor: {
type: String,
value: '',
},
@@ -34,7 +34,7 @@ Component({
* 监听属性变化
*/
observers: {
'svg, color': function () {
'svg, fillColor': function () {
this.generateSvgDataUri();
}
},
@@ -46,12 +46,24 @@ Component({
* 生成完整的 SVG Data URI
*/
generateSvgDataUri() {
const { svg, color } = this.data;
const { svg, fillColor } = this.data;
if (fillColor === 'transparent') return;
if (!svg) return;
// 创建完整的 SVG 字符串,替换填充颜色
const coloredSvg = svg.replace(/fill="#ff0000"/g, `fill="${color || '#ff0000'}"`);
const completeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">${coloredSvg}</svg>`;
// 处理 fill 属性:无论原来有没有 fill,都要赋值为 fillColor
let processedSvg = svg;
// 1. 如果有 fill 属性,全部替换为 fillColor
if (/fill="[^"]*"/.test(processedSvg)) {
processedSvg = processedSvg.replace(/fill="[^"]*"/g, `fill="${fillColor || '#ff0000'}"`);
} else {
// 2. 没有 fill 属性,则在第一个标签内加上 fill
processedSvg = processedSvg.replace(
/(<(?:circle|rect|polygon|ellipse|path|line)[^>]*?)(\/?>)/,
`$1 fill="${fillColor || '#ff0000'}"$2`
);
}
const completeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">${processedSvg}</svg>`;
// 转换为 Data URI
const svgDataUri = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(completeSvg)}`;
@@ -65,8 +77,13 @@ Component({
this.triggerEvent('onDelete', { key });
},
onColorTap() {
const { key, color } = this.data;
this.triggerEvent('onColorTap', { key, color });
const { key, fillColor } = this.data;
this.triggerEvent('onColorTap', { key, fillColor });
},
onShapeTap() {
const { key } = this.data;
this.triggerEvent('onShapeTap', { key });
},
},
});
@@ -1,9 +1,13 @@
<view class="shape-card">
<image src="{{ svgDataUri }}" class="shape-card-img" mode="aspectFit" />
<image
src="{{ svgDataUri }}"
class="shape-card-img"
mode="aspectFit"
bind:tap="onShapeTap" />
<view class="divider" />
<view
class="shape-color"
style="background-color: {{ color }}"
style="background-color: {{ fillColor }}"
bind:tap="onColorTap" />
<van-icon
class-prefix="toy-icon"
@@ -0,0 +1,8 @@
{
"component": true,
"styleIsolation": "isolated",
"usingComponents": {
"van-popup": "@vant/weapp/popup/index",
"toy-button": "/ui/button/button"
}
}
@@ -0,0 +1,101 @@
.shape-picker-popup {
border-radius: 20rpx;
.shape-picker-container {
width: 642rpx;
background-color: #fff;
border-radius: 20rpx;
box-shadow: 0 3rpx 4rpx rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
padding: 36rpx 36rpx;
box-sizing: border-box;
align-items: center;
.shape-picker-title {
font-size: 32rpx;
color: #141414;
font-weight: bold;
margin-bottom: 30rpx;
text-align: center;
}
.shape-options-wrapper {
padding: 28rpx 0;
width: 100%;
height: 500rpx;
overflow-y: auto;
}
.shape-options {
display: grid;
grid-template-columns: repeat(3, 186rpx);
gap: 12rpx;
width: 100%;
justify-content: center;
.shape-option {
width: 140rpx;
display: flex;
flex-direction: column;
align-items: center;
transition: all 0.2s ease;
padding: 10rpx;
border-radius: 10rpx;
border: 1rpx solid transparent;
.shape-svg-container {
width: 140rpx;
height: 140rpx;
display: flex;
align-items: center;
justify-content: center;
border: 1rpx solid #141414;
border-radius: 8rpx;
background-color: #fafafa;
margin-bottom: 8rpx;
position: relative;
.shape-svg {
width: 140rpx;
height: 140rpx;
display: block;
}
}
.shape-name {
font-size: 24rpx;
color: #141414;
text-align: center;
line-height: 1.2;
}
&.selected {
// transform: scale(1.1);
// border: 1px solid rgb(147, 211, 51);
// background-color: rgba(64, 150, 255, 0.05);
.shape-svg-container {
border: 1px solid #93d333;
// border: 1px solid #4096ff;
// background-color: #fff;
// box-shadow: 0 0 6rpx 2rpx rgba(147, 211, 51, 0.2);
}
.shape-name {
color: rgb(147, 211, 51);
// font-weight: bold;
}
}
}
}
}
}
.btn-area {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
margin: 10rpx 24rpx;
}
@@ -0,0 +1,122 @@
import { SHAPES, ShapeCard } from '../../constants/shapes';
Component({
options: {},
/**
* 组件的属性列表
*/
properties: {
show: {
type: Boolean,
value: false,
},
selectedShape: {
type: String,
value: '',
},
currentShapeKey: {
type: String,
value: '',
},
},
/**
* 组件的初始数据
*/
data: {
shapes: [] as (ShapeCard & { svgDataUrl: string })[],
selectedShapes: [] as string[], // 多选模式下选中的形状数组
},
lifetimes: {
attached() {
// 处理 SVG 数据,转换为 data URL
const shapesWithDataUrl = SHAPES.map(shape => {
const completeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">${shape.svg}</svg>`;
const svgDataUri = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(completeSvg)}`;
return {
...shape,
svgDataUrl: svgDataUri
};
});
console.log('shapesWithDataUrl---:', shapesWithDataUrl);
this.setData({
shapes: shapesWithDataUrl
});
},
},
/**
* 组件的方法列表
*/
methods: {
onClose() {
this.triggerEvent('onClose');
},
onSelectShape(e: WechatMiniprogram.TouchEvent) {
const { index } = e.currentTarget.dataset;
const { currentShapeKey, shapes } = this.data;
if (currentShapeKey) {
// 单选模式:currentShapeKey 不为空时,只能选中一个形状
this.setData({
shapes: shapes.map((item, i) => ({
...item,
checked: i === index
}))
});
} else {
// 多选模式:currentShapeKey 为空时,支持多选,最多可选6个
let shapesCopy = this.data.shapes.map(item => ({ ...item }));
const targetIndex = index;
// 统计当前已选中的数量
const checkedCount = shapesCopy.filter(item => item.checked).length;
if (shapesCopy[targetIndex].checked) {
// 如果已选中,则取消选中
shapesCopy[targetIndex].checked = false;
} else {
if (checkedCount >= 6) {
wx.showToast({
title: '最多可选择6个形状',
icon: 'none',
duration: 1500
});
return;
}
// 如果未选中,则添加到选中列表
shapesCopy[targetIndex].checked = true;
}
this.setData({
shapes: shapesCopy,
});
}
},
onChange() {
const { selectedShape, selectedShapes, currentShapeKey } = this.data;
if (currentShapeKey) {
// 单选模式:返回单个选中的形状
this.triggerEvent('onChange', { shape: selectedShape, currentShapeKey });
} else {
// 多选模式:返回选中的形状数组
this.triggerEvent('onChange', { shapes: selectedShapes });
}
},
// 获取选中状态的 CSS 类名
getShapeClass(shapeId: string): string {
const { currentShapeKey, selectedShapes } = this.data;
if (currentShapeKey) {
// 单选模式:检查是否等于当前选中的形状
return shapeId === currentShapeKey ? 'selected single-mode' : '';
} else {
// 多选模式:检查是否在选中列表中
return selectedShapes.includes(shapeId) ? 'selected multi-mode' : '';
}
},
},
});
@@ -0,0 +1,46 @@
<van-popup
show="{{ show }}"
bind:close="onClose"
custom-class="shape-picker-popup">
<view class="shape-picker-container">
<text class="shape-picker-title"
>选择形状{{ currentShapeKey ? '' : '(多选)' }}</text
>
<view class="shape-options-wrapper">
<view class="shape-options">
<view
wx:for="{{shapes}}"
wx:key="id"
class="shape-option {{item.checked ? 'selected' : '' }}"
bindtap="onSelectShape"
data-index="{{index}}"
data-shape="{{item.id}}">
<view class="shape-svg-container">
<image
class="shape-svg"
src="{{ item.svgDataUrl }}"
mode="aspectFit" />
<!-- 多选模式下显示选中标记 -->
<view wx:if="{{ item.checked }}" class="selected-icon">
<toy-icon
name="ic_check"
size="24"
color="#93d333" />
</view>
</view>
<text class="shape-name">{{item.name}}</text>
</view>
</view>
</view>
<view class="btn-area">
<toy-button
type="primary"
bind:click="onChange"
width="400rpx"
disabled="{{ currentShapeKey ? !selectedShape : selectedShapes.length === 0 }}">
确认选择{{ !currentShapeKey && selectedShapes.length > 0 ? ''
+ selectedShapes.length + '个)' : '' }}
</toy-button>
</view>
</view>
</van-popup>