feat:重构按钮和icon组件

This commit is contained in:
R524809
2026-04-07 17:23:05 +08:00
parent 672d457a35
commit 653737c481
95 changed files with 3261 additions and 1342 deletions
+141
View File
@@ -0,0 +1,141 @@
# Toy Button 组件
基于涂鸦丫设计规范的按钮组件,当前视觉参考 `stitch-ui/letterTracing/code.html` 中底部“分享 / 保存到相册”按钮,内部不依赖 Vant。
## 功能特性
- ✅ 多种按钮类型:primary、secondary、green、white
- ✅ 丰富的状态:正常、禁用、加载中
- ✅ 扁平化模式支持
- ✅ 图标支持
- ✅ 自定义尺寸
- ✅ 参考设计稿的缩放点击动效
- ✅ 与 doodle-ui 样式完全一致
## 使用方法
### 基础用法
```xml
<!-- 主要按钮 -->
<toy-button type="primary" bind:click="onClick">主要按钮</toy-button>
<!-- 次要按钮 -->
<toy-button type="secondary" bind:click="onClick">次要按钮</toy-button>
<!-- 绿色按钮 -->
<toy-button type="green" bind:click="onClick">绿色按钮</toy-button>
<!-- 白色按钮 -->
<toy-button type="white" bind:click="onClick">白色按钮</toy-button>
```
### 按钮状态
```xml
<!-- 禁用状态 -->
<toy-button type="primary" disabled>禁用按钮</toy-button>
<!-- 加载状态 -->
<toy-button type="primary" loading>加载中</toy-button>
<!-- 扁平化模式 -->
<toy-button type="primary" flat>扁平按钮</toy-button>
```
### 带图标按钮
```xml
<!-- 带图标 -->
<toy-button type="primary" icon="add">添加</toy-button>
<!-- 自定义图标颜色 -->
<toy-button type="white" icon="edit" icon-color="#FFD719">编辑</toy-button>
```
### 自定义尺寸
```xml
<!-- 自定义宽度 -->
<toy-button type="primary" width="300rpx">自定义宽度</toy-button>
<!-- 自定义高度 -->
<toy-button type="primary" height="100rpx">自定义高度</toy-button>
```
## 属性说明
| 属性 | 类型 | 默认值 | 说明 |
| --------------- | ------- | ---------- | ------------------------------------------ |
| type | String | 'default' | 按钮类型:primary、secondary、green、white |
| disabled | Boolean | false | 是否禁用 |
| loading | Boolean | false | 是否显示加载状态 |
| flat | Boolean | false | 是否扁平化模式 |
| plain | Boolean | false | 是否朴素模式(仅white类型有效) |
| width | String | '' | 按钮宽度 |
| height | String | '' | 按钮高度 |
| icon | String | '' | 图标名称 |
| iconColor | String | '#ffffff' | 图标颜色 |
| iconSize | String | '44rpx' | 图标大小 |
| iconClassPrefix | String | 'toy-icon' | 图标类名前缀 |
## 事件说明
| 事件名 | 说明 | 回调参数 |
| ------ | -------------- | -------- |
| click | 点击按钮时触发 | event |
## 样式说明
### 按钮类型
- **primary**: 黄色背景 (#FFD700),深色文字 (#6c5a00)
- **secondary**: 绿色背景 (#9CD343),白色文字
- **green**: 绿色背景 (#9CD343),白色文字(与secondary相同)
- **white**: 米白背景,深色文字,浅边框
### 点击动效
- 所有按钮都支持 `scale(0.95)` 的点击缩放效果
- `flat` 模式下更接近底部操作栏按钮视觉
- 动效持续时间约为 180ms
### 禁用状态
- 背景色变为浅灰色 (#F5F5F5)
- 文字颜色变为浅灰色 (#CCCCCC)
- 移除阴影效果
## 设计规范
本组件完全遵循 doodle-ui 的设计规范:
- 圆角:24rpx (对应 doodle-ui 的 12px)
- 字体大小:48rpx (对应 doodle-ui 的 24px)
- 字体粗细:600
- 内边距:0 48rpx
- 最小宽度:240rpx
- 过渡动画:0.15s ease-in-out
- 点击动效:抖动缩放动画
## 测试页面
可以通过访问 `pages/buttonTest/buttonTest` 页面查看所有按钮类型的效果。
## 更新日志
### v2.1.0
- 修复圆角大小,与 doodle-ui 保持一致 (24rpx)
- 修复字体大小,与 doodle-ui 保持一致 (48rpx)
- 修复点击抖动效果,移除 wxs 中的 setTimeout
- 优化按钮动画,使用纯 CSS 实现抖动效果
- 改进所有按钮类型的点击反馈
### v2.0.0
- 完全重构样式,与 doodle-ui 保持一致
- 添加 secondary 类型支持
- 优化点击动效
- 改进扁平化模式
- 统一设计规范
+8
View File
@@ -0,0 +1,8 @@
{
"component": true,
"usingComponents": {
"toy-icon": "/toy/icon/icon"
},
"styleIsolation": "apply-shared",
"addGlobalClass": true
}
+194
View File
@@ -0,0 +1,194 @@
@button-default-height: 92rpx;
@button-border-radius: 32rpx;
@button-default-font-size: 30rpx;
@button-default-color: #fff;
@button-transition: 0.18s ease;
@button-shadow-primary: 0 8rpx 20rpx rgba(255, 215, 0, 0.22);
@button-shadow-green: 0 8rpx 20rpx rgba(156, 211, 67, 0.22);
@button-shadow-white: 0 6rpx 16rpx rgba(50, 46, 37, 0.08);
@button-shadow-pressed: 0 4rpx 10rpx rgba(50, 46, 37, 0.1);
@button-disabled-bg: #ece6da;
@button-disabled-color: #b8b2a6;
wx-button:not([size=mini]) {
margin-left: auto;
margin-right: auto;
width: auto;
}
.toy-button {
--button-bg: #f0e7d6;
--button-color: @button-default-color;
--button-border-color: transparent;
--button-shadow: none;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 180rpx;
height: @button-default-height;
line-height: 1;
padding: 0 28rpx;
border-radius: @button-border-radius;
border: 2rpx solid var(--button-border-color);
background: var(--button-bg);
color: var(--button-color);
box-shadow: var(--button-shadow);
font-size: @button-default-font-size;
font-weight: 700;
letter-spacing: 0.5rpx;
text-align: center;
cursor: pointer;
transition:
transform @button-transition,
box-shadow @button-transition,
background-color @button-transition,
border-color @button-transition,
color @button-transition,
opacity @button-transition;
&::after {
border: none;
}
&--default {
--button-bg: #f0e7d6;
--button-color: #322e25;
--button-border-color: #e5dcc9;
--button-shadow: @button-shadow-white;
}
&--large {
width: 100%;
}
&--small {
min-width: 136rpx;
height: 72rpx;
padding: 0 20rpx;
border-radius: 20rpx;
font-size: 26rpx;
}
&--mini {
min-width: 96rpx;
height: 56rpx;
padding: 0 16rpx;
border-radius: 18rpx;
font-size: 24rpx;
}
&--primary {
--button-bg: #ffd700;
--button-color: #6c5a00;
--button-shadow: @button-shadow-primary;
}
&--green,
&--secondary {
--button-bg: #9cd343;
--button-color: #ffffff;
--button-shadow: @button-shadow-green;
}
&--white {
--button-bg: rgba(255, 255, 255, 0.96);
--button-color: #322e25;
--button-border-color: #ebe3d2;
--button-shadow: @button-shadow-white;
}
&.plain.toy-button--white {
--button-bg: transparent;
--button-color: #ffffff;
--button-border-color: rgba(255, 255, 255, 0.92);
--button-shadow: none;
}
&.disabled {
--button-bg: @button-disabled-bg !important;
--button-color: @button-disabled-color !important;
--button-border-color: transparent !important;
--button-shadow: none !important;
opacity: 1;
}
&.loading {
opacity: 0.92;
}
&.active {
transform: scale(0.95);
box-shadow: @button-shadow-pressed;
}
&--flat {
min-width: 0;
padding: 0 24rpx;
border-width: 0;
border-radius: 32rpx;
&.toy-button--green,
&.toy-button--secondary {
--button-bg: #9cd343;
--button-color: #ffffff;
--button-shadow: 0 4rpx 12rpx rgba(50, 46, 37, 0.08);
font-weight: 700;
}
&.toy-button--primary {
--button-bg: #ffd700;
--button-color: #6c5a00;
--button-shadow: 0 8rpx 18rpx rgba(108, 90, 0, 0.12);
font-weight: 800;
}
&.toy-button--white {
--button-bg: rgba(255, 255, 255, 0.96);
--button-color: #322e25;
--button-shadow: 0 4rpx 12rpx rgba(50, 46, 37, 0.08);
}
&.toy-button--white.plain {
--button-bg: transparent;
--button-color: #ffffff;
--button-shadow: none;
border-width: 2rpx;
}
&.active {
transform: scale(0.95);
}
&.disabled {
--button-bg: #e6e0d4 !important;
--button-color: #b8b2a6 !important;
--button-shadow: none !important;
}
}
.loading-class,
&__icon {
margin-right: 12rpx;
flex-shrink: 0;
}
&__spinner {
box-sizing: border-box;
border-radius: 50%;
border-width: 4rpx;
border-style: solid;
animation: toy-button-spin 0.8s linear infinite;
}
}
@keyframes toy-button-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
+260
View File
@@ -0,0 +1,260 @@
Component({
options: {
styleIsolation: 'apply-shared',
addGlobalClass: true,
},
properties: {
type: {
type: String,
value: 'default',
},
openType: {
type: String,
value: '',
},
plain: {
type: Boolean,
value: false,
},
size: {
type: String,
value: 'normal',
},
disabled: {
type: Boolean,
value: false,
},
flat: {
type: Boolean,
value: false,
},
loading: {
type: Boolean,
value: false,
},
style: {
type: String,
value: '',
},
width: {
type: String,
value: '',
},
height: {
type: String,
value: '',
},
loadingType: {
type: String,
value: 'circular',
},
loadingSize: {
type: String,
value: '20px',
},
loadingColor: {
type: String,
value: '#ffffff',
},
icon: {
type: String,
value: '',
},
iconClassPrefix: {
type: String,
value: 'toy-icon',
},
iconSize: {
type: String,
value: '44rpx',
},
iconColor: {
type: String,
value: '#ffffff',
},
sessionFrom: {
type: String,
value: '',
},
sendMessageTitle: {
type: String,
value: '',
},
sendMessagePath: {
type: String,
value: '',
},
sendMessageImg: {
type: String,
value: '',
},
showMessageCard: {
type: Boolean,
value: false,
},
appParameter: {
type: String,
value: '',
},
ariaLabel: {
type: String,
value: '',
},
},
data: {
pressed: false,
rootClassName: '',
rootStyle: '',
resolvedIconColor: '#ffffff',
resolvedLoadingColor: '#ffffff',
resolvedIconSize: '44rpx',
resolvedLoadingSize: '20px',
},
observers: {
'type,plain,size,disabled,flat,loading,style,width,height,iconColor,iconSize,loadingColor,loadingSize,pressed':
function () {
this.syncViewState();
},
},
lifetimes: {
attached() {
this.syncViewState();
},
},
methods: {
syncViewState() {
const {
type,
plain,
size,
disabled,
flat,
loading,
style,
width,
height,
iconColor,
iconSize,
loadingColor,
loadingSize,
pressed,
} = this.data;
const classNames = ['toy-button', `toy-button--${type}`, `toy-button--${size}`];
if (flat) {
classNames.push('toy-button--flat');
}
if (plain) {
classNames.push('plain');
}
if (disabled) {
classNames.push('disabled');
}
if (loading) {
classNames.push('loading');
}
if (pressed && !disabled && !loading) {
classNames.push('active');
}
const styleSegments = [style];
const normalizedWidth = this.normalizeUnit(width);
const normalizedHeight = this.normalizeUnit(height);
if (normalizedWidth) {
styleSegments.push(`width:${normalizedWidth}`);
}
if (normalizedHeight) {
styleSegments.push(`height:${normalizedHeight}`);
}
const resolvedIconColor = this.getResolvedIconColor(
iconColor,
type,
plain,
disabled,
);
const resolvedLoadingColor = loadingColor || resolvedIconColor;
this.setData({
rootClassName: classNames.join(' '),
rootStyle: styleSegments.filter(Boolean).join(';'),
resolvedIconColor,
resolvedLoadingColor,
resolvedIconSize: this.normalizeUnit(iconSize) || '44rpx',
resolvedLoadingSize: this.normalizeUnit(loadingSize) || '20px',
});
},
normalizeUnit(value: string) {
if (!value) {
return '';
}
return /^\d+(\.\d+)?$/.test(value) ? `${value}rpx` : value;
},
getResolvedIconColor(
iconColor: string,
type: string,
plain: boolean,
disabled: boolean,
) {
if (disabled) {
return '#b8b2a6';
}
if (type === 'primary') {
return '#6c5a00';
}
if (type === 'white' && !plain) {
return '#322e25';
}
return iconColor || '#ffffff';
},
onTouchStart() {
if (this.data.disabled || this.data.loading) {
return;
}
this.setData({ pressed: true });
},
onTouchEnd() {
if (!this.data.pressed) {
return;
}
this.setData({ pressed: false });
},
onTouchCancel() {
if (!this.data.pressed) {
return;
}
this.setData({ pressed: false });
},
onClick(e: WechatMiniprogram.TouchEvent): void {
this.triggerEvent('click', e);
},
onGetUserInfo(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('getuserinfo', e.detail);
},
onContact(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('contact', e.detail);
},
onGetPhoneNumber(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('getphonenumber', e.detail);
},
onGetRealTimePhoneNumber(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('getrealtimephonenumber', e.detail);
},
onAgreePrivacyAuthorization(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('agreeprivacyauthorization', e.detail);
},
onError(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('error', e.detail);
},
onLaunchApp(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('launchapp', e.detail);
},
onOpenSetting(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('opensetting', e.detail);
},
onChooseAvatar(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('chooseavatar', e.detail);
},
},
externalClasses: ['custom-class'],
});
+41
View File
@@ -0,0 +1,41 @@
<button
data-id="test-toy-button-id"
class="{{rootClassName}} custom-class"
style="{{rootStyle}}"
session-from="{{ sessionFrom }}"
open-type="{{ disabled || loading ? '' : openType }}"
send-message-title="{{ sendMessageTitle }}"
send-message-path="{{ sendMessagePath }}"
send-message-img="{{ sendMessageImg }}"
show-message-card="{{ showMessageCard }}"
app-parameter="{{ appParameter }}"
aria-label="{{ ariaLabel }}"
bindtouchstart="onTouchStart"
bindtouchend="onTouchEnd"
bindtouchcancel="onTouchCancel"
bind:tap="{{ disabled || loading ? '' : 'onClick'}}"
bindgetuserinfo="onGetUserInfo"
bindcontact="onContact"
bindgetphonenumber="onGetPhoneNumber"
bindgetrealtimephonenumber="onGetRealTimePhoneNumber"
bindagreeprivacyauthorization="onAgreePrivacyAuthorization"
binderror="onError"
bindlaunchapp="onLaunchApp"
bindopensetting="onOpenSetting"
bindchooseavatar="onChooseAvatar">
<block wx:if="{{ loading }}">
<view
class="loading-class toy-button__spinner toy-button__spinner--{{loadingType}}"
style="width:{{resolvedLoadingSize}};height:{{resolvedLoadingSize}};border-color:transparent;border-top-color:{{resolvedLoadingColor}};border-right-color:{{resolvedLoadingColor}};"></view>
</block>
<block wx:else>
<toy-icon
wx:if="{{ icon }}"
name="{{ icon }}"
size="{{ resolvedIconSize }}"
color="{{ resolvedIconColor }}"
class-prefix="{{ iconClassPrefix }}"
custom-class="toy-button__icon" />
</block>
<slot></slot>
</button>