feat: remake most of forntend

This commit is contained in:
alivender
2025-04-26 19:59:35 +08:00
parent 4e741f9ef8
commit bc4f44ecaa
41 changed files with 84095 additions and 672 deletions

View File

@@ -0,0 +1,48 @@
<template> <div class="ddr-component" :style="{ width: width + 'px', height: height + 'px' }">
<img
src="../equipments/svg/ddr.svg"
:width="width"
:height="height"
alt="DDR内存"
class="svg-image"
draggable="false"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
size?: number;
}
const props = withDefaults(defineProps<Props>(), {
size: 1
});
// 计算实际宽高
const width = computed(() => 120 * props.size);
const height = computed(() => 80 * props.size);
</script>
<style scoped>
.ddr-component {
display: block;
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
}
.svg-image {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none; /* 禁止鼠标交互 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
</style>

View File

@@ -0,0 +1,48 @@
<template> <div class="eth-component" :style="{ width: width + 'px', height: height + 'px' }">
<img
src="../equipments/svg/eth.svg"
:width="width"
:height="height"
alt="以太网接口"
class="svg-image"
draggable="false"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
size?: number;
}
const props = withDefaults(defineProps<Props>(), {
size: 1
});
// 计算实际宽高
const width = computed(() => 100 * props.size);
const height = computed(() => 60 * props.size);
</script>
<style scoped>
.eth-component {
display: block;
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
}
.svg-image {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none; /* 禁止鼠标交互 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
</style>

View File

@@ -0,0 +1,48 @@
<template> <div class="hdmi-component" :style="{ width: width + 'px', height: height + 'px' }">
<img
src="../equipments/svg/hdmi.svg"
:width="width"
:height="height"
alt="HDMI接口"
class="svg-image"
draggable="false"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
size?: number;
}
const props = withDefaults(defineProps<Props>(), {
size: 1
});
// 计算实际宽高
const width = computed(() => 100 * props.size);
const height = computed(() => 50 * props.size);
</script>
<style scoped>
.hdmi-component {
display: block;
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
}
.svg-image {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none; /* 禁止鼠标交互 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
</style>

View File

@@ -1,7 +1,13 @@
<template>
<div class="relative" :style="{width: `${props.width}px`, height: `${props.height}px`}">
<!-- 简化的SVG按钮 -->
<svg xmlns="http://www.w3.org/2000/svg" :width="props.width" :height="props.height" viewBox="0 0 1600 1600">
<template>
<div class="button-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }">
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="400 400 800 800"
class="mechanical-button"
>
<!-- defs 和按钮底座保持不变 -->
<defs>
<filter id="btn-shadow">
<feGaussianBlur in="SourceAlpha" stdDeviation="20" result="blur" />
@@ -11,7 +17,7 @@
<feMergeNode in="offsetBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</filter>
<linearGradient id="normal" gradientTransform="rotate(45 0 0)">
<stop stop-color="#4b4b4b" offset="0" />
<stop stop-color="#171717" offset="1" />
@@ -21,35 +27,34 @@
<stop stop-color="#4b4b4b" offset="1" />
</linearGradient>
</defs>
<!-- 按钮底座 -->
<rect width="800" height="800" x="400" y="400" fill="#464646" rx="20" />
<rect width="700" height="700" x="450" y="450" fill="#eaeaea" rx="20" />
<!-- 装饰螺丝 -->
<circle r="20" cx="1075" cy="1075" fill="#171717" />
<circle r="20" cx="1075" cy="525" fill="#171717" />
<circle r="20" cx="525" cy="525" fill="#171717" />
<circle r="20" cx="525" cy="1075" fill="#171717" />
<!-- 按钮主体 -->
<circle r="220" cx="800" cy="800" fill="black" filter="url(#btn-shadow)" />
<circle
:r="btnHeight"
cx="800"
cy="800"
:fill="isKeyPressed ? 'url(#pressed)' : 'url(#normal)'"
<circle
:r="btnHeight"
cx="800"
cy="800"
:fill="isKeyPressed ? 'url(#pressed)' : 'url(#normal)'"
fill-opacity="0.9"
@mousedown="toggleButtonState(true)"
@mouseup="toggleButtonState(false)"
@contextmenu.prevent="openContextMenu($event)"
style="pointer-events: auto; transition: all 20ms ease-in-out;"
@mousedown="toggleButtonState(true)"
@mouseup="toggleButtonState(false)"
@mouseleave="toggleButtonState(false)"
style="pointer-events: auto; transition: all 20ms ease-in-out; cursor: pointer;"
/>
<!-- 按键文字 -->
<text
v-if="bindKey"
x="800"
<text
v-if="displayText"
x="800"
y="800"
font-size="310"
text-anchor="middle"
@@ -57,98 +62,169 @@
fill="#ccc"
style="font-family: Arial; filter: url(#btn-shadow); user-select: none; pointer-events: none; mix-blend-mode: overlay;"
>
{{ bindKey.toUpperCase() }}
{{ displayText }}
</text>
</svg>
<!-- 使用DaisyUI的卡片组件实现上下文菜单 -->
<div v-if="showContextMenu"
class="card card-compact fixed z-50 shadow-lg bg-base-100 border border-base-300"
:style="{ top: contextMenuY + 'px', left: contextMenuX + 'px' }"
@click.stop>
<div class="card-body p-0">
<button class="btn btn-ghost justify-start normal-case w-full h-full" @click="startBinding">
<span v-if="isBinding">请输入</span>
<span v-else>绑定按键: {{ bindKey ? bindKey.toUpperCase() : '未绑定' }}</span>
</button>
</div>
<!-- 嵌入Pin组件覆盖在按钮上 -->
<div class="pin-wrapper" :style="{
position: 'absolute',
top: '80%',
left: '50%',
transform: 'translate(-50%, 20%)',
zIndex: 3,
pointerEvents: 'auto'
}">
<Pin
direction="output"
type="digital"
appearance="None"
:label="props.label"
:constraint="props.constraint"
:size="0.8"
@value-change="handlePinValueChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { ref, onMounted, onUnmounted, computed } from 'vue';
import Pin from './Pin.vue';
interface Props {
width?: string | number
height?: string | number
// 从Pin组件继承属性
interface PinProps {
label?: string;
constraint?: string;
// 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
appearance?: 'None' | 'Dip' | 'SMT';
}
// 按钮特有属性
interface ButtonProps {
size?: number;
bindKey?: string;
buttonText?: string;
}
// 组合两个接口
interface Props extends PinProps, ButtonProps {}
const props = withDefaults(defineProps<Props>(), {
width: 160,
height: 160,
})
size: 1,
bindKey: '',
buttonText: '',
label: 'BTN',
constraint: '',
// 这些值会被覆盖,但需要默认值以满足类型要求
direction: 'output',
type: 'digital',
appearance: 'Dip'
});
const bindKey = ref('');
let isKeyPressed = false;
// 计算实际宽高
const width = computed(() => 160 * props.size);
const height = computed(() => 160 * props.size);
// 计算文本显示内容
const displayText = computed(() => {
if (props.buttonText) return props.buttonText;
return props.bindKey ? props.bindKey.toUpperCase() : '';
});
// 定义组件发出的事件
const emit = defineEmits([
'update:bindKey',
'update:label',
'update:constraint',
'press',
'release',
'click',
'value-change'
]);
// 内部状态
const isKeyPressed = ref(false);
const btnHeight = ref(200);
const colorMatrix = ref("1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0");
const isBinding = ref(false);
const showContextMenu = ref(false);
const contextMenuX = ref(0);
const contextMenuY = ref(0);
// 处理Pin值变化
function handlePinValueChange(value: any) {
emit('value-change', value);
}
// --- 按键状态逻辑 ---
function toggleButtonState(isPressed: boolean) {
btnHeight.value = isPressed ? 210 : 200;
colorMatrix.value = isPressed
? "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.7 0"
: "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0";
isKeyPressed.value = isPressed;
btnHeight.value = isPressed ? 180 : 200;
// 发出事件通知父组件
if (isPressed) {
emit('press');
} else {
emit('release');
emit('click');
}
}
function openContextMenu(e: MouseEvent) {
contextMenuX.value = e.clientX;
contextMenuY.value = e.clientY;
showContextMenu.value = true;
}
function closeContextMenu() {
showContextMenu.value = false;
}
function startBinding() {
if (isBinding.value) return;
isBinding.value = true;
window.addEventListener('keydown', onBindingKeyDown, { once: true });
}
function onBindingKeyDown(e: KeyboardEvent) {
bindKey.value = e.key.toLowerCase();
isBinding.value = false;
}
function handleKeyDown(e: KeyboardEvent) {
if (e.key.toLowerCase() === bindKey.value && !isKeyPressed) {
isKeyPressed = true;
// 处理键盘事件
function handleKeyDown(event: KeyboardEvent) {
if (event.key === props.bindKey) {
toggleButtonState(true);
setTimeout(() => toggleButtonState(false), 150);
}
}
function handleKeyUp(e: KeyboardEvent) {
if (e.key.toLowerCase() === bindKey.value) {
isKeyPressed = false;
toggleButtonState(false);
}
}
// --- 生命周期钩子 ---
onMounted(() => {
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
window.addEventListener('click', closeContextMenu);
document.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
window.removeEventListener('click', closeContextMenu);
document.removeEventListener('keydown', handleKeyDown);
});
// 向外暴露方法
defineExpose({
toggleButtonState,
getInfo: () => ({
// 按钮特有属性
bindKey: props.bindKey,
buttonText: props.buttonText,
// 继承自Pin的属性
label: props.label,
constraint: props.constraint,
// 固定的Pin属性
direction: 'output',
type: 'digital',
appearance: 'None'
})
});
</script>
<style scoped>
.button-container {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.mechanical-button {
display: block;
padding: 0;
margin: 0;
line-height: 0;
font-size: 0;
box-sizing: content-box;
overflow: visible;
}
.pin-wrapper {
width: 100%;
display: flex;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,220 @@
<template> <div class="motherboard-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }"> <!-- 主板 SVG -->
<img
src="../equipments/svg/motherboard.svg"
:width="width"
:height="height"
alt="主板"
class="svg-image"
draggable="false"
/>
<!-- 嵌入各种组件 --> <!-- HDMI -->
<div class="component-wrapper hdmi-wrapper" :style="{
position: 'absolute',
top: `${140 * props.size}px`,
left: `${-48 * props.size}px`,
zIndex: 10
}">
<HDMI :size="1.5*props.size" />
</div>
<!-- HDMI -->
<div class="component-wrapper hdmi-wrapper" :style="{
position: 'absolute',
top: `${260 * props.size}px`,
left: `${-48 * props.size}px`,
zIndex: 10
}">
<HDMI :size="1.5*props.size" />
</div> <!-- ETH -->
<div class="component-wrapper eth-wrapper" :style="{
position: 'absolute',
top: `${365 * props.size}px`,
left: `${-10 * props.size}px`,
zIndex: 10
}">
<ETH :size="1.5*props.size" />
</div>
<!-- DDR -->
<div class="component-wrapper ddr-wrapper" :style="{
position: 'absolute',
top: `${224 * props.size}px`,
right: `${250 * props.size}px`,
zIndex: 10
}">
<DDR :size="1.2*props.size" />
</div>
<!-- SD -->
<div class="component-wrapper sd-wrapper" :style="{
position: 'absolute',
bottom: `${130 * props.size}px`,
right: `${172 * props.size}px`,
zIndex: 10
}">
<SD :size="1.2*props.size" />
</div>
<!-- SFP -->
<div class="component-wrapper sfp-wrapper" :style="{
position: 'absolute',
bottom: `${210 * props.size}px`,
right: `${-46 * props.size}px`,
zIndex: 10
}">
<SFP :size="1.84*props.size" />
</div>
<!-- SFP -->
<div class="component-wrapper sfp-wrapper" :style="{
position: 'absolute',
bottom: `${290 * props.size}px`,
right: `${-46 * props.size}px`,
zIndex: 10
}">
<SFP :size="1.84*props.size" />
</div>
<!-- SMA -->
<div class="component-wrapper sma-wrapper" :style="{
position: 'absolute',
top: `${110 * props.size}px`,
right: `${204 * props.size}px`,
zIndex: 10
}">
<SMA :size="0.75*props.size" />
</div>
<!-- SMA -->
<div class="component-wrapper sma-wrapper" :style="{
position: 'absolute',
top: `${170 * props.size}px`,
right: `${204 * props.size}px`,
zIndex: 10
}">
<SMA :size="0.75*props.size" />
</div>
<!-- SMA -->
<div class="component-wrapper sma-wrapper" :style="{
position: 'absolute',
top: `${250 * props.size}px`,
right: `${204 * props.size}px`,
zIndex: 10
}">
<SMA :size="0.75*props.size" />
</div>
<!-- SMA -->
<div class="component-wrapper sma-wrapper" :style="{
position: 'absolute',
top: `${310 * props.size}px`,
right: `${204 * props.size}px`,
zIndex: 10
}">
<SMA :size="0.75*props.size" />
</div>
<!-- BUTTON -->
<div class="component-wrapper button-wrapper" :style="{
position: 'absolute',
bottom: `${140 * props.size}px`,
right: `${430 * props.size}px`,
zIndex: 10
}">
<MechanicalButton :size="0.175*props.size" />
</div>
<div class="component-wrapper button-wrapper" :style="{
position: 'absolute',
bottom: `${140 * props.size}px`,
right: `${397 * props.size}px`,
zIndex: 10
}">
<MechanicalButton :size="0.175*props.size" />
</div>
<div class="component-wrapper button-wrapper" :style="{
position: 'absolute',
bottom: `${140 * props.size}px`,
right: `${364 * props.size}px`,
zIndex: 10
}">
<MechanicalButton :size="0.175*props.size" />
</div>
<div class="component-wrapper button-wrapper" :style="{
position: 'absolute',
bottom: `${140 * props.size}px`,
right: `${331 * props.size}px`,
zIndex: 10
}">
<MechanicalButton :size="0.175*props.size" />
</div>
<div class="component-wrapper button-wrapper" :style="{
position: 'absolute',
bottom: `${140 * props.size}px`,
right: `${298 * props.size}px`,
zIndex: 10
}">
<MechanicalButton :size="0.175*props.size" />
</div>
<div class="component-wrapper button-wrapper" :style="{
position: 'absolute',
bottom: `${140 * props.size}px`,
right: `${265 * props.size}px`,
zIndex: 10
}">
<MechanicalButton :size="0.175*props.size" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import HDMI from './HDMI.vue';
import DDR from './DDR.vue';
import ETH from './ETH.vue';
import SD from './SD.vue';
import SFP from './SFP.vue';
import SMA from './SMA.vue';
import MechanicalButton from './MechanicalButton.vue';
interface Props {
size?: number;
}
const props = withDefaults(defineProps<Props>(), {
size: 1
});
// 计算实际宽高
const width = computed(() => 800 * props.size);
const height = computed(() => 600 * props.size);
// 向外暴露方法
defineExpose({
getInfo: () => ({
size: props.size
})
});
</script>
<style scoped>
.motherboard-container {
display: block;
position: relative;
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
}
.svg-image {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none; /* 禁止鼠标交互 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.component-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
:viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
class="pin-component"
>
<g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
<g v-if="props.appearance === 'None'">
<g transform="translate(-12.5, -12.5)" class="interactive">
<circle
style="fill:#909090"
cx="12.5"
cy="12.5"
r="3.75" />
</g>
</g>
<g v-else-if="props.appearance === 'Dip'">
<!-- 使用inkscape创建的SVG替代原有Dip样式 -->
<g transform="translate(-12.5, -12.5)" class="interactive">
<rect
:style="`fill:${props.type === 'analog' ? '#2a6099' : '#000000'};fill-opacity:0.772973`"
width="25"
height="25"
x="0"
y="0"
rx="2.5" />
<circle
style="fill:#ecececc5;fill-opacity:0.772973"
cx="12.5"
cy="12.5"
r="3.75" />
<text
style="font-size:6.85px;text-align:start;fill:#ffffff;fill-opacity:0.772973"
x="7.3"
y="7"
xml:space="preserve">{{ props.label }}</text>
</g>
</g>
<g v-else-if="props.appearance === 'SMT'">
<rect x="-20" y="-10" width="40" height="20" fill="#aaa" rx="2" ry="2" />
<rect x="-18" y="-8" width="36" height="16" :fill="getColorByType" rx="1" ry="1" />
<rect x="-16" y="-6" width="26" height="12" :fill="getColorByType" rx="1" ry="1" />
<text text-anchor="middle" dominant-baseline="middle" font-size="8" fill="white" x="-3">{{ props.label }}</text>
</g>
</g>
</svg>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
interface Props {
size?: number;
label?: string;
constraint?: string;
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
appearance?: 'None' | 'Dip' | 'SMT';
}
const props = withDefaults(defineProps<Props>(), {
size: 1,
label: 'PIN',
constraint: '',
direction: 'input',
type: 'digital',
appearance: 'Dip'
});
const emit = defineEmits([
'value-change'
]);
// 内部状态
const analogValue = ref(0);
const width = computed(() => props.appearance === 'None' ? 40 * props.size : 30 * props.size);
const height = computed(() => {
if (props.appearance === 'None') return 20 * props.size;
if (props.appearance === 'Dip') return 30 * props.size; // 调整Dip样式高度
return 60 * props.size;
});
const viewBoxWidth = computed(() => props.appearance === 'None' ? 40 : 30);
const viewBoxHeight = computed(() => {
if (props.appearance === 'None') return 20;
if (props.appearance === 'Dip') return 30; // 调整Dip样式视图高度
return 60;
});
const getColorByType = computed(() => {
return props.type === 'analog' ? '#2a6099' : '#444';
});
function updateAnalogValue(value: number) {
if (props.type !== 'analog') return;
analogValue.value = Math.max(0, Math.min(1, value));
emit('value-change', {
label: props.label,
constraint: props.constraint,
value: analogValue.value
});
}
defineExpose({
setAnalogValue: updateAnalogValue,
getAnalogValue: () => analogValue.value,
getInfo: () => ({
label: props.label,
constraint: props.constraint,
direction: props.direction,
type: props.type,
appearance: props.appearance
})
});
</script>
<style scoped>
.pin-component {
display: block;
user-select: none;
}
.interactive {
cursor: pointer;
transition: filter 0.2s;
}
.interactive:hover {
filter: brightness(1.2);
}
</style>

View File

@@ -0,0 +1,48 @@
<template> <div class="sd-component" :style="{ width: width + 'px', height: height + 'px' }">
<img
src="../equipments/svg/sd.svg"
:width="width"
:height="height"
alt="SD卡插槽"
class="svg-image"
draggable="false"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
size?: number;
}
const props = withDefaults(defineProps<Props>(), {
size: 1
});
// 计算实际宽高
const width = computed(() => 80 * props.size);
const height = computed(() => 60 * props.size);
</script>
<style scoped>
.sd-component {
display: block;
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
}
.svg-image {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none; /* 禁止鼠标交互 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
</style>

View File

@@ -0,0 +1,48 @@
<template> <div class="sfp-component" :style="{ width: width + 'px', height: height + 'px' }">
<img
src="../equipments/svg/sfp.svg"
:width="width"
:height="height"
alt="SFP光纤模块"
class="svg-image"
draggable="false"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
size?: number;
}
const props = withDefaults(defineProps<Props>(), {
size: 1
});
// 计算实际宽高
const width = computed(() => 120 * props.size);
const height = computed(() => 40 * props.size);
</script>
<style scoped>
.sfp-component {
display: block;
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
}
.svg-image {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none; /* 禁止鼠标交互 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
</style>

View File

@@ -0,0 +1,48 @@
<template> <div class="sma-component" :style="{ width: width + 'px', height: height + 'px' }">
<img
src="../equipments/svg/sma.svg"
:width="width"
:height="height"
alt="SMA连接器"
class="svg-image"
draggable="false"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
size?: number;
}
const props = withDefaults(defineProps<Props>(), {
size: 1
});
// 计算实际宽高
const width = computed(() => 40 * props.size);
const height = computed(() => 40 * props.size);
</script>
<style scoped>
.sma-component {
display: block;
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
}
.svg-image {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none; /* 禁止鼠标交互 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
</style>

View File

@@ -0,0 +1,193 @@
<template>
<div class="led-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }">
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="0 0 100 60"
class="smt-led"
>
<!-- LED 基座 -->
<rect width="100" height="60" x="0" y="0" fill="#333" rx="5" ry="5" />
<!-- LED 主体 -->
<rect width="90" height="50" x="5" y="5" fill="#222" rx="3" ry="3" />
<!-- LED 发光部分 -->
<rect
width="70"
height="30"
x="15"
y="15"
:fill="ledColor"
:style="{ opacity: isOn ? brightness/100 : 0.2 }"
rx="15"
ry="15"
@click="toggleLed"
class="interactive"
/>
<!-- LED 光晕效果 -->
<rect
v-if="isOn"
width="76"
height="36"
x="12"
y="12"
:fill="ledColor"
:style="{ opacity: brightness/100 * 0.3 }"
rx="18"
ry="18"
filter="blur(5px)"
class="glow"
/>
</svg>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
// LED特有属性
interface Props {
size?: number;
color?: string;
initialOn?: boolean;
brightness?: number;
constraint?: string;
}
// 组件属性定义
const props = withDefaults(defineProps<Props>(), {
size: 1,
color: 'red',
initialOn: false,
brightness: 80, // 亮度默认为80%
constraint: ''
});
// 计算实际宽高
const width = computed(() => 100 * props.size);
const height = computed(() => 60 * props.size);
// 内部状态
const isOn = ref(props.initialOn);
const brightness = ref(props.brightness);
// LED 颜色映射表
const colorMap: Record<string, string> = {
'red': '#ff3333',
'green': '#33ff33',
'blue': '#3333ff',
'yellow': '#ffff33',
'orange': '#ff9933',
'white': '#ffffff',
'purple': '#9933ff'
};
// 计算实际LED颜色
const ledColor = computed(() => {
return colorMap[props.color.toLowerCase()] || props.color;
});
// 定义组件发出的事件
const emit = defineEmits([
'toggle',
'brightness-change',
'value-change'
]);
// 手动切换LED状态
function toggleLed() {
isOn.value = !isOn.value;
emit('toggle', isOn.value);
emit('value-change', {
isOn: isOn.value,
brightness: brightness.value
});
}
// 设置亮度
function setBrightness(value: number) {
// 限制亮度值在0-100范围内
brightness.value = Math.max(0, Math.min(100, value));
emit('brightness-change', brightness.value);
emit('value-change', {
isOn: isOn.value,
brightness: brightness.value
});
}
// 手动设置LED开关状态
function setLedState(on: boolean) {
isOn.value = on;
emit('toggle', isOn.value);
emit('value-change', {
isOn: isOn.value,
brightness: brightness.value
});
}
// 监听props变化
watch(() => props.brightness, (newVal) => {
brightness.value = newVal;
});
watch(() => props.initialOn, (newVal) => {
isOn.value = newVal;
});
// 向外暴露方法
defineExpose({
toggleLed,
setBrightness,
setLedState,
getInfo: () => ({
// LED特有属性
color: props.color,
isOn: isOn.value,
brightness: brightness.value,
constraint: props.constraint
})
});
</script>
<style scoped>
.led-container {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.smt-led {
display: block;
padding: 0;
margin: 0;
line-height: 0;
font-size: 0;
box-sizing: content-box;
overflow: visible;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.interactive {
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.interactive:hover {
filter: brightness(1.2);
}
.glow {
pointer-events: none;
}
</style>

View File

@@ -1,6 +1,13 @@
// filepath: c:\_Project\FPGA_WebLab\FPGA_WebLab\src\components\equipments\Switch.vue
<template>
<svg xmlns="http://www.w3.org/2000/svg" :width="props.width" :height="props.height" viewBox="0 0 16 16">
<def>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
:viewBox="`4 6 ${props.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>
@@ -15,49 +22,130 @@
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</def>
</defs>
<g>
<rect width="8" height="4" x="4" y="6" fill="#c01401" rx="0.1" />
<text fill="white" font-size="0.7" x="4.25" y="6.75">ON</text>
<!-- 红色背景随开关数量变化宽度 -->
<rect :width="props.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>
<rect class="glow" @click="toggleBtnStatus(0)" width="0.7" height="2" fill="#68716f" x="5.15" y="7" rx="0.1" />
<rect class="glow" @click="toggleBtnStatus(1)" width="0.7" height="2" fill="#68716f" x="6.15" y="7" rx="0.1" />
<rect class="glow" @click="toggleBtnStatus(2)" width="0.7" height="2" fill="#68716f" x="7.15" y="7" rx="0.1" />
<rect class="glow" @click="toggleBtnStatus(3)" width="0.7" height="2" fill="#68716f" x="8.15" y="7" rx="0.1" />
<rect class="glow" @click="toggleBtnStatus(4)" width="0.7" height="2" fill="#68716f" x="9.15" y="7" rx="0.1" />
<rect class="glow" @click="toggleBtnStatus(5)" width="0.7" height="2" fill="#68716f" x="10.15" y="7" rx="0.1" />
<template v-for="(_, index) in Array(props.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>
<rect @click="toggleBtnStatus(0)" width="0.65" height="0.65" fill="white" x="5.175" :y="btnLocation[0]" rx="0.1"
opacity="1" />
<rect @click="toggleBtnStatus(1)" width="0.65" height="0.65" fill="white" x="6.175" :y="btnLocation[1]" rx="0.1"
opacity="1" />
<rect @click="toggleBtnStatus(2)" width="0.65" height="0.65" fill="white" x="7.175" :y="btnLocation[2]" rx="0.1"
opacity="1" />
<rect @click="toggleBtnStatus(3)" width="0.65" height="0.65" fill="white" x="8.175" :y="btnLocation[3]" rx="0.1"
opacity="1" />
<rect @click="toggleBtnStatus(4)" width="0.65" height="0.65" fill="white" x="9.175" :y="btnLocation[4]" rx="0.1"
opacity="1" />
<rect @click="toggleBtnStatus(5)" width="0.65" height="0.65" fill="white" x="10.175" :y="btnLocation[5]"
rx="0.1" opacity="1" />
<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 { computed, ref } from "vue";
import { computed, ref, watch } from "vue";
interface Props {
width?: string | number;
height?: string | number;
size?: number;
switchCount?: number;
// 新增属性
initialValues?: boolean[] | string; // 开关的初始状态,可以是布尔数组或逗号分隔的字符串
showLabels?: boolean; // 是否显示标签
}
const props = withDefaults(defineProps<Props>(), {
width: 160,
height: 160,
size: 1,
switchCount: 6,
initialValues: () => [],
showLabels: true
});
// 计算实际宽高
const width = computed(() => {
// 每个开关占用25px宽度再加上两侧边距(20px)
return (props.switchCount * 25 + 20) * props.size;
});
const height = computed(() => 85 * props.size); // 高度保持固定比例
// 定义发出的事件
const emit = defineEmits(['change', 'switch-toggle']);
// 解析初始值,支持字符串和数组两种格式
const parseInitialValues = () => {
if (Array.isArray(props.initialValues)) {
return [...props.initialValues].slice(0, props.switchCount);
} else if (typeof props.initialValues === 'string' && props.initialValues.trim() !== '') {
// 将逗号分隔的字符串转换为布尔数组
const values = props.initialValues.split(',')
.map(val => val.trim() === '1' || val.trim().toLowerCase() === 'true')
.slice(0, props.switchCount);
// 如果数组长度小于开关数量,用 false 填充
while (values.length < props.switchCount) {
values.push(false);
}
return values;
}
// 默认返回全部为 false 的数组
return Array(props.switchCount).fill(false);
};
// 初始化按钮状态
const btnStatus = ref(parseInitialValues());
// 监听 switchCount 变化,调整开关状态数组
watch(() => props.switchCount, (newCount) => {
if (newCount !== btnStatus.value.length) {
// 如果新数量大于当前数量,则扩展数组
if (newCount > btnStatus.value.length) {
btnStatus.value = [
...btnStatus.value,
...Array(newCount - btnStatus.value.length).fill(false)
];
} else {
// 如果新数量小于当前数量,则截断数组
btnStatus.value = btnStatus.value.slice(0, newCount);
}
}
}, { immediate: true });
// 监听 initialValues 变化,更新开关状态
watch(() => props.initialValues, () => {
btnStatus.value = parseInitialValues();
});
const btnStatus = ref([false, false, false, false, false, false]);
const btnLocation = computed(() => {
return btnStatus.value.map((status) => {
return status ? 7.025 : 8.325;
@@ -65,20 +153,58 @@ const btnLocation = computed(() => {
});
function setBtnStatus(btnNum: number, isOn: boolean): void {
btnStatus.value[btnNum] = isOn;
if (btnNum >= 0 && btnNum < btnStatus.value.length) {
btnStatus.value[btnNum] = isOn;
emit('change', { index: btnNum, value: isOn, states: [...btnStatus.value] });
}
}
function toggleBtnStatus(btnNum: number): void {
btnStatus.value[btnNum] = !btnStatus.value[btnNum];
if (btnNum >= 0 && btnNum < btnStatus.value.length) {
btnStatus.value[btnNum] = !btnStatus.value[btnNum];
emit('switch-toggle', {
index: btnNum,
value: btnStatus.value[btnNum],
states: [...btnStatus.value]
});
}
}
// 一次性设置所有开关状态
function setAllStates(states: boolean[]): void {
const newStates = states.slice(0, props.switchCount);
while (newStates.length < props.switchCount) {
newStates.push(false);
}
btnStatus.value = newStates;
emit('change', { states: [...btnStatus.value] });
}
// 暴露组件方法和状态
defineExpose({
setBtnStatus,
toggleBtnStatus,
setAllStates,
getBtnStatus: () => [...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;
}
.glow:hover {
filter: url(#glow);
.interactive {
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,327 @@
// 组件配置声明
export type PropType = 'string' | 'number' | 'boolean' | 'select';
// 定义选择类型选项
export interface PropOption {
value: string | number | boolean;
label: string;
}
export interface PropConfig {
name: string;
type: string;
label: string;
default: any;
min?: number;
max?: number;
step?: number;
options?: PropOption[];
description?: string;
category?: string; // 用于在UI中分组属性
}
export interface ComponentConfig {
props: PropConfig[];
}
// 存储所有组件的配置
const componentConfigs: Record<string, ComponentConfig> = {
MechanicalButton: {
props: [
{
name: 'bindKey',
type: 'string',
label: '绑定按键',
default: '',
description: '触发按钮按下的键盘按键'
},
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '按钮的相对大小1代表标准大小'
},
{
name: 'buttonText',
type: 'string',
label: '按钮文本',
default: '',
description: '按钮上显示的自定义文本,优先级高于绑定按键'
},
{
name: 'label',
type: 'string',
label: '引脚标签',
default: 'BTN',
description: '引脚的标签文本'
},
{
name: 'constraint',
type: 'string',
label: '引脚约束',
default: '',
description: '相同约束字符串的引脚将被视为有电气连接'
}
]
},
Switch: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '开关的相对大小1代表标准大小'
},
{
name: 'switchCount',
type: 'number',
label: '开关数量',
default: 6,
min: 1,
max: 12,
step: 1,
description: '可翻转开关的数量'
},
{
name: 'showLabels',
type: 'boolean',
label: '显示标签',
default: true,
description: '是否显示开关编号标签'
},
{
name: 'initialValues',
type: 'string',
label: '初始状态',
default: '',
description: '开关的初始状态格式为逗号分隔的0/1如"1,0,1"表示第1、3个开关打开'
}
]
},
Pin: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '引脚的相对大小1代表标准大小'
},
{
name: 'label',
type: 'string',
label: '引脚标签',
default: 'PIN',
description: '用于标识引脚的名称'
},
{
name: 'constraint',
type: 'string',
label: '引脚约束',
default: '',
description: '相同约束字符串的引脚将被视为有电气连接'
},
{
name: 'direction',
type: 'select',
label: '输入/输出特性',
default: 'input',
options: [
{ value: 'input', label: '输入' },
{ value: 'output', label: '输出' },
{ value: 'inout', label: '双向' }
],
description: '引脚的输入/输出特性'
},
{
name: 'type',
type: 'select',
label: '模数特性',
default: 'digital',
options: [
{ value: 'digital', label: 'digital' },
{ value: 'analog', label: 'analog' }
],
description: '引脚的模数特性,数字或模拟'
},
{
name: 'appearance',
type: 'select',
label: '引脚样式',
default: 'Dip',
options: [
{ value: 'None', label: 'None' },
{ value: 'Dip', label: 'Dip' },
{ value: 'SMT', label: 'SMT' }
],
description: '引脚的外观样式,不影响功能'
}
]
},
HDMI: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'HDMI接口的相对大小1代表标准大小'
}
]
},
DDR: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'DDR内存的相对大小1代表标准大小'
}
]
},
ETH: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '以太网接口的相对大小1代表标准大小'
}
]
},
SD: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'SD卡插槽的相对大小1代表标准大小'
}
]
},
SFP: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'SFP光纤模块的相对大小1代表标准大小'
}
]
},
SMA: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'SMA连接器的相对大小1代表标准大小'
}
]
}, MotherBoard: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 2,
step: 0.1,
description: '主板的相对大小1代表标准大小'
}
]
}, SMT_LED: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'LED的相对大小1代表标准大小'
},
{
name: 'color',
type: 'select',
label: '颜色',
default: 'red',
options: [
{ value: 'red', label: '红色' },
{ value: 'green', label: '绿色' },
{ value: 'blue', label: '蓝色' },
{ value: 'yellow', label: '黄色' },
{ value: 'orange', label: '橙色' },
{ value: 'white', label: '白色' },
{ value: 'purple', label: '紫色' }
],
description: 'LED的颜色'
},
{
name: 'initialOn',
type: 'boolean',
label: '初始状态',
default: false,
description: 'LED的初始开关状态'
},
{
name: 'brightness',
type: 'number',
label: '亮度(%)',
default: 80,
min: 0,
max: 100,
step: 5,
description: 'LED的亮度百分比范围0-100'
},
{
name: 'constraint',
type: 'string',
label: '连接约束',
default: '',
description: '相同约束字符串的组件将被视为有电气连接'
}
]
}
};
// 获取组件配置的函数
export function getComponentConfig(type: string): ComponentConfig | null {
return componentConfigs[type] || null;
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="160"
height="160"
viewBox="400 400 800 800"
>
<!-- 按钮底座 -->
<rect width="800" height="800" x="400" y="400" fill="#464646" rx="20" />
<rect width="700" height="700" x="450" y="450" fill="#eaeaea" rx="20" />
<!-- 装饰螺丝 -->
<circle r="20" cx="1075" cy="1075" fill="#171717" />
<circle r="20" cx="1075" cy="525" fill="#171717" />
<circle r="20" cx="525" cy="525" fill="#171717" />
<circle r="20" cx="525" cy="1075" fill="#171717" />
<!-- 按钮主体 -->
<circle r="220" cx="800" cy="800" fill="black" />
<circle
r="200"
cx="800"
cy="800"
fill="#4b4b4b"
fill-opacity="0.9"
/>
</svg>

After

Width:  |  Height:  |  Size: 754 B

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="100"
height="60"
viewBox="0 0 100 60"
>
<!-- LED 基座 -->
<rect width="100" height="60" x="0" y="0" fill="#333" rx="5" ry="5" />
<!-- LED 主体 -->
<rect width="90" height="50" x="5" y="5" fill="#222" rx="3" ry="3" />
<!-- LED 发光部分 -->
<rect
width="70"
height="30"
x="15"
y="15"
fill="#ff3333"
opacity="0.8"
rx="15"
ry="15"
/>
<!-- LED 光晕效果 -->
<rect
width="76"
height="36"
x="12"
y="12"
fill="#ff3333"
opacity="0.2"
rx="18"
ry="18"
filter="blur(3px)"
/>
</svg>

After

Width:  |  Height:  |  Size: 708 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="4.9999995mm"
height="5mm"
viewBox="0 0 4.9999995 5"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-10,-10.000002)">
<rect
style="fill:#000000;fill-opacity:0.772973;stroke-width:0.265;stroke-dasharray:none"
id="rect1"
width="5"
height="5"
x="10"
y="10"
rx="0.5"
onclick="" />
<circle
style="fill:#ececec;fill-opacity:0.772973;stroke-width:0.264999;stroke-dasharray:none"
id="path1"
cx="12.5"
cy="12.5"
r="0.75" />
<text
xml:space="preserve"
style="font-size:1.3717px;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;fill-opacity:0.772973;stroke-width:0.114488;stroke-dasharray:none"
x="11.473036"
y="11.410812"
id="text2"><tspan
id="tspan2"
style="fill:#ffffff;stroke-width:0.114488"
x="11.473036"
y="11.410812">Pn</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="53.600014mm"
height="16.279999mm"
viewBox="0 0 53.600014 16.279999"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-86.17357,-29.131738)"><g
id="g4493"><path
d="m 89.863578,29.151737 v 0.44 h -3.690004 v 2.54 h 0.44 v 3.95 h -0.44 v 2.97 h 0.43 v 3.95 h -0.43 v 2.1 h 3.770004 v 0.31 h 3.95 v -0.38 h 3.95 v 0.38 h 4.220002 v -0.35 h 3.95 v 0.35 h 3.95 v -0.41 h 3.95 v 0.41 h 3.66 v -0.45 h 3.95 v 0.45 h 3.95 v -0.51 h 3.95 v 0.51 h 4.22 v -0.48 h 3.95 v 0.48 h 2.18 v -16.28 h -2.26 v 0.28 h -3.95 v -0.28 h -4.22 v 0.25 h -3.95 v -0.25 h -3.95 v 0.31 h -3.95 v -0.31 h -3.66 v 0.35 h -3.95 v -0.35 h -3.95 v 0.41 h -3.95 v -0.41 h -4.220002 v 0.38 h -3.95 v -0.38 z m 21.090002,2.82 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.82,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m -28.710002,0.03 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.680002,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 29.17,0.21 h 6.25 c 0.22,0 0.39,0.11 0.39,0.25 0,0.14 -0.17,0.25 -0.39,0.25 h -6.25 c -0.22,0 -0.39,-0.11 -0.39,-0.25 0,-0.14 0.17,-0.25 0.39,-0.25 z m -0.07,2 h 6.25 c 0.22,0 0.39,0.11 0.39,0.25 0,0.14 -0.17,0.25 -0.39,0.25 h -6.25 c -0.22,0 -0.39,-0.11 -0.39,-0.25 0,-0.14 0.17,-0.25 0.39,-0.25 z m -15.17,2.3 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.82,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m -28.710002,0.03 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.680002,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 24.42,3.36 h 6.25 c 0.22,0 0.39,0.11 0.39,0.25 0,0.14 -0.17,0.25 -0.39,0.25 h -6.25 c -0.22,0 -0.39,-0.11 -0.39,-0.25 0,-0.14 0.17,-0.25 0.39,-0.25 z m -19.84,1.31 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.82,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m -28.710002,0.03 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.680002,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 28.99,0.63 h 6.25 c 0.22,0 0.39,0.11 0.39,0.25 0,0.14 -0.17,0.25 -0.39,0.25 h -6.25 c -0.22,0 -0.39,-0.11 -0.39,-0.25 0,-0.14 0.17,-0.25 0.39,-0.25 z"
style="font-variation-settings:'wght' 700;fill:#ececec;fill-opacity:1;stroke-width:0.518999;stroke-linecap:round;stroke-linejoin:round;paint-order:fill markers stroke"
id="path4484-9-9" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="200"
height="100"
viewBox="0 0 200 100"
>
<!-- 开关基座 -->
<rect width="200" height="100" x="0" y="0" fill="#444" rx="5" ry="5" />
<!-- 开关面板 -->
<rect width="180" height="80" x="10" y="10" fill="#333" rx="4" ry="4" />
<!-- 开关拨片示例 (6个) -->
<g fill="#ddd">
<rect width="20" height="40" x="25" y="30" rx="2" ry="2" />
<rect width="20" height="40" x="55" y="30" rx="2" ry="2" />
<rect width="20" height="40" x="85" y="30" rx="2" ry="2" />
<rect width="20" height="40" x="115" y="30" rx="2" ry="2" />
<rect width="20" height="40" x="145" y="30" rx="2" ry="2" />
<rect width="20" height="40" x="175" y="30" rx="2" ry="2" />
</g>
<!-- 开关标签 -->
<g fill="#fff" font-size="8" text-anchor="middle">
<text x="25" y="80">1</text>
<text x="55" y="80">2</text>
<text x="85" y="80">3</text>
<text x="115" y="80">4</text>
<text x="145" y="80">5</text>
<text x="175" y="80">6</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB