修改河南项目的管理后台ui

This commit is contained in:
2026-09-27 10:00:35 +08:00
parent 266dad03e4
commit 34c7fa9d72
@@ -0,0 +1,187 @@
<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>