first commit

This commit is contained in:
爱喝水的木子
2026-01-08 14:05:55 +08:00
commit 74d351ba32
20 changed files with 12593 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

138
NEW_UI_GUIDE.md Normal file
View File

@@ -0,0 +1,138 @@
# 新UI设计指南
## 🎨 设计理念
新的UI界面采用了现代化的**玻璃拟态设计**风格,结合了以下设计原则:
- **玻璃拟态 (Glassmorphism)** - 半透明背景与模糊效果
- **渐变色彩** - 流畅的色彩过渡
- **微交互** - 丰富的悬停和点击反馈
- **响应式设计** - 完美适配各种设备
## 🌟 主要特性
### 1. 视觉层次
- **英雄头部** - 包含Logo和欢迎文案
- **智能搜索** - 支持实时搜索与自动补全提示
- **标签云** - 分类标签带有流行度和趋势标识
- **卡片网格** - 响应式网格布局,自动适配屏幕宽度
### 2. 交互体验
- **悬停效果** - 卡片上浮、光晕、阴影增强
- **点击反馈** - 涟漪动画、缩放效果
- **状态指示** - 搜索结果统计、空状态提示
- **快捷操作** - 随机打乱、重置筛选
### 3. 智能功能
- **分类图标** - 每个分类都有独特的emoji图标
- **颜色编码** - 不同分类使用不同主题色
- **动态标签** - 热门标签自动高亮、趋势标签动画
- **URL格式化** - 简洁显示网站域名
## 🎯 设计亮点
### 玻璃拟态组件
```css
/* 背景 */
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.25);
```
### 动画系统
- `float` - Logo悬浮动画
- `pulse` - 趋势标签呼吸动画
- `glow-pulse` - 卡片发光脉冲
- `ripple` - 点击涟漪效果
- `fadeInUp` - 卡片渐入动画
### 响应式断点
- **1024px** - 中等屏幕优化
- **768px** - 平板设备适配
- **480px** - 手机设备优化
## 🎨 色彩系统
### 主题渐变
```css
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
```
### 分类色彩
- 影视娱乐: #FF6B6B
- 个人工具站: #4ECDC4
- 编程学习: #45B7D1
- 前端开发: #96CEB4
- 开发平台: #FFEAA7
- 开发工具: #DDA0DD
- 实用工具: #98D8C8
- 设计资源: #F7DC6F
## 📱 移动端优化
1. **触摸友好** - 更大的点击区域
2. **手势优化** - 防止误触,支持快速操作
3. **性能优化** - 减少动画,提升流畅度
4. **视觉调整** - 适配小屏幕的布局变化
## ✨ 无障碍特性
- **高对比度模式** - 适配 `prefers-contrast: high`
- **减少动画** - 适配 `prefers-reduced-motion: reduce`
- **键盘导航** - 清晰的焦点指示
- **屏幕阅读器** - 语义化HTML结构
## 🚀 使用指南
### 搜索功能
1. 在搜索框中输入关键词
2. 实时查看匹配结果
3. 按 ESC 清空搜索
4. 点击 ✕ 按钮重置
### 分类浏览
1. 浏览标签云中的分类
2. 点击标签筛选内容
3. 热门分类自动高亮
4. 趋势分类有动画标识
### 快捷操作
- **随机** - 打乱当前显示顺序
- **重置** - 清除所有筛选条件
## 🎨 组件结构
```
App.vue
├── Hero Header (头部区域)
├── Controls Section (控制区域)
│ ├── SearchBox (搜索框)
│ ├── Tags Cloud (标签云)
│ └── Result Stats (结果统计)
├── Content Section (内容区域)
│ ├── Loading State (加载状态)
│ ├── Cards Grid (卡片网格)
│ └── Empty State (空状态)
└── App Footer (底部统计与操作)
```
## 🔧 技术实现
### 状态管理
- Vue 3 Composition API
- Refs 和 Computed Properties
- 响应式数据绑定
### 样式架构
- Scoped CSS 组件样式
- CSS 自定义属性 (CSS Variables)
- CSS Grid 布局系统
### 动画系统
- CSS Keyframes
- Transition Groups
- JavaScript 动画控制
---
这个新UI设计将导航体验提升到了一个全新的水平不仅视觉上更加吸引人功能上也更加智能和易用。

122
README.md Normal file
View File

