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>
|