Files
henan-backend/ruoyi-ui/src/components/CapsuleSwitch/index.vue
T

188 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!--
胶囊开关(替代 el-switch,用于后台所有布尔/状态列)
## 为什么自己写一个,而不是继续改 el-switch 的样式
Element 的 el-switch 把「轨道」「滑块」和「左右两段文字」拆成四个各自定位的元素,
文字用 is-active 控制显隐、轨道用 CSS 变量上色。后台原先 17 个页面各自拿
::v-deep 去覆盖它,改出来的东西越修越乱(滑块跑出轨道、两段文字同时出现),
**连续三次返工都没收敛**。这里改成自己拼 DOM:一个轨道容器 + 一个滑块 + 一段文字,
任何时刻只有一段文字、位置全部由本组件算好,不再依赖 Element 内部结构。
## 视觉规则(用户 2026-09-24 定的口径)
| 值 | 轨道 | 滑块 | 文字 |
|---|---|---|---|
| 真(生效) | 绿 #457450 | 白圆点停在**右侧** | 「启用」白字,落在左半边 |
| 假(未生效) | 白 + 细描边 | 白圆点停在**左侧** | 「禁用」灰字,落在右半边 |
⇒ **绿 = 该项生效,白 = 该项未生效**;文字是结果,整颗开关只出现一次。
## 尺寸要点(改尺寸必读)
容器 `66×26`;滑块 `22` 停在 `left:2`(开启 `translateX(38px)`);文字框宽 `36`,
关闭态 `left:26`、开启态 `left:3` —— **文字只在滑块不在的那半边**,两态重叠宽度都是 0。
⚠️ 早期版本是 54px 宽、文字框横跨整条轨道(`left:11; width:34`),结果**滑块压住文字**
(用户 2026-09-24 反馈「启用」的「用」被盖掉)。**根因是文字框与滑块占用了同一段横向空间**,
不是 z-index 或颜色问题。加宽容器把两者分到各半边才根治。
## 用法(与 el-switch 的 v-model 语义一致,可直接替换)
<capsule-switch v-model="scope.row.status" @change="save(scope.row)" />
// 自定义两端文案
<capsule-switch v-model="row.isDefault" active-text="是" inactive-text="否" />
// 库里是 '0'/'1' 字符串时(Element 写法兼容)
<capsule-switch v-model="row.status" active-value="0" inactive-value="1" />
若页面用 `:active-value` / `:inactive-value` 传了非布尔值,本组件内部把
传入值与 activeValue 比较得出开关位置,change 时再回吐对应的原始值,
⇒ 父组件拿到的仍然是原来的 '0'/'1',**不必改后端契约**。
-->
<div
class="capsule-switch"
:class="{ 'is-on': isOn, 'is-disabled': disabled }"
role="switch"
:aria-checked="isOn"
:aria-disabled="disabled"
tabindex="0"
@click="toggle"
@keydown.enter.prevent="toggle"
@keydown.space.prevent="toggle"
>
<span class="capsule-switch__text">{{ isOn ? activeText : inactiveText }}</span>
<span class="capsule-switch__dot"></span>
</div>
</template>
<script>
export default {
name: 'CapsuleSwitch',
props: {
// 兼容 el-switch:父组件既可能传布尔,也可能传 '0'/'1'
value: {
type: [Boolean, String, Number],
default: false
},
activeValue: {
type: [Boolean, String, Number],
default: true
},
inactiveValue: {
type: [Boolean, String, Number],
default: false
},
activeText: {
type: String,
default: '启用'
},
inactiveText: {
type: String,
default: '禁用'
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
isOn() {
return this.value === this.activeValue;
}
},
methods: {
toggle() {
if (this.disabled) return;
const next = this.isOn ? this.inactiveValue : this.activeValue;
this.$emit('input', next);
this.$emit('change', next);
}
}
};
</script>
<style lang="scss" scoped>
/* 尺寸常量:宽 66、高 26,滑块 22,两端留 2px 缝
⚠️ 改尺寸必须重算 —— 约束是「文字不能被滑块压住」:
关闭:滑块停在左(x 3~25),文字只占右半边(x 27~63),两者不重叠
开启:滑块停在右(x 41~63),文字只占左半边(x 3~39),两者不重叠
容器宽度必须 ≥ 滑块(22) + 文字所需宽度(36) + 两侧留白(约 8),
54px 那版文字框横跨整条轨道 ⇒ 必然被滑块压住(用户 2026-09-24 反馈的正是这个)。 */
.capsule-switch {
position: relative;
display: inline-block;
width: 66px;
height: 26px;
border-radius: 13px;
border: 1px solid #d8c3b0;
background: #fffdfa;
cursor: pointer;
vertical-align: middle;
transition: background-color 0.3s, border-color 0.3s;
user-select: none;
outline: none;
&:focus-visible {
box-shadow: 0 0 0 3px rgba(69, 116, 80, 0.22);
}
/* 滑块:白色正圆 */
&__dot {
position: absolute;
top: 1px;
left: 2px;
width: 22px;
height: 22px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(139, 115, 85, 0.3);
transition: transform 0.3s;
z-index: 2;
}
/* 文字:整颗开关只渲染这一段,换的是内容不是显隐 ⇒ 不可能出现两段文字。
⚠️ 必须 z-index 高于滑块,否则关闭态下会被白圆点盖住(「启用」= 绿底白字,压在绿块上反而难读)。 */
&__text {
position: absolute;
top: 0;
left: 26px;
width: 36px;
height: 24px;
line-height: 24px;
font-size: 11px;
font-weight: 500;
text-align: center;
color: #8b755a;
transition: color 0.3s, left 0.3s;
pointer-events: none;
z-index: 3;
}
&__dot,
&__text {
pointer-events: none;
}
/* ---- 开启:绿底 + 滑块右移 38(2 → 40),文字换到左半边并改白 ---- */
&.is-on {
border-color: #457450;
background: #457450;
.capsule-switch__dot {
transform: translateX(38px);
}
.capsule-switch__text {
left: 3px;
color: #fff;
}
}
&.is-disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
</style>