@@ -0,0 +1,122 @@
# Vue3 智能导航系统
一个纯前端的Vue3导航项目采用**上下分区布局**:上半部分是搜索和分类网格,下半部分是随机移动的不规则多边形卡片,完美解决闪烁问题。
## 功能特点
- 🎨 **智能上下布局**:
- 上半部分:搜索框 + 分类网格(点击筛选)
- 下半部分:随机移动的不规则多边形卡片
- 🔍 **实时搜索**: 支持搜索网站名称、描述、分类和URL
- 🎯 **多边形卡片**:
- 使用 `clip-path` 实现梯形、五边形、菱形等不规则形状
- 每个卡片都有不同的尺寸和随机旋转
- 仅在下半部分区域移动,永不越界
-**无闪烁设计**:
- 完全移除悬停暂停功能
- 卡片持续平滑移动,无任何中断
- 纯CSS过渡无JavaScript状态冲突
- 🔗 **网站导航**: 从 `public/link.json` 读取链接数据
- 📱 **响应式设计**: 适配不同屏幕尺寸
-**纯前端**: 无需后端可部署在Vercel等静态托管平台
## 项目结构
```
├── public/
│ └── link.json # 网站链接数据
├── src/
│ ├── components/
│ │ └── FloatingCard.vue # 浮动卡片组件
│ ├── App.vue # 主应用组件
│ └── main.js # 应用入口
├── index.html # HTML入口
├── vite.config.js # Vite配置
├── package.json # 项目依赖
├── vercel.json # Vercel部署配置
└── README.md # 项目说明
```
## 数据格式
`link.json` 文件格式要求:
```json
[
{
"name": "网站名称",
"url": "https://example.com",
"catelog": "分类",
"desc": "网站描述"
}
]
```
## 安装和运行
### 本地开发
```bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建生产版本
npm run build
# 预览生产版本
npm run preview
```
### 部署到Vercel
1. 将项目推送到GitHub仓库
2. 登录Vercel并导入GitHub仓库
3. Vercel会自动检测并部署
## 使用说明
1. 首次运行时,项目会自动从 `public/link.json` 读取数据
2. 如果 `public/link.json` 不存在,会使用内置的示例数据
3. 卡片会在屏幕上随机移动,鼠标悬停时会暂停
4. 点击任意卡片即可在新标签页中打开对应网站
5. 可以修改 `link.json` 文件来添加或更新链接
## 自定义配置
### 修改卡片数量
`src/App.vue` 中修改 `maxCards` 变量:
```javascript
const maxCards = ref(20) // 默认显示15个卡片
```
### 修改动画速度
`src/components/FloatingCard.vue` 中修改动画参数:
```javascript
const ease = 0.02 // 移动平滑度
const time = Date.now() / 1000 // 动画时间因子
```
## 技术栈
- Vue 3.3
- Vite 4.4
- Element Plus (用于加载状态)
- 纯JavaScript动画 (requestAnimationFrame)
## 浏览器兼容性
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
## 许可证
MIT License

1
dist/assets/index-331758b3.css vendored Normal file

File diff suppressed because one or more lines are too long

17
dist/assets/index-694da4b5.js vendored Normal file

File diff suppressed because one or more lines are too long

14
dist/index.html vendored Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>导航卡片 - 随机移动网站导航</title>
<script type="module" crossorigin src="/assets/index-694da4b5.js"></script>
<link rel="stylesheet" href="/assets/index-331758b3.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

2936
dist/link.json vendored Normal file

File diff suppressed because it is too large Load Diff

12
index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>导航卡片</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

2936
link.json Normal file

File diff suppressed because it is too large Load Diff

1115
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "vue3-navigation-dashboard",
"version": "1.0.0",
"description": "纯前端Vue3导航项目带随机移动卡片",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1",
"element-plus": "^2.4.0",
"vue": "^3.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.0",
"vite": "^4.4.0"
}
}

2936
public/link.json Normal file

File diff suppressed because it is too large Load Diff

895
src/App.vue Normal file
View 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>

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

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

View 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
View 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
View 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;
}

16
vercel Normal file
View File

@@ -0,0 +1,16 @@
{
"version": 2,
"builds": [
{
"src": "dist/**",
"use": "@vercel/static"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/$1"
}
],
"outputDirectory": "dist"
}

14
vite.config.js Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
open: true
},
build: {
outDir: 'dist',
assetsDir: 'assets'
}
})