265 lines
8.7 KiB
Plaintext
265 lines
8.7 KiB
Plaintext
---
|
||
interface Props {
|
||
title: string;
|
||
description?: string;
|
||
image?: string;
|
||
link?: string;
|
||
className?: string;
|
||
color?: string;
|
||
spotlightSize?: 'small' | 'medium' | 'large';
|
||
spotlightIntensity?: 'subtle' | 'medium' | 'strong';
|
||
}
|
||
|
||
const {
|
||
title,
|
||
description,
|
||
image,
|
||
link,
|
||
className = "",
|
||
color = "rgba(75, 107, 255, 0.3)", // 默认是蓝色光晕
|
||
spotlightSize = 'medium',
|
||
spotlightIntensity = 'medium'
|
||
} = Astro.props;
|
||
|
||
const id = `spotlight-card-${Math.random().toString(36).substring(2, 11)}`;
|
||
|
||
// 计算光晕大小
|
||
const sizeMap = {
|
||
small: '70%',
|
||
medium: '100%',
|
||
large: '130%'
|
||
};
|
||
|
||
// 计算光晕强度
|
||
const intensityMap = {
|
||
subtle: '0.15',
|
||
medium: '0.25',
|
||
strong: '0.4'
|
||
};
|
||
|
||
const spotlightSizeValue = sizeMap[spotlightSize];
|
||
const spotlightIntensityValue = intensityMap[spotlightIntensity];
|
||
---
|
||
|
||
<div
|
||
id={id}
|
||
class={`spotlight-card group relative overflow-hidden rounded-xl border border-white/10 dark:bg-color-dark-card bg-white p-6 shadow-lg transition-all hover:shadow-xl ${className}`}
|
||
style="transform-style: preserve-3d; transform: perspective(1000px);"
|
||
data-spotlight-size={spotlightSizeValue}
|
||
data-spotlight-intensity={spotlightIntensityValue}
|
||
>
|
||
<div class="spotlight-primary absolute pointer-events-none inset-0 z-0 transition duration-300 opacity-0"></div>
|
||
<div class="spotlight-secondary absolute pointer-events-none inset-0 z-0 transition duration-300 opacity-0"></div>
|
||
<div class="spotlight-border absolute pointer-events-none inset-0 z-0 transition duration-300 opacity-0"></div>
|
||
|
||
{image && (
|
||
<div class="mb-4 overflow-hidden rounded-lg">
|
||
<div class="h-40 bg-gray-300 dark:bg-gray-700 flex items-center justify-center">
|
||
<span class="text-gray-500 dark:text-gray-400">{title} 图片</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<h3 class="group-hover:text-color-primary-600 dark:group-hover:text-color-primary-400 text-lg font-semibold text-gray-900 dark:text-white mb-2 transition-colors">
|
||
{title}
|
||
</h3>
|
||
|
||
{description && (
|
||
<p class="text-gray-600 dark:text-gray-400 mb-4 text-sm">
|
||
{description}
|
||
</p>
|
||
)}
|
||
|
||
{link && (
|
||
<a
|
||
href={link}
|
||
class="text-color-primary-600 hover:text-color-primary-700 dark:text-color-primary-400 dark:hover:text-color-primary-300 font-medium text-sm"
|
||
>
|
||
查看详情 →
|
||
</a>
|
||
)}
|
||
|
||
<slot />
|
||
</div>
|
||
|
||
<style define:vars={{
|
||
spotlightColor: color,
|
||
spotlightSize: spotlightSizeValue,
|
||
spotlightIntensity: spotlightIntensityValue
|
||
}}>
|
||
.spotlight-primary {
|
||
background: radial-gradient(
|
||
circle at center,
|
||
var(--spotlightColor) 0%,
|
||
transparent 70%
|
||
);
|
||
filter: blur(5px);
|
||
opacity: 0;
|
||
/* 确保径向渐变是一个完美的圆 */
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.spotlight-secondary {
|
||
background: radial-gradient(
|
||
circle at center,
|
||
rgba(255, 255, 255, 0.1) 0%,
|
||
transparent 60%
|
||
);
|
||
opacity: 0;
|
||
/* 确保径向渐变是一个完美的圆 */
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.spotlight-border {
|
||
background: none;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: inherit;
|
||
opacity: 0;
|
||
}
|
||
|
||
[data-theme='dark'] .spotlight-primary {
|
||
background: radial-gradient(
|
||
circle at center,
|
||
var(--spotlightColor) 0%,
|
||
transparent 70%
|
||
);
|
||
}
|
||
|
||
[data-theme='dark'] .spotlight-secondary {
|
||
background: radial-gradient(
|
||
circle at center,
|
||
rgba(255, 255, 255, 0.07) 0%,
|
||
transparent 60%
|
||
);
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
// 等待 DOM 加载完成
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// 获取所有光标效果卡片
|
||
const cards = document.querySelectorAll('.spotlight-card');
|
||
|
||
cards.forEach(card => {
|
||
const primaryEffect = card.querySelector('.spotlight-primary');
|
||
const secondaryEffect = card.querySelector('.spotlight-secondary');
|
||
const borderEffect = card.querySelector('.spotlight-border');
|
||
|
||
if (!primaryEffect || !secondaryEffect || !borderEffect) return;
|
||
|
||
// 获取配置
|
||
const size = card.getAttribute('data-spotlight-size') || '100%';
|
||
const intensity = parseFloat(card.getAttribute('data-spotlight-intensity') || '0.25');
|
||
|
||
// 上一次鼠标位置
|
||
let lastX = 0;
|
||
let lastY = 0;
|
||
|
||
// 平滑因子 (值越小越平滑)
|
||
const smoothFactor = 0.15;
|
||
|
||
// 用于动画的requestAnimationFrame ID
|
||
let animationFrameId: number | null = null;
|
||
|
||
// 鼠标移入时添加效果
|
||
card.addEventListener('mouseenter', () => {
|
||
// 计算光效尺寸 - 使用卡片对角线长度确保覆盖整个卡片区域
|
||
const rect = card.getBoundingClientRect();
|
||
const diagonalLength = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
|
||
const effectSize = diagonalLength * 1.5 + 'px'; // 增加50%确保覆盖
|
||
|
||
// 设置光效元素尺寸
|
||
(primaryEffect as HTMLElement).style.width = effectSize;
|
||
(primaryEffect as HTMLElement).style.height = effectSize;
|
||
(secondaryEffect as HTMLElement).style.width = effectSize;
|
||
(secondaryEffect as HTMLElement).style.height = effectSize;
|
||
|
||
// 设置可见度
|
||
(primaryEffect as HTMLElement).style.opacity = intensity.toString();
|
||
(secondaryEffect as HTMLElement).style.opacity = (intensity * 0.7).toString();
|
||
(borderEffect as HTMLElement).style.opacity = (intensity * 0.5).toString();
|
||
|
||
// 启动平滑动画
|
||
if (animationFrameId === null) {
|
||
animateSpotlight();
|
||
}
|
||
});
|
||
|
||
// 鼠标移出时移除效果
|
||
card.addEventListener('mouseleave', () => {
|
||
(primaryEffect as HTMLElement).style.opacity = '0';
|
||
(secondaryEffect as HTMLElement).style.opacity = '0';
|
||
(borderEffect as HTMLElement).style.opacity = '0';
|
||
|
||
// 重置卡片的 3D 效果
|
||
(card as HTMLElement).style.transform = 'perspective(1000px)';
|
||
|
||
// 取消动画
|
||
if (animationFrameId !== null) {
|
||
cancelAnimationFrame(animationFrameId);
|
||
animationFrameId = null;
|
||
}
|
||
});
|
||
|
||
// 鼠标移动时更新光标位置和 3D 效果
|
||
card.addEventListener('mousemove', (e) => {
|
||
const mouseEvent = e as MouseEvent;
|
||
const rect = card.getBoundingClientRect();
|
||
lastX = mouseEvent.clientX - rect.left;
|
||
lastY = mouseEvent.clientY - rect.top;
|
||
|
||
// 计算 3D 旋转效果
|
||
const centerX = rect.width / 2;
|
||
const centerY = rect.height / 2;
|
||
const rotateY = (lastX - centerX) / 20;
|
||
const rotateX = (centerY - lastY) / 20;
|
||
|
||
// 应用 3D 效果
|
||
(card as HTMLElement).style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
|
||
});
|
||
|
||
// 平滑动画函数
|
||
function animateSpotlight() {
|
||
const rect = card.getBoundingClientRect();
|
||
|
||
// 获取当前位置
|
||
const currentPrimaryStyle = window.getComputedStyle(primaryEffect as Element);
|
||
const currentSecondaryStyle = window.getComputedStyle(secondaryEffect as Element);
|
||
|
||
// 解析当前位置
|
||
let currentX = parseInt(currentPrimaryStyle.left || '0');
|
||
let currentY = parseInt(currentPrimaryStyle.top || '0');
|
||
|
||
if (isNaN(currentX)) currentX = rect.width / 2;
|
||
if (isNaN(currentY)) currentY = rect.height / 2;
|
||
|
||
// 计算新位置 (平滑过渡)
|
||
const newX = currentX + (lastX - currentX) * smoothFactor;
|
||
const newY = currentY + (lastY - currentY) * smoothFactor;
|
||
|
||
// 应用位置
|
||
// 不再在这里设置宽高,而是在mouseenter时设置一次
|
||
(primaryEffect as HTMLElement).style.left = `${newX}px`;
|
||
(primaryEffect as HTMLElement).style.top = `${newY}px`;
|
||
(primaryEffect as HTMLElement).style.transform = `translate(-50%, -50%)`;
|
||
|
||
(secondaryEffect as HTMLElement).style.left = `${newX}px`;
|
||
(secondaryEffect as HTMLElement).style.top = `${newY}px`;
|
||
(secondaryEffect as HTMLElement).style.transform = `translate(-50%, -50%) scale(1.2)`;
|
||
|
||
// 边框位置不需要移动,但可以随鼠标位置变化透明度
|
||
const distanceFromCenter = Math.sqrt(
|
||
Math.pow((newX / rect.width - 0.5) * 2, 2) +
|
||
Math.pow((newY / rect.height - 0.5) * 2, 2)
|
||
);
|
||
|
||
// 越靠近边缘,边框越明显
|
||
const borderOpacity = Math.min(distanceFromCenter * intensity, intensity * 0.5);
|
||
(borderEffect as HTMLElement).style.opacity = borderOpacity.toString();
|
||
|
||
// 继续动画循环
|
||
animationFrameId = requestAnimationFrame(animateSpotlight);
|
||
}
|
||
});
|
||
});
|
||
</script> |