241 lines
5.6 KiB
Vue
241 lines
5.6 KiB
Vue
<template>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
:width="width"
|
|
:height="height"
|
|
:viewBox="`4 6 ${switchCount + 2} 4`"
|
|
class="dip-switch"
|
|
>
|
|
<defs>
|
|
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
|
<feFlood
|
|
result="flood"
|
|
flood-color="#f08a5d"
|
|
flood-opacity="1"
|
|
></feFlood>
|
|
<feComposite
|
|
in="flood"
|
|
result="mask"
|
|
in2="SourceGraphic"
|
|
operator="in"
|
|
></feComposite>
|
|
<feMorphology
|
|
in="mask"
|
|
result="dilated"
|
|
operator="dilate"
|
|
radius="0.02"
|
|
></feMorphology>
|
|
<feGaussianBlur in="dilated" stdDeviation="0.05" result="blur1" />
|
|
<feGaussianBlur in="dilated" stdDeviation="0.1" result="blur2" />
|
|
<feGaussianBlur in="dilated" stdDeviation="0.2" result="blur3" />
|
|
<feMerge>
|
|
<feMergeNode in="blur3" />
|
|
<feMergeNode in="blur2" />
|
|
<feMergeNode in="blur1" />
|
|
<feMergeNode in="SourceGraphic" />
|
|
</feMerge>
|
|
</filter>
|
|
</defs>
|
|
<g>
|
|
<rect
|
|
:width="switchCount + 2"
|
|
height="4"
|
|
x="4"
|
|
y="6"
|
|
fill="#c01401"
|
|
rx="0.1"
|
|
/>
|
|
<text
|
|
v-if="props.showLabels"
|
|
fill="white"
|
|
font-size="0.7"
|
|
x="4.25"
|
|
y="6.75"
|
|
>
|
|
ON
|
|
</text>
|
|
<g>
|
|
<template v-for="(_, index) in Array(switchCount)" :key="index">
|
|
<rect
|
|
class="glow interactive"
|
|
@click="toggleBtnStatus(index)"
|
|
width="0.7"
|
|
height="2"
|
|
fill="#68716f"
|
|
:x="5.15 + index"
|
|
y="7"
|
|
rx="0.1"
|
|
/>
|
|
<text
|
|
v-if="props.showLabels"
|
|
:x="5.5 + index"
|
|
y="9.5"
|
|
font-size="0.4"
|
|
text-anchor="middle"
|
|
fill="#444"
|
|
>
|
|
{{ index + 1 }}
|
|
</text>
|
|
</template>
|
|
</g>
|
|
<g>
|
|
<template
|
|
v-for="(location, index) in btnLocation"
|
|
:key="`btn-${index}`"
|
|
>
|
|
<rect
|
|
class="interactive"
|
|
@click="toggleBtnStatus(index)"
|
|
width="0.65"
|
|
height="0.65"
|
|
fill="white"
|
|
:x="5.175 + index"
|
|
:y="location"
|
|
rx="0.1"
|
|
opacity="1"
|
|
/>
|
|
</template>
|
|
</g>
|
|
</g>
|
|
</svg>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { SwitchClient } from "@/APIClient";
|
|
import { AuthManager } from "@/utils/AuthManager";
|
|
import { isUndefined } from "lodash";
|
|
import { ref, computed, watch, onMounted } from "vue";
|
|
|
|
interface Props {
|
|
size?: number;
|
|
enableDigitalTwin?: boolean;
|
|
switchCount?: number;
|
|
initialValues?: string;
|
|
showLabels?: boolean;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
size: 1,
|
|
enableDigitalTwin: false,
|
|
switchCount: 6,
|
|
initialValues: "",
|
|
showLabels: true,
|
|
});
|
|
|
|
const switchCount = computed(() => {
|
|
if (props.enableDigitalTwin) return 5;
|
|
else return props.switchCount;
|
|
});
|
|
|
|
function getClient() {
|
|
return AuthManager.createClient(SwitchClient);
|
|
}
|
|
|
|
// 解析初始值
|
|
function parseInitialValues(): boolean[] {
|
|
if (Array.isArray(props.initialValues)) {
|
|
return [...props.initialValues].slice(0, switchCount.value);
|
|
}
|
|
if (
|
|
typeof props.initialValues === "string" &&
|
|
props.initialValues.trim() !== ""
|
|
) {
|
|
const arr = props.initialValues
|
|
.split(",")
|
|
.map((val) => val.trim() === "1" || val.trim().toLowerCase() === "true");
|
|
while (arr.length < props.switchCount) arr.push(false);
|
|
return arr.slice(0, props.switchCount);
|
|
}
|
|
return Array(switchCount.value).fill(false);
|
|
}
|
|
|
|
// 状态唯一真相
|
|
const btnStatus = ref<boolean[]>(parseInitialValues());
|
|
|
|
// 计算宽高
|
|
const width = computed(() => (switchCount.value * 25 + 20) * props.size);
|
|
const height = computed(() => 85 * props.size);
|
|
|
|
// 按钮位置
|
|
const btnLocation = computed(() =>
|
|
btnStatus.value.map((status) => (status ? 7.025 : 8.325)),
|
|
);
|
|
|
|
// 状态变更统一处理
|
|
function updateStatus(newStates: boolean[], index?: number) {
|
|
btnStatus.value = newStates.slice(0, switchCount.value);
|
|
if (props.enableDigitalTwin) {
|
|
try {
|
|
const client = getClient();
|
|
if (!isUndefined(index))
|
|
client.setSwitchOnOff(index + 1, newStates[index]);
|
|
else client.setMultiSwitchsOnOff(btnStatus.value);
|
|
} catch (error: any) {}
|
|
}
|
|
}
|
|
|
|
// 切换单个
|
|
function toggleBtnStatus(idx: number) {
|
|
if (idx < 0 || idx >= btnStatus.value.length) return;
|
|
const newStates = [...btnStatus.value];
|
|
newStates[idx] = !newStates[idx];
|
|
updateStatus(newStates, idx);
|
|
}
|
|
|
|
// 单个设置
|
|
function setBtnStatus(idx: number, isOn: boolean) {
|
|
if (idx < 0 || idx >= btnStatus.value.length) return;
|
|
const newStates = [...btnStatus.value];
|
|
newStates[idx] = isOn;
|
|
updateStatus(newStates, idx);
|
|
}
|
|
|
|
// 监听 props 变化只同步一次
|
|
watch(
|
|
() => props.enableDigitalTwin,
|
|
(newVal) => {
|
|
const client = getClient();
|
|
client.setEnable(newVal);
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
watch(
|
|
() => [switchCount.value, props.initialValues],
|
|
() => {
|
|
btnStatus.value = parseInitialValues();
|
|
updateStatus(btnStatus.value);
|
|
},
|
|
);
|
|
</script>
|
|
|
|
<style scoped lang="postcss">
|
|
.dip-switch {
|
|
display: block;
|
|
padding: 0;
|
|
margin: 0;
|
|
line-height: 0;
|
|
font-size: 0;
|
|
box-sizing: content-box;
|
|
overflow: visible;
|
|
}
|
|
rect {
|
|
transition: all 100ms ease-in-out;
|
|
}
|
|
.interactive {
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
|
|
<script lang="ts">
|
|
export function getDefaultProps() {
|
|
return {
|
|
size: 1,
|
|
enableDigitalTwin: false,
|
|
switchCount: 6,
|
|
initialValues: "",
|
|
showLabels: true,
|
|
};
|
|
}
|
|
</script>
|