first commit
This commit is contained in:
895
src/App.vue
Normal file
895
src/App.vue
Normal file
@@ -0,0 +1,895 @@
|
||||
<template>
|
||||
<Analytics />
|
||||
<SpeedInsights />
|
||||
<div class="app-container">
|
||||
<!-- 玻璃拟态背景层 -->
|
||||
<div class="glass-bg"></div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="main-content">
|
||||
<!-- 头部区域 -->
|
||||
<header class="hero-header">
|
||||
<div class="logo-area">
|
||||
<div class="logo-icon">🧭</div>
|
||||
<h1 class="hero-title">智能导航中心</h1>
|
||||
</div>
|
||||
<p class="hero-subtitle">探索、发现、连接 - 您的个性化导航助手</p>
|
||||
</header>
|
||||
|
||||
<!-- 搜索和过滤区域 -->
|
||||
<div class="controls-section">
|
||||
<!-- 搜索框 -->
|
||||
<SearchBox v-if="!loading" @search="handleSearch" @clear="handleClearSearch"
|
||||
:filteredCount="filteredCards.length" />
|
||||
|
||||
<!-- 智能分类标签云 -->
|
||||
<div class="tags-cloud" v-if="!loading && !searchQuery">
|
||||
<div class="tags-wrapper">
|
||||
<button v-for="category in displayCategories" :key="category" class="tag-item" :class="{
|
||||
active: selectedCategory === category,
|
||||
popular: getCategoryCount(category) > 5,
|
||||
trending: Math.random() > 0.7
|
||||
}" @click="selectCategory(category)">
|
||||
<span class="tag-icon">{{ getCategoryIcon(category) }}</span>
|
||||
{{ category }}
|
||||
<span class="tag-count">{{ getCategoryCount(category) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果统计 -->
|
||||
<div class="result-stats" v-if="searchQuery && filteredCards.length > 0">
|
||||
<div class="stats-badge">
|
||||
<span class="stats-icon">🎯</span>
|
||||
找到 <strong>{{ filteredCards.length }}</strong> 个精准结果
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 智能内容展示区 -->
|
||||
<div class="content-section">
|
||||
<!-- 加载状态 -->
|
||||
<div class="loading-state" v-if="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在加载导航数据...</p>
|
||||
</div>
|
||||
|
||||
<!-- 卡片网格 -->
|
||||
<div class="cards-grid" v-else>
|
||||
<div v-for="(card, index) in displayCards" :key="card.url + card.index" class="card-wrapper"
|
||||
:style="{ animationDelay: `${index * 0.05}s` }">
|
||||
<SmartCard :card="card" :index="index" :category-color="getCategoryColor(card.catelog)"
|
||||
@click="openLink(card.url)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div class="empty-state" v-if="!loading && displayCards.length === 0">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<h3>未找到匹配的内容</h3>
|
||||
<p>尝试其他关键词或浏览全部分类</p>
|
||||
<button class="reset-btn" @click="resetFilters">重置筛选</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部统计和快捷操作 -->
|
||||
<footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
<div class="stats-overview">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总链接</span>
|
||||
<span class="stat-value">{{ allLinks.length }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">当前显示</span>
|
||||
<span class="stat-value">{{ displayCards.length }}</span>
|
||||
</div>
|
||||
<div class="stat-item" v-if="searchQuery">
|
||||
<span class="stat-label">搜索结果</span>
|
||||
<span class="stat-value">{{ filteredCards.length }}</span>
|
||||
</div>
|
||||
<div class="stat-item" v-else-if="selectedCategory">
|
||||
<span class="stat-label">分类</span>
|
||||
<span class="stat-value">{{ categoryCards.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<button class="action-btn secondary" @click="resetFilters" v-if="searchQuery || selectedCategory">
|
||||
<span>✨</span> 重置
|
||||
</button>
|
||||
<button class="action-btn primary" @click="shuffleDisplay">
|
||||
<span>🎲</span> 随机
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import Analytics from '@vercel/analytics/vue';
|
||||
import SpeedInsights from '@vercel/speed-insights/vue';
|
||||
import SmartCard from './components/SmartCard.vue'
|
||||
import SearchBox from './components/SearchBox.vue'
|
||||
|
||||
const allLinks = ref([])
|
||||
const loading = ref(true)
|
||||
const searchQuery = ref('')
|
||||
const selectedCategory = ref('')
|
||||
|
||||
// 搜索功能
|
||||
const filteredCards = computed(() => {
|
||||
if (!searchQuery.value.trim()) return []
|
||||
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return allLinks.value.filter(item => {
|
||||
return item.name.toLowerCase().includes(query) ||
|
||||
item.desc.toLowerCase().includes(query) ||
|
||||
item.catelog.toLowerCase().includes(query) ||
|
||||
item.url.toLowerCase().includes(query)
|
||||
})
|
||||
})
|
||||
|
||||
// 分类卡片
|
||||
const categoryCards = computed(() => {
|
||||
if (!selectedCategory.value) return []
|
||||
return allLinks.value.filter(item => item.catelog === selectedCategory.value)
|
||||
})
|
||||
|
||||
// 分类网格
|
||||
const displayCategories = computed(() => {
|
||||
if (allLinks.value.length === 0) return []
|
||||
const categories = [...new Set(allLinks.value.map(item => item.catelog))]
|
||||
return categories.sort((a, b) => getCategoryCount(b) - getCategoryCount(a))
|
||||
})
|
||||
|
||||
// 显示的卡片内容
|
||||
const displayCards = computed(() => {
|
||||
if (allLinks.value.length === 0) return []
|
||||
|
||||
let source = []
|
||||
|
||||
if (searchQuery.value.trim() && filteredCards.value.length > 0) {
|
||||
source = filteredCards.value
|
||||
} else if (selectedCategory.value && categoryCards.value.length > 0) {
|
||||
source = categoryCards.value
|
||||
} else {
|
||||
source = [...allLinks.value].sort(() => Math.random() - 0.5).slice(0, 12)
|
||||
}
|
||||
|
||||
return source.map((item, index) => ({
|
||||
...item,
|
||||
index
|
||||
}))
|
||||
})
|
||||
|
||||
// 分类统计
|
||||
const getCategoryCount = (category) => {
|
||||
return allLinks.value.filter(item => item.catelog === category).length
|
||||
}
|
||||
|
||||
// 分类图标
|
||||
const getCategoryIcon = (category) => {
|
||||
const icons = {
|
||||
'影视娱乐': '🎬',
|
||||
'个人工具站': '🛠️',
|
||||
'编程学习': '📚',
|
||||
'前端开发': '💻',
|
||||
'开发平台': '🌐',
|
||||
'开发工具': '🔧',
|
||||
'实用工具': '⚙️',
|
||||
'设计资源': '🎨'
|
||||
}
|
||||
return icons[category] || '📦'
|
||||
}
|
||||
|
||||
// 分类颜色
|
||||
const getCategoryColor = (category) => {
|
||||
const colors = {
|
||||
'影视娱乐': '#FF6B6B',
|
||||
'个人工具站': '#4ECDC4',
|
||||
'编程学习': '#45B7D1',
|
||||
'前端开发': '#96CEB4',
|
||||
'开发平台': '#FFEAA7',
|
||||
'开发工具': '#DDA0DD',
|
||||
'实用工具': '#98D8C8',
|
||||
'设计资源': '#F7DC6F'
|
||||
}
|
||||
return colors[category] || '#95A5A6'
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (query) => {
|
||||
searchQuery.value = query
|
||||
selectedCategory.value = ''
|
||||
}
|
||||
|
||||
const handleClearSearch = () => {
|
||||
searchQuery.value = ''
|
||||
}
|
||||
|
||||
// 选择分类
|
||||
const selectCategory = (category) => {
|
||||
selectedCategory.value = selectedCategory.value === category ? '' : category
|
||||
searchQuery.value = ''
|
||||
}
|
||||
|
||||
// 重置筛选
|
||||
const resetFilters = () => {
|
||||
searchQuery.value = ''
|
||||
selectedCategory.value = ''
|
||||
}
|
||||
|
||||
// 随机打乱
|
||||
const shuffleDisplay = () => {
|
||||
if (allLinks.value.length > 0) {
|
||||
allLinks.value = [...allLinks.value].sort(() => Math.random() - 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开链接
|
||||
const openLink = (url) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadLinks = async () => {
|
||||
try {
|
||||
const response = await fetch('/link.json')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
allLinks.value = data
|
||||
} else {
|
||||
loadFallbackData()
|
||||
}
|
||||
} catch (error) {
|
||||
loadFallbackData()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 内置数据
|
||||
const loadFallbackData = () => {
|
||||
allLinks.value = [
|
||||
{
|
||||
"name": "爱壹帆影视网",
|
||||
"url": "https://www.iyf.lv",
|
||||
"catelog": "影视娱乐",
|
||||
"desc": "提供影视资源在线观看与下载的影视平台"
|
||||
},
|
||||
{
|
||||
"name": "Kimivod影视资源站",
|
||||
"url": "https://kimivod.com/",
|
||||
"catelog": "影视娱乐",
|
||||
"desc": "聚合各类影视内容的在线播放网站"
|
||||
},
|
||||
{
|
||||
"name": "Umami个人数据统计平台",
|
||||
"url": "https://umami.lideshan.top/",
|
||||
"catelog": "个人工具站",
|
||||
"desc": "基于Umami搭建的轻量级网站访问数据统计分析工具"
|
||||
},
|
||||
{
|
||||
"name": "Docker自建加速站",
|
||||
"url": "https://docker.020417.xyz",
|
||||
"catelog": "个人工具站",
|
||||
"desc": "个人搭建的Docker镜像加速服务平台"
|
||||
},
|
||||
{
|
||||
"name": "在线图片格式转换工具",
|
||||
"url": "https://pic.020417.xyz/",
|
||||
"catelog": "个人工具站",
|
||||
"desc": "支持多种图片格式在线转换的便捷工具"
|
||||
},
|
||||
{
|
||||
"name": "开发者备忘录工具",
|
||||
"url": "https://dev.020417.xyz/",
|
||||
"catelog": "个人工具站",
|
||||
"desc": "面向开发者的在线备忘录与知识整理工具"
|
||||
},
|
||||
{
|
||||
"name": "菜鸟教程网",
|
||||
"url": "https://www.runoob.com/",
|
||||
"catelog": "编程学习",
|
||||
"desc": "提供多编程语言基础教程的入门学习平台"
|
||||
},
|
||||
{
|
||||
"name": "Vue官方文档",
|
||||
"url": "https://cn.vuejs.org/",
|
||||
"catelog": "前端开发",
|
||||
"desc": "渐进式JavaScript框架,适用于构建高效Web应用"
|
||||
},
|
||||
{
|
||||
"name": "MDN Web开发文档",
|
||||
"url": "https://developer.mozilla.org/zh-CN/docs/Learn",
|
||||
"catelog": "前端开发",
|
||||
"desc": "提供Web技术学习资源的权威开发者文档平台"
|
||||
},
|
||||
{
|
||||
"name": "GitHub代码托管平台",
|
||||
"url": "https://github.com/",
|
||||
"catelog": "开发平台",
|
||||
"desc": "全球最大的开源代码托管与协作开发平台"
|
||||
},
|
||||
{
|
||||
"name": "Vercel部署平台",
|
||||
"url": "https://vercel.com/",
|
||||
"catelog": "前端开发",
|
||||
"desc": "专注于Web应用快速构建与部署的平台"
|
||||
},
|
||||
{
|
||||
"name": "Element Plus组件库",
|
||||
"url": "https://element-plus.org/zh-CN/",
|
||||
"catelog": "前端开发",
|
||||
"desc": "基于Vue3的企业级UI组件库"
|
||||
},
|
||||
{
|
||||
"name": "JSON.cn解析工具",
|
||||
"url": "https://www.json.cn/",
|
||||
"catelog": "开发工具",
|
||||
"desc": "轻量级JSON数据解析、格式化与验证工具"
|
||||
},
|
||||
{
|
||||
"name": "ProcessOn在线协作工具",
|
||||
"url": "https://www.processon.com/diagrams",
|
||||
"catelog": "实用工具",
|
||||
"desc": "支持流程图、思维导图的在线协作绘图工具"
|
||||
},
|
||||
{
|
||||
"name": "iLovePDF PDF处理工具",
|
||||
"url": "https://www.ilovepdf.com/zh-cn/word_to_pdf",
|
||||
"catelog": "实用工具",
|
||||
"desc": "支持PDF合并、拆分、转换的在线工具"
|
||||
},
|
||||
{
|
||||
"name": "Carbon代码转图片工具",
|
||||
"url": "https://carbon.now.sh/",
|
||||
"catelog": "开发工具",
|
||||
"desc": "将代码片段转换为精美图片的在线工具"
|
||||
},
|
||||
{
|
||||
"name": "程序员盒子工具平台",
|
||||
"url": "https://www.coderutil.com/",
|
||||
"catelog": "开发工具",
|
||||
"desc": "聚合多种程序员常用工具与技术博文的资源平台"
|
||||
},
|
||||
{
|
||||
"name": "优设设计导航网",
|
||||
"url": "https://hao.uisdc.com/",
|
||||
"catelog": "设计资源",
|
||||
"desc": "为设计师精选优质设计网站与资源的导航平台"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLinks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 主容器 */
|
||||
.app-container {
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 玻璃拟态背景层 */
|
||||
.glass-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 20%),
|
||||
radial-gradient(circle at 90% 80%, rgba(255, 255, 255, 0.08) 0%, transparent 20%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 主要内容区域 - 固定布局 */
|
||||
.main-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 头部区域 - 固定高度 */
|
||||
.hero-header {
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
padding: 12px 20px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 0 0 20px 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-bottom: none;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.logo-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 2rem;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 0.95rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 400;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 控制区域 */
|
||||
.controls-section {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 标签云 */
|
||||
.tags-cloud {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tags-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
max-width: 1000px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.85rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.tag-item:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.tag-item:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tag-item.active {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #667eea;
|
||||
transform: scale(1.05) translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tag-item.popular {
|
||||
background: linear-gradient(135deg, #FF6B6B, #FF8E53);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tag-item.trending {
|
||||
background: linear-gradient(135deg, #FDC830, #F37335);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.tag-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.tag-count {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 1px 5px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 结果统计 */
|
||||
.result-stats {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.stats-badge {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* 内容区域 - 可滚动 */
|
||||
.content-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-state p {
|
||||
color: white;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 卡片网格 */
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
animation: fadeInUp 0.5s ease-out both;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
color: white;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
margin-top: 8px;
|
||||
padding: 10px 20px;
|
||||
background: white;
|
||||
color: #667eea;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 底部区域 - 固定在底部 */
|
||||
.app-footer {
|
||||
flex-shrink: 0;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
padding: 16px 20px;
|
||||
margin: 0 20px 20px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stats-overview {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.3rem;
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: white;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 动画定义 */
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.cards-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hero-header {
|
||||
padding: 24px 16px 16px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.tags-wrapper {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.stats-overview {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.hero-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-overview {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
392
src/components/FloatingCard.vue
Normal file
392
src/components/FloatingCard.vue
Normal file
@@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<div
|
||||
class="floating-card"
|
||||
:class="{ 'is-hovered': isHovered }"
|
||||
:style="cardStyle"
|
||||
@click="handleClick"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="card-title">{{ card.name }}</div>
|
||||
<div class="card-url">{{ card.url }}</div>
|
||||
<div class="card-desc">{{ card.desc }}</div>
|
||||
<div class="card-category">{{ card.catelog }}</div>
|
||||
</div>
|
||||
<!-- 悬停时的发光边框效果 -->
|
||||
<div v-if="isHovered" class="hover-glow"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
card: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
polygonShape: {
|
||||
type: String,
|
||||
default: '20% 0%, 80% 0%, 100% 100%, 0% 100%' // 默认梯形
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const position = ref({ x: 0, y: 0 })
|
||||
const targetPosition = ref({ x: 0, y: 0 })
|
||||
const size = ref({ width: 200, height: 140 })
|
||||
const rotation = ref(0)
|
||||
const animationId = ref(null)
|
||||
const isHovered = ref(false)
|
||||
const isAnimating = ref(true) // 动画状态控制
|
||||
|
||||
// 计算不规则尺寸
|
||||
const calculateSize = () => {
|
||||
const baseWidth = 160
|
||||
const baseHeight = 110
|
||||
const widthVariation = Math.random() * 60 - 10
|
||||
const heightVariation = Math.random() * 40 - 10
|
||||
return {
|
||||
width: baseWidth + widthVariation,
|
||||
height: baseHeight + heightVariation
|
||||
}
|
||||
}
|
||||
|
||||
// 获取初始位置 - 均匀分布
|
||||
const getRandomPosition = () => {
|
||||
const containerWidth = window.innerWidth
|
||||
const containerHeight = window.innerHeight
|
||||
|
||||
// 下半部分区域(从40%开始到85%)
|
||||
const startY = containerHeight * 0.4
|
||||
const endY = containerHeight * 0.85
|
||||
const availableHeight = endY - startY
|
||||
|
||||
// 使用网格布局思路,但添加随机偏移
|
||||
const totalCards = 15 // 假设最多15个卡片
|
||||
const cardsPerRow = Math.ceil(Math.sqrt(totalCards))
|
||||
const row = Math.floor(props.index / cardsPerRow)
|
||||
const col = props.index % cardsPerRow
|
||||
|
||||
const gridWidth = containerWidth / cardsPerRow
|
||||
const gridHeight = availableHeight / Math.ceil(totalCards / cardsPerRow)
|
||||
|
||||
// 基础网格位置 + 随机偏移
|
||||
const baseX = gridWidth * col + gridWidth / 2
|
||||
const baseY = startY + gridHeight * row + gridHeight / 2
|
||||
|
||||
const randomOffsetX = (Math.random() - 0.5) * (gridWidth * 0.4)
|
||||
const randomOffsetY = (Math.random() - 0.5) * (gridHeight * 0.4)
|
||||
|
||||
const x = baseX + randomOffsetX - size.value.width / 2
|
||||
const y = baseY + randomOffsetY - size.value.height / 2
|
||||
|
||||
// 边界检查
|
||||
const margin = 10
|
||||
return {
|
||||
x: Math.max(margin, Math.min(x, containerWidth - size.value.width - margin)),
|
||||
y: Math.max(startY, Math.min(y, endY - size.value.height - margin))
|
||||
}
|
||||
}
|
||||
|
||||
// 计算新的目标位置 - 缓慢移动
|
||||
const calculateNewTarget = () => {
|
||||
const containerWidth = window.innerWidth
|
||||
const containerHeight = window.innerHeight
|
||||
const startY = containerHeight * 0.4
|
||||
const endY = containerHeight * 0.85
|
||||
|
||||
// 小范围移动,保持在当前网格附近
|
||||
const time = Date.now() / 1000
|
||||
const smallRange = 30 // 移动范围减小
|
||||
|
||||
const waveX = Math.sin(time * 0.2 + props.index) * smallRange
|
||||
const waveY = Math.cos(time * 0.2 + props.index) * smallRange
|
||||
|
||||
let newX = position.value.x + waveX
|
||||
let newY = position.value.y + waveY
|
||||
|
||||
// 严格边界检查
|
||||
const maxX = containerWidth - size.value.width - 10
|
||||
const maxY = endY - size.value.height - 10
|
||||
const minY = startY
|
||||
|
||||
newX = Math.max(10, Math.min(newX, maxX))
|
||||
newY = Math.max(minY, Math.min(newY, maxY))
|
||||
|
||||
// 随机旋转
|
||||
if (Math.random() > 0.95) {
|
||||
rotation.value += (Math.random() - 0.5) * 2
|
||||
}
|
||||
|
||||
return { x: newX, y: newY }
|
||||
}
|
||||
|
||||
// 动画循环 - 简化版本
|
||||
const animate = () => {
|
||||
if (!isAnimating.value) return
|
||||
|
||||
// 检查是否悬停
|
||||
if (!isHovered.value) {
|
||||
// 检查是否到达目标位置
|
||||
const dx = targetPosition.value.x - position.value.x
|
||||
const dy = targetPosition.value.y - position.value.y
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if (distance < 2) {
|
||||
// 到达目标,计算新目标
|
||||
targetPosition.value = calculateNewTarget()
|
||||
}
|
||||
|
||||
// 平滑移动
|
||||
const ease = 0.03
|
||||
position.value.x += dx * ease
|
||||
position.value.y += dy * ease
|
||||
}
|
||||
|
||||
animationId.value = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
// 鼠标悬停处理
|
||||
const handleMouseEnter = (event) => {
|
||||
console.log(`Card ${props.index} - Mouse Enter, stopping animation`)
|
||||
isHovered.value = true
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
const handleMouseLeave = (event) => {
|
||||
console.log(`Card ${props.index} - Mouse Leave, resuming animation`)
|
||||
isHovered.value = false
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
// 处理点击
|
||||
const handleClick = () => {
|
||||
emit('click')
|
||||
}
|
||||
|
||||
// 计算卡片样式
|
||||
const cardStyle = computed(() => {
|
||||
const zIndex = isHovered.value ? 200 : 10 + props.index
|
||||
|
||||
if (isHovered.value) {
|
||||
return {
|
||||
transform: `translate(${position.value.x}px, ${position.value.y}px) rotate(${rotation.value}deg) scale(1.08)`,
|
||||
width: `${size.value.width}px`,
|
||||
height: `${size.value.height}px`,
|
||||
clipPath: `polygon(${props.polygonShape})`,
|
||||
zIndex: zIndex,
|
||||
opacity: 1,
|
||||
pointerEvents: 'auto',
|
||||
position: 'fixed !important'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
transform: `translate(${position.value.x}px, ${position.value.y}px) rotate(${rotation.value}deg)`,
|
||||
width: `${size.value.width}px`,
|
||||
height: `${size.value.height}px`,
|
||||
clipPath: `polygon(${props.polygonShape})`,
|
||||
zIndex: zIndex,
|
||||
opacity: 1,
|
||||
pointerEvents: 'auto'
|
||||
}
|
||||
})
|
||||
|
||||
// 响应窗口大小变化
|
||||
const handleResize = () => {
|
||||
// 重新计算初始位置
|
||||
position.value = getRandomPosition()
|
||||
targetPosition.value = calculateNewTarget()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 计算初始尺寸
|
||||
size.value = calculateSize()
|
||||
|
||||
// 初始化位置
|
||||
position.value = getRandomPosition()
|
||||
targetPosition.value = calculateNewTarget()
|
||||
|
||||
// 延迟启动动画
|
||||
setTimeout(() => {
|
||||
animate()
|
||||
}, 200 + props.index * 30)
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
isAnimating.value = false
|
||||
if (animationId.value) {
|
||||
cancelAnimationFrame(animationId.value)
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.floating-card {
|
||||
position: absolute;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(240, 240, 240, 0.95) 100%);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.floating-card.is-hovered {
|
||||
position: fixed !important;
|
||||
box-shadow: 0 12px 40px rgba(102, 126, 234, 0.6), 0 0 20px rgba(102, 126, 234, 0.4);
|
||||
border-color: rgba(102, 126, 234, 0.8);
|
||||
filter: brightness(1.1);
|
||||
transform: scale(1.08) !important;
|
||||
}
|
||||
|
||||
.floating-card:active {
|
||||
transform: scale(0.95) !important;
|
||||
}
|
||||
|
||||
/* 发光边框效果 */
|
||||
.hover-glow {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
background: linear-gradient(45deg, #667eea, #764ba2, #f093fb, #667eea);
|
||||
background-size: 400% 400%;
|
||||
border-radius: inherit;
|
||||
clip-path: inherit;
|
||||
opacity: 0.6;
|
||||
animation: glow-pulse 2s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes glow-pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.4;
|
||||
filter: blur(8px);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
filter: blur(12px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 悬停内容高亮 */
|
||||
.floating-card.is-hovered .card-title {
|
||||
color: #667eea;
|
||||
font-weight: 800;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.floating-card.is-hovered .card-url {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.floating-card.is-hovered .card-desc {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.floating-card.is-hovered .card-category {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(240, 147, 251, 0.4);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-url {
|
||||
font-size: 0.65rem;
|
||||
color: #666;
|
||||
margin-bottom: 3px;
|
||||
word-break: break-all;
|
||||
font-family: 'Courier New', monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.7rem;
|
||||
color: #888;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card-category {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.card-content {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.card-url {
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.card-category {
|
||||
font-size: 0.5rem;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
364
src/components/SearchBox.vue
Normal file
364
src/components/SearchBox.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<div class="search-container">
|
||||
<div class="search-box" :class="{ 'is-focused': isFocused }">
|
||||
<div class="search-icon">
|
||||
<span>🔍</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索网站名称、描述、分类或URL..."
|
||||
@input="handleSearch"
|
||||
@keydown.esc="clearSearch"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="clear-btn"
|
||||
v-if="searchQuery"
|
||||
@click="clearSearch"
|
||||
title="清空搜索"
|
||||
>
|
||||
<span>✕</span>
|
||||
</button>
|
||||
|
||||
<div class="search-decoration"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="search-hint" v-if="!searchQuery">
|
||||
<span class="hint-icon">💡</span>
|
||||
<span class="hint-text">试试搜索 "前端开发" 或 "工具"</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
filteredCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const searchQuery = ref('')
|
||||
const isFocused = ref(false)
|
||||
const emit = defineEmits(['search', 'clear'])
|
||||
|
||||
const handleSearch = () => {
|
||||
emit('search', searchQuery.value)
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
searchQuery.value = ''
|
||||
emit('clear')
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
clearSearch
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-container {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-box::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.search-box.is-focused {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 6px 30px rgba(102, 126, 234, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.search-box.is-focused::before {
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 1.3rem;
|
||||
margin-right: 12px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-box.is-focused .search-icon {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
padding: 8px 4px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.search-box.is-focused input {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
margin-left: 8px;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.clear-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 搜索装饰 */
|
||||
.search-decoration {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0%;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2, #f093fb);
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.search-box.is-focused .search-decoration {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 搜索统计 */
|
||||
.search-stats {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 10px 20px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.95rem;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.stats-content strong {
|
||||
color: #667eea;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* 搜索提示 */
|
||||
.search-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.9;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.hint-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 动画定义 */
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-container {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
padding: 10px 14px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 1.1rem;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
font-size: 0.85rem;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.search-hint {
|
||||
font-size: 0.8rem;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.search-box {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.search-hint {
|
||||
font-size: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.search-box {
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.search-box.is-focused {
|
||||
border-color: #764ba2;
|
||||
}
|
||||
|
||||
input {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: #764ba2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.search-box.is-focused::before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
461
src/components/SmartCard.vue
Normal file
461
src/components/SmartCard.vue
Normal file
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<div
|
||||
class="smart-card"
|
||||
:class="{
|
||||
'is-hovered': isHovered,
|
||||
'is-pressed': isPressed
|
||||
}"
|
||||
:style="cardStyle"
|
||||
@click="handleClick"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@mousedown="handleMouseDown"
|
||||
@mouseup="handleMouseUp"
|
||||
>
|
||||
<!-- 卡片背景装饰 -->
|
||||
<div class="card-decoration" :style="{ background: categoryColor }"></div>
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<div class="card-content">
|
||||
<div class="card-header">
|
||||
<div class="category-badge" :style="{ background: categoryColor }">
|
||||
{{ card.catelog }}
|
||||
</div>
|
||||
<h3 class="card-title">{{ card.name }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<p class="card-description">{{ card.desc }}</p>
|
||||
<div class="card-url">{{ formatUrl(card.url) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="interaction-hint">
|
||||
<span class="hint-icon">🔗</span>
|
||||
<span class="hint-text">点击访问</span>
|
||||
</div>
|
||||
<div class="hover-actions" v-if="isHovered">
|
||||
<button class="action-icon" title="新标签打开">
|
||||
<span>↗</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 悬停光效层 -->
|
||||
<div v-if="isHovered" class="hover-glow" :style="{ background: categoryColor }"></div>
|
||||
|
||||
<!-- 点击涟漪效果 -->
|
||||
<div v-if="isPressed" class="ripple-effect" :style="{ background: categoryColor }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
card: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
categoryColor: {
|
||||
type: String,
|
||||
default: '#667eea'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const isHovered = ref(false)
|
||||
const isPressed = ref(false)
|
||||
const rotation = ref(0)
|
||||
|
||||
// 计算卡片样式
|
||||
const cardStyle = computed(() => {
|
||||
const baseZIndex = isHovered.value ? 100 + props.index : 10 + props.index
|
||||
|
||||
let transform = `translateZ(0)`
|
||||
|
||||
if (isHovered.value) {
|
||||
transform += ` scale(1.03) translateY(-4px)`
|
||||
}
|
||||
|
||||
if (isPressed.value) {
|
||||
transform += ` scale(0.98)`
|
||||
}
|
||||
|
||||
return {
|
||||
transform,
|
||||
zIndex: baseZIndex,
|
||||
'--category-color': props.categoryColor
|
||||
}
|
||||
})
|
||||
|
||||
// 格式化URL显示
|
||||
const formatUrl = (url) => {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.hostname.replace('www.', '')
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标进入
|
||||
const handleMouseEnter = (event) => {
|
||||
isHovered.value = true
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
// 鼠标离开
|
||||
const handleMouseLeave = (event) => {
|
||||
isHovered.value = false
|
||||
isPressed.value = false
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
// 鼠标按下
|
||||
const handleMouseDown = (event) => {
|
||||
isPressed.value = true
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
// 鼠标释放
|
||||
const handleMouseUp = (event) => {
|
||||
isPressed.value = false
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
// 处理点击
|
||||
const handleClick = (event) => {
|
||||
// 防止在拖动时触发
|
||||
if (!isPressed.value) {
|
||||
isPressed.value = true
|
||||
setTimeout(() => {
|
||||
isPressed.value = false
|
||||
emit('click')
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.smart-card {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
min-height: 140px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
backdrop-filter: blur(10px);
|
||||
user-select: none;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.smart-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--category-color);
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.smart-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.smart-card.is-hovered {
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
}
|
||||
|
||||
.smart-card.is-pressed {
|
||||
transform: scale(0.98) translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.card-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 0 16px 0 12px;
|
||||
opacity: 0.15;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.smart-card.is-hovered .card-decoration {
|
||||
opacity: 0.25;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
/* 卡片内容容器 */
|
||||
.card-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
align-self: flex-start;
|
||||
backdrop-filter: blur(4px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
letter-spacing: -0.3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 卡片主体 */
|
||||
.card-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 0.85rem;
|
||||
color: #555;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
min-height: 2.4em;
|
||||
}
|
||||
|
||||
.card-url {
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 4px 8px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 6px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* 卡片底部 */
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.interaction-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.3s ease;
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.smart-card.is-hovered .interaction-hint {
|
||||
opacity: 1;
|
||||
color: var(--category-color);
|
||||
}
|
||||
|
||||
.hint-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hover-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-icon:hover {
|
||||
background: var(--category-color);
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 悬停光效层 */
|
||||
.hover-glow {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
border-radius: 16px;
|
||||
opacity: 0;
|
||||
animation: glow-pulse 1.5s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.smart-card.is-hovered .hover-glow {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
@keyframes glow-pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
filter: blur(8px);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
filter: blur(12px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 涟漪效果 */
|
||||
.ripple-effect {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 0.6;
|
||||
animation: ripple 0.6s ease-out;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
to {
|
||||
transform: translate(-50%, -50%) scale(4);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 点击反馈 */
|
||||
.smart-card:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.smart-card {
|
||||
padding: 12px;
|
||||
min-height: 120px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 0.8rem;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.card-url {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.smart-card {
|
||||
padding: 10px;
|
||||
min-height: 110px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
-webkit-line-clamp: 1;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式支持(如果需要) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.smart-card {
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.card-url {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
src/main.js
Normal file
5
src/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
197
src/style.css
Normal file
197
src/style.css
Normal file
@@ -0,0 +1,197 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* 按钮基础样式重置 */
|
||||
button {
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 输入框基础样式 */
|
||||
input {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* 通用动画类 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 工具类 */
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.backdrop-glass {
|
||||
backdrop-filter: blur(20px);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 响应式栅格 */
|
||||
.grid-responsive {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/* 防止 iOS 缩放 */
|
||||
input, textarea, select {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 高对比度模式支持 */
|
||||
@media (prefers-contrast: high) {
|
||||
.smart-card,
|
||||
.search-box,
|
||||
.tag-item {
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画模式 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 打印样式优化 */
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.glass-bg,
|
||||
.app-footer,
|
||||
.controls-section,
|
||||
.hero-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.smart-card {
|
||||
break-inside: avoid;
|
||||
box-shadow: none;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
/* 选中文本样式 */
|
||||
::selection {
|
||||
background: rgba(102, 126, 234, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 焦点可见性 */
|
||||
:focus-visible {
|
||||
outline: 2px solid #667eea;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 隐藏类 */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 文本截断 */
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 多行文本截断 */
|
||||
.text-ellipsis-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.text-ellipsis-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
Reference in New Issue
Block a user