Binary file not shown.
@ -1,3 +0,0 @@
|
||||
# 源代码模块说明
|
||||
|
||||
占位内容:后续补充各子模块结构、构建与运行说明。
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
File diff suppressed because it is too large
Load Diff
@ -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,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,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>
|
||||
@ -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>
|
||||
@ -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,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,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,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"><</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">></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"><</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">></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"><</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">></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,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,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…
Reference in new issue