From 66d251dcc48dbb4368e4591e36293fdf0e63e9e9 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 17 Mar 2026 17:28:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E6=B7=BB=E5=8A=A0=20TypeScri?= =?UTF-8?q?pt=20=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89=E5=92=8C=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 TypeScript API 客户端 (api/index.ts) - 添加全局样式 (styles/) - 添加类型定义 (types/) - 添加 Vue 组件 (components/) Co-Authored-By: Claude Opus 4.6 --- frontend/src/api/index.ts | 94 ++++ .../components/common/CreateProjectDialog.vue | 423 +++++++++++++++++ .../src/components/common/DeleteDialog.vue | 188 ++++++++ frontend/src/components/common/EmptyState.vue | 96 ++++ .../src/components/common/ProjectCard.vue | 202 +++++++++ frontend/src/styles/home.scss | 426 ++++++++++++++++++ frontend/src/types/api.d.ts | 32 ++ frontend/src/types/common.d.ts | 60 +++ frontend/src/types/index.ts | 7 + frontend/src/types/model.d.ts | 30 ++ frontend/src/types/project.d.ts | 21 + 11 files changed, 1579 insertions(+) create mode 100644 frontend/src/api/index.ts create mode 100644 frontend/src/components/common/CreateProjectDialog.vue create mode 100644 frontend/src/components/common/DeleteDialog.vue create mode 100644 frontend/src/components/common/EmptyState.vue create mode 100644 frontend/src/components/common/ProjectCard.vue create mode 100644 frontend/src/styles/home.scss create mode 100644 frontend/src/types/api.d.ts create mode 100644 frontend/src/types/common.d.ts create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/src/types/model.d.ts create mode 100644 frontend/src/types/project.d.ts diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..75aaafc --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,94 @@ +import axios from 'axios' +import type { AxiosInstance } from 'axios' +import type { Project, ProjectCreate, ProjectUpdate } from '@/types' + +const request: AxiosInstance = axios.create({ + baseURL: import.meta.env.PROD + ? '/api/v1' + : 'http://10.10.10.77:8000/api/v1', + timeout: 60000 +}) + +// Request interceptor +request.interceptors.request.use( + config => { + return config + }, + error => { + return Promise.reject(error) + } +) + +// Response interceptor +request.interceptors.response.use( + response => { + const data = response.data + // Handle new ApiResponse format + if (data.success !== undefined) { + if (data.success) { + return data.data // Return the actual data + } else { + return Promise.reject(new Error(data.message || data.error || '请求失败')) + } + } + return data + }, + error => { + const message = error.response?.data?.message || error.message || '请求失败' + console.error('API Error:', message) + return Promise.reject(error) + } +) + +export const projectApi = { + list: () => request.get('/projects/'), + get: (id: string) => request.get(`/projects/${id}`), + create: (data: ProjectCreate) => request.post<{ id: string }>('/projects/', data), + update: (id: string, data: ProjectUpdate) => request.put(`/projects/${id}`, data), + delete: (id: string) => request.delete(`/projects/${id}`) +} + +export const fileApi = { + upload: (projectId: string, formData: FormData) => + request.post(`/projects/${projectId}/files/upload`, formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }), + list: (projectId: string) => request.get(`/projects/${projectId}/files/`), + get: (projectId: string, fileId: string) => request.get(`/projects/${projectId}/files/${fileId}`), + delete: (projectId: string, fileId: string) => request.delete(`/projects/${projectId}/files/${fileId}`) +} + +export const chunkApi = { + split: (projectId: string, data: any) => request.post(`/projects/${projectId}/chunks/split`, data), + list: (projectId: string, params?: any) => request.get(`/projects/${projectId}/chunks/`, { params }), + get: (projectId: string, chunkId: string) => request.get(`/projects/${projectId}/chunks/${chunkId}`), + update: (projectId: string, chunkId: string, data: any) => request.put(`/projects/${projectId}/chunks/${chunkId}`, data), + delete: (projectId: string, chunkId: string) => request.delete(`/projects/${projectId}/chunks/${chunkId}`) +} + +export const questionApi = { + generate: (projectId: string, data: any) => request.post(`/projects/${projectId}/generate-questions`, data), + list: (projectId: string, params: { chunkId: string }) => request.get(`/projects/${projectId}/chunks/${params.chunkId}/questions`), + update: (projectId: string, questionId: string, data: any) => request.put(`/projects/${projectId}/questions/${questionId}`, data), + delete: (projectId: string, questionId: string) => request.delete(`/projects/${projectId}/questions/${questionId}`) +} + +export const datasetApi = { + list: (projectId: string) => request.get(`/projects/${projectId}/datasets/`), + create: (projectId: string, data: any) => request.post(`/projects/${projectId}/datasets/`, data), + get: (projectId: string, datasetId: string) => request.get(`/projects/${projectId}/datasets/${datasetId}`), + delete: (projectId: string, datasetId: string) => request.delete(`/projects/${projectId}/datasets/${datasetId}`), + export: (projectId: string, datasetId: string, data: any) => + request.post(`/projects/${projectId}/datasets/${datasetId}/export`, data, { + responseType: 'blob' + }) +} + +export const evalApi = { + list: (projectId: string) => request.get(`/projects/${projectId}/eval-datasets/`), + create: (projectId: string, data: any) => request.post(`/projects/${projectId}/eval-datasets/`, data), + run: (projectId: string, evalId: string) => request.post(`/projects/${projectId}/eval-datasets/${evalId}/evaluate`), + getResults: (projectId: string, taskId: string) => request.get(`/projects/${projectId}/eval-tasks/${taskId}`) +} + +export default request diff --git a/frontend/src/components/common/CreateProjectDialog.vue b/frontend/src/components/common/CreateProjectDialog.vue new file mode 100644 index 0000000..edd83dc --- /dev/null +++ b/frontend/src/components/common/CreateProjectDialog.vue @@ -0,0 +1,423 @@ + + + + + diff --git a/frontend/src/components/common/DeleteDialog.vue b/frontend/src/components/common/DeleteDialog.vue new file mode 100644 index 0000000..2dbdb8c --- /dev/null +++ b/frontend/src/components/common/DeleteDialog.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/frontend/src/components/common/EmptyState.vue b/frontend/src/components/common/EmptyState.vue new file mode 100644 index 0000000..1dde958 --- /dev/null +++ b/frontend/src/components/common/EmptyState.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/frontend/src/components/common/ProjectCard.vue b/frontend/src/components/common/ProjectCard.vue new file mode 100644 index 0000000..690d475 --- /dev/null +++ b/frontend/src/components/common/ProjectCard.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/frontend/src/styles/home.scss b/frontend/src/styles/home.scss new file mode 100644 index 0000000..8650969 --- /dev/null +++ b/frontend/src/styles/home.scss @@ -0,0 +1,426 @@ +/* ======================== + 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; +} + +.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 - 全息粒子矩阵 + ======================== */ +.hero-visual { + position: relative; + height: 420px; + perspective: 1000px; +} + +/* 全息卡片基础样式 */ +.hologram-card { + position: absolute; + width: 180px; + padding: 24px 20px; + background: linear-gradient(135deg, rgba(20, 20, 30, 0.9) 0%, rgba(10, 10, 18, 0.95) 100%); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + backdrop-filter: blur(20px); + cursor: pointer; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + transform-style: preserve-3d; + animation: hologramFloat 6s ease-in-out infinite; + overflow: hidden; +} + +/* 卡片位置 */ +.hologram-card.card-1 { + top: 10px; + right: 60px; + animation-delay: 0s; +} + +.hologram-card.card-2 { + top: 130px; + left: 30px; + animation-delay: -2s; +} + +.hologram-card.card-3 { + bottom: 20px; + right: 80px; + animation-delay: -4s; +} + +@keyframes hologramFloat { + 0%, 100% { transform: translateY(0) rotateX(0) rotateY(0); } + 25% { transform: translateY(-8px) rotateX(2deg) rotateY(-2deg); } + 50% { transform: translateY(0) rotateX(0) rotateY(0); } + 75% { transform: translateY(-5px) rotateX(-1deg) rotateY(2deg); } +} + +/* 悬浮时的3D效果 */ +.hologram-card:hover { + transform: translateY(-15px) scale(1.05); + border-color: rgba(0, 212, 255, 0.4); + box-shadow: + 0 25px 50px -12px rgba(0, 0, 0, 0.5), + 0 0 30px rgba(0, 212, 255, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.hologram-card:hover .scan-line { + animation: scanMove 1.5s linear infinite; +} + +.hologram-card:hover .pulse-ring { + animation: pulseRing 2s ease-out infinite; +} + +.hologram-card:hover .particle { + animation: particleBurst 1s ease-out forwards; + animation-delay: calc(var(--i, 0) * 0.1s); +} + +/* 卡片背景 */ +.card-bg { + position: absolute; + inset: 0; + background: radial-gradient(ellipse at top, rgba(0, 212, 255, 0.08) 0%, transparent 50%), + radial-gradient(ellipse at bottom right, rgba(124, 58, 237, 0.08) 0%, transparent 50%); + opacity: 0.8; +} + +.card-2 .card-bg { + background: radial-gradient(ellipse at top, rgba(124, 58, 237, 0.12) 0%, transparent 50%), + radial-gradient(ellipse at bottom right, rgba(0, 212, 255, 0.06) 0%, transparent 50%); +} + +.card-3 .card-bg { + background: radial-gradient(ellipse at top, rgba(6, 182, 212, 0.12) 0%, transparent 50%), + radial-gradient(ellipse at bottom right, rgba(124, 58, 237, 0.06) 0%, transparent 50%); +} + +/* 扫描线效果 */ +.scan-line { + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient( + transparent 0%, + rgba(0, 212, 255, 0.03) 50%, + transparent 100% + ); + transform: rotate(30deg); + pointer-events: none; +} + +@keyframes scanMove { + 0% { transform: translateY(-100%) rotate(30deg); } + 100% { transform: translateY(100%) rotate(30deg); } +} + +/* 粒子容器 */ +.particles-container { + position: absolute; + inset: 0; + pointer-events: none; + overflow: hidden; +} + +.particle { + position: absolute; + width: 3px; + height: 3px; + border-radius: 50%; + opacity: 0; + left: var(--x); + top: var(--y); +} + +.card-1 .particle { + background: var(--accent-primary); + box-shadow: 0 0 6px var(--accent-primary); +} + +.card-2 .particle { + background: var(--accent-secondary); + box-shadow: 0 0 6px var(--accent-secondary); +} + +.card-3 .particle { + background: var(--accent-tertiary); + box-shadow: 0 0 6px var(--accent-tertiary); +} + +@keyframes particleBurst { + 0% { opacity: 0; transform: scale(0); } + 20% { opacity: 1; transform: scale(1); } + 100% { opacity: 0; transform: scale(2); } +} + +/* 脉动光环 */ +.pulse-ring { + position: absolute; + top: 50%; + left: 50%; + width: 60px; + height: 60px; + transform: translate(-50%, -50%); + border-radius: 50%; + border: 1px solid rgba(0, 212, 255, 0.3); + opacity: 0; + pointer-events: none; +} + +.card-2 .pulse-ring { border-color: rgba(124, 58, 237, 0.3); } +.card-3 .pulse-ring { border-color: rgba(6, 182, 212, 0.3); } + +@keyframes pulseRing { + 0% { opacity: 0.6; transform: translate(-50%, -50%) scale(0.5); } + 100% { opacity: 0; transform: translate(-50%, -50%) scale(2); } +} + +/* 卡片内容 */ +.card-content { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 12px; +} + +/* 图标包装器 */ +.icon-wrapper { + position: relative; + width: 64px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 18px; + background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.02) 100%); + border: 1px solid rgba(255,255,255,0.1); + transition: all 0.3s ease; +} + +.icon-wrapper.cyan { + background: linear-gradient(135deg, rgba(0, 212, 255, 0.2) 0%, rgba(0, 212, 255, 0.05) 100%); + border-color: rgba(0, 212, 255, 0.3); + color: var(--accent-primary); +} + +.icon-wrapper.violet { + background: linear-gradient(135deg, rgba(124, 58, 237, 0.2) 0%, rgba(124, 58, 237, 0.05) 100%); + border-color: rgba(124, 58, 237, 0.3); + color: var(--accent-secondary); +} + +.icon-wrapper.teal { + background: linear-gradient(135deg, rgba(6, 182, 212, 0.2) 0%, rgba(6, 182, 212, 0.05) 100%); + border-color: rgba(6, 182, 212, 0.3); + color: var(--accent-tertiary); +} + +/* 图标发光 */ +.icon-glow { + position: absolute; + inset: -2px; + border-radius: 20px; + background: inherit; + filter: blur(15px); + opacity: 0.5; + z-index: -1; +} + +.hologram-card:hover .icon-wrapper { + transform: scale(1.1); + box-shadow: 0 0 30px rgba(0, 212, 255, 0.4); +} + +.hologram-card:hover .icon-glow { opacity: 0.8; } + +/* 标签文字 */ +.card-label { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + letter-spacing: 0.02em; +} + +.card-sublabel { + font-size: 11px; + color: var(--text-muted); + letter-spacing: 0.05em; + text-transform: uppercase; +} + +/* 响应式 */ +@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; +} + +/* 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/types/api.d.ts b/frontend/src/types/api.d.ts new file mode 100644 index 0000000..6c21444 --- /dev/null +++ b/frontend/src/types/api.d.ts @@ -0,0 +1,32 @@ +/** + * API Response Types + */ + +// Base API response wrapper +export interface ApiResponse { + success: boolean + message: string + data: T + error: string | null + timestamp: string +} + +// Paginated response +export interface PaginatedResponse extends ApiResponse { + page?: number + page_size?: number + total?: number +} + +// List items wrapper +export interface ListResponse { + items: T[] + total: number + page: number + page_size: number +} + +// Simple ID response +export interface IdResponse { + id: string +} diff --git a/frontend/src/types/common.d.ts b/frontend/src/types/common.d.ts new file mode 100644 index 0000000..b466704 --- /dev/null +++ b/frontend/src/types/common.d.ts @@ -0,0 +1,60 @@ +/** + * Common Types + */ + +// File types +export interface FileItem { + id: string + filename: string + file_type: string + size?: number + status: string + created_at: string + updated_at: string +} + +// Chunk types +export interface Chunk { + id: string + name?: string + content: string + summary?: string + word_count?: number + file_id?: string + created_at: string + updated_at: string +} + +// Question types +export interface Question { + id: string + content: string + answer?: string + question_type?: string + chunk_id?: string + source: string + created_at: string + updated_at: string +} + +// Dataset types +export interface Dataset { + id: string + name: string + description?: string + dataset_type?: string + question_count?: number + created_at: string + updated_at: string +} + +// Dialog props +export interface DialogProps { + visible: boolean + loading?: boolean +} + +export interface DeleteDialogProps extends DialogProps { + itemName?: string + itemType?: string +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..a75f751 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,7 @@ +/** + * Type exports + */ +export * from './api' +export * from './project' +export * from './model' +export * from './common' diff --git a/frontend/src/types/model.d.ts b/frontend/src/types/model.d.ts new file mode 100644 index 0000000..d53fd55 --- /dev/null +++ b/frontend/src/types/model.d.ts @@ -0,0 +1,30 @@ +/** + * Model Configuration Types + */ + +export interface ModelConfig { + id: string + provider: ModelProvider + model_name: string + api_key?: string + api_base?: string + is_default: 'true' | 'false' + created_at?: string + updated_at?: string +} + +export type ModelProvider = 'openai' | 'anthropic' | 'google' | 'other' + +export interface ModelCreate { + provider: ModelProvider + model_name: string + api_key: string + api_base?: string + is_default: boolean +} + +export interface ProviderOption { + value: ModelProvider + label: string + abbr: string +} diff --git a/frontend/src/types/project.d.ts b/frontend/src/types/project.d.ts new file mode 100644 index 0000000..0daf62f --- /dev/null +++ b/frontend/src/types/project.d.ts @@ -0,0 +1,21 @@ +/** + * Project Types + */ + +export interface Project { + id: string + name: string + description?: string + created_at: string + updated_at: string +} + +export interface ProjectCreate { + name: string + description?: string +} + +export interface ProjectUpdate { + name?: string + description?: string +}