547 lines
18 KiB
Vue
547 lines
18 KiB
Vue
|
|
<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>
|