first commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
138
NEW_UI_GUIDE.md
Normal file
138
NEW_UI_GUIDE.md
Normal 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
122
README.md
Normal 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
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
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
14
dist/index.html
vendored
Normal 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
2936
dist/link.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
12
index.html
Normal file
12
index.html
Normal 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>
|
||||
1115
package-lock.json
generated
Normal file
1115
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal 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
2936
public/link.json
Normal file
File diff suppressed because it is too large
Load Diff
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;
|
||||
}
|
||||
16
vercel
Normal file
16
vercel
Normal 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
14
vite.config.js
Normal 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'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user