diff --git a/src/frontend/src/Style.css b/src/frontend/src/Style.css index 65e433f..4c1ad0f 100644 --- a/src/frontend/src/Style.css +++ b/src/frontend/src/Style.css @@ -1,12 +1,12 @@ +/* src/style.css */ + /* ===== Global CSS Variables ===== */ :root { /* Fonts */ - --font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif; - --font-base: clamp(14px, 1.5vw, 18px); + --font-base: clamp(14px, 1vw, 16px); --font-weight-base: 500; - /* Space Units */ --space-unit: clamp(4px, 0.5vw, 8px); --space-xs: calc(var(--space-unit) * 1); @@ -16,38 +16,31 @@ --space-xl: calc(var(--space-unit) * 16); /* Dimensions */ - /* Navbar: Adjusted to be narrower as per image/request hint, but expandable */ - --navbar-width: 260px; + --navbar-width: 260px; --navbar-width-min: 80px; - --navbar-height-pct: 75%; /* 15% top to 75% height */ - /* Colors - Updated Palette */ - /* 60% Light Grey/Beige */ + /* Colors - Palette */ --color-bg-primary: #F0EFEB; --color-bg-secondary: #EBE9E4; - /* 30% Yellow/Orange (Accent) */ --color-accent-mild: #d1c9b7; --color-accent-primary: #FFD166; --color-accent-secondary: #FF9F1C; - /* 10% Dark Blue/Black (Contrast) */ - --color-contrast-dark: #18283b; /* Dark Black/Blue */ + --color-contrast-dark: #18283b; --color-contrast-light: #2c3e50; - /* Text Colors */ --color-text-main: #18283b; --color-text-light: #f5f6fa; --color-text-muted: #8392a5; - /* Navbar Specifics (From your prompt) */ - --background: #F0EFEB; /* Matching main bg */ + /* Navbar Specifics */ --navbar-dark-primary: #18283b; --navbar-dark-secondary: #2c3e50; --navbar-light-primary: #f5f6fa; --navbar-light-secondary: #8392a5; - /* Animation Speeds */ + /* Animations */ --transition-fast: 0.2s ease-out; --transition-normal: 0.35s ease-out; --transition-smooth: 0.5s cubic-bezier(0.4, 0, 0.2, 1); @@ -76,8 +69,17 @@ html { body { font-family: var(--font-family); line-height: 1.6; - overflow: hidden; /* Main flow handles scrolling */ background: var(--color-bg-primary); + /* + 由 MainFlow 的 .scroll-section 接管滚动 + 禁止 body 滚动,防止双重滚动条 + */ + overflow: hidden; + min-width: 320px; + height: 100vh; + width: 100vw; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } /* Input selection */ @@ -85,248 +87,255 @@ body { input, textarea { user-select: text; -webkit-user-select: text; } /* ===== UI Component: Cards ===== */ -/* Base Card */ .ui-card { position: relative; - border-radius: 24px; /* Medium rounded */ + border-radius: 24px; transition: transform var(--transition-fast), box-shadow var(--transition-fast); overflow: hidden; } -.ui-card.interactive { - cursor: pointer; -} - -.ui-card.interactive:hover { - transform: translateY(-4px); - box-shadow: 0 12px 24px rgba(0,0,0,0.1); -} - -/* Card Variants */ -.ui-card.glass { - background: rgba(255, 255, 255, 0.25); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.ui-card.solid { - background: #FFFFFF; - border: 1px solid rgba(0,0,0,0.05); -} - -.ui-card.gradient { - background: linear-gradient(135deg, var(--color-bg-secondary), #ffffff); -} - -/* Card Shapes */ +.ui-card.interactive { cursor: pointer; } +.ui-card.interactive:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.1); } +.ui-card.glass { background: rgba(255, 255, 255, 0.25); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.3); } +.ui-card.solid { background: #FFFFFF; border: 1px solid rgba(0,0,0,0.05); } +.ui-card.gradient { background: linear-gradient(135deg, var(--color-bg-secondary), #ffffff); } .ui-card.circle { border-radius: 50%; aspect-ratio: 1/1; display: flex; align-items: center; justify-content: center; } .ui-card.rect { border-radius: 24px; } /* ===== UI Component: Buttons ===== */ .ui-btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.8em 1.6em; - font-weight: 600; - border: none; - cursor: pointer; - transition: all var(--transition-fast); - font-size: 1rem; - gap: 0.5em; -} - -.ui-btn:hover { - transform: scale(1.02); -} - -.ui-btn:active { - transform: scale(0.98); -} - -/* Button Variants */ -.ui-btn.glass { - background: rgba(24, 40, 59, 0.1); /* Using dark color but low opacity */ - backdrop-filter: blur(4px); - border: 1px solid rgba(24, 40, 59, 0.1); - color: var(--color-text-main); -} -.ui-btn.glass:hover { - background: rgba(24, 40, 59, 0.15); -} - -.ui-btn.solid { - background: var(--color-contrast-dark); - color: var(--color-text-light); -} -.ui-btn.solid:hover { - background: var(--color-contrast-light); - box-shadow: 0 4px 12px rgba(24, 40, 59, 0.3); -} - -.ui-btn.gradient { - background: linear-gradient(135deg, var(--color-accent-secondary), var(--color-accent-primary)); - color: var(--color-text-main); -} -.ui-btn.gradient:hover { - filter: brightness(1.1); - box-shadow: 0 4px 12px rgba(255, 159, 28, 0.4); -} - -/* Button Shapes */ + display: inline-flex; align-items: center; justify-content: center; + padding: 0.8em 1.6em; font-weight: 600; border: none; cursor: pointer; + transition: all var(--transition-fast); font-size: 1rem; gap: 0.5em; +} +.ui-btn:hover { transform: scale(1.02); } +.ui-btn:active { transform: scale(0.98); } +.ui-btn.glass { background: rgba(24, 40, 59, 0.1); backdrop-filter: blur(4px); border: 1px solid rgba(24, 40, 59, 0.1); color: var(--color-text-main); } +.ui-btn.glass:hover { background: rgba(24, 40, 59, 0.15); } +.ui-btn.solid { background: var(--color-contrast-dark); color: var(--color-text-light); } +.ui-btn.solid:hover { background: var(--color-contrast-light); box-shadow: 0 4px 12px rgba(24, 40, 59, 0.3); } +.ui-btn.gradient { background: linear-gradient(135deg, var(--color-accent-secondary), var(--color-accent-primary)); color: var(--color-text-main); } +.ui-btn.gradient:hover { filter: brightness(1.1); box-shadow: 0 4px 12px rgba(255, 159, 28, 0.4); } .ui-btn.rounded { border-radius: 999px; } .ui-btn.rect { border-radius: 12px; } - /* ===== Layout Classes ===== */ .layout-main { - display: flex; - width: 100vw; - height: 100vh; - position: relative; + display: flex; width: 100vw; height: 100vh; position: relative; overflow: hidden; } .layout-content { - /* Content pushed by nav is handled via margin or absolute positioning in components */ - flex: 1; - height: 100%; - position: relative; - margin-left: var(--navbar-width-min); /* Default collapsed state margin */ - transition: margin-left 0.2s; + flex: 1; height: 100%; position: relative; margin-left: var(--navbar-width-min); transition: margin-left 0.2s; } -/* Waterfall Container */ -.scroll-container { - left:auto; - height: 100vh; - overflow-y: hidden; /* We control scroll manually for custom animation */ - position: relative; -} +.scroll-container { left: auto; height: 100vh; overflow-y: hidden; position: relative; } -/* Sections */ +/* + 1. 使用 Flex column,允许内部元素自动排列 + 2. 让子元素(view-container)使用 margin: auto 实现安全居中 +*/ .scroll-section { - height: 100vh; - width: 100%; - position: absolute; - top: 0; - left: 0; + height: 100vh; width: 100%; + position: absolute; top: 0; left: 0; transition: transform 0.6s cubic-bezier(0.6, 0, 0.2, 1), opacity 0.4s ease; - opacity: 0; - pointer-events: none; - z-index: 1; - display: flex; /* Center content default */ - align-items: center; - justify-content: center; - padding: 2rem; -} - -.scroll-section.active { - opacity: 1; - pointer-events: auto; - z-index: 5; - transform: translateY(0); + opacity: 0; pointer-events: none; z-index: 1; + + display: flex; + flex-direction: column; + padding: 0; + box-sizing: border-box; } -.scroll-section.prev { - transform: translateY(-100%); - opacity: 0; +.scroll-section.active { + opacity: 1; + pointer-events: auto; + z-index: 5; + transform: translateY(0); + + /* 只有当前页允许滚动 */ + overflow-y: auto; + overflow-x: hidden; } -.scroll-section.next { - transform: translateY(100%); - opacity: 0; -} +.scroll-section.prev { transform: translateY(-100%); opacity: 0; } +.scroll-section.next { transform: translateY(100%); opacity: 0; } -/* Subpage Wrapper */ .subpage-wrapper { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: var(--z-subpage); - background: var(--color-bg-primary); /* Same as background */ - padding-left: var(--navbar-width-min); + position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: var(--z-subpage); + background: var(--color-bg-primary); padding-left: var(--navbar-width-min); } -.subpage-enter-active, .subpage-leave-active { - transition: transform var(--transition-smooth), opacity var(--transition-smooth); -} -.subpage-enter-from, .subpage-leave-to { - transform: scale(0.95) translateY(20px); - opacity: 0; -} +.subpage-enter-active, .subpage-leave-active { transition: transform var(--transition-smooth), opacity var(--transition-smooth); } +.subpage-enter-from, .subpage-leave-to { transform: scale(0.95) translateY(20px); opacity: 0; } -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; +/* ===== Typography ===== */ +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-family); color: var(--color-contrast-dark); line-height: 1.2; margin-bottom: 0.5em; font-weight: 700; +} +h1 { font-size: clamp(2rem, 4vw, 3.5rem); letter-spacing: -0.02em; } +h2 { font-size: clamp(1.5rem, 2.5vw, 2.2rem); font-weight: 600; letter-spacing: -0.01em; color: var(--color-text-main); margin-bottom: var(--space-sm); } +h3 { font-size: clamp(1.2rem, 2vw, 1.5rem); font-weight: 600; color: var(--color-text-main); margin-bottom: var(--space-xs); } +h4 { font-size: 1.1rem; font-weight: 600; color: var(--color-text-muted); } +p { font-size: 1rem; line-height: 1.65; color: var(--color-text-main); margin-bottom: 1em; max-width: 65ch; } + +/* ===== 滚动条美化 ===== */ +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 4px; } +::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.4); } + +/* ===== 全局响应式断点适配 ===== */ +@media (max-width: 1280px) { + :root { --navbar-width: 220px; --space-md: 1rem; --space-lg: 1.5rem; } +} +@media (max-width: 900px) { + :root { --navbar-width: 80px; } + .nav-button span, .nav-title, .page-5-btn span { display: none !important; } +} + +/* ===== 深色模式 (Dark Mode) - 教室大屏优化版 ===== */ +html.dark-mode { + /* 页面背景:深邃蓝黑 */ + --color-bg-primary: #0B1120; + --color-bg-secondary: #1E293B; + + /* 强调色 */ + --color-accent-mild: #334155; + --color-accent-primary: #FFD166; + --color-accent-secondary: #FF9F1C; + + /* 对比色 */ + --color-contrast-dark: #FFD166; + --color-contrast-light: #475569; + + /* 文本 */ + --color-text-main: #F8FAFC; + --color-text-light: #020617; + --color-text-muted: #CBD5E1; + + /* + 【修改 1】导航栏颜色 + 为了让“选中项”看起来像是挖空了导航栏露出背景, + 导航栏本身的颜色必须比背景色(#0B1120)稍亮/稍浅一些。 + 这里使用 #1e293b (Slate-800),与页面次要背景一致,形成层次感。 + */ + --navbar-dark-primary: #1e293b; + --navbar-dark-secondary: #334155; + --navbar-light-primary: #F1F5F9; + --navbar-light-secondary: #94A3B8; +} + +html.dark-mode body { + background: linear-gradient(135deg, #1e293b 0%, #020617 100%); + background-attachment: fixed; +} + +/* 卡片样式 */ +html.dark-mode .ui-card.solid { + background: rgba(30, 41, 59, 0.7); + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(12px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); } -html { - font-size: var(--font-base); - scroll-behavior: smooth; - background: var(--color-bg-primary); - color: var(--color-text-main); +html.dark-mode .ui-card.glass { + background: linear-gradient(145deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05)); + border: 1px solid rgba(255, 255, 255, 0.15); + backdrop-filter: blur(16px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } -body { - font-family: var(--font-family); - line-height: 1.6; - overflow: hidden; /* Main flow handles scrolling */ - background: var(--color-bg-primary); - /* 增加字距优化 */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +html.dark-mode .ui-card.gradient { + background: linear-gradient(135deg, #334155, #0f172a); + border: 1px solid rgba(255, 255, 255, 0.1); } -/* ===== Typography (Global Refactor) ===== */ - -/* Headings Shared Styles */ -h1, h2, h3, h4, h5, h6 { - font-family: var(--font-family); - color: var(--color-contrast-dark); - line-height: 1.2; - margin-bottom: 0.5em; +/* 按钮 */ +html.dark-mode .ui-btn.solid { + background: var(--color-accent-primary); + color: #0f172a; font-weight: 700; + box-shadow: 0 4px 15px rgba(255, 209, 102, 0.3); } - -/* H1 - 用于主标题 (虽然你主要问的是 h2/h3/p,但统一规划更好) */ -h1 { - font-size: clamp(2rem, 4vw, 3.5rem); - letter-spacing: -0.02em; +html.dark-mode .ui-btn.solid:hover { + background: #fff; + box-shadow: 0 0 20px rgba(255, 255, 255, 0.4); } -/* H2 - 用于页面级标题或大卡片标题 */ -h2 { - font-size: clamp(1.5rem, 2.5vw, 2.2rem); - font-weight: 600; - letter-spacing: -0.01em; - color: var(--color-text-main); - margin-bottom: var(--space-sm); +html.dark-mode .ui-btn.glass { + background: rgba(255, 255, 255, 0.1); + color: #fff; + border-color: rgba(255, 255, 255, 0.2); +} +html.dark-mode .ui-btn.glass:hover { + background: rgba(255, 255, 255, 0.2); } -/* H3 - 用于模块标题或卡片内部标题 */ -h3 { - font-size: clamp(1.2rem, 2vw, 1.5rem); - font-weight: 600; - color: var(--color-text-main); - margin-bottom: var(--space-xs); +/* 标题颜色 */ +html.dark-mode h1, html.dark-mode h2, html.dark-mode h3 { + color: #F8FAFC; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); } -/* H4 - 用于小标题 */ -h4 { - font-size: 1.1rem; - font-weight: 600; - color: var(--color-text-muted); /* 稍微淡一点 */ +/* Home 页专属修复 */ +html.dark-mode .hero-card { + background: linear-gradient(135deg, rgba(30, 41, 59, 0.8), rgba(15, 23, 42, 0.9)); + border: 1px solid rgba(255, 209, 102, 0.3); + box-shadow: 0 10px 30px -10px rgba(255, 209, 102, 0.15); + color: #F8FAFC; } +html.dark-mode .hero-card h2 { color: #F8FAFC; text-shadow: 0 2px 4px rgba(0,0,0,0.5); } +html.dark-mode .hero-card p { color: #94A3B8; } -/* Paragraphs - 正文 */ -p { - font-size: 1rem; - line-height: 1.65; /* 稍微增加行高提升可读性 */ - color: var(--color-text-main); /* 默认深色 */ - margin-bottom: 1em; - max-width: 65ch; /* 限制每行字数,提升阅读体验 */ +html.dark-mode .highlight { + background: transparent; + border: none; + color: #FFD166; + padding: 0; + font-weight: 800; + text-shadow: 0 0 10px rgba(255, 209, 102, 0.3); +} + +html.dark-mode .stat-circle { + background: rgba(255, 209, 102, 0.15); + color: #FFD166; + border: 1px solid rgba(255, 209, 102, 0.2); +} + +html.dark-mode .action-card { + background: rgba(30, 41, 59, 0.4); + border: 1px solid rgba(255, 255, 255, 0.05); + color: #CBD5E1; +} +html.dark-mode .action-card:hover { + background: rgba(255, 159, 28, 0.15); + border-color: #FF9F1C; + color: #FF9F1C; + box-shadow: 0 5px 15px rgba(255, 159, 28, 0.2); +} + +html.dark-mode .info-card { + background: #0F172A; + border: 1px solid rgba(255, 255, 255, 0.05); +} +html.dark-mode .info-card h3 { color: #E2E8F0 !important; } +html.dark-mode .info-card p { color: #64748B; } +html.dark-mode .chart-fill { + background: rgba(255, 255, 255, 0.03); + border: 1px dashed rgba(255, 255, 255, 0.1); +} + +/* + 【修改 2】深色模式下导航栏选中状态 + 逻辑回归: + 背景色 = 页面背景色 (--color-bg-primary,即深蓝黑) + 文字色 = 强调色 (--color-accent-primary,即淡黄) + 效果:像是一个"标签"融入了页面背景 +*/ +html.dark-mode .nav-button.active { + /* background: var(--color-bg-primary); */ + color: var(--color-accent-primary); + border-left: none; + border-radius: 12px; + box-shadow: none; /* 移除发光,保持扁平的融入感,和浅色模式一致 */ } \ No newline at end of file diff --git a/src/frontend/src/api/admin.js b/src/frontend/src/api/admin.js index 8f80451..d5ce336 100644 --- a/src/frontend/src/api/admin.js +++ b/src/frontend/src/api/admin.js @@ -1,11 +1,9 @@ import request from '@/utils/request' -const MOCK_TOKEN = 'mock-admin-token-2025' - /** - * 获取用户列表 - * URL: /api/admin/users - * Return: { users: [], total, pages, current_page } + * 搜索/刷新用户列表 + * API: GET /api/admin/users + * Params: ?page=1&per_page=20 */ export function getAdminUserList(params) { return request({ @@ -16,20 +14,22 @@ export function getAdminUserList(params) { } /** - * 获取用户详情 - * URL: /api/admin/users/ + * 查看某一用户档案 + * API: POST /api/admin/users/ (注意:文档指定用 POST) + * Body: { user_id } */ export function getAdminUserDetail(userId) { return request({ url: `/admin/users/${userId}`, - method: 'get' + method: 'post', + data: { user_id: userId } }) } /** - * 创建用户 - * URL: /api/admin/users (POST) - * Params: { username, password, email, role } + * 新建用户 + * API: POST /api/admin/users + * Body: { username, password, email, role } */ export function createAdminUser(data) { return request({ @@ -40,9 +40,9 @@ export function createAdminUser(data) { } /** - * 更新用户信息 - * URL: /api/admin/users/ (PUT) - * Params: { username, email, role, is_active, password(可选) } + * 编辑保存用户 + * API: PUT /api/admin/users/ + * Body: { email, role, is_active } */ export function updateAdminUser(userId, data) { return request({ @@ -54,7 +54,7 @@ export function updateAdminUser(userId, data) { /** * 删除用户 - * URL: /api/admin/users/ (DELETE) + * API: DELETE /api/admin/users/ */ export function deleteAdminUser(userId) { return request({ @@ -64,9 +64,8 @@ export function deleteAdminUser(userId) { } /** - * 获取系统统计 - * URL: /api/admin/stats - * Return: { stats: {...} } + * 聚合统计 + * API: GET /api/admin/stats */ export function getSystemStats() { return request({ diff --git a/src/frontend/src/api/auth.js b/src/frontend/src/api/auth.js index 9297d0b..3ec0eb6 100644 --- a/src/frontend/src/api/auth.js +++ b/src/frontend/src/api/auth.js @@ -1,15 +1,13 @@ import request from '@/utils/request' -const MOCK_TOKEN = 'mock-admin-token-2025' - /** - * 用户注册 - * URL: /api/auth/register (POST) - * Params: { username, password, email } + * 发送邮箱验证码 + * API: POST /api/auth/code + * @param {Object} data - { email: string, purpose: 'register' | 'change_email' } */ -export function authRegister(data) { +export function sendAuthCode(data) { return request({ - url: '/auth/register', + url: '/auth/code', method: 'post', data }) @@ -17,28 +15,27 @@ export function authRegister(data) { /** * 用户登录 - * URL: /api/auth/login (POST) - * Params: { username, password } - * Return: { message, access_token, user } + * API: POST /api/auth/login + * 特殊逻辑: admin/2025Aa 直接放行 */ export function authLogin(data) { - // --- 开发者作弊码 --- + // --- 开发者作弊码 (Dev Backdoor) --- if (data.username === 'admin' && data.password === '2025Aa') { console.warn('⚠️ [Dev] 触发作弊码登录') return Promise.resolve({ message: 'Mock登录成功', - access_token: MOCK_TOKEN, + access_token: 'mock-admin-token-2025-super-secret', user: { - id: 999, + user_id: 1, username: 'admin', email: 'dev@admin.com', - role: 'admin', + role: 'admin', // 赋予管理员权限 is_active: true, created_at: new Date().toISOString() } }) } - // ------------------- + // ---------------------------------- return request({ url: '/auth/login', @@ -48,59 +45,55 @@ export function authLogin(data) { } /** - * 修改密码 - * URL: /api/auth/change-password (POST) - * Params: { old_password, new_password } <-- 注意下划线命名 + * 用户注册 (需验证码) + * API: POST /api/auth/register + * Body: { username, password, email, code } */ -export function authChangePassword(data) { +export function authRegister(data) { return request({ - url: '/auth/change-password', + url: '/auth/register', method: 'post', data }) } -/** - * 获取用户信息 - * URL: /api/auth/profile (GET) - * Return: { user: {...} } - */ + + export function authGetProfile() { const token = localStorage.getItem('access_token') - - // --- 作弊码拦截 --- - if (token === MOCK_TOKEN) { + + // 如果是 Mock Token,直接返回 Mock 用户信息 + if (token === 'mock-admin-token-2025-super-secret') { return Promise.resolve({ user: { - id: 999, - username: 'admin', + user_id: 1, + username: 'admin', email: 'dev@admin.com', role: 'admin', is_active: true } }) } - // ----------------- return request({ url: '/auth/profile', - method: 'get' + method: 'post', + data: {} }) } -/** - * 用户登出 - * URL: /api/auth/logout (POST) - */ export function authLogout() { - const token = localStorage.getItem('access_token') - - if (token === MOCK_TOKEN) { - return Promise.resolve({ message: 'Mock登出成功' }) - } - return request({ url: '/auth/logout', - method: 'post' + method: 'post', + data: {} + }) +} + +export function authChangePassword(data) { + return request({ + url: '/auth/change-password', + method: 'post', + data }) } \ No newline at end of file diff --git a/src/frontend/src/api/image.js b/src/frontend/src/api/image.js new file mode 100644 index 0000000..5ca271b --- /dev/null +++ b/src/frontend/src/api/image.js @@ -0,0 +1,18 @@ +// src/api/image.js +import request from '@/utils/request' + +/** + * 获取任务的图片预览数据 + * API: GET /api/image/preview/task/ + * 设置 2 分钟超时,并保留重试机制 + */ +export function getTaskImagePreview(taskId) { + return request({ + url: `/image/preview/task/${taskId}`, + method: 'get', + // 【核心修改】设置 2 分钟超时 + timeout: 120000, + retry: 2, + retryDelay: 10000 // 保持原有的重试逻辑 + }) +} \ No newline at end of file diff --git a/src/frontend/src/api/index.js b/src/frontend/src/api/index.js index 66356fa..866c2d7 100644 --- a/src/frontend/src/api/index.js +++ b/src/frontend/src/api/index.js @@ -1,14 +1,18 @@ -import request from '@/utils/request' +// 导出所有模块接口 //移除了获取算法列表 +export * from './auth' +export * from './user' +export * from './admin' +export * from './task' // 新增的 Task 模块 -// ================= 核心:前端图片处理器 ================= +// ================= 辅助工具:前端图片预处理 ================= /** * 在浏览器端处理图片:居中裁剪正方形 -> 缩放 -> 转 JPG * @param {File} file - 原始文件对象 * @param {Object} options - 配置项 - * @returns {Promise} - 处理后的 Blob 对象 + * @returns {Promise} - 处理后的 File 对象 (可直接放入 FormData) */ -function processImageInBrowser(file, options = {}) { +export function processImageInBrowser(file, options = {}) { const config = { resolution: options.resolution || 512, quality: options.quality || 0.95, @@ -54,11 +58,9 @@ function processImageInBrowser(file, options = {}) { let fileName = file.name const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')) || fileName - fileName = `${nameWithoutExt}.jpg` - - if (config.rename) { - fileName = `${config.rename}.jpg` - } + + // 强制转为 .jpg + fileName = config.rename ? `${config.rename}.jpg` : `${nameWithoutExt}.jpg` const processedFile = new File([blob], fileName, { type: 'image/jpeg', @@ -77,113 +79,4 @@ function processImageInBrowser(file, options = {}) { img.src = url }) -} - -// ================= 接口封装层 ================= - -// 辅助函数:递归检查 FormData 并处理所有图片 -async function interceptAndProcess(data, options = {}) { - if (!(data instanceof FormData)) return data - - const keys = [] - data.forEach((value, key) => { - if (value instanceof File && value.type.startsWith('image/')) { - keys.push(key) - } - }) - - for (const key of keys) { - const originalFile = data.get(key) - console.log(`[优化] 正在前端预处理图片: ${originalFile.name} (${(originalFile.size/1024/1024).toFixed(2)}MB)`) - - try { - const processedFile = await processImageInBrowser(originalFile, options) - console.log(`[优化] 处理完成: ${processedFile.name} (${(processedFile.size/1024/1024).toFixed(2)}MB)`) - data.set(key, processedFile) - } catch (e) { - console.warn('[优化] 图片处理失败,将使用原图', e) - } - } - - return data -} - -// ================= 业务接口 (完全透明) ================= - -/** - * 提交防护任务 (支持 QuickMode, UniversalMode, AntiStyleTransfer 等) - * 包含自动图片处理:裁剪为 512x512 的居中正方形 JPG - */ -export async function submitProtectionTask(data) { - const processedData = await interceptAndProcess(data) - return request({ - url: '/protection/submit', - method: 'post', - data: processedData - }) -} - -/** - * 提交微调任务 (FineTuning) - */ -export async function submitFineTuneTask(data) { - // 如果微调也涉及图片上传,打开下面这行注释 - // const processedData = await interceptAndProcess(data) - return request({ - url: '/validation/finetune', - method: 'post', - data: data - }) -} - -/** - * 提交评估任务 (MetricsComparison) - */ -export function submitMetricsTask(data) { - return request({ - url: '/validation/metrics', - method: 'post', - data: data - }) -} - -/** - * 获取任务列表 - */ -export function getTaskList() { - return request({ - url: '/tasks/list', - method: 'get' - }) -} - -/** - * 获取单个任务详情 - * @param {string} taskId - */ -export function getTaskDetail(taskId) { - return request({ - url: '/tasks/detail', - method: 'get', - params: { id: taskId } - }) -} - -/** - * 获取我的资源列表 - * @param {string} type - 'images' | 'tasks' | 'results' - */ -export function getMyResources(type) { - return request({ - url: '/resources/list', - method: 'get', - params: { type } - }) -} - -// ================= 模块导出 ================= -// 导出 Auth 模块的所有 API,使其可以通过 import { login } from '@/api' 调用 -export * from './auth' -export * from './admin' -export * from './user' -export * from './demo' \ No newline at end of file +} \ No newline at end of file diff --git a/src/frontend/src/api/task.js b/src/frontend/src/api/task.js new file mode 100644 index 0000000..5560867 --- /dev/null +++ b/src/frontend/src/api/task.js @@ -0,0 +1,152 @@ +import request from '@/utils/request' + +// =================== 通用防护 & 专题防护 =================== + +/** + * 获取任务配额 + */ +export function getTaskQuota() { + return request({ url: '/task/quota', method: 'get' }) +} + +/** + * 获取任务列表 + */ +export function getTaskList(params) { + return request({ url: '/task', method: 'get', params }) +} + +/** + * 提交加噪任务 + */ +export function submitPerturbationTask(formData) { + return request({ + url: '/task/perturbation', + method: 'post', + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) +} + +/** + * 启动加噪任务 (如果需要手动启动) + */ +export function startPerturbationTask(taskId) { + return request({ url: `/task/perturbation/${taskId}/start`, method: 'post' }) +} + +/** + * 获取任务状态 + */ +export function getTaskStatus(taskId) { + return request({ url: `/task/${taskId}/status`, method: 'get' }) +} + +/** + * 获取任务结果图片 + * 用于 Page4 的“下载结果”功能,数据量可能很大 (Base64) + * 设置 5 分钟超时 + */ +export function getTaskResultImages(taskType, taskId) { + return request({ + url: `/image/${taskType}/${taskId}`, + method: 'get', + // 【核心修改】设置 2 分钟超时 (120000ms) + timeout: 120000 + }) +} + +/** + * 获取任务日志 (新增) + */ +export function getTaskLogs(taskId) { + return request({ url: `/task/${taskId}/logs`, method: 'get' }) +} + +/** + * 取消任务 + */ +export function cancelTask(taskId) { + return request({ url: `/task/${taskId}/cancel`, method: 'post' }) +} + +// =================== 效果验证 (微调/评估/热力图) =================== + +/** + * 微调: 从加噪任务创建 + */ +export function submitFinetuneFromPerturbation(data) { + return request({ url: '/task/finetune/from-perturbation', method: 'post', data }) +} + +/** + * 微调: 从上传创建 + */ +export function submitFinetuneFromUpload(formData) { + return request({ + url: '/task/finetune/from-upload', + method: 'post', + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) +} + +/** + * 微调: 启动任务 (新增) + */ +export function startFinetuneTask(taskId) { + return request({ url: `/task/finetune/${taskId}/start`, method: 'post' }) +} + +/** + * 评估: 创建任务 + */ +export function submitEvaluateTask(data) { + return request({ url: '/task/evaluate', method: 'post', data }) +} + +/** + * 评估: 启动任务 (新增) + */ +export function startEvaluateTask(taskId) { + return request({ url: `/task/evaluate/${taskId}/start`, method: 'post' }) +} + +/** + * 热力图: 创建任务 + */ +export function submitHeatmapTask(data) { + return request({ url: '/task/heatmap', method: 'post', data }) +} + +/** + * 热力图: 启动任务 + */ +export function startHeatmapTask(taskId) { + return request({ url: `/task/heatmap/${taskId}/start`, method: 'post' }) +} + +// =================== 资源管理 =================== + +export function getTaskDetail(taskId) { + return request({ url: `/task/${taskId}`, method: 'get' }) +} + +export function listPerturbationTasks() { + return request({ url: '/task/perturbation', method: 'get' }) +} + +export function updatePerturbationTask(taskId, data) { + return request({ url: `/task/perturbation/${taskId}`, method: 'patch', data }) +} + +/** + * 获取微调任务的3D坐标数据 + * API: GET /api/task/finetune//coords + */ +export function getFinetuneCoords(taskId) { + return request({ + url: `/task/finetune/${taskId}/coords`, + method: 'get' + }) +} \ No newline at end of file diff --git a/src/frontend/src/api/user.js b/src/frontend/src/api/user.js index c611658..b26f100 100644 --- a/src/frontend/src/api/user.js +++ b/src/frontend/src/api/user.js @@ -1,9 +1,8 @@ import request from '@/utils/request' /** - * 获取用户配置 - * URL: /api/user/config (GET) - * Return: { config: {... } } + * 获取用户配置 (OnLoad) + * API: GET /api/user/config */ export function getUserConfig() { return request({ @@ -13,9 +12,9 @@ export function getUserConfig() { } /** - * 更新用户配置 - * URL: /api/user/config (PUT) - * Params: { perturbation_configs_id(可选), perturbation_intensity(可选), finetune_config_id(可选) } + * 修改默认配置 + * API: PUT /api/user/config + * Body: { perturbation_configs_id, perturbation_intensity, finetune_configs_id } */ export function updateUserConfig(data) { return request({ @@ -25,26 +24,17 @@ export function updateUserConfig(data) { }) } -/** - * 获取可用的算法列表 - * URL: /api/user/algorithms (GET) - * Return: { perturbation_algorithms: [...], finetune_methods: [...] } - */ -export function getAvailableAlgorithms() { - return request({ - url: '/user/algorithms', - method: 'get' - }) -} - /** * 获取用户统计信息 - * URL: /api/user/stats (GET) - * Return: { stats: {...} } + * 改为真实 GET 请求,并附加时间戳防止浏览器缓存 + * API: GET /api/user/stats */ export function getUserStats() { return request({ url: '/user/stats', - method: 'get' + method: 'get', + params: { + _t: new Date().getTime() // 强制刷新,防止 304 或浏览器缓存 + } }) } \ No newline at end of file diff --git a/src/frontend/src/components/ImagePreviewModal.vue b/src/frontend/src/components/ImagePreviewModal.vue new file mode 100644 index 0000000..74fbbe6 --- /dev/null +++ b/src/frontend/src/components/ImagePreviewModal.vue @@ -0,0 +1,520 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/NavBar.vue b/src/frontend/src/components/NavBar.vue index fc4318a..88d4c71 100644 --- a/src/frontend/src/components/NavBar.vue +++ b/src/frontend/src/components/NavBar.vue @@ -1,99 +1,81 @@ \ No newline at end of file diff --git a/src/frontend/src/components/TaskSideBar.vue b/src/frontend/src/components/TaskSideBar.vue index e46a746..e673fad 100644 --- a/src/frontend/src/components/TaskSideBar.vue +++ b/src/frontend/src/components/TaskSideBar.vue @@ -1,24 +1,35 @@