Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor
This commit is contained in:
@@ -3,7 +3,10 @@
|
||||
<a-layout-sider
|
||||
:width="SIDER_WIDTH"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
:style="{ background: 'var(--ant-color-bg-elevated)', borderRight: '1px solid var(--ant-color-border)' }"
|
||||
:style="{
|
||||
background: 'var(--ant-color-bg-elevated)',
|
||||
borderRight: '1px solid var(--ant-color-border)',
|
||||
}"
|
||||
>
|
||||
<div class="sider-content">
|
||||
<a-menu
|
||||
@@ -34,17 +37,18 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
HomeOutlined,
|
||||
FileTextOutlined,
|
||||
CalendarOutlined,
|
||||
UnorderedListOutlined,
|
||||
ControlOutlined,
|
||||
FileTextOutlined,
|
||||
HistoryOutlined,
|
||||
HomeOutlined,
|
||||
SettingOutlined,
|
||||
UnorderedListOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { computed, h } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useTheme } from '../composables/useTheme.ts'
|
||||
import { useRouteLock } from '../composables/useRouteLock.ts'
|
||||
import type { MenuProps } from 'ant-design-vue'
|
||||
|
||||
const SIDER_WIDTH = 160
|
||||
@@ -52,6 +56,7 @@ const SIDER_WIDTH = 160
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { isDark } = useTheme()
|
||||
const { isRouteLocked, triggerBlockCallback } = useRouteLock()
|
||||
|
||||
// 工具:生成菜单项
|
||||
const icon = (Comp: any) => () => h(Comp)
|
||||
@@ -79,54 +84,99 @@ const selectedKeys = computed(() => {
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = info => {
|
||||
const target = String(info.key)
|
||||
|
||||
// 检查路由是否被锁定
|
||||
if (isRouteLocked.value) {
|
||||
// 如果路由被锁定,触发回调而不进行路由跳转
|
||||
triggerBlockCallback(target)
|
||||
return
|
||||
}
|
||||
|
||||
if (route.path !== target) router.push(target)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sider-content { height:100%; display:flex; flex-direction:column; padding:10px 3px; }
|
||||
.sider-content :deep(.ant-menu) { border-inline-end: none !important; background: transparent !important; }
|
||||
.sider-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 3px;
|
||||
}
|
||||
|
||||
.sider-content :deep(.ant-menu) {
|
||||
border-inline-end: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 菜单项外框居中(左右留空),内容左对齐 */
|
||||
.sider-content :deep(.ant-menu .ant-menu-item) {
|
||||
color: var(--ant-color-text);
|
||||
margin: 2px auto; /* 水平居中 */
|
||||
width: calc(100% - 16px); /* 两侧各留 8px 空隙 */
|
||||
margin: 2px auto; /* 水平居中 */
|
||||
width: calc(100% - 16px); /* 两侧各留 8px 空隙 */
|
||||
border-radius: 6px;
|
||||
padding: 5px 16px !important; /* 左右内边距 */
|
||||
padding: 5px 16px !important; /* 左右内边距 */
|
||||
line-height: 36px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start; /* 左对齐图标与文字 */
|
||||
justify-content: flex-start; /* 左对齐图标与文字 */
|
||||
gap: 6px;
|
||||
transition: background .16s ease, color .16s ease;
|
||||
transition:
|
||||
background 0.16s ease,
|
||||
color 0.16s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sider-content :deep(.ant-menu .ant-menu-item .anticon) {
|
||||
color: var(--ant-color-text-secondary);
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
transition: color .16s ease;
|
||||
transition: color 0.16s ease;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Hover */
|
||||
.sider-content :deep(.ant-menu .ant-menu-item:hover) {
|
||||
background: var(--ant-color-primary-bg);
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
.sider-content :deep(.ant-menu .ant-menu-item:hover .anticon) { color: var(--ant-color-text); }
|
||||
|
||||
.sider-content :deep(.ant-menu .ant-menu-item:hover .anticon) {
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
/* Selected */
|
||||
.sider-content :deep(.ant-menu .ant-menu-item-selected) {
|
||||
background: var(--ant-color-primary-bg);
|
||||
color: var(--ant-color-text) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
.sider-content :deep(.ant-menu .ant-menu-item-selected .anticon) { color: var(--ant-color-text-secondary); }
|
||||
|
||||
.sider-content :deep(.ant-menu .ant-menu-item-selected .anticon) {
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
.sider-content :deep(.ant-menu-light .ant-menu-item::after),
|
||||
.sider-content :deep(.ant-menu-dark .ant-menu-item::after) { display: none; }
|
||||
.bottom-menu { margin-top:auto; }
|
||||
.content-area { min-height:0; overflow:auto; scrollbar-width:none; -ms-overflow-style:none; padding:32px;}
|
||||
.content-area::-webkit-scrollbar { display:none; }
|
||||
.sider-content :deep(.ant-menu-dark .ant-menu-item::after) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bottom-menu {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 使用标准 Sider 布局,去除 fixed 与 marginLeft,保持菜单样式与滚动行为 -->
|
||||
<!-- 使用标准 Sider 布局,去除 fixed 与 marginLeft,保持菜单样式与滚动行为 -->
|
||||
|
||||
@@ -1,483 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isDev"
|
||||
class="debug-panel"
|
||||
:class="{ collapsed: isCollapsed, dragging: isDragging }"
|
||||
:style="{ left: `${panelPosition.x}px`, top: `${panelPosition.y}px` }"
|
||||
>
|
||||
<div class="debug-header" @mousedown="handleDragStart" @click="toggleCollapse">
|
||||
<span class="debug-title"
|
||||
>🐛 调试面板 <span v-if="isDragging" class="drag-indicator">📌</span></span
|
||||
>
|
||||
<span class="toggle-btn">{{ isCollapsed ? '展开' : '收起' }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!isCollapsed" class="debug-content" @mousedown.stop>
|
||||
<!-- 路由信息 -->
|
||||
<div class="debug-section">
|
||||
<h4>🛣️ 当前路由信息</h4>
|
||||
<div class="info-item">
|
||||
<span class="label">路径:</span>
|
||||
<span class="value">{{ currentRoute.path }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">名称:</span>
|
||||
<span class="value">{{ currentRoute.name || '未命名' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">标题:</span>
|
||||
<span class="value">{{ currentRoute.meta?.title || '无标题' }}</span>
|
||||
</div>
|
||||
<div v-if="Object.keys(currentRoute.params).length > 0" class="info-item">
|
||||
<span class="label">参数:</span>
|
||||
<pre class="value">{{ JSON.stringify(currentRoute.params, null, 2) }}</pre>
|
||||
</div>
|
||||
<div v-if="Object.keys(currentRoute.query).length > 0" class="info-item">
|
||||
<span class="label">查询:</span>
|
||||
<pre class="value">{{ JSON.stringify(currentRoute.query, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 路由历史 -->
|
||||
<div class="debug-section">
|
||||
<h4>📚 路由历史 (最近10条)</h4>
|
||||
<div class="route-history">
|
||||
<div
|
||||
v-for="(route, index) in routeHistory"
|
||||
:key="index"
|
||||
class="history-item"
|
||||
:class="{ current: index === 0 }"
|
||||
>
|
||||
<span class="time">{{ route.time }}</span>
|
||||
<span class="path">{{ route.path }}</span>
|
||||
<span class="name">{{ route.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 环境信息 -->
|
||||
<div class="debug-section">
|
||||
<h4>⚙️ 环境信息</h4>
|
||||
<div class="info-item">
|
||||
<span class="label">Vue版本:</span>
|
||||
<span class="value">{{ vueVersion }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">开发模式:</span>
|
||||
<span class="value">{{ isDev ? '是' : '否' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">当前时间:</span>
|
||||
<span class="value">{{ currentTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷导航 -->
|
||||
<div class="debug-section">
|
||||
<h4>🚀 快捷导航</h4>
|
||||
<div class="quick-nav">
|
||||
<button
|
||||
v-for="route in commonRoutes"
|
||||
:key="route.path"
|
||||
@click="navigateTo(route.path)"
|
||||
class="nav-btn"
|
||||
:class="{ active: currentRoute.path === route.path }"
|
||||
>
|
||||
{{ route.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DebugPanel />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, version, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
interface RouteHistoryItem {
|
||||
path: string
|
||||
name: string | null | undefined
|
||||
time: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 开发环境检测
|
||||
const isDev = ref(import.meta.env.DEV)
|
||||
|
||||
// 面板状态
|
||||
const isCollapsed = ref(false)
|
||||
const isDragging = ref(false)
|
||||
|
||||
// 面板位置
|
||||
const panelPosition = ref({
|
||||
x: window.innerWidth - 360, // 默认右侧位置
|
||||
y: 80, // 默认顶部位置
|
||||
})
|
||||
|
||||
// 拖拽相关状态
|
||||
const dragState = ref({
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startPanelX: 0,
|
||||
startPanelY: 0,
|
||||
})
|
||||
|
||||
// 当前路由信息
|
||||
const currentRoute = computed(() => ({
|
||||
path: route.path,
|
||||
name: route.name,
|
||||
params: route.params,
|
||||
query: route.query,
|
||||
meta: route.meta,
|
||||
}))
|
||||
|
||||
// 路由历史记录
|
||||
const routeHistory = ref<RouteHistoryItem[]>([])
|
||||
|
||||
// Vue版本
|
||||
const vueVersion = ref(version)
|
||||
|
||||
// 当前时间
|
||||
const currentTime = ref('')
|
||||
|
||||
// 常用路由
|
||||
const commonRoutes = [
|
||||
{ path: '/initialization', title: '初始化' },
|
||||
{ path: '/home', title: '主页' },
|
||||
{ path: '/scripts', title: '脚本管理' },
|
||||
{ path: '/plans', title: '计划管理' },
|
||||
{ path: '/queue', title: '调度队列' },
|
||||
{ path: '/settings', title: '设置' },
|
||||
{ path: '/logs', title: '日志' },
|
||||
]
|
||||
|
||||
// 更新时间
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString()
|
||||
}
|
||||
|
||||
// 添加路由记录
|
||||
const addRouteHistory = (newRoute: typeof route) => {
|
||||
const historyItem: RouteHistoryItem = {
|
||||
path: newRoute.path,
|
||||
name: newRoute.name,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
}
|
||||
|
||||
routeHistory.value.unshift(historyItem)
|
||||
// 只保留最近10条记录
|
||||
if (routeHistory.value.length > 10) {
|
||||
routeHistory.value = routeHistory.value.slice(0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽开始
|
||||
const handleDragStart = (e: MouseEvent) => {
|
||||
isDragging.value = true
|
||||
dragState.value = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
startPanelX: panelPosition.value.x,
|
||||
startPanelY: panelPosition.value.y,
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleDragMove)
|
||||
document.addEventListener('mouseup', handleDragEnd)
|
||||
|
||||
// 防止文本选择
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// 拖拽移动
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
const deltaX = e.clientX - dragState.value.startX
|
||||
const deltaY = e.clientY - dragState.value.startY
|
||||
|
||||
let newX = dragState.value.startPanelX + deltaX
|
||||
let newY = dragState.value.startPanelY + deltaY
|
||||
|
||||
// 边界检测,确保面板不会超出屏幕
|
||||
const panelWidth = isCollapsed.value ? 120 : 350
|
||||
const panelHeight = 400 // 预估高度
|
||||
|
||||
newX = Math.max(0, Math.min(window.innerWidth - panelWidth, newX))
|
||||
newY = Math.max(0, Math.min(window.innerHeight - panelHeight, newY))
|
||||
|
||||
panelPosition.value.x = newX
|
||||
panelPosition.value.y = newY
|
||||
}
|
||||
|
||||
// 拖拽结束
|
||||
const handleDragEnd = () => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleDragMove)
|
||||
document.removeEventListener('mouseup', handleDragEnd)
|
||||
}
|
||||
|
||||
// 切换面板状态
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
}
|
||||
|
||||
// 导航到指定路由
|
||||
const navigateTo = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
// 定时器
|
||||
let timeInterval: NodeJS.Timeout
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
route,
|
||||
newRoute => {
|
||||
addRouteHistory(newRoute)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
|
||||
// 添加键盘快捷键
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
// Ctrl + Shift + D 切换调试面板
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
|
||||
e.preventDefault()
|
||||
toggleCollapse()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress)
|
||||
|
||||
// 窗口大小改变时重新调整位置
|
||||
const handleResize = () => {
|
||||
const panelWidth = isCollapsed.value ? 120 : 350
|
||||
const panelHeight = 400
|
||||
|
||||
panelPosition.value.x = Math.max(
|
||||
0,
|
||||
Math.min(window.innerWidth - panelWidth, panelPosition.value.x)
|
||||
)
|
||||
panelPosition.value.y = Math.max(
|
||||
0,
|
||||
Math.min(window.innerHeight - panelHeight, panelPosition.value.y)
|
||||
)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
// 清理函数
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyPress)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
document.removeEventListener('mousemove', handleDragMove)
|
||||
document.removeEventListener('mouseup', handleDragEnd)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
}
|
||||
})
|
||||
import DebugPanel from './devtools/index.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.debug-panel {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 10px;
|
||||
width: 350px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
z-index: 9999;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.debug-panel.collapsed {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.debug-panel.dragging {
|
||||
cursor: grabbing;
|
||||
transition: none; /* 拖动时禁用过渡效果 */
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.debug-header {
|
||||
padding: 8px 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.debug-header:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.debug-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
font-size: 10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.debug-content {
|
||||
padding: 12px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.debug-section {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.debug-section:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.debug-section h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #4caf50;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.label {
|
||||
min-width: 50px;
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
color: #fff;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.value pre {
|
||||
margin: 0;
|
||||
font-size: 10px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.route-history {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.history-item.current {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.history-item .time {
|
||||
color: #999;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.history-item .path {
|
||||
color: #2196f3;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.history-item .name {
|
||||
color: #4caf50;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.quick-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.nav-btn.active {
|
||||
background: #4caf50;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.drag-indicator {
|
||||
animation: bounce 0.5s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.debug-content::-webkit-scrollbar,
|
||||
.route-history::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.debug-content::-webkit-scrollbar-track,
|
||||
.route-history::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.debug-content::-webkit-scrollbar-thumb,
|
||||
.route-history::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
v-if="script.type === 'MAA' && props.activeConnections.has(script.id)"
|
||||
type="primary"
|
||||
size="middle"
|
||||
style="background: #52c41a; border-color: #52c41a;"
|
||||
style="background: #52c41a; border-color: #52c41a"
|
||||
@click="handleSaveMAAConfig(script)"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -102,7 +102,7 @@
|
||||
ghost-class="user-ghost"
|
||||
chosen-class="user-chosen"
|
||||
drag-class="user-drag"
|
||||
@end="(evt) => onUserDragEnd(evt, script)"
|
||||
@end="evt => onUserDragEnd(evt, script)"
|
||||
class="users-list"
|
||||
>
|
||||
<template #item="{ element: user }">
|
||||
@@ -149,7 +149,9 @@
|
||||
|
||||
<!-- 剩余天数 -->
|
||||
<a-tag
|
||||
v-if="user.Info.RemainedDay !== undefined && user.Info.RemainedDay !== null"
|
||||
v-if="
|
||||
user.Info.RemainedDay !== undefined && user.Info.RemainedDay !== null
|
||||
"
|
||||
class="info-tag"
|
||||
:color="getRemainingDayColor(user.Info.RemainedDay)"
|
||||
>
|
||||
@@ -177,7 +179,9 @@
|
||||
<!-- 额外关卡 - 只有不为-或空时才显示 -->
|
||||
<a-tag
|
||||
v-if="
|
||||
user.Info.Stage_1 && user.Info.Stage_1 !== '-' && user.Info.Stage_1 !== ''
|
||||
user.Info.Stage_1 &&
|
||||
user.Info.Stage_1 !== '-' &&
|
||||
user.Info.Stage_1 !== ''
|
||||
"
|
||||
class="info-tag"
|
||||
color="geekblue"
|
||||
@@ -187,7 +191,9 @@
|
||||
|
||||
<a-tag
|
||||
v-if="
|
||||
user.Info.Stage_2 && user.Info.Stage_2 !== '-' && user.Info.Stage_2 !== ''
|
||||
user.Info.Stage_2 &&
|
||||
user.Info.Stage_2 !== '-' &&
|
||||
user.Info.Stage_2 !== ''
|
||||
"
|
||||
class="info-tag"
|
||||
color="geekblue"
|
||||
@@ -197,7 +203,9 @@
|
||||
|
||||
<a-tag
|
||||
v-if="
|
||||
user.Info.Stage_3 && user.Info.Stage_3 !== '-' && user.Info.Stage_3 !== ''
|
||||
user.Info.Stage_3 &&
|
||||
user.Info.Stage_3 !== '-' &&
|
||||
user.Info.Stage_3 !== ''
|
||||
"
|
||||
class="info-tag"
|
||||
color="geekblue"
|
||||
@@ -225,7 +233,9 @@
|
||||
<div v-if="script.type === 'General'" class="user-info-tags">
|
||||
<!-- 剩余天数 -->
|
||||
<a-tag
|
||||
v-if="user.Info.RemainedDay !== undefined && user.Info.RemainedDay !== null"
|
||||
v-if="
|
||||
user.Info.RemainedDay !== undefined && user.Info.RemainedDay !== null
|
||||
"
|
||||
class="info-tag"
|
||||
:color="getRemainingDayColor(user.Info.RemainedDay)"
|
||||
>
|
||||
@@ -321,13 +331,21 @@ interface Props {
|
||||
|
||||
interface Emits {
|
||||
(e: 'edit', script: Script): void
|
||||
|
||||
(e: 'delete', script: Script): void
|
||||
|
||||
(e: 'addUser', script: Script): void
|
||||
|
||||
(e: 'editUser', user: User): void
|
||||
|
||||
(e: 'deleteUser', user: User): void
|
||||
|
||||
(e: 'startMaaConfig', script: Script): void
|
||||
|
||||
(e: 'saveMaaConfig', script: Script): void
|
||||
|
||||
(e: 'toggleUserStatus', user: User): void
|
||||
|
||||
(e: 'scriptsReordered', scripts: Script[]): void
|
||||
}
|
||||
|
||||
@@ -348,7 +366,7 @@ const localScripts = ref<Script[]>([])
|
||||
// 监听props变化,更新本地状态
|
||||
watch(
|
||||
() => props.scripts,
|
||||
(newScripts) => {
|
||||
newScripts => {
|
||||
localScripts.value = [...newScripts]
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
@@ -411,9 +429,25 @@ const getRemainingDayText = (remainedDay: number): string => {
|
||||
// 处理脚本拖拽结束
|
||||
const onScriptDragEnd = async () => {
|
||||
try {
|
||||
const scriptIds = localScripts.value.map((script) => script.id)
|
||||
// 获取当前脚本ID顺序
|
||||
const currentScriptIds = localScripts.value.map(script => script.id)
|
||||
// 获取原始脚本ID顺序
|
||||
const originalScriptIds = props.scripts.map(script => script.id)
|
||||
|
||||
// 检查顺序是否发生变化
|
||||
const hasOrderChanged =
|
||||
currentScriptIds.length !== originalScriptIds.length ||
|
||||
currentScriptIds.some((id, index) => id !== originalScriptIds[index])
|
||||
|
||||
// 如果顺序没有变化,则不触发后端更新
|
||||
if (!hasOrderChanged) {
|
||||
console.log('脚本顺序未发生变化,跳过后端更新')
|
||||
return
|
||||
}
|
||||
|
||||
// 顺序发生变化,调用后端API更新
|
||||
await Service.reorderScriptApiScriptsOrderPost({
|
||||
indexList: scriptIds,
|
||||
indexList: currentScriptIds,
|
||||
})
|
||||
|
||||
// 通知父组件脚本顺序已更改
|
||||
@@ -432,7 +466,7 @@ const onScriptDragEnd = async () => {
|
||||
// 处理用户拖拽结束
|
||||
const onUserDragEnd = async (evt: any, script: Script) => {
|
||||
try {
|
||||
const userIds = script.users?.map((user) => user.id) || []
|
||||
const userIds = script.users?.map(user => user.id) || []
|
||||
await Service.reorderUserApiScriptsUserOrderPost({
|
||||
scriptId: script.id,
|
||||
indexList: userIds,
|
||||
@@ -444,7 +478,7 @@ const onUserDragEnd = async (evt: any, script: Script) => {
|
||||
message.error('保存用户排序失败')
|
||||
|
||||
// 恢复原始顺序 - 找到原始脚本并恢复用户顺序
|
||||
const originalScript = props.scripts.find((s) => s.id === script.id)
|
||||
const originalScript = props.scripts.find(s => s.id === script.id)
|
||||
if (originalScript && originalScript.users) {
|
||||
script.users = [...originalScript.users]
|
||||
}
|
||||
|
||||
174
frontend/src/components/devtools/EnvironmentPage.vue
Normal file
174
frontend/src/components/devtools/EnvironmentPage.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="environment-page">
|
||||
<!-- 环境信息 -->
|
||||
<div class="debug-section">
|
||||
<h4>⚙️ 环境信息</h4>
|
||||
<div class="info-item">
|
||||
<span class="label">Vue版本:</span>
|
||||
<span class="value">{{ vueVersion }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">开发模式:</span>
|
||||
<span class="value">{{ isDev ? '是' : '否' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">当前时间:</span>
|
||||
<span class="value">{{ currentTime }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">用户代理:</span>
|
||||
<span class="value">{{ userAgent }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">屏幕分辨率:</span>
|
||||
<span class="value">{{ screenResolution }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">窗口尺寸:</span>
|
||||
<span class="value">{{ windowSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能信息 -->
|
||||
<div class="debug-section">
|
||||
<h4>📊 性能信息</h4>
|
||||
<div class="info-item">
|
||||
<span class="label">内存使用:</span>
|
||||
<span class="value">{{ memoryInfo }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">页面加载时间:</span>
|
||||
<span class="value">{{ loadTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, version } from 'vue'
|
||||
|
||||
// Vue版本
|
||||
const vueVersion = ref(version)
|
||||
|
||||
// 开发环境检测
|
||||
const isDev = ref(process.env.NODE_ENV === 'development' || import.meta.env?.DEV === true)
|
||||
|
||||
// 当前时间
|
||||
const currentTime = ref('')
|
||||
|
||||
// 环境信息
|
||||
const userAgent = ref('')
|
||||
const screenResolution = ref('')
|
||||
const windowSize = ref('')
|
||||
const memoryInfo = ref('')
|
||||
const loadTime = ref('')
|
||||
|
||||
// 更新时间
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString()
|
||||
}
|
||||
|
||||
// 更新窗口尺寸
|
||||
const updateWindowSize = () => {
|
||||
windowSize.value = `${window.innerWidth}x${window.innerHeight}`
|
||||
}
|
||||
|
||||
// 获取内存信息
|
||||
const updateMemoryInfo = () => {
|
||||
if ('memory' in performance) {
|
||||
const memory = (performance as any).memory
|
||||
const used = Math.round(memory.usedJSHeapSize / 1024 / 1024)
|
||||
const total = Math.round(memory.totalJSHeapSize / 1024 / 1024)
|
||||
const limit = Math.round(memory.jsHeapSizeLimit / 1024 / 1024)
|
||||
memoryInfo.value = `${used}MB / ${total}MB (限制: ${limit}MB)`
|
||||
} else {
|
||||
memoryInfo.value = '不支持'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取页面加载时间
|
||||
const getLoadTime = () => {
|
||||
if ('performance' in window) {
|
||||
const perfData = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
|
||||
if (perfData) {
|
||||
const loadTime = Math.round(perfData.loadEventEnd - perfData.navigationStart)
|
||||
return `${loadTime}ms`
|
||||
}
|
||||
}
|
||||
return '未知'
|
||||
}
|
||||
|
||||
// 定时器
|
||||
let timeInterval: NodeJS.Timeout
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化环境信息
|
||||
userAgent.value = navigator.userAgent
|
||||
screenResolution.value = `${screen.width}x${screen.height}`
|
||||
loadTime.value = getLoadTime()
|
||||
|
||||
// 更新时间和窗口尺寸
|
||||
updateTime()
|
||||
updateWindowSize()
|
||||
updateMemoryInfo()
|
||||
|
||||
// 设置定时器
|
||||
timeInterval = setInterval(() => {
|
||||
updateTime()
|
||||
updateMemoryInfo()
|
||||
}, 1000)
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', updateWindowSize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
}
|
||||
window.removeEventListener('resize', updateWindowSize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.environment-page {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.debug-section {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.debug-section:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.debug-section h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #4caf50;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.label {
|
||||
min-width: 70px;
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
color: #fff;
|
||||
word-break: break-word;
|
||||
font-size: 10px;
|
||||
}
|
||||
</style>
|
||||
199
frontend/src/components/devtools/QuickNavPage.vue
Normal file
199
frontend/src/components/devtools/QuickNavPage.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="quick-nav-page">
|
||||
<!-- 快捷导航 -->
|
||||
<div class="debug-section">
|
||||
<h4>🚀 快捷导航</h4>
|
||||
<div class="quick-nav">
|
||||
<button
|
||||
v-for="route in commonRoutes"
|
||||
:key="route.path"
|
||||
@click="navigateTo(route.path)"
|
||||
class="nav-btn"
|
||||
:class="{ active: currentRoute.path === route.path }"
|
||||
>
|
||||
{{ route.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 开发工具 -->
|
||||
<div class="debug-section">
|
||||
<h4>🛠️ 开发工具</h4>
|
||||
<div class="tool-actions">
|
||||
<button @click="clearStorage" class="action-btn">清除存储</button>
|
||||
<button @click="reloadPage" class="action-btn">重新加载</button>
|
||||
<button @click="toggleConsole" class="action-btn">切换控制台</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷键说明 -->
|
||||
<div class="debug-section">
|
||||
<h4>⌨️ 快捷键</h4>
|
||||
<div class="shortcut-list">
|
||||
<div class="shortcut-item">
|
||||
<span class="keys">Ctrl + Shift + D</span>
|
||||
<span class="desc">切换调试面板</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span class="keys">F12</span>
|
||||
<span class="desc">开发者工具</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span class="keys">Ctrl + R</span>
|
||||
<span class="desc">刷新页面</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 当前路由信息
|
||||
const currentRoute = computed(() => ({
|
||||
path: route.path,
|
||||
name: route.name,
|
||||
params: route.params,
|
||||
query: route.query,
|
||||
meta: route.meta,
|
||||
}))
|
||||
|
||||
// 常用路由
|
||||
const commonRoutes = [
|
||||
{ path: '/initialization', title: '初始化' },
|
||||
{ path: '/home', title: '主页' },
|
||||
{ path: '/scripts', title: '脚本管理' },
|
||||
{ path: '/plans', title: '计划管理' },
|
||||
{ path: '/queue', title: '调度队列' },
|
||||
{ path: '/settings', title: '设置' },
|
||||
{ path: '/logs', title: '日志' },
|
||||
]
|
||||
|
||||
// 导航到指定路由
|
||||
const navigateTo = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
// 清除本地存储
|
||||
const clearStorage = () => {
|
||||
if (confirm('确定要清除所有本地存储数据吗?')) {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
console.log('本地存储已清除')
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载页面
|
||||
const reloadPage = () => {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
// 切换控制台(仅在开发环境有效)
|
||||
const toggleConsole = () => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('控制台切换功能仅在开发环境可用')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quick-nav-page {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.debug-section {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.debug-section:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.debug-section h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #4caf50;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quick-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.nav-btn.active {
|
||||
background: #4caf50;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.tool-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgba(255, 152, 0, 0.3);
|
||||
border-color: rgba(255, 152, 0, 0.5);
|
||||
}
|
||||
|
||||
.shortcut-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.keys {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
font-family: monospace;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
192
frontend/src/components/devtools/RouteInfoPage.vue
Normal file
192
frontend/src/components/devtools/RouteInfoPage.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="route-info-page">
|
||||
<!-- 当前路由信息 -->
|
||||
<div class="debug-section">
|
||||
<h4>🛣️ 当前路由信息</h4>
|
||||
<div class="info-item">
|
||||
<span class="label">路径:</span>
|
||||
<span class="value">{{ currentRoute.path }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">名称:</span>
|
||||
<span class="value">{{ currentRoute.name || '未命名' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">标题:</span>
|
||||
<span class="value">{{ currentRoute.meta?.title || '无标题' }}</span>
|
||||
</div>
|
||||
<div v-if="Object.keys(currentRoute.params).length > 0" class="info-item">
|
||||
<span class="label">参数:</span>
|
||||
<pre class="value">{{ JSON.stringify(currentRoute.params, null, 2) }}</pre>
|
||||
</div>
|
||||
<div v-if="Object.keys(currentRoute.query).length > 0" class="info-item">
|
||||
<span class="label">查询:</span>
|
||||
<pre class="value">{{ JSON.stringify(currentRoute.query, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 路由历史 -->
|
||||
<div class="debug-section">
|
||||
<h4>📚 路由历史 (最近10条)</h4>
|
||||
<div class="route-history">
|
||||
<div
|
||||
v-for="(route, index) in routeHistory"
|
||||
:key="index"
|
||||
class="history-item"
|
||||
:class="{ current: index === 0 }"
|
||||
>
|
||||
<span class="time">{{ route.time }}</span>
|
||||
<span class="path">{{ route.path }}</span>
|
||||
<span class="name">{{ route.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
interface RouteHistoryItem {
|
||||
path: string
|
||||
name: string | null | undefined
|
||||
time: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 当前路由信息
|
||||
const currentRoute = computed(() => ({
|
||||
path: route.path,
|
||||
name: route.name,
|
||||
params: route.params,
|
||||
query: route.query,
|
||||
meta: route.meta,
|
||||
}))
|
||||
|
||||
// 路由历史记录
|
||||
const routeHistory = ref<RouteHistoryItem[]>([])
|
||||
|
||||
// 添加路由记录
|
||||
const addRouteHistory = (newRoute: typeof route) => {
|
||||
const historyItem: RouteHistoryItem = {
|
||||
path: newRoute.path,
|
||||
name: typeof newRoute.name === 'string' ? newRoute.name : String(newRoute.name || ''),
|
||||
time: new Date().toLocaleTimeString(),
|
||||
}
|
||||
|
||||
routeHistory.value.unshift(historyItem)
|
||||
// 只保留最近10条记录
|
||||
if (routeHistory.value.length > 10) {
|
||||
routeHistory.value = routeHistory.value.slice(0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
route,
|
||||
newRoute => {
|
||||
addRouteHistory(newRoute)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.route-info-page {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.debug-section {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.debug-section:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.debug-section h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #4caf50;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.label {
|
||||
min-width: 50px;
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
color: #fff;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.value pre {
|
||||
margin: 0;
|
||||
font-size: 10px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.route-history {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.history-item.current {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.history-item .time {
|
||||
color: #999;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.history-item .path {
|
||||
color: #2196f3;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.history-item .name {
|
||||
color: #4caf50;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.route-history::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.route-history::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.route-history::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
314
frontend/src/components/devtools/index.vue
Normal file
314
frontend/src/components/devtools/index.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isDev"
|
||||
class="debug-panel"
|
||||
:class="{ collapsed: isCollapsed, dragging: isDragging }"
|
||||
:style="{ left: `${panelPosition.x}px`, top: `${panelPosition.y}px` }"
|
||||
>
|
||||
<div class="debug-header">
|
||||
<span class="debug-title drag-handle" @mousedown="handleDragStart">
|
||||
调试面板 <span v-if="isDragging" class="drag-indicator">📌</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="toggle-btn" @click="toggleCollapse" @mousedown.stop>
|
||||
{{ isCollapsed ? '展开' : '收起' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isCollapsed" class="debug-content" @mousedown.stop>
|
||||
<!-- 页面切换选项卡 -->
|
||||
<div class="debug-tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
@click="setActiveTab(tab.key)"
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
>
|
||||
{{ tab.icon }} {{ tab.title }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<div class="debug-pages">
|
||||
<component :is="currentComponent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import RouteInfoPage from './RouteInfoPage.vue'
|
||||
import EnvironmentPage from './EnvironmentPage.vue'
|
||||
import QuickNavPage from './QuickNavPage.vue'
|
||||
|
||||
// 调试页面配置
|
||||
const tabs = [
|
||||
{ key: 'route', title: '路由', icon: '🛣️', component: RouteInfoPage },
|
||||
{ key: 'env', title: '环境', icon: '⚙️', component: EnvironmentPage },
|
||||
{ key: 'nav', title: '导航', icon: '🚀', component: QuickNavPage },
|
||||
]
|
||||
|
||||
// 开发环境检测
|
||||
const isDev = ref(process.env.NODE_ENV === 'development' || import.meta.env?.DEV === true)
|
||||
|
||||
// 面板状态
|
||||
const isCollapsed = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const activeTab = ref('route')
|
||||
|
||||
// 面板位置
|
||||
const panelPosition = ref({
|
||||
x: window.innerWidth - 360, // 默认右侧位置
|
||||
y: 80, // 默认顶部位置
|
||||
})
|
||||
|
||||
// 拖拽相关状态
|
||||
const dragState = ref({
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startPanelX: 0,
|
||||
startPanelY: 0,
|
||||
})
|
||||
|
||||
// 当前组件
|
||||
const currentComponent = computed(() => {
|
||||
const tab = tabs.find(t => t.key === activeTab.value)
|
||||
return tab?.component || RouteInfoPage
|
||||
})
|
||||
|
||||
// 设置活动选项卡
|
||||
const setActiveTab = (tabKey: string) => {
|
||||
activeTab.value = tabKey
|
||||
}
|
||||
|
||||
// 拖拽开始
|
||||
const handleDragStart = (e: MouseEvent) => {
|
||||
isDragging.value = true
|
||||
dragState.value = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
startPanelX: panelPosition.value.x,
|
||||
startPanelY: panelPosition.value.y,
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleDragMove)
|
||||
document.addEventListener('mouseup', handleDragEnd)
|
||||
|
||||
// 防止文本选择
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// 拖拽移动
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
const deltaX = e.clientX - dragState.value.startX
|
||||
const deltaY = e.clientY - dragState.value.startY
|
||||
|
||||
let newX = dragState.value.startPanelX + deltaX
|
||||
let newY = dragState.value.startPanelY + deltaY
|
||||
|
||||
// 边界检测,确保面板不会超出屏幕
|
||||
const panelWidth = isCollapsed.value ? 120 : 350
|
||||
const panelHeight = 400 // 预估高度
|
||||
|
||||
newX = Math.max(0, Math.min(window.innerWidth - panelWidth, newX))
|
||||
newY = Math.max(0, Math.min(window.innerHeight - panelHeight, newY))
|
||||
|
||||
panelPosition.value.x = newX
|
||||
panelPosition.value.y = newY
|
||||
}
|
||||
|
||||
// 拖拽结束
|
||||
const handleDragEnd = () => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleDragMove)
|
||||
document.removeEventListener('mouseup', handleDragEnd)
|
||||
}
|
||||
|
||||
// 切换面板状态
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 添加键盘快捷键
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
// Ctrl + Shift + D 切换调试面板
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
|
||||
e.preventDefault()
|
||||
toggleCollapse()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress)
|
||||
|
||||
// 窗口大小改变时重新调整位置
|
||||
const handleResize = () => {
|
||||
const panelWidth = isCollapsed.value ? 120 : 350
|
||||
const panelHeight = 400
|
||||
|
||||
panelPosition.value.x = Math.max(
|
||||
0,
|
||||
Math.min(window.innerWidth - panelWidth, panelPosition.value.x)
|
||||
)
|
||||
panelPosition.value.y = Math.max(
|
||||
0,
|
||||
Math.min(window.innerHeight - panelHeight, panelPosition.value.y)
|
||||
)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
// 清理函数
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyPress)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
document.removeEventListener('mousemove', handleDragMove)
|
||||
document.removeEventListener('mouseup', handleDragEnd)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.debug-panel {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 10px;
|
||||
width: 350px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
z-index: 9999;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.debug-panel.collapsed {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.debug-panel.dragging {
|
||||
cursor: grabbing;
|
||||
transition: none;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.debug-header {
|
||||
padding: 8px 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
font-weight: bold;
|
||||
cursor: grab;
|
||||
flex: 1;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.debug-content {
|
||||
padding: 0;
|
||||
max-height: 60vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.debug-tabs {
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 8px 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
transition: all 0.2s ease;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tab-btn:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: #4caf50;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.debug-pages {
|
||||
padding: 12px;
|
||||
max-height: calc(60vh - 40px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.drag-indicator {
|
||||
animation: bounce 0.5s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.debug-pages::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.debug-pages::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.debug-pages::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
49
frontend/src/composables/useRouteLock.example.js
Normal file
49
frontend/src/composables/useRouteLock.example.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// 使用示例:在任何组件中使用路由锁定功能
|
||||
|
||||
import { useRouteLock } from '@/composables/useRouteLock'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const { lockRoute, unlockRoute, isRouteLocked } = useRouteLock()
|
||||
|
||||
// 示例1:在表单编辑时锁定路由
|
||||
const startEditing = () => {
|
||||
lockRoute(targetRoute => {
|
||||
// 用户尝试切换路由时的回调
|
||||
console.log(`用户尝试切换到 ${targetRoute},但表单正在编辑中`)
|
||||
|
||||
// 可以显示确认对话框
|
||||
if (confirm('表单正在编辑中,确定要离开吗?')) {
|
||||
unlockRoute() // 解锁路由
|
||||
// 然后可以手动导航到目标路由
|
||||
router.push(targetRoute)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 示例2:保存完成后解锁路由
|
||||
const saveForm = async () => {
|
||||
try {
|
||||
// 保存逻辑...
|
||||
await saveData()
|
||||
|
||||
// 保存成功后解锁路由
|
||||
unlockRoute()
|
||||
} catch (error) {
|
||||
console.error('保存失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 示例3:取消编辑时解锁路由
|
||||
const cancelEdit = () => {
|
||||
unlockRoute()
|
||||
}
|
||||
|
||||
return {
|
||||
startEditing,
|
||||
saveForm,
|
||||
cancelEdit,
|
||||
isRouteLocked,
|
||||
}
|
||||
},
|
||||
}
|
||||
50
frontend/src/composables/useRouteLock.ts
Normal file
50
frontend/src/composables/useRouteLock.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
type RouteBlockCallback = (targetRoute: string) => void
|
||||
|
||||
const isRouteLocked = ref(false)
|
||||
let blockCallback: RouteBlockCallback | null = null
|
||||
|
||||
export function useRouteLock() {
|
||||
/**
|
||||
* 锁定路由切换
|
||||
* @param callback 当用户尝试切换路由时的回调函数
|
||||
*/
|
||||
const lockRoute = (callback: RouteBlockCallback) => {
|
||||
isRouteLocked.value = true
|
||||
blockCallback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* 解锁路由切换
|
||||
*/
|
||||
const unlockRoute = () => {
|
||||
isRouteLocked.value = false
|
||||
blockCallback = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路由是否被锁定
|
||||
*/
|
||||
const checkRouteLocked = () => {
|
||||
return isRouteLocked.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发路由阻止回调
|
||||
* @param targetRoute 用户尝试访问的目标路由
|
||||
*/
|
||||
const triggerBlockCallback = (targetRoute: string) => {
|
||||
if (blockCallback) {
|
||||
blockCallback(targetRoute)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isRouteLocked,
|
||||
lockRoute,
|
||||
unlockRoute,
|
||||
checkRouteLocked,
|
||||
triggerBlockCallback,
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,15 @@ export function useScriptApi() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取脚本列表
|
||||
const getScripts = async (): Promise<ScriptDetail[]> => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
// 获取脚本列表(可选择是否管理 loading 状态,避免嵌套调用时提前结束 loading)
|
||||
const getScripts = async (manageLoading: boolean = true): Promise<ScriptDetail[]> => {
|
||||
if (manageLoading) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
} else {
|
||||
// 仅清理错误,不改变外部 loading
|
||||
error.value = null
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await Service.getScriptsApiScriptsGetPost({})
|
||||
@@ -73,18 +78,20 @@ export function useScriptApi() {
|
||||
}
|
||||
return []
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (manageLoading) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取脚本列表及其用户数据
|
||||
// 获取脚本列表及其用户数据(统一管理一次 loading)
|
||||
const getScriptsWithUsers = async (): Promise<ScriptDetail[]> => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// 首先获取脚本列表
|
||||
const scriptDetails = await getScripts()
|
||||
// 首先获取脚本列表,但不在内部结束 loading
|
||||
const scriptDetails = await getScripts(false)
|
||||
|
||||
// 为每个脚本获取用户数据
|
||||
const scriptsWithUsers = await Promise.all(
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
<template>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<a-spin size="large" tip="加载中,请稍候..." />
|
||||
<!-- MAA配置遮罩层 -->
|
||||
<div v-if="showMAAConfigMask" class="maa-config-mask">
|
||||
<div class="mask-content">
|
||||
<div class="mask-icon">
|
||||
<SettingOutlined :style="{ fontSize: '48px', color: '#1890ff' }" />
|
||||
</div>
|
||||
<h2 class="mask-title">正在进行MAA配置</h2>
|
||||
<p class="mask-description">
|
||||
当前正在配置MAA脚本,请在MAA配置界面完成相关设置。
|
||||
<br />
|
||||
配置完成后,请点击"保存配置"按钮来解除页面锁定。
|
||||
</p>
|
||||
<div class="mask-actions">
|
||||
<a-button
|
||||
v-if="currentConfigScript"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleSaveMAAConfig(currentConfigScript)"
|
||||
>
|
||||
保存配置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
@@ -20,7 +40,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="scripts.length === 0" class="empty-state">
|
||||
<!-- 增加 loadedOnce 条件,避免初始渲染时闪烁 -->
|
||||
<div v-if="!loading && loadedOnce && scripts.length === 0" class="empty-state">
|
||||
<div class="empty-content">
|
||||
<div class="empty-image-container">
|
||||
<img src="@/assets/NoData.png" alt="暂无数据" class="empty-image" />
|
||||
@@ -237,7 +258,7 @@ import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
const router = useRouter()
|
||||
const { addScript, deleteScript, getScriptsWithUsers, loading } = useScriptApi()
|
||||
const { addScript, deleteScript, getScriptsWithUsers } = useScriptApi()
|
||||
const { updateUser, deleteUser } = useUserApi()
|
||||
const { subscribe, unsubscribe } = useWebSocket()
|
||||
const { getWebConfigTemplates, importScriptFromWeb } = useTemplateApi()
|
||||
@@ -250,6 +271,8 @@ const md = new MarkdownIt({
|
||||
})
|
||||
|
||||
const scripts = ref<Script[]>([])
|
||||
// 增加:标记是否已经完成过一次脚本列表加载(成功或失败都算一次)
|
||||
const loadedOnce = ref(false)
|
||||
const typeSelectVisible = ref(false)
|
||||
const generalModeSelectVisible = ref(false)
|
||||
const templateSelectVisible = ref(false)
|
||||
@@ -260,6 +283,8 @@ const templates = ref<WebConfigTemplate[]>([])
|
||||
const addLoading = ref(false)
|
||||
const templateLoading = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const showMAAConfigMask = ref(false) // 控制MAA配置遮罩层的显示
|
||||
const currentConfigScript = ref<Script | null>(null) // 当前正在配置的脚本
|
||||
|
||||
// WebSocket连接管理
|
||||
const activeConnections = ref<Map<string, string>>(new Map()) // scriptId -> websocketId
|
||||
@@ -305,6 +330,9 @@ const loadScripts = async () => {
|
||||
} catch (error) {
|
||||
console.error('加载脚本列表失败:', error)
|
||||
message.error('加载脚本列表失败')
|
||||
} finally {
|
||||
// 首次加载结束(不论成功失败)后置位,避免初始闪烁
|
||||
loadedOnce.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,18 +543,31 @@ const handleStartMAAConfig = async (script: Script) => {
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
// 显示遮罩层
|
||||
showMAAConfigMask.value = true
|
||||
currentConfigScript.value = script
|
||||
|
||||
// 订阅WebSocket消息
|
||||
subscribe(response.websocketId, {
|
||||
onError: error => {
|
||||
console.error(`脚本 ${script.name} 连接错误:`, error)
|
||||
message.error(`MAA配置连接失败: ${error}`)
|
||||
activeConnections.value.delete(script.id)
|
||||
},
|
||||
onResult: (data: any) => {
|
||||
onMessage: (wsMessage: any) => {
|
||||
// 处理错误消息
|
||||
if (wsMessage.type === 'error') {
|
||||
console.error(`脚本 ${script.name} 连接错误:`, wsMessage.data)
|
||||
message.error(`MAA配置连接失败: ${wsMessage.data}`)
|
||||
activeConnections.value.delete(script.id)
|
||||
// 连接错误时隐藏遮罩
|
||||
showMAAConfigMask.value = false
|
||||
currentConfigScript.value = null
|
||||
return
|
||||
}
|
||||
|
||||
// 处理配置完成消息(兼容任何结构)
|
||||
if (data.Accomplish) {
|
||||
if (wsMessage.data && wsMessage.data.Accomplish) {
|
||||
message.success(`${script.name} 配置已完成`)
|
||||
activeConnections.value.delete(script.id)
|
||||
// 自动隐藏遮罩
|
||||
showMAAConfigMask.value = false
|
||||
currentConfigScript.value = null
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -544,6 +585,9 @@ const handleStartMAAConfig = async (script: Script) => {
|
||||
unsubscribe(wsId)
|
||||
}
|
||||
activeConnections.value.delete(script.id)
|
||||
// 超时时隐藏遮罩
|
||||
showMAAConfigMask.value = false
|
||||
currentConfigScript.value = null
|
||||
message.info(`${script.name} 配置会话已超时断开`)
|
||||
}
|
||||
},
|
||||
@@ -575,6 +619,11 @@ const handleSaveMAAConfig = async (script: Script) => {
|
||||
// 取消订阅
|
||||
unsubscribe(websocketId)
|
||||
activeConnections.value.delete(script.id)
|
||||
|
||||
// 隐藏遮罩
|
||||
showMAAConfigMask.value = false
|
||||
currentConfigScript.value = null
|
||||
|
||||
message.success(`${script.name} 的配置已保存`)
|
||||
} else {
|
||||
message.error(response.message || '保存配置失败')
|
||||
@@ -604,12 +653,9 @@ const handleToggleUserStatus = async (user: User) => {
|
||||
})
|
||||
|
||||
if (result) {
|
||||
// 更新本地数据状态
|
||||
const targetUser = script.users.find(u => u.id === user.id)
|
||||
if (targetUser) {
|
||||
targetUser.Info.Status = newStatus
|
||||
}
|
||||
message.success('用户状态更新成功')
|
||||
// 更新本地用户状态
|
||||
user.Info.Status = newStatus
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新用户状态失败:', error)
|
||||
@@ -619,175 +665,186 @@ const handleToggleUserStatus = async (user: User) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-container {
|
||||
.maa-config-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.scripts-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 24px;
|
||||
padding: 0 4px;
|
||||
.mask-content {
|
||||
background: var(--ant-color-bg-elevated);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow:
|
||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--ant-color-border);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
.mask-icon {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
.mask-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px;
|
||||
color: var(--ant-color-text);
|
||||
background: linear-gradient(135deg, var(--ant-color-primary), var(--ant-color-primary-hover));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.mask-description {
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
margin: 0 0 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mask-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link .anticon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100vh - 200px);
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-image-container {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 22px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
margin: 0;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 模态框通用样式 */
|
||||
.type-select-modal :deep(.ant-modal-content),
|
||||
.general-mode-modal :deep(.ant-modal-content),
|
||||
.template-select-modal :deep(.ant-modal-content) {
|
||||
border-radius: 12px;
|
||||
.scripts-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.type-select-modal :deep(.ant-modal-header),
|
||||
.general-mode-modal :deep(.ant-modal-header),
|
||||
.template-select_modal :deep(.ant-modal-header) {
|
||||
border-bottom: 1px solid var(--ant-color-border);
|
||||
padding: 16px 24px;
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
.type-select-modal :deep(.ant-modal-title),
|
||||
.general-mode_modal :deep(.ant-modal-title),
|
||||
.template-select-modal :deep(.ant-modal-title) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
.type-select-modal,
|
||||
.general-mode-modal,
|
||||
.template-select-modal {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.type-select-modal :deep(.ant-modal-body),
|
||||
.general-mode-modal :deep(.ant-modal-body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.template-select-modal :deep(.ant-modal-body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 选择组样式 */
|
||||
.type-selection,
|
||||
.mode-selection {
|
||||
margin: 16px 0;
|
||||
.mode-selection,
|
||||
.template-selection {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.type-radio-group,
|
||||
.mode-radio-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.type-radio-group :deep(.ant-radio-button-wrapper),
|
||||
.mode-radio-group :deep(.ant-radio-button-wrapper) {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--ant-color-bg-container);
|
||||
transition: all 0.3s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.type-radio-group :deep(.ant-radio-button-wrapper:hover),
|
||||
.mode-radio-group :deep(.ant-radio-button-wrapper:hover) {
|
||||
border-color: var(--ant-color-primary);
|
||||
}
|
||||
|
||||
.type-radio-group :deep(.ant-radio-button-wrapper-checked),
|
||||
.mode-radio-group :deep(.ant-radio-button-wrapper-checked) {
|
||||
border-color: var(--ant-color-primary);
|
||||
background: var(--ant-color-primary-bg);
|
||||
color: var(--ant-color-primary);
|
||||
}
|
||||
|
||||
.type-radio-group :deep(.ant-radio-button-wrapper::before),
|
||||
.mode-radio-group :deep(.ant-radio-button-wrapper::before) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.type-radio-group :deep(.ant-radio-button-wrapper .ant-radio-button),
|
||||
.mode-radio-group :deep(.ant-radio-button-wrapper .ant-radio-button) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.type-content {
|
||||
.type-option,
|
||||
.mode-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: var(--ant-color-bg-container);
|
||||
}
|
||||
|
||||
.type-option:hover,
|
||||
.mode-option:hover {
|
||||
border-color: var(--ant-color-primary);
|
||||
background: var(--ant-color-primary-bg);
|
||||
}
|
||||
|
||||
.type-content,
|
||||
.mode-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.type-logo-container {
|
||||
.type-logo-container,
|
||||
.mode-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--ant-color-bg-elevated);
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
flex-shrink: 0;
|
||||
border-radius: 6px;
|
||||
background: var(--ant-color-primary-bg);
|
||||
}
|
||||
|
||||
.type-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.mode-icon {
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
color: var(--ant-color-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.type-info,
|
||||
@@ -798,68 +855,53 @@ const handleToggleUserStatus = async (user: User) => {
|
||||
.type-title,
|
||||
.mode-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
margin: 0 0 4px;
|
||||
color: var(--ant-color-text);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.type-description,
|
||||
.mode-description {
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 模板选择样式 */
|
||||
.template-selection {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.no-templates {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.no-templates-content {
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
.no-templates-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.templates-container {
|
||||
height: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.templates-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid var(--ant-color-border);
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.templates-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 10px;
|
||||
background: var(--ant-color-primary);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.count-text {
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
@@ -869,70 +911,59 @@ const handleToggleUserStatus = async (user: User) => {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.template-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.templates-list {
|
||||
flex: 1;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 0 24px 24px;
|
||||
/* 隐藏滚动条 */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
.templates-list::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.no-search-results {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
.no-results-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
|
||||
.no-results-tip {
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
|
||||
.template-item {
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 6px;
|
||||
background: var(--ant-color-bg-container);
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
border-color: var(--ant-color-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
.template-item {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--ant-color-border);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
background: var(--ant-color-bg-container);
|
||||
}
|
||||
|
||||
.template-item.selected {
|
||||
border-color: var(--ant-color-primary);
|
||||
.template-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
background: var(--ant-color-primary-bg);
|
||||
}
|
||||
|
||||
.template-item.selected {
|
||||
background: var(--ant-color-primary-bg);
|
||||
border-color: var(--ant-color-primary);
|
||||
}
|
||||
|
||||
.template-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.template-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.template-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.template-name {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
margin: 0 0 4px;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
@@ -951,15 +982,39 @@ const handleToggleUserStatus = async (user: User) => {
|
||||
}
|
||||
|
||||
.template-description {
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.template-description :deep(p) {
|
||||
margin: 0 0 8px 0;
|
||||
.no-search-results,
|
||||
.no-templates {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
.template-description :deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
.no-results-icon,
|
||||
.no-templates-icon {
|
||||
font-size: 48px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.no-templates-content h3 {
|
||||
color: var(--ant-color-text);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.no-templates-content p {
|
||||
color: var(--ant-color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.no-results-tip {
|
||||
font-size: 12px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user