diff --git a/frontend/src/composables/index.ts b/frontend/src/composables/index.ts new file mode 100644 index 0000000..1513b31 --- /dev/null +++ b/frontend/src/composables/index.ts @@ -0,0 +1,6 @@ +/** + * Composables - 可复用业务逻辑 + */ +export * from './useFormatters' +export * from './useProjects' +export * from './useModels' diff --git a/frontend/src/composables/useFormatters.ts b/frontend/src/composables/useFormatters.ts new file mode 100644 index 0000000..cd0b174 --- /dev/null +++ b/frontend/src/composables/useFormatters.ts @@ -0,0 +1,71 @@ +/** + * 格式化工具函数 + */ + +/** + * 格式化文件大小 + */ +export function formatSize(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +/** + * 格式化日期 + */ +export function formatDate(date: string | Date): string { + const d = new Date(date) + const year = d.getFullYear() + const month = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} + +/** + * 格式化日期时间 + */ +export function formatDateTime(date: string | Date): string { + const d = new Date(date) + const year = d.getFullYear() + const month = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + const hours = String(d.getHours()).padStart(2, '0') + const minutes = String(d.getMinutes()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}` +} + +/** + * 格式化相对时间 + */ +export function formatRelativeTime(date: string | Date): string { + const now = new Date() + const d = new Date(date) + const diff = now.getTime() - d.getTime() + + const seconds = Math.floor(diff / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (days > 7) { + return formatDate(date) + } else if (days > 0) { + return `${days} 天前` + } else if (hours > 0) { + return `${hours} 小时前` + } else if (minutes > 0) { + return `${minutes} 分钟前` + } else { + return '刚刚' + } +} + +/** + * 格式化数字(千分位) + */ +export function formatNumber(num: number): string { + return num.toLocaleString('zh-CN') +} diff --git a/frontend/src/composables/useModels.ts b/frontend/src/composables/useModels.ts new file mode 100644 index 0000000..8579e25 --- /dev/null +++ b/frontend/src/composables/useModels.ts @@ -0,0 +1,112 @@ +/** + * 模型相关业务逻辑 + */ +import { ref } from 'vue' +import { modelApi } from '@/api' +import type { Model } from '@/types' +import { ElMessage } from 'element-plus' + +export function useModels() { + const loading = ref(false) + const models = ref([]) + + /** + * 获取模型列表 + */ + const fetchModels = async () => { + loading.value = true + try { + const res = await modelApi.list() + // 处理两种响应格式 + if (Array.isArray(res)) { + models.value = res + } else if (res?.data && Array.isArray(res.data)) { + models.value = res.data + } else if (res?.results && Array.isArray(res.results)) { + models.value = res.results + } else { + models.value = [] + } + } catch (error: any) { + console.error('获取模型列表失败:', error) + ElMessage.error('获取模型列表失败') + models.value = [] + } finally { + loading.value = false + } + } + + /** + * 添加模型 + */ + const addModel = async (data: Partial): Promise => { + try { + await modelApi.create(data) + ElMessage.success('添加成功') + await fetchModels() + return true + } catch (error: any) { + console.error('添加模型失败:', error) + ElMessage.error(error?.message || '添加模型失败') + return false + } + } + + /** + * 更新模型 + */ + const updateModel = async (id: number, data: Partial): Promise => { + try { + await modelApi.update(id, data) + ElMessage.success('更新成功') + await fetchModels() + return true + } catch (error: any) { + console.error('更新模型失败:', error) + ElMessage.error(error?.message || '更新模型失败') + return false + } + } + + /** + * 删除模型 + */ + const deleteModel = async (id: number): Promise => { + try { + await modelApi.delete(id) + ElMessage.success('删除成功') + await fetchModels() + return true + } catch (error: any) { + console.error('删除模型失败:', error) + ElMessage.error(error?.message || '删除模型失败') + return false + } + } + + /** + * 设置默认模型 + */ + const setDefaultModel = async (id: number): Promise => { + try { + await modelApi.setDefault(id) + ElMessage.success('设置成功') + await fetchModels() + return true + } catch (error: any) { + console.error('设置默认模型失败:', error) + ElMessage.error(error?.message || '设置默认模型失败') + return false + } + } + + return { + loading, + models, + fetchModels, + addModel, + updateModel, + deleteModel, + setDefaultModel + } +} diff --git a/frontend/src/composables/useProjects.ts b/frontend/src/composables/useProjects.ts new file mode 100644 index 0000000..d0e3633 --- /dev/null +++ b/frontend/src/composables/useProjects.ts @@ -0,0 +1,98 @@ +/** + * 项目相关业务逻辑 + */ +import { ref } from 'vue' +import { projectApi } from '@/api' +import type { Project, ProjectCreate } from '@/types' +import { ElMessage } from 'element-plus' + +export function useProjects() { + const loading = ref(false) + const projects = ref([]) + + /** + * 获取项目列表 + */ + const fetchProjects = async () => { + loading.value = true + try { + const res = await projectApi.list() + // 处理两种响应格式 + if (Array.isArray(res)) { + projects.value = res + } else if (res?.data && Array.isArray(res.data)) { + projects.value = res.data + } else if (res?.results && Array.isArray(res.results)) { + projects.value = res.results + } else { + projects.value = [] + } + } catch (error: any) { + console.error('获取项目列表失败:', error) + ElMessage.error('获取项目列表失败') + projects.value = [] + } finally { + loading.value = false + } + } + + /** + * 创建项目 + */ + const createProject = async (data: ProjectCreate): Promise => { + try { + const res = await projectApi.create(data) + ElMessage.success('创建成功') + await fetchProjects() + return res + } catch (error: any) { + console.error('创建项目失败:', error) + ElMessage.error(error?.message || '创建项目失败') + return null + } + } + + /** + * 删除项目 + */ + const deleteProject = async (id: number): Promise => { + try { + await projectApi.delete(id) + ElMessage.success('删除成功') + await fetchProjects() + return true + } catch (error: any) { + console.error('删除项目失败:', error) + ElMessage.error(error?.message || '删除项目失败') + return false + } + } + + /** + * 获取项目详情 + */ + const fetchProject = async (id: number): Promise => { + try { + const res = await projectApi.get(id) + if (res && typeof res === 'object' && 'id' in res) { + return res as Project + } else if (res?.data) { + return res.data as Project + } + return null + } catch (error: any) { + console.error('获取项目详情失败:', error) + ElMessage.error('获取项目详情失败') + return null + } + } + + return { + loading, + projects, + fetchProjects, + createProject, + deleteProject, + fetchProject + } +} diff --git a/frontend/src/styles/index.scss b/frontend/src/styles/index.scss new file mode 100644 index 0000000..e5bea0c --- /dev/null +++ b/frontend/src/styles/index.scss @@ -0,0 +1,9 @@ +/** + * 样式入口文件 + */ + +// 页面样式 +@import './pages/home'; +// 后续可以添加更多页面样式 +// @import './pages/project'; +// @import './pages/settings'; diff --git a/frontend/src/styles/pages/home.scss b/frontend/src/styles/pages/home.scss new file mode 100644 index 0000000..5ef0a9f --- /dev/null +++ b/frontend/src/styles/pages/home.scss @@ -0,0 +1,882 @@ +/* ======================== + HomeView Styles + ======================== */ + +.home { + min-height: 100vh; + padding: 60px 40px 80px; + max-width: 1400px; + margin: 0 auto; +} + +/* Hero Section */ +.hero { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 60px; + align-items: center; + margin-bottom: 60px; + position: relative; + overflow: hidden; +} + +/* Hero background glamour effects - extends to full section */ +.hero::before { + content: ''; + position: absolute; + top: -100px; + right: -100px; + width: 600px; + height: 600px; + background: radial-gradient( + circle, + rgba(0, 212, 255, 0.12) 0%, + rgba(0, 212, 255, 0.04) 30%, + transparent 60% + ); + filter: blur(60px); + animation: heroGlowMove 15s ease-in-out infinite; + pointer-events: none; + z-index: 0; +} + +.hero::after { + content: ''; + position: absolute; + top: 50%; + right: 0; + width: 400px; + height: 400px; + background: radial-gradient( + circle, + rgba(124, 58, 237, 0.1) 0%, + transparent 60% + ); + filter: blur(50px); + animation: heroGlowMove 20s ease-in-out infinite reverse; + pointer-events: none; + z-index: 0; +} + +@keyframes heroGlowMove { + 0%, 100% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(-30px, 20px) scale(1.1); } +} + +.hero-content, +.hero-visual { + position: relative; + z-index: 1; +} + +.hero-logo { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 24px; + + svg { + filter: drop-shadow(0 0 16px rgba(0, 212, 255, 0.35)); + } +} + +.logo-text { + font-size: 28px; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.5px; +} + +.logo-highlight { + background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 14px; + background: var(--accent-primary-muted); + border: 1px solid rgba(0, 212, 255, 0.2); + border-radius: 100px; + font-size: 13px; + color: var(--accent-primary); + margin-bottom: 20px; +} + +.badge-dot { + width: 6px; + height: 6px; + background: var(--accent-primary); + border-radius: 50%; + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(1.2); } +} + +.hero-title { + font-size: 56px; + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.03em; + margin-bottom: 20px; +} + +.hero-subtitle { + font-size: 18px; + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 32px; + max-width: 480px; +} + +.hero-actions { + display: flex; + gap: 14px; +} + +.btn-primary { + padding: 14px 28px; + font-size: 15px; + border-radius: var(--radius-md); +} + +.btn-secondary { + padding: 14px 28px; + font-size: 15px; + border-radius: var(--radius-md); + background: var(--bg-tertiary); + border: 1px solid var(--border-default); + color: var(--text-primary); +} + +.btn-secondary:hover { + background: var(--bg-hover); + border-color: var(--border-strong); +} + +/* ======================== + Hero Visual - Modern Abstract (lightweight, no container boundary) + ======================== */ +.hero-visual { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 50%; + pointer-events: none; +} + +/* Galaxy Background - central star cluster */ +.galaxy-bg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 500px; + height: 500px; + z-index: 1; +} + +/* Galaxy core - bright central region */ +.galaxy-core { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 120px; + height: 120px; + background: radial-gradient( + circle, + rgba(255, 250, 240, 0.9) 0%, + rgba(255, 220, 180, 0.6) 20%, + rgba(255, 180, 100, 0.3) 40%, + rgba(100, 80, 200, 0.15) 60%, + transparent 80% + ); + border-radius: 50%; + filter: blur(2px); + animation: corePulse 4s ease-in-out infinite; +} + +@keyframes corePulse { + 0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; } + 50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.9; } +} + +/* Galaxy spiral arms */ +.galaxy-spiral { + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 100%; + transform: translate(-50%, -50%); +} + +.spiral-arm { + position: absolute; + top: 50%; + left: 50%; + width: 200px; + height: 200px; + border: 1px solid transparent; + border-radius: 50%; + opacity: 0.15; + animation: spiralRotate 60s linear infinite; +} + +.spiral-arm::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + background: conic-gradient( + from 0deg, + transparent 0deg, + rgba(0, 212, 255, 0.3) 30deg, + transparent 60deg, + rgba(124, 58, 237, 0.3) 120deg, + transparent 150deg, + rgba(0, 212, 255, 0.2) 210deg, + transparent 240deg, + rgba(124, 58, 237, 0.2) 330deg, + transparent 360deg + ); + mask-image: radial-gradient(circle, black 30%, transparent 70%); + -webkit-mask-image: radial-gradient(circle, black 30%, transparent 70%); +} + +.spiral-arm-1 { + transform: translate(-50%, -50%) rotate(0deg); + animation-delay: 0s; +} + +.spiral-arm-2 { + transform: translate(-50%, -50%) rotate(60deg); + animation-delay: -20s; +} + +.spiral-arm-3 { + transform: translate(-50%, -50%) rotate(120deg); + animation-delay: -40s; +} + +@keyframes spiralRotate { + from { transform: translate(-50%, -50%) rotate(0deg); } + to { transform: translate(-50%, -50%) rotate(360deg); } +} + +/* Orbiting stars around galaxy */ +.orbit-ring { + position: absolute; + top: 50%; + left: 50%; + border-radius: 50%; + border: 1px solid transparent; + transform: translate(-50%, -50%); +} + +.orbit-ring-1 { + width: 180px; + height: 180px; + border-color: rgba(0, 212, 255, 0.1); + animation: orbitRotate 25s linear infinite; +} + +.orbit-ring-2 { + width: 260px; + height: 260px; + border-color: rgba(124, 58, 237, 0.08); + animation: orbitRotate 35s linear infinite reverse; +} + +.orbit-ring-3 { + width: 340px; + height: 340px; + border-color: rgba(6, 182, 212, 0.06); + animation: orbitRotate 45s linear infinite; +} + +.orbit-ring-4 { + width: 420px; + height: 420px; + border-color: rgba(124, 58, 237, 0.05); + animation: orbitRotate 55s linear infinite reverse; +} + +@keyframes orbitRotate { + from { transform: translate(-50%, -50%) rotate(0deg); } + to { transform: translate(-50%, -50%) rotate(360deg); } +} + +/* Stars on orbits */ +.orbit-star { + position: absolute; + width: 3px; + height: 3px; + background: #fff; + border-radius: 50%; + box-shadow: 0 0 6px 1px rgba(255, 255, 255, 0.5); + animation: starTwinkle 3s ease-in-out infinite; +} + +.orbit-ring-1 .orbit-star:nth-child(1) { top: 0; left: 50%; transform: translate(-50%, -50%); } +.orbit-ring-1 .orbit-star:nth-child(2) { top: 25%; right: 10%; } +.orbit-ring-1 .orbit-star:nth-child(3) { bottom: 15%; left: 20%; } +.orbit-ring-1 .orbit-star:nth-child(4) { top: 60%; right: 25%; animation-delay: -1s; } + +.orbit-ring-2 .orbit-star:nth-child(1) { top: 15%; right: 30%; animation-delay: -0.5s; } +.orbit-ring-2 .orbit-star:nth-child(2) { bottom: 20%; left: 15%; animation-delay: -1.5s; } +.orbit-ring-2 .orbit-star:nth-child(3) { top: 40%; left: 5%; animation-delay: -2s; } +.orbit-ring-2 .orbit-star:nth-child(4) { bottom: 5%; right: 20%; animation-delay: -2.5s; } + +.orbit-ring-3 .orbit-star:nth-child(1) { top: 30%; right: 10%; animation-delay: -0.3s; } +.orbit-ring-3 .orbit-star:nth-child(2) { bottom: 25%; left: 25%; animation-delay: -1.2s; } +.orbit-ring-3 .orbit-star:nth-child(3) { top: 10%; left: 40%; animation-delay: -2.1s; } + +.orbit-ring-4 .orbit-star:nth-child(1) { top: 20%; right: 35%; animation-delay: -0.7s; } +.orbit-ring-4 .orbit-star:nth-child(2) { bottom: 30%; left: 20%; animation-delay: -1.8s; } + +@keyframes starTwinkle { + 0%, 100% { opacity: 0.5; transform: translate(-50%, -50%) scale(1); } + 50% { opacity: 1; transform: translate(-50%, -50%) scale(1.3); } +} + +/* Nebula clouds */ +.nebula-cloud { + position: absolute; + border-radius: 50%; + filter: blur(40px); + opacity: 0.2; + animation: nebulaFloat 20s ease-in-out infinite; +} + +.nebula-1 { + width: 200px; + height: 150px; + background: radial-gradient(ellipse, rgba(0, 212, 255, 0.3) 0%, transparent 70%); + top: 30%; + left: 20%; + animation-delay: 0s; +} + +.nebula-2 { + width: 180px; + height: 120px; + background: radial-gradient(ellipse, rgba(124, 58, 237, 0.25) 0%, transparent 70%); + bottom: 25%; + right: 15%; + animation-delay: -10s; +} + +.nebula-3 { + width: 150px; + height: 100px; + background: radial-gradient(ellipse, rgba(6, 182, 212, 0.2) 0%, transparent 70%); + top: 60%; + left: 35%; + animation-delay: -5s; +} + +@keyframes nebulaFloat { + 0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.2; } + 50% { transform: translate(10px, -15px) scale(1.1); opacity: 0.3; } +} + +/* Light rays - subtle */ +.light-rays { + position: absolute; + inset: 0; + overflow: hidden; + opacity: 0.5; +} + +.ray { + position: absolute; + top: -50%; + left: 50%; + width: 1px; + height: 200%; + background: linear-gradient( + 180deg, + transparent 0%, + rgba(255, 255, 255, 0.02) 50%, + transparent 100% + ); + transform-origin: top center; + animation: rayRotate 40s linear infinite; +} + +.ray:nth-child(1) { transform: rotate(-20deg); } +.ray:nth-child(2) { transform: rotate(0deg); } +.ray:nth-child(3) { transform: rotate(20deg); } +.ray:nth-child(4) { transform: rotate(40deg); } +.ray:nth-child(5) { transform: rotate(-40deg); } + +@keyframes rayRotate { + 0% { transform: rotate(-20deg); } + 100% { transform: rotate(20deg); } +} + +/* Ambient floating particles */ +.ambient-particle { + position: absolute; + width: 3px; + height: 3px; + background: rgba(255, 255, 255, 0.3); + border-radius: 50%; + animation: ambientFloat 20s ease-in-out infinite; +} + +.ambient-particle:nth-child(1) { left: 15%; top: 25%; animation-delay: 0s; } +.ambient-particle:nth-child(2) { left: 30%; top: 60%; animation-delay: -5s; } +.ambient-particle:nth-child(3) { left: 60%; top: 20%; animation-delay: -10s; } +.ambient-particle:nth-child(4) { left: 75%; top: 70%; animation-delay: -15s; } +.ambient-particle:nth-child(5) { left: 45%; top: 85%; animation-delay: -7s; } + +@keyframes ambientFloat { + 0%, 100% { transform: translateY(0) scale(1); opacity: 0.2; } + 50% { transform: translateY(-40px) scale(1.2); opacity: 0.4; } +} + +/* === Abstract Gradient Orbs - more subtle === */ +.orb { + position: absolute; + border-radius: 50%; + filter: blur(100px); + opacity: 0.25; + animation: orbFloat 25s ease-in-out infinite; +} + +.orb-1 { + width: 400px; + height: 400px; + background: radial-gradient(circle, rgba(0, 212, 255, 0.2) 0%, transparent 70%); + top: -10%; + right: 5%; + animation-delay: 0s; +} + +.orb-2 { + width: 350px; + height: 350px; + background: radial-gradient(circle, rgba(124, 58, 237, 0.15) 0%, transparent 70%); + bottom: -5%; + left: 25%; + animation-delay: -10s; +} + +.orb-3 { + width: 300px; + height: 300px; + background: radial-gradient(circle, rgba(6, 182, 212, 0.12) 0%, transparent 70%); + top: 30%; + right: 25%; + animation-delay: -18s; +} + +@keyframes orbFloat { + 0%, 100% { transform: translate(0, 0) scale(1); } + 33% { transform: translate(30px, -30px) scale(1.08); } + 66% { transform: translate(-20px, 20px) scale(0.95); } +} + +/* Central floating UI mockup */ +.floating-ui { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 220px; + padding: 20px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + backdrop-filter: blur(20px); + animation: uiFloat 8s ease-in-out infinite; + z-index: 2; +} + +@keyframes uiFloat { + 0%, 100% { transform: translate(-50%, -50%) translateY(0); } + 50% { transform: translate(-50%, -50%) translateY(-12px); } +} + +.ui-header { + display: flex; + gap: 6px; + margin-bottom: 16px; +} + +.ui-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.15); +} + +.ui-dot:nth-child(1) { background: rgba(239, 68, 68, 0.6); } +.ui-dot:nth-child(2) { background: rgba(234, 179, 8, 0.6); } +.ui-dot:nth-child(3) { background: rgba(34, 197, 94, 0.6); } + +.ui-content { + display: flex; + flex-direction: column; + gap: 8px; +} + +.ui-line { + height: 8px; + background: rgba(255, 255, 255, 0.08); + border-radius: 4px; +} + +.ui-line.short { width: 60%; } + +.ui-badge { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 16px; + padding: 8px 12px; + background: rgba(34, 197, 94, 0.15); + border: 1px solid rgba(34, 197, 94, 0.3); + border-radius: 20px; + color: #22c55e; + font-size: 12px; + font-weight: 500; +} + +/* Floating feature pills */ +.feature-pill { + position: absolute; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 100px; + backdrop-filter: blur(16px); + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + white-space: nowrap; + animation: pillFloat 10s ease-in-out infinite; + transition: all 0.3s ease; + z-index: 3; +} + +.feature-pill:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); + transform: scale(1.05); +} + +.feature-pill .el-icon { + font-size: 16px; +} + +.pill-1 { + top: 18%; + right: 30%; + animation-delay: 0s; +} + +.pill-1 .el-icon { color: #06b6d4; } + +.pill-2 { + top: 42%; + left: 8%; + animation-delay: -3s; +} + +.pill-2 .el-icon { color: #a855f7; } + +.pill-3 { + bottom: 18%; + right: 22%; + animation-delay: -6s; +} + +.pill-3 .el-icon { color: #22c55e; } + +.pill-4 { + top: 10%; + right: 50%; + animation-delay: -2s; +} + +.pill-4 .el-icon { color: #f59e0b; } + +.pill-5 { + top: 60%; + left: 12%; + animation-delay: -4s; +} + +.pill-5 .el-icon { color: #ec4899; } + +.pill-6 { + bottom: 8%; + right: 45%; + animation-delay: -7s; +} + +.pill-6 .el-icon { color: #6366f1; } + +.pill-7 { + top: 32%; + right: 8%; + animation-delay: -9s; +} + +.pill-7 .el-icon { color: #14b8a6; } + +/* Orbital float animation - each pill orbits around center */ +@keyframes pillFloat { + 0% { transform: translate(0, 0) rotate(0deg); } + 25% { transform: translate(8px, -12px) rotate(5deg); } + 50% { transform: translate(0, -20px) rotate(0deg); } + 75% { transform: translate(-8px, -12px) rotate(-5deg); } + 100% { transform: translate(0, 0) rotate(0deg); } +} + +/* Each pill has unique orbit parameters */ +.pill-1 { + animation: pillOrbit1 12s ease-in-out infinite; +} + +.pill-2 { + animation: pillOrbit2 14s ease-in-out infinite; +} + +.pill-3 { + animation: pillOrbit3 13s ease-in-out infinite; +} + +.pill-4 { + animation: pillOrbit4 15s ease-in-out infinite; +} + +.pill-5 { + animation: pillOrbit5 11s ease-in-out infinite; +} + +.pill-6 { + animation: pillOrbit6 16s ease-in-out infinite; +} + +.pill-7 { + animation: pillOrbit7 12s ease-in-out infinite; +} + +@keyframes pillOrbit1 { + 0% { transform: translate(0, 0); } + 25% { transform: translate(15px, -10px); } + 50% { transform: translate(25px, 0); } + 75% { transform: translate(15px, 10px); } + 100% { transform: translate(0, 0); } +} + +@keyframes pillOrbit2 { + 0% { transform: translate(0, 0); } + 25% { transform: translate(-12px, -15px); } + 50% { transform: translate(-20px, -5px); } + 75% { transform: translate(-12px, 10px); } + 100% { transform: translate(0, 0); } +} + +@keyframes pillOrbit3 { + 0% { transform: translate(0, 0); } + 25% { transform: translate(10px, 12px); } + 50% { transform: translate(20px, 5px); } + 75% { transform: translate(10px, -8px); } + 100% { transform: translate(0, 0); } +} + +@keyframes pillOrbit4 { + 0% { transform: translate(0, 0); } + 25% { transform: translate(-8px, 18px); } + 50% { transform: translate(-15px, 8px); } + 75% { transform: translate(-5px, -10px); } + 100% { transform: translate(0, 0); } +} + +@keyframes pillOrbit5 { + 0% { transform: translate(0, 0); } + 25% { transform: translate(18px, 5px); } + 50% { transform: translate(10px, -15px); } + 75% { transform: translate(-5px, -10px); } + 100% { transform: translate(0, 0); } +} + +@keyframes pillOrbit6 { + 0% { transform: translate(0, 0); } + 25% { transform: translate(-15px, 8px); } + 50% { transform: translate(-8px, -18px); } + 75% { transform: translate(10px, -12px); } + 100% { transform: translate(0, 0); } +} + +@keyframes pillOrbit7 { + 0% { transform: translate(0, 0); } + 25% { transform: translate(12px, -8px); } + 50% { transform: translate(5px, 15px); } + 75% { transform: translate(-10px, 10px); } + 100% { transform: translate(0, 0); } +} + +/* 响应式 */ +@media (max-width: 1200px) { + .hero-visual { display: none; } +} + +/* Quick Actions */ +.quick-actions { margin-bottom: 50px; } + +.action-card { + display: flex; + align-items: center; + gap: 20px; + padding: 24px; + background: var(--glass-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + cursor: pointer; + transition: all var(--transition-base); +} + +.action-card:hover { + border-color: var(--accent-primary); + transform: translateX(6px); +} + +.action-icon { + width: 52px; + height: 52px; + display: flex; + align-items: center; + justify-content: center; + background: var(--accent-primary-muted); + border-radius: var(--radius-md); + font-size: 24px; + color: var(--accent-primary); +} + +.action-info h3 { font-size: 16px; font-weight: 600; margin-bottom: 4px; } +.action-info p { font-size: 14px; color: var(--text-tertiary); } + +.action-arrow { + margin-left: auto; + font-size: 20px; + color: var(--text-muted); + transition: transform var(--transition-base); +} + +.action-card:hover .action-arrow { + transform: translateX(4px); + color: var(--accent-primary); +} + +/* Projects Section */ +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 28px; +} + +.section-title h2 { font-size: 24px; font-weight: 600; margin-bottom: 4px; } +.section-title p { font-size: 14px; color: var(--text-tertiary); } + +.add-btn { padding: 10px 18px; border-radius: var(--radius-md); } + +.projects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 20px; +} + +.pagination-wrapper { + display: flex; + justify-content: center; + margin-top: 40px; + padding: 20px 0; +} + +/* Minimal Pagination */ +.pagination-minimal { + display: flex; + align-items: center; + gap: 24px; + padding: 12px 24px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 12px; +} + +.page-info { + font-size: 13px; + color: var(--text-muted); + font-weight: 400; + letter-spacing: 0.5px; +} + +.page-arrows { + display: flex; + gap: 8px; +} + +.arrow-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.arrow-btn:hover:not(:disabled) { + background: rgba(0, 212, 255, 0.1); + border-color: rgba(0, 212, 255, 0.3); +} + +.arrow-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.arrow-btn .el-icon { + font-size: 14px; + color: var(--text-secondary); +} + +/* Responsive */ +@media (max-width: 1024px) { + .hero { + grid-template-columns: 1fr; + text-align: center; + } + .hero-subtitle { max-width: 100%; } + .hero-actions { justify-content: center; } +} + +@media (max-width: 640px) { + .home { padding: 40px 20px 60px; } + .hero-title { font-size: 36px; } + .hero-actions { flex-direction: column; } + .projects-grid { grid-template-columns: 1fr; } +} diff --git a/frontend/src/views/CrawlerView.vue b/frontend/src/views/CrawlerView.vue new file mode 100644 index 0000000..3e545b7 --- /dev/null +++ b/frontend/src/views/CrawlerView.vue @@ -0,0 +1,386 @@ + + + + +