前端更新 #11

Merged
hnu202326010215 merged 8 commits from yangyixuan_branch into develop 1 month ago

@ -1,3 +0,0 @@
# 源代码模块说明
占位内容:后续补充各子模块结构、构建与运行说明。

@ -22,3 +22,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
repomix-output.xml
node_modules

@ -1,516 +1,5 @@
# Museguard Platform (Frontend)
# Vue 3 + Vite
这是一个基于 **Vue 3 + Vite** 构建的现代化前端项目,旨在提供一套完整的 **图像隐私防护与效果评估** 解决方案。
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
该平台允许用户通过多种算法对图像进行"加噪"处理,以防御 AI 风格迁移、DeepFake 人脸编辑等恶意操作并提供了详细的微调生图验证、数据指标分析PSNR/SSIM/FID/LPIPS及热力图可视化功能。
---
## 目录
- [核心功能](#核心功能)
- [技术栈](#技术栈)
- [项目结构](#项目结构)
- [快速开始](#快速开始)
- [配置说明](#配置说明)
- [API 模块说明](#api-模块说明)
- [页面路由一览](#页面路由一览)
- [开发指南](#开发指南)
- [常见问题](#常见问题)
---
## 核心功能
项目包含 **五大核心模块**
### 1. 🏠 首页展示 (Home)
| 子页面 | 功能说明 |
|--------|----------|
| **原理图解** | 展示平台支持的扰动算法、微调算法、评估指标等原理介绍 |
| **样例预览** | 查看演示图片(原图 vs 加噪图 vs 对比效果) |
| **论文支持** | 相关学术论文与技术文档展示 |
### 2. 🛡️ 通用防护 (General Protection)
| 模式 | 功能说明 |
|------|----------|
| **快速模式** | 使用系统推荐的默认配置,一键上传图片即可开始防护 |
| **通用模式** | 自定义选择加密算法、扰动强度及目标风格 |
### 3. 🎯 专题防护 (Topic Protection)
| 专题 | 功能说明 |
|------|----------|
| **防风格迁移** | 针对艺术风格模仿的专项防御 |
| **防人脸编辑** | 针对 DeepFake 和面部重绘的防御 |
| **防定制生成** | 抵抗 LoRA 等模型对特定画风的训练 |
### 4. 📊 效果验证 (Effect Validation)
| 功能 | 说明 |
|------|------|
| **微调生图验证** | 模拟攻击者微调过程,直观展示防护前后的生成效果差异 |
| **数据指标对比** | 基于 PSNR、SSIM、FID、LPIPS 等指标量化分析防护质量 |
| **热力图分析** | 通过 Attention Map 和频域分析揭示隐形扰动分布 |
### 5. 📁 资源管理 (My Resources)
| 功能 | 说明 |
|------|------|
| **我的任务** | 查看历史任务状态(排队、运行中、已完成、失败) |
| **已防护图片** | 管理已处理的防护图片 |
| **验证结果** | 查看生成的评估报告 |
| **账号中心** | 用户资料、密码修改、个人配置、管理员控制台 |
---
## 技术栈
本项目采用轻量、高效的现代前端技术栈:
| 类别 | 技术 | 说明 |
|------|------|------|
| **核心框架** | [Vue 3](https://vuejs.org/) | Composition API + `<script setup>` 语法 |
| **构建工具** | [Vite](https://vitejs. dev/) | 秒级启动,热更新极快 |
| **路由管理** | [Vue Router 4](https://router.vuejs.org/) | 全站路由懒加载优化 |
| **网络请求** | [Axios](https://axios-http.com/) | 封装了拦截器与统一错误处理 |
| **样式架构** | 原生 CSS3 + CSS Variables | 模块化设计,无第三方 UI 库依赖 |
---
## 项目结构
```text
src/
├── api/ # 📡 统一接口管理层
│ ├── index.js # 主入口,导出所有 API + 图片预处理逻辑
│ ├── auth.js # 认证相关 API登录/注册/登出/改密码)
│ ├── user.js # 用户相关 API配置/算法/统计)
│ ├── admin.js # 管理员 API用户管理/系统统计)
│ └── demo.js # 演示数据 API样例图片/算法信息)
├── components/ # 🧩 公共组件
│ ├── NavBar.vue # 顶部导航栏
│ ├── TaskSideBar.vue # 任务状态侧边栏(多页面复用)
│ └── TaskDetails.vue # 任务详情弹窗
├── router/
│ └── index.js # 🛤️ 路由配置(已实现懒加载)
├── utils/
│ └── request.js # 🔧 Axios 二次封装(超时/拦截/错误提示)
├── views/ # 📄 页面视图
│ ├── Home.vue # 首页
│ ├── Login.vue # 登录页
│ ├── Register.vue # 注册页
│ ├── GeneralProtection.vue # 通用防护入口
│ ├── TopicProtection.vue # 专题防护入口
│ ├── EffectValidation.vue # 效果验证入口
│ ├── MyResources.vue # 我的资源入口
│ ├── AccountCenter.vue # 账号中心
│ │
│ ├── home-subpages/ # 首页子页面
│ │ ├── PrincipleDiagram.vue # 原理图解
│ │ ├── SamplePreview.vue # 样例预览
│ │ └── PaperSupport.vue # 论文支持
│ │
│ ├── general-protect-subpages/ # 通用防护子页面
│ │ ├── QuickMode.vue # 快速模式
│ │ └── UniversalMode.vue # 通用模式
│ │
│ ├── topic-protect-subpages/ # 专题防护子页面
│ │ └── AntiStyleTransfer.vue # 防风格迁移
│ │
│ ├── effect-validate-subpages/ # 效果验证子页面
│ │ ├── FineTuning.vue # 微调生图
│ │ ├── MetricsComparison.vue # 数据指标对比
│ │ └── HeatmapComparison.vue # 热力图分析
│ │
│ ├── my-resources-subpages/ # 资源管理子页面
│ │ ├── MyTaskResources.vue # 我的任务
│ │ ├── ProtectedImages.vue # 已防护图片
│ │ └── ValidationResults.vue # 验证结果
│ │
│ └── account-center-subpages/ # 账号中心子页面
│ ├── EditProfile.vue # 编辑资料
│ ├── ChangePassword.vue # 修改密码
│ ├── UserConfig.vue # 用户配置
│ └── AdminUserManage.vue # 管理员控制台
├── App.vue # 🌳 根组件
├── main.js # 🚀 入口文件
└── Style.css # 🎨 全局样式 + CSS 变量
```
### 架构亮点
1. **前端图片预处理**:在 `src/api/index.js` 中实现了浏览器端的 Canvas 图片裁剪与压缩(居中裁剪为 512x512 正方形 JPG减轻后端压力并加快上传速度。
2. **组件高度复用**`TaskSideBar` 被抽离为独立组件,在通用防护、专题防护、效果验证等多个页面中复用,保持 UI 一致性。
3. **样式分层管理**
- `Style.css` 处理全局样式和通用组件样式
- 各子页面的特有样式(如 `GeneralProtectSub.css`)独立管理,互不干扰
4. **API 模块化**:按业务领域拆分为 `auth`、`user`、`admin`、`demo` 四个模块,便于维护和扩展。
---
## 快速开始
### 1. 环境准备
确保你的电脑已安装 [Node.js](https://nodejs.org/)(推荐 v16+)。
验证安装:
```bash
node -v # 应显示 v16. x.x 或更高版本
npm -v # 应显示 8.x.x 或更高版本
```
### 2. 克隆项目
```bash
git clone <你的仓库地址>
cd frontend
```
### 3. 安装依赖
```bash
npm install
```
> 💡 如果安装缓慢,可以使用国内镜像:
> ```bash
> npm install --registry=https://registry.npmmirror.com
> ```
### 4. 启动开发服务器
```bash
npm run dev
```
启动成功后,访问终端显示的地址(通常是 `http://localhost:5173`)。
### 5. 构建生产版本
```bash
npm run build
```
构建产物将生成在 `dist/` 目录下,可部署到任意静态服务器。
### 6. 预览生产构建
```bash
npm run preview
```
---
## 配置说明
### 后端接口代理
本项目默认开启了开发环境代理,以解决跨域问题。配置位于 `vite.config.js`
```javascript
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0. 1:6001', // 后端服务地址
changeOrigin: true, // 必须开启
}
}
}
```
> ⚠️ **重要提示**
> - 确保后端服务运行在 `6001` 端口
> - 如果后端地址不同,请修改 `target`
> - 生产环境需要配置 Nginx 反向代理
### 环境变量(可选)
如需区分开发/生产环境,可创建以下文件:
```bash
# . env. development
VITE_API_BASE_URL=http://127.0.0. 1:6001
# .env.production
VITE_API_BASE_URL=https://your-production-api. com
```
---
## API 模块说明
所有 API 统一从 `@/api/index.js` 导出,使用时只需:
```javascript
import { authLogin, getUserStats, listDemoImages } from '@/api/index'
```
### Auth 模块 (`auth.js`)
| 函数名 | 方法 | 路径 | 说明 |
|--------|------|------|------|
| `authRegister(data)` | POST | `/auth/register` | 用户注册 |
| `authLogin(data)` | POST | `/auth/login` | 用户登录 |
| `authChangePassword(data)` | POST | `/auth/change-password` | 修改密码 |
| `authGetProfile()` | GET | `/auth/profile` | 获取用户信息 |
| `authLogout()` | POST | `/auth/logout` | 用户登出 |
**登录示例:**
```javascript
const res = await authLogin({ username: 'admin', password: '123456' })
// 返回: { message: '登录成功', access_token: '... ', user: {... } }
localStorage.setItem('access_token', res. access_token)
```
### User 模块 (`user.js`)
| 函数名 | 方法 | 路径 | 说明 |
|--------|------|------|------|
| `getUserConfig()` | GET | `/user/config` | 获取用户配置 |
| `updateUserConfig(data)` | PUT | `/user/config` | 更新用户配置 |
| `getAvailableAlgorithms()` | GET | `/user/algorithms` | 获取可用算法列表 |
| `getUserStats()` | GET | `/user/stats` | 获取用户统计信息 |
**获取算法列表示例:**
```javascript
const res = await getAvailableAlgorithms()
// 返回结构:
// {
// perturbation_algorithms: [
// { id, method_code, method_name, description }
// ],
// finetune_methods: [
// { id, method_code, method_name, description }
// ]
// }
```
### Demo 模块 (`demo.js`)
| 函数名 | 方法 | 路径 | 说明 |
|--------|------|------|------|
| `listDemoImages()` | GET | `/demo/images` | 获取演示图片列表 |
| `getDemoAlgorithms()` | GET | `/demo/algorithms` | 获取演示算法信息 |
| `getDemoStats()` | GET | `/demo/stats` | 获取演示统计信息 |
| `getDemoOriginalImageUrl(filename)` | - | - | 构造原始图片 URL |
| `getDemoPerturbedImageUrl(filename)` | - | - | 构造加噪图片 URL |
| `getDemoComparisonImageUrl(filename)` | - | - | 构造对比图片 URL |
**演示图片数据结构:**
```javascript
{
id: 'sample1',
name: 'sample1',
original: '/api/demo/image/original/sample1.jpg',
perturbed: ['/api/demo/image/perturbed/sample1_noisy.jpg'],
comparisons: ['/api/demo/image/comparison/sample1_compare.jpg']
}
```
### Admin 模块 (`admin.js`)
| 函数名 | 方法 | 路径 | 说明 |
|--------|------|------|------|
| `getAdminUserList(params)` | GET | `/admin/users` | 获取用户列表(分页) |
| `getAdminUserDetail(userId)` | GET | `/admin/users/:id` | 获取用户详情 |
| `createAdminUser(data)` | POST | `/admin/users` | 创建用户 |
| `updateAdminUser(userId, data)` | PUT | `/admin/users/:id` | 更新用户 |
| `deleteAdminUser(userId)` | DELETE | `/admin/users/:id` | 删除用户 |
| `getSystemStats()` | GET | `/admin/stats` | 获取系统统计 |
**系统统计数据结构:**
```javascript
{
stats: {
users: { total, active, admin },
tasks: { total, completed, processing, failed, waiting },
images: { total }
}
}
```
---
## 页面路由一览
| 路径 | 页面 | 需要登录 | 显示导航栏 |
|------|------|:--------:|:----------:|
| `/login` | 登录页 | ❌ | ❌ |
| `/register` | 注册页 | ❌ | ❌ |
| `/` | 首页 | ✅ | ✅ |
| `/principle` | 原理图解 | ✅ | ✅ |
| `/sample` | 样例预览 | ✅ | ✅ |
| `/paper` | 论文支持 | ✅ | ✅ |
| `/general-protection` | 通用防护入口 | ✅ | ✅ |
| `/quick-mode` | 快速模式 | ✅ | ✅ |
| `/universal-mode` | 通用模式 | ✅ | ✅ |
| `/topic-protection` | 专题防护入口 | ✅ | ✅ |
| `/topic-protection/style-transfer` | 防风格迁移 | ✅ | ✅ |
| `/effect-validation` | 效果验证入口 | ✅ | ✅ |
| `/effect-validation/fine-tuning` | 微调生图 | ✅ | ✅ |
| `/effect-validation/metrics` | 数据指标对比 | ✅ | ✅ |
| `/effect-validation/heatmap` | 热力图分析 | ✅ | ✅ |
| `/my-resources` | 我的资源入口 | ✅ | ✅ |
| `/my-resources/tasks` | 我的任务 | ✅ | ✅ |
| `/my-resources/protected-images` | 已防护图片 | ✅ | ✅ |
| `/my-resources/validation-results` | 验证结果 | ✅ | ✅ |
| `/account` | 账号中心 | ✅ | ✅ |
| `/account/edit-profile` | 编辑资料 | ✅ | ✅ |
| `/account/change-password` | 修改密码 | ✅ | ✅ |
| `/account/config` | 用户配置 | ✅ | ✅ |
| `/account/admin-users` | 管理员控制台 | ✅ | ✅ |
---
## 开发指南
### 添加新页面
1. 在 `src/views/` 下创建 `. vue` 文件
2. 在 `src/router/index.js` 中添加路由配置:
```javascript
{
path: '/your-new-page',
name: 'YourNewPage',
component: () => import('../views/YourNewPage.vue'),
meta: { requiresAuth: true, hideNavBar: false }
}
```
### 添加新 API
1. 在对应的 `src/api/*. js` 文件中添加函数
2. 确保在 `src/api/index.js` 中导出
```javascript
// src/api/user.js
export function yourNewApi(data) {
return request({
url: '/user/your-endpoint',
method: 'post',
data
})
}
// src/api/index.js
export * from './user' // 已有这行,自动导出新函数
```
### 样式开发规范
1. **全局样式**:修改 `src/Style.css`
2. **页面特有样式**:在对应的 `.css` 文件中添加(如 `GeneralProtectSub. css`
3. **组件样式**:使用 `<style scoped>` 避免污染
### 开发者快捷登录
为方便开发调试,项目内置了一个"作弊码"登录:
- 用户名:`admin`
- 密码:`2025Aa`
此账号会跳过后端验证,直接使用 Mock 数据登录。
> ⚠️ 请勿在生产环境保留此功能,上线前应删除 `src/api/auth.js` 中的相关代码。
---
## 常见问题
### Q1: 启动时报错 "ENOENT: no such file or directory"
**原因**:依赖未安装或安装不完整
**解决**
```bash
rm -rf node_modules package-lock.json
npm install
```
### Q2: 页面显示 "登录已过期,请重新登录"
**原因**Token 失效或后端服务未启动
**解决**
1. 检查后端服务是否正常运行
2. 清除浏览器 LocalStorage 后重新登录
3. 检查 `vite.config.js` 中的代理配置是否正确
### Q3: 图片上传后显示异常
**原因**:前端预处理将图片统一转换为 512x512 JPG 格式
**说明**:这是设计行为,目的是确保图片尺寸一致性和减少传输大小。如需修改,请编辑 `src/api/index. js` 中的 `processImageInBrowser` 函数。
### Q4: 如何修改默认端口?
编辑 `vite.config.js`
```javascript
server: {
port: 3000, // 改为你想要的端口
...
}
```
### Q5: 生产环境如何配置?
1. 修改 API 请求基础路径(如果需要)
2. 配置 Nginx 反向代理示例:
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
root /path/to/dist;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://127.0.0. 1:6001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
---
## 版本信息
- **当前版本**0.0.0 (开发中)
- **Vue 版本**3. 5.24
- **Vite 版本**7.2.4
---
## 许可证
[LICENSE 信息占位符]
---
## 贡献指南
欢迎提交 Issue 和 Pull Request
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 提交 Pull Request
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

@ -1,10 +1,9 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<title>museguard</title>
</head>
<body>
<div id="app"></div>

File diff suppressed because it is too large Load Diff

@ -1,21 +0,0 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.13.2",
"repomix": "^1.9.2",
"vue": "^3.5.24",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.2.4"
}
}

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because it is too large Load Diff

@ -1,39 +1,9 @@
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import NavBar from './components/NavBar.vue'
const route = useRoute()
//
// 1. route.name
// 2. meta.hideNavBar
const showNavBar = computed(() => {
if (!route.name) return false //
return !route.meta.hideNavBar
})
</script>
<template>
<NavBar v-if="showNavBar" />
<div class="main-content" :class="{ 'with-navbar': showNavBar, 'no-navbar': !showNavBar }">
<router-view></router-view>
</div>
<router-view />
</template>
<style>
/* 保持不变 */
.main-content.with-navbar {
padding-top: 6rem;
min-height: 100vh;
box-sizing: border-box;
}
.main-content.no-navbar {
padding-top: 0;
height: 100vh;
display: flex;
flex-direction: column;
}
</style>
<style scoped>
</style>

@ -1,587 +1,332 @@
/* 变量与重置 */
/* ===== Global CSS Variables ===== */
:root {
font-size: 16px;
--primary-color: #333;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #f5f5f5; /* 全局默认背景 */
}
#app {
width: 100%;
/* Fonts */
--font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
--font-base: clamp(14px, 1.5vw, 18px);
--font-weight-base: 500;
/* Space Units */
--space-unit: clamp(4px, 0.5vw, 8px);
--space-xs: calc(var(--space-unit) * 1);
--space-sm: calc(var(--space-unit) * 2);
--space-md: calc(var(--space-unit) * 4);
--space-lg: calc(var(--space-unit) * 8);
--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-min: 80px;
--navbar-height-pct: 75%; /* 15% top to 75% height */
/* Colors - Updated Palette */
/* 60% Light Grey/Beige */
--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-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-dark-primary: #18283b;
--navbar-dark-secondary: #2c3e50;
--navbar-light-primary: #f5f6fa;
--navbar-light-secondary: #8392a5;
/* Animation Speeds */
--transition-fast: 0.2s ease-out;
--transition-normal: 0.35s ease-out;
--transition-smooth: 0.5s cubic-bezier(0.4, 0, 0.2, 1);
/* Z-Index */
--z-nav: 100;
--z-close-btn: 110;
--z-subpage: 50;
--z-mainpage: 10;
}
/* ===== Reset & Base ===== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.std-input, .std-select {
padding: 0.8rem;
border: 1px solid #333;
font-size: 1.05rem;
width: 100%;
box-sizing: border-box;
html {
font-size: var(--font-base);
scroll-behavior: smooth;
background: var(--color-bg-primary);
color: var(--color-text-main);
}
/* ShareStyle.css - 六大页面通用样式库 */
.shared-page-container {
min-height: 100vh;
/* 默认高度处理,适配有无 Navbar 的情况 */
height: calc(100vh - 6rem);
background-color: #f5f5f5; /* 默认灰底 */
display: flex;
flex-direction: column; /* 默认垂直排列,方便扩展 */
align-items: center; /* 水平居中 */
justify-content: center; /* 垂直居中 */
position: relative;
width: 100%;
box-sizing: border-box;
body {
font-family: var(--font-family);
line-height: 1.6;
overflow: hidden; /* Main flow handles scrolling */
background: var(--color-bg-primary);
}
/* Input selection */
*:not(input):not(textarea) { user-select: none; -webkit-user-select: none; }
input, textarea { user-select: text; -webkit-user-select: text; }
/* --- 卡片网格布局--- */
.shared-card-grid {
display: flex;
justify-content: center;
align-items: stretch;
gap: 2rem; /* 统一间距 */
width: 90%;
max-width: 80rem;
padding: 0 2rem;
box-sizing: border-box;
/* ===== UI Component: Cards ===== */
/* Base Card */
.ui-card {
position: relative;
border-radius: 24px; /* Medium rounded */
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
overflow: hidden;
}
/* --- 通用交互卡片 --- */
.shared-card {
background: white;
border: 0.125rem solid #000;
padding: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
transition: transform 0.2s, box-shadow 0.2s;
.ui-card.interactive {
cursor: pointer;
width: 18rem;
min-height: 25rem;
}
.shared-card:hover {
transform: translateY(-0.3rem);
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
.ui-card.interactive:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0,0,0,0.1);
}
.shared-card-title {
font-size: 1.4rem;
font-weight: bold;
margin-bottom: 1rem;
text-align: center;
/* 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);
}
.shared-card-content {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: #555;
font-size: 0.95rem;
.ui-card.solid {
background: #FFFFFF;
border: 1px solid rgba(0,0,0,0.05);
}
/* 卡片内的视觉占位区域 (灰色虚线框等) */
.shared-card-visual {
flex: 1;
width: 100%;
margin-bottom: 1.5rem;
display: flex;
justify-content: center;
align-items: center;
border: 1px dashed #999;
background-color: #f9f9f9;
color: #555;
font-size: 0.9rem;
.ui-card.gradient {
background: linear-gradient(135deg, var(--color-bg-secondary), #ffffff);
}
/* --- 操作按钮组 --- */
.shared-action-group {
display: flex;
flex-direction: column;
gap: 0.8rem;
width: 100%;
align-items: center;
}
/* Card Shapes */
.ui-card.circle { border-radius: 50%; aspect-ratio: 1/1; display: flex; align-items: center; justify-content: center; }
.ui-card.rect { border-radius: 24px; }
/* 通用按钮样式 */
.shared-btn {
padding: 0.6rem 1rem;
font-size: 1.1rem;
cursor: pointer;
width: 100%;
border: 0.125rem solid #000;
background: white;
transition: all 0.2s;
text-align: center;
}
.shared-btn:hover {
background: #f0f0f0;
}
/* 实心/强调按钮 */
.shared-btn-primary {
background: white;
font-weight: bold;
}
.shared-btn-primary:hover {
background: #000;
color: white;
}
/* --- 模态框 (Modal) --- */
.shared-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
/* ===== UI Component: Buttons ===== */
.ui-btn {
display: inline-flex;
align-items: center;
z-index: 2000;
}
.shared-modal-content {
background: white;
padding: 2rem;
border: 0.125rem solid #000;
border-radius: 0.5rem;
width: 30rem;
max-width: 90%;
text-align: center;
display: flex;
flex-direction: column;
gap: 1rem;
}
.shared-modal-title {
margin: 0;
font-size: 1.5rem;
}
.shared-modal-text {
color: #555;
line-height: 1.6;
text-align: left;
}
.shared-modal-close-btn {
align-self: center;
padding: 0.5rem 2rem;
background: #333;
color: white;
justify-content: center;
padding: 0.8em 1.6em;
font-weight: 600;
border: none;
cursor: pointer;
transition: all var(--transition-fast);
font-size: 1rem;
margin-top: 1rem;
}
.shared-modal-close-btn:hover {
background: #000;
gap: 0.5em;
}
/* =========================================
1. (Global Base)
========================================= */
/* 所有页面通用的容器几何属性 (颜色和对齐方式由各页面自己决定) */
.page-container {
height: calc(100vh - 6rem);
display: flex;
position: relative;
overflow: hidden;
width: 100%;
box-sizing: border-box;
.ui-btn:hover {
transform: scale(1.02);
}
/* 通用返回按钮定位 */
.back-btn-container {
position: fixed; /* 不随页面滚动 */
top: 8rem;
left: 1rem;
z-index: 10;
width: 15rem;
.ui-btn:active {
transform: scale(0.98);
}
/* 通用返回按钮样式 */
.common-back-btn {
border: 0.125rem solid #000;
background: white;
padding: 0.625rem 2rem;
font-size: 1.1rem;
cursor: pointer;
text-align: center;
flex-shrink: 0;
width: 10rem;
box-sizing: border-box;
white-space: nowrap;
/* 禁止文字选中 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-touch-callout: none;
/* 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);
}
.common-back-btn:hover {
background: #333;
color: white;
.ui-btn.glass:hover {
background: rgba(24, 40, 59, 0.15);
}
.shared-back-link {
background: none;
border: none;
color: #333;
cursor: pointer;
font-size: 0.95rem;
padding: 0;
.ui-btn.solid {
background: var(--color-contrast-dark);
color: var(--color-text-light);
}
.shared-back-link:hover {
text-decoration: underline;
color: #000;
.ui-btn.solid:hover {
background: var(--color-contrast-light);
box-shadow: 0 4px 12px rgba(24, 40, 59, 0.3);
}
/* 页面标题 (通用) */
.page-title {
text-align: center;
margin-bottom: 1.5rem;
font-size: 1.6rem;
font-weight: bold;
.ui-btn.gradient {
background: linear-gradient(135deg, var(--color-accent-secondary), var(--color-accent-primary));
color: var(--color-text-main);
}
/* =========================================
2. (Sidebar System)
: ev, gen, topic
========================================= */
.sidebar-placeholder {
width: 0;
flex-shrink: 0;
.ui-btn.gradient:hover {
filter: brightness(1.1);
box-shadow: 0 4px 12px rgba(255, 159, 28, 0.4);
}
.task-card {
position: absolute;
top: 10rem;
left: 1.25rem;
width: 15rem;
background: white;
border: 0.125rem solid #000;
padding: 1rem;
display: flex;
flex-direction: column;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
height: auto;
max-height: calc(100vh - 15rem);
box-sizing: border-box;
z-index: 10;
}
.task-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
/* Button Shapes */
.ui-btn.rounded { border-radius: 999px; }
.ui-btn.rect { border-radius: 12px; }
.task-title {
font-size: 1.1rem;
text-align: center;
margin-bottom: 1rem;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
.task-list {
list-style: none;
padding: 0;
margin: 0;
flex: 1;
overflow-y: auto;
}
.task-item {
margin-bottom: 1rem;
padding: 0.5rem;
border: 1px solid #ddd;
background: #fafafa;
}
.task-header {
/* ===== Layout Classes ===== */
.layout-main {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.progress-bar-container {
height: 0.5rem;
background: #eee;
border-radius: 0.25rem;
overflow: hidden;
width: 100vw;
height: 100vh;
position: relative;
}
.progress-bar {
.layout-content {
/* Content pushed by nav is handled via margin or absolute positioning in components */
flex: 1;
height: 100%;
background: #333;
}
.task-footer {
margin-top: 1rem;
text-align: center;
font-size: 0.9rem;
color: #666;
border-top: 1px solid #eee;
padding-top: 0.5rem;
position: relative;
margin-left: var(--navbar-width-min); /* Default collapsed state margin */
transition: margin-left 0.2s;
}
/* =========================================
3. (Main Form Layout)
: ev, gen, topic
========================================= */
.main-form-area {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
margin-left: 19.25rem;
margin-right: 10rem;
height: 100%;
overflow-y: auto;
/* Waterfall Container */
.scroll-container {
left:auto;
height: 100vh;
overflow-y: hidden; /* We control scroll manually for custom animation */
position: relative;
}
.form-container {
background: white;
border: 0.125rem solid #000;
padding: 3rem;
/* Sections */
.scroll-section {
height: 100vh;
width: 100%;
max-width: 90rem;
height: auto;
min-height: 60%;
display: flex;
flex-direction: column;
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;
gap: 2.5rem;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.08);
}
@media (min-width: 1600px) {
.form-container {
max-width: 95%;
}
}
/* =========================================
4. UI (UI Components)
:
========================================= */
/* --- 4.1 表单元素 --- */
.form-row {
display: flex;
gap: 2rem;
align-items: flex-start;
}
.form-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.form-group label {
font-weight: bold;
font-size: 1.2rem;
}
.std-input, .std-select {
padding: 0.8rem;
border: 1px solid #333;
font-size: 1.05rem;
width: 100%;
box-sizing: border-box;
outline: none;
padding: 2rem;
}
.std-input:focus {
border-color: #000;
box-shadow: 0 0 0 1px #000;
.scroll-section.active {
opacity: 1;
pointer-events: auto;
z-index: 5;
transform: translateY(0);
}
.hint-text {
font-size: 0.9rem;
color: #666;
margin-top: 0.2rem;
.scroll-section.prev {
transform: translateY(-100%);
opacity: 0;
}
/* --- 4.2 单选组与VIP标签 --- */
.radio-group {
display: flex;
gap: 2rem;
align-items: center;
height: 2.5rem;
.scroll-section.next {
transform: translateY(100%);
opacity: 0;
}
.radio-group label {
font-weight: normal;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.1rem;
/* 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);
}
.vip-option {
position: relative;
color: purple;
font-weight: bold !important;
.subpage-enter-active, .subpage-leave-active {
transition: transform var(--transition-smooth), opacity var(--transition-smooth);
}
.vip-tag {
background: purple;
color: white;
font-size: 0.7rem;
padding: 0.1rem 0.3rem;
border-radius: 4px;
margin-left: 0.3rem;
.subpage-enter-from, .subpage-leave-to {
transform: scale(0.95) translateY(20px);
opacity: 0;
}
/* --- 4.3 按钮集合 --- */
/* 底部大按钮 (ev, gen, topic) */
.start-btn {
background: #333;
color: white;
border: none;
padding: 1rem 4rem;
font-size: 1.25rem;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
}
.start-btn:hover {
background: #000;
transform: translateY(-1px);
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* 操作栏 (ev, gen, topic) */
.action-row {
margin-top: auto;
justify-content: space-between;
align-items: center;
border-top: 1px solid #eee;
padding-top: 2rem;
display: flex;
html {
font-size: var(--font-base);
scroll-behavior: smooth;
background: var(--color-bg-primary);
color: var(--color-text-main);
}
/* 小操作按钮 (列表页用) */
.action-btn {
padding: 0.4rem 0.8rem;
border: 1px solid #000;
background: #fff;
cursor: pointer;
font-size: 0.9rem;
min-width: 5rem;
text-align: center;
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;
}
.action-btn:hover { background: #f0f0f0; }
/* 资源选择/上传按钮 */
.source-btn, .upload-btn {
border: 1px solid #333;
background: #eaf4ff; /* source-btn默认 */
padding: 0.8rem 1.5rem;
cursor: pointer;
color: #0056b3;
font-weight: bold;
text-align: left;
font-size: 1.05rem;
transition: all 0.2s;
}
.upload-btn { background: #f0f0f0; color: #333; } /* upload-btn 覆盖 */
/* ===== Typography (Global Refactor) ===== */
.source-btn:hover, .upload-btn:hover {
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
filter: brightness(0.95);
/* 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;
font-weight: 700;
}
/* --- 4.4 其他通用组件 --- */
/* 文件名显示 */
.file-name {
font-size: 0.95rem;
color: #555;
font-style: italic;
/* H1 - 用于主标题 (虽然你主要问的是 h2/h3/p但统一规划更好) */
h1 {
font-size: clamp(2rem, 4vw, 3.5rem);
letter-spacing: -0.02em;
}
/* 数据集/状态概况条 */
.dataset-summary {
font-size: 1rem;
color: #333;
background: #f6f8fb;
border: 1px solid #e0e6f0;
padding: 1rem 1.25rem;
border-radius: 0.4rem;
margin-top: 0.5rem;
}
.dataset-summary .highlight {
display: inline-block;
margin: 0 0.3rem;
padding: 0.1rem 0.5rem;
background: #111;
color: #fff;
border-radius: 0.3rem;
font-weight: bold;
}
/* 模态框遮罩 */
.modal-overlay, .shared-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
/* 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);
}
/* 上传区域容器 */
.upload-section {
display: flex;
align-items: center;
gap: 1rem;
/* H3 - 用于模块标题或卡片内部标题 */
h3 {
font-size: clamp(1.2rem, 2vw, 1.5rem);
font-weight: 600;
color: var(--color-text-main);
margin-bottom: var(--space-xs);
}
/* 列表滚动容器 (res) */
.content-scroll-wrapper {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1rem;
padding-right: 0.5rem;
/* H4 - 用于小标题 */
h4 {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-text-muted); /* 稍微淡一点 */
}
/* 空状态 */
.empty-hint, .empty-row {
text-align: center;
color: #999;
/* Paragraphs - 正文 */
p {
font-size: 1rem;
line-height: 1.65; /* 稍微增加行高提升可读性 */
color: var(--color-text-main); /* 默认深色 */
margin-bottom: 1em;
max-width: 65ch; /* 限制每行字数,提升阅读体验 */
}

@ -0,0 +1,52 @@
<script setup>
const emit = defineEmits(['close'])
const handleClose = () => {
emit('close')
}
</script>
<template>
<button class="close-btn" @click="handleClose" aria-label="">
<span class="btn-text">返回</span>
</button>
</template>
<style scoped>
.close-btn {
/* Fixed top right position */
position: fixed;
top: var(--space-md);
right: var(--space-md);
/* Size & Ratio 16:9 */
height: 45px;
width: 80px; /* 16:9 approx */
/* Style */
background: var(--color-contrast-dark);
color: var(--color-text-light);
border: none;
border-radius: 12px;
/* Layout */
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: var(--z-close-btn);
transition: transform var(--transition-fast), background-color var(--transition-fast);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.close-btn:hover {
transform: translateY(-2px);
background: var(--color-contrast-light);
}
.btn-text {
font-weight: 600;
font-size: 0.9rem;
}
</style>

@ -1,81 +1,380 @@
<template>
<nav class="navbar">
<!-- Left: Logo -->
<div class="nav-left">
<router-link to="/" class="nav-item logo-link">LOGO</router-link>
</div>
<!-- Middle: Navigation Links -->
<div class="nav-center">
<router-link to="/general-protection" class="nav-item">通用防护</router-link>
<router-link to="/topic-protection" class="nav-item">专题防护</router-link>
<router-link to="/effect-validation" class="nav-item">效果验证</router-link>
<router-link to="/my-resources" class="nav-item">我的资源</router-link>
</div>
<!-- Right: Account Center -->
<div class="nav-right">
<router-link to="/account" class="nav-item">账号中心</router-link>
</div>
</nav>
</template>
<script setup>
//
</script>
<style scoped>
.navbar {
display: flex;
justify-content: space-between; /* 左右两端对齐 */
align-items: center;
padding: 0 2rem; /* 左右增加内边距,防止贴边 */
border-bottom: 0.0625rem solid #ccc;
position: fixed;
top: 0;
left: 0;
height: 6rem;
width: 100%;
background-color: white;
z-index: 1000;
box-sizing: border-box;
}
.nav-center {
flex: 1; /* 占据剩余所有空间 */
display: flex;
justify-content: space-evenly; /* 在空间内均分间隔 */
padding: 0 2rem; /* 与左右区域保持一定距离 */
}
.nav-left, .nav-right {
flex-shrink: 0; /* 防止左右元素被压缩 */
}
.nav-right {
font-weight: bold;
}
.nav-item {
border: 0.125rem solid #000;
padding: 0.5rem 1.25rem;
text-decoration: none;
color: black;
display: inline-block;
white-space: nowrap;
background-color: white;
transition: background-color 0.2s;
}
/* LOGO 样式 */
.logo-link {
font-weight: bold;
font-size: 1.2rem;
}
/* 当前页面高亮*/
.router-link-active {
background-color: #ffffcc;
}
<script setup>
import { ref, computed, watch } from 'vue'
import { useRoute } from 'vue-router'
const props = defineProps({
currentSection: {
type: String,
default: 'home'
}
})
const emit = defineEmits(['navigate', 'logout', 'toggle']) // Added 'toggle'
const route = useRoute()
// Toggle State
const isExpanded = ref(false)
// Watch for changes in isExpanded and emit to parent
watch(isExpanded, (newValue) => {
emit('toggle', newValue)
})
// Navigation Items for Waterfall
const navItems = [
{ id: 'home', label: '首页', icon: 'fas fa-home' },
{ id: 'page1', label: '通用防护', icon: 'fas fa-shield-alt' },
{ id: 'page2', label: '专题防护', icon: 'fas fa-cubes' },
{ id: 'page3', label: '效果验证', icon: 'fas fa-chart-line' },
{ id: 'page4', label: '我的资源', icon: 'fas fa-database' }
]
const navCount = navItems.length
// Calculate Active Position for the "Highlight" box
const activeIndex = computed(() => {
const idx = navItems.findIndex(item => item.id === props.currentSection)
return idx >= 0 ? idx : 0 // Default to top if not found
})
// Calculate heights for the 10%-90% distribution
const itemHeightPercent = 80 / navCount // Total 80% usable space
const highlightTop = computed(() => {
// Start at 10% offset + (index * itemHeight)
return `${10 + (activeIndex.value * itemHeightPercent)}%`
})
const highlightHeight = `${itemHeightPercent}%`
const handleNavClick = (id) => {
emit('navigate', id)
}
const handleLogout = () => {
emit('logout')
}
const handlePage5 = () => {
emit('navigate', 'page5')
}
// Check if we are on page5 to highlight correctly or deselect
const isPage5Active = computed(() => props.currentSection === 'page5')
</script>
<template>
<!--
Changed: Removed fixed width constraints on the container if it was interfering,
but kept the z-index and positioning logic.
The container itself allows clicks to pass through.
-->
<div id="navbar-container">
<input type="checkbox" id="nav-toggle" v-model="isExpanded">
<!-- Main Navigation Bar -->
<div id="nav-bar">
<!-- Header / Toggle -->
<div id="nav-header">
<a id="nav-title" href="#">MuseGuard</a>
<label for="nav-toggle">
<span id="nav-toggle-burger"></span>
</label>
<hr>
</div>
<!-- Content / Links -->
<div id="nav-content">
<!-- Highlight Box (Only show if not on Page 5) -->
<div
v-if="!isPage5Active"
id="nav-content-highlight"
:style="{ top: highlightTop, height: highlightHeight }"
></div>
<!-- Navigation Items Container -->
<div class="nav-items-container">
<div
v-for="item in navItems"
:key="item.id"
class="nav-button"
:class="{ active: currentSection === item.id }"
@click="handleNavClick(item.id)"
>
<i :class="item.icon"></i>
<span>{{ item.label }}</span>
</div>
</div>
</div>
</div>
<!-- External Floating Buttons (Page 5 & Logout) -->
<div class="external-actions">
<!-- Page 5 Button -->
<button
class="ui-btn solid rounded page-5-btn"
:class="{ active: isPage5Active }"
@click="handlePage5"
title="页面5"
>
<i class="fas fa-user-circle"></i>
<span v-if="isExpanded"></span>
</button>
<!-- Logout Button -->
<button
class="ui-btn glass circle logout-btn"
@click="handleLogout"
title="登出"
>
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
</div>
</template>
<style scoped>
/* Font Awesome Placeholder for icons */
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css');
#navbar-container {
position: fixed;
top: 0;
left: 0;
height: 100vh;
/*
Crucial change: Make the container width dynamic so it doesn't
trap mouse events in empty space if it was too wide,
or visual bugs.
However, since pointer-events is none, it doesn't block clicks.
The real width is determined by children.
*/
width: 100vw;
z-index: var(--z-nav);
pointer-events: none; /* Let clicks pass through empty areas */
}
/* Re-implementing provided SCSS logic in CSS */
#nav-bar {
pointer-events: auto;
position: absolute;
left: 16px; /* 1vw approx */
top: 15vh; /* Starts at 15% down */
height: 50vh; /* Occupies 60% (approx 75% max) */
background: var(--navbar-dark-primary);
border-radius: 16px;
display: flex;
flex-direction: column;
color: var(--navbar-light-primary);
font-family: var(--font-family);
overflow: hidden;
transition: width 0.2s ease-out, height 0.2s;
width: v-bind("isExpanded ? 'var(--navbar-width)' : 'var(--navbar-width-min)'");
}
/* Checkbox Logic for Collapse */
#nav-toggle { display: none; }
#nav-header {
position: relative;
width: 100%;
min-height: 80px;
background: var(--navbar-dark-primary);
border-radius: 16px 16px 0 0;
z-index: 2;
display: flex;
align-items: center;
padding: 0 16px;
}
#nav-title {
font-size: 1.5rem;
font-weight: bold;
opacity: v-bind("isExpanded ? 1 : 0");
transition: opacity 0.2s;
white-space: nowrap;
color: var(--color-accent-secondary);
/* Prevent title from breaking layout when collapsed */
overflow: hidden;
}
#nav-header hr {
position: absolute;
bottom: 0;
left: 16px;
width: calc(100% - 32px);
border: none;
border-top: 1px solid var(--navbar-dark-secondary);
margin: 0;
}
label[for="nav-toggle"] {
position: absolute;
right: 0;
/*
Ensure the toggle button area is always the size of the min-width
so it remains clickable and consistent in position
*/
width: var(--navbar-width-min);
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
#nav-toggle-burger {
position: relative;
width: 16px;
height: 2px;
background: var(--navbar-light-primary);
border-radius: 99px;
transition: background 0.2s;
}
#nav-toggle-burger:before, #nav-toggle-burger:after {
content: '';
position: absolute;
width: 16px;
height: 2px;
background: var(--navbar-light-primary);
border-radius: 99px;
transition: transform 0.2s;
}
#nav-toggle-burger:before { top: -6px; }
#nav-toggle-burger:after { top: 6px; }
#nav-content {
flex: 1;
background: var(--navbar-dark-primary);
overflow: hidden;
position: relative;
}
/* Highlight Block */
#nav-content-highlight {
position: absolute;
left: 15px;
width: calc(100% - 15px);
background: var(--color-bg-primary);
border-radius: 15px 0 0 15px;
transition: top 0.3s ease-out;
z-index: 0;
}
#nav-content-highlight::before,
#nav-content-highlight::after {
content: '';
position: absolute;
right: 0;
width: 32px;
height: 32px;
border-radius: 50%;
background: transparent;
z-index: 1;
}
#nav-content-highlight::before {
bottom: 100%;
box-shadow: 15px 15px var(--color-bg-primary);
}
#nav-content-highlight::after {
top: 100%;
box-shadow: 15px -15px var(--color-bg-primary);
}
.nav-items-container {
position: absolute;
top: 10%;
height: 80%;
width: 100%;
left: 0;
display: flex;
flex-direction: column;
}
.nav-button {
position: relative;
margin-left: 16px;
flex: 1;
display: flex;
align-items: center;
color: var(--navbar-light-secondary);
cursor: pointer;
z-index: 1;
transition: color 0.2s;
padding-left: 0;
/* Ensure text doesn't overflow when collapsed */
overflow: hidden;
font-family: var(--font-family);
}
.nav-button:hover {
color: var(--navbar-light-primary);
}
.nav-button.active {
color: var(--navbar-dark-primary);
}
.nav-button i {
min-width: 3rem;
text-align: center;
font-size: 1.5rem;
z-index: 2;
/* Fix icon width so it doesn't jump */
flex-shrink: 0;
}
.nav-button span {
opacity: v-bind("isExpanded ? 1 : 0");
transition: opacity 0.2s;
white-space: nowrap;
z-index: 2;
}
/* External Actions (Page 5 & Logout) */
.external-actions {
pointer-events: auto;
position: absolute;
bottom: 5vh;
left: 16px;
display: flex;
flex-direction: column;
gap: 16px;
width: v-bind("isExpanded ? 'var(--navbar-width)' : 'var(--navbar-width-min)'");
align-items: center;
transition: width 0.2s ease-out;
}
.page-5-btn {
width: 100%;
max-width: calc(100% - 32px); /* Account for margin/padding logic */
height: 50px;
justify-content: v-bind("isExpanded ? 'flex-start' : 'center'");
padding: 0;
background: var(--color-accent-secondary);
color: var(--navbar-light-primary);
box-shadow: 0 4px 10px rgba(255, 159, 28, 0.3);
overflow: hidden; /* Prevent text spill */
}
.page-5-btn i {
font-size: 1.5rem;
margin-left: v-bind("isExpanded ? '16px' : '0'");
transition: margin-left 0.2s;
}
.page-5-btn span {
margin-left: 12px;
white-space: nowrap;
}
.page-5-btn.active { border: 2px solid var(--color-contrast-dark); }
.logout-btn {
width: 54px;
height: 54px;
background: rgba(255,255,255,0.5);
border: 1px solid white;
color: var(--navbar-dark-primary);
}
</style>

@ -1,169 +0,0 @@
<template>
<div class="details-container">
<div class="details-card">
<h3 class="task-title">任务详情</h3>
<ul class="task-list">
<li v-for="task in tasks" :key="task.id" class="task-item" @click="showSingleTaskDetail(task)">
<div class="task-header">
<span>任务 {{ task.id }}</span>
<span class="task-status">{{ task.status === 'running' ? '运行中' : '等待中' }}</span>
</div>
<div v-if="task.status === 'running'" class="progress-bar-container">
<div class="progress-bar" :style="{ width: task.progress + '%' }"></div>
</div>
<div class="expanded-detail">
<small>点击查看详情...</small>
</div>
</li>
</ul>
<div class="action-footer">
<button class="action-btn primary" @click="goBack"></button>
<button class="action-btn secondary" @click="goToResources"></button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// Mock Data
const tasks = ref([
{ id: '1', status: 'running', progress: 45 },
{ id: '3', status: 'waiting', progress: 0 },
{ id: 'A', status: 'waiting', progress: 0 }
])
const goBack = () => {
router.back()
}
const goToResources = () => {
router.push('/my-resources')
}
const showSingleTaskDetail = (task) => {
alert(`查看任务 ${task.id} 的详细信息...`)
}
</script>
<style scoped>
.details-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.9);
animation: zoomIn 0.3s ease-out;
}
.details-card {
width: 60%;
max-width: 50rem;
height: 70%;
background: white;
border: 0.125rem solid #000;
padding: 2rem;
display: flex;
flex-direction: column;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.1);
}
.task-title {
font-size: 1.5rem;
text-align: center;
margin-bottom: 2rem;
border-bottom: 1px solid #eee;
padding-bottom: 1rem;
}
.task-list {
list-style: none;
padding: 0;
margin: 0;
flex: 1;
overflow-y: auto;
}
.task-item {
margin-bottom: 1rem;
padding: 1rem;
border: 1px solid #ddd;
background: #fafafa;
cursor: pointer;
transition: background-color 0.2s;
}
.task-item:hover {
background-color: #f0f0f0;
}
.task-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.progress-bar-container {
height: 0.8rem;
background: #eee;
border-radius: 0.4rem;
overflow: hidden;
margin-top: 0.5rem;
}
.progress-bar {
height: 100%;
background: #333;
}
.expanded-detail {
margin-top: 0.5rem;
color: #666;
font-size: 0.9rem;
}
.action-footer {
margin-top: 2rem;
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.action-btn {
padding: 0.8rem 1.5rem;
border: 1px solid #000;
cursor: pointer;
font-size: 1.1rem;
}
.action-btn.primary {
background-color: #333;
color: white;
border: none;
}
.action-btn.secondary {
background-color: white;
color: #333;
}
.action-btn:hover {
opacity: 0.9;
}
@keyframes zoomIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>

@ -1,25 +1,31 @@
<template>
<div class="sidebar-placeholder">
<div class="back-btn-container">
<button class="common-back-btn" @click="$emit('back')"></button>
<!-- 这里的 h-full 确保它撑满左侧区域 -->
<div class="ui-card solid sidebar-card">
<div class="sidebar-header">
<h3 class="task-title">任务队列</h3>
<div class="monitor-dot"></div>
</div>
<ul class="task-list">
<li v-if="tasks.length === 0" class="empty-state">
暂无任务运行
</li>
<li v-for="task in tasks" :key="task.id" class="task-item">
<div class="task-header">
<span class="task-id">#{{ task.id }}</span>
<span class="status-tag" :class="task.status">
{{ task.status === 'running' ? '运行中' : '等待中' }}
</span>
</div>
<div v-if="task.status === 'running'" class="progress-container">
<div class="progress-bar" :style="{ width: task.progress + '%' }"></div>
</div>
</li>
</ul>
<div class="task-card" @click="$emit('details')">
<h3 class="task-title">当前正在执行的任务表</h3>
<ul class="task-list">
<li v-for="task in tasks" :key="task.id" class="task-item">
<div class="task-header">
<span>任务 {{ task.id }}</span>
<span class="task-status">{{ task.status === 'running' ? '运行中' : '等待中' }}</span>
</div>
<div v-if="task.status === 'running'" class="progress-bar-container">
<div class="progress-bar" :style="{ width: task.progress + '%' }"></div>
</div>
</li>
</ul>
<div class="task-footer">
还可提交 {{ remainingSlots }}
</div>
<div class="task-footer">
<span>剩余额度</span>
<span class="quota-num">{{ remainingSlots }}</span>
</div>
</div>
</template>
@ -28,19 +34,121 @@
import { computed } from 'vue'
const props = defineProps({
tasks: {
type: Array,
default: () => []
},
maxSlots: {
type: Number,
default: 5
}
tasks: { type: Array, default: () => [] },
maxSlots: { type: Number, default: 5 }
})
defineEmits(['back', 'details'])
const remainingSlots = computed(() => Math.max(0, props.maxSlots - props.tasks.length))
</script>
const remainingSlots = computed(() => {
return Math.max(0, props.maxSlots - props.tasks.length)
})
</script>
<style scoped>
.sidebar-card {
height: 100%; /* 关键:撑满高度 */
display: flex;
flex-direction: column;
background: #ffffff; /* 强制白底,确保可见性 */
border: 1px solid rgba(0,0,0,0.05);
padding: 24px;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid var(--color-bg-primary);
}
.task-title {
margin: 0;
font-size: 1.1rem;
color: var(--color-contrast-dark);
}
.monitor-dot {
width: 8px;
height: 8px;
background: var(--color-accent-secondary);
border-radius: 50%;
box-shadow: 0 0 10px var(--color-accent-secondary);
}
.task-list {
flex: 1;
list-style: none;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.empty-state {
text-align: center;
color: var(--color-text-muted);
font-size: 0.9rem;
margin-top: 20px;
}
.task-item {
background: var(--color-bg-primary);
border-radius: 12px;
padding: 12px;
border: 1px solid transparent;
transition: all 0.2s;
}
.task-item:hover {
border-color: var(--color-accent-mild);
background: #fff;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85rem;
font-weight: 600;
color: var(--color-text-main);
}
.status-tag {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 99px;
}
.status-tag.running { color: var(--color-accent-secondary); background: rgba(255, 159, 28, 0.1); }
.status-tag.waiting { color: var(--color-text-muted); background: rgba(0,0,0,0.05); }
.progress-container {
height: 4px;
background: rgba(0,0,0,0.1);
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.progress-bar {
height: 100%;
background: var(--color-accent-secondary);
transition: width 0.3s ease;
}
.task-footer {
margin-top: 20px;
padding-top: 15px;
border-top: 2px solid var(--color-bg-primary);
display: flex;
justify-content: space-between;
align-items: center;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.quota-num {
font-weight: 700;
color: var(--color-contrast-dark);
font-size: 1.2rem;
}
</style>

@ -1,6 +1,8 @@
import { createApp } from 'vue'
import './Style.css'
import './style.css'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
const app = createApp(App)
app.use(router)
app.mount('#app')

@ -1,176 +1,77 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue'),
meta: { requiresAuth: false, hideNavBar: true }
},
{
path: '/register',
name: 'Register',
component: () => import('../views/Register.vue'),
meta: { requiresAuth: false, hideNavBar: true }
},
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/principle',
name: 'PrincipleDiagram',
component: () => import('../views/home-subpages/PrincipleDiagram.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/sample',
name: 'SamplePreview',
component: () => import('../views/home-subpages/SamplePreview.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/paper',
name: 'PaperSupport',
component: () => import('../views/home-subpages/PaperSupport.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/general-protection',
name: 'GeneralProtection',
component: () => import('../views/GeneralProtection.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/universal-mode',
name: 'UniversalMode',
component: () => import('../views/general-protect-subpages/UniversalMode.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/quick-mode',
name: 'QuickMode',
component: () => import('../views/general-protect-subpages/QuickMode.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/topic-protection',
name: 'TopicProtection',
component: () => import('../views/TopicProtection.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/topic-protection/style-transfer',
name: 'AntiStyleTransfer',
component: () => import('../views/topic-protect-subpages/AntiStyleTransfer.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/effect-validation',
name: 'EffectValidation',
component: () => import('../views/EffectValidation.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/effect-validation/fine-tuning',
name: 'FineTuning',
component: () => import('../views/effect-validate-subpages/FineTuning.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/effect-validation/metrics',
name: 'MetricsComparison',
component: () => import('../views/effect-validate-subpages/MetricsComparison.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/effect-validation/heatmap',
name: 'HeatmapComparison',
component: () => import('../views/effect-validate-subpages/HeatmapComparison.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/my-resources',
name: 'MyResources',
component: () => import('../views/MyResources.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/my-resources/tasks',
name: 'MyTaskResources',
component: () => import('../views/my-resources-subpages/MyTaskResources.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/my-resources/protected-images',
name: 'ProtectedImages',
component: () => import('../views/my-resources-subpages/ProtectedImages.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/my-resources/validation-results',
name: 'ValidationResults',
component: () => import('../views/my-resources-subpages/ValidationResults.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/account',
name: 'Account',
component: () => import('../views/AccountCenter.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/account/edit-profile',
name: 'EditProfile',
component: () => import('../views/account-center-subpages/EditProfile.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/account/change-password',
name: 'ChangePassword',
component: () => import('../views/account-center-subpages/ChangePassword.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/account/admin-users',
name: 'AdminUserManage',
component: () => import('../views/account-center-subpages/AdminUserManage.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/account/config',
name: 'UserConfig',
component: () => import('../views/account-center-subpages/UserConfig.vue'),
meta: { requiresAuth: true, hideNavBar: false }
},
{
path: '/task-details',
name: 'TaskDetails',
component: () => import('../components/TaskDetails.vue'),
meta: { requiresAuth: true, hideNavBar: false }
}
]
const router = createRouter({
history: createWebHistory(),
routes
routes: [
{
path: '/login',
name: 'Login',
component: () => import('../views/LoginView.vue')
},
{
path: '/',
name: 'Main',
component: () => import('../views/MainFlow.vue'),
children: [
// 子页面路由 - 按主页面分组
{
path: 'home/PrincipleDiagram',
name: 'PrincipleDiagram',
component: () => import('../views/home/subpages/PrincipleDiagram.vue'),
meta: { parent: 'home' }
},
{
path: 'home/SamplePreview',
name: 'SamplePreview',
component: () => import('../views/home/subpages/SamplePreview.vue'),
meta: { parent: 'home' }
},
{
path: 'home/PaperSupport',
name: 'PaperSupport',
component: () => import('../views/home/subpages/PaperSupport.vue'),
meta: { parent: 'home' }
},
{
path: 'page1/UniversalMode',
name: 'UniversalMode',
component: () => import('../views/Page1/subpages/UniversalMode.vue'),
meta: { parent: 'page1' }
},
{
path: 'page1/QuickMode',
name: 'QuickMode',
component: () => import('../views/Page1/subpages/QuickMode.vue'),
meta: { parent: 'page1' }
},
{
path: 'page2/:subpage',
name: 'Page2Sub',
component: () => import('../views/Page2/subpages/SubpageContainer.vue'),
meta: { parent: 'page2' }
},
{
path: 'page3/:subpage',
name: 'Page3Sub',
component: () => import('../views/Page3/subpages/SubpageContainer.vue'),
meta: { parent: 'page3' }
},
{
path: 'page4/:subpage',
name: 'Page4Sub',
component: () => import('../views/Page4/subpages/SubpageContainer.vue'),
meta: { parent: 'page4' }
},
{
path: 'page5/:subpage',
name: 'Page5Sub',
component: () => import('../views/Page5/subpages/SubpageContainer.vue'),
meta: { parent: 'page5' }
}
]
}
]
})
export default router
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('access_token')
const requiresAuth = to. meta.requiresAuth !== false
if (requiresAuth && !token) {
next('/login')
} else if (token && (to.path === '/login' || to.path === '/register')) {
next('/')
} else {
next()
}
})
export default router

@ -1,510 +0,0 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { authGetProfile, authLogout, getUserStats } from '@/api/index'
const router = useRouter()
const userData = ref({
id: '',
username: '加载中...',
email: '',
role: 'user'
})
const userStats = ref(null)
const statsLoading = ref(true)
const profileLoading = ref(true)
const goBack = () => router.push('/')
const goEditProfile = () => router.push('/account/edit-profile')
const goChangePassword = () => router.push('/account/change-password')
const goUserConfig = () => router.push('/account/config')
const goToAdminManage = () => router.push('/account/admin-users')
const fetchUserProfile = async () => {
profileLoading.value = true
try {
const res = await authGetProfile()
if (res && res.user) {
userData.value = res.user
}
} catch (error) {
console.error('获取用户信息失败', error)
userData.value.username = '获取失败'
} finally {
profileLoading.value = false
}
}
const fetchUserStats = async () => {
statsLoading.value = true
try {
const res = await getUserStats()
if (res && res.stats) {
userStats.value = res.stats
}
} catch (error) {
console.error('获取用户统计失败', error)
userStats.value = null
} finally {
statsLoading.value = false
}
}
const handleLogout = async () => {
if (!confirm('确定要退出登录吗?')) return
try {
await authLogout()
} catch (e) {
console.warn('强制登出')
} finally {
localStorage.removeItem('access_token')
localStorage.removeItem('user_info')
router.push('/login')
}
}
const completionRate = computed(() => {
if (!userStats.value || userStats.value.total_tasks === 0) return 0
return Math.round((userStats.value.completed_tasks / userStats.value.total_tasks) * 100)
})
const successRate = computed(() => {
if (!userStats.value) return 0
const finished = userStats.value.completed_tasks + userStats.value.failed_tasks
if (finished === 0) return 0
return Math.round((userStats.value.completed_tasks / finished) * 100)
})
onMounted(() => {
fetchUserProfile()
fetchUserStats()
})
</script>
<template>
<div class="shared-page-container account-bg">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="web-main">
<aside class="sidebar-panel">
<div class="user-card">
<div class="avatar">
{{ userData.username ? userData.username.slice(0, 1).toUpperCase() : 'U' }}
</div>
<h3 class="username">{{ userData.username }}</h3>
<span class="vip-badge" v-if="userData.role === 'admin'"></span>
<span class="vip-badge user-badge" v-else></span>
</div>
<div class="quick-actions">
<button class="action-btn" @click="goEditProfile"></button>
<button class="action-btn" @click="goChangePassword"></button>
<button class="action-btn" @click="goUserConfig"></button>
<button class="action-btn logout" @click="handleLogout">退</button>
</div>
</aside>
<main class="content-panel">
<section class="info-section">
<h2 class="section-title">基本信息</h2>
<div class="info-grid" v-if="!profileLoading">
<div class="info-item">
<span class="label">用户ID</span>
<span class="value">{{ userData.id || '-' }}</span>
</div>
<div class="info-item">
<span class="label">用户名</span>
<span class="value">{{ userData.username }}</span>
</div>
<div class="info-item">
<span class="label">邮箱</span>
<span class="value">{{ userData.email || '未设置' }}</span>
</div>
<div class="info-item">
<span class="label">账户角色</span>
<span class="value">{{ userData.role === 'admin' ? '管理员' : '普通用户' }}</span>
</div>
</div>
<div v-else class="empty-hint">加载用户信息中...</div>
</section>
<section class="info-section">
<div class="section-header">
<h2 class="section-title no-border">使用统计</h2>
<button class="action-btn refresh-btn" @click="fetchUserStats" :disabled="statsLoading">
{{ statsLoading ? '刷新中.. .' : '刷新' }}
</button>
</div>
<div v-if="statsLoading" class="empty-hint">...</div>
<div v-else-if="userStats" class="stats-content">
<div class="stats-cards">
<div class="stat-card">
<div class="stat-val">{{ userStats.total_tasks }}</div>
<div class="stat-label">总任务数</div>
</div>
<div class="stat-card">
<div class="stat-val text-green">{{ userStats.completed_tasks }}</div>
<div class="stat-label">已完成</div>
</div>
<div class="stat-card">
<div class="stat-val text-orange">{{ userStats.processing_tasks }}</div>
<div class="stat-label">处理中</div>
</div>
<div class="stat-card">
<div class="stat-val text-red">{{ userStats.failed_tasks }}</div>
<div class="stat-label">失败</div>
</div>
<div class="stat-card">
<div class="stat-val">{{ userStats.total_images }}</div>
<div class="stat-label">处理图片</div>
</div>
</div>
<div class="progress-section">
<div class="progress-row">
<span class="progress-label">任务完成率</span>
<div class="progress-bar-container">
<div class="progress-bar" :style="{ width: completionRate + '%' }"></div>
</div>
<span class="progress-val">{{ completionRate }}%</span>
</div>
<div class="progress-row">
<span class="progress-label">任务成功率</span>
<div class="progress-bar-container">
<div class="progress-bar" :style="{ width: successRate + '%' }"></div>
</div>
<span class="progress-val">{{ successRate }}%</span>
</div>
</div>
<p v-if="userStats.total_tasks === 0" class="dataset-summary">
您还没有提交过任何任务快去体验图像防护功能吧
</p>
<p v-else class="dataset-summary">
您已累计提交 <span class="highlight">{{ userStats.total_tasks }}</span> 个任务
处理了 <span class="highlight">{{ userStats.total_images }}</span> 张图片
<span v-if="userStats.processing_tasks > 0">
当前有 <span class="highlight">{{ userStats.processing_tasks }}</span> 个任务正在处理中
</span>
</p>
</div>
<div v-else class="empty-hint">
统计数据加载失败
<button class="action-btn" style="margin-top: 1rem;" @click="fetchUserStats"></button>
</div>
</section>
<section v-if="userData.role === 'admin'" class="info-section admin-section">
<h2 class="section-title">管理员功能</h2>
<div class="admin-grid">
<div class="admin-item" @click="goToAdminManage"></div>
</div>
</section>
</main>
</div>
</div>
</template>
<style scoped>
.account-bg {
background-color: #f5f5f5;
}
.web-main {
display: flex;
max-width: 1200px;
margin: -20rem auto 2rem auto;
width: 100%;
padding: 0 2rem;
gap: 2rem;
}
.sidebar-panel {
width: 280px;
flex-shrink: 0;
}
.content-panel {
flex: 1;
display: flex;
flex-direction: column;
gap: 1. 5rem;
}
.user-card {
background: white;
border: 1px solid #ddd;
padding: 2rem;
text-align: center;
margin-bottom: 1rem;
}
.avatar {
width: 80px;
height: 80px;
background: #333;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.5rem;
margin: 0 auto 1rem;
}
.username {
margin: 0 0 0.5rem 0;
font-size: 1.3rem;
}
.vip-badge {
display: inline-block;
background: gold;
color: #333;
padding: 0.2rem 0.6rem;
border-radius: 3px;
font-size: 0.8rem;
font-weight: bold;
}
.vip-badge.user-badge {
background: #e0e0e0;
}
.quick-actions {
display: flex;
flex-direction: column;
gap: 0. 5rem;
}
.action-btn {
width: 100%;
padding: 0.75rem 1rem;
font-size: 0.95rem;
border: 1px solid #ddd;
background: white;
cursor: pointer;
text-align: left;
}
.action-btn:hover {
background: #f9f9f9;
}
.action-btn .logout {
color: #d32f2f;
border-color: #ffcccc;
}
.action-btn .logout:hover {
background: #fff5f5;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.refresh-btn {
width: auto;
text-align: center;
}
.info-section {
background: white;
border: 1px solid #ddd;
padding: 1.5rem 2rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1. 5rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #eee;
}
.section-title {
margin: 0 0 1. 5rem 0;
font-size: 1.1rem;
padding-bottom: 0. 75rem;
border-bottom: 1px solid #eee;
}
.section-title.no-border {
margin: 0;
padding: 0;
border: none;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1. 5rem;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0. 3rem;
}
.label {
color: #888;
font-size: 0.85rem;
}
.value {
font-weight: 500;
font-size: 1rem;
}
.stats-content {
display: flex;
flex-direction: column;
gap: 1. 5rem;
}
.stats-cards {
display: flex;
justify-content: space-between;
gap: 1rem;
}
.stat-card {
flex: 1;
text-align: center;
padding: 1rem;
background: #fafafa;
border: 1px solid #eee;
}
.stat-val {
font-size: 1.5rem;
font-weight: bold;
color: #333;
}
.stat-label {
font-size: 0.85rem;
color: #666;
margin-top: 0.25rem;
}
.text-green {
color: #2e7d32;
}
.text-orange {
color: #f57c00;
}
.text-red {
color: #d32f2f;
}
.progress-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
background: #fafafa;
border: 1px solid #eee;
}
.progress-row {
display: flex;
align-items: center;
gap: 1rem;
}
.progress-label {
width: 100px;
font-size: 0.9rem;
color: #666;
}
.progress-val {
width: 50px;
font-size: 0.9rem;
font-weight: bold;
text-align: right;
}
.admin-section {
background: #fffef5;
}
.admin-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.admin-item {
background: #fafafa;
border: 1px solid #eee;
padding: 1rem;
text-align: center;
font-size: 0.9rem;
color: #555;
cursor: pointer;
}
.admin-item:hover {
background: #f0f0f0;
}
@media (max-width: 1024px) {
.web-main {
flex-direction: column;
margin-top: -10rem;
}
.sidebar-panel {
width: 100%;
}
.stats-cards {
flex-wrap: wrap;
}
.stat-card {
min-width: calc(33% - 1rem);
}
.admin-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.stat-card {
min-width: calc(50% - 0.5rem);
}
.info-grid {
grid-template-columns: 1fr;
}
.progress-row {
flex-wrap: wrap;
}
.progress-label {
width: 100%;
}
.progress-val {
width: auto;
}
}
</style>

@ -1,131 +0,0 @@
<template>
<!-- 本地覆盖背景色 -->
<div class="shared-page-container effect-bg">
<!-- 返回按钮 -->
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<!-- 卡片网格 -->
<div class="shared-card-grid">
<!-- Card 1 -->
<div class="shared-card">
<h3 class="shared-card-title">微调生图</h3>
<div class="shared-card-visual visual-tuning">
<span>(背景图设计)</span>
</div>
<div class="shared-action-group">
<button class="shared-btn" @click="openModal('tuning')"></button>
<button class="shared-btn shared-btn-primary" @click="startValidation('tuning')"></button>
</div>
</div>
<!-- Card 2 -->
<div class="shared-card">
<h3 class="shared-card-title">数据指标对比</h3>
<div class="shared-card-visual visual-metrics">
<span>(背景图设计)</span>
</div>
<div class="shared-action-group">
<button class="shared-btn" @click="openModal('metrics')"></button>
<button class="shared-btn shared-btn-primary" @click="startValidation('metrics')"></button>
</div>
</div>
<!-- Card 3 -->
<div class="shared-card">
<h3 class="shared-card-title">热力图与频域对比</h3>
<div class="shared-card-visual visual-heatmap">
<span>(背景图设计)</span>
</div>
<div class="shared-action-group">
<button class="shared-btn" @click="openModal('heatmap')"></button>
<button class="shared-btn shared-btn-primary" @click="startValidation('heatmap')"></button>
</div>
</div>
</div>
<!-- Modal -->
<div v-if="isModalOpen" class="shared-modal-overlay" @click="closeModal">
<div class="shared-modal-content" @click.stop>
<h3 class="shared-modal-title">{{ currentModalTitle }}</h3>
<p class="shared-modal-text">{{ currentModalDesc }}</p>
<button class="shared-modal-close-btn" @click="closeModal"></button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isModalOpen = ref(false)
const currentModalTitle = ref('')
const currentModalDesc = ref('')
const modalContent = {
tuning: {
title: '微调生图',
desc: '模拟攻击者对图片进行微调生成的全过程,直观展示防护前后的生成效果差异。'
},
metrics: {
title: '数据指标对比',
desc: '通过PSNR、SSIM等专业图像质量评估指标量化分析防护对图像质量的影响。'
},
heatmap: {
title: '热力图与频域对比',
desc: '利用Attention Map热力图和频谱分析揭示肉眼难以察觉的隐形扰动分布。'
}
}
const goBack = () => {
router.push('/')
}
const openModal = (type) => {
const content = modalContent[type]
currentModalTitle.value = content.title
currentModalDesc.value = content.desc
isModalOpen.value = true
}
const closeModal = () => {
isModalOpen.value = false
}
const startValidation = (type) => {
if (type === 'tuning') {
router.push('/effect-validation/fine-tuning')
} else if (type === 'metrics') {
router.push('/effect-validation/metrics')
} else if (type === 'heatmap') {
router.push('/effect-validation/heatmap')
} else {
// Future: Navigate to specific subpage
alert(`即将进入 ${modalContent[type].title} 验证页`)
}
}
</script>
<style scoped>
.effect-bg {
background-color: #7ebfff58; /* 特色背景色 */
}
/* 特色渐变背景样式 */
.visual-tuning {
background: linear-gradient(135deg, #fff 0%, #e6f7ff 100%);
}
.visual-metrics {
background: linear-gradient(135deg, #fff 0%, #fff0f5 100%);
}
.visual-heatmap {
background: linear-gradient(135deg, #fff 0%, #f0fff4 100%);
}
</style>

@ -1,98 +0,0 @@
<template>
<div class="shared-page-container general-bg">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="shared-card-grid">
<!-- Card 1 -->
<div class="shared-card large-card">
<h2 class="shared-card-title mode-title">快速模式</h2>
<div class="shared-card-content mode-desc">
<p>背景图案好好设计</p>
</div>
<div class="shared-action-group">
<button class="shared-btn" @click="showDetails('quick')"></button>
<button class="shared-btn" @click="startMode('quick')"></button>
</div>
</div>
<!-- Card 2 -->
<div class="shared-card large-card">
<h2 class="shared-card-title mode-title">通用模式</h2>
<div class="shared-card-content mode-desc">
<p>背景图案好好设计</p>
</div>
<div class="shared-action-group">
<button class="shared-btn" @click="showDetails('universal')"></button>
<button class="shared-btn" @click="startMode('universal')"></button>
</div>
</div>
</div>
<!-- Modal -->
<div v-if="showModal" class="shared-modal-overlay" @click="closeModal">
<div class="shared-modal-content" @click.stop>
<h3 class="shared-modal-title">{{ modalTitle }}</h3>
<p class="shared-modal-text">{{ modalContent }}</p>
<p class="shared-modal-text">更多详细信息占位符...</p>
<button class="shared-modal-close-btn" @click="closeModal"></button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const showModal = ref(false)
const modalTitle = ref('')
const modalContent = ref('')
const goBack = () => {
router.push('/')
}
const showDetails = (mode) => {
modalTitle.value = mode === 'quick' ? '快速模式详情' : '通用模式详情'
modalContent.value = '这里是该模式的详细介绍内容(占位符)。此模式采用了先进的算法来提供高效的防护效果,具体操作步骤和适用场景请参考后续文档...'
showModal.value = true
}
const closeModal = () => {
showModal.value = false
}
const startMode = (mode) => {
console.log(`Start ${mode} mode`)
if (mode === 'universal') {
router.push('/universal-mode')
} else if (mode === 'quick') {
router.push('/quick-mode')
}
}
</script>
<style scoped>
.general-bg {
background-color: #26975e76; /* 特色背景色 */
}
.large-card {
width: 20rem;
min-height: 30rem;
}
.mode-title {
font-size: 2rem;
font-family: 'KaiTi', 'Kaiti SC', serif;
}
.mode-desc {
font-style: italic;
color: #888;
}
</style>

@ -1,106 +0,0 @@
<template>
<div class="shared-page-container home-bg">
<div class="demo-stats-bar" v-if="demoStats">
<span class="demo-stat-item">演示图片: <strong>{{ demoStats.original_images }}</strong></span>
<span class="demo-stat-item">支持算法: <strong>{{ demoStats.supported_algorithms }}</strong></span>
<span class="demo-stat-item">评估指标: <strong>{{ demoStats. evaluation_metrics }}</strong></span>
</div>
<div class="shared-card-grid">
<div class="shared-card large-card" @click="navigateTo('principle')">
<h2 class="shared-card-title">原理图解</h2>
<div class="shared-card-content">
<p>了解防护算法原理与评估指标</p>
</div>
<p class="sub-text">点击打开</p>
</div>
<div class="shared-card large-card" @click="navigateTo('sample')">
<h2 class="shared-card-title">样例预览</h2>
<div class="shared-card-content">
<p>查看演示图片与防护效果对比</p>
</div>
<p class="sub-text">点击打开</p>
</div>
<div class="shared-card large-card" @click="navigateTo('paper')">
<h2 class="shared-card-title">论文支持</h2>
<div class="shared-card-content">
<p>相关学术论文与技术文档</p>
</div>
<p class="sub-text">点击打开</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getDemoStats } from '@/api/index'
const router = useRouter()
const demoStats = ref(null)
const navigateTo = (type) => {
if (type === 'principle') {
router.push('/principle')
} else if (type === 'sample') {
router.push('/sample')
} else if (type === 'paper') {
router.push('/paper')
} else {
alert('This page is coming soon!')
}
}
const fetchDemoStats = async () => {
try {
const res = await getDemoStats()
if (res && res.demo_stats) {
demoStats. value = res.demo_stats
}
} catch (error) {
console.error('获取演示统计失败', error)
}
}
onMounted(() => {
fetchDemoStats()
})
</script>
<style scoped>
.home-bg {
background-color: #deb64a8c;
}
.demo-stats-bar {
position: absolute;
top: 1rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 2rem;
background: rgba(255, 255, 255, 0.9);
padding: 0.5rem 1. 5rem;
border-radius: 4px;
border: 1px solid #ddd;
font-size: 0.9rem;
color: #555;
}
.demo-stat-item strong {
color: #333;
}
.large-card {
width: 22rem;
}
.sub-text {
color: blue;
font-size: 0.9rem;
margin-top: 1rem;
}
</style>

@ -1,94 +0,0 @@
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { authLogin } from '@/api/index'
const router = useRouter()
const form = ref({ username: '', password: '' })
const loading = ref(false)
const handleLogin = async () => {
if (!form.value.username || !form.value.password) {
alert('请填写完整用户名和密码')
return
}
loading.value = true
try {
// : { message: '', access_token: '...', user: {...} }
// request.js data
const res = await authLogin(form.value)
if (res.access_token) {
localStorage.setItem('access_token', res.access_token)
// 便
localStorage.setItem('user_info', JSON.stringify(res.user))
//
router.push('/')
} else {
throw new Error('响应缺失 Token')
}
} catch (error) {
console.error('Login Error:', error)
// request.js
} finally {
loading.value = false
}
}
</script>
<template>
<div class="shared-page-container auth-bg">
<div class="form-container auth-card">
<h2 class="page-title">用户登录</h2>
<div class="form-row">
<div class="form-group full-width">
<label>用户名</label>
<input type="text" v-model="form.username" class="std-input" placeholder="请输入用户名"
@keyup.enter="handleLogin" />
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label>密码</label>
<input type="password" v-model="form.password" class="std-input" placeholder="请输入密码"
@keyup.enter="handleLogin" />
</div>
</div>
<div class="form-actions">
<button class="start-btn" @click="handleLogin" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
<p class="hint-text" style="margin-top: 1rem; text-align: center;">
还没有账号 <router-link to="/register">去注册</router-link>
</p>
</div>
</div>
</div>
</template>
<style scoped>
.auth-bg {
background-color: #e0e0e0;
}
.auth-card {
max-width: 30rem;
min-height: auto;
padding: 3rem;
}
.full-width {
width: 100%;
}
.start-btn {
width: 100%;
}
.start-btn:disabled {
background-color: #999;
cursor: not-allowed;
}
</style>

@ -0,0 +1,113 @@
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const handleLogin = () => {
router.push('/')
}
</script>
<template>
<div class="login-container">
<div class="ui-card glass login-card">
<div class="brand-logo">MUSE</div>
<h1>Welcome Back</h1>
<p>Please login to your dashboard.</p>
<div class="form-group">
<input type="text" placeholder="Username" class="input-field" />
<input type="password" placeholder="Password" class="input-field" />
</div>
<button class="ui-btn gradient rect full-width" @click="handleLogin">
Login
</button>
</div>
</div>
</template>
<style scoped>
.login-container {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-contrast-dark);
position: relative;
overflow: hidden;
}
/* Decorative background blobs */
.login-container::before {
content: '';
position: absolute;
width: 600px;
height: 600px;
background: var(--color-accent-primary);
border-radius: 50%;
top: -100px;
left: -100px;
filter: blur(80px);
opacity: 0.5;
}
.login-container::after {
content: '';
position: absolute;
width: 500px;
height: 500px;
background: var(--color-accent-secondary);
border-radius: 50%;
bottom: -50px;
right: -50px;
filter: blur(100px);
opacity: 0.4;
}
.login-card {
width: 400px;
padding: 40px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
z-index: 10;
background: rgba(255,255,255,0.9); /* Lighter glass for login */
}
.brand-logo {
font-weight: 900;
font-size: 1.5rem;
letter-spacing: 2px;
margin-bottom: 20px;
color: var(--color-contrast-dark);
}
.form-group {
width: 100%;
margin: 30px 0;
display: flex;
flex-direction: column;
gap: 15px;
}
.input-field {
width: 100%;
padding: 15px;
border-radius: 12px;
border: 1px solid #ddd;
background: #f9f9f9;
outline: none;
transition: border-color 0.2s;
}
.input-field:focus {
border-color: var(--color-accent-secondary);
}
.full-width {
width: 100%;
}
</style>

@ -0,0 +1,339 @@
<script setup>
import { ref, provide, onMounted, onUnmounted, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import NavBar from '../components/NavBar.vue'
import Button from '../components/Button.vue'
import HomePage from './home/HomePage.vue'
import Page1 from './Page1/Page1.vue'
import Page2 from './Page2/Page2.vue'
import Page3 from './Page3/Page3.vue'
import Page4 from './Page4/Page4.vue'
import Page5 from './Page5/Page5.vue'
const router = useRouter()
const route = useRoute()
/* 导航状态 */
const currentSection = ref('home')
const showSubpage = ref(false)
const isNavExpanded = ref(false)
// UI
const isManualClosing = ref(false)
/* 页面配置 */
const sections = ['home', 'page1', 'page2', 'page3', 'page4']
const sectionMap = {
home: 0,
page1: 1,
page2: 2,
page3: 3,
page4: 4
}
const currentIndex = computed(() => sectionMap[currentSection.value])
/* ---------------------------------------------------------
路由监听核心修复
--------------------------------------------------------- */
// /退
watch(
() => route.path,
(newPath) => {
// 1.
if (newPath === '/' && showSubpage.value && !isManualClosing.value) {
// 退
// URL UI
showSubpage.value = false
}
// 2.
else if (newPath !== '/' && !showSubpage.value) {
showSubpage.value = true
}
//
if (newPath === '/') {
isManualClosing.value = false
}
}
)
/* ---------------------------------------------------------
导航与关闭逻辑
--------------------------------------------------------- */
//
const closeSubpage = () => {
// watch
isManualClosing.value = true
// 1.
showSubpage.value = false
// 2.
setTimeout(() => {
router.push('/')
// isManualClosing watch watch
}, 300)
}
//
const openSubpage = (parentPage, subpageName) => {
showSubpage.value = true
router.push(`/${parentPage}/${subpageName}`)
}
//
const handleNavigate = (id) => {
if (id === currentSection.value) {
if (showSubpage.value) closeSubpage()
return
}
const performSwitch = () => {
if (id === 'page5') {
currentSection.value = 'page5'
return
}
if (currentSection.value === 'page5') {
currentSection.value = id
return
}
currentSection.value = id
}
if (showSubpage.value) {
closeSubpage()
setTimeout(() => {
performSwitch()
}, 300)
} else {
performSwitch()
}
}
const handleNavToggle = (expanded) => {
isNavExpanded.value = expanded
}
const handleLogout = () => {
router.push('/login')
}
provide('openSubpage', openSubpage)
/* ---------------------------------------------------------
滚动逻辑
--------------------------------------------------------- */
const SCROLL_COOLDOWN = 250
const SCROLL_THRESHOLD = 50
let lastScrollTime = 0
let scrollAccumulator = 0
let resetTimer = null
const handleWheel = (e) => {
//
if (currentSection.value === 'page5' || showSubpage.value) return
const now = Date.now()
if (now - lastScrollTime < SCROLL_COOLDOWN) {
e.preventDefault()
return
}
e.preventDefault()
scrollAccumulator += e.deltaY
clearTimeout(resetTimer)
resetTimer = setTimeout(() => {
scrollAccumulator = 0
}, 150)
if (Math.abs(scrollAccumulator) > SCROLL_THRESHOLD) {
let nextIndex = currentIndex.value
if (scrollAccumulator > 0) {
if (nextIndex < sections.length - 1) nextIndex++
} else {
if (nextIndex > 0) nextIndex--
}
if (nextIndex !== currentIndex.value) {
handleNavigate(sections[nextIndex])
lastScrollTime = now
}
scrollAccumulator = 0
}
}
//
const checkRoute = () => {
if (route.params.subpage) {
showSubpage.value = true
} else {
showSubpage.value = false
}
}
onMounted(() => {
checkRoute()
window.addEventListener('wheel', handleWheel, { passive: false })
})
onUnmounted(() => {
window.removeEventListener('wheel', handleWheel)
})
</script>
<template>
<div class="layout-main">
<NavBar
:current-section="currentSection"
@navigate="handleNavigate"
@logout="handleLogout"
@toggle="handleNavToggle"
/>
<div class="layout-content" :class="{ 'nav-expanded': isNavExpanded }">
<!-- 瀑布流容器 -->
<div class="scroll-container" v-if="currentSection !== 'page5'">
<div
v-for="(pageId, index) in sections"
:key="pageId"
class="scroll-section"
:class="{
'is-active': index === currentIndex,
'is-prev': index < currentIndex,
'is-next': index > currentIndex
}"
>
<component :is="
pageId === 'home' ? HomePage :
pageId === 'page1' ? Page1 :
pageId === 'page2' ? Page2 :
pageId === 'page3' ? Page3 :
Page4
" />
</div>
</div>
<!-- Page 5 -->
<div v-else class="page-standalone">
<Page5 />
</div>
</div>
<!-- 子页面遮罩 (Fade 动画) -->
<Transition name="fade">
<div v-if="showSubpage" class="subpage-wrapper" :class="{ 'nav-expanded': isNavExpanded }">
<Button @close="closeSubpage" />
<router-view />
</div>
</Transition>
</div>
</template>
<style scoped>
.layout-main {
display: flex;
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
}
.layout-content {
flex: 1;
height: 100%;
position: relative;
margin-left: 100px;
transition: margin-left 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.layout-content.nav-expanded {
margin-left: 280px;
}
.scroll-container {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
/* 主页面布局 */
.scroll-section {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 2vh 2vw;
transition: transform 0.7s cubic-bezier(0.19, 1, 0.22, 1), opacity 0.5s ease;
will-change: transform;
z-index: 1;
}
.scroll-section.is-active {
transform: translateY(0);
opacity: 1;
z-index: 10;
pointer-events: auto;
}
.scroll-section.is-prev {
transform: translateY(-100%);
opacity: 0.5;
pointer-events: none;
z-index: 0;
}
.scroll-section.is-next {
transform: translateY(100%);
opacity: 0.5;
pointer-events: none;
z-index: 0;
}
.page-standalone {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background: var(--color-bg-secondary);
z-index: 20;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 子页面样式 */
.subpage-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 50;
background: var(--color-bg-primary);
padding-left: 100px;
transition: padding-left 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
box-shadow: -20px 0 40px rgba(0,0,0,0.1);
}
.subpage-wrapper.nav-expanded {
padding-left: 280px;
}
/* Fade 动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
</style>

@ -1,67 +0,0 @@
<template>
<div class="shared-page-container my-resources-bg">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="shared-card-grid">
<div class="shared-card large-card" @click="goTo('tasks')">
<h2 class="shared-card-title">我的任务</h2>
<div class="shared-card-content">
<p>任务资源总览占位图区域</p>
</div>
</div>
<div class="shared-card large-card" @click="goTo('images')">
<h2 class="shared-card-title">已防护图片</h2>
<div class="shared-card-content">
<p>展示已防护图片资源占位图区域</p>
</div>
</div>
<div class="shared-card large-card" @click="goTo('results')">
<h2 class="shared-card-title">效果验证结果</h2>
<div class="shared-card-content">
<p>查看验证结果相关资源占位图区域</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const goBack = () => {
router.push('/')
}
const goTo = (type) => {
if (type === 'tasks') {
router.push('/my-resources/tasks')
} else if (type === 'images') {
router.push('/my-resources/protected-images')
} else if (type === 'results') {
router.push('/my-resources/validation-results')
}
}
</script>
<style scoped>
.my-resources-bg {
background-color: #e24a4a61; /* 特色背景色 */
}
.large-card {
width: 22rem;
}
.sub-text {
color: blue;
font-size: 0.9rem;
margin-top: 1rem;
}
</style>

@ -0,0 +1,263 @@
<template>
<div class="view-container">
<div class="header-row">
<h1 class="page-header">通用防护</h1>
<p class="header-desc">General Image Privacy Protection</p>
</div>
<div class="card-grid">
<!-- 主卡片通用模式 (Universal Mode) -->
<div class="ui-card solid interactive main-card" @click="OpenUniversal">
<div class="card-bg-icon"><i class="fas fa-sliders-h"></i></div>
<div class="content">
<div class="badge-pill">Advanced</div>
<h2>通用模式</h2>
<p>支持自定义选择加密算法AdvNoise, Mist等与扰动强度适合需要精细化控制防护效果的场景</p>
<div class="feature-list">
<span><i class="fas fa-check"></i> 多算法支持</span>
<span><i class="fas fa-check"></i> 强度可调</span>
<span><i class="fas fa-check"></i> 参数配置</span>
</div>
<button class="ui-btn gradient rect mt-auto">
进入专家配置
<i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
<!-- 右侧列 -->
<div class="right-col">
<!-- 快速模式 (Quick Mode) -->
<div class="ui-card gradient interactive sub-card" @click="OpenQuick">
<div class="icon-circle">
<i class="fas fa-bolt"></i>
</div>
<div class="text-group">
<h3>快速模式</h3>
<p>系统自动推荐最佳配置一键上传即可防护</p>
</div>
</div>
<!-- 系统状态 (算法库) -->
<div class="ui-card glass sub-card info-only">
<div class="icon-circle dark">
<i class="fas fa-microchip"></i>
</div>
<div class="text-group">
<h3>算法库状态</h3>
<div class="stat-row">
<span class="big-num">4</span>
<span class="stat-desc">Active Algorithms</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue'
const openSubpage = inject('openSubpage')
const OpenUniversal = () => {
openSubpage('page1', 'UniversalMode')
}
const OpenQuick = () => {
openSubpage('page1', 'QuickMode')
}
</script>
<style scoped>
.view-container {
top: 5%;
width: 100%;
height: 90%;
display: flex;
flex-direction: column;
}
.header-row {
flex: 0 0 auto;
margin-bottom: var(--space-md);
}
.page-header {
font-size: 2.5rem;
color: var(--color-contrast-dark);
margin-bottom: 5px;
}
.header-desc {
color: var(--color-text-muted);
font-size: 1.1rem;
}
.card-grid {
flex: 1;
display: grid;
grid-template-columns: 1.6fr 1fr; /* 左侧稍宽 */
gap: var(--space-md);
min-height: 0;
}
/* === 主卡片样式 === */
.main-card {
padding: var(--space-xl);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
transition: all 0.3s;
}
.main-card:hover {
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
transform: translateY(-5px);
}
.card-bg-icon {
position: absolute;
right: -20px;
bottom: -20px;
font-size: 15rem;
color: var(--color-text-main);
opacity: 0.03;
pointer-events: none;
transition: transform 0.5s;
}
.main-card:hover .card-bg-icon {
transform: rotate(-10deg) scale(1.1);
}
.content {
position: relative;
z-index: 2;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.badge-pill {
background: var(--color-contrast-dark);
color: #fff;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
margin-bottom: 20px;
text-transform: uppercase;
letter-spacing: 1px;
}
.main-card h2 {
font-size: 2.2rem;
margin-bottom: 15px;
color: var(--color-text-main);
}
.main-card p {
font-size: 1.1rem;
color: var(--color-text-muted);
max-width: 90%;
margin-bottom: 30px;
}
.feature-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 30px;
}
.feature-list span {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: var(--color-text-main);
}
.feature-list i {
color: var(--color-accent-secondary);
}
.mt-auto {
margin-top: auto;
}
/* === 右侧列样式 === */
.right-col {
display: grid;
grid-template-rows: 1fr 1fr;
gap: var(--space-md);
}
.sub-card {
padding: var(--space-lg);
display: flex;
flex-direction: column;
justify-content: center;
gap: 15px;
border-radius: 24px;
}
.icon-circle {
width: 50px;
height: 50px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: var(--color-text-main);
}
.icon-circle.dark {
background: var(--color-contrast-dark);
color: #fff;
}
.text-group h3 {
font-size: 1.4rem;
margin-bottom: 5px;
}
.text-group p {
font-size: 0.95rem;
opacity: 0.9;
margin: 0;
}
.stat-row {
display: flex;
align-items: baseline;
gap: 10px;
margin-top: 5px;
}
.big-num {
font-size: 2.5rem;
font-weight: 800;
color: var(--color-contrast-dark);
line-height: 1;
}
.stat-desc {
font-weight: 600;
color: var(--color-text-muted);
}
.info-only {
cursor: default;
}
</style>

@ -0,0 +1,390 @@
<template>
<div class="subpage-layout">
<!--
修复点
1. 删除了可能引起冲突的 class
2. 强制指定 grid-layout 样式
-->
<div class="layout-grid">
<!-- 左侧任务栏 (固定宽度) -->
<aside class="grid-sidebar">
<TaskSideBar :tasks="tasks" />
</aside>
<!-- 右侧主操作卡片 (自适应) -->
<main class="grid-main">
<div class="ui-card solid content-card">
<div class="card-header">
<div>
<h2>快速模式</h2>
<p class="subtitle">Quick Mode Protection</p>
</div>
<span class="tag">AI Guard</span>
</div>
<div class="card-body">
<p class="desc-text">使用系统推荐的默认参数配置上传图片后即可一键完成隐私防护处理</p>
<!-- 表单区域 -->
<div class="form-wrapper">
<!-- 1. 任务名 -->
<div class="form-group">
<label>任务名称</label>
<input type="text" v-model="formData.taskName" class="ui-input" placeholder="例如:我的自拍照防护..." />
</div>
<!-- 2. 风格选择 -->
<div class="form-group">
<label>防护风格目标</label>
<div class="style-selector">
<div
class="style-option"
:class="{ active: formData.style === 'face' }"
@click="formData.style = 'face'"
>
<div class="icon-circle"><i class="fas fa-smile"></i></div>
<div class="option-text">
<span class="opt-title">人脸保护</span>
<span class="opt-desc">针对面部识别算法</span>
</div>
<div class="check-mark" v-if="formData.style === 'face'"><i class="fas fa-check"></i></div>
</div>
<div
class="style-option vip"
:class="{ active: formData.style === 'art' }"
@click="formData.style = 'art'"
>
<div class="icon-circle"><i class="fas fa-paint-brush"></i></div>
<div class="option-text">
<span class="opt-title">艺术风格</span>
<span class="opt-desc">防风格迁移拷贝</span>
</div>
<span class="badge">VIP</span>
</div>
</div>
</div>
<!-- 3. 上传与提交 -->
<div class="upload-section">
<div class="upload-zone" @click="triggerFileUpload" :class="{ 'has-file': formData.fileName }">
<input
type="file"
ref="fileInput"
@change="handleFileChange"
style="display: none"
accept="image/*"
/>
<div class="upload-content">
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<div class="upload-text">
<span class="main-tip" v-if="!formData.fileName"></span>
<span class="file-name" v-else>{{ formData.fileName }}</span>
<span class="sub-tip" v-if="!formData.fileName"> JPG, PNG </span>
</div>
</div>
</div>
<div class="submit-area">
<button class="ui-btn gradient rect big-btn" @click="submitTask">
<i class="fas fa-shield-alt"></i>
开始防护
</button>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import TaskSideBar from '@/components/TaskSideBar.vue'
/* --- 逻辑保持不变 --- */
const fileInput = ref(null)
const tasks = ref([
{ id: '1024', status: 'running', progress: 65 },
{ id: '1025', status: 'waiting', progress: 0 }
])
const formData = ref({
taskName: '',
style: 'face',
fileName: '',
file: null
})
const triggerFileUpload = () => {
fileInput.value.click()
}
const handleFileChange = (event) => {
const file = event.target.files[0]
if (file) {
formData.value.fileName = file.name
formData.value.file = file
}
}
const submitTask = () => {
if (!formData.value.fileName) {
alert('请先上传图片')
return
}
if (!formData.value.taskName) {
alert('请填写任务名称')
return
}
console.log('Submitting Quick Task:', formData.value)
alert('快速任务已提交 (模拟)')
}
</script>
<style scoped>
/* 外层容器:确保居中且有内边距 */
.subpage-layout {
width: 100%;
height: 100%;
padding: 40px;
display: flex;
justify-content: center;
align-items: center;
background: var(--color-bg-primary);
}
.layout-grid {
display: grid;
grid-template-columns: 300px 1fr;
gap: 30px;
width: 100%;
max-width: 1400px;
height: 85vh; /* 恢复为 85vh */
margin-bottom: 0; /* 清除刚才添加的边距 */
}
/* 左侧栏 */
.grid-sidebar {
height: 100%;
overflow: hidden;
}
/* 右侧主内容 */
.grid-main {
height: 100%;
min-width: 0; /* 防止 grid 溢出 */
}
.content-card {
height: 100%;
display: flex;
flex-direction: column;
padding: 0; /* 重置 padding由内部控制 */
background: #ffffff;
}
/* 卡片头部 */
.card-header {
padding: 30px 40px;
border-bottom: 1px solid rgba(0,0,0,0.05);
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.subtitle {
color: var(--color-text-muted);
font-size: 0.9rem;
margin-top: 5px;
}
.tag {
background: var(--color-accent-primary);
color: var(--color-contrast-dark);
padding: 6px 16px;
border-radius: 20px;
font-weight: 700;
font-size: 0.8rem;
}
/* 卡片主体 */
.card-body {
padding: 40px;
flex: 1;
overflow-y: auto; /* 内容过多可滚动 */
}
.desc-text {
color: var(--color-text-main);
margin-bottom: 40px;
padding: 15px 20px;
background: rgba(24, 40, 59, 0.03);
border-left: 4px solid var(--color-accent-secondary);
border-radius: 4px;
}
/* 表单组件 */
.form-wrapper {
display: flex;
flex-direction: column;
gap: 30px;
}
.form-group label {
display: block;
font-size: 1rem;
font-weight: 600;
margin-bottom: 12px;
color: var(--color-text-main);
}
.ui-input {
width: 100%;
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 12px;
font-size: 1rem;
transition: all 0.2s;
background: #f8f9fa;
}
.ui-input:focus {
background: #fff;
border-color: var(--color-accent-secondary);
box-shadow: 0 0 0 4px rgba(255, 159, 28, 0.1);
}
/* 风格选择器 */
.style-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.style-option {
position: relative;
border: 2px solid transparent;
background: #f8f9fa;
border-radius: 16px;
padding: 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 15px;
transition: all 0.2s;
}
.style-option:hover {
background: #fff;
box-shadow: 0 10px 20px rgba(0,0,0,0.05);
}
.style-option.active {
background: #fff;
border-color: var(--color-contrast-dark);
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.icon-circle {
width: 48px;
height: 48px;
background: #e9ecef;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: var(--color-text-muted);
transition: all 0.2s;
}
.style-option.active .icon-circle {
background: var(--color-contrast-dark);
color: #fff;
}
.option-text {
display: flex;
flex-direction: column;
}
.opt-title { font-weight: 700; font-size: 1rem; }
.opt-desc { font-size: 0.8rem; color: var(--color-text-muted); }
.check-mark {
position: absolute;
top: 15px;
right: 15px;
color: var(--color-contrast-dark);
}
.badge {
position: absolute;
top: -10px;
right: 15px;
background: linear-gradient(135deg, #FFD166, #FF9F1C);
color: var(--color-text-main);
padding: 4px 10px;
border-radius: 10px;
font-size: 0.7rem;
font-weight: 800;
box-shadow: 0 4px 10px rgba(255, 159, 28, 0.3);
}
/* 上传区 */
.upload-zone {
border: 2px dashed #dbe2e8;
border-radius: 16px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: #fcfcfc;
}
.upload-zone:hover {
border-color: var(--color-accent-secondary);
background: rgba(255, 159, 28, 0.02);
}
.upload-zone.has-file {
border-style: solid;
border-color: var(--color-contrast-dark);
background: #fff;
}
.upload-icon {
font-size: 3rem;
color: var(--color-text-muted);
margin-bottom: 15px;
}
.upload-text {
display: flex;
flex-direction: column;
gap: 5px;
}
.main-tip { font-weight: 600; font-size: 1.1rem; }
.file-name { font-weight: 700; font-size: 1.2rem; color: var(--color-contrast-dark); }
.sub-tip { font-size: 0.9rem; color: var(--color-text-muted); }
.submit-area {
margin-top: 20px;
}
.big-btn {
width: 100%;
height: 60px;
font-size: 1.2rem;
border-radius: 16px;
letter-spacing: 1px;
}
</style>

@ -0,0 +1,409 @@
<template>
<div class="subpage-layout">
<div class="layout-grid">
<!-- 左侧任务栏 -->
<aside class="grid-sidebar">
<TaskSideBar :tasks="tasks" />
</aside>
<!-- 右侧主操作卡片 -->
<main class="grid-main">
<div class="ui-card solid content-card">
<div class="card-header">
<div>
<h2>通用模式</h2>
<p class="subtitle">Universal Protection Mode</p>
</div>
<span class="tag">Advanced</span>
</div>
<div class="card-body">
<p class="desc-text">支持自定义选择加密算法与扰动强度针对特定场景进行更精细化的隐私保护</p>
<div class="form-wrapper">
<!-- 1. 任务名 -->
<div class="form-group">
<label>任务名称</label>
<input type="text" v-model="formData.taskName" class="ui-input" placeholder="输入任务名称..." />
</div>
<!-- 2. 算法与强度 (一行两列布局) -->
<div class="row-group">
<!-- [修改点] 自定义下拉菜单 -->
<div class="form-group half">
<label>加密算法</label>
<!-- 点击外部关闭遮罩 (仅在打开时显示) -->
<div
v-if="isDropdownOpen"
class="click-outside-overlay"
@click="isDropdownOpen = false"
></div>
<div class="custom-select-container">
<!-- 触发器 (显示的框) -->
<div
class="select-trigger"
:class="{ 'is-open': isDropdownOpen }"
@click="isDropdownOpen = !isDropdownOpen"
>
<span :class="{ 'placeholder': !formData.algorithm }">
{{ currentAlgoName }}
</span>
<i class="fas fa-chevron-down arrow-icon"></i>
</div>
<!-- 下拉列表 (弹窗) -->
<Transition name="dropdown">
<div v-if="isDropdownOpen" class="select-options">
<div
v-for="algo in perturbationAlgorithms"
:key="algo.id"
class="option-item"
:class="{ selected: formData.algorithm === algo.id }"
@click="selectAlgo(algo)"
>
<span>{{ algo.method_name }}</span>
<i v-if="formData.algorithm === algo.id" class="fas fa-check check-icon"></i>
</div>
</div>
</Transition>
</div>
</div>
<!-- 强度选择 -->
<div class="form-group half">
<label>扰动强度</label>
<div class="strength-selector">
<div
class="str-item"
:class="{ active: formData.strength === 64 }"
@click="formData.strength = 64"
></div>
<div
class="str-item"
:class="{ active: formData.strength === 128 }"
@click="formData.strength = 128"
></div>
<div
class="str-item"
:class="{ active: formData.strength === 192 }"
@click="formData.strength = 192"
></div>
</div>
</div>
</div>
<!-- 3. 风格选择 -->
<div class="form-group">
<label>防护风格目标</label>
<div class="style-selector">
<div
class="style-option"
:class="{ active: formData.style === 'face' }"
@click="formData.style = 'face'"
>
<div class="icon-circle"><i class="fas fa-smile"></i></div>
<div class="option-text">
<span class="opt-title">人脸保护</span>
</div>
<div class="check-mark" v-if="formData.style === 'face'"><i class="fas fa-check"></i></div>
</div>
<div
class="style-option vip"
:class="{ active: formData.style === 'art' }"
@click="formData.style = 'art'"
>
<div class="icon-circle"><i class="fas fa-paint-brush"></i></div>
<div class="option-text">
<span class="opt-title">艺术风格</span>
</div>
<span class="badge">VIP</span>
</div>
</div>
</div>
<!-- 4. 上传与提交 -->
<div class="upload-section">
<div class="upload-zone" @click="triggerFileUpload" :class="{ 'has-file': formData.fileName }">
<input
type="file"
ref="fileInput"
@change="handleFileChange"
style="display: none"
accept="image/*"
/>
<div class="upload-content">
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<div class="upload-text">
<span class="main-tip" v-if="!formData.fileName"></span>
<span class="file-name" v-else>{{ formData.fileName }}</span>
</div>
</div>
</div>
<div class="submit-area">
<button class="ui-btn solid rect big-btn" @click="submitTask">
<i class="fas fa-cogs"></i>
开始计算
</button>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import TaskSideBar from '@/components/TaskSideBar.vue'
const fileInput = ref(null)
const perturbationAlgorithms = ref([])
//
const isDropdownOpen = ref(false)
const tasks = ref([
{ id: '1026', status: 'running', progress: 20 },
{ id: '1027', status: 'waiting', progress: 0 }
])
const formData = ref({
taskName: '',
algorithm: '', // ID
strength: 128,
style: 'face',
fileName: '',
file: null
})
//
const currentAlgoName = computed(() => {
if (!formData.value.algorithm) return '请选择加密算法'
const algo = perturbationAlgorithms.value.find(a => a.id === formData.value.algorithm)
return algo ? algo.method_name : '未知算法'
})
const fetchAlgorithms = async () => {
// Mock data
setTimeout(() => {
perturbationAlgorithms.value = [
{ id: 'adv_noise', method_name: 'AdvNoise v1.0 (推荐)' },
{ id: 'mist', method_name: 'Mist (High-Res)' },
{ id: 'fawkes', method_name: 'Fawkes Privacy' },
{ id: 'low_key', method_name: 'LowKey Filter' }
]
}, 500)
}
const selectAlgo = (algo) => {
formData.value.algorithm = algo.id
isDropdownOpen.value = false //
}
const triggerFileUpload = () => {
fileInput.value.click()
}
const handleFileChange = (event) => {
const file = event.target.files[0]
if (file) {
formData.value.fileName = file.name
formData.value.file = file
}
}
const submitTask = () => {
if (!formData.value.fileName) return alert('请先上传图片')
if (!formData.value.taskName) return alert('请填写任务名称')
if (!formData.value.algorithm) return alert('请选择加密算法')
console.log('Submitting Universal Task:', formData.value)
alert('通用防护任务已提交 (模拟)')
}
onMounted(() => {
fetchAlgorithms()
})
</script>
<style scoped>
/* 基础布局保持不变 */
.subpage-layout {
width: 100%;
height: 100%;
padding: 40px;
display: flex;
justify-content: center;
align-items: center;
background: var(--color-bg-primary);
}
.layout-grid {
display: grid;
grid-template-columns: 300px 1fr;
gap: 30px;
width: 100%;
max-width: 1400px;
/* 修改 1稍微减小高度 给底部腾出物理空间 */
height: 88vh;
/* 修改 2这就是“隐形的砖头” */
/* 在卡片下面垫 60pxFlex 居中计算时,卡片就会视觉上移 30px */
margin-bottom: 60px;
}
.grid-sidebar { height: 100%; overflow: hidden; }
.grid-main { height: 100%; min-width: 0; }
.content-card { height: 100%; display: flex; flex-direction: column; padding: 0; background: #ffffff; }
.card-header { padding: 30px 40px; border-bottom: 1px solid rgba(0,0,0,0.05); display: flex; justify-content: space-between; align-items: flex-start; }
.subtitle { color: var(--color-text-muted); font-size: 0.9rem; margin-top: 5px; }
.tag { background: var(--color-contrast-dark); color: #fff; padding: 6px 16px; border-radius: 20px; font-weight: 700; font-size: 0.8rem; }
.card-body { padding: 40px; flex: 1; overflow-y: auto; }
.desc-text { color: var(--color-text-main); margin-bottom: 40px; padding: 15px 20px; background: rgba(24, 40, 59, 0.03); border-left: 4px solid var(--color-contrast-dark); border-radius: 4px; }
.form-wrapper { display: flex; flex-direction: column; gap: 30px; }
.form-group label { display: block; font-size: 1rem; font-weight: 600; margin-bottom: 12px; color: var(--color-text-main); }
.ui-input { width: 100%; padding: 16px; border: 1px solid #e0e0e0; border-radius: 12px; font-size: 1rem; background: #f8f9fa; outline: none; transition: all 0.2s; }
.ui-input:focus { background: #fff; border-color: var(--color-contrast-dark); }
.row-group { display: flex; gap: 30px; }
.half { flex: 1; }
/* === [核心] 自定义下拉菜单样式 === */
.custom-select-container {
position: relative;
width: 100%;
}
/* 透明遮罩,用于点击外部关闭 */
.click-outside-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 90;
cursor: default;
}
/* 触发框 */
.select-trigger {
width: 100%;
padding: 16px;
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 12px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
z-index: 91; /* 保证在遮罩之上 */
}
.select-trigger:hover {
background: #fff;
border-color: #d0d0d0;
}
.select-trigger.is-open {
background: #fff;
border-color: var(--color-contrast-dark);
box-shadow: 0 0 0 4px rgba(24, 40, 59, 0.05);
}
.placeholder {
color: var(--color-text-muted);
}
.arrow-icon {
color: var(--color-text-muted);
transition: transform 0.3s ease;
}
.select-trigger.is-open .arrow-icon {
transform: rotate(180deg);
}
/* 下拉列表容器 */
.select-options {
position: absolute;
top: 110%; /* 稍微留点空隙 */
left: 0;
width: 100%;
background: #fff;
border: 1px solid rgba(0,0,0,0.05);
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
z-index: 100;
padding: 8px;
max-height: 250px;
overflow-y: auto;
}
/* 选项项 */
.option-item {
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--color-text-main);
transition: all 0.1s;
}
.option-item:hover {
background: #f1f3f5;
}
.option-item.selected {
background: rgba(24, 40, 59, 0.05);
color: var(--color-contrast-dark);
font-weight: 600;
}
.check-icon {
color: var(--color-accent-secondary);
}
/* 动画 */
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.2s ease-out;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* === 其他样式 (强度选择器等) === */
.strength-selector { display: flex; background: #f8f9fa; border-radius: 12px; padding: 5px; border: 1px solid #e0e0e0; }
.str-item { flex: 1; text-align: center; padding: 10px; border-radius: 8px; cursor: pointer; font-weight: 600; color: var(--color-text-muted); transition: all 0.2s; }
.str-item.active { background: var(--color-contrast-dark); color: #fff; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
.style-selector { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.style-option { position: relative; background: #f8f9fa; border-radius: 16px; padding: 20px; cursor: pointer; display: flex; align-items: center; gap: 15px; border: 2px solid transparent; transition: all 0.2s; }
.style-option.active { background: #fff; border-color: var(--color-contrast-dark); }
.icon-circle { width: 48px; height: 48px; background: #e9ecef; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; }
.style-option.active .icon-circle { background: var(--color-contrast-dark); color: #fff; }
.opt-title { font-weight: 700; }
.check-mark { position: absolute; top: 15px; right: 15px; color: var(--color-contrast-dark); }
.badge { position: absolute; top: -10px; right: 15px; background: linear-gradient(135deg, #FFD166, #FF9F1C); color: var(--color-text-main); padding: 4px 10px; border-radius: 10px; font-size: 0.7rem; font-weight: 800; }
.upload-zone { border: 2px dashed #dbe2e8; border-radius: 16px; padding: 30px; text-align: center; cursor: pointer; transition: all 0.2s; background: #fcfcfc; }
.upload-zone:hover { border-color: var(--color-contrast-dark); background: rgba(0,0,0,0.01); }
.upload-zone.has-file { border-style: solid; border-color: var(--color-contrast-dark); background: #fff; }
.upload-icon { font-size: 2rem; color: var(--color-text-muted); margin-bottom: 10px; }
.file-name { font-weight: 700; color: var(--color-contrast-dark); }
.submit-area { margin-top: 20px; }
.big-btn { width: 100%; height: 60px; font-size: 1.2rem; border-radius: 16px; }
</style>

@ -0,0 +1,146 @@
<template>
<div class="view-container">
<div class="header-row">
<h1 class="page-header">专题防护</h1>
<p class="header-desc">Topic-Specific Protection Scenarios</p>
</div>
<div class="card-grid">
<!-- 顶部横条主打功能 (防风格迁移) -->
<div class="ui-card gradient interactive top-card" @click="handleOpenStyle">
<div class="flex-row">
<div class="text-content">
<h3>防风格迁移</h3>
<p>针对艺术风格迁移算法的专项防御保护原创画风</p>
</div>
<div class="icon-box">
<i class="fas fa-palette"></i>
</div>
</div>
</div>
<!-- 下方布局重要这里只保留了两个卡片 -->
<div class="bottom-row">
<!-- 卡片1防人脸编辑 -->
<div class="ui-card glass interactive col-card" @click="handleOpenFace">
<div class="icon-header">
<i class="fas fa-user-shield"></i>
</div>
<h4>防人脸编辑</h4>
<p class="desc">DeepFake 防御</p>
</div>
<!-- 卡片2防定制生成 -->
<div class="ui-card glass interactive col-card" @click="handleOpenCustom">
<div class="icon-header">
<i class="fas fa-robot"></i>
</div>
<h4>防定制生成</h4>
<p class="desc">对抗 LoRA 训练</p>
</div>
<!-- 已删除Coming Soon 卡片已被移除 -->
</div>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue'
const openSubpage = inject('openSubpage')
const handleOpenStyle = () => openSubpage('page2', 'style')
const handleOpenFace = () => openSubpage('page2', 'face')
const handleOpenCustom = () => openSubpage('page2', 'custom')
</script>
<style scoped>
.view-container {
top: 5%;
width: 100%;
height: 90%;
display: flex;
flex-direction: column;
}
.header-row {
flex: 0 0 auto;
margin-bottom: var(--space-md);
}
.page-header {
font-size: 2.5rem;
color: var(--color-contrast-dark);
margin-bottom: 5px;
}
.header-desc {
color: var(--color-text-muted);
font-size: 1.1rem;
}
.card-grid {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-md);
}
/* 顶部大卡片 */
.top-card {
flex: 0 0 35%;
padding: var(--space-lg);
display: flex;
align-items: center;
position: relative;
overflow: hidden;
}
.flex-row {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 2;
}
.text-content h3 { font-size: 2rem; margin-bottom: 10px; }
.text-content p { font-size: 1.1rem; opacity: 0.9; }
.icon-box {
font-size: 6rem;
opacity: 0.8;
color: var(--color-contrast-dark);
}
/* 底部卡片 */
.bottom-row {
flex: 1;
display: grid;
/* 【重要修改】改为两列均分,填补空缺 */
grid-template-columns: 1fr 1fr;
gap: var(--space-md);
}
.col-card {
padding: var(--space-lg);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
transition: all 0.2s;
}
.icon-header {
font-size: 2.5rem;
margin-bottom: 15px;
color: var(--color-contrast-dark);
}
.desc {
font-size: 0.9rem;
color: var(--color-text-muted);
}
</style>

@ -0,0 +1,261 @@
<template>
<div class="subpage-layout">
<div class="layout-grid">
<!-- 左侧任务栏 -->
<aside class="grid-sidebar">
<TaskSideBar :tasks="tasks" />
</aside>
<!-- 右侧主操作卡片 -->
<main class="grid-main">
<div class="ui-card solid content-card">
<div class="card-header">
<div>
<h2>{{ pageTitle }}</h2>
<p class="subtitle">Topic Protection</p>
</div>
<span class="tag">Specialized</span>
</div>
<div class="card-body">
<p class="desc-text">本模块针对特定攻击场景提供定制化防御部分参数由算法固定以确保最佳防护效果</p>
<div class="form-wrapper">
<!-- 1. 任务名 -->
<div class="form-group">
<label>任务名称</label>
<input type="text" v-model="formData.taskName" class="ui-input" placeholder="输入任务名称..." />
</div>
<!-- 2. 只读参数区域 (专题防护特色) -->
<div class="row-group">
<div class="form-group half">
<label>防护算法 (定制)</label>
<div class="readonly-field">
<i class="fas fa-lock lock-icon"></i>
<span>{{ fixedConfig.algorithm }}</span>
</div>
</div>
<div class="form-group half">
<label>扰动强度 (定制)</label>
<div class="readonly-field">
<i class="fas fa-lock lock-icon"></i>
<span>{{ fixedConfig.strength }}</span>
</div>
</div>
</div>
<!-- 3. 风格选择 -->
<div class="form-group">
<label>防护风格目标</label>
<div class="style-selector">
<div
class="style-option"
:class="{ active: formData.style === 'face' }"
@click="formData.style = 'face'"
>
<div class="icon-circle"><i class="fas fa-smile"></i></div>
<div class="option-text">
<span class="opt-title">人脸保护</span>
</div>
<div class="check-mark" v-if="formData.style === 'face'"><i class="fas fa-check"></i></div>
</div>
<div
class="style-option vip"
:class="{ active: formData.style === 'art' }"
@click="formData.style = 'art'"
>
<div class="icon-circle"><i class="fas fa-paint-brush"></i></div>
<div class="option-text">
<span class="opt-title">艺术风格</span>
</div>
<span class="badge">VIP</span>
</div>
</div>
</div>
<!-- 4. 上传与提交 -->
<div class="upload-section">
<div class="upload-zone" @click="triggerFileUpload" :class="{ 'has-file': formData.fileName }">
<input
type="file"
ref="fileInput"
@change="handleFileChange"
style="display: none"
accept="image/*"
/>
<div class="upload-content">
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<div class="upload-text">
<span class="main-tip" v-if="!formData.fileName"></span>
<span class="file-name" v-else>{{ formData.fileName }}</span>
</div>
</div>
</div>
<div class="submit-area">
<button class="ui-btn gradient rect big-btn" @click="submitTask">
<i class="fas fa-magic"></i>
启动专项防御
</button>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import TaskSideBar from '@/components/TaskSideBar.vue'
const route = useRoute()
const fileInput = ref(null)
//
const tasks = ref([
{ id: '2001', status: 'running', progress: 85 },
{ id: '2002', status: 'waiting', progress: 0 }
])
const formData = ref({
taskName: '',
style: 'face',
fileName: '',
file: null
})
// (subpage)
const subpageType = computed(() => route.params.subpage)
const pageTitle = computed(() => {
const map = {
'style': '防风格迁移',
'face': '防人脸编辑',
'custom': '防定制生成'
}
return map[subpageType.value] || '专题防护'
})
//
const fixedConfig = computed(() => {
if (subpageType.value === 'style') {
return { algorithm: 'Anti-Mist v2.0', strength: 'High / 高 (Fixed)' }
} else if (subpageType.value === 'face') {
return { algorithm: 'Fawkes-Pro', strength: 'Mid / 中 (Fixed)' }
} else {
return { algorithm: 'Glaze-Shield', strength: 'Adaptive / 自适应' }
}
})
const triggerFileUpload = () => {
fileInput.value.click()
}
const handleFileChange = (event) => {
const file = event.target.files[0]
if (file) {
formData.value.fileName = file.name
formData.value.file = file
}
}
const submitTask = () => {
if (!formData.value.fileName) return alert('请先上传图片')
if (!formData.value.taskName) return alert('请填写任务名称')
console.log(`Submitting Topic Task [${subpageType.value}]:`, formData.value)
alert('专题防护任务已提交 (模拟)')
}
</script>
<style scoped>
/* === 布局复用 (Page1) === */
.subpage-layout {
width: 100%;
height: 100%;
padding: 40px;
display: flex;
justify-content: center;
align-items: center;
background: var(--color-bg-primary);
}
.layout-grid {
display: grid;
grid-template-columns: 300px 1fr;
gap: 30px;
width: 100%;
max-width: 1400px;
/* 修改 1稍微减小高度 (85vh -> 82vh),给底部腾出物理空间 */
height: 90vh;
/* 修改 2这就是“隐形的砖头” */
/* 在卡片下面垫 60pxFlex 居中计算时,卡片就会视觉上移 30px */
margin-bottom: 60px;
}
.grid-sidebar { height: 100%; overflow: hidden; }
.grid-main { height: 100%; min-width: 0; }
.content-card { height: 100%; display: flex; flex-direction: column; padding: 0; background: #ffffff; }
.card-header { padding: 30px 40px; border-bottom: 1px solid rgba(0,0,0,0.05); display: flex; justify-content: space-between; align-items: flex-start; }
.subtitle { color: var(--color-text-muted); font-size: 0.9rem; margin-top: 5px; }
.tag { background: var(--color-contrast-dark); color: #fff; padding: 6px 16px; border-radius: 20px; font-weight: 700; font-size: 0.8rem; }
.card-body { padding: 40px; flex: 1; overflow-y: auto; }
.desc-text { color: var(--color-text-main); margin-bottom: 40px; padding: 15px 20px; background: rgba(24, 40, 59, 0.03); border-left: 4px solid var(--color-contrast-dark); border-radius: 4px; }
.form-wrapper { display: flex; flex-direction: column; gap: 30px; }
.form-group label { display: block; font-size: 1rem; font-weight: 600; margin-bottom: 12px; color: var(--color-text-main); }
.ui-input { width: 100%; padding: 16px; border: 1px solid #e0e0e0; border-radius: 12px; font-size: 1rem; background: #f8f9fa; outline: none; transition: all 0.2s; }
.ui-input:focus { background: #fff; border-color: var(--color-contrast-dark); }
.row-group { display: flex; gap: 30px; }
.half { flex: 1; }
/* === 专题防护特有样式 === */
/* 只读字段样式 */
.readonly-field {
width: 100%;
padding: 16px;
background: #f1f3f5; /* 灰色背景 */
border: 1px dashed #ced4da; /* 虚线框 */
border-radius: 12px;
color: var(--color-text-muted);
display: flex;
align-items: center;
gap: 10px;
cursor: not-allowed;
user-select: none;
}
.lock-icon {
font-size: 0.9rem;
color: #adb5bd;
}
/* 风格选择器 & 上传区 (复用) */
.style-selector { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.style-option { position: relative; background: #f8f9fa; border-radius: 16px; padding: 20px; cursor: pointer; display: flex; align-items: center; gap: 15px; border: 2px solid transparent; transition: all 0.2s; }
.style-option.active { background: #fff; border-color: var(--color-contrast-dark); }
.icon-circle { width: 48px; height: 48px; background: #e9ecef; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; }
.style-option.active .icon-circle { background: var(--color-contrast-dark); color: #fff; }
.opt-title { font-weight: 700; }
.check-mark { position: absolute; top: 15px; right: 15px; color: var(--color-contrast-dark); }
.badge { position: absolute; top: -10px; right: 15px; background: linear-gradient(135deg, #FFD166, #FF9F1C); color: var(--color-text-main); padding: 4px 10px; border-radius: 10px; font-size: 0.7rem; font-weight: 800; }
.upload-zone { border: 2px dashed #dbe2e8; border-radius: 16px; padding: 30px; text-align: center; cursor: pointer; transition: all 0.2s; background: #fcfcfc; }
.upload-zone:hover { border-color: var(--color-contrast-dark); background: rgba(0,0,0,0.01); }
.upload-zone.has-file { border-style: solid; border-color: var(--color-contrast-dark); background: #fff; }
.upload-icon { font-size: 2rem; color: var(--color-text-muted); margin-bottom: 10px; }
.file-name { font-weight: 700; color: var(--color-contrast-dark); }
.submit-area { margin-top: 20px; }
.big-btn { width: 100%; height: 60px; font-size: 1.2rem; border-radius: 16px; }
</style>

@ -0,0 +1,164 @@
<template>
<div class="view-container">
<div class="header-row">
<h1 class="page-header">效果验证</h1>
<p class="header-desc">Simulate attacks & Evaluate protection quality</p>
</div>
<div class="card-grid">
<!-- 主卡片微调生图 (Fine Tuning) -->
<div class="ui-card solid interactive main-card" @click="handleOpenFineTune">
<div class="card-bg-decoration"></div>
<div class="content">
<div class="icon-wrapper big">
<i class="fas fa-brain"></i>
</div>
<div class="text-group">
<h3>微调生图验证</h3>
<p>模拟攻击者对图片进行LoRA微调直观展示防护前后的生成效果差异</p>
</div>
<div class="ui-btn gradient rounded">开始验证</div>
</div>
</div>
<!-- 下方两列指标与热力图 -->
<div class="bottom-row">
<!-- 数据指标 -->
<div class="ui-card glass interactive sub-card" @click="handleOpenMetrics">
<div class="icon-wrapper">
<i class="fas fa-chart-bar"></i>
</div>
<div class="text-group">
<h4>数据指标对比</h4>
<p>PSNR / SSIM / FID 量化分析</p>
</div>
</div>
<!-- 热力图 -->
<div class="ui-card glass interactive sub-card" @click="handleOpenHeatmap">
<div class="icon-wrapper">
<i class="fas fa-wave-square"></i>
</div>
<div class="text-group">
<h4>热力图与频域</h4>
<p>Attention Map / DCT 频谱分析</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue'
const openSubpage = inject('openSubpage')
const handleOpenFineTune = () => openSubpage('page3', 'fine-tuning')
const handleOpenMetrics = () => openSubpage('page3', 'metrics')
const handleOpenHeatmap = () => openSubpage('page3', 'heatmap')
</script>
<style scoped>
.view-container {
top: 5%;
width: 100%;
height: 90%;
display: flex;
flex-direction: column;
}
.header-row {
flex: 0 0 auto;
margin-bottom: var(--space-md);
}
.page-header {
font-size: 2.5rem;
color: var(--color-contrast-dark);
margin-bottom: 5px;
}
.header-desc {
color: var(--color-text-muted);
font-size: 1.1rem;
}
.card-grid {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-md);
min-height: 0;
}
/* 主卡片 */
.main-card {
flex: 2; /* 占 2/3 高度 */
padding: var(--space-xl);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
}
.card-bg-decoration {
position: absolute;
top: 0;
right: 0;
width: 60%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 159, 28, 0.05));
clip-path: polygon(20% 0%, 100% 0, 100% 100%, 0% 100%);
}
.content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
max-width: 600px;
}
.icon-wrapper {
width: 50px;
height: 50px;
border-radius: 12px;
background: var(--color-contrast-dark);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
}
.icon-wrapper.big {
width: 64px;
height: 64px;
font-size: 1.5rem;
background: var(--color-accent-secondary);
}
.text-group h3 { font-size: 2rem; margin-bottom: 10px; }
.text-group p { font-size: 1.1rem; color: var(--color-text-main); }
/* 底部小卡片 */
.bottom-row {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-md);
}
.sub-card {
padding: var(--space-lg);
display: flex;
align-items: center;
gap: 20px;
}
.sub-card h4 { font-size: 1.3rem; margin-bottom: 5px; color: var(--color-contrast-dark); }
.sub-card p { font-size: 0.9rem; color: var(--color-text-muted); margin: 0; }
</style>

@ -0,0 +1,261 @@
<template>
<div class="subpage-layout">
<div class="layout-grid">
<!-- 左侧任务栏 -->
<aside class="grid-sidebar">
<TaskSideBar :tasks="tasks" />
</aside>
<!-- 右侧主操作卡片 -->
<main class="grid-main">
<div class="ui-card solid content-card">
<div class="card-header">
<div>
<h2>{{ pageTitle }}</h2>
<p class="subtitle">Validation Task Setup</p>
</div>
<span class="tag">Analysis</span>
</div>
<div class="card-body">
<p class="desc-text">选择已完成的防护任务作为数据源对其进行攻击模拟或指标计算以验证防护效果</p>
<div class="form-wrapper">
<!-- 1. 任务名 -->
<div class="form-group">
<label>验证任务名称</label>
<input type="text" v-model="formData.taskName" class="ui-input" placeholder="例如第1批次效果验证..." />
</div>
<!-- 2. 数据源选择 (核心交互) -->
<div class="form-group">
<label>选择数据源 (原始防护任务)</label>
<div class="source-selector">
<!-- 触发器 -->
<div
class="source-trigger"
:class="{ active: isSourceListOpen, selected: formData.sourceId }"
@click="isSourceListOpen = !isSourceListOpen"
>
<div class="trigger-left">
<div class="icon-box"><i class="fas fa-database"></i></div>
<div class="trigger-info">
<span class="t-title">{{ currentSourceName }}</span>
<span class="t-desc" v-if="formData.sourceId">ID: {{ formData.sourceId }}</span>
<span class="t-desc" v-else>...</span>
</div>
</div>
<i class="fas fa-chevron-down arrow"></i>
</div>
<!-- 展开的任务列表 -->
<Transition name="expand">
<div v-if="isSourceListOpen" class="source-list">
<div class="list-header">可选历史任务</div>
<div class="list-body">
<div
v-for="item in historyTasks"
:key="item.id"
class="source-item"
@click="selectSource(item)"
>
<div class="s-info">
<span class="s-name">{{ item.name }}</span>
<span class="s-date">{{ item.date }}</span>
</div>
<span class="s-tag">{{ item.imageCount }}张图</span>
</div>
</div>
</div>
</Transition>
</div>
</div>
<!-- 3. 可视化类型 (仅热力图显示) -->
<div class="form-group" v-if="subpageType === 'heatmap'">
<label>可视化类型</label>
<div class="style-selector">
<div class="style-option" :class="{ active: formData.visType === 'attention' }" @click="formData.visType = 'attention'">
<i class="fas fa-eye"></i> <span>Attention Map</span>
</div>
<div class="style-option" :class="{ active: formData.visType === 'frequency' }" @click="formData.visType = 'frequency'">
<i class="fas fa-wave-square"></i> <span>频域分析</span>
</div>
</div>
</div>
<!-- 4. 提交 -->
<div class="submit-area">
<button class="ui-btn solid rect big-btn" @click="submitTask">
<i class="fas fa-play"></i>
开始验证分析
</button>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import TaskSideBar from '@/components/TaskSideBar.vue'
const route = useRoute()
const isSourceListOpen = ref(false)
//
const tasks = ref([
{ id: '3001', status: 'running', progress: 45 },
{ id: '3002', status: 'waiting', progress: 0 }
])
//
const historyTasks = [
{ id: '1024', name: '风景图通用防护任务', date: '2023-11-20', imageCount: 12 },
{ id: '1025', name: '人脸隐私加噪测试', date: '2023-11-21', imageCount: 5 },
{ id: '1026', name: '艺术风格迁移防御', date: '2023-11-22', imageCount: 8 }
]
const formData = ref({
taskName: '',
sourceId: '',
sourceName: '',
visType: 'attention'
})
const subpageType = computed(() => route.params.subpage)
const pageTitle = computed(() => {
const map = {
'tuning': '微调生图验证',
'metrics': '数据指标对比',
'heatmap': '热力图分析'
}
return map[subpageType.value] || '效果验证'
})
const currentSourceName = computed(() => {
return formData.value.sourceName || '请选择数据源'
})
const selectSource = (item) => {
formData.value.sourceId = item.id
formData.value.sourceName = item.name
isSourceListOpen.value = false
}
const submitTask = () => {
if (!formData.value.taskName) return alert('请填写任务名称')
if (!formData.value.sourceId) return alert('请选择数据源')
console.log(`Submitting Validation [${subpageType.value}]:`, formData.value)
alert('验证任务已提交 (模拟)')
}
</script>
<style scoped>
/* === 基础布局复用 (Page1) === */
.subpage-layout { width: 100%; height: 100%; padding: 40px; display: flex; justify-content: center; align-items: center; background: var(--color-bg-primary); }
.layout-grid { display: grid; grid-template-columns: 300px 1fr; gap: 30px; width: 100%; max-width: 1400px; height: 85vh; }
.grid-sidebar { height: 100%; overflow: hidden; }
.grid-main { height: 100%; min-width: 0; }
.content-card { height: 100%; display: flex; flex-direction: column; padding: 0; background: #ffffff; }
.card-header { padding: 30px 40px; border-bottom: 1px solid rgba(0,0,0,0.05); display: flex; justify-content: space-between; align-items: flex-start; }
.subtitle { color: var(--color-text-muted); font-size: 0.9rem; margin-top: 5px; }
.tag { background: var(--color-contrast-dark); color: #fff; padding: 6px 16px; border-radius: 20px; font-weight: 700; font-size: 0.8rem; }
.card-body { padding: 40px; flex: 1; overflow-y: auto; }
.desc-text { color: var(--color-text-main); margin-bottom: 40px; padding: 15px 20px; background: rgba(24, 40, 59, 0.03); border-left: 4px solid var(--color-contrast-dark); border-radius: 4px; }
.form-wrapper { display: flex; flex-direction: column; gap: 30px; }
.form-group label { display: block; font-size: 1rem; font-weight: 600; margin-bottom: 12px; color: var(--color-text-main); }
.ui-input { width: 100%; padding: 16px; border: 1px solid #e0e0e0; border-radius: 12px; font-size: 1rem; background: #f8f9fa; outline: none; transition: all 0.2s; }
.ui-input:focus { background: #fff; border-color: var(--color-contrast-dark); }
.submit-area { margin-top: 20px; }
.big-btn { width: 100%; height: 60px; font-size: 1.2rem; border-radius: 16px; }
/* === 验证页面特有样式 === */
/* 数据源选择器 */
.source-selector {
position: relative;
width: 100%;
}
.source-trigger {
border: 1px solid #e0e0e0;
border-radius: 16px;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
background: #f8f9fa;
transition: all 0.2s;
}
.source-trigger:hover { background: #fff; border-color: #ccc; }
.source-trigger.active { background: #fff; border-color: var(--color-contrast-dark); box-shadow: 0 0 0 4px rgba(24, 40, 59, 0.05); }
.source-trigger.selected .icon-box { background: var(--color-contrast-dark); color: #fff; }
.trigger-left { display: flex; align-items: center; gap: 15px; }
.icon-box { width: 40px; height: 40px; background: #e9ecef; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: var(--color-text-muted); transition: all 0.2s; }
.trigger-info { display: flex; flex-direction: column; }
.t-title { font-weight: 600; font-size: 1rem; }
.t-desc { font-size: 0.8rem; color: var(--color-text-muted); }
.arrow { transition: transform 0.3s; color: var(--color-text-muted); }
.source-trigger.active .arrow { transform: rotate(180deg); }
/* 下拉列表 */
.source-list {
margin-top: 10px;
background: #fff;
border: 1px solid rgba(0,0,0,0.05);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
overflow: hidden;
}
.list-header {
padding: 10px 20px;
font-size: 0.8rem;
font-weight: 700;
color: var(--color-text-muted);
background: #f8f9fa;
border-bottom: 1px solid #eee;
}
.source-item {
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: background 0.1s;
border-bottom: 1px solid #f8f9fa;
}
.source-item:last-child { border-bottom: none; }
.source-item:hover { background: #f0f7ff; }
.s-info { display: flex; flex-direction: column; }
.s-name { font-weight: 600; color: var(--color-text-main); }
.s-date { font-size: 0.8rem; color: var(--color-text-muted); }
.s-tag { font-size: 0.8rem; padding: 2px 8px; background: #e9ecef; border-radius: 4px; color: var(--color-text-muted); }
/* 动画 */
.expand-enter-active, .expand-leave-active { transition: all 0.2s ease; max-height: 300px; opacity: 1; }
.expand-enter-from, .expand-leave-to { max-height: 0; opacity: 0; }
/* 简单选项 */
.style-selector { display: flex; gap: 20px; }
.style-option { flex: 1; padding: 15px; border-radius: 12px; background: #f8f9fa; border: 1px solid #e0e0e0; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 10px; font-weight: 600; transition: all 0.2s; }
.style-option:hover { background: #fff; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
.style-option.active { background: var(--color-contrast-dark); color: #fff; border-color: var(--color-contrast-dark); }
</style>

@ -0,0 +1,165 @@
<template>
<div class="view-container">
<div class="header-section">
<h1 class="page-header">资源管理</h1>
<p class="page-desc">My Assets & History</p>
</div>
<div class="split-layout">
<!-- 左侧主面板任务历史 -->
<div class="ui-card solid interactive left-panel" @click="handleOpen('tasks')">
<div class="panel-header">
<div class="icon-box main">
<i class="fas fa-tasks"></i>
</div>
<div class="text-group">
<h2>任务历史</h2>
<p>查看所有防护任务的状态与详情</p>
</div>
</div>
<!-- 装饰性的列表预览 -->
<div class="list-preview">
<div class="preview-item" v-for="n in 3" :key="n">
<div class="line sm"></div>
<div class="line lg"></div>
<div class="badge-dot"></div>
</div>
</div>
<div class="ui-btn gradient rect mt-auto">管理任务</div>
</div>
<!-- 右侧次级面板 -->
<div class="right-col">
<!-- 已防护图片 -->
<div class="ui-card glass interactive sub-panel" @click="handleOpen('images')">
<div class="icon-box sub">
<i class="fas fa-images"></i>
</div>
<div class="text-group">
<h3>图片库</h3>
<p>已处理的加密图像资源</p>
</div>
</div>
<!-- 验证结果 -->
<div class="ui-card glass interactive sub-panel" @click="handleOpen('results')">
<div class="icon-box sub">
<i class="fas fa-file-alt"></i>
</div>
<div class="text-group">
<h3>分析报告</h3>
<p>效果验证生成的评估文档</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue'
const openSubpage = inject('openSubpage')
const handleOpen = (type) => {
openSubpage('page4', type)
}
</script>
<style scoped>
.view-container {
top: 5%;
width: 100%;
height: 90%;
display: flex;
flex-direction: column;
}
.header-section { margin-bottom: var(--space-md); flex: 0 0 auto; }
.page-header { font-size: 2.5rem; color: var(--color-contrast-dark); margin-bottom: 5px; }
.page-desc { color: var(--color-text-muted); font-size: 1.1rem; }
.split-layout {
flex: 1;
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: var(--space-md);
min-height: 0;
}
/* 左侧面板 */
.left-panel {
padding: var(--space-xl);
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 30px;
}
.icon-box {
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-box.main { width: 64px; height: 64px; background: var(--color-contrast-dark); color: #fff; font-size: 1.8rem; }
.icon-box.sub { width: 50px; height: 50px; background: rgba(255,255,255,0.6); color: var(--color-contrast-dark); font-size: 1.4rem; }
.text-group h2 { font-size: 1.8rem; margin: 0; }
.text-group h3 { font-size: 1.4rem; margin-bottom: 5px; }
.text-group p { margin: 0; color: var(--color-text-muted); }
.list-preview {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
opacity: 0.6;
margin-bottom: 20px;
}
.preview-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: var(--color-bg-secondary);
border-radius: 8px;
}
.line { height: 8px; background: #ddd; border-radius: 4px; }
.line.sm { width: 40px; }
.line.lg { flex: 1; }
.badge-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-accent-secondary); }
.mt-auto { margin-top: auto; align-self: flex-start; }
/* 右侧列 */
.right-col {
display: grid;
grid-template-rows: 1fr 1fr;
gap: var(--space-md);
}
.sub-panel {
padding: var(--space-lg);
display: flex;
align-items: center;
gap: 20px;
transition: all 0.2s;
}
.sub-panel:hover {
transform: translateX(-5px);
background: #fff;
}
</style>

@ -0,0 +1,243 @@
<template>
<div class="subpage-layout">
<div class="layout-grid">
<!-- 左侧侧边栏 (显示当前队列保持上下文) -->
<aside class="grid-sidebar">
<TaskSideBar :tasks="sidebarTasks" />
</aside>
<!-- 右侧数据列表区域 -->
<main class="grid-main">
<div class="ui-card solid content-card">
<!-- 头部动态标题 -->
<div class="card-header">
<div>
<h2>{{ pageInfo.title }}</h2>
<p class="subtitle">{{ pageInfo.subtitle }}</p>
</div>
<span class="tag">{{ pageInfo.tag }}</span>
</div>
<div class="card-body">
<!-- === 场景 1: 任务列表 === -->
<div v-if="subpageType === 'tasks'" class="view-section">
<!-- 状态筛选器 -->
<div class="filter-tabs">
<div
v-for="tab in statusTabs"
:key="tab.key"
class="tab-item"
:class="{ active: currentStatus === tab.key }"
@click="currentStatus = tab.key"
>
{{ tab.label }}
</div>
</div>
<!-- 任务列表 (Div 模拟表格) -->
<div class="data-list">
<div v-for="task in filteredTasks" :key="task.id" class="list-card">
<div class="lc-left">
<span class="lc-id">#{{ task.id }}</span>
<div class="lc-info">
<span class="lc-title">{{ task.name }}</span>
<span class="lc-date">{{ task.createdAt }}</span>
</div>
</div>
<div class="lc-right">
<span class="status-badge" :class="task.status">{{ statusLabel(task.status) }}</span>
<button class="ui-btn glass rounded sm">详情</button>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredTasks.length === 0" class="empty-hint"></div>
</div>
</div>
<!-- === 场景 2: 图片库 === -->
<div v-else-if="subpageType === 'images'" class="view-section">
<div class="image-grid">
<div v-for="img in protectedImages" :key="img.id" class="img-card">
<div class="img-placeholder">
<i class="fas fa-image"></i>
</div>
<div class="img-meta">
<span class="img-title">{{ img.taskName }}</span>
<span class="img-tag">{{ img.type }}</span>
</div>
</div>
</div>
</div>
<!-- === 场景 3: 验证结果 === -->
<div v-else-if="subpageType === 'results'" class="view-section">
<div class="data-list">
<div v-for="res in validationResults" :key="res.id" class="list-card result-style">
<div class="lc-icon">
<i class="fas fa-file-contract"></i>
</div>
<div class="lc-info">
<span class="lc-title">{{ res.taskName }}</span>
<span class="lc-desc">{{ res.resultType }} - {{ res.compareType }}</span>
</div>
<div class="lc-actions">
<i class="fas fa-download action-icon"></i>
<i class="fas fa-eye action-icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import TaskSideBar from '@/components/TaskSideBar.vue'
const route = useRoute()
const subpageType = computed(() => route.params.subpage)
// 使 (Mock)
const sidebarTasks = ref([
{ id: '1028', status: 'running', progress: 55 },
{ id: '1029', status: 'waiting', progress: 0 }
])
// === ===
const pageInfo = computed(() => {
const map = {
'tasks': { title: '任务历史', subtitle: 'Task History Management', tag: 'Tasks' },
'images': { title: '图片资源库', subtitle: 'Protected Image Assets', tag: 'Gallery' },
'results': { title: '验证报告', subtitle: 'Validation & Analysis Reports', tag: 'Reports' }
}
return map[subpageType.value] || { title: '资源详情', subtitle: '', tag: 'Data' }
})
// === 1: ===
const currentStatus = ref('all')
const statusTabs = [
{ key: 'all', label: '全部' },
{ key: 'running', label: '运行中' },
{ key: 'completed', label: '已完成' },
{ key: 'failed', label: '失败' }
]
const allTasks = [
{ id: '1024', name: '风景图通用防护', status: 'completed', createdAt: '2023-11-20' },
{ id: '1025', name: '人脸隐私保护', status: 'running', createdAt: '2023-11-21' },
{ id: '1023', name: '测试任务失败案例', status: 'failed', createdAt: '2023-11-19' },
{ id: '1022', name: '批量艺术风格', status: 'completed', createdAt: '2023-11-18' }
]
const filteredTasks = computed(() => {
if (currentStatus.value === 'all') return allTasks
return allTasks.filter(t => t.status === currentStatus.value)
})
const statusLabel = (s) => {
const map = { running: '运行中', completed: '已完成', failed: '失败' }
return map[s] || s
}
// === 2: ===
const protectedImages = [
{ id: 'P01', taskName: '任务 #1024', type: '通用防护' },
{ id: 'P02', taskName: '任务 #1025', type: '人脸保护' },
{ id: 'P03', taskName: '任务 #1022', type: '风格迁移' },
{ id: 'P04', taskName: '任务 #1022', type: '风格迁移' }
]
// === 3: ===
const validationResults = [
{ id: 'R01', taskName: '微调效果对比-A组', compareType: '样例图', resultType: '图像报告' },
{ id: 'R02', taskName: 'PSNR指标分析', compareType: '数据表', resultType: 'CSV表格' }
]
</script>
<style scoped>
/* 基础布局复用 */
.subpage-layout { width: 100%; height: 100%; padding: 40px; display: flex; justify-content: center; align-items: center; background: var(--color-bg-primary); }
.layout-grid { display: grid; grid-template-columns: 300px 1fr; gap: 30px; width: 100%; max-width: 1400px; height: 85vh; }
.grid-sidebar { height: 100%; overflow: hidden; }
.grid-main { height: 100%; min-width: 0; }
.content-card { height: 100%; display: flex; flex-direction: column; padding: 0; background: #ffffff; }
.card-header { padding: 30px 40px; border-bottom: 1px solid rgba(0,0,0,0.05); display: flex; justify-content: space-between; align-items: flex-start; }
.subtitle { color: var(--color-text-muted); font-size: 0.9rem; margin-top: 5px; }
.tag { background: var(--color-contrast-dark); color: #fff; padding: 6px 16px; border-radius: 20px; font-weight: 700; font-size: 0.8rem; }
.card-body { padding: 40px; flex: 1; overflow-y: auto; }
/* === 筛选 Tab === */
.filter-tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
}
.tab-item {
padding: 8px 16px;
border-radius: 8px;
font-size: 0.9rem;
color: var(--color-text-muted);
cursor: pointer;
transition: all 0.2s;
}
.tab-item:hover { background: #f8f9fa; }
.tab-item.active { background: var(--color-contrast-dark); color: #fff; font-weight: 600; }
/* === 列表卡片样式 (用于任务和结果) === */
.data-list { display: flex; flex-direction: column; gap: 15px; }
.list-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border: 1px solid #eee;
border-radius: 12px;
transition: all 0.2s;
background: #fcfcfc;
}
.list-card:hover { border-color: var(--color-contrast-dark); background: #fff; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
.lc-left { display: flex; gap: 20px; align-items: center; }
.lc-id { font-weight: 700; color: var(--color-accent-secondary); background: rgba(255, 159, 28, 0.1); padding: 4px 8px; border-radius: 6px; font-size: 0.9rem; }
.lc-info { display: flex; flex-direction: column; }
.lc-title { font-weight: 600; font-size: 1rem; color: var(--color-text-main); }
.lc-date { font-size: 0.8rem; color: var(--color-text-muted); }
.lc-right { display: flex; align-items: center; gap: 15px; }
.status-badge { font-size: 0.85rem; font-weight: 600; padding: 4px 10px; border-radius: 99px; }
.status-badge.completed { color: #2e7d32; background: #e8f5e9; }
.status-badge.running { color: #1976d2; background: #e3f2fd; }
.status-badge.failed { color: #c62828; background: #ffebee; }
.sm { font-size: 0.8rem; padding: 0.4em 1em; }
/* === 结果列表特有样式 === */
.result-style .lc-icon { width: 40px; height: 40px; background: #f1f3f5; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: var(--color-text-muted); }
.result-style .lc-desc { font-size: 0.85rem; color: var(--color-text-muted); }
.lc-actions { display: flex; gap: 15px; font-size: 1.1rem; color: var(--color-text-muted); }
.action-icon { cursor: pointer; transition: color 0.2s; }
.action-icon:hover { color: var(--color-contrast-dark); }
/* === 图片网格 === */
.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
.img-card { border: 1px solid #eee; border-radius: 12px; overflow: hidden; transition: all 0.2s; }
.img-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.05); }
.img-placeholder { height: 150px; background: #f1f3f5; display: flex; align-items: center; justify-content: center; color: #adb5bd; font-size: 2rem; }
.img-meta { padding: 15px; display: flex; flex-direction: column; }
.img-title { font-weight: 600; font-size: 0.95rem; }
.img-tag { font-size: 0.8rem; color: var(--color-text-muted); margin-top: 5px; }
.empty-hint { text-align: center; color: var(--color-text-muted); padding: 40px; }
</style>

@ -0,0 +1,212 @@
<template>
<div class="page5-container">
<!-- 1. 顶部用户信息卡片 -->
<div class="header-section">
<div class="user-profile-card ui-card glass">
<div class="avatar-circle">
{{ userInitials }}
</div>
<div class="user-info">
<div class="name-row">
<h1>{{ userData.username }}</h1>
<span class="role-badge" :class="userData.role">
{{ userData.role === 'admin' ? '管理员' : '普通用户' }}
</span>
</div>
<p class="email">{{ userData.email || 'loading...' }}</p>
</div>
<div class="logout-wrapper">
<button class="ui-btn glass circle" @click="handleLogout" title="退出登录">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
</div>
</div>
<!-- 2. 统计数据看板 (改为3列布局) -->
<div class="stats-grid">
<div class="stat-item ui-card solid">
<div class="stat-label">总任务</div>
<div class="stat-val">{{ userStats?.total_tasks || 0 }}</div>
</div>
<div class="stat-item ui-card solid">
<div class="stat-label">已完成</div>
<div class="stat-val success">{{ userStats?.completed_tasks || 0 }}</div>
</div>
<div class="stat-item ui-card solid">
<div class="stat-label">处理中</div>
<div class="stat-val warning">{{ userStats?.processing_tasks || 0 }}</div>
</div>
</div>
<!-- 3. 功能菜单 Grid -->
<h3 class="section-title">账户设置</h3>
<div class="settings-grid">
<!-- 编辑资料 -->
<div class="ui-card solid setting-item interactive" @click="openSub('profile')">
<div class="icon-box"><i class="fas fa-user-edit"></i></div>
<div class="text">
<h4>编辑资料</h4>
<p>查看与修改个人基本信息</p>
</div>
<i class="fas fa-chevron-right arrow"></i>
</div>
<!-- 修改密码 -->
<div class="ui-card solid setting-item interactive" @click="openSub('password')">
<div class="icon-box"><i class="fas fa-key"></i></div>
<div class="text">
<h4>修改密码</h4>
<p>更新账户登录密码</p>
</div>
<i class="fas fa-chevron-right arrow"></i>
</div>
<!-- 用户配置 -->
<div class="ui-card solid setting-item interactive" @click="openSub('config')">
<div class="icon-box"><i class="fas fa-sliders-h"></i></div>
<div class="text">
<h4>系统配置</h4>
<p>设置默认算法与参数偏好</p>
</div>
<i class="fas fa-chevron-right arrow"></i>
</div>
<!-- 管理员入口 (仅管理员可见) -->
<div v-if="userData.role === 'admin'" class="ui-card gradient setting-item interactive" @click="openSub('admin')">
<div class="icon-box dark"><i class="fas fa-users-cog"></i></div>
<div class="text">
<h4>用户管理</h4>
<p>Admin Console</p>
</div>
<i class="fas fa-chevron-right arrow"></i>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, inject } from 'vue'
import { useRouter } from 'vue-router'
import { authGetProfile, getUserStats, authLogout } from '@/api/index'
const router = useRouter()
const openSubpage = inject('openSubpage')
const userData = ref({ username: 'User', email: '', role: 'user' })
const userStats = ref(null)
const userInitials = computed(() => {
return (userData.value.username?.[0] || 'U').toUpperCase()
})
const openSub = (type) => {
openSubpage('page5', type)
}
const handleLogout = async () => {
if(confirm('确定要退出登录吗?')) {
try { await authLogout() } catch(e){}
localStorage.removeItem('access_token')
router.push('/login')
}
}
const fetchData = async () => {
try {
const [profileRes, statsRes] = await Promise.all([
authGetProfile(),
getUserStats()
])
if (profileRes?.user) userData.value = profileRes.user
if (statsRes?.stats) userStats.value = statsRes.stats
} catch (error) {
console.error(error)
}
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.page5-container {
padding: 100px 10% 50px;
height: 100vh;
overflow-y: auto;
background: var(--color-bg-secondary);
}
/* 顶部用户卡片 */
.header-section { margin-bottom: 30px; }
.user-profile-card {
display: flex;
align-items: center;
padding: 30px 40px;
gap: 30px;
}
.avatar-circle {
width: 80px; height: 80px;
border-radius: 50%;
background: var(--color-contrast-dark);
color: #fff;
font-size: 2.5rem;
font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.user-info { flex: 1; }
.name-row { display: flex; align-items: center; gap: 15px; margin-bottom: 5px; }
.name-row h1 { margin: 0; font-size: 2rem; }
.role-badge { padding: 4px 10px; border-radius: 6px; font-size: 0.8rem; font-weight: 700; text-transform: uppercase; }
.role-badge.user { background: #e0e0e0; color: #555; }
.role-badge.admin { background: var(--color-accent-secondary); color: #fff; }
.email { color: var(--color-text-muted); margin: 0; }
/* 统计 Grid - 改为3列 */
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 修改这里3列均分 */
gap: 20px;
margin-bottom: 40px;
}
.stat-item {
padding: 20px;
text-align: center;
}
.stat-label { font-size: 0.9rem; color: var(--color-text-muted); margin-bottom: 5px; }
.stat-val { font-size: 1.8rem; font-weight: 800; color: var(--color-text-main); }
.stat-val.success { color: #2e7d32; }
.stat-val.warning { color: var(--color-accent-secondary); }
/* 设置菜单 */
.section-title { font-size: 1.2rem; color: var(--color-text-muted); margin-bottom: 20px; }
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
padding-bottom: 50px;
}
.setting-item {
padding: 20px 25px;
display: flex;
align-items: center;
gap: 20px;
}
.setting-item:hover { transform: translateY(-3px); box-shadow: 0 10px 20px rgba(0,0,0,0.05); }
.icon-box { width: 50px; height: 50px; background: var(--color-bg-primary); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; color: var(--color-contrast-dark); }
.icon-box.dark { background: rgba(0,0,0,0.2); color: #fff; }
.text h4 { margin: 0 0 5px 0; font-size: 1.1rem; }
.text p { margin: 0; font-size: 0.9rem; color: var(--color-text-muted); }
.arrow { margin-left: auto; color: var(--color-text-muted); opacity: 0.5; }
</style>

@ -0,0 +1,197 @@
<template>
<div class="subpage-layout">
<div class="center-wrapper">
<div class="ui-card solid content-card">
<div class="card-header">
<div>
<h2>{{ pageInfo.title }}</h2>
<p class="subtitle">{{ pageInfo.subtitle }}</p>
</div>
<span class="tag">Setting</span>
</div>
<div class="card-body">
<!-- === 1. 修改密码表单 === -->
<div v-if="subpageType === 'password'" class="form-container">
<div class="form-group">
<label>当前密码</label>
<input type="password" v-model="pwdForm.oldPassword" class="ui-input" />
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" v-model="pwdForm.newPassword" class="ui-input" />
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" v-model="pwdForm.confirmPassword" class="ui-input" />
</div>
<div class="action-row">
<button class="ui-btn gradient rect" @click="submitPassword"></button>
</div>
</div>
<!-- === 2. 用户配置表单 === -->
<div v-else-if="subpageType === 'config'" class="form-container">
<div class="form-group">
<label>默认扰动算法</label>
<select v-model="configForm.perturbation_configs_id" class="ui-input">
<option :value="null">不设置默认值</option>
<option value="adv_noise">AdvNoise v1.0</option>
<option value="mist">Mist</option>
</select>
</div>
<div class="form-group">
<label>默认强度 (0-255)</label>
<input type="number" v-model.number="configForm.perturbation_intensity" class="ui-input" />
</div>
<div class="action-row">
<button class="ui-btn gradient rect" @click="submitConfig"></button>
</div>
</div>
<!-- === 3. 编辑资料 (只读提示) === -->
<div v-else-if="subpageType === 'profile'" class="info-view">
<div class="notice-box">
<i class="fas fa-info-circle"></i>
<p>当前系统暂不支持用户自行修改用户名邮箱如需变更请联系管理员</p>
</div>
</div>
<!-- === 4. 管理员用户列表 === -->
<div v-else-if="subpageType === 'admin'" class="admin-view">
<div class="list-header">
<input type="text" placeholder="搜索用户..." class="ui-input sm" v-model="searchKeyword" />
<button class="ui-btn solid sm" @click="fetchAdminData"></button>
</div>
<div class="user-list">
<div class="list-head-row">
<span>ID</span><span>用户名</span><span>角色</span><span>操作</span>
</div>
<div v-for="u in adminUsers" :key="u.id" class="list-row">
<span>#{{ u.id }}</span>
<span class="u-name">{{ u.username }}</span>
<span>
<span class="role-tag" :class="u.role">{{ u.role }}</span>
</span>
<div class="actions">
<button class="text-btn">编辑</button>
<button class="text-btn danger">删除</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { authChangePassword, getUserConfig, updateUserConfig, getAdminUserList } from '@/api/index'
const route = useRoute()
const router = useRouter()
const subpageType = computed(() => route.params.subpage)
//
const pageInfo = computed(() => {
const map = {
password: { title: '修改密码', subtitle: 'Update Security Credentials' },
config: { title: '用户配置', subtitle: 'System Preferences' },
profile: { title: '编辑资料', subtitle: 'Basic Information' },
admin: { title: '用户管理', subtitle: 'Administrator Console' }
}
return map[subpageType.value] || { title: '设置', subtitle: '' }
})
// --- ---
const pwdForm = ref({ oldPassword: '', newPassword: '', confirmPassword: '' })
const configForm = ref({ perturbation_configs_id: null, perturbation_intensity: null })
const adminUsers = ref([])
const searchKeyword = ref('')
// --- ---
// 1.
const submitPassword = async () => {
if (pwdForm.value.newPassword !== pwdForm.value.confirmPassword) return alert('两次密码不一致')
try {
await authChangePassword({
old_password: pwdForm.value.oldPassword,
new_password: pwdForm.value.newPassword
})
alert('修改成功,请重新登录')
router.push('/login')
} catch (e) { console.error(e) }
}
// 2.
const fetchConfig = async () => {
const res = await getUserConfig()
if (res?.config) configForm.value = res.config
}
const submitConfig = async () => {
await updateUserConfig(configForm.value)
alert('配置已保存')
}
// 3. (Mock)
const fetchAdminData = async () => {
// const res = await getAdminUserList()
// if (res?.users) adminUsers.value = res.users
// Mock
adminUsers.value = [
{ id: 1, username: 'admin', role: 'admin' },
{ id: 2, username: 'test_user', role: 'user' },
{ id: 3, username: 'guest', role: 'user' }
]
}
//
onMounted(() => {
if (subpageType.value === 'config') fetchConfig()
if (subpageType.value === 'admin') fetchAdminData()
})
</script>
<style scoped>
.subpage-layout { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; background: rgba(0,0,0,0.4); backdrop-filter: blur(5px); }
.center-wrapper { width: 100%; max-width: 600px; padding: 20px; }
.content-card { background: #fff; display: flex; flex-direction: column; max-height: 80vh; }
.card-header { padding: 25px 30px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; }
.subtitle { font-size: 0.9rem; color: #999; margin-top: 5px; }
.tag { background: var(--color-contrast-dark); color: #fff; padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; height: fit-content; }
.card-body { padding: 30px; overflow-y: auto; }
/* 表单样式 */
.form-container { display: flex; flex-direction: column; gap: 20px; }
.form-group label { display: block; font-weight: 600; margin-bottom: 8px; font-size: 0.95rem; }
.ui-input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; }
.ui-input:focus { border-color: var(--color-accent-secondary); outline: none; }
.action-row { margin-top: 10px; display: flex; justify-content: flex-end; }
/* 提示框 */
.notice-box { background: #f8f9fa; padding: 20px; border-radius: 8px; display: flex; gap: 15px; align-items: center; color: #666; }
.notice-box i { font-size: 1.5rem; color: var(--color-accent-secondary); }
/* 管理员列表 */
.list-header { display: flex; gap: 10px; margin-bottom: 15px; }
.sm { padding: 8px 12px; font-size: 0.9rem; }
.user-list { border: 1px solid #eee; border-radius: 8px; }
.list-head-row { display: grid; grid-template-columns: 1fr 2fr 1fr 1fr; padding: 10px 15px; background: #f8f9fa; font-weight: 600; font-size: 0.9rem; color: #666; }
.list-row { display: grid; grid-template-columns: 1fr 2fr 1fr 1fr; padding: 12px 15px; border-top: 1px solid #eee; align-items: center; font-size: 0.95rem; }
.u-name { font-weight: 600; }
.role-tag { font-size: 0.8rem; padding: 2px 6px; border-radius: 4px; background: #eee; }
.role-tag.admin { background: var(--color-contrast-dark); color: #fff; }
.text-btn { background: none; border: none; color: var(--color-accent-secondary); cursor: pointer; margin-right: 10px; font-size: 0.9rem; }
.text-btn.danger { color: #dc3545; }
</style>

@ -1,83 +0,0 @@
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { authRegister } from '@/api/index'
const router = useRouter()
const form = ref({ username: '', email: '', password: '' })
const loading = ref(false)
const handleRegister = async () => {
if (!form.value.username || !form.value.password || !form.value.email) {
alert('请填写完整注册信息')
return
}
loading.value = true
try {
// : 201 Created, body: { message: '', user: ... }
const res = await authRegister(form.value)
alert(res.message || '注册成功,请前往登录')
router.push('/login')
} catch (error) {
console.error('Register Error:', error)
} finally {
loading.value = false
}
}
</script>
<!-- Template 同原样确保 button disabled 状态即可 -->
<template>
<div class="shared-page-container auth-bg">
<div class="form-container auth-card">
<h2 class="page-title">用户注册</h2>
<div class="form-row">
<div class="form-group full-width">
<label>用户名</label>
<input type="text" v-model="form.username" class="std-input" placeholder="设置用户名" />
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label>邮箱</label>
<input type="email" v-model="form.email" class="std-input" placeholder="绑定的邮箱" />
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label>密码</label>
<input type="password" v-model="form.password" class="std-input" placeholder="设置密码" />
</div>
</div>
<div class="form-actions">
<button class="start-btn" @click="handleRegister" :disabled="loading">注册</button>
<p class="hint-text" style="margin-top: 1rem; text-align: center;">
已有账号 <router-link to="/login">去登录</router-link>
</p>
</div>
</div>
</div>
</template>
<style scoped>
.auth-bg {
background-color: #e0e0e0;
}
.auth-card {
max-width: 30rem;
min-height: auto;
padding: 3rem;
}
.full-width {
width: 100%;
}
.start-btn {
width: 100%;
}
.start-btn:disabled {
background-color: #999;
}
</style>

@ -1,122 +0,0 @@
<template>
<div class="shared-page-container topic-bg">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="shared-card-grid">
<!-- Card 1 -->
<div class="shared-card">
<h3 class="shared-card-title">防风格迁移</h3>
<div class="shared-card-visual style-visual">
<span>(背景图设计)</span>
</div>
<div class="shared-action-group">
<button class="shared-btn" @click="openModal('style')"></button>
<button class="shared-btn shared-btn-primary" @click="startTopic('style')"></button>
</div>
</div>
<!-- Card 2 -->
<div class="shared-card">
<h3 class="shared-card-title">防人脸编辑</h3>
<div class="shared-card-visual face-visual">
<span>(背景图设计)</span>
</div>
<div class="shared-action-group">
<button class="shared-btn" @click="openModal('face')"></button>
<button class="shared-btn shared-btn-primary" @click="startTopic('face')"></button>
</div>
</div>
<!-- Card 3 -->
<div class="shared-card">
<h3 class="shared-card-title">防定制生成</h3>
<div class="shared-card-visual custom-visual">
<span>(背景图设计)</span>
</div>
<div class="shared-action-group">
<button class="shared-btn" @click="openModal('custom')"></button>
<button class="shared-btn shared-btn-primary" @click="startTopic('custom')"></button>
</div>
</div>
</div>
<!-- Modal -->
<div v-if="isModalOpen" class="shared-modal-overlay" @click="closeModal">
<div class="shared-modal-content" @click.stop>
<h3 class="shared-modal-title">{{ currentModalTitle }}</h3>
<p class="shared-modal-text">{{ currentModalDesc }}</p>
<button class="shared-modal-close-btn" @click="closeModal"></button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isModalOpen = ref(false)
const currentModalTitle = ref('')
const currentModalDesc = ref('')
const modalContent = {
style: {
title: '防风格迁移',
desc: '针对艺术风格迁移算法的防护,保护您的原创作品风格不被轻易模仿。'
},
face: {
title: '防人脸编辑',
desc: '针对人脸编辑和深度伪造DeepFake技术的防护保障肖像权安全。'
},
custom: {
title: '防定制生成',
desc: '针对LoRA等定制化模型训练的防护防止您的作品被用于训练特定风格的生成模型。'
}
}
const goBack = () => {
router.back()
}
const openModal = (type) => {
const content = modalContent[type]
currentModalTitle.value = content.title
currentModalDesc.value = content.desc
isModalOpen.value = true
}
const closeModal = () => {
isModalOpen.value = false
}
const startTopic = (type) => {
if (type === 'style') {
router.push('/topic-protection/style-transfer')
} else {
// Future: Navigate to specific subpage, e.g., /topic/face
alert(`即将进入 ${modalContent[type].title} 功能页`)
}
}
</script>
<style scoped>
.topic-bg {
background: #ae78e439;
}
.style-visual {
background: linear-gradient(135deg, #fff 0%, #f0f0ff 100%);
}
.face-visual {
background: linear-gradient(135deg, #fff 0%, #fff0f0 100%);
}
.custom-visual {
background: linear-gradient(135deg, #fff 0%, #f0fff0 100%);
}
</style>

@ -1,102 +0,0 @@
/* CommonStyle_acc.css - 管理与表单类页面特有样式 */
.page-container {
background-color: #4cc4bc8a; /* 特有背景色 */
justify-content: center; /* 上下左右居中 */
align-items: center;
}
/* 中央内容卡片 (Account特有比通用的 form-container 小) */
.central-card {
background: white;
border: 0.125rem solid #000;
width: 40rem;
max-width: 95%;
padding: 2rem 3rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* 表单内部结构 (Account特有) */
.form-body {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-item {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.form-item label {
font-size: 0.95rem;
color: #555;
font-weight: 500;
}
/* 底部按钮区域 (Account特有) */
.form-actions {
margin-top: 1.5rem;
text-align: center;
}
/* 主按钮 (Account特有尺寸稍小) */
.primary-btn {
padding: 0.6rem 2.5rem;
border: 1px solid #000;
background: #fff;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
.primary-btn:hover { background: #f5f5f5; }
/* 管理表格通用样式 */
.toolbar {
margin-bottom: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-box { display: flex; gap: 0.5rem; }
.search-btn {
padding: 0.5rem 1rem;
border: 1px solid #333;
background: #333;
color: white;
cursor: pointer;
font-size: 0.9rem;
}
.search-btn:hover { background: #444; }
.table-wrapper { flex: 1; overflow-y: auto; }
.std-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.std-table th, .std-table td {
border: 1px solid #eee;
padding: 0.5rem 0.75rem;
text-align: left;
}
.std-table thead { background: #f5f5f5; }
.link-btn {
border: none;
background: none;
color: #1976d2;
cursor: pointer;
margin-right: 0.5rem;
padding: 0;
font-size: 0.9rem;
}
.link-btn:hover { text-decoration: underline; }

@ -1,432 +0,0 @@
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import {
getAdminUserList,
createAdminUser,
updateAdminUser,
deleteAdminUser,
getSystemStats
} from '@/api/index'
const router = useRouter()
const goBack = () => router.push('/account')
//
const loading = ref(false)
const users = ref([])
const total = ref(0)
const currentPage = ref(1)
const systemStats = ref(null)
const searchKeyword = ref('')
//
const showModal = ref(false)
const isEditMode = ref(false)
const currentUserId = ref(null)
const formData = ref({
username: '',
email: '',
password: '',
role: 'user',
is_active: true
})
// ()
const fetchData = async () => {
loading.value = true
try {
//
const params = {
page: currentPage.value,
per_page: 20 // 20
}
// ( q username)
// list_users
// "访"
//
if (searchKeyword.value.trim()) {
params.q = searchKeyword.value.trim()
}
// +
const listPromise = getAdminUserList(params)
const statsPromise = getSystemStats().catch(() => ({ stats: null }))
const [listRes, statsRes] = await Promise.all([listPromise, statsPromise])
//
if (listRes) {
users.value = listRes.users || []
total.value = listRes.total || 0
}
if (statsRes && statsRes.stats) {
systemStats.value = statsRes.stats
}
} catch (error) {
console.error('加载数据失败', error)
// request.js
users.value = []
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
currentPage.value = 1 //
fetchData()
}
//
const handleDelete = async (user) => {
if (!confirm(`确定要删除用户 "${user.username}" (ID: ${user.id}) 吗?此操作不可恢复。`)) return
try {
await deleteAdminUser(user.id)
alert('删除成功')
//
fetchData()
} catch (error) {
console.error(error)
}
}
// (/)
const submitForm = async () => {
//
if (!formData.value.username) { alert('用户名必填'); return }
if (!formData.value.email) { alert('邮箱必填'); return }
//
if (!isEditMode.value && !formData.value.password) { alert('初始密码必填'); return }
try {
if (isEditMode.value) {
// --- ---
const updateData = {
username: formData.value.username,
email: formData.value.email,
role: formData.value.role,
is_active: formData.value.is_active
}
// password
if (formData.value.password) {
updateData.password = formData.value.password
}
await updateAdminUser(currentUserId.value, updateData)
alert('用户信息更新成功')
} else {
// --- ---
await createAdminUser({
username: formData.value.username,
email: formData.value.email,
password: formData.value.password,
role: formData.value.role
// active is_active
})
alert('用户创建成功')
}
closeModal()
fetchData() //
} catch (error) {
console.error(error)
}
}
//
const openCreateModal = () => {
isEditMode.value = false
currentUserId.value = null
//
formData.value = {
username: '',
email: '',
password: '',
role: 'user',
is_active: true
}
showModal.value = true
}
//
const openEditModal = (user) => {
isEditMode.value = true
currentUserId.value = user.id
//
formData.value = {
username: user.username,
email: user.email,
password: '', //
role: user.role,
is_active: user.is_active
}
showModal.value = true
}
const closeModal = () => { showModal.value = false }
//
const onSearchKeyup = (e) => {
if (e.key === 'Enter') handleSearch()
}
onMounted(() => {
fetchData()
})
</script>
<template>
<div class="page-container">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="central-card wide-card">
<h2 class="page-title">管理员控制台</h2>
<!-- 1. 系统统计看板 (真实数据) -->
<div class="stats-row" v-if="systemStats">
<div class="stat-item">
<div class="stat-val">{{ systemStats.users.total }}</div>
<div class="stat-label">总用户数</div>
</div>
<div class="stat-item">
<div class="stat-val">{{ systemStats.tasks.total }}</div>
<div class="stat-label">总任务数</div>
</div>
<div class="stat-item">
<div class="stat-val">{{ systemStats.images.total }}</div>
<div class="stat-label">处理图片</div>
</div>
<div class="stat-item">
<div class="stat-val">{{ systemStats.users.active }}</div>
<div class="stat-label">活跃用户</div>
</div>
</div>
<div class="stats-row" v-else>
<div style="width:100%; text-align:center; color:#999;">统计数据加载中...</div>
</div>
<!-- 2. 工具栏 -->
<div class="toolbar">
<div class="search-box">
<input v-model="searchKeyword" type="text" placeholder="搜索用户名..." class="std-input search-width"
@keyup="onSearchKeyup" />
<button class="search-btn" @click="handleSearch"></button>
</div>
<button class="primary-btn" @click="openCreateModal"></button>
</div>
<!-- 3. 用户列表表格 -->
<div class="table-wrapper">
<table class="std-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="6" class="empty-row">数据加载中...</td>
</tr>
<tr v-else-if="users.length === 0">
<td colspan="6" class="empty-row">暂无用户数据</td>
</tr>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>
<span :class="{ 'tag-admin': user.role === 'admin' }">{{ user.role }}</span>
</td>
<td>
<!-- 后端返回 bool int做兼容处理 -->
<span :class="user.is_active ? 'text-green' : 'text-red'">
{{ user.is_active ? '正常' : '禁用' }}
</span>
</td>
<td>
<button class="link-btn" @click="openEditModal(user)"></button>
<button class="link-btn delete-btn" @click="handleDelete(user)"></button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 简单的分页提示 (如果需要完整分页组件可扩展) -->
<div class="pagination-info" v-if="total > 0">
{{ total }} 条记录
</div>
</div>
<!-- 4. 新增/编辑 用户模态框 -->
<div v-if="showModal" class="shared-modal-overlay" @click="closeModal">
<div class="shared-modal-content form-modal" @click.stop>
<h3 class="shared-modal-title">{{ isEditMode ? '编辑用户' : '新增用户' }}</h3>
<div class="modal-form">
<div class="form-group">
<label>用户名</label>
<input type="text" v-model="formData.username" class="std-input" placeholder="请输入用户名" />
</div>
<div class="form-group">
<label>邮箱</label>
<input type="email" v-model="formData.email" class="std-input" placeholder="请输入邮箱" />
</div>
<div class="form-group">
<label>密码</label>
<input type="password" v-model="formData.password" class="std-input"
:placeholder="isEditMode ? '不修改请留空' : '请输入初始密码'" />
</div>
<div class="form-group">
<label>角色</label>
<select v-model="formData.role" class="std-select">
<option value="user">普通用户 (user)</option>
<option value="admin">管理员 (admin)</option>
</select>
</div>
<!-- 只有编辑模式才显示状态修改或者看后端是否允许创建时指定状态 -->
<div class="form-group">
<label>状态</label>
<div class="radio-group small-radio">
<label><input type="radio" :value="true" v-model="formData.is_active" /> 正常</label>
<label><input type="radio" :value="false" v-model="formData.is_active" /> 禁用</label>
</div>
</div>
</div>
<div class="modal-actions">
<button class="cancel-btn" @click="closeModal"></button>
<button class="confirm-btn" @click="submitForm"></button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
@import './AccountCenterStyleSub.css';
.wide-card {
width: 70rem;
height: 85vh;
padding: 2rem;
}
.search-width {
width: 240px;
}
.stats-row {
display: flex;
justify-content: space-around;
background: #f9f9f9;
padding: 1rem;
margin-bottom: 1.5rem;
border: 1px dashed #ccc;
border-radius: 4px;
}
.stat-item {
text-align: center;
}
.stat-val {
font-size: 1.5rem;
font-weight: bold;
color: #333;
}
.stat-label {
font-size: 0.9rem;
color: #666;
}
.text-green {
color: green;
font-weight: bold;
}
.text-red {
color: red;
font-weight: bold;
}
.tag-admin {
background: #333;
color: white;
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.8rem;
}
.delete-btn {
color: #d32f2f;
margin-left: 0.5rem;
}
.delete-btn:hover {
color: red;
}
.form-modal {
width: 30rem;
text-align: left;
}
.modal-form {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
margin-bottom: 1.5rem;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.cancel-btn,
.confirm-btn {
padding: 0.5rem 1.5rem;
cursor: pointer;
}
.cancel-btn {
background: white;
border: 1px solid #ccc;
}
.confirm-btn {
background: #333;
color: white;
border: none;
}
.small-radio {
height: auto;
gap: 1rem;
}
.pagination-info {
text-align: right;
font-size: 0.9rem;
color: #666;
margin-top: 0.5rem;
}
</style>

@ -1,100 +0,0 @@
<template>
<div class="page-container">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="central-card">
<h2 class="page-title">修改密码</h2>
<div class="form-body">
<div class="form-item">
<label>当前密码</label>
<input
type="password"
v-model="form.oldPassword"
placeholder="请输入当前密码"
class="std-input"
/>
</div>
<div class="form-item">
<label>新密码</label>
<input
type="password"
v-model="form.newPassword"
placeholder="请输入新密码"
class="std-input"
/>
</div>
<div class="form-item">
<label>确认新密码</label>
<input
type="password"
v-model="form.confirmPassword"
placeholder="请再次输入新密码"
class="std-input"
/>
</div>
</div>
<div class="form-actions">
<button class="primary-btn" @click="handleSubmit"></button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { authChangePassword } from '@/api/index'
const router = useRouter()
const form = ref({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const goBack = () => router.push('/account')
const handleSubmit = async () => {
// 1.
if (!form.value.oldPassword || !form.value.newPassword) {
alert('请输入旧密码和新密码')
return
}
if (form.value.newPassword !== form.value.confirmPassword) {
alert('两次输入的新密码不一致')
return
}
if (form.value.oldPassword === form.value.newPassword) {
alert('新密码不能与旧密码相同')
return
}
try {
// 2. API (线)
await authChangePassword({
old_password: form.value.oldPassword,
new_password: form.value.newPassword
})
// 3.
alert('密码修改成功,请使用新密码重新登录')
//
localStorage.removeItem('access_token')
localStorage.removeItem('user_info')
router.push('/login')
} catch (error) {
console.error('修改密码失败:', error)
// request.js
}
}
</script>
<style scoped>
@import './AccountCenterStyleSub.css';
.form-body { gap: 1.5rem; }
</style>

@ -1,52 +0,0 @@
<template>
<div class="page-container">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="central-card">
<h2 class="page-title">编辑资料</h2>
<div class="info-content">
<div class="notice-box">
<p><strong>功能说明</strong></p>
<p>当前系统暂不支持用户自行修改用户名邮箱</p>
<p>如需变更个人信息请联系系统管理员</p>
</div>
</div>
<div class="form-actions">
<button class="primary-btn" @click="goBack"></button>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const goBack = () => router.push('/account')
</script>
<style scoped>
@import './AccountCenterStyleSub.css';
.info-content {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}
.notice-box {
background: #f9f9f9;
border: 1px dashed #999;
padding: 2rem;
color: #555;
line-height: 1.8;
text-align: left;
border-radius: 4px;
}
</style>

@ -1,152 +0,0 @@
<template>
<div class="page-container">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="central-card">
<h2 class="page-title">用户配置</h2>
<div class="form-body" v-if="! loading">
<div class="form-item">
<label>默认扰动算法</label>
<select v-model="configForm.perturbation_configs_id" class="std-select">
<option :value="null">不设置默认值</option>
<option
v-for="algo in perturbationAlgorithms"
:key="algo.id"
:value="algo.id"
>
{{ algo.method_name }}
</option>
</select>
</div>
<div class="form-item">
<label>默认扰动强度 (0-255)</label>
<input
type="number"
v-model.number="configForm. perturbation_intensity"
class="std-input"
min="0"
max="255"
placeholder="请输入0-255之间的数值"
/>
</div>
<div class="form-item">
<label>默认微调算法</label>
<select v-model="configForm.finetune_config_id" class="std-select">
<option :value="null">不设置默认值</option>
<option
v-for="method in finetuneMethods"
:key="method.id"
:value="method.id"
>
{{ method.method_name }}
</option>
</select>
</div>
</div>
<div v-else class="loading-hint">
加载中...
</div>
<div class="form-actions">
<button class="primary-btn" @click="handleSubmit" :disabled="loading">保存配置</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getUserConfig, updateUserConfig, getAvailableAlgorithms } from '@/api/index'
const router = useRouter()
const loading = ref(true)
const configForm = ref({
perturbation_configs_id: null,
perturbation_intensity: null,
finetune_config_id: null
})
const perturbationAlgorithms = ref([])
const finetuneMethods = ref([])
const goBack = () => router.push('/account')
const fetchData = async () => {
loading.value = true
try {
const [configRes, algoRes] = await Promise.all([
getUserConfig(),
getAvailableAlgorithms()
])
if (configRes && configRes.config) {
configForm.value.perturbation_configs_id = configRes.config.perturbation_configs_id || null
configForm.value.perturbation_intensity = configRes.config.perturbation_intensity || null
configForm.value.finetune_config_id = configRes.config.finetune_config_id || null
}
if (algoRes) {
perturbationAlgorithms.value = algoRes.perturbation_algorithms || []
finetuneMethods.value = algoRes.finetune_methods || []
}
} catch (error) {
console. error('加载配置失败', error)
} finally {
loading. value = false
}
}
const handleSubmit = async () => {
if (configForm.value. perturbation_intensity !== null) {
const intensity = configForm.value.perturbation_intensity
if (intensity < 0 || intensity > 255) {
alert('扰动强度必须在0-255之间')
return
}
}
try {
const updateData = {}
if (configForm.value.perturbation_configs_id !== null) {
updateData.perturbation_configs_id = configForm.value.perturbation_configs_id
}
if (configForm.value. perturbation_intensity !== null) {
updateData.perturbation_intensity = configForm.value. perturbation_intensity
}
if (configForm. value.finetune_config_id !== null) {
updateData.finetune_config_id = configForm.value.finetune_config_id
}
await updateUserConfig(updateData)
alert('用户配置更新成功')
} catch (error) {
console.error('更新配置失败', error)
}
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
@import './AccountCenterStyleSub.css';
.form-body {
gap: 1.5rem;
}
.loading-hint {
text-align: center;
color: #999;
padding: 2rem;
}
</style>

@ -1,62 +0,0 @@
/* CommonStyle_ev.css - 效果验证页面特有样式 */
.page-container {
background-color: #f0f4f8; /* 特有背景色 */
}
/* 模态框内部结构 (EV特有) */
.modal-content {
background: white;
padding: 2rem;
border: 0.125rem solid #000;
border-radius: 0.5rem;
width: 30rem;
max-width: 90%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.resource-list {
max-height: 20rem;
overflow-y: auto;
border: 1px solid #eee;
}
.resource-item {
display: flex;
align-items: center;
padding: 0.8rem;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.resource-item:hover { background: #f9f9f9; }
.resource-item.selected { background: #eaf4ff; border-left: 4px solid #0056b3; }
.res-icon {
width: 3rem;
height: 3rem;
background: #ddd;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.8rem;
margin-right: 1rem;
}
.res-info { flex: 1; }
.res-name { font-weight: bold; font-size: 1rem; }
.res-date { font-size: 0.8rem; color: #888; }
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.cancel-btn, .confirm-btn {
padding: 0.5rem 1.5rem;
cursor: pointer;
}
.cancel-btn { background: white; border: 1px solid #ccc; }
.confirm-btn { background: #333; color: white; border: none; }

@ -1,223 +0,0 @@
<template>
<div class="page-container">
<TaskSideBar
:tasks="tasks"
@back="goBack"
@details="showTaskDetails"
/>
<div class="main-form-area">
<div class="form-container">
<div class="form-row">
<div class="form-group full-width">
<label>任务命名</label>
<input type="text" class="std-input" placeholder="例如:人脸隐私微调-批次A" v-model="formData.jobName" />
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label>选择微调算法</label>
<select v-model="formData.algorithm" class="std-select">
<option value="" disabled>请选择算法</option>
<option
v-for="method in finetuneMethods"
:key="method.id"
:value="method.id"
>
{{ method.method_name }} - {{ method.description }}
</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>选图片风格</label>
<div class="radio-group">
<label><input type="radio" value="face" v-model="formData.style" /> 人脸</label>
<label class="vip-option">
<input type="radio" value="art" v-model="formData.style" /> 绘画/艺术品
<span class="vip-tag">VIP</span>
</label>
</div>
<p class="hint-text">每种风格对应一种固定的提示词</p>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>选择数据源</label>
<div class="data-source-control">
<button class="source-btn" @click="openResourceModal">
{{ formData.sourceName || '从"防护加噪"任务中选数据源' }}
</button>
<span class="source-hint">从后端保存图中选择而非用户上传</span>
</div>
<p v-if="currentPairs.length" class="dataset-summary">
当前选择: <span class="highlight">{{ formData.sourceName }}</span>
包含 <span class="highlight">{{ currentPairs. length }}</span> 组原图 + 加噪图
</p>
</div>
</div>
<div class="form-row action-row">
<div class="info-display">
<p v-if="formData.sourceId && formData.jobName" class="ready-text">
已就绪: 准备基于{{ formData.sourceName }}执行微调
</p>
</div>
<button class="start-btn" @click="submitTask">, , </button>
</div>
</div>
</div>
<div v-if="isResourceModalOpen" class="modal-overlay" @click="closeResourceModal">
<div class="modal-content" @click.stop>
<h3>"已防护"任务中选择</h3>
<div class="resource-list">
<div v-for="res in protectedResources" :key="res.id" class="resource-item"
:class="{ selected: selectedResId === res.id }" @click="selectResource(res)">
<div class="res-icon">TASK</div>
<div class="res-info">
<div class="res-name">{{ res.name }}</div>
<div class="res-date">{{ res.date }}</div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="cancel-btn" @click="closeResourceModal"></button>
<button class="confirm-btn" @click="confirmResourceSelection"></button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import TaskSideBar from '@/components/TaskSideBar.vue'
import { getAvailableAlgorithms } from '@/api/index'
const router = useRouter()
const isResourceModalOpen = ref(false)
const selectedResId = ref(null)
const tempSelectedRes = ref(null)
const tasks = ref([
{ id: 'F-1', status: 'running', progress: 30 },
{ id: 'F-2', status: 'waiting', progress: 0 }
])
const finetuneMethods = ref([])
const protectedResources = [
{ id: 'T-1001', name: '防护加噪任务-风景', date: '2023-11-24 10:00' },
{ id: 'T-1002', name: '防护加噪任务-人脸', date: '2023-11-24 11:30' },
{ id: 'T-1003', name: '防护加噪任务-艺术', date: '2023-11-24 14:20' }
]
const formData = ref({
jobName: '',
algorithm: '',
style: 'face',
sourceId: '',
sourceName: ''
})
const resourceImagePairs = {
'T-1001': [
{ id: 'IMG-101', label: '风景原图A', originalUrl: '/mock/origin/landscape-a.jpg', noisyUrl: '/mock/noisy/landscape-a.jpg' },
{ id: 'IMG-102', label: '风景原图B', originalUrl: '/mock/origin/landscape-b.jpg', noisyUrl: '/mock/noisy/landscape-b.jpg' },
{ id: 'IMG-103', label: '风景原图C', originalUrl: '/mock/origin/landscape-c.jpg', noisyUrl: '/mock/noisy/landscape-c.jpg' }
],
'T-1002': [
{ id: 'IMG-201', label: '人脸原图A', originalUrl: '/mock/origin/face-a.jpg', noisyUrl: '/mock/noisy/face-a.jpg' },
{ id: 'IMG-202', label: '人脸原图B', originalUrl: '/mock/origin/face-b. jpg', noisyUrl: '/mock/noisy/face-b.jpg' },
{ id: 'IMG-203', label: '人脸原图C', originalUrl: '/mock/origin/face-c.jpg', noisyUrl: '/mock/noisy/face-c. jpg' }
],
'T-1003': [
{ id: 'IMG-301', label: '艺术原图A', originalUrl: '/mock/origin/art-a.jpg', noisyUrl: '/mock/noisy/art-a.jpg' },
{ id: 'IMG-302', label: '艺术原图B', originalUrl: '/mock/origin/art-b. jpg', noisyUrl: '/mock/noisy/art-b.jpg' }
]
}
const currentPairs = ref([])
const fineTuneResults = ref([])
const goBack = () => {
router.push('/effect-validation')
}
const showTaskDetails = () => {
router.push('/task-details')
}
const openResourceModal = () => {
isResourceModalOpen. value = true
selectedResId.value = formData.value.sourceId
tempSelectedRes.value = null
}
const closeResourceModal = () => {
isResourceModalOpen.value = false
}
const selectResource = (res) => {
selectedResId.value = res. id
tempSelectedRes.value = res
}
const confirmResourceSelection = () => {
if (tempSelectedRes.value) {
formData.value.sourceId = tempSelectedRes.value.id
formData.value.sourceName = tempSelectedRes.value. name
currentPairs.value = resourceImagePairs[tempSelectedRes.value.id] || []
fineTuneResults. value = []
}
closeResourceModal()
}
const fetchAlgorithms = async () => {
try {
const res = await getAvailableAlgorithms()
if (res && res. finetune_methods) {
finetuneMethods. value = res.finetune_methods
}
} catch (error) {
console.error('获取微调算法列表失败', error)
}
}
const submitTask = () => {
if (!formData.value.jobName. trim()) {
alert('请先输入微调任务名')
return
}
if (! formData.value. algorithm) {
alert('请先选择微调算法')
return
}
if (!formData.value.sourceId) {
alert('请先选择数据源')
return
}
if (! currentPairs.value. length) {
alert('所选数据源中暂无可用原图/加噪图')
return
}
alert('微调任务已提交')
}
onMounted(() => {
fetchAlgorithms()
})
</script>
<style scoped>
@import './EffectValidSub.css';
</style>

@ -1,158 +0,0 @@
<template>
<div class="page-container">
<TaskSideBar
:tasks="tasks"
@back="goBack"
@details="showTaskDetails"
/>
<div class="main-form-area">
<div class="form-container">
<div class="form-row">
<label style="width: 120px;">取任务名/编号</label>
<input type="text" v-model="formData.taskName" placeholder="输入任务名称" class="std-input" />
</div>
<div class="form-row">
<label>选择可视化类型</label>
<div class="radio-group">
<label>
<input type="radio" value="attention" v-model="formData.visType">
热力图 (Attention Map)
</label>
<label>
<input type="radio" value="frequency" v-model="formData.visType">
频域分析 (DCT Spectrum)
</label>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>选择已防护图像 (数据源)</label>
<button class="source-btn" @click="openResourceModal">
{{ formData.sourceName || '点击选择数据源' }}
</button>
<p v-if="formData.sourceName" class="dataset-summary">
已选择: <span class="highlight">{{ formData.sourceName }}</span>系统将自动检索其原始未防护版本进行对比
</p>
<p v-else class="hint-text">请选择一个任务将自动对比其原始版本</p>
</div>
</div>
<div class="form-row action-row">
<div class="info-display">
<p v-if="formData.sourceName && formData.taskName" class="ready-text">
已就绪: 将生成{{ formData.visType === 'attention' ? '热力图' : '频域图' }}报告
</p>
</div>
<button class="start-btn" @click="submitTask"></button>
</div>
</div>
</div>
<div v-if="isResourceModalOpen" class="modal-overlay" @click="closeResourceModal">
<div class="modal-content" @click.stop>
<h3>选择已防护图像</h3>
<div class="resource-list">
<div v-for="res in protectedResources" :key="res.id" class="resource-item"
:class="{ selected: selectedResId === res.id }" @click="selectResource(res)">
<div class="res-icon">IMG</div>
<div class="res-info">
<div class="res-name">{{ res.name }}</div>
<div class="res-date">{{ res.date }}</div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="cancel-btn" @click="closeResourceModal"></button>
<button class="confirm-btn" @click="confirmResourceSelection"></button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import TaskSideBar from '@/components/TaskSideBar.vue'
const router = useRouter()
const isResourceModalOpen = ref(false)
const selectedResId = ref(null)
const tempSelectedRes = ref(null)
const tasks = ref([
{ id: 'H-1', status: 'running', progress: 80 },
{ id: 'H-2', status: 'waiting', progress: 0 }
])
const protectedResources = [
{ id: 'P-101', name: '已防护_风景.jpg', date: '2023-11-24 10:00' },
{ id: 'P-102', name: '已防护_人像.png', date: '2023-11-24 11:30' },
{ id: 'P-103', name: '已防护_艺术画.png', date: '2023-11-24 14:20' }
]
const formData = ref({
taskName: '',
visType: 'attention',
sourceId: '',
sourceName: ''
})
const goBack = () => {
router.push('/effect-validation')
}
const showTaskDetails = () => {
router.push('/task-details')
}
const openResourceModal = () => {
isResourceModalOpen.value = true
selectedResId.value = formData.value.sourceId
tempSelectedRes.value = null
}
const closeResourceModal = () => {
isResourceModalOpen.value = false
}
const selectResource = (res) => {
selectedResId.value = res.id
tempSelectedRes.value = res
}
const confirmResourceSelection = () => {
if (tempSelectedRes.value) {
formData.value.sourceId = tempSelectedRes.value.id
formData.value.sourceName = tempSelectedRes.value.name
}
closeResourceModal()
}
const submitTask = () => {
if (!formData.value.taskName) {
alert('请输入任务名称')
return
}
if (!formData.value.sourceId) {
alert('请选择数据源')
return
}
if (!formData.value.visType) {
alert('请选择可视化类型')
return
}
console.log('Submitting Heatmap Task:', formData.value)
alert('热力图对比生成任务已提交')
}
</script>
<style scoped>
@import './EffectValidSub.css';
</style>

@ -1,181 +0,0 @@
<template>
<div class="page-container">
<TaskSideBar
:tasks="tasks"
@back="goBack"
@details="showTaskDetails"
/>
<div class="main-form-area">
<div class="form-container">
<div class="form-row">
<div class="form-group">
<label>选择原始图像数据源</label>
<button class="source-btn" @click="openResourceModal('noising')">
{{ formData.noisingTaskName || '点击选择' }}
</button>
<p class="hint-text">
从已完成的加噪任务中选择将原图图组X拿来用作对比
</p>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>选择微调生图数据源</label>
<button class="source-btn" @click="openResourceModal('fine-tuning')">
{{ formData.fineTuningTaskName || '点击选择' }}
</button>
<p class="hint-text">
从已完成的微调生图任务中选择将其生成结果Y和Y'图组分别和X进行对比评估
</p>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>当前任务图片概况</label>
<p v-if="selectedNoisingTask && selectedFineTuningTask" class="dataset-summary">
加噪任务含 <span class="highlight">{{ selectedNoisingTask.count }}</span> 张原图
微调任务生成 <span class="highlight">{{ selectedFineTuningTask.count }}</span> 组结果
</p>
<p v-else class="dataset-summary placeholder">
请先完成两个数据源的选择
</p>
</div>
</div>
<div class="form-row action-row">
<div class="info-display">
<p v-if="formData.noisingTaskId && formData.fineTuningTaskId" class="ready-text">
已就绪: 将对比{{ formData.noisingTaskName }}{{ formData.fineTuningTaskName }}
</p>
</div>
<button class="start-btn" @click="submitTask"></button>
</div>
</div>
</div>
<div v-if="isResourceModalOpen" class="modal-overlay" @click="closeResourceModal">
<div class="modal-content" @click.stop>
<h3>
<span v-if="modalType === 'noising'"> (A)</span>
<span v-else> (B)</span>
</h3>
<div class="resource-list">
<div v-for="res in currentResourceList" :key="res.id" class="resource-item"
:class="{ selected: selectedResId === res.id }" @click="selectResource(res)">
<div class="res-icon">TASK</div>
<div class="res-info">
<div class="res-name">{{ res.name }}</div>
<div class="res-date">{{ res.date }} ({{ res.count }})</div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="cancel-btn" @click="closeResourceModal"></button>
<button class="confirm-btn" @click="confirmResourceSelection"></button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import TaskSideBar from '@/components/TaskSideBar.vue'
const router = useRouter()
const isResourceModalOpen = ref(false)
const modalType = ref('')
const selectedResId = ref(null)
const tempSelectedRes = ref(null)
const selectedNoisingTask = ref(null)
const selectedFineTuningTask = ref(null)
const tasks = ref([
{ id: 'M-1', status: 'running', progress: 10 },
{ id: 'M-2', status: 'waiting', progress: 0 }
])
const noisingTasks = [
{ id: 'NZ-001', name: '加噪任务-数据集A', date: '2023-11-20 09:00', count: 100 },
{ id: 'NZ-002', name: '加噪任务-数据集B', date: '2023-11-21 14:20', count: 50 }
]
const fineTuningTasks = [
{ id: 'FT-001', name: '微调生图-实验X', date: '2023-11-25 10:00', count: 100 },
{ id: 'FT-002', name: '微调生图-实验Y', date: '2023-11-25 15:30', count: 50 }
]
const formData = ref({
noisingTaskId: '',
noisingTaskName: '',
fineTuningTaskId: '',
fineTuningTaskName: '',
metrics: ['PSNR', 'SSIM']
})
const currentResourceList = computed(() => {
if (modalType.value === 'noising') {
return noisingTasks
}
return fineTuningTasks
})
const goBack = () => {
router.push('/effect-validation')
}
const showTaskDetails = () => {
router.push('/task-details')
}
const openResourceModal = (type) => {
modalType.value = type
isResourceModalOpen.value = true
selectedResId.value = type === 'noising' ? formData.value.noisingTaskId : formData.value.fineTuningTaskId
tempSelectedRes.value = null
}
const closeResourceModal = () => {
isResourceModalOpen.value = false
}
const selectResource = (res) => {
selectedResId.value = res.id
tempSelectedRes.value = res
}
const confirmResourceSelection = () => {
if (tempSelectedRes.value) {
if (modalType.value === 'noising') {
formData.value.noisingTaskId = tempSelectedRes.value.id
formData.value.noisingTaskName = tempSelectedRes.value.name
selectedNoisingTask.value = tempSelectedRes.value
} else {
formData.value.fineTuningTaskId = tempSelectedRes.value.id
formData.value.fineTuningTaskName = tempSelectedRes.value.name
selectedFineTuningTask.value = tempSelectedRes.value
}
}
closeResourceModal()
}
const submitTask = () => {
if (!formData.value.noisingTaskId) {
alert('请选择加噪任务作为原始数据源')
return
}
if (!formData.value.fineTuningTaskId) {
alert('请选择微调生图任务作为对比数据源')
return
}
console.log('Submitting Metrics Task:', payload)
alert('指标分析任务已提交')
}
</script>
<style scoped>
@import './EffectValidSub.css';
</style>

@ -1,18 +0,0 @@
/* CommonStyle_gen.css - 通用防护页面特有样式 */
.page-container {
background-color: #f0f4f8; /* 特有背景色 */
}
/* 模式标题与介绍 (Gen特有) */
.mode-header {
font-size: 1.5rem;
margin: 0;
}
.mode-intro {
color: #666;
margin-top: -1rem;
border-bottom: 1px solid #eee;
padding-bottom: 1rem;
}

@ -1,118 +0,0 @@
<template>
<div class="page-container">
<TaskSideBar
:tasks="tasks"
@back="goBack"
@details="showTaskDetails"
/>
<div class="main-form-area">
<div class="form-container">
<h2 class="mode-header">快速模式</h2>
<p class="mode-intro">快速模式使用系统默认的最佳推荐配置您只需上传图片即可开始防护</p>
<div class="form-row">
<div class="form-group">
<label>取任务名/编号</label>
<input type="text" v-model="formData.taskName" placeholder="输入任务名称" class="std-input" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>选择图片风格</label>
<div class="radio-group">
<label><input type="radio" value="face" v-model="formData.style" /> 人脸</label>
<label class="vip-option">
<input type="radio" value="art" v-model="formData.style" /> 绘画/艺术品
<span class="vip-tag">VIP</span>
</label>
</div>
<p class="hint-text">每种风格对应一种固定的提示词</p>
</div>
</div>
<div class="form-row action-row">
<div class="upload-section">
<input
type="file"
ref="fileInput"
@change="handleFileChange"
style="display: none"
accept="image/*"
/>
<button class="upload-btn" @click="triggerFileUpload"></button>
<span class="file-name" v-if="formData.fileName">{{ formData.fileName }}</span>
<span class="file-name" v-else></span>
</div>
<button class="start-btn" @click="submitTask"></button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import TaskSideBar from '@/components/TaskSideBar.vue'
const router = useRouter()
const fileInput = ref(null)
const tasks = ref([
{ id: '1', status: 'running', progress: 45 },
{ id: '3', status: 'waiting', progress: 0 },
{ id: 'A', status: 'waiting', progress: 0 }
])
const showTaskDetails = () => {
router.push('/task-details')
}
const formData = ref({
taskName: '',
style: 'face',
fileName: '',
file: null
})
const goBack = () => {
router.push('/general-protection')
}
const triggerFileUpload = () => {
fileInput.value.click()
}
const handleFileChange = (event) => {
const file = event.target.files[0]
if (file) {
formData.value.fileName = file.name
formData.value.file = file
}
}
const submitTask = () => {
if (!formData.value.fileName) {
alert('请先上传图片')
return
}
if (!formData.value.taskName) {
alert('请填写任务名称')
return
}
if (!formData.value.style) {
alert('请选择图片风格')
return
}
console.log('Submitting Quick Task:', formData.value)
alert('快速任务已提交!')
}
</script>
<style scoped>
@import './GeneralProtectSub.css';
</style>

@ -1,164 +0,0 @@
<template>
<div class="page-container">
<TaskSideBar
:tasks="tasks"
@back="goBack"
@details="showTaskDetails"
/>
<div class="main-form-area">
<div class="form-container">
<div class="form-row">
<div class="form-group">
<label>取任务名/编号</label>
<input type="text" v-model="formData.taskName" placeholder="输入任务名称" class="std-input" />
</div>
<div class="form-group">
<label>选择加密算法</label>
<select v-model="formData.algorithm" class="std-select">
<option value="" disabled>请选择算法</option>
<option
v-for="algo in perturbationAlgorithms"
:key="algo. id"
:value="algo.id"
>
{{ algo.method_name }} - {{ algo.description }}
</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>选择扰动强度</label>
<div class="radio-group">
<label><input type="radio" :value="64" v-model="formData.strength" /> </label>
<label><input type="radio" :value="128" v-model="formData.strength" /> </label>
<label><input type="radio" :value="192" v-model="formData.strength" /> </label>
</div>
</div>
<div class="form-group">
<label>选择图片风格</label>
<div class="radio-group">
<label><input type="radio" value="face" v-model="formData.style" /> 人脸</label>
<label class="vip-option">
<input type="radio" value="art" v-model="formData.style" /> 绘画/艺术品
<span class="vip-tag">VIP</span>
</label>
</div>
<p class="hint-text">每种风格对应一种固定的提示词</p>
</div>
</div>
<div class="form-row action-row">
<div class="upload-section">
<input
type="file"
ref="fileInput"
@change="handleFileChange"
style="display: none"
accept="image/*"
/>
<button class="upload-btn" @click="triggerFileUpload"></button>
<span class="file-name" v-if="formData.fileName">{{ formData. fileName }}</span>
<span class="file-name" v-else></span>
</div>
<button class="start-btn" @click="submitTask"> / </button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import TaskSideBar from '@/components/TaskSideBar.vue'
import { getAvailableAlgorithms } from '@/api/index'
const router = useRouter()
const fileInput = ref(null)
const tasks = ref([
{ id: '1', status: 'running', progress: 45 },
{ id: '3', status: 'waiting', progress: 0 },
{ id: 'A', status: 'waiting', progress: 0 }
])
const perturbationAlgorithms = ref([])
const showTaskDetails = () => {
router.push('/task-details')
}
const formData = ref({
taskName: '',
algorithm: '',
strength: 128,
style: 'face',
fileName: '',
file: null
})
const goBack = () => {
router.push('/general-protection')
}
const triggerFileUpload = () => {
fileInput.value. click()
}
const handleFileChange = (event) => {
const file = event.target.files[0]
if (file) {
formData.value.fileName = file.name
formData.value.file = file
}
}
const fetchAlgorithms = async () => {
try {
const res = await getAvailableAlgorithms()
if (res && res.perturbation_algorithms) {
perturbationAlgorithms.value = res. perturbation_algorithms
}
} catch (error) {
console. error('获取算法列表失败', error)
}
}
const submitTask = () => {
if (!formData.value.fileName) {
alert('请先上传图片')
return
}
if (!formData.value.taskName) {
alert('请填写任务名称')
return
}
if (!formData.value.algorithm) {
alert('请选择加密算法')
return
}
if (!formData.value.strength) {
alert('请选择扰动强度')
return
}
if (! formData.value. style) {
alert('请选择图片风格')
return
}
console.log('Submitting:', formData.value)
alert('通用防护任务已提交')
}
onMounted(() => {
fetchAlgorithms()
})
</script>
<style scoped>
@import './GeneralProtectSub.css';
</style>

@ -1,109 +0,0 @@
/* CommonStyle_home.css - 翻页书本类页面特有样式 */
.page-container {
background-color: #f5f5f5; /* 特有背景色 */
flex-direction: column;
}
/* 书本整体布局 */
.book-layout {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
width: 100%;
padding-top: 3rem;
box-sizing: border-box;
}
/* 导航条 */
.nav-side {
width: 4rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
color: #555;
background-color: rgba(200, 200, 200, 0.1);
user-select: none;
transition: background-color 0.3s, opacity 0.3s;
flex-shrink: 0;
}
.nav-side:hover:not(.disabled) { background-color: rgba(200, 200, 200, 0.3); }
.nav-side.disabled { opacity: 0.3; cursor: not-allowed; }
.arrow { font-size: 2rem; font-weight: bold; }
.hint { font-size: 0.9rem; margin-top: 0.5rem; }
/* 中间内容页 */
.content-page {
width: 85%;
height: auto;
aspect-ratio: 16 / 9;
max-height: 75vh;
background-color: white;
border: 0.125rem solid #333;
padding: 2.5rem;
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
position: relative;
margin: 0;
box-sizing: border-box;
}
.page-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
overflow-y: auto;
}
.placeholder-content {
margin-top: 1rem;
text-align: center;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.mock-image-box {
width: 80%;
height: 18rem;
background-color: #eee;
border: 1px dashed #999;
margin: 1.5rem auto;
display: flex;
align-items: center;
justify-content: center;
color: #666;
font-size: 1.2rem;
}
/* 底部分页器 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 0.8rem;
padding-top: 1rem;
margin-top: auto;
border-top: 1px solid #eee;
width: 100%;
}
.dot {
color: #ccc;
font-size: 1.5rem;
cursor: pointer;
line-height: 1;
transition: color 0.2s;
}
.dot:hover { color: #999; }
.dot.active { color: #333; }
.page-text { margin-left: 1rem; color: #666; font-size: 0.9rem; }

@ -1,65 +0,0 @@
<template>
<div class="page-container">
<!-- Back Button -->
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="book-layout">
<!-- Left Navigation Area -->
<div class="nav-side left" @click="prevPage" :class="{ disabled: currentPage === 1 }">
<div class="arrow">&lt;</div>
<div class="hint">上一页</div>
</div>
<!-- Main Content Area -->
<div class="content-page">
<div class="page-content">
<h2 class="page-title">论文支持 - {{ currentPage }} </h2>
<div class="placeholder-content">
<p>(论文细则占位符)</p>
<p>这里是第 {{ currentPage }} 页的论文细则展示...</p>
<div class="mock-image-box">[ 论文内容位置 ]</div>
</div>
</div>
<!-- Page Indicator -->
<div class="pagination">
<span
v-for="n in totalPages"
:key="n"
class="dot"
:class="{ active: currentPage === n }"
@click="currentPage = n"
></span>
<span class="page-text">{{ currentPage }} / {{ totalPages }}</span>
</div>
</div>
<!-- Right Navigation Area -->
<div class="nav-side right" @click="nextPage" :class="{ disabled: currentPage === totalPages }">
<div class="arrow">&gt;</div>
<div class="hint">下一页</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const currentPage = ref(1)
const totalPages = ref(5)
const goBack = () => router.push('/')
const prevPage = () => { if (currentPage.value > 1) currentPage.value-- }
const nextPage = () => { if (currentPage.value < totalPages.value) currentPage.value++ }
</script>
<style scoped>
@import './HomeSub.css';
/* 本地特有样式:暂无,完全复用通用样式 */
</style>

@ -1,266 +0,0 @@
<template>
<div class="page-container">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="book-layout">
<div class="nav-side left" @click="prevPage" :class="{ disabled: currentPage === 1 }">
<div class="arrow">&lt;</div>
<div class="hint">上一页</div>
</div>
<div class="content-page">
<div class="page-content">
<h2 class="page-title">原理图解 - {{ currentPage }} </h2>
<div v-if="loading" class="loading-hint">... </div>
<div v-else class="principle-content">
<div v-if="currentPage === 1" class="section-block">
<h3>平台概览</h3>
<div class="stats-grid" v-if="demoStats">
<div class="stat-card">
<div class="stat-value">{{ demoStats.original_images }}</div>
<div class="stat-label">演示原图</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ demoStats.supported_algorithms }}</div>
<div class="stat-label">支持算法</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ demoStats.evaluation_metrics }}</div>
<div class="stat-label">评估指标</div>
</div>
</div>
</div>
<div v-else-if="currentPage === 2" class="section-block">
<h3>扰动算法</h3>
<div class="algorithm-list">
<div v-for="algo in perturbationAlgorithms" :key="algo. id" class="algorithm-item">
<div class="algo-name">{{ algo.name }}</div>
<div class="algo-code">代码: {{ algo.code }}</div>
<div class="algo-desc">{{ algo.description }}</div>
</div>
<div v-if="perturbationAlgorithms.length === 0" class="empty-hint"></div>
</div>
</div>
<div v-else-if="currentPage === 3" class="section-block">
<h3>微调算法</h3>
<div class="algorithm-list">
<div v-for="algo in finetuneAlgorithms" :key="algo.id" class="algorithm-item">
<div class="algo-name">{{ algo.name }}</div>
<div class="algo-code">代码: {{ algo.code }}</div>
<div class="algo-desc">{{ algo.description }}</div>
</div>
<div v-if="finetuneAlgorithms.length === 0" class="empty-hint"></div>
</div>
</div>
<div v-else-if="currentPage === 4" class="section-block">
<h3>评估指标</h3>
<div class="metrics-list">
<div v-for="metric in evaluationMetrics" :key="metric.name" class="metric-item">
<div class="metric-name">{{ metric.name }}</div>
<div class="metric-desc">{{ metric.description }}</div>
</div>
</div>
</div>
<div v-else class="section-block">
<h3>更多内容</h3>
<div class="placeholder-content">
<p>更多原理介绍内容...</p>
<div class="mock-image-box">[ 图片 / 图表位置 ]</div>
</div>
</div>
</div>
</div>
<div class="pagination">
<span
v-for="n in totalPages"
:key="n"
class="dot"
:class="{ active: currentPage === n }"
@click="currentPage = n"
></span>
<span class="page-text">{{ currentPage }} / {{ totalPages }}</span>
</div>
</div>
<div class="nav-side right" @click="nextPage" :class="{ disabled: currentPage === totalPages }">
<div class="arrow">&gt;</div>
<div class="hint">下一页</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getDemoAlgorithms, getDemoStats } from '@/api/index'
const router = useRouter()
const currentPage = ref(1)
const totalPages = ref(5)
const loading = ref(true)
const demoStats = ref(null)
const perturbationAlgorithms = ref([])
const finetuneAlgorithms = ref([])
const evaluationMetrics = ref([])
const goBack = () => router.back()
const prevPage = () => { if (currentPage.value > 1) currentPage.value-- }
const nextPage = () => { if (currentPage.value < totalPages.value) currentPage.value++ }
const fetchData = async () => {
loading.value = true
try {
const [statsRes, algoRes] = await Promise.all([
getDemoStats(). catch(() => null),
getDemoAlgorithms().catch(() => null)
])
if (statsRes && statsRes.demo_stats) {
demoStats.value = statsRes.demo_stats
}
if (algoRes) {
perturbationAlgorithms.value = algoRes.perturbation_algorithms || []
finetuneAlgorithms.value = algoRes.finetune_algorithms || []
evaluationMetrics. value = algoRes.evaluation_metrics || []
}
} catch (error) {
console.error('获取演示数据失败', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
@import './HomeSub.css';
.loading-hint {
text-align: center;
color: #999;
padding: 2rem;
}
.principle-content {
width: 100%;
padding: 0 1rem;
}
.section-block {
width: 100%;
}
.section-block h3 {
margin: 0 0 1. 5rem 0;
font-size: 1.2rem;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
.stats-grid {
display: flex;
justify-content: center;
gap: 2rem;
}
.stat-card {
background: #f9f9f9;
border: 1px solid #eee;
padding: 1.5rem 2rem;
text-align: center;
border-radius: 4px;
min-width: 120px;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #333;
}
.stat-label {
font-size: 0.9rem;
color: #666;
margin-top: 0.3rem;
}
.algorithm-list {
display: flex;
flex-direction: column;
gap: 1rem;
max-height: 300px;
overflow-y: auto;
}
.algorithm-item {
background: #fafafa;
border: 1px solid #eee;
padding: 1rem;
border-radius: 4px;
}
.algo-name {
font-weight: bold;
font-size: 1. 05rem;
color: #333;
}
.algo-code {
font-size: 0.85rem;
color: #888;
margin-top: 0. 2rem;
}
.algo-desc {
font-size: 0.9rem;
color: #555;
margin-top: 0. 5rem;
}
.metrics-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.metric-item {
background: #f5f5f5;
border: 1px solid #e0e0e0;
padding: 1rem;
border-radius: 4px;
}
.metric-name {
font-weight: bold;
font-size: 1.1rem;
color: #333;
}
.metric-desc {
font-size: 0.9rem;
color: #555;
margin-top: 0. 3rem;
}
.empty-hint {
text-align: center;
color: #999;
padding: 1rem;
}
</style>

@ -1,212 +0,0 @@
<template>
<div class="page-container">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="book-layout">
<div class="nav-side left" @click="prevPage" :class="{ disabled: currentPage === 1 }">
<div class="arrow">&lt;</div>
<div class="hint">上一页</div>
</div>
<div class="content-page">
<div class="page-content">
<h2 class="page-title">样例预览 - {{ currentPage }} </h2>
<div v-if="loading" class="loading-hint">...</div>
<div v-else-if="currentImage" class="sample-content">
<div class="sample-header">
<h3>{{ currentImage.name }}</h3>
</div>
<div class="image-comparison">
<div class="image-card">
<div class="image-label">原始图片</div>
<img :src="currentImage.original" :alt="currentImage.name + ' - 原图'" class="demo-image" />
</div>
<div class="image-card" v-if="currentImage.perturbed && currentImage.perturbed.length > 0">
<div class="image-label">加噪图片</div>
<img :src="currentImage.perturbed[0]" :alt="currentImage.name + ' - 加噪'" class="demo-image" />
</div>
</div>
<div v-if="currentImage.comparisons && currentImage. comparisons.length > 0" class="comparison-section">
<div class="image-label">对比效果</div>
<div class="comparison-grid">
<img
v-for="(comp, idx) in currentImage. comparisons"
:key="idx"
:src="comp"
:alt="'对比图 ' + (idx + 1)"
class="comparison-image"
/>
</div>
</div>
</div>
<div v-else class="empty-hint">
暂无演示样例数据
</div>
</div>
<div class="pagination">
<span
v-for="n in totalPages"
:key="n"
class="dot"
:class="{ active: currentPage === n }"
@click="currentPage = n"
></span>
<span class="page-text">{{ currentPage }} / {{ totalPages }}</span>
</div>
</div>
<div class="nav-side right" @click="nextPage" :class="{ disabled: currentPage === totalPages }">
<div class="arrow">&gt;</div>
<div class="hint">下一页</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { listDemoImages } from '@/api/index'
const router = useRouter()
const currentPage = ref(1)
const loading = ref(true)
const demoImages = ref([])
const totalPages = computed(() => {
return Math.max(1, demoImages.value. length)
})
const currentImage = computed(() => {
if (demoImages.value. length === 0) return null
return demoImages.value[currentPage.value - 1] || null
})
const goBack = () => {
router.push('/')
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
const fetchDemoImages = async () => {
loading.value = true
try {
const res = await listDemoImages()
if (res && res.demo_images) {
demoImages. value = res.demo_images
}
} catch (error) {
console.error('获取演示图片失败', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchDemoImages()
})
</script>
<style scoped>
@import './HomeSub.css';
.loading-hint {
text-align: center;
color: #999;
padding: 3rem;
font-size: 1. 1rem;
}
.sample-content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.sample-header h3 {
margin: 0;
font-size: 1.3rem;
color: #333;
}
.image-comparison {
display: flex;
justify-content: center;
gap: 2rem;
width: 100%;
}
.image-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.image-label {
font-size: 0.95rem;
color: #666;
font-weight: 500;
}
.demo-image {
max-width: 280px;
max-height: 280px;
border: 1px solid #ddd;
border-radius: 4px;
object-fit: contain;
background: #f9f9f9;
}
.comparison-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
}
.comparison-grid {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.comparison-image {
max-width: 200px;
max-height: 200px;
border: 1px solid #ddd;
border-radius: 4px;
object-fit: contain;
background: #f9f9f9;
}
.empty-hint {
text-align: center;
color: #999;
padding: 3rem;
}
</style>

@ -0,0 +1,120 @@
<script setup>
import { inject } from 'vue'
const openSubpage = inject('openSubpage')
const openPrincipleDiagram = () => {
openSubpage('home', 'PrincipleDiagram')
}
const openSamplePreview = () => {
openSubpage('home', 'SamplePreview')
}
const openPaperSupport = () => {
openSubpage('home', 'PaperSupport')
}
</script>
<template>
<div class="view-container">
<div class="grid-layout">
<!-- Hero Card (撑满高度的 60%) -->
<div class="ui-card solid interactive hero-card" @click="openPaperSupport">
<div class="hero-content">
<h2>论文支持<span class="highlight">Protective_Perturbation</span></h2>
<p>ASPL+SimAC+PID+CAAT+Glaze</p>
</div>
<div class="stat-circle">
<span>85%</span>
</div>
</div>
<!-- Secondary Info (右侧全高) -->
<div class="ui-card solid interactive info-card" @click="openPrincipleDiagram">
<h3 style="color: var(--color-text-light);">原理详解</h3>
<p>Diffusion is all you need</p>
<div class="chart-fill"></div>
</div>
<!-- Small Actions (左下角) -->
<div class="ui-card gradient interactive action-card" @click="openSamplePreview">
<i class="fas fa-play"></i>
<span>样例效果尝鲜</span>
</div>
</div>
</div>
</template>
<style scoped>
.view-container {
top: 5%;
width: 100%;
height: 90%;
display: flex;
flex-direction: column;
}
.grid-layout {
display: grid;
grid-template-columns: 2fr 1fr; /* 左 2 : 右 1 */
grid-template-rows: 60% 1fr; /* 上 60% : 下 剩余 */
gap: var(--space-md);
width: 100%;
height: 100%;
}
.hero-card {
grid-column: 1 / 2;
grid-row: 1 / 2;
padding: var(--space-lg);
display: flex;
justify-content: space-between;
align-items: flex-start;
background: var(--color-accent-primary);
}
.info-card {
grid-column: 2 / 3;
grid-row: 1 / 3; /* 占据整列 */
background: var(--color-contrast-dark);
color: var(--color-text-light);
padding: var(--space-lg);
display: flex;
flex-direction: column;
}
.action-card {
grid-column: 1 / 2;
grid-row: 2 / 3;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 10px;
font-size: 1.5rem;
background: var(--color-accent-mild);
font-weight: bold;
}
.highlight { color: var(--color-accent-secondary); }
.stat-circle {
width: 100px;
height: 100px;
border-radius: 50%;
background: var(--color-accent-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bold;
color: var(--color-contrast-dark);
}
.chart-fill {
margin-top: 20px;
flex: 1; /* 撑满剩余空间 */
background: rgba(255,255,255,0.1);
border-radius: 16px;
width: 100%;
}
</style>

@ -0,0 +1,73 @@
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<div class="subpage-layout">
<div class="ui-card solid content-card">
<div class="header">
<h2>Papers Here</h2>
<span class="tag">Subpage</span>
</div>
<div class="body-text">
<p>This is the detailed subpage content overlay. It maintains the same design language as the parent.</p>
<div class="placeholder-grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.subpage-layout {
top: 5%;
width: 100%;
height: 90%;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-xl);
}
.content-card {
width: 100%;
max-width: 900px;
height: 80vh;
padding: var(--space-lg);
box-shadow: 0 20px 50px rgba(0,0,0,0.05);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-lg);
border-bottom: 1px solid rgba(0,0,0,0.05);
padding-bottom: var(--space-md);
}
.tag {
background: var(--color-accent-primary);
padding: 4px 12px;
border-radius: 99px;
font-size: 0.8rem;
font-weight: bold;
}
.placeholder-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-top: 40px;
}
.item {
height: 150px;
background: var(--color-bg-primary);
border-radius: 16px;
}
</style>

@ -0,0 +1,73 @@
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<div class="subpage-layout">
<div class="ui-card solid content-card">
<div class="header">
<h2>Principle</h2>
<span class="tag">Subpage</span>
</div>
<div class="body-text">
<p>This is the detailed subpage content overlay. It maintains the same design language as the parent.</p>
<div class="placeholder-grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.subpage-layout {
top: 5%;
width: 100%;
height: 90%;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-xl);
}
.content-card {
width: 100%;
max-width: 900px;
height: 80vh;
padding: var(--space-lg);
box-shadow: 0 20px 50px rgba(0,0,0,0.05);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-lg);
border-bottom: 1px solid rgba(0,0,0,0.05);
padding-bottom: var(--space-md);
}
.tag {
background: var(--color-accent-primary);
padding: 4px 12px;
border-radius: 99px;
font-size: 0.8rem;
font-weight: bold;
}
.placeholder-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-top: 40px;
}
.item {
height: 150px;
background: var(--color-bg-primary);
border-radius: 16px;
}
</style>

@ -0,0 +1,73 @@
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<div class="subpage-layout">
<div class="ui-card solid content-card">
<div class="header">
<h2>Samples</h2>
<span class="tag">Subpage</span>
</div>
<div class="body-text">
<p>This is the detailed subpage content overlay. It maintains the same design language as the parent.</p>
<div class="placeholder-grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.subpage-layout {
top: 5%;
width: 100%;
height: 90%;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-xl);
}
.content-card {
width: 100%;
max-width: 900px;
height: 80vh;
padding: var(--space-lg);
box-shadow: 0 20px 50px rgba(0,0,0,0.05);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-lg);
border-bottom: 1px solid rgba(0,0,0,0.05);
padding-bottom: var(--space-md);
}
.tag {
background: var(--color-accent-primary);
padding: 4px 12px;
border-radius: 99px;
font-size: 0.8rem;
font-weight: bold;
}
.placeholder-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-top: 40px;
}
.item {
height: 150px;
background: var(--color-bg-primary);
border-radius: 16px;
}
</style>

@ -1,61 +0,0 @@
/* CommonStyle_res.css - 资源列表类页面特有样式 */
.page-container {
background-color: #de743fa1; /* 特有背景色 */
justify-content: center;
align-items: center;
}
/* 中心宽卡片容器 (Res特有) */
.resource-card {
background: white;
border: 0.125rem solid #000;
width: 62rem;
max-width: 92%;
padding: 2rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
height: 30rem;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* 通用列表行样式 (Res特有) */
.list-row {
display: grid;
grid-template-columns: 2fr 2fr 1fr;
align-items: center;
border: 1px solid #ddd;
padding: 1rem 1.5rem;
min-height: 6rem;
box-sizing: border-box;
background: #fff;
transition: background 0.2s;
}
.list-row:hover { background: #fafafa; }
/* 列样式 */
.info-col { display: flex; flex-direction: column; gap: 0.3rem; }
.info-title { font-weight: bold; font-size: 1rem; }
.info-meta { font-size: 0.9rem; color: #666; }
.preview-col {
justify-self: center;
width: 8rem;
height: 5rem;
border: 1px dashed #aaa;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
color: #777;
background: #f9f9f9;
}
.actions-col {
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: center;
align-items: flex-end;
}

@ -1,185 +0,0 @@
<template>
<div class="page-container">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="resource-card">
<h2 class="page-title">我的任务总览</h2>
<!-- Status Filters (保留本地) -->
<div class="status-filters">
<button
v-for="item in statusTabs"
:key="item.key"
class="status-btn"
:class="{ active: currentStatus === item.key }"
@click="currentStatus = item.key"
>
{{ item.label }}
</button>
</div>
<!-- Task Table (使用通用滚动容器 + 本地表格) -->
<div class="content-scroll-wrapper">
<table class="task-table">
<thead>
<tr>
<th>任务 ID</th>
<th>任务名称</th>
<th>状态</th>
<th>创建时间</th>
<th>完成时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="filteredTasks.length === 0">
<td colspan="6" class="empty-hint">当前条件下暂无任务</td>
</tr>
<tr v-for="task in filteredTasks" :key="task.id">
<td>{{ task.id }}</td>
<td>{{ task.name }}</td>
<td>
<span class="status-tag" :class="task.status">
{{ statusLabel(task.status) }}
</span>
</td>
<td>{{ task.createdAt }}</td>
<td>{{ task.finishedAt || '-' }}</td>
<td>
<button class="action-btn small" @click="viewTaskDetails(task)"></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Task Detail Modal -->
<TaskDetailModal
v-if="showTaskDetail && selectedTask"
:task="selectedTask"
@close="closeTaskDetail"
@pause="handlePauseTask"
@cancel="handleCancelTask"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
// import TaskDetailModal from '@/components/TaskDetailModal.vue' // Assume exists
const router = useRouter()
const goBack = () => {
router.push('/my-resources')
}
const statusTabs = [
{ key: 'all', label: '全部任务' },
{ key: 'queued', label: '排队中' },
{ key: 'running', label: '运行中' },
{ key: 'completed', label: '已完成' },
{ key: 'failed', label: '失败' }
]
const currentStatus = ref('all')
const allTasks = ref([
// ... (Mock data remains same) ...
{ id: 'T-1001', name: '风景图通用防护', status: 'completed', createdAt: '2023-11-20 10:00', finishedAt: '2023-11-20 10:15' },
{ id: 'T-1002', name: '人脸隐私保护', status: 'running', createdAt: '2023-11-21 14:30', finishedAt: '' }
])
const filteredTasks = computed(() => {
if (currentStatus.value === 'all') return allTasks.value
return allTasks.value.filter(task => task.status === currentStatus.value)
})
const statusLabel = (status) => {
const map = { queued: '排队中', running: '运行中', completed: '已完成', failed: '失败', cancelled: '已取消', paused: '已暂停' }
return map[status] || status
}
const showTaskDetail = ref(false)
const selectedTask = ref(null)
const viewTaskDetails = (task) => {
selectedTask.value = task
showTaskDetail.value = true
}
const closeTaskDetail = () => {
showTaskDetail.value = false
selectedTask.value = null
}
const handlePauseTask = (taskId) => alert(`任务 ${taskId} 已暂停`)
const handleCancelTask = (taskId) => alert(`任务 ${taskId} 已取消`)
</script>
<style scoped>
@import './MyResourcesSub.css';
/* --- 本地特有样式 --- */
/* 状态筛选按钮 */
.status-filters {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
}
.status-btn {
padding: 0.4rem 1.2rem;
border: 1px solid #ccc;
background: #f8f8f8;
cursor: pointer;
font-size: 0.9rem;
}
.status-btn.active {
border-color: #000;
background: #fff;
font-weight: bold;
}
/* 表格样式 (Table 结构无法完全用 Grid Row 替代,保留本地) */
.task-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.task-table th,
.task-table td {
border: 1px solid #eee;
padding: 0.5rem 0.75rem;
text-align: left;
vertical-align: middle;
white-space: nowrap;
}
.task-table thead {
background: #f5f5f5;
}
/* 状态标签颜色 */
.status-tag { padding: 0.1rem 0.5rem; border-radius: 0.2rem; font-size: 0.85rem; }
.status-tag.queued { background: #fff8e1; color: #ff9800; }
.status-tag.running { background: #e3f2fd; color: #1976d2; }
.status-tag.completed { background: #e8f5e9; color: #2e7d32; }
.status-tag.failed { background: #ffebee; color: #c62828; }
/* 微调通用按钮在表格里的样式 */
.action-btn.small {
padding: 0.2rem 0.5rem;
min-width: auto;
font-size: 0.85rem;
border-color: #1976d2;
color: #1976d2;
}
</style>

@ -1,59 +0,0 @@
<template>
<div class="page-container">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="resource-card">
<h2 class="page-title">已防护图片详情</h2>
<div class="content-scroll-wrapper">
<div
v-for="item in protectedList"
:key="item.id"
class="list-row"
>
<!-- Left: Task info -->
<div class="info-col">
<div class="info-title">{{ item.taskName }}</div>
<div class="info-meta">防护类型{{ item.protectionType }}</div>
<div class="info-meta">图片数量{{ item.imageCount }}</div>
</div>
<!-- Middle: Preview placeholder -->
<div class="preview-col">
<span>预览图占位</span>
</div>
<!-- Right: Actions -->
<div class="actions-col">
<button class="action-btn">查看图片</button>
<button class="action-btn">下载图片</button>
</div>
</div>
<div v-if="protectedList.length === 0" class="empty-hint">
暂无已防护图片记录
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const goBack = () => router.push('/my-resources')
const protectedList = ref([
{ id: 'P-2001', taskName: '任务 A - 通用防护', protectionType: '通用防护', imageCount: 3 },
{ id: 'P-2002', taskName: '任务 B - 专题防护', protectionType: '防人脸编辑', imageCount: 5 },
{ id: 'P-2003', taskName: '任务 C - 批量防护', protectionType: '防风格迁移', imageCount: 8 }
])
</script>
<style scoped>
@import './MyResourcesSub.css';
</style>

@ -1,59 +0,0 @@
<template>
<div class="page-container">
<div class="back-btn-container">
<button class="common-back-btn" @click="goBack"></button>
</div>
<div class="resource-card">
<h2 class="page-title">效果验证结果详情</h2>
<div class="content-scroll-wrapper">
<div
v-for="item in resultList"
:key="item.id"
class="list-row"
>
<!-- Left: Task & comparison info -->
<div class="info-col">
<div class="info-title">{{ item.taskName }}</div>
<div class="info-meta">对比类型{{ item.compareType }}</div>
<div class="info-meta">结果形式{{ item.resultType }}</div>
</div>
<!-- Middle: Preview placeholder -->
<div class="preview-col">
<span>结果预览占位</span>
</div>
<!-- Right: Actions -->
<div class="actions-col">
<button class="action-btn">查看报告</button>
<button class="action-btn">下载报告</button>
</div>
</div>
<div v-if="resultList.length === 0" class="empty-hint">
暂无效果验证结果记录
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const goBack = () => router.push('/my-resources')
const resultList = ref([
{ id: 'R-3001', taskName: '任务 A - 微调方式对比', compareType: '对比类型:样例图', resultType: '图像报告' },
{ id: 'R-3002', taskName: '任务 B - 数据指标对比', compareType: '对比类型:数据指标', resultType: '数据表 (CSV)' },
{ id: 'R-3003', taskName: '任务 C - 频域可视化', compareType: '对比类型:频域图', resultType: '图像 + 指标' }
])
</script>
<style scoped>
@import './MyResourcesSub.css';
</style>

@ -1,122 +0,0 @@
<template>
<div class="page-container">
<TaskSideBar
:tasks="tasks"
@back="goBack"
@details="showTaskDetails"
/>
<div class="main-form-area">
<div class="form-container">
<div class="form-row">
<div class="form-group">
<label>取任务名/编号</label>
<input type="text" v-model="formData.taskName" placeholder="输入任务名称" class="std-input" />
</div>
<div class="form-group">
<label>专题防护算法 (定制)</label>
<div class="read-only-field">Anti-Mist v2.0 (不可选)</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>扰动强度 (定制)</label>
<div class="read-only-field">高级 / High (不可选)</div>
</div>
<div class="form-group">
<label>选图片风格</label>
<div class="radio-group">
<label><input type="radio" value="face" v-model="formData.style" /> 人脸</label>
<label class="vip-option">
<input type="radio" value="art" v-model="formData.style" /> 绘画/艺术品
<span class="vip-tag">VIP</span>
</label>
</div>
<p class="hint-text">每种风格对应一种固定的提示词</p>
</div>
</div>
<div class="form-row action-row">
<div class="upload-section">
<input
type="file"
ref="fileInput"
@change="handleFileChange"
style="display: none"
accept="image/*"
/>
<button class="upload-btn" @click="triggerFileUpload"></button>
<span class="file-name" v-if="formData.fileName">{{ formData.fileName }}</span>
<span class="file-name" v-else></span>
</div>
<button class="start-btn" @click="submitTask">, , </button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import TaskSideBar from '@/components/TaskSideBar.vue'
const router = useRouter()
const fileInput = ref(null)
const tasks = ref([
{ id: '1', status: 'running', progress: 60 },
{ id: '3', status: 'waiting', progress: 0 },
{ id: 'A', status: 'waiting', progress: 0 }
])
const showTaskDetails = () => {
router.push('/task-details')
}
const formData = ref({
taskName: '',
style: 'face',
fileName: '',
file: null
})
const goBack = () => {
router.push('/topic-protection')
}
const triggerFileUpload = () => {
fileInput.value.click()
}
const handleFileChange = (event) => {
const file = event.target.files[0]
if (file) {
formData.value.fileName = file.name
formData.value.file = file
}
}
const submitTask = () => {
if (!formData.value.fileName) {
alert('请先上传图片文件')
return
}
if (!formData.value.taskName) {
alert('请填写任务名称')
return
}
if (!formData.value.style) {
alert('请选择图片风格')
return
}
console.log('Submitting Topic Protection Task:', formData.value)
alert('专题防护任务已提交')
}
</script>
<style scoped>
@import './TopicProtectSub.css';
</style>

@ -1,17 +0,0 @@
/* CommonStyle_topic.css - 专题防护类页面特有样式 */
.page-container {
background-color: #fffaf0; /* 特有背景色 */
}
/* 只读字段 (Topic特有) */
.read-only-field {
padding: 0.8rem;
border: 1px dashed #999;
background: #f0f0f0;
color: #555;
font-size: 1rem;
font-style: italic;
width: 100%;
box-sizing: border-box;
}

@ -1,22 +1,21 @@
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import path from 'path' // 记得引入 path
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src') // 确保 @ 指向 src 目录,方便引用
'@': path.resolve(__dirname, './src') // 确保 @ 指向 src
}
},
server: {
port: 5173, // 前端开发服务器端口(默认)
port: 5173,
proxy: {
// 代理配置核心:匹配 /api 开头的请求
'/api': {
target: 'http://127.0.0.1:6001', // SSH 隧道映射的后端地址
changeOrigin: true, // 必须开启,欺骗后端这是本地请求
target: 'http://127.0.0.1:6001', // 的后端地址
changeOrigin: true,
}
}
}

Loading…
Cancel
Save