深色模式优化 #32

Merged
hnu202326010215 merged 1 commits from yangyixuan_branch into develop 3 weeks ago

@ -36,7 +36,7 @@
<!-- Loading -->
<div v-if="loading" class="loading-state">
<i class="fas fa-spinner fa-spin fa-3x"></i>
<p>加载预览中...</p>
<!--p>加载预览中...</p-->
</div>
<!-- Error -->
@ -545,4 +545,118 @@ const close = () => { emit('close') }
.modal-fade-enter-active, .modal-fade-leave-active { transition: opacity 0.3s ease; }
.modal-fade-enter-from, .modal-fade-leave-to { opacity: 0; }
/*
===========================================
🌑 深色模式适配 (Dark Mode)
===========================================
*/
/* 弹窗卡片主体 */
html.dark-mode .preview-card {
background: #1e293b; /* Slate 800 */
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
}
/* 头部 */
html.dark-mode .card-header {
background: rgba(30, 41, 59, 0.95);
border-bottom-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .header-info h3 {
color: #f1f5f9;
}
html.dark-mode .task-tag {
background: rgba(255, 255, 255, 0.1);
color: #cbd5e1;
}
html.dark-mode .close-btn {
color: #94a3b8;
}
html.dark-mode .close-btn:hover {
color: #fff;
}
/* 内容区域背景 */
html.dark-mode .card-body {
background: #0f172a; /* Slate 900 */
}
html.dark-mode .loading-state,
html.dark-mode .error-state {
color: #94a3b8;
}
/* === 图片对比模式 === */
html.dark-mode .img-box {
background: #1e293b;
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
html.dark-mode .no-img {
color: #64748b;
}
html.dark-mode .divider {
color: #475569;
}
/* 导航箭头 (保持浅色或反转) */
html.dark-mode .nav-arrow {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
html.dark-mode .nav-arrow:hover {
background: var(--color-accent-secondary);
border-color: var(--color-accent-secondary);
}
/* === 报告/热力图模式 === */
html.dark-mode .report-stage {
background: #0f172a;
}
html.dark-mode .report-container {
background: #1e293b;
/* 滚动条深色适配 */
scrollbar-color: #475569 #1e293b;
}
html.dark-mode .report-container::-webkit-scrollbar-track {
background: #1e293b;
}
html.dark-mode .report-container::-webkit-scrollbar-thumb {
background-color: #475569;
}
html.dark-mode .report-header-tip {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
color: #e2e8f0;
}
html.dark-mode .sub-tip {
color: #94a3b8;
}
/* 底部 */
html.dark-mode .card-footer {
background: #1e293b;
border-top-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .thumb-item {
background: #334155;
color: #94a3b8;
}
html.dark-mode .thumb-item.active {
background: var(--color-accent-secondary); /* 保持高亮色 */
color: #fff;
}
</style>

@ -144,4 +144,33 @@ const backgroundStyle = computed(() => {
font-size: 0.8rem;
margin-top: 5px;
}
/*
===========================================
🌑 深色模式适配 (Dark Mode)
===========================================
*/
html.dark-mode .intensity-slider-container {
background: #0f172a; /* Slate 900 */
border-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .intensity-slider-container:hover {
background: #1e293b; /* Slate 800 */
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
html.dark-mode .label {
color: #f1f5f9;
}
html.dark-mode .custom-range::-webkit-slider-thumb {
background: #1e293b;
border-color: var(--color-accent-secondary);
}
html.dark-mode .range-labels {
color: #64748b;
}
</style>

@ -245,10 +245,16 @@ label[for="nav-toggle"] {
/* === 降级 - 底部导航栏模式 (宽 < 900px 或 高缩放) === */
/* === 降级 - 顶部导航栏模式 (宽 < 900px 或 高缩放) === */
@media (max-width: 900px) {
#navbar-container {
width: 100% !important; height: auto !important;
top: auto; bottom: 0; left: 0;
/* 改为定位在顶部 */
top: 0;
bottom: auto;
left: 0;
flex-direction: row; justify-content: center;
background: transparent; z-index: 999;
}
@ -257,9 +263,14 @@ label[for="nav-toggle"] {
width: 100% !important; height: 70px !important;
min-height: auto; max-height: none;
margin: 0 !important;
border-radius: 20px 20px 0 0;
/* 圆角改为下面两个角,更像挂在顶部的 header */
border-radius: 0 0 20px 20px;
flex-direction: row;
box-shadow: 0 -5px 20px rgba(0,0,0,0.1);
/* 阴影向下投射 */
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
#nav-header { display: none; }
@ -283,30 +294,32 @@ label[for="nav-toggle"] {
.nav-button.active {
background: var(--color-bg-primary);
color: var(--color-accent-secondary);
transform: translateY(-5px);
transform: translateY(5px); /* 向下微动 */
box-shadow: 0 5px 10px rgba(0,0,0,0.1);
}
/*
核心修复
在窄屏/高缩放模式下强制按钮横向排列 (row)
这样只占用一行高度不会被顶出屏幕
外部操作按钮组 (登出/主题/个人中心)
导航栏在顶部后这些按钮如果也在顶部会很挤
建议将它们作为 "悬浮按钮 (FAB)" 放在屏幕右下角
*/
.external-actions {
position: absolute;
right: 15px;
bottom: 85px; /* 位于底部导航栏上方 */
position: fixed; /* 固定定位,脱离导航栏流 */
right: 20px;
bottom: 30px; /* 放在底部,方便手指点击 */
flex-direction: row; /* 横向排列 */
align-items: center; /* 垂直居中对齐 */
flex-direction: column-reverse; /* 竖向排列,或者 row 横向排列 */
align-items: center;
width: auto !important;
margin: 0; padding: 0;
gap: 12px;
gap: 15px;
z-index: 1000; /* 确保在最上层 */
}
/* 稍微缩小按钮尺寸,适配移动端 */
.page-5-btn, .logout-btn, .theme-btn {
width: 45px; height: 45px; font-size: 1.1rem;
width: 48px; height: 48px; font-size: 1.1rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.logout-btn { background: rgba(255, 107, 107, 0.9); color: white; border: none; }

@ -0,0 +1,145 @@
<template>
<Transition name="toast-fade">
<div v-if="visible" class="toast-wrapper" :class="type" :style="{ top: `${top}px` }">
<div class="icon-box">
<i class="fas" :class="iconClass"></i>
</div>
<div class="content">
<h4 class="title">{{ title }}</h4>
<p class="message">{{ message }}</p>
</div>
</div>
</Transition>
</template>
<script setup>
import { computed, ref, onMounted } from 'vue'
const props = defineProps({
type: { type: String, default: 'info' }, // success, error, warning, info
message: { type: String, required: true },
duration: { type: Number, default: 3000 },
offset: { type: Number, default: 20 },
id: { type: String, default: '' },
onClose: { type: Function, default: () => {} }
})
const visible = ref(false)
const top = ref(props.offset)
const iconClass = computed(() => {
switch (props.type) {
case 'success': return 'fa-check-circle'
case 'error': return 'fa-times-circle'
case 'warning': return 'fa-exclamation-triangle'
default: return 'fa-info-circle'
}
})
const title = computed(() => {
switch (props.type) {
case 'success': return 'Success'
case 'error': return 'Error'
case 'warning': return 'Warning'
default: return 'Notice'
}
})
onMounted(() => {
visible.value = true
//
if (props.duration > 0) {
setTimeout(() => {
close()
}, props.duration)
}
})
const close = () => {
visible.value = false
// DOM
setTimeout(() => {
props.onClose(props.id)
}, 300)
}
defineExpose({ close })
</script>
<style scoped>
.toast-wrapper {
position: fixed;
right: 20px;
z-index: 9999; /* 确保在最上层 */
display: flex;
align-items: flex-start;
gap: 15px;
min-width: 320px;
max-width: 450px;
padding: 16px 20px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(0,0,0,0.05);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
overflow: hidden;
}
/* 左侧装饰条 */
.toast-wrapper::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 4px;
}
/* === 类型配色 === */
/* Success */
.toast-wrapper.success::before { background: #2e7d32; }
.toast-wrapper.success .icon-box { color: #2e7d32; background: #e8f5e9; }
/* Error */
.toast-wrapper.error::before { background: #dc3545; }
.toast-wrapper.error .icon-box { color: #dc3545; background: #ffebee; }
/* Warning */
.toast-wrapper.warning::before { background: var(--color-accent-secondary); }
.toast-wrapper.warning .icon-box { color: var(--color-accent-secondary); background: #fff8e1; }
/* Icon */
.icon-box {
width: 36px; height: 36px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem;
flex-shrink: 0;
}
/* Content */
.content { flex: 1; }
.title {
margin: 0 0 4px 0;
font-size: 0.95rem;
font-weight: 700;
color: var(--color-contrast-dark);
}
.message {
margin: 0;
font-size: 0.85rem;
color: #64748b;
line-height: 1.4;
word-break: break-all;
}
/* 动画 */
.toast-fade-enter-from,
.toast-fade-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>

@ -1,6 +1,7 @@
import axios from 'axios'
import router from '@/router'
import { useUserStore } from '@/stores/userStore'
import toast from '@/utils/toast'
// 创建 axios 实例
const service = axios.create({
@ -17,6 +18,11 @@ service.interceptors.request.use(
if (userStore.token) {
config.headers['Authorization'] = `Bearer ${userStore.token}`
}
//如果是二进制流请求,处理特殊配置
if (config.returnRawResponse) {
config.responseType = 'arraybuffer'
}
return config
},
error => {
@ -57,7 +63,7 @@ service.interceptors.response.use(undefined, (err) => {
// === 响应拦截器 ===
service.interceptors.response.use(
response => {
// 如果配置了 returnRawResponse则返回完整对象包含 headers
//支持返回完整响应(用于 Multipart 解析获取 Headers
if (response.config.returnRawResponse) {
return response
}
@ -68,23 +74,30 @@ service.interceptors.response.use(
if (error.response) {
const status = error.response.status
// 尝试解析 Blob/ArrayBuffer 类型的错误信息
const data = error.response.data || {}
// 优先使用后端返回的 error 描述
const serverMsg = data.error || data.message
// 如果是 JSON 错误信息
let serverMsg = data.error || data.message
// 如果返回的是 ArrayBuffer (图片流报错), 需要转回文本
if (data instanceof ArrayBuffer) {
try {
const text = new TextDecoder().decode(data)
const json = JSON.parse(text)
serverMsg = json.error || json.message
} catch (e) { /* ignore */ }
}
switch (status) {
case 400: message = serverMsg || '请求参数错误'; break;
case 401:
// === [核心修改点] ===
// 如果请求的是登录接口401 代表账号密码错误
if (error.config.url.includes('/auth/login')) {
message = '用户名或密码错误';
} else {
// 如果是其他接口401 代表 Token 过期
message = '登录已过期,请重新登录';
const userStore = useUserStore()
userStore.logout()
if (router.currentRoute.value.path !== '/login') {
router.push('/login');
}
@ -99,10 +112,10 @@ service.interceptors.response.use(
message = '请求超时,请检查网络';
}
// 修改弹窗逻辑:
// 如果不是 Token 过期的 401是密码错误的 401或者其他错误都弹窗提示
// 使用 Toast 替代 Alert (排除 Token 过期的自动跳转,避免重复弹窗)
// 401 非登录接口报错通常伴随跳转,可根据需求决定是否弹窗
if (error.response?.status !== 401 || error.config.url.includes('/auth/login')) {
alert(message)
toast.error(message)
}
return Promise.reject(new Error(message))

@ -0,0 +1,68 @@
// src/utils/toast.js
import { createVNode, render } from 'vue'
import ToastComponent from '@/components/Toast.vue'
const instances = []
let seed = 1
/**
* 核心渲染函数
* @param {Object} options - { type, message, duration }
*/
const toast = (options) => {
// 创建容器
const container = document.createElement('div')
const id = `toast_${seed++}`
// 计算垂直偏移量 (堆叠效果)
let verticalOffset = 20
instances.forEach(({ vm }) => {
verticalOffset += (vm.el?.offsetHeight || 60) + 16
})
// 销毁回调
const onClose = () => {
const idx = instances.findIndex(item => item.id === id)
if (idx === -1) return
// 移除当前实例
const { container } = instances[idx]
render(null, container) // 卸载 Vue 组件
// document.body.removeChild(container) // 移除 DOM (render null 有时会残留空 div手动移除更保险)
// 注意createVNode 渲染的组件在 leave 动画结束后会由组件内部逻辑处理,
// 但这里我们简单处理:组件 close 后触发 props.onClose
instances.splice(idx, 1) // 从数组移除
// 重新计算剩余 Toast 的位置 (向上移动填补空缺)
// 简化版:这里暂不实现动态上移补位动画,仅防止重叠
// 若要完美补位需要响应式 top这里为简单起见不做复杂布局重算
}
// 创建虚拟节点
const vm = createVNode(ToastComponent, {
...options,
id,
offset: verticalOffset,
onClose: (closedId) => {
// 从 DOM 中彻底移除
render(null, container)
const idx = instances.findIndex(ins => ins.id === closedId)
if (idx !== -1) instances.splice(idx, 1)
}
})
// 渲染并挂载
render(vm, container)
document.body.appendChild(container.firstElementChild) // 将组件根元素移入 Body
instances.push({ id, vm, container })
}
// 导出便捷方法
export default {
success: (msg) => toast({ type: 'success', message: msg }),
error: (msg) => toast({ type: 'error', message: msg }),
warning: (msg) => toast({ type: 'warning', message: msg }),
info: (msg) => toast({ type: 'info', message: msg })
}

@ -332,10 +332,9 @@ onUnmounted(() => {
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
/* === 核心修改:高缩放/窄屏模式 === */
/* 当屏幕宽度小于 900px约等于放大 200%-300%)时 */
/* === 高缩放/窄屏模式 === */
@media (max-width: 900px) {
/* 1. 内容取消左边距(因为导航栏跑下面去了) */
/* 1. 内容取消左边距 */
.layout-content, .layout-content.nav-expanded {
margin-left: 0 !important;
}
@ -346,9 +345,10 @@ onUnmounted(() => {
z-index: 200;
}
/* 3. 增加底部 Padding防止内容被底部导航遮挡 */
/* 3. 调整 Padding 以适应顶部导航栏 */
.scroll-section.is-active, .page-standalone, .subpage-wrapper {
padding-bottom: 90px !important;
padding-top: 100px !important;
padding-bottom: 40px !important;
}
}
</style>

@ -224,4 +224,47 @@ onUnmounted(() => { if (specificPollTimer) clearInterval(specificPollTimer) })
.card-body { overflow: visible; padding: 20px; }
.style-selector { grid-template-columns: 1fr; gap: 10px; }
}
/*
===========================================
🌑 深色模式适配 (Dark Mode)
===========================================
*/
html.dark-mode .content-card {
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.05);
}
html.dark-mode .card-header { border-bottom-color: rgba(255, 255, 255, 0.08); }
html.dark-mode .card-header h2 { color: #f1f5f9; }
html.dark-mode .desc-text {
background: rgba(255, 255, 255, 0.05);
color: #cbd5e1;
border-left-color: var(--color-accent-secondary);
}
html.dark-mode .form-group label { color: #e2e8f0; }
html.dark-mode .ui-input {
background: #0f172a;
border-color: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
html.dark-mode .style-option {
background: #0f172a;
}
html.dark-mode .style-option:hover { background: #1e293b; }
html.dark-mode .style-option.active {
background: #1e293b;
border-color: var(--color-accent-secondary);
}
html.dark-mode .icon-circle { background: #1e293b; color: #94a3b8; }
html.dark-mode .style-option.active .icon-circle { background: var(--color-accent-secondary); color: #1e293b; }
html.dark-mode .opt-title { color: #f1f5f9; }
html.dark-mode .upload-zone {
background: #0f172a;
border-color: rgba(255,255,255,0.1);
}
html.dark-mode .upload-zone.has-file {
background: rgba(46, 125, 50, 0.1);
border-color: #2e7d32;
}
html.dark-mode .file-name { color: #f1f5f9; }
</style>

@ -394,4 +394,99 @@ const startSpecificPolling = (taskId) => {
.row-group { flex-direction: column; gap: 15px; }
.style-selector { grid-template-columns: 1fr; }
}
/*
===========================================
🌑 深色模式适配 (Dark Mode)
===========================================
*/
html.dark-mode .content-card {
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.05);
}
html.dark-mode .card-header {
border-bottom-color: rgba(255, 255, 255, 0.08);
}
html.dark-mode .card-header h2 { color: #f1f5f9; }
html.dark-mode .desc-text {
background: rgba(255, 255, 255, 0.05);
color: #cbd5e1;
border-left-color: var(--color-accent-secondary);
}
html.dark-mode .form-group label {
color: #e2e8f0;
}
/* 输入框 & 下拉框 */
html.dark-mode .ui-input,
html.dark-mode .select-trigger {
background: #0f172a;
border-color: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
html.dark-mode .ui-input:focus,
html.dark-mode .select-trigger.is-open {
border-color: var(--color-accent-secondary);
background: #0f172a;
}
html.dark-mode .select-options {
background: #1e293b;
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
html.dark-mode .option-item { color: #f1f5f9; }
html.dark-mode .option-item:hover { background: rgba(255,255,255,0.05); }
html.dark-mode .option-item.selected { background: rgba(255, 159, 28, 0.1); color: var(--color-accent-secondary); }
/* 风格卡片 */
html.dark-mode .style-option {
background: #0f172a;
border-color: transparent;
}
html.dark-mode .style-option.active {
background: #1e293b;
border-color: var(--color-contrast-dark); /* 或 accent color */
}
html.dark-mode .icon-circle {
background: #1e293b;
color: #94a3b8;
}
html.dark-mode .style-option.active .icon-circle {
background: var(--color-contrast-dark);
color: #1e293b;
}
html.dark-mode .opt-title { color: #f1f5f9; }
html.dark-mode .opt-desc { color: #94a3b8; }
/* 强度切换 */
html.dark-mode .mode-toggle { background: #0f172a; }
html.dark-mode .mode-toggle span { color: #94a3b8; }
html.dark-mode .mode-toggle span.active { background: #334155; color: #f1f5f9; }
html.dark-mode .strength-selector { background: #0f172a; border-color: rgba(255,255,255,0.1); }
html.dark-mode .str-item { color: #94a3b8; }
html.dark-mode .str-item:hover { background: rgba(255,255,255,0.05); }
html.dark-mode .str-item.active { background: var(--color-accent-secondary); color: #1e293b; }
/* 上传区域 */
html.dark-mode .upload-zone {
background: #0f172a;
border-color: rgba(255,255,255,0.1);
}
html.dark-mode .upload-zone:hover {
border-color: var(--color-accent-secondary);
background: rgba(255,255,255,0.02);
}
html.dark-mode .upload-zone.has-file {
background: rgba(46, 125, 50, 0.1);
border-color: #2e7d32;
}
html.dark-mode .file-name { color: #f1f5f9; }
</style>

@ -339,4 +339,61 @@ const submitTask = async () => {
.style-selector { grid-template-columns: 1fr; gap: 10px; }
.style-presets-grid { grid-template-columns: 1fr; }
}
/*
===========================================
🌑 深色模式适配 (Dark Mode)
===========================================
*/
html.dark-mode .content-card {
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.05);
}
html.dark-mode .card-header { border-bottom-color: rgba(255, 255, 255, 0.08); }
html.dark-mode .card-header h2 { color: #f1f5f9; }
html.dark-mode .desc-text {
background: rgba(255, 255, 255, 0.05);
color: #cbd5e1;
border-left-color: var(--color-contrast-dark); /* or accent */
}
html.dark-mode .form-group label { color: #e2e8f0; }
html.dark-mode .ui-input {
background: #0f172a;
border-color: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
html.dark-mode .readonly-field {
background: #0f172a;
border-color: rgba(255,255,255,0.1);
color: #94a3b8;
}
html.dark-mode .style-option { background: #0f172a; }
html.dark-mode .style-option.active { background: #1e293b; border-color: var(--color-accent-secondary); }
html.dark-mode .icon-circle { background: #1e293b; color: #94a3b8; }
html.dark-mode .style-option.active .icon-circle { background: var(--color-accent-secondary); color: #1e293b; }
html.dark-mode .opt-title { color: #f1f5f9; }
/* 风格预设卡片 */
html.dark-mode .preset-card {
background: #0f172a;
border-color: transparent;
}
html.dark-mode .preset-card:hover { background: #1e293b; }
html.dark-mode .preset-card.active {
background: #1e293b;
border-color: var(--color-accent-secondary);
}
html.dark-mode .preset-name { color: #f1f5f9; }
html.dark-mode .preset-desc { color: #94a3b8; }
html.dark-mode .upload-zone {
background: #0f172a;
border-color: rgba(255,255,255,0.1);
}
html.dark-mode .upload-zone.has-file {
background: rgba(46, 125, 50, 0.1);
border-color: #2e7d32;
}
html.dark-mode .file-name { color: #f1f5f9; }
</style>

@ -495,4 +495,72 @@ onMounted(() => taskStore.fetchTasks())
.tab-btn { width: 100%; text-align: center; }
.style-selector { grid-template-columns: 1fr; }
}
/*
===========================================
🌑 深色模式适配 (Dark Mode)
===========================================
*/
html.dark-mode .content-card {
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.05);
}
html.dark-mode .card-header { border-bottom-color: rgba(255, 255, 255, 0.08); }
html.dark-mode .card-header h2 { color: #f1f5f9; }
html.dark-mode .form-group label { color: #e2e8f0; }
html.dark-mode .ui-input {
background: #0f172a;
border-color: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
/* Tabs */
html.dark-mode .mode-tabs { border-bottom-color: rgba(255,255,255,0.1); }
html.dark-mode .tab-btn { color: #94a3b8; }
html.dark-mode .tab-btn.active { background: var(--color-accent-secondary); color: #1e293b; }
html.dark-mode .tab-btn.vip.active { color: #fff; }
/* 来源选择 */
html.dark-mode .source-trigger {
background: #0f172a;
border-color: rgba(255,255,255,0.1);
color: #f1f5f9;
}
html.dark-mode .source-list {
background: #1e293b;
border-color: rgba(255,255,255,0.1);
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
html.dark-mode .source-item { color: #cbd5e1; }
html.dark-mode .source-item:hover { background: rgba(255,255,255,0.05); }
/* 图片选择网格 */
html.dark-mode .image-select-container {
background: #0f172a;
border-color: rgba(255,255,255,0.1);
}
html.dark-mode .img-item { background: #1e293b; }
html.dark-mode .single-preview-area {
background: #0f172a;
border-color: rgba(255,255,255,0.1);
}
html.dark-mode .preview-label { color: #94a3b8; }
html.dark-mode .preview-img-sm { border-color: rgba(255,255,255,0.1); }
/* 样式选择卡片 */
html.dark-mode .style-option { background: #0f172a; }
html.dark-mode .style-option:hover { background: #1e293b; }
html.dark-mode .style-option.active {
background: #1e293b;
border-color: var(--color-accent-secondary);
}
html.dark-mode .opt-title { color: #f1f5f9; }
html.dark-mode .opt-desc { color: #94a3b8; }
html.dark-mode .upload-zone {
background: #0f172a;
border-color: rgba(255,255,255,0.1);
}
html.dark-mode .file-name { color: #f1f5f9; }
</style>

@ -8,7 +8,7 @@
<p class="page-desc">Task History Management</p>
</div>
<!-- 状态筛选 Tab (保留原有的状态切换) -->
<!-- 状态筛选 -->
<div class="filter-tabs">
<div v-for="tab in statusTabs" :key="tab.key" class="tab-item"
:class="{ active: currentStatus === tab.key }"
@ -22,7 +22,7 @@
<div class="main-content">
<div class="ui-card modern-table-card">
<!-- 新增工具栏搜索与类型筛选 -->
<!-- 工具栏搜索与类型筛选 -->
<div class="toolbar-section">
<div class="search-box">
<i class="fas fa-search search-icon"></i>
@ -46,7 +46,7 @@
</div>
</div>
<!-- 桌面表头 (汉化) -->
<!-- 桌面表头 -->
<div class="list-header-row desktop-only-flex">
<div class="col-id sortable" @click="handleSort('task_id', $event)">
ID <i :class="getSortIcon('task_id')"></i>
@ -96,6 +96,7 @@
</div>
<div class="col-info desktop-only-cell">
<!-- 这里的 class 样式已修改支持换行 -->
<span class="task-name">{{ getTaskName(task) }}</span>
</div>
@ -205,8 +206,8 @@ const userStore = useUserStore()
//
const currentStatus = ref('all')
const searchKeyword = ref('') //
const selectedTaskType = ref('all') //
const searchKeyword = ref('')
const selectedTaskType = ref('all')
const statusTabs = [
{ key: 'all', label: '全部' },
@ -221,7 +222,6 @@ const showPreview = ref(false)
const previewTaskId = ref(null)
const previewTaskType = ref('')
//
const showLogModal = ref(false)
const currentLogTaskId = ref(null)
const logContent = ref('')
@ -231,7 +231,6 @@ const normalizeStatus = (status) => {
return status
}
//
const formatStatusLabel = (s) => {
const map = {
running: '运行中', processing: '处理中', pending: '排队中', waiting: '排队中',
@ -240,7 +239,6 @@ const formatStatusLabel = (s) => {
return map[s] || s
}
//
const formatTypeLabel = (t) => {
const map = {
perturbation: '通用防护',
@ -263,33 +261,26 @@ const formatDate = (iso) => iso ? new Date(iso).toLocaleString('zh-CN', { hour12
const handleStatusChange = (status) => { currentStatus.value = status; currentPage.value = 1 }
// === ===
const filteredAndSortedTasks = computed(() => {
let result = [...(taskStore.tasks || [])]
// 1.
if (currentStatus.value !== 'all') {
result = result.filter(t => normalizeStatus(t.status) === currentStatus.value)
}
// 2. ()
if (selectedTaskType.value !== 'all') {
result = result.filter(t => t.task_type === selectedTaskType.value)
}
// 3. ()
if (searchKeyword.value.trim()) {
const keyword = searchKeyword.value.toLowerCase().trim()
result = result.filter(t => {
// ID
const idMatch = String(t.task_id).includes(keyword)
//
const nameMatch = getTaskName(t).toLowerCase().includes(keyword)
return idMatch || nameMatch
})
}
// 4.
if (sortRules.value.length > 0) {
result.sort((a, b) => {
const rule = sortRules.value[0]
@ -399,6 +390,7 @@ onMounted(() => taskStore.fetchTasks())
box-sizing: border-box; padding: 40px 20px 120px 20px;
}
/* Header */
.header-section {
flex: 0 0 auto; margin-bottom: 30px;
display: flex; justify-content: space-between; align-items: flex-end;
@ -409,6 +401,7 @@ onMounted(() => taskStore.fetchTasks())
}
.page-desc { color: #94a3b8; margin-top: 6px; font-weight: 500; }
/* Filter Tabs */
.filter-tabs {
display: flex; background: rgba(255,255,255,0.6);
padding: 5px; border-radius: 12px; gap: 5px;
@ -483,7 +476,7 @@ onMounted(() => taskStore.fetchTasks())
border: 1px solid #e2e8f0; border-radius: 8px;
font-size: 0.9rem; color: #334155;
background: #f8fafc; cursor: pointer;
appearance: none; /* remove default arrow */
appearance: none;
}
.filter-select:focus {
background: #fff; border-color: var(--color-accent-secondary);
@ -497,11 +490,31 @@ onMounted(() => taskStore.fetchTasks())
*/
.list-header-row {
display: grid;
grid-template-columns: 80px 2fr 120px 180px 140px 160px;
/*
全列自适应比例分配
不再使用固定宽度而是用 minmax + fr 组合
总权重约 8按内容重要性分配
1. ID: 0.5fr (最小60px)
2. 任务名: 3fr (最小200px占主要空间)
3. 类型: 1fr (最小100px)
4. 时间: 1.5fr (最小160px时间较长)
5. 状态: 1fr (最小100px)
6. 操作: 1fr (最小120px)
效果无论屏幕多宽所有列都会均匀拉伸不再有死角空白
*/
grid-template-columns:
minmax(60px, 0.5fr)
minmax(200px, 3fr)
minmax(100px, 1fr)
minmax(160px, 1.5fr)
minmax(100px, 1fr)
minmax(120px, 1fr);
padding: 20px 30px;
align-items: center;
background: transparent;
border-bottom: 2px solid #e2e8f0; /* 加深线条 */
border-bottom: 2px solid #e2e8f0;
font-size: 0.9rem;
font-weight: 700;
@ -518,33 +531,43 @@ onMounted(() => taskStore.fetchTasks())
/*
------------------------------------------
Table Rows (Clean Style)
Table Rows (Clean Style + Multiline Support)
------------------------------------------
*/
.list-row {
display: grid;
grid-template-columns: 80px 2fr 120px 180px 140px 160px;
padding: 18px 30px;
align-items: center;
/* 与 Header 保持完全一致的列定义 */
grid-template-columns:
minmax(60px, 0.5fr)
minmax(200px, 3fr)
minmax(100px, 1fr)
minmax(160px, 1.5fr)
minmax(100px, 1fr)
minmax(120px, 1fr);
padding: 20px 30px;
align-items: center;
/* 分隔线设计 */
border-bottom: 1px solid #e2e8f0;
border-bottom: 1px solid #e2e8f0;
background: #fff;
transition: background-color 0.2s;
}
/* 移除悬浮特效,仅保留背景色微调 */
.list-row:hover {
background-color: #f8fafc; /* 极淡的灰色 */
/* transform: none; box-shadow: none; removed */
background-color: #f8fafc; /* 仅背景色微调 */
}
.id-text { font-family: monospace; color: #94a3b8; font-weight: 600; }
/* 任务名称样式:允许换行 */
.task-name {
font-weight: 600; color: #334155; font-size: 0.95rem;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
white-space: normal; /* 允许换行 */
word-break: break-word; /* 防止长单词溢出 */
line-height: 1.5; /* 增加行高,多行时更好看 */
padding-right: 20px;
}
.time-text { color: #64748b; font-size: 0.85rem; font-variant-numeric: tabular-nums; }
.type-tag {
@ -589,7 +612,7 @@ onMounted(() => taskStore.fetchTasks())
.pagination-footer {
padding: 20px 30px;
display: flex; justify-content: flex-end; align-items: center;
border-top: 1px solid #e2e8f0; /* 分隔线 */
border-top: 1px solid #e2e8f0;
background: #fff;
}
.page-info { margin: 0 15px; color: #64748b; font-size: 0.9rem; font-weight: 500; }
@ -616,7 +639,6 @@ onMounted(() => taskStore.fetchTasks())
.header-section { flex-direction: column; align-items: flex-start; gap: 15px; }
.filter-tabs { width: 100%; overflow-x: auto; padding-bottom: 5px; -webkit-overflow-scrolling: touch; }
/* 移动端隐藏工具栏的部分样式,或者调整布局 */
.toolbar-section { flex-direction: column; align-items: stretch; padding: 15px; }
.search-box { width: 100%; }
@ -629,13 +651,16 @@ onMounted(() => taskStore.fetchTasks())
align-items: stretch;
padding: 20px;
margin-bottom: 0;
border-bottom: 10px solid #f1f5f9; /* 移动端用粗线条分隔 */
border-bottom: 10px solid #f1f5f9;
}
.mobile-row-header { display: flex; justify-content: space-between; align-items: center; }
.id-badge { background: #f1f5f9; padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; color: #64748b; font-weight: 700; }
.mobile-task-name { font-size: 1.1rem; font-weight: 700; color: #1e293b; margin: 5px 0; }
.mobile-task-name {
font-size: 1.1rem; font-weight: 700; color: #1e293b; margin: 5px 0;
white-space: normal; word-break: break-word; line-height: 1.4;
}
.mobile-meta-row { display: flex; justify-content: space-between; align-items: center; font-size: 0.85rem; color: #94a3b8; }
.col-action {
@ -694,4 +719,150 @@ onMounted(() => taskStore.fetchTasks())
align-items: center; justify-content: center; gap: 15px; color: #888;
}
.loading-log i { font-size: 2rem; color: var(--color-accent-secondary); }
html.dark-mode .view-container {
/* 确保容器本身背景融合 */
color: #e2e8f0;
}
html.dark-mode .page-header {
color: #f1f5f9;
}
html.dark-mode .modern-table-card {
background: #1e293b; /* Slate 800 */
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 20px 40px -5px rgba(0, 0, 0, 0.3);
}
/* --- 搜索栏 --- */
html.dark-mode .toolbar-section {
border-bottom-color: rgba(255, 255, 255, 0.08);
}
html.dark-mode .search-input,
html.dark-mode .filter-select {
background: #0f172a; /* Slate 900 */
border-color: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
html.dark-mode .search-input:focus,
html.dark-mode .filter-select:focus {
border-color: var(--color-accent-secondary);
background: #0f172a;
}
/* --- 表头 --- */
html.dark-mode .list-header-row {
border-bottom-color: rgba(255, 255, 255, 0.08);
color: #94a3b8;
}
/* --- 表格行 --- */
html.dark-mode .list-row {
background: #1e293b;
border-bottom-color: rgba(255, 255, 255, 0.05);
}
html.dark-mode .list-row:hover {
background-color: rgba(255, 255, 255, 0.04); /* 微弱的悬浮高亮 */
}
/* 文本颜色修正 */
html.dark-mode .task-name {
color: #f8fafc; /* 亮白色 */
}
html.dark-mode .id-text,
html.dark-mode .time-text {
color: #94a3b8; /* 灰色 */
}
html.dark-mode .type-tag {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
color: #cbd5e1;
}
/* --- 状态胶囊 (降低亮度,增加透明度) --- */
html.dark-mode .status-pill.completed {
background: rgba(22, 163, 74, 0.2);
color: #4ade80;
}
html.dark-mode .status-pill.running {
background: rgba(7, 89, 133, 0.3);
color: #38bdf8;
}
html.dark-mode .status-pill.failed {
background: rgba(153, 27, 27, 0.3);
color: #f87171;
}
/* --- 操作按钮 --- */
html.dark-mode .action-btn {
color: #94a3b8;
}
html.dark-mode .action-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
/* --- 分页器 --- */
html.dark-mode .pagination-footer {
background: #1e293b;
border-top-color: rgba(255, 255, 255, 0.08);
}
html.dark-mode .page-btn {
background: #0f172a;
border-color: rgba(255, 255, 255, 0.1);
color: #cbd5e1;
}
html.dark-mode .page-btn:hover:not(:disabled) {
background: #334155;
color: #fff;
}
html.dark-mode .page-btn:disabled {
background: #1e293b;
opacity: 0.3;
}
html.dark-mode .empty-state {
color: #64748b;
}
/* --- 移动端卡片 --- */
html.dark-mode .list-row {
/* 移动端复用了 list-row但如果在窄屏下有特定边框需要处理 */
box-shadow: none;
}
html.dark-mode .id-badge {
background: rgba(255, 255, 255, 0.05);
color: #94a3b8;
}
html.dark-mode .mobile-task-name {
color: #f1f5f9;
}
@media (max-width: 900px) {
html.dark-mode .list-row {
border: 1px solid rgba(255, 255, 255, 0.05);
background: #1e293b;
border-bottom: 10px solid #0f172a; /* 深色背景间隙 */
}
html.dark-mode .col-action {
border-top-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .action-btn {
background: rgba(255, 255, 255, 0.05);
}
}
</style>

@ -449,4 +449,56 @@ onMounted(() => {
justify-content: flex-end;
}
}
/*
===========================================
🌑 深色模式适配 (Dark Mode)
===========================================
*/
html.dark-mode .content-card,
html.dark-mode .sub-modal-card {
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.05);
}
html.dark-mode .overlay-header,
html.dark-mode .card-header,
html.dark-mode .log-header {
border-bottom-color: rgba(255, 255, 255, 0.08);
}
html.dark-mode h3 { color: #f1f5f9; }
html.dark-mode .form-group label { color: #e2e8f0; }
html.dark-mode .ui-input {
background: #0f172a;
border-color: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
/* 管理员表格 */
html.dark-mode .table-wrapper { border-color: rgba(255,255,255,0.1); }
html.dark-mode .list-head-row {
background: #0f172a;
border-bottom-color: rgba(255,255,255,0.1);
color: #94a3b8;
}
html.dark-mode .list-row {
border-bottom-color: rgba(255,255,255,0.05);
color: #cbd5e1;
}
html.dark-mode .list-row:hover { background: rgba(255,255,255,0.02); }
html.dark-mode .u-name { color: #f1f5f9; }
html.dark-mode .u-email { color: #94a3b8; }
html.dark-mode .empty-row { color: #64748b; }
/* 角色标签 */
html.dark-mode .role-tag.user { background: rgba(255,255,255,0.1); color: #cbd5e1; }
html.dark-mode .role-tag.vip { background: rgba(255, 193, 7, 0.2); color: #ffc107; }
/* 分页 */
html.dark-mode .page-btn {
background: #0f172a;
border-color: rgba(255,255,255,0.1);
color: #cbd5e1;
}
html.dark-mode .page-btn:hover:not(:disabled) { background: #334155; }
</style>

@ -20,40 +20,45 @@
</div>
<div class="form-group">
<!-- Username -->
<div class="input-wrapper">
<i class="fas fa-user input-icon"></i>
<div class="user-box">
<input
type="text"
v-model="form.username"
placeholder="Username"
class="input-field"
name="username"
required
v-model="form.username"
/>
<label>Username</label>
<i class="fas fa-user input-icon"></i>
</div>
<!-- Email -->
<div class="input-wrapper">
<i class="fas fa-envelope input-icon"></i>
<div class="user-box">
<input
type="email"
v-model="form.email"
placeholder="Email"
class="input-field"
name="email"
required
v-model="form.email"
/>
<label>Email Address</label>
<i class="fas fa-envelope input-icon"></i>
</div>
<!-- Code -->
<!-- Code Group -->
<div class="code-group">
<div class="input-wrapper flex-1">
<i class="fas fa-shield-alt input-icon"></i>
<div class="user-box flex-1">
<input
type="text"
v-model="form.code"
placeholder="Code"
class="input-field code-input"
name="code"
required
v-model="form.code"
maxlength="6"
/>
<label>Verification Code</label>
<i class="fas fa-shield-alt input-icon"></i>
</div>
<button
class="ui-btn solid sm code-btn"
:disabled="isSending || countdown > 0"
@ -64,15 +69,17 @@
</div>
<!-- Password -->
<div class="input-wrapper">
<i class="fas fa-lock input-icon"></i>
<div class="user-box">
<input
type="password"
v-model="form.password"
placeholder="Password"
class="input-field"
name="password"
required
v-model="form.password"
/>
<label>Password</label>
<i class="fas fa-lock input-icon"></i>
</div>
</div>
<button
@ -165,9 +172,9 @@ onUnmounted(() => {
</script>
<style scoped>
/* 样式与 LoginView 保持一致 */
/* === 容器基础样式 (复用 LoginView) === */
.login-container {
--color-text-main: #18283b; /* 强制深色文字,适配白色卡片 */
--color-text-main: #18283b;
--color-accent-secondary: #FF9F1C;
background: #18283b;
width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center;
@ -183,73 +190,104 @@ onUnmounted(() => {
}
.login-card {
width: 900px;
min-height: 550px;
padding: 0;
display: flex;
flex-direction: row;
overflow: hidden;
width: 900px; min-height: 550px; padding: 0;
display: flex; flex-direction: row; overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
background: rgba(255, 255, 255, 0.95);
z-index: 10;
background: rgba(255, 255, 255, 0.95); z-index: 10;
}
.brand-side {
flex: 4;
background: linear-gradient(135deg, var(--color-contrast-dark), #2c3e50);
color: #fff;
padding: 60px 40px;
display: flex;
flex-direction: column;
justify-content: center;
color: #fff; padding: 60px 40px;
display: flex; flex-direction: column; justify-content: center;
position: relative; overflow: hidden;
}
.brand-content { position: relative; z-index: 2; }
.logo-text { font-size: 2rem; font-weight: 900; letter-spacing: 4px; margin-bottom: 40px; color: var(--color-accent-primary); }
.slogan { font-size: 2.2rem; line-height: 1.2; margin-bottom: 20px; font-weight: 700; color: #ffffff; }
.desc { font-size: 1.1rem; color: rgba(255, 255, 255, 0.9); line-height: 1.6; }
.circle-deco { position: absolute; width: 300px; height: 300px; border: 40px solid rgba(255, 255, 255, 0.05); border-radius: 50%; bottom: -100px; right: -100px; z-index: 1; }
.form-side { flex: 5; padding: 50px; display: flex; flex-direction: column; justify-content: center; background: #fff; }
.form-header { margin-bottom: 30px; }
.form-header h1 { font-size: 2rem; color: var(--color-contrast-dark); margin-bottom: 10px; }
.form-header p { color: #666; }
.form-group { margin-bottom: 30px; display: flex; flex-direction: column; gap: 25px; }
/* === Floating Label 核心样式 (复用 LoginView) === */
.user-box {
position: relative;
overflow: hidden;
margin-bottom: 0; /* gap handle spacing */
}
.brand-content { position: relative; z-index: 2; }
.logo-text {
font-size: 2rem;
font-weight: 900;
letter-spacing: 4px;
margin-bottom: 40px;
color: var(--color-accent-primary);
.user-box input {
width: 100%;
padding: 10px 25px 10px 30px; /* Left padding space for icon */
font-size: 1rem;
color: var(--color-text-main);
border: none;
border-bottom: 1px solid #ddd;
outline: none;
background: transparent;
transition: all 0.3s;
}
.slogan {
font-size: 2.2rem;
line-height: 1.2;
margin-bottom: 20px;
font-weight: 700;
color: #ffffff; /* [修改] 强制纯白 */
/* Label 初始状态 */
.user-box label {
position: absolute;
top: 10px;
left: 30px; /* Align with input text start */
padding: 0;
font-size: 1rem;
color: #999;
pointer-events: none;
transition: .5s;
}
.desc {
font-size: 1.1rem; /* [修改] 调大 */
color: rgba(255, 255, 255, 0.9); /* [修改] 强制高亮白 */
line-height: 1.6;
/* 移除了 display: none (之前移动端隐藏逻辑保持不变,这里只改桌面端样式) */
/* Icon */
.input-icon {
position: absolute;
top: 12px;
left: 0;
font-size: 1rem;
color: #999;
transition: .5s;
}
.circle-deco { position: absolute; width: 300px; height: 300px; border: 40px solid rgba(255, 255, 255, 0.05); border-radius: 50%; bottom: -100px; right: -100px; z-index: 1; }
.form-side { flex: 5; padding: 50px; display: flex; flex-direction: column; justify-content: center; }
.form-header { margin-bottom: 30px; }
.form-header h1 { font-size: 2rem; color: var(--color-contrast-dark); margin-bottom: 10px; }
.form-header p { color: #666; }
/* === 核心动画逻辑 === */
/* 当输入框聚焦 或 输入框有内容(valid) 时label 上浮变色 */
.user-box input:focus ~ label,
.user-box input:valid ~ label {
top: -14px;
left: 0;
color: var(--color-accent-secondary);
font-size: 0.8rem;
font-weight: 600;
}
.form-group { margin-bottom: 30px; display: flex; flex-direction: column; gap: 15px; }
.user-box input:focus {
border-bottom: 1px solid var(--color-accent-secondary);
}
.input-wrapper { position: relative; display: flex; align-items: center; width: 100%; }
.input-icon { position: absolute; left: 15px; color: #999; font-size: 1rem; z-index: 2; }
.input-field { width: 100%; padding: 15px 15px 15px 45px; border-radius: 12px; border: 1px solid #e0e0e0; background: #f8f9fa; font-size: 1rem; transition: all 0.3s; }
.input-field:focus { background: #fff; border-color: var(--color-accent-secondary); box-shadow: 0 0 0 4px rgba(255, 159, 28, 0.1); }
.user-box input:focus ~ .input-icon {
color: var(--color-accent-secondary);
}
/* 验证码特殊布局 */
.code-group { display: flex; gap: 10px; align-items: center; }
.code-group { display: flex; gap: 15px; align-items: flex-end; }
.flex-1 { flex: 1; }
.code-btn { width: 110px; height: 50px; border-radius: 12px; font-size: 0.9rem; background: var(--color-contrast-dark); color: #fff; flex-shrink: 0; }
.code-btn:disabled { background: #ccc; cursor: not-allowed; }
.code-btn {
width: 110px; height: 42px; /* 高度匹配输入框视觉 */
border-radius: 8px; font-size: 0.85rem;
background: var(--color-contrast-dark); color: #fff;
flex-shrink: 0;
margin-bottom: 2px; /* 微调对齐 */
}
.code-btn:disabled { background: #ccc; cursor: not-allowed; opacity: 0.7; }
.full-width { width: 100%; height: 50px; font-size: 1.1rem; border-radius: 12px; }
.full-width { width: 100%; height: 50px; font-size: 1.1rem; border-radius: 12px; margin-top: 10px; }
.footer-link { margin-top: 20px; text-align: center; font-size: 0.95rem; color: #666; }
.footer-link a { color: var(--color-accent-secondary); font-weight: 700; text-decoration: none; margin-left: 5px; }
.footer-link a:hover { text-decoration: underline; }

Loading…
Cancel
Save