CMVR-IOT-UI/src/views/device/robotManger/TechButtonWithLine.vue

547 lines
18 KiB
Vue
Raw Normal View History

2025-09-12 09:12:58 +08:00
<template>
<div
class="tech-button-with-line"
:style="buttonStyle"
ref="techButtonContainer"
@mouseover="isHovering = true"
@mouseleave="isHovering = false"
>
<!-- 新增一个包裹层,用于应用 clip-path -->
<div class="button-shape-clipper" :style="clipperStyle">
<div class="button-content-wrapper">
<slot name="button-content">
<span class="button-text">{{ buttonName }}</span>
</slot>
</div>
</div>
<!-- SVG 部分保持原有结构,但内部样式和滤镜会改变 -->
<svg
v-if="svgReady"
class="line-svg"
:width="svgWidth"
:height="svgHeight"
:style="{
top: `0px`,
left: `0px`,
}"
>
<defs>
<!-- 霓虹光效滤镜 -->
<!-- stdDeviation 控制模糊程度,值越大越模糊,光效越柔和 -->
<filter id="neon-filter" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" :stdDeviation="props.neonGlowIntensity" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- 扫描线效果渐变 -->
<!-- 动态调整扫描线颜色 -->
<linearGradient id="scan-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgba(255,255,255,0);stop-opacity:1" />
<stop offset="50%" :style="`stop-color:${props.lineHoverColor};stop-opacity:0.8`" />
<stop offset="100%" style="stop-color:rgba(255,255,255,0);stop-opacity:1" />
</linearGradient>
</defs>
<!-- 主线条,应用霓虹光效和扫描线 -->
<polyline
:points="linePoints"
stroke="url(#scan-gradient)"
:stroke-width="props.lineStrokeWidth"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
filter="url(#neon-filter)"
class="main-line"
/>
<!-- 可选:一条细微的基线,用于对比或增加层次 -->
<polyline
:points="baseLinePoints"
:stroke="props.lineColor"
stroke-width="1"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
class="base-line"
/>
</svg>
</div>
</template>
<script setup>
import { ref, computed, onMounted, nextTick, watch, defineProps, onUnmounted } from 'vue';
// --- Props ---
// 保持原有 props,并增加一些用于样式控制的 props
const props = defineProps({
totalButtons: { type: Number, required: true, default: 1 },
buttonIndex: { type: Number, required: true },
buttonPosition: { type: String, required: true, validator: (value) => ['left', 'right'].includes(value) },
buttonOffsetX: { type: Number, default: 350 },
endX: { type: Number, required: true },
endY: { type: Number, required: true },
buttonName: { type: String, default: '科技按钮' },
buttonWidth: { type: [Number, String], default: 150 },
buttonHeight: { type: [Number, String], default: 40 },
linePadding: { type: Number, default: 20 },
// --- 仅用于样式的 Props ---
buttonGradient: { type: Array, default: () => ['#00f0ff', '#00d0ff'] }, // 按钮背景渐变色
buttonHoverGradient: { type: Array, default: () => ['#00d0ff', '#00f0ff'] }, // 悬停时的渐变色
buttonBorderColor: { type: String, default: 'rgba(0, 255, 255, 0.6)' },
lineColor: { type: String, default: 'rgba(0, 150, 200, 0.4)' }, // 细线基础颜色
lineHoverColor: { type: String, default: 'rgba(0, 255, 255, 1)' }, // 扫描线高亮颜色
lineStrokeWidth: { type: Number, default: 3 },
neonGlowIntensity: { type: Number, default: 3 }, // 霓虹光效强度
scanLineSpeed: { type: Number, default: 8 }, // 扫描线动画速度
animationDuration: { type: Number, default: 0.4 }, // 基础动画时长
hoverScale: { type: Number, default: 1.03 }, // 悬停时的放大比例
// --- New Props for Flowing Light Effect ---
flowLineColor: { type: String, default: 'rgba(0, 255, 255, 0.8)' }, // 流光颜色
flowLineAnimationSpeed: { type: Number, default: 7 }, // 流光动画速度
});
// --- Refs ---
const techButtonContainer = ref(null);
const svgReady = ref(false);
const parentContainerRef = ref(null);
const parentHeight = ref(0);
const parentWidth = ref(0);
const svgWidth = ref(0);
const svgHeight = ref(0);
const svgLeftOffset = ref(0);
const svgTopOffset = ref(0);
const isHovering = ref(false); // 追踪鼠标是否悬停
// --- Computed Properties ---
// 计算父容器的水平中心线 (保持不变)
const autoCenterX = computed(() => parentWidth.value / 2);
// 计算按钮在父容器中的 X 坐标(全局坐标)(保持不变)
const calculatedButtonX = computed(() => {
if (!parentWidth.value) return 0;
if (props.buttonPosition === 'left') {
return autoCenterX.value - props.buttonOffsetX;
} else if (props.buttonPosition === 'right') {
return autoCenterX.value + props.buttonOffsetX;
}
return 0;
});
// 计算按钮在父容器中的 Y 坐标(全局坐标)(保持不变)
const calculatedButtonY = computed(() => {
if (!parentHeight.value || props.totalButtons <= 0) return 0;
const buttonSlotHeight = parentHeight.value / props.totalButtons;
const centerY = (props.buttonIndex - 1) * buttonSlotHeight + buttonSlotHeight / 2;
return centerY;
});
// 获取按钮的实际宽度和高度 (保持不变)
const actualButtonWidth = computed(() => {
if (techButtonContainer.value) {
return techButtonContainer.value.offsetWidth;
}
return typeof props.buttonWidth === 'number' ? props.buttonWidth : 150;
});
const actualButtonHeight = computed(() => {
if (techButtonContainer.value) {
return techButtonContainer.value.offsetHeight;
}
return typeof props.buttonHeight === 'number' ? props.buttonHeight : 40;
});
// 线的终点相对于按钮中心点的偏移 (SVG 内部坐标系) (保持不变)
const relativeEndX = computed(() => props.endX - calculatedButtonX.value + actualButtonWidth.value / 2);
const relativeEndY = computed(() => props.endY - calculatedButtonY.value + actualButtonHeight.value / 2);
// 按钮的样式
const buttonStyle = computed(() => {
const buttonWidthPx = typeof props.buttonWidth === 'number' ? `${props.buttonWidth}px` : props.buttonWidth;
const buttonHeightPx = typeof props.buttonHeight === 'number' ? `${props.buttonHeight}px` : props.buttonHeight;
const currentGradient = isHovering.value ? props.buttonHoverGradient : props.buttonGradient;
const style = {
position: 'absolute',
left: `${calculatedButtonX.value}px`,
top: `${calculatedButtonY.value}px`,
transform: 'translate(-50%, -50%)', // 居中对齐
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxSizing: 'border-box',
width: buttonWidthPx,
height: buttonHeightPx,
borderRadius: '15px', // 强制平直四角
// 更硬朗的背景,减少模糊,增加纹理感
background: `linear-gradient(135deg, ${currentGradient[0]}, ${currentGradient[1]})`,
// 模拟金属/磨砂质感,但更锐利
boxShadow: `
inset 0 0 0 2px ${props.buttonBorderColor}, /* 内部边框 */
0 0 5px rgba(0, 150, 200, 0.5), /* 较弱的主光晕 */
0 0 12px rgba(0, 200, 255, 0.6), /* 第二层光晕 */
0 3px 10px rgba(0, 0, 0, 0.4) /* 底部阴影 */
`,
color: '#ffffff',
fontSize: '14px',
fontWeight: 'bold', // 更粗的字体
letterSpacing: '0.7px',
textShadow: '0 0 5px rgba(0, 150, 200, 0.9)', // 文本的锐利光效
transition: `all ${props.animationDuration}s ease-in-out`,
overflow: 'visible', // 允许流光伪元素溢出
zIndex: 1,
};
if (isHovering.value) {
style.transform = `translate(-50%, -50%) scale(${props.hoverScale})`;
style.boxShadow = `
inset 0 0 0 3px ${props.buttonBorderColor}, /* 悬停时内部边框变宽 */
0 0 8px rgba(0, 200, 255, 0.7),
0 0 18px rgba(0, 230, 255, 0.8),
0 5px 15px rgba(0, 0, 0, 0.5) /* 底部阴影增强 */
`;
}
return style;
});
// 按钮形状裁剪器的样式
const clipperStyle = computed(() => {
const currentGradient = isHovering.value ? props.buttonHoverGradient : props.buttonGradient;
const style = {
position: 'relative', // 相对于 techButtonContainer 定位
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxSizing: 'border-box',
borderRadius: '0px', // **clipper 的形状由 clip-path 定义**
background: `linear-gradient(135deg, ${currentGradient[0]}, ${currentGradient[1]})`,
boxShadow: `
inset 0 0 0 2px ${props.buttonBorderColor}, /* 内部边框 */
0 0 5px rgba(0, 150, 200, 0.5), /* 较弱的主光晕 */
0 0 12px rgba(0, 200, 255, 0.6), /* 第二层光晕 */
0 3px 10px rgba(0, 0, 0, 0.4) /* 底部阴影 */
`,
color: '#ffffff',
fontSize: '14px',
fontWeight: 'bold',
letterSpacing: '0.7px',
textShadow: '0 0 5px rgba(0, 150, 200, 0.9)', // 文本的锐利光效
transition: `all ${props.animationDuration}s ease-in-out`,
overflow: 'hidden', // **clipper 内部的内容会被 clip-path 裁剪**
clipPath: `polygon(
6px 0, /* 左上角内缩进5px */
calc(100% - 6px) 0, /* 右上角内缩进5px */
100% 6px, /* 右上角外延5px */
100% calc(100% - 6px), /* 右下角内缩进5px */
calc(100% - 6px) 100%,
6px 100%, /* 左下角内缩进5px */
0 calc(100% - 6px),
0 6px, /* 左下角外延5px */
6px 0px /* 左上角内缩进5px(重复起点,闭合多边形) */
)`,
zIndex: 1, // 确保clipper在流光之上
};
if (isHovering.value) {
style.boxShadow = `
inset 0 0 0 3px ${props.buttonBorderColor}, /* 悬停时内部边框变宽 */
0 0 8px rgba(0, 200, 255, 0.7),
0 0 18px rgba(0, 230, 255, 0.8),
0 5px 15px rgba(0, 0, 0, 0.5) /* 底部阴影增强 */
`;
}
return style;
});
// 线的三个关键点 (相对于 SVG 内部坐标系 (0,0) 是按钮中心)
// --- 保持原有逻辑 ---
const linePoints = computed(() => {
if (!svgReady.value) return '';
const startPointX = (props.buttonPosition === 'left' ? props.buttonWidth : 0);
const startPointY = props.buttonHeight/2;
const endPointX = relativeEndX.value;
const endPointY = relativeEndY.value;
// L 形连接点:先垂直,再水平。
const middlePointY = endPointY;
// 调整连接点的 X 坐标,使其更具科技感,可以稍微偏离 Y 轴
// 这里的 +10 和 -10 是示例,可以根据实际效果调整
const adjustedMiddlePointX = startPointX + (props.buttonPosition === 'left' ? 100 : -100);
return `${startPointX},${startPointY} ${adjustedMiddlePointX},${middlePointY} ${endPointX},${endPointY}`;
});
// 细微基线的点 (保持原有逻辑)
const baseLinePoints = computed(() => {
if (!svgReady.value) return '';
const startPointX = (props.buttonPosition === 'left' ? props.buttonWidth : 0);
const startPointY = props.buttonHeight/2;
const endPointX = relativeEndX.value;
const endPointY = relativeEndY.value;
const adjustedMiddlePointX = startPointX + (props.buttonPosition === 'left' ? 100 : -100);
return `${startPointX},${startPointY} ${adjustedMiddlePointX},${endPointY} ${endPointX},${endPointY}`;
});
// --- Methods --- (保持不变)
const updateSvgDimensions = async () => {
if (!techButtonContainer.value) {
svgReady.value = false;
return;
}
await nextTick();
if (!parentContainerRef.value || parentContainerRef.value.offsetHeight === 0 || parentContainerRef.value.offsetWidth === 0) {
svgReady.value = false;
return;
}
parentHeight.value = parentContainerRef.value.offsetHeight;
parentWidth.value = parentContainerRef.value.offsetWidth;
if (parentHeight.value <= 0 || parentWidth.value <= 0 || props.totalButtons <= 0) {
svgReady.value = false;
return;
}
let minX = Math.min(0, 0, relativeEndX.value);
let maxX = Math.max(0, 0, relativeEndX.value);
let minY = Math.min(0, relativeEndY.value, relativeEndY.value);
let maxY = Math.max(0, relativeEndY.value, relativeEndY.value);
minX -= props.linePadding;
maxX += props.linePadding;
minY -= props.linePadding;
maxY += props.linePadding;
svgWidth.value = maxX - minX;
svgHeight.value = maxY - minY;
svgLeftOffset.value = minX;
svgTopOffset.value = minY;
svgReady.value = true;
};
// --- Resize Observer --- (保持不变)
let resizeObserver = null;
const setupResizeObserver = () => {
if (resizeObserver) {
resizeObserver.disconnect();
}
if (parentContainerRef.value) {
resizeObserver = new ResizeObserver(() => {
const newHeight = parentContainerRef.value.offsetHeight;
const newWidth = parentContainerRef.value.offsetWidth;
if (newHeight !== parentHeight.value || newWidth !== parentWidth.value) {
parentHeight.value = newHeight;
parentWidth.value = newWidth;
debouncedUpdate(updateSvgDimensions)();
}
});
resizeObserver.observe(parentContainerRef.value);
} else {
console.error("TechButtonWithLine: Could not find parent element to observe.");
}
};
// --- Lifecycle Hooks --- (保持不变)
onMounted(() => {
if (techButtonContainer.value && techButtonContainer.value.parentElement) {
parentContainerRef.value = techButtonContainer.value.parentElement.parentElement;
setupResizeObserver();
} else {
console.error("TechButtonWithLine: Could not find techButtonContainer or its parent.");
}
updateSvgDimensions();
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect();
}
});
// --- Watchers --- (保持不变)
const debouncedUpdate = (fn, delay = 10) => {
let timer = null;
return () => {
clearTimeout(timer);
timer = setTimeout(fn, delay);
};
};
watch(
[
() => parentHeight.value,
() => parentWidth.value,
() => props.totalButtons,
() => props.buttonIndex,
() => props.buttonPosition,
() => props.endX,
() => props.endY,
() => props.buttonOffsetX,
() => props.linePadding,
() => props.buttonWidth,
() => props.buttonHeight,
// 监听用于样式变化的 props
() => props.buttonGradient,
() => props.buttonHoverGradient,
() => props.lineColor,
() => props.lineHoverColor,
() => props.lineStrokeWidth,
() => props.neonGlowIntensity,
() => props.scanLineSpeed,
() => props.animationDuration,
() => props.hoverScale,
() => props.flowLineColor,
() => props.flowLineAnimationSpeed,
],
debouncedUpdate(updateSvgDimensions),
{ immediate: true, deep: true }
);
</script>
<style scoped>
/* --- Base Styles for the Button Container --- */
.tech-button-with-line {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
overflow: visible; /* 允许 SVG 滤镜和动画效果溢出 */
z-index: 1;
border-radius: 8px; /* 科技感圆角 */
/* 基础过渡效果,用于缩放和阴影 */
/* transition: transform v-bind('`${props.animationDuration}s`') ease-in-out, box-shadow v-bind('`${props.animationDuration}s`') ease-in-out, background v-bind('`${props.animationDuration}s`') ease-in-out; */
}
.button-shape-clipper {
position: relative; /* 相对于 techButtonContainer,以填充其空间 */
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-radius: 0px; /* **clipper 的形状由 clip-path 定义** */
overflow: hidden; /* **clipper 内部的内容会被 clip-path 裁剪** */
transition: all v-bind('`${props.animationDuration}s`') ease-in-out; /* 过渡背景和阴影 */
z-index: 1; /* 确保 clipper 在流光之上 */
}
.button-content-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none; /* 让鼠标事件穿透到按钮本身 */
}
.button-text {
font-family: 'Orbitron', sans-serif; /* 科技感字体 */
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
/* color: v-bind('props.buttonBorderColor'); 文本颜色与边框颜色呼应 */
color: #fff;
}
/* --- SVG Line Styles --- */
.line-svg {
position: absolute;
pointer-events: none; /* SVG 不捕获鼠标事件 */
overflow: visible; /* 允许滤镜和渐变效果溢出 SVG bounds */
transition: all v-bind('`${props.animationDuration}s`') ease-in-out;
z-index: 0; /* 放在按钮后面 */
}
/* 主线条的动画 */
.main-line {
animation: scanline v-bind('`${props.scanLineSpeed}s`') linear infinite;
/* 调整 stroke-dasharray 来控制扫描线的长度和间隔 */
stroke-dasharray: 15px, 80px; /* 15px 显示, 100px 隐藏 */
stroke-dashoffset: 0; /* 初始位置 */
}
/* 鼠标悬停时,主线条颜色变亮,动画速度可能加快 */
.tech-button-with-line:hover .main-line {
stroke: v-bind('props.lineHoverColor'); /* 悬停时线条颜色 */
animation-duration: v-bind('`${props.scanLineSpeed / 1.5}s`'); /* 悬停时速度加快 */
stroke-dasharray: 20px, 120px; /* 悬停时线条显示部分变长 */
}
/* 细微基线样式 */
.base-line {
opacity: 0.6;
transition: all v-bind('`${props.animationDuration}s`') ease-in-out;
}
.tech-button-with-line:hover .base-line {
opacity: 0.9; /* 悬停时基线也变亮 */
}
/* --- Flowing Light Border Effect --- */
/* 使用 ::before 伪元素实现四周的流光 */
.tech-button-with-line::before {
content: '';
position: absolute;
top: -4px; /* 稍向外延伸 */
left: -4px;
right: -4px;
bottom: -4px;
/* 关键:创建动态的流光渐变 */
/* 渐变方向是从左到右,颜色从透明到高亮再到透明 */
background: linear-gradient(
90deg,
transparent,
v-bind('props.flowLineColor'),
transparent
);
border-radius: 0px; /* **与按钮一致的平直四角** */
opacity: 0.8; /* 初始可见度 */
filter: blur(5px); /* 轻微模糊,模拟光晕 */
z-index: -1; /* 放在按钮内容后面 */
animation: flowLight v-bind('`${props.flowLineAnimationSpeed}s`') linear infinite;
}
/* 悬停时,流光效果可以增强 */
.tech-button-with-line:hover::before {
opacity: 1;
filter: blur(8px); /* 悬停时模糊度增加 */
}
/* --- Keyframes for Animations --- */
@keyframes scanline {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -1000; /* 确保足够长以扫过整个线段 */
}
}
/* 流光动画 */
@keyframes flowLight {
0% { background-position: -100% 0; } /* 从左侧完全移出 */
50% { background-position: 100% 0; } /* 移动到右侧完全移出 */
100% { background-position: -100% 0; } /* 回到初始状态,循环 */
}
</style>