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
+8
View File
@@ -0,0 +1,8 @@
{
"component": true,
"usingComponents": {
"toy-icon": "/toy/icon/icon"
},
"styleIsolation": "apply-shared",
"addGlobalClass": true
}
+317
View File
@@ -0,0 +1,317 @@
@import '../../style/theme.less';
/* ── design tokens(严格对齐 stitch-ui/buttonUi/code.html 色谱) ── */
/* primary 色系 */
@btn-primary-solid: #ffd709; /* primary-container / primary-fixed */
@btn-primary-gradient-from: #ffd709; /* primary-fixed */
@btn-primary-gradient-to: #efc900; /* primary-fixed-dim */
@btn-primary-text: #5b4b00; /* on-primary-container */
@btn-primary-pressed-bg: #5e4e00; /* primary-dim */
@btn-primary-pressed-text: #fff2cd; /* on-primary */
@btn-primary-ring: rgba(108, 90, 0, 0.10);
/* secondary 色系 */
@btn-secondary-bg: #9cd343; /* tertiary-container / tertiary-fixed */
@btn-secondary-text-on-tint: #5a8c1a;
/* error 色系 */
@btn-error-bg: #f95630; /* error-container */
@btn-error-text: #520c00; /* on-error-container */
/* surface 参考 */
@btn-surface-variant: #e5dcc9;
@btn-surface-container-high: #eae2d0;
@btn-surface-container-highest: #e5dcc9;
@btn-outline-variant-20: rgba(179, 172, 159, 0.20);
/* 阴影 */
@btn-shadow-primary: 0 8rpx 24rpx rgba(108, 90, 0, 0.15);
@btn-shadow-primary-lg: 0 8rpx 24rpx rgba(255, 215, 0, 0.3);
@btn-shadow-secondary: 0 8rpx 24rpx rgba(156, 211, 67, 0.18);
@btn-shadow-error: 0 8rpx 24rpx rgba(249, 86, 48, 0.18);
@btn-shadow-default: @shadow;
@btn-shadow-pressed: none;
/* 圆角 */
@btn-radius: @radius-lg;
@btn-radius-sm: 20rpx;
@btn-radius-mini: 18rpx;
/* 过渡 */
@btn-transition: 0.18s ease;
/* 禁用 */
@btn-disabled-bg: @bg-gray;
@btn-disabled-color: @text-disable;
/* ── reset native button chrome ── */
wx-button:not([size=mini]) {
margin-left: auto;
margin-right: auto;
width: auto;
}
/* ── base ── */
.toy-btn {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10rpx;
min-width: 180rpx;
height: 92rpx;
padding: 0 32rpx;
border-radius: @btn-radius;
border: none;
background: @bg-gray;
color: @text-title;
box-shadow: @btn-shadow-default;
font-size: 30rpx;
font-weight: 700;
letter-spacing: 0.5rpx;
text-align: center;
line-height: 1;
transition:
transform @btn-transition,
box-shadow @btn-transition,
background @btn-transition,
color @btn-transition,
opacity @btn-transition;
&::after {
border: none;
}
/* ═══════ type variants ═══════ */
/* 默认:surface-container 底 + on-surface 字 */
&--default {
background: @bg-gray;
color: @text-title;
box-shadow: @btn-shadow-default;
}
/*
* 主色:中号/小号/mini 用纯色 primary-container (#ffd709)
* 大号单独覆盖为渐变(见 &--large 内)
*/
&--primary {
background: @btn-primary-solid;
color: @btn-primary-text;
box-shadow: @btn-shadow-primary;
}
/* 次级(语义名;视觉上目前为绿色) */
&--secondary {
background: @btn-secondary-bg;
color: @text-white;
box-shadow: @btn-shadow-secondary;
}
/* @deprecated 向后兼容,等同 secondary */
&--green {
background: @btn-secondary-bg;
color: @text-white;
box-shadow: @btn-shadow-secondary;
}
/* 危险操作:error-container 底 + on-error-container 深字 */
&--error {
background: @btn-error-bg;
color: @btn-error-text;
box-shadow: @btn-shadow-error;
}
/* 白色 / 浅底 */
&--white {
background: @bg-white;
color: @text-title;
box-shadow: @btn-shadow-default;
}
/* ═══════ size variants ═══════ */
/* 大号 / 全宽:primary 此时启用渐变 + 更强阴影 */
&--large {
width: 100%;
height: 100rpx;
padding: 0 40rpx;
font-size: 34rpx;
&.toy-btn--primary {
background: linear-gradient(135deg, @btn-primary-gradient-from, @btn-primary-gradient-to);
box-shadow: @btn-shadow-primary-lg;
}
}
&--normal {
min-width: 180rpx;
}
&--small {
min-width: 136rpx;
height: 72rpx;
padding: 0 24rpx;
border-radius: @btn-radius-sm;
font-size: 26rpx;
}
&--mini {
min-width: 96rpx;
height: 56rpx;
padding: 0 16rpx;
border-radius: @btn-radius-mini;
font-size: 24rpx;
}
/* ═══════ variant: outlined (描边) ═══════ */
&--outlined {
background: transparent !important;
box-shadow: none !important;
border: 4rpx solid @btn-surface-variant;
&.toy-btn--primary {
border-color: @btn-primary-solid;
color: @btn-primary-text;
}
&.toy-btn--secondary,
&.toy-btn--green {
border-color: @btn-secondary-bg;
color: @btn-secondary-text-on-tint;
}
&.toy-btn--error {
border-color: @btn-error-bg;
color: #b02500;
}
&.toy-btn--default {
border-color: @btn-surface-variant;
color: @text-secondary;
}
}
/* ═══════ variant: ghost (幽灵) ═══════ */
&--ghost {
background: transparent !important;
border: none !important;
box-shadow: none !important;
&.toy-btn--primary {
color: #6c5a00;
}
&.toy-btn--secondary,
&.toy-btn--green {
color: @btn-secondary-text-on-tint;
}
&.toy-btn--error {
color: #b02500;
}
&.toy-btn--default {
color: @text-secondary;
}
&.toy-btn--active {
background: @btn-surface-container-high !important;
}
}
/* ═══════ icon-only(纯图标按钮) ═══════ */
&--icon-only {
min-width: 0;
padding: 0;
gap: 0;
&.toy-btn--large {
width: 128rpx;
height: 128rpx;
border-radius: @btn-radius;
}
&.toy-btn--normal {
width: 112rpx;
height: 112rpx;
border-radius: @btn-radius;
}
&.toy-btn--small {
width: 88rpx;
height: 88rpx;
border-radius: @btn-radius-sm;
}
&.toy-btn--mini {
width: 72rpx;
height: 72rpx;
border-radius: @btn-radius-mini;
}
}
/* ═══════ block ═══════ */
&--block {
display: flex;
width: 100%;
}
/* ═══════ states ═══════ */
/* 通用按下态:缩放 0.95 */
&--active {
transform: scale(0.95);
box-shadow: @btn-shadow-pressed;
}
/* 图标按钮按下态:缩放 0.90 */
&--icon-only&--active {
transform: scale(0.90);
}
/* primary 按下态:切到 primary-dim 底色 + on-primary 字 + ring */
&--primary&--active {
background: @btn-primary-pressed-bg !important;
color: @btn-primary-pressed-text;
box-shadow: 0 0 0 8rpx @btn-primary-ring;
}
&--disabled {
background: @btn-disabled-bg !important;
color: @btn-disabled-color !important;
border-color: transparent !important;
box-shadow: none !important;
opacity: 1;
}
&--loading {
opacity: 0.88;
}
/* ═══════ inner elements ═══════ */
&__icon {
flex-shrink: 0;
}
&__label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__spinner {
box-sizing: border-box;
border-radius: 50%;
border: 4rpx solid transparent;
border-style: solid;
animation: toy-btn-spin 0.8s linear infinite;
flex-shrink: 0;
}
}
@keyframes toy-btn-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
+284
View File
@@ -0,0 +1,284 @@
Component({
options: {
styleIsolation: 'apply-shared',
addGlobalClass: true,
},
properties: {
type: {
type: String,
value: 'default',
},
size: {
type: String,
value: 'normal',
},
variant: {
type: String,
value: 'filled',
},
block: {
type: Boolean,
value: false,
},
disabled: {
type: Boolean,
value: false,
},
loading: {
type: Boolean,
value: false,
},
icon: {
type: String,
value: '',
},
iconOnly: {
type: Boolean,
value: false,
},
iconClassPrefix: {
type: String,
value: 'toy-icon',
},
iconSize: {
type: String,
value: '',
},
iconColor: {
type: String,
value: '',
},
openType: {
type: String,
value: '',
},
style: {
type: String,
value: '',
},
width: {
type: String,
value: '',
},
height: {
type: String,
value: '',
},
loadingSize: {
type: String,
value: '20px',
},
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: '',
},
},
externalClasses: ['custom-class'],
data: {
pressed: false,
rootClassName: '',
rootStyle: '',
resolvedIconColor: '',
resolvedIconSize: '44rpx',
resolvedLoadingColor: '#ffffff',
},
observers: {
'type,size,variant,block,disabled,loading,iconOnly,style,width,height,iconColor,iconSize,pressed':
function () {
this.syncViewState();
},
},
lifetimes: {
attached() {
this.syncViewState();
},
},
methods: {
syncViewState() {
const {
type,
size,
variant,
block,
disabled,
loading,
iconOnly,
style,
width,
height,
iconColor,
iconSize,
pressed,
} = this.data;
const visualType = type === 'green' ? 'secondary' : type;
const cls = ['toy-btn'];
cls.push(`toy-btn--${visualType}`);
cls.push(`toy-btn--${size}`);
if (variant !== 'filled') {
cls.push(`toy-btn--${variant}`);
}
if (block) {
cls.push('toy-btn--block');
}
if (iconOnly) {
cls.push('toy-btn--icon-only');
}
if (disabled) {
cls.push('toy-btn--disabled');
}
if (loading) {
cls.push('toy-btn--loading');
}
if (pressed && !disabled && !loading) {
cls.push('toy-btn--active');
}
const styleParts = [style];
const w = this.normalizeUnit(width);
const h = this.normalizeUnit(height);
if (w) styleParts.push(`width:${w}`);
if (h) styleParts.push(`height:${h}`);
const resolvedIconColor = this.resolveIconColor(
iconColor,
visualType,
variant,
disabled,
);
const resolvedIconSize =
this.normalizeUnit(iconSize) ||
this.getDefaultIconSize(size, iconOnly);
this.setData({
rootClassName: cls.join(' '),
rootStyle: styleParts.filter(Boolean).join(';'),
resolvedIconColor,
resolvedIconSize,
resolvedLoadingColor: resolvedIconColor || '#ffffff',
});
},
normalizeUnit(value: string) {
if (!value) return '';
return /^\d+(\.\d+)?$/.test(value) ? `${value}rpx` : value;
},
getDefaultIconSize(size: string, iconOnly: boolean) {
if (iconOnly) {
const map: Record<string, string> = {
large: '56rpx',
normal: '48rpx',
small: '44rpx',
mini: '40rpx',
};
return map[size] || '48rpx';
}
const map: Record<string, string> = {
large: '44rpx',
normal: '40rpx',
small: '36rpx',
mini: '32rpx',
};
return map[size] || '40rpx';
},
resolveIconColor(
iconColor: string,
type: string,
variant: string,
disabled: boolean,
) {
if (disabled) return '#b8b2a6';
if (iconColor) return iconColor;
if (variant === 'outlined' || variant === 'ghost') {
const outlinedMap: Record<string, string> = {
primary: '#6c5a00',
secondary: '#5a8c1a',
error: '#b02500',
default: '#605b50',
};
return outlinedMap[type] || '#605b50';
}
const filledMap: Record<string, string> = {
primary: '#5b4b00',
error: '#520c00',
default: '#322e25',
white: '#322e25',
};
return filledMap[type] || '#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) {
if (this.data.disabled || this.data.loading) return;
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);
},
},
});
+40
View File
@@ -0,0 +1,40 @@
<button
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="onClick"
bindgetuserinfo="onGetUserInfo"
bindcontact="onContact"
bindgetphonenumber="onGetPhoneNumber"
bindgetrealtimephonenumber="onGetRealTimePhoneNumber"
bindagreeprivacyauthorization="onAgreePrivacyAuthorization"
binderror="onError"
bindlaunchapp="onLaunchApp"
bindopensetting="onOpenSetting"
bindchooseavatar="onChooseAvatar">
<view wx:if="{{ loading }}" class="toy-btn__spinner"
style="width:{{loadingSize}};height:{{loadingSize}};border-top-color:{{resolvedLoadingColor}};border-right-color:{{resolvedLoadingColor}};"></view>
<toy-icon
wx:elif="{{ icon }}"
name="{{ icon }}"
size="{{ resolvedIconSize }}"
color="{{ resolvedIconColor }}"
class-prefix="{{ iconClassPrefix }}"
custom-class="toy-btn__icon" />
<view wx:if="{{ !iconOnly }}" class="toy-btn__label">
<slot></slot>
</view>
</button>