first commit

This commit is contained in:
root 2024-12-01 00:17:27 +08:00
parent 30da3bd709
commit 55725f3f0f
24 changed files with 4863 additions and 0 deletions

1
.env.development Normal file
View file

@ -0,0 +1 @@
VITE_API_BASE_URL=http://localhost:5098

58
.gitignore vendored Normal file
View file

@ -0,0 +1,58 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Dependencies
node_modules
.yarn
.pnp
.pnp.js
# Build output
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment files
.env
.env.local
.env.*.local
# Cache files
.DS_Store
Thumbs.db
# Testing
coverage
/cypress/videos/
/cypress/screenshots/
# Temporary files
*.tmp
*.bak
*.swp
# System Files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

13
index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1082
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

22
package.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "admin-system",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.7.8",
"element-plus": "^2.9.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.4.6"
}
}

14
src/App.vue Normal file
View file

@ -0,0 +1,14 @@
<template>
<router-view></router-view>
</template>
<script setup>
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
</style>

75
src/api/request.js Normal file
View file

@ -0,0 +1,75 @@
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import router from '../router'
import { API_BASE_URL } from '../config/api.config'
const request = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
request.interceptors.request.use(
config => {
// 如果不是登录请求就添加token
if (!config.url.includes('/user/login') && !config.url.includes('/user/register')) {
const token = JSON.parse(localStorage.getItem('token')).token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
console.log('Request Headers:', config.headers.Authorization)
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
response => {
const { data } = response
console.log('Response Data:', data)
if (data.retcode === 0) {
return data
}
// 显示错误消息对话框
ElMessageBox.alert(data.message || '操作失败', '错误', {
type: 'error',
confirmButtonText: '确定'
})
return Promise.reject(new Error(data.message || '操作失败'))
},
error => {
console.error('Request Error:', error)
if (error.response) {
switch (error.response.status) {
case 401:
localStorage.removeItem('token')
localStorage.removeItem('userRole')
router.push('/login')
break
case 403:
ElMessage.error('没有权限访问')
break
default:
ElMessage.error(error.response.data?.message || '请求失败')
}
} else if (error.request) {
// 请求已发出但没有收到响应
ElMessage.error('服务器无响应,请检查后端服务是否启动')
} else {
// 请求配置出错
ElMessage.error('请求配置错误')
}
return Promise.reject(error)
}
)
export default request

95
src/api/task.js Normal file
View file

@ -0,0 +1,95 @@
import request from './request'
import { API_ENDPOINTS } from '../config/api.config'
export const TaskAPI = {
/**
* 获取任务列表
* @param {Object} params 查询参数
* @param {number} params.page 页码
* @param {number} params.pageSize 每页数量
* @param {number} params.status 任务状态
*/
getTaskList(params) {
return request.get(API_ENDPOINTS.TASK.LIST, { params })
},
/**
* 获取任务进程信息
* @param {string} taskId 任务ID
*/
getProcessInfo(taskId) {
return request.get(`/Management/GetProcessInfo/${taskId}`)
},
/**
* 删除任务
* @param {string} taskId 任务ID
*/
deleteTask(taskId) {
return request.delete(`/Management/DeleteTask/${taskId}`)
},
/**
* 获取任务详细信息
* @param {string} taskId 任务ID
*/
getTaskDetails(taskId) {
return request.get(`/Management/GetTaskDetail/${taskId}`)
},
/**
* 终止任务
* @param {string} taskId 任务ID
*/
killTask(taskId) {
return request.post(`/Management/KillTask/${taskId}`)
},
/**
* 获取系统配置
*/
getSystemConfig() {
return request.get('/Management/GetSystemConfig')
},
/**
* 获取任务概览数据
*/
getTaskOverview() {
return request.get('/Management/GetTaskOverview')
},
/**
* 重试任务
* @param {string} taskId 任务ID
*/
retryTask(taskId) {
return request.post(`/Management/RetryTask/${taskId}`)
},
/**
* 立即执行任务调整优先级
* @param {string} taskId 任务ID
*/
prioritizeTask(taskId) {
return request.post(`/Management/PrioritizeTask/${taskId}`)
},
/**
* 立即执行任务
* @param {string} taskId 任务ID
*/
executeImmediately(taskId) {
return request.post(`/Management/ExecuteImmediately/${taskId}`)
},
/**
* 添加离线任务
* @param {string} url 下载地址
*/
addOfflineTask(url) {
return request.post('/AddOfflineTask', null, {
params: { url }
})
}
}

107
src/api/user.js Normal file
View file

@ -0,0 +1,107 @@
import request from './request'
export const UserAPI = {
/**
* 用户注册
* @param {Object} data 注册信息
* @param {string} data.username 用户名
* @param {string} data.password 密码
*/
register(data) {
return request.post('/user/register', data)
},
/**
* 用户登录
* @param {Object} data 登录信息
* @param {string} data.username 用户名
* @param {string} data.password 密码
*/
login(data) {
return request.post('/user/login', data)
},
/**
* 获取用户信息
*/
getUserInfo() {
return request.get('/user/info')
},
/**
* 获取用户列表
* @param {Object} params 查询参数
* @param {number} params.page 页码
* @param {number} params.pageSize 每页数量
*/
getUserList(params) {
return request.get('/user/list', { params })
},
/**
* 修改用户权限
* @param {string} userId 用户ID
* @param {number} newMask 新的权限掩码
*/
updateUserMask(userId, newMask) {
return request.post(`/user/updateMask/${userId}`, {
newMask
})
},
/**
* 修改昵称
* @param {string} newNickname 新昵称
*/
updateNickname(newNickname) {
return request.post('/user/updateNickname', {
newNickname
})
},
/**
* 修改密码
* @param {string} oldPassword 旧密码
* @param {string} newPassword 新密码
*/
updatePassword(oldPassword, newPassword) {
return request.post('/user/updatePassword', {
oldPassword,
newPassword
})
},
/**
* 重置用户密码超级管理员
* @param {string} userId 用户ID
* @param {string} newPassword 新密码
*/
resetPassword(userId, newPassword) {
return request.post(`/user/resetPassword/${userId}`, {
newPassword
})
},
/**
* 获取用户事件列表
* @param {Object} params 查询参数
* @param {number} params.page 页码
* @param {number} params.pageSize 每页数量
* @param {string} [params.userId] 用户ID超级管理员可选
* @param {string} [params.keyword] 搜索关键词
*/
getUserEvents(params) {
return request.get('/user/events', { params })
},
/**
* 修改用户昵称超级管理员
* @param {string} userId 用户ID
* @param {string} newNickname 新昵称
*/
updateUserNickname(userId, newNickname) {
return request.post(`/user/updateNickname/${userId}`, {
newNickname
})
}
}

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

8
src/config/api.config.js Normal file
View file

@ -0,0 +1,8 @@
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000'
export const API_ENDPOINTS = {
TASK: {
LIST: '/Management/GetTaskList',
PROCESS: (id) => `/Management/${id}/process`,
}
}

196
src/layout/Layout.vue Normal file
View file

@ -0,0 +1,196 @@
<template>
<el-container class="layout-container">
<el-aside width="220px" class="aside">
<div class="logo">
<img src="../assets/logo.png" alt="logo" class="logo-img">
<span class="logo-text">iFileProxy<br />数据管理系统</span>
</div>
<el-menu
router
:default-active="$route.path"
class="el-menu-vertical"
background-color="#001529"
text-color="rgba(255,255,255,0.65)"
active-text-color="#fff">
<el-menu-item index="/dashboard">
<el-icon><DataLine /></el-icon>
<span>概览面板</span>
</el-menu-item>
<template v-if="userRole === 'admin'">
<el-menu-item index="/tasks">
<el-icon><List /></el-icon>
<span>任务管理</span>
</el-menu-item>
<el-menu-item index="/config">
<el-icon><Setting /></el-icon>
<span>系统配置</span>
</el-menu-item>
<template v-if="userInfo.mask === 2">
<el-menu-item index="/users">
<el-icon><User /></el-icon>
<span>用户管理</span>
</el-menu-item>
</template>
<el-menu-item index="/events">
<el-icon><List /></el-icon>
<span>事件记录</span>
</el-menu-item>
</template>
</el-menu>
</el-aside>
<el-container>
<el-header class="header">
<div class="header-left">
<el-icon class="toggle-icon"><Expand /></el-icon>
</div>
<div class="header-right">
<el-dropdown>
<span class="user-info">
<el-avatar size="small" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
<span class="username">你好,
{{ userInfo.nickname }}
</span>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="router.push('/profile')">
<el-icon><User /></el-icon>个人信息
</el-dropdown-item>
<el-dropdown-item divided @click="handleLogout">
<el-icon><SwitchButton /></el-icon>退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main class="main-container">
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { DataLine, List, Expand, Setting, User, SwitchButton } from '@element-plus/icons-vue'
const router = useRouter()
const userRole = computed(() => localStorage.getItem('userRole'))
const userInfo = computed(() => JSON.parse(localStorage.getItem('userInfo')))
const handleLogout = () => {
localStorage.removeItem('token')
localStorage.removeItem('isAuthenticated')
localStorage.removeItem('userRole')
router.push('/login')
}
</script>
<style scoped>
.layout-container {
height: 100vh;
}
.aside {
background-color: #001529;
transition: width 0.3s;
overflow: hidden;
}
.logo {
height: 60px;
padding: 10px 20px;
display: flex;
align-items: center;
background: #002140;
}
.logo-img {
width: 32px;
height: 32px;
margin-right: 12px;
}
.logo-text {
color: white;
font-size: 16px;
font-weight: 600;
}
.header {
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
}
.header-left {
display: flex;
align-items: center;
}
.toggle-icon {
font-size: 20px;
cursor: pointer;
color: #666;
}
.header-right {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
cursor: pointer;
}
.username {
margin-left: 8px;
font-size: 14px;
color: #666;
}
.main-container {
background-color: #f0f2f5;
padding: 20px;
}
.el-menu {
border-right: none;
}
.el-menu-vertical {
height: calc(100% - 60px);
}
:deep(.el-menu-item) {
height: 50px;
line-height: 50px;
}
:deep(.el-menu-item.is-active) {
background-color: #1890ff !important;
}
:deep(.el-menu-item:hover) {
background-color: #001f3d !important;
}
:deep(.el-dropdown-menu__item) {
display: flex;
align-items: center;
gap: 8px;
}
:deep(.el-dropdown-menu__item .el-icon) {
margin: 0;
}
</style>

17
src/main.js Normal file
View file

@ -0,0 +1,17 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(ElementPlus)
app.mount('#app')

119
src/router/index.js Normal file
View file

@ -0,0 +1,119 @@
import { createRouter, createWebHistory } from 'vue-router'
import { ElMessage } from 'element-plus'
import Layout from '../layout/Layout.vue'
import { UserAPI } from '../api/user'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('../views/LoginView.vue')
},
{
path: '/register',
name: 'Register',
component: () => import('../views/RegisterView.vue')
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue'),
meta: { roles: ['admin', 'guest'] }
},
{
path: 'tasks',
name: 'Tasks',
component: () => import('../views/TaskManagement.vue'),
meta: { roles: ['admin'] }
},
{
path: 'config',
name: 'Config',
component: () => import('../views/ConfigView.vue'),
meta: { roles: ['admin'] }
},
{
path: 'users',
name: 'Users',
component: () => import('../views/UserManagement.vue'),
meta: { roles: ['admin'] }
},
{
path: 'profile',
name: 'Profile',
component: () => import('../views/UserProfile.vue'),
meta: { roles: ['admin', 'guest'] }
},
{
path: 'events',
name: 'Events',
component: () => import('../views/UserEvents.vue'),
meta: { roles: ['admin', 'guest'] }
}
]
},
{
path: '/:pathMatch(.*)*',
redirect: '/dashboard'
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 简化路由守卫
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem('token')
const isAuthenticated = localStorage.getItem('isAuthenticated')
const userRole = localStorage.getItem('userRole')
// 登录页和注册页可以直接访问
if (to.path === '/login' || to.path === '/register') {
if (isAuthenticated) {
next('/dashboard')
} else {
next()
}
return
}
if (!isAuthenticated) {
next('/login')
return
}
// 如果是管理员但没有用户信息,尝试获取
if (userRole === 'admin' && token) {
try {
const response = await UserAPI.getUserInfo()
if (response.retcode !== 0) {
// 用户信息获取失败,清除登录状态
localStorage.clear()
next('/login')
return
}
} catch (error) {
localStorage.clear()
next('/login')
return
}
}
// 检查权限
if (to.meta.roles && !to.meta.roles.includes(userRole)) {
ElMessage.error('没有访问权限')
next('/dashboard')
return
}
next()
})
export default router

247
src/views/ConfigView.vue Normal file
View file

@ -0,0 +1,247 @@
<template>
<div class="config-view">
<el-card class="config-card">
<template #header>
<div class="card-header">
<span class="title">系统配置</span>
<el-button type="primary" @click="handleRefresh">
<el-icon><Refresh /></el-icon>刷新配置
</el-button>
</div>
</template>
<el-tabs type="border-card">
<el-tab-pane label="下载配置">
<el-descriptions :column="2" border>
<el-descriptions-item label="临时文件位置">
{{ config.Download.savePath }}
</el-descriptions-item>
<el-descriptions-item label="下载线程数">
{{ config.Download.threadNum }}
</el-descriptions-item>
<el-descriptions-item label="最大文件大小">
{{ formatFileSize(config.Download.maxAllowedFileSize) }}
<el-tooltip
content="仅针对新添加的任务 若是已经添加任务的文件则不受限制"
placement="top">
<el-icon class="ml-2"><InfoFilled /></el-icon>
</el-tooltip>
</el-descriptions-item>
<el-descriptions-item label="最大并行任务">
{{ config.Download.maxParallelTasks }}
</el-descriptions-item>
<el-descriptions-item label="最大队列长度">
{{ config.Download.maxQueueLength }}
</el-descriptions-item>
<el-descriptions-item label="缓存生命周期">
{{ formatDuration(config.Download.cacheLifetime) }}
</el-descriptions-item>
<el-descriptions-item label="Aria2c路径">
{{ config.Download.aria2cPath }}
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="安全配置">
<el-descriptions :column="1" border>
<el-descriptions-item label="禁止代理的主机">
<el-tag
v-for="host in config.Security.blockedHost"
:key="host"
class="mx-1"
type="danger">
{{ host }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="禁止代理的文件">
<el-tag
v-for="file in config.Security.blockedFileName"
:key="file"
class="mx-1"
type="danger">
{{ file }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="IP黑名单">
<el-tag
v-for="ip in config.Security.blockedClientIP"
:key="ip"
class="mx-1"
type="danger">
{{ ip }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="IP每日请求限制">
{{ config.Security.dailyRequestLimitPerIP }} /
</el-descriptions-item>
<el-descriptions-item label="统计请求的路径">
<el-tag
v-for="route in config.Security.routesToTrack"
:key="route"
class="mx-1"
type="info">
{{ route }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="允许不同IP下载">
<el-tag :type="config.Security.allowDifferentIPsForDownload ? 'success' : 'danger'">
{{ config.Security.allowDifferentIPsForDownload ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="数据库配置">
<el-descriptions :column="2" border>
<el-descriptions-item label="数据库主机">
{{ config.Database.Common.Host }}
</el-descriptions-item>
<el-descriptions-item label="端口">
{{ config.Database.Common.Port }}
</el-descriptions-item>
<el-descriptions-item label="用户名">
{{ config.Database.Common.User }}
</el-descriptions-item>
<el-descriptions-item label="密码">
********
</el-descriptions-item>
<el-descriptions-item label="数据库" :span="2">
<el-tag
v-for="db in config.Database.Databases"
:key="db.DatabaseName"
class="mx-1">
{{ db.DatabaseName }}
<el-tooltip
v-if="db.Description"
:content="db.Description"
placement="top">
<el-icon class="ml-2"><InfoFilled /></el-icon>
</el-tooltip>
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh, InfoFilled } from '@element-plus/icons-vue'
import { TaskAPI } from '../api/task'
const config = ref({
Download: {
savePath: '',
threadNum: 0,
maxAllowedFileSize: 0,
maxParallelTasks: 0,
maxQueueLength: 0,
aria2cPath: '',
cacheLifetime: 0
},
Security: {
blockedHost: [],
blockedFileName: [],
blockedClientIP: [],
routesToTrack: [],
dailyRequestLimitPerIP: 0,
allowDifferentIPsForDownload: false
},
Database: {
Common: {
Host: '',
Port: 0,
User: '',
Password: '********'
},
Databases: []
}
})
const formatFileSize = (bytes) => {
if (!bytes) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let i = 0
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024
i++
}
return `${bytes.toFixed(2)} ${units[i]}`
}
const formatDuration = (seconds) => {
if (seconds < 60) return `${seconds}`
if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`
return `${Math.floor(seconds / 3600)}小时${Math.floor((seconds % 3600) / 60)}分钟`
}
const fetchConfig = async () => {
try {
const response = await TaskAPI.getSystemConfig()
if (response.retcode === 0) {
config.value = response.data
}
} catch (error) {
console.error('获取系统配置失败:', error)
}
}
const handleRefresh = async () => {
try {
await fetchConfig()
ElMessage.success('配置已刷新')
} catch (error) {
ElMessage.error('刷新配置失败')
}
}
onMounted(() => {
fetchConfig()
})
</script>
<style scoped>
.config-view {
padding: 20px;
}
.config-card {
background: #fff;
border-radius: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 16px;
font-weight: 500;
}
:deep(.el-descriptions__label) {
width: 140px;
font-weight: 500;
}
.mx-1 {
margin: 0 4px;
}
.ml-2 {
margin-left: 4px;
}
:deep(.el-tabs__content) {
padding: 20px;
}
:deep(.el-tag) {
margin: 4px;
}
</style>

305
src/views/Dashboard.vue Normal file
View file

@ -0,0 +1,305 @@
<template>
<div class="dashboard">
<el-row :gutter="20" v-loading="loading" element-loading-text="加载中...">
<el-col :span="6">
<el-card shadow="hover" class="stat-card total-card">
<div class="stat-header">
<el-icon class="header-icon"><DataLine /></el-icon>
<span class="header-title">总任务数</span>
</div>
<div class="stat-content">
<span class="number">{{ totalTasks }}</span>
<span class="label">个任务</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card running-card">
<div class="stat-header">
<el-icon class="header-icon"><Loading /></el-icon>
<span class="header-title">进行中</span>
</div>
<div class="stat-content">
<span class="number">{{ overview.Running }}</span>
<span class="label">个任务</span>
</div>
<div class="sub-info" v-if="overview.Queuing > 0">
<el-icon><Connection /></el-icon>
<span>队列中: {{ overview.Queuing }}</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card error-card">
<div class="stat-header">
<el-icon class="header-icon"><Warning /></el-icon>
<span class="header-title">异常任务</span>
</div>
<div class="stat-content">
<span class="number">{{ overview.Error }}</span>
<span class="label">个任务</span>
</div>
<div class="sub-info">
<template v-if="overview.Canceled > 0">
<el-icon><CircleClose /></el-icon>
<span class="sub-item">已取消: {{ overview.Canceled }}</span>
</template>
<template v-if="hasUnknownStates">
<el-tooltip
effect="dark"
placement="bottom">
<template #content>
<template v-for="(value, key) in unknownStates" :key="key">
<div>{{ key }}: {{ value }}</div>
</template>
</template>
<span class="sub-item">
<el-icon><More /></el-icon>
其他: {{ unknownStatesCount }}
</span>
</el-tooltip>
</template>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card success-card">
<div class="stat-header">
<el-icon class="header-icon"><Select /></el-icon>
<span class="header-title">已完成</span>
</div>
<div class="stat-content">
<span class="number">{{ overview.End }}</span>
<span class="label">个任务</span>
</div>
<div class="sub-info">
<template v-if="overview.Cached > 0">
<el-icon><Files /></el-icon>
<span class="sub-item">已缓存: {{ overview.Cached }}</span>
</template>
<template v-if="overview.Cleaned > 0">
<el-icon><Delete /></el-icon>
<span class="sub-item">已清理: {{ overview.Cleaned }}</span>
</template>
</div>
</el-card>
</el-col>
</el-row>
<div class="refresh-info">
<el-icon><Timer /></el-icon>
<span>数据将在 {{ countdown }} 秒后刷新</span>
<el-button
link
type="primary"
@click="fetchOverview"
:loading="loading">
<el-icon><RefreshRight /></el-icon>立即刷新
</el-button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { TaskAPI } from '../api/task'
import {
DataLine, Loading, Warning, Select,
Connection, CircleClose, Files, Timer,
RefreshRight, More, Delete
} from '@element-plus/icons-vue'
const overview = ref({
NoInit: 0,
Running: 0,
Error: 0,
End: 0,
Cached: 0,
Cleaned: 0,
Queuing: 0,
Canceled: 0
})
const countdown = ref(30) // 30
let timer = null
const totalTasks = computed(() => {
return Object.values(overview.value).reduce((sum, count) => sum + count, 0)
})
const loading = ref(false) //
const fetchOverview = async () => {
loading.value = true //
try {
const response = await TaskAPI.getTaskOverview()
if (response.retcode === 0) {
overview.value = response.data
}
} catch (error) {
console.error('获取概览数据失败:', error)
} finally {
loading.value = false //
}
}
const startTimer = () => {
timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
fetchOverview()
countdown.value = 30
}
}, 1000)
}
onMounted(() => {
fetchOverview()
startTimer()
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
//
const knownStates = [
'NoInit', 'Running', 'Error', 'End',
'Cached', 'Cleaned', 'Queuing', 'Canceled'
]
//
const unknownStates = computed(() => {
const states = {}
//
if (overview.value.NoInit > 0) {
states['未初始化'] = overview.value.NoInit
}
//
Object.entries(overview.value).forEach(([key, value]) => {
if (!knownStates.includes(key) && value > 0 && key !== 'status' && key !== 'NoInit') {
states[key] = value
}
})
return states
})
//
const unknownStatesCount = computed(() => {
return Object.values(unknownStates.value).reduce((sum, count) => sum + count, 0)
})
//
const hasUnknownStates = computed(() => {
return unknownStatesCount.value > 0
})
</script>
<style scoped>
.dashboard {
padding: 20px;
}
.stat-card {
height: 160px;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.header-icon {
font-size: 20px;
margin-right: 8px;
}
.header-title {
font-size: 16px;
color: #606266;
}
.stat-content {
text-align: center;
}
.number {
font-size: 36px;
font-weight: bold;
color: #303133;
}
.label {
font-size: 14px;
color: #909399;
margin-left: 8px;
}
.sub-info {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 12px; /* 增加间距 */
color: #909399;
font-size: 13px;
}
.sub-item {
display: flex;
align-items: center;
gap: 4px;
}
/* 工具提示样式 */
:deep(.el-tooltip__popper) {
min-width: 120px;
}
:deep(.el-tooltip__popper div) {
line-height: 1.8;
}
.refresh-info {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #909399;
font-size: 13px;
}
/* 卡片主题色 */
.total-card .header-icon { color: #409EFF; }
.running-card .header-icon { color: #E6A23C; }
.error-card .header-icon { color: #F56C6C; }
.success-card .header-icon { color: #67C23A; }
.total-card:hover { box-shadow: 0 4px 12px rgba(64,158,255,0.1); }
.running-card:hover { box-shadow: 0 4px 12px rgba(230,162,60,0.1); }
.error-card:hover { box-shadow: 0 4px 12px rgba(245,108,108,0.1); }
.success-card:hover { box-shadow: 0 4px 12px rgba(103,194,58,0.1); }
/* 添加遮罩层样式 */
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.7);
border-radius: 4px;
}
</style>

208
src/views/LoginView.vue Normal file
View file

@ -0,0 +1,208 @@
<template>
<div class="login-container">
<el-card class="login-card">
<template #header>
<div class="login-header">
<img src="../assets/logo.png" alt="logo" class="login-logo">
<h2>管理员登录</h2>
</div>
</template>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
label-width="100px"
@keyup.enter="handleLogin">
<el-form-item label="管理员账号" prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入管理员账号"
:prefix-icon="User"
clearable />
</el-form-item>
<el-form-item label="登录密码" prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入登录密码"
:prefix-icon="Lock"
show-password />
</el-form-item>
<el-button
type="primary"
class="login-button"
:loading="loading"
@click="handleLogin">
登录
</el-button>
<div class="guest-login">
<el-button
type="info"
link
@click="handleGuestLogin">
游客登录>>
</el-button>
<el-divider direction="vertical" />
<el-button
type="primary"
link
@click="handleRegister">
注册账号
</el-button>
</div>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, Lock } from '@element-plus/icons-vue'
import { UserAPI } from '../api/user'
const router = useRouter()
const loginFormRef = ref(null)
const loading = ref(false)
const loginForm = ref({
username: '',
password: ''
})
const loginRules = {
username: [
{ required: true, message: '请输入管理员账号', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入登录密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
]
}
//
const handleGuestLogin = () => {
localStorage.setItem('isAuthenticated', 'true')
localStorage.setItem('userRole', 'guest')
router.push('/dashboard')
ElMessage.success('游客登录成功')
}
//
const handleLogin = async () => {
if (!loginFormRef.value) return
await loginFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
try {
const response = await UserAPI.login({
username: loginForm.value.username,
password: loginForm.value.password
})
if (response.retcode === 0) {
//
localStorage.setItem('token', JSON.stringify(response.data))
localStorage.setItem('isAuthenticated', 'true')
//
const userInfo = await UserAPI.getUserInfo()
if (userInfo.retcode === 0) {
localStorage.setItem('userRole', 'admin')
localStorage.setItem('userInfo', JSON.stringify(userInfo.data))
ElMessage.success('登录成功')
router.push('/dashboard')
}
}
} catch (error) {
console.error('登录失败:', error)
} finally {
loading.value = false
}
}
})
}
//
const handleRegister = () => {
router.push('/register')
}
</script>
<style scoped>
.login-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #f0f2f5;
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"%3E%3Cpath fill="%230099ff" fill-opacity="0.1" d="M0,160L48,144C96,128,192,96,288,106.7C384,117,480,171,576,165.3C672,160,768,96,864,90.7C960,85,1056,139,1152,144C1248,149,1344,107,1392,85.3L1440,64L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"%3E%3C/path%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.login-card {
width: 480px;
margin: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.login-header {
text-align: center;
margin-bottom: 20px;
}
.login-logo {
width: 64px;
height: 64px;
margin-bottom: 16px;
}
.login-button {
width: 100%;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
:deep(.el-input-group__append) {
padding: 0;
width: 120px;
}
:deep(.el-input-group__append button) {
border: none;
height: 100%;
padding: 0;
width: 100%;
font-size: 13px;
}
.code-button {
white-space: nowrap;
}
.guest-login {
text-align: center;
margin-top: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.form-footer {
padding: 0 0 0 100px; /* 左内边距与label宽度相同 */
margin-top: 20px;
}
</style>

201
src/views/RegisterView.vue Normal file
View file

@ -0,0 +1,201 @@
<template>
<div class="register-container">
<el-card class="register-card">
<template #header>
<div class="register-header">
<img src="../assets/logo.png" alt="logo" class="register-logo">
<h2>用户注册</h2>
</div>
</template>
<el-form
ref="registerFormRef"
:model="registerForm"
:rules="registerRules"
label-width="100px"
@keyup.enter="handleRegister">
<el-form-item label="用户名" prop="username">
<el-input
v-model="registerForm.username"
placeholder="请输入用户名"
:prefix-icon="User"
clearable />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input
v-model="registerForm.nickname"
placeholder="请输入昵称"
:prefix-icon="UserFilled"
clearable />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="registerForm.password"
type="password"
placeholder="请输入密码"
:prefix-icon="Lock"
show-password />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="registerForm.confirmPassword"
type="password"
placeholder="请再次输入密码"
:prefix-icon="Lock"
show-password />
</el-form-item>
<el-button
type="primary"
class="register-button"
:loading="loading"
@click="handleRegister">
注册
</el-button>
<div class="login-link">
已有账号
<el-button
type="primary"
link
@click="router.push('/login')">
立即登录
</el-button>
</div>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, UserFilled, Lock } from '@element-plus/icons-vue'
import { UserAPI } from '../api/user'
const router = useRouter()
const registerFormRef = ref(null)
const loading = ref(false)
const registerForm = ref({
username: '',
nickname: '',
password: '',
confirmPassword: ''
})
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'))
} else {
if (registerForm.value.confirmPassword !== '') {
registerFormRef.value.validateField('confirmPassword')
}
callback()
}
}
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== registerForm.value.password) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
const registerRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
nickname: [
{ required: true, message: '请输入昵称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
password: [
{ validator: validatePass, trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
],
confirmPassword: [
{ validator: validatePass2, trigger: 'blur' }
]
}
const handleRegister = async () => {
if (!registerFormRef.value) return
await registerFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
try {
const response = await UserAPI.register({
username: registerForm.value.username,
nickname: registerForm.value.nickname,
password: registerForm.value.password
})
if (response.retcode === 0) {
ElMessage.success('注册成功,请登录')
router.push('/login')
}
} catch (error) {
console.error('注册失败:', error)
} finally {
loading.value = false
}
}
})
}
</script>
<style scoped>
.register-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #f0f2f5;
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"%3E%3Cpath fill="%230099ff" fill-opacity="0.1" d="M0,160L48,144C96,128,192,96,288,106.7C384,117,480,171,576,165.3C672,160,768,96,864,90.7C960,85,1056,139,1152,144C1248,149,1344,107,1392,85.3L1440,64L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"%3E%3C/path%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.register-card {
width: 480px;
margin: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.register-header {
text-align: center;
margin-bottom: 20px;
}
.register-logo {
width: 64px;
height: 64px;
margin-bottom: 16px;
}
.register-button {
width: 100%;
}
.login-link {
text-align: center;
margin-top: 12px;
color: #606266;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
</style>

View file

@ -0,0 +1,780 @@
<template>
<div class="task-management">
<el-card class="task-card">
<template #header>
<div class="card-header">
<div class="header-left">
<span class="title">任务列表</span>
<el-tag class="task-count" type="info" effect="plain">
{{ filteredTasks.length }}
</el-tag>
</div>
<div class="header-right">
<el-input
v-model="searchQuery"
placeholder="搜索任务..."
class="search-input"
clearable
@clear="handleSearch">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="handleAddTask">
<el-icon><Plus /></el-icon>新建任务
</el-button>
</div>
</div>
</template>
<el-table
v-loading="loading"
:data="filteredTasks"
style="width: 100%"
:header-cell-style="{ background: '#f5f7fa' }"
border>
<el-table-column prop="tid" label="任务UUID" width="250" />
<el-table-column prop="client_ip" label="客户端IP" width="140" />
<el-table-column prop="add_time" label="添加时间" width="180">
<template #default="scope">
{{ formatDateTime(scope.row.add_time) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="130">
<template #default="scope">
<el-tag
:type="getStatusType(scope.row.status, scope.row.queue_position)"
effect="light"
:class="['status-tag', `status-${scope.row.status}`]">
{{ getStatusLabel(scope.row.status) }}
{{ scope.row.status === 6 ?
(scope.row.queue_position === -1 ? ' 错误' : '#' + scope.row.queue_position)
: ''
}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="420">
<template #default="scope">
<el-button
link
type="primary"
@click="handleViewDetails(scope.row)">
<el-icon><InfoFilled /></el-icon>详情
</el-button>
<el-button
v-if="scope.row.status === 1"
link
type="primary"
@click="handleViewProcess(scope.row)">
<el-icon><Monitor /></el-icon>进程信息
</el-button>
<el-button
v-if="scope.row.status === 6"
link
type="success"
@click="handlePrioritize(scope.row)">
<el-icon><Top /></el-icon>插至队首
</el-button>
<el-button
v-if="scope.row.status === 6"
link
type="success"
@click="handleExecuteNow(scope.row)">
<el-icon><VideoPlay /></el-icon>立即执行
</el-button>
<el-button
v-if="[2, 7].includes(scope.row.status)"
link
type="success"
@click="handleRetry(scope.row)">
<el-icon><RefreshRight /></el-icon>重试
</el-button>
<el-button
link
:type="scope.row.status === 1 ? 'warning' : 'danger'"
@click="scope.row.status === 1 ? handleStop(scope.row) : handleDelete(scope.row)">
<el-icon>
<component :is="scope.row.status === 1 ? 'VideoPause' : 'Delete'" />
</el-icon>
{{ scope.row.status === 1 ? '停止' : '删除' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
<el-dialog
v-model="processDialogVisible"
title="进程信息"
width="700px"
destroy-on-close>
<div v-if="currentProcess" class="process-info">
<el-descriptions :column="2" border>
<el-descriptions-item label="进程ID">
{{ currentProcess.processId }}
</el-descriptions-item>
<el-descriptions-item label="进程名称">
{{ currentProcess.processName }}
</el-descriptions-item>
<el-descriptions-item label="启动时间">
{{ formatDateTime(currentProcess.startTime) }}
</el-descriptions-item>
<el-descriptions-item label="运行时长">
{{ formatDuration(currentProcess.runningTime) }}
</el-descriptions-item>
<el-descriptions-item label="线程数">
{{ currentProcess.threadCount }}
</el-descriptions-item>
<el-descriptions-item label="优先级">
{{ currentProcess.priorityClass }}
</el-descriptions-item>
<el-descriptions-item label="工作目录" :span="2">
{{ currentProcess.workingDirectory }}
</el-descriptions-item>
<el-descriptions-item label="命令行" :span="2">
<el-input
type="textarea"
:rows="2"
v-model="currentProcess.commandLine"
readonly
/>
</el-descriptions-item>
</el-descriptions>
<div class="threads-title">线程列表</div>
<el-table :data="currentProcess.threads" size="small" max-height="200">
<el-table-column prop="threadId" label="线程ID" width="100" />
<el-table-column prop="threadState" label="状态" width="120">
<template #default="scope">
<el-tag size="small" :type="getThreadStateType(scope.row.threadState)">
{{ getThreadStateLabel(scope.row.threadState) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" />
</el-table>
</div>
</el-dialog>
<el-dialog
v-model="detailsDialogVisible"
title="任务详细信息"
width="700px"
destroy-on-close>
<div v-if="currentTaskDetails" class="task-details">
<el-descriptions :column="2" border>
<el-descriptions-item label="任务ID" :span="2">
{{ currentTaskDetails.tid }}
</el-descriptions-item>
<el-descriptions-item label="文件名称">
{{ currentTaskDetails.file_name }}
</el-descriptions-item>
<el-descriptions-item label="文件大小">
{{ formatFileSize(currentTaskDetails.size) }}
</el-descriptions-item>
<el-descriptions-item label="客户端IP">
{{ currentTaskDetails.client_ip }}
</el-descriptions-item>
<el-descriptions-item label="队列位置">
{{ currentTaskDetails.queue_position || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDateTime(currentTaskDetails.add_time) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDateTime(currentTaskDetails.update_time) }}
</el-descriptions-item>
<el-descriptions-item label="文件Hash" :span="2">
{{ currentTaskDetails.hash }}
</el-descriptions-item>
<el-descriptions-item label="标签" :span="2">
<el-tag
v-if="currentTaskDetails.tag"
:type="getTagType(currentTaskDetails.tag)"
effect="light">
{{ currentTaskDetails.tag }}
</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="下载地址" :span="2">
<el-input
type="textarea"
:rows="2"
v-model="currentTaskDetails.url"
readonly
/>
</el-descriptions-item>
</el-descriptions>
<div class="details-actions">
<el-button type="primary" @click="copyUrl">
<el-icon><DocumentCopy /></el-icon>复制下载地址
</el-button>
<el-button type="primary" @click="downloadFile" :disabled="!currentTaskDetails.url">
<el-icon><Download /></el-icon>下载文件
</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { TaskAPI } from '../api/task'
import {
Plus, Delete, Monitor, Search,
InfoFilled, DocumentCopy, Download,
VideoPlay, VideoPause, RefreshRight,
Top
} from '@element-plus/icons-vue'
const loading = ref(false)
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const tasks = ref([])
const searchQuery = ref('')
//
const filteredTasks = computed(() => {
if (!searchQuery.value) return tasks.value
const query = searchQuery.value.toLowerCase()
return tasks.value.filter(task => {
return (
task.tid?.toLowerCase().includes(query) ||
task.file_name?.toLowerCase().includes(query) ||
task.client_ip?.toLowerCase().includes(query) ||
formatDateTime(task.add_time)?.toLowerCase().includes(query)
)
})
})
//
const fetchTasks = async () => {
loading.value = true
try {
console.log('Fetching tasks with params:', {
page: currentPage.value,
pageSize: pageSize.value
})
const response = await TaskAPI.getTaskList({
page: currentPage.value,
pageSize: pageSize.value
})
console.log('Raw response:', response)
if (response.data) {
tasks.value = response.data.data || []
total.value = response.data.total || 0
}
} catch (error) {
console.error('Error details:', {
message: error.message,
config: error.config,
response: error.response,
request: error.request
})
ElMessage.error('获取任务列表失败')
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
//
console.log('Searching for:', searchQuery.value)
}
//
const handlePageChange = (page) => {
currentPage.value = page
fetchTasks()
}
//
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchTasks()
}
//
onMounted(() => {
fetchTasks()
})
//
const statusMap = {
0: {
label: '未初始化',
type: 'info'
},
1: {
label: '正在进行',
type: 'primary'
},
2: {
label: '执行错误',
type: 'danger'
},
3: {
label: '已完成',
type: 'success'
},
4: {
label: '已缓存',
type: 'success'
},
5: {
label: '已清理',
type: 'info'
},
6: {
label: '队列',
type: 'warning'
},
7: {
label: '已取消',
type: 'info'
}
}
//
const getStatusType = (status, queuePosition) => {
// -1
if (status === 6 && queuePosition === -1) {
return 'danger'
}
return statusMap[status]?.type || 'info'
}
const getStatusLabel = (status) => {
return statusMap[status]?.label || `未知状态(${status})`
}
const handleAddTask = () => {
ElMessageBox.prompt('请输入下载地址', '新建任务', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '请输入文件下载地址',
inputPattern: /^https?:\/\/.+/,
inputErrorMessage: '请输入有效的下载地址',
inputType: 'textarea',
inputRows: 3,
customClass: 'add-task-dialog'
}).then(async ({ value }) => {
try {
const response = await TaskAPI.addOfflineTask(value)
if (response.retcode === 0) {
ElMessage.success('任务已添加')
fetchTasks() //
}
} catch (error) {
console.error('添加任务失败:', error)
}
}).catch(() => {
//
})
}
const handleEdit = (row) => {
//
}
const handleDelete = (row) => {
ElMessageBox.confirm(
'确定要删除该任务吗?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const response = await TaskAPI.deleteTask(row.tid)
//
ElMessage.success('删除成功')
fetchTasks()
} catch (error) {
//
console.error('删除任务失败:', error)
}
})
}
const processDialogVisible = ref(false)
const currentProcess = ref(null)
const handleViewProcess = async (task) => {
try {
const response = await TaskAPI.getProcessInfo(task.tid)
if (response.retcode === 0) {
currentProcess.value = response.data
processDialogVisible.value = true
}
} catch (error) {
console.error('获取进程信息失败:', error)
}
}
//
const formatDateTime = (dateStr) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString()
}
//
const formatDuration = (seconds) => {
if (!seconds) return '-'
const minutes = Math.floor(seconds / 60)
const remainingSeconds = (seconds % 60).toFixed(1)
if (minutes === 0) {
return `${remainingSeconds}`
}
return `${minutes}${remainingSeconds}`
}
// 线
const threadStateMap = {
0: { label: '已初始化', type: 'info' }, // Initialized
1: { label: '就绪', type: 'warning' }, // Ready
2: { label: '运行中', type: 'success' }, // Running
3: { label: '待机', type: 'info' }, // Standby
4: { label: '已终止', type: 'danger' }, // Terminated
5: { label: '等待中', type: 'info' }, // Wait
6: { label: '过渡中', type: 'warning' }, // Transition
7: { label: '未知', type: 'info' } // Unknown
}
const getThreadStateType = (state) => {
return threadStateMap[state]?.type || 'info'
}
const getThreadStateLabel = (state) => {
return threadStateMap[state]?.label || `未知(${state})`
}
const detailsDialogVisible = ref(false)
const currentTaskDetails = ref(null)
const handleViewDetails = async (task) => {
try {
const response = await TaskAPI.getTaskDetails(task.tid)
if (response.retcode === 0) {
currentTaskDetails.value = response.data
detailsDialogVisible.value = true
}
} catch (error) {
console.error('获取任务详情失败:', error)
}
}
const formatFileSize = (bytes) => {
if (!bytes) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let i = 0
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024
i++
}
return `${bytes.toFixed(2)} ${units[i]}`
}
const copyUrl = () => {
if (currentTaskDetails.value?.url) {
navigator.clipboard.writeText(currentTaskDetails.value.url)
.then(() => {
ElMessage.success('下载地址已复制到剪贴板')
})
.catch(() => {
ElMessage.error('复制失败')
})
}
}
const downloadFile = () => {
if (currentTaskDetails.value?.url) {
window.open(currentTaskDetails.value.url, '_blank')
}
}
const handleStop = (row) => {
ElMessageBox.confirm(
'确定要停止该任务吗?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const response = await TaskAPI.killTask(row.tid)
if (response.retcode === 0) {
ElMessage.success('任务已停止')
fetchTasks() //
}
} catch (error) {
console.error('停止任务失败:', error)
}
})
}
const handlePrioritize = (row) => {
ElMessageBox.confirm(
'确定要将该任务移至队首吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}
).then(async () => {
try {
const response = await TaskAPI.prioritizeTask(row.tid)
if (response.retcode === 0) {
ElMessage.success('任务已移至队首')
fetchTasks()
}
} catch (error) {
console.error('移动任务失败:', error)
}
})
}
const handleExecuteNow = (row) => {
ElMessageBox.confirm(
'确定要立即执行该任务吗?这将跳过队列直接开始执行',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const response = await TaskAPI.executeImmediately(row.tid)
if (response.retcode === 0) {
ElMessage.success('任务已开始执行')
fetchTasks()
}
} catch (error) {
console.error('执行任务失败:', error)
}
})
}
const handleRetry = (row) => {
ElMessageBox.confirm(
'确定要重试该任务吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}
).then(async () => {
try {
const response = await TaskAPI.retryTask(row.tid)
if (response.retcode === 0) {
ElMessage.success('任务已重新加入队列')
fetchTasks() //
}
} catch (error) {
console.error('重试任务失败:', error)
}
})
}
//
const getTagType = (tag) => {
switch (tag?.toUpperCase()) {
case 'CLEANED':
return 'info'
case 'CACHED':
return 'success'
default:
return 'default'
}
}
</script>
<style scoped>
.task-management {
padding: 20px;
}
.task-card {
background: #fff;
border-radius: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
}
.title {
font-size: 16px;
font-weight: 500;
margin-right: 12px;
}
.task-count {
font-size: 13px;
}
:deep(.el-button) {
display: inline-flex;
align-items: center;
gap: 4px;
}
:deep(.el-table) {
margin-top: 20px;
}
.status-tag {
width: 100px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.status-1 {
animation: pulse 1.5s infinite;
}
.status-6 {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
.process-info {
padding: 10px;
}
.threads-title {
font-size: 16px;
font-weight: 500;
margin: 20px 0 10px;
color: #606266;
}
:deep(.el-descriptions) {
margin-bottom: 20px;
}
:deep(.el-descriptions__label) {
width: 120px;
font-weight: 500;
}
:deep(.el-textarea__inner) {
font-family: monospace;
font-size: 13px;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.search-input {
width: 240px;
}
:deep(.el-input__wrapper) {
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
:deep(.el-input__wrapper:hover) {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
:deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px #409eff inset !important;
}
.task-details {
padding: 10px;
}
.details-actions {
margin-top: 20px;
display: flex;
gap: 12px;
justify-content: flex-end;
}
:deep(.el-descriptions__cell) {
word-break: break-all;
}
:deep(.el-button .el-icon) {
margin-right: 4px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
/* 添加队列错误状态的样式 */
.status-6.el-tag--danger {
animation: none; /* 移除脉冲动画 */
}
/* 添加任务对话框样式 */
:deep(.add-task-dialog) {
.el-message-box__input {
textarea {
font-family: monospace;
font-size: 13px;
}
}
}
</style>

249
src/views/UserEvents.vue Normal file
View file

@ -0,0 +1,249 @@
<template>
<div class="events-container">
<el-card class="events-card">
<template #header>
<div class="card-header">
<div class="header-left">
<span class="title">事件记录</span>
<el-tag class="event-count" type="info" effect="plain">
{{ total }}
</el-tag>
</div>
<div class="header-right">
<template v-if="isSuperAdmin">
<el-select
v-model="selectedUserId"
placeholder="选择用户"
clearable
class="user-select">
<el-option
v-for="user in userList"
:key="user.userId"
:label="`${user.nickname}(${user.username})`"
:value="user.userId"
/>
</el-select>
</template>
<el-input
v-model="searchQuery"
placeholder="搜索事件..."
class="search-input"
clearable
@clear="handleSearch">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
</div>
</template>
<el-table
v-loading="loading"
:data="events"
style="width: 100%"
:header-cell-style="{ background: '#f5f7fa' }"
border>
<el-table-column prop="eventTime" label="时间" width="180">
<template #default="scope">
{{ formatDateTime(scope.row.eventTime) }}
</template>
</el-table-column>
<el-table-column prop="eventType" label="类型" width="120">
<template #default="scope">
<el-tag :type="getEventTypeStyle(scope.row.eventType)">
{{ getEventTypeLabel(scope.row.eventType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" width="150" />
<el-table-column prop="nickname" label="昵称" width="150" />
<el-table-column prop="eventIP" label="IP地址" width="140" />
<el-table-column prop="eventDetail" label="详细信息" min-width="200" />
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { UserAPI } from '../api/user'
const loading = ref(false)
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const events = ref([])
const searchQuery = ref('')
const selectedUserId = ref('')
const userList = ref([])
//
const userInfo = computed(() => JSON.parse(localStorage.getItem('userInfo')))
const isSuperAdmin = computed(() => userInfo.value?.mask === 2)
//
const eventTypes = {
0: { label: '登录', type: 'info' },
1: { label: '登出', type: 'info' },
2: { label: '注册', type: 'success' },
3: { label: '权限变更', type: 'warning' },
4: { label: '密码修改', type: 'warning' },
5: { label: '昵称修改', type: 'info' }
}
const getEventTypeLabel = (type) => eventTypes[type]?.label || `未知(${type})`
const getEventTypeStyle = (type) => eventTypes[type]?.type || 'info'
//
const fetchEvents = async () => {
loading.value = true
try {
const params = {
page: currentPage.value,
pageSize: pageSize.value,
keyword: searchQuery.value || undefined
}
if (isSuperAdmin.value && selectedUserId.value) {
params.userId = selectedUserId.value
}
const response = await UserAPI.getUserEvents(params)
if (response.data) {
events.value = response.data.data || []
total.value = response.data.total || 0
}
} catch (error) {
console.error('获取事件列表失败:', error)
} finally {
loading.value = false
}
}
//
const fetchUserList = async () => {
if (!isSuperAdmin.value) return
try {
const response = await UserAPI.getUserList({
page: 1,
pageSize: 1000 //
})
if (response.data) {
userList.value = response.data.data || []
}
} catch (error) {
console.error('获取用户列表失败:', error)
}
}
//
const handleSearch = () => {
currentPage.value = 1
fetchEvents()
}
//
const handlePageChange = (page) => {
currentPage.value = page
fetchEvents()
}
// <EFBFBD><EFBFBD><EFBFBD>
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchEvents()
}
//
const formatDateTime = (dateStr) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString()
}
//
watch(selectedUserId, () => {
currentPage.value = 1
fetchEvents()
})
//
onMounted(() => {
fetchEvents()
if (isSuperAdmin.value) {
fetchUserList()
}
})
</script>
<style scoped>
.events-container {
padding: 20px;
}
.events-card {
background: #fff;
border-radius: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
}
.title {
font-size: 16px;
font-weight: 500;
margin-right: 12px;
}
.event-count {
font-size: 13px;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.search-input {
width: 240px;
}
.user-select {
width: 200px;
}
:deep(.el-table) {
margin-top: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>

View file

@ -0,0 +1,523 @@
<template>
<div class="user-management">
<el-card class="user-card">
<template #header>
<div class="card-header">
<div class="header-left">
<span class="title">用户列表</span>
<el-tag class="user-count" type="info" effect="plain">
{{ total }}
</el-tag>
</div>
<div class="header-right">
<el-input
v-model="searchQuery"
placeholder="搜索用户..."
class="search-input"
clearable
@clear="handleSearch">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
</div>
</template>
<el-table
v-loading="loading"
:data="filteredUsers"
style="width: 100%"
:header-cell-style="{ background: '#f5f7fa' }"
border>
<el-table-column prop="username" label="用户名" width="150" />
<el-table-column prop="nickname" label="昵称" width="150" />
<el-table-column prop="lastLoginIP" label="最后登录IP" width="140" />
<el-table-column prop="lastLoginTime" label="最后登录时间" width="180">
<template #default="scope">
{{ formatDateTime(scope.row.lastLoginTime) }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180">
<template #default="scope">
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="mask" label="权限" width="120">
<template #default="scope">
<el-tag :type="getMaskType(scope.row.mask)">
{{ getMaskLabel(scope.row.mask) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="handleUpdateMask(scope.row)"
:disabled="scope.row.mask === 2">
<el-icon><Edit /></el-icon>修改权限
</el-button>
<el-button
link
type="primary"
@click="handleUpdateNickname(scope.row)"
:disabled="scope.row.mask === 2">
<el-icon><EditPen /></el-icon>修改昵称
</el-button>
<el-button
link
type="warning"
@click="handleResetPassword(scope.row)"
:disabled="scope.row.mask === 2">
<el-icon><Key /></el-icon>重置密码
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
<!-- 修改权限对话框 -->
<el-dialog
v-model="maskDialogVisible"
title="修改用户权限"
width="400px">
<el-form :model="maskForm" label-width="80px">
<el-form-item label="权限级别">
<el-select v-model="maskForm.newMask">
<el-option :value="0" label="普通用户" />
<el-option :value="1" label="管理员" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="maskDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUpdateMask" :loading="updating">
确定
</el-button>
</span>
</template>
</el-dialog>
<!-- 添加重置密码对话框 -->
<el-dialog
v-model="resetPasswordDialogVisible"
title="重置用户密码"
width="400px">
<el-form
ref="resetPasswordFormRef"
:model="resetPasswordForm"
:rules="resetPasswordRules"
label-width="80px">
<el-form-item label="新密码" prop="newPassword">
<el-input
v-model="resetPasswordForm.newPassword"
type="password"
show-password
placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="resetPasswordForm.confirmPassword"
type="password"
show-password
placeholder="请再次输入新密码" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="resetPasswordDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitResetPassword" :loading="updating">
确定
</el-button>
</span>
</template>
</el-dialog>
<!-- 添加修改昵称对话框 -->
<el-dialog
v-model="nicknameDialogVisible"
title="修改用户昵称"
width="400px">
<el-form
ref="nicknameFormRef"
:model="nicknameForm"
:rules="nicknameRules"
label-width="80px">
<el-form-item label="新昵称" prop="newNickname">
<el-input
v-model="nicknameForm.newNickname"
placeholder="请输入新昵称" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="nicknameDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUpdateNickname" :loading="updating">
确定
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Edit, EditPen, Key } from '@element-plus/icons-vue'
import { UserAPI } from '../api/user'
const loading = ref(false)
const updating = ref(false)
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const users = ref([])
const searchQuery = ref('')
//
const maskDialogVisible = ref(false)
const currentUser = ref(null)
const maskForm = ref({
newMask: 0
})
//
const resetPasswordDialogVisible = ref(false)
const resetPasswordFormRef = ref(null)
const resetPasswordForm = ref({
newPassword: '',
confirmPassword: ''
})
//
const nicknameDialogVisible = ref(false)
const nicknameFormRef = ref(null)
const nicknameForm = ref({
newNickname: ''
})
const nicknameRules = {
newNickname: [
{ required: true, message: '请输入新昵称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
]
}
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入新密码'))
} else {
if (resetPasswordForm.value.confirmPassword !== '') {
resetPasswordFormRef.value.validateField('confirmPassword')
}
callback()
}
}
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入新密码'))
} else if (value !== resetPasswordForm.value.newPassword) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
const resetPasswordRules = {
newPassword: [
{ validator: validatePass, trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
],
confirmPassword: [
{ validator: validatePass2, trigger: 'blur' }
]
}
//
const filteredUsers = computed(() => {
if (!searchQuery.value) return users.value
const query = searchQuery.value.toLowerCase()
return users.value.filter(user => {
return (
user.username?.toLowerCase().includes(query) ||
user.nickname?.toLowerCase().includes(query) ||
user.lastLoginIP?.includes(query)
)
})
})
//
const fetchUsers = async () => {
loading.value = true
try {
const response = await UserAPI.getUserList({
page: currentPage.value,
pageSize: pageSize.value
})
if (response.data) {
users.value = response.data.data || []
total.value = response.data.total || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
console.log('Searching for:', searchQuery.value)
}
//
const handlePageChange = (page) => {
currentPage.value = page
fetchUsers()
}
//
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchUsers()
}
//
const formatDateTime = (dateStr) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString()
}
//
const getMaskType = (mask) => {
switch (mask) {
case 2:
return 'danger'
case 1:
return 'warning'
default:
return 'info'
}
}
//
const getMaskLabel = (mask) => {
switch (mask) {
case 2:
return '超级管理员'
case 1:
return '管理员'
default:
return '普通用户'
}
}
//
const handleUpdateMask = (user) => {
currentUser.value = user
maskForm.value.newMask = user.mask
maskDialogVisible.value = true
}
//
const submitUpdateMask = async () => {
if (!currentUser.value) return
updating.value = true
try {
const response = await UserAPI.updateUserMask(
currentUser.value.userId,
maskForm.value.newMask
)
if (response.retcode === 0) {
ElMessage.success('权限修改成功')
maskDialogVisible.value = false
fetchUsers()
}
} catch (error) {
console.error('修改权限失败:', error)
} finally {
updating.value = false
}
}
//
const handleResetPassword = (user) => {
currentUser.value = user
resetPasswordForm.value = {
newPassword: '',
confirmPassword: ''
}
resetPasswordDialogVisible.value = true
}
//
const submitResetPassword = async () => {
if (!resetPasswordFormRef.value || !currentUser.value) return
await resetPasswordFormRef.value.validate(async (valid) => {
if (valid) {
ElMessageBox.confirm(
`确定要重置用户 ${currentUser.value.nickname}(${currentUser.value.username}) 的密码吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
updating.value = true
try {
const response = await UserAPI.resetPassword(
currentUser.value.userId,
resetPasswordForm.value.newPassword
)
if (response.retcode === 0) {
ElMessage.success('密码重置成功')
resetPasswordDialogVisible.value = false
}
} catch (error) {
console.error('重置密码失败:', error)
} finally {
updating.value = false
}
}).catch(() => {
//
})
}
})
}
//
const handleUpdateNickname = (user) => {
currentUser.value = user
nicknameForm.value.newNickname = user.nickname
nicknameDialogVisible.value = true
}
//
const submitUpdateNickname = async () => {
if (!nicknameFormRef.value || !currentUser.value) return
await nicknameFormRef.value.validate(async (valid) => {
if (valid) {
ElMessageBox.confirm(
`确定要修改用户 ${currentUser.value.username} 的昵称吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}
).then(async () => {
updating.value = true
try {
const response = await UserAPI.updateUserNickname(
currentUser.value.userId,
nicknameForm.value.newNickname
)
if (response.retcode === 0) {
ElMessage.success('昵称修改成功')
nicknameDialogVisible.value = false
fetchUsers() //
}
} catch (error) {
console.error('修改昵称失败:', error)
} finally {
updating.value = false
}
}).catch(() => {
//
})
}
})
}
//
onMounted(() => {
fetchUsers()
})
</script>
<style scoped>
.user-management {
padding: 20px;
}
.user-card {
background: #fff;
border-radius: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
}
.title {
font-size: 16px;
font-weight: 500;
margin-right: 12px;
}
.user-count {
font-size: 13px;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.search-input {
width: 240px;
}
:deep(.el-button) {
display: inline-flex;
align-items: center;
gap: 4px;
}
:deep(.el-table) {
margin-top: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
</style>

335
src/views/UserProfile.vue Normal file
View file

@ -0,0 +1,335 @@
<template>
<div class="profile-container">
<el-card class="profile-card">
<template #header>
<div class="card-header">
<span class="title">个人信息</span>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="用户ID">
{{ userInfo.userId }}
</el-descriptions-item>
<el-descriptions-item label="用户名">
{{ userInfo.username }}
</el-descriptions-item>
<el-descriptions-item label="昵称">
{{ userInfo.nickname }}
</el-descriptions-item>
<el-descriptions-item label="权限级别">
<el-tag :type="getMaskType(userInfo.mask)">
{{ getMaskLabel(userInfo.mask) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="账号状态">
<el-tag :type="userInfo.state === 0 ? 'success' : 'danger'">
{{ userInfo.state === 0 ? '正常' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="注册时间">
{{ formatDateTime(userInfo.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="最后登录时间">
{{ formatDateTime(userInfo.lastLoginTime) }}
</el-descriptions-item>
<el-descriptions-item label="最后登录IP">
{{ userInfo.lastLoginIP }}
</el-descriptions-item>
</el-descriptions>
<div class="profile-actions">
<el-button type="primary" @click="showNicknameDialog">
<el-icon><Edit /></el-icon>修改昵称
</el-button>
<el-button type="warning" @click="showPasswordDialog">
<el-icon><Lock /></el-icon>修改密码
</el-button>
</div>
</el-card>
<!-- 修改昵称对话框 -->
<el-dialog
v-model="nicknameDialogVisible"
title="修改昵称"
width="400px">
<el-form
ref="nicknameFormRef"
:model="nicknameForm"
:rules="nicknameRules"
label-width="80px">
<el-form-item label="新昵称" prop="newNickname">
<el-input
v-model="nicknameForm.newNickname"
placeholder="请输入新昵称" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="nicknameDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleUpdateNickname" :loading="updating">
确定
</el-button>
</span>
</template>
</el-dialog>
<!-- 修改密码对话框 -->
<el-dialog
v-model="passwordDialogVisible"
title="修改密码"
width="400px">
<el-form
ref="passwordFormRef"
:model="passwordForm"
:rules="passwordRules"
label-width="80px">
<el-form-item label="原密码" prop="oldPassword">
<el-input
v-model="passwordForm.oldPassword"
type="password"
show-password
placeholder="请输入原密码" />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input
v-model="passwordForm.newPassword"
type="password"
show-password
placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="passwordForm.confirmPassword"
type="password"
show-password
placeholder="请再次输入新密码" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="passwordDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleUpdatePassword" :loading="updating">
确定
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Edit, Lock } from '@element-plus/icons-vue'
import { UserAPI } from '../api/user'
//
const updateTrigger = ref(0)
// userInfo updateTrigger
const userInfo = computed(() => {
// updateTrigger
updateTrigger.value
return JSON.parse(localStorage.getItem('userInfo'))
})
//
const formatDateTime = (dateStr) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString()
}
//
const getMaskType = (mask) => {
switch (mask) {
case 2:
return 'danger'
case 1:
return 'warning'
default:
return 'info'
}
}
//
const getMaskLabel = (mask) => {
switch (mask) {
case 2:
return '超级管理员'
case 1:
return '管理员'
default:
return '普通用户'
}
}
const updating = ref(false)
const nicknameDialogVisible = ref(false)
const passwordDialogVisible = ref(false)
const nicknameFormRef = ref(null)
const passwordFormRef = ref(null)
const nicknameForm = ref({
newNickname: ''
})
const passwordForm = ref({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const nicknameRules = {
newNickname: [
{ required: true, message: '请输入新昵称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
]
}
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入新密码'))
} else {
if (passwordForm.value.confirmPassword !== '') {
passwordFormRef.value.validateField('confirmPassword')
}
callback()
}
}
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入新密码'))
} else if (value !== passwordForm.value.newPassword) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
const passwordRules = {
oldPassword: [
{ required: true, message: '请输入原密码', trigger: 'blur' }
],
newPassword: [
{ validator: validatePass, trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
],
confirmPassword: [
{ validator: validatePass2, trigger: 'blur' }
]
}
const showNicknameDialog = () => {
nicknameForm.value.newNickname = userInfo.value.nickname
nicknameDialogVisible.value = true
}
const showPasswordDialog = () => {
passwordForm.value = {
oldPassword: '',
newPassword: '',
confirmPassword: ''
}
passwordDialogVisible.value = true
}
const handleUpdateNickname = async () => {
if (!nicknameFormRef.value) return
await nicknameFormRef.value.validate(async (valid) => {
if (valid) {
updating.value = true
try {
const response = await UserAPI.updateNickname(nicknameForm.value.newNickname)
if (response.retcode === 0) {
//
const userInfoResponse = await UserAPI.getUserInfo()
if (userInfoResponse.retcode === 0) {
//
localStorage.setItem('userInfo', JSON.stringify(userInfoResponse.data))
//
updateTrigger.value++
ElMessage.success('昵称修改成功')
nicknameDialogVisible.value = false
}
}
} catch (error) {
console.error('修改昵称失败:', error)
} finally {
updating.value = false
}
}
})
}
const handleUpdatePassword = async () => {
if (!passwordFormRef.value) return
await passwordFormRef.value.validate(async (valid) => {
if (valid) {
updating.value = true
try {
const response = await UserAPI.updatePassword(
passwordForm.value.oldPassword,
passwordForm.value.newPassword
)
if (response.retcode === 0) {
ElMessage.success('密码修改成功,请重新登录')
//
localStorage.clear()
router.push('/login')
}
} catch (error) {
console.error('修改密码失败:', error)
} finally {
updating.value = false
}
}
})
}
</script>
<style scoped>
.profile-container {
padding: 20px;
}
.profile-card {
max-width: 800px;
margin: 0 auto;
background: #fff;
border-radius: 4px;
}
.card-header {
display: flex;
align-items: center;
}
.title {
font-size: 16px;
font-weight: 500;
}
:deep(.el-descriptions__label) {
width: 120px;
font-weight: 500;
}
:deep(.el-descriptions__cell) {
padding: 16px 24px;
}
.profile-actions {
margin-top: 24px;
display: flex;
gap: 16px;
justify-content: center;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
</style>

192
src/views/VerifyAdmin.vue Normal file
View file

@ -0,0 +1,192 @@
<template>
<div class="verify-container">
<el-card class="verify-card">
<template #header>
<div class="verify-header">
<h2>管理员身份验证</h2>
<div class="verify-tip">请输入管理员邮箱验证码完成二次验证</div>
</div>
</template>
<el-form
ref="verifyFormRef"
:model="verifyForm"
:rules="verifyRules"
label-width="100px">
<el-form-item label="管理员邮箱" prop="email">
<el-input
v-model="verifyForm.email"
placeholder="请输入管理员邮箱"
:prefix-icon="Message"
clearable>
<template #append>
<el-button
:disabled="!canSendCode"
@click="handleSendCode"
:loading="sendingCode"
class="code-button">
{{ codeButtonText }}
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="codeSent" label="验证码" prop="verifyCode">
<el-input
v-model="verifyForm.verifyCode"
placeholder="请输入6位验证码"
:prefix-icon="Key"
maxlength="6" />
</el-form-item>
<el-button
type="primary"
class="verify-button"
:loading="loading"
:disabled="!canVerify"
@click="handleVerify">
验证并登录
</el-button>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Message, Key } from '@element-plus/icons-vue'
const router = useRouter()
const verifyFormRef = ref(null)
const loading = ref(false)
const sendingCode = ref(false)
const codeSent = ref(false)
const countdown = ref(0)
const correctEmail = 'admin@example.com'
const verifyForm = ref({
email: '',
verifyCode: ''
})
const verifyRules = {
email: [
{ required: true, message: '请输入管理员邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
verifyCode: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ len: 6, message: '验证码长度为6位', trigger: 'blur' }
]
}
const canSendCode = computed(() => {
return verifyForm.value.email === correctEmail && countdown.value === 0
})
const canVerify = computed(() => {
return codeSent.value &&
verifyForm.value.email === correctEmail &&
verifyForm.value.verifyCode.length === 6
})
const codeButtonText = computed(() => {
return countdown.value > 0 ? `${countdown.value}秒后重试` : '发送验证码'
})
const handleSendCode = async () => {
if (!canSendCode.value) return
sendingCode.value = true
try {
await new Promise(resolve => setTimeout(resolve, 1000))
ElMessage.success('验证码已发送到邮箱')
codeSent.value = true
countdown.value = 60
const timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
}
}, 1000)
} catch (error) {
ElMessage.error('发送验证码失败')
} finally {
sendingCode.value = false
}
}
const handleVerify = async () => {
if (!verifyFormRef.value) return
await verifyFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 1000))
localStorage.setItem('isAuthenticated', 'true')
localStorage.setItem('userRole', 'admin')
ElMessage.success('验证成功')
router.push('/dashboard')
} catch (error) {
ElMessage.error('验证失败')
} finally {
loading.value = false
}
}
})
}
</script>
<style scoped>
.verify-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #f0f2f5;
}
.verify-card {
width: 480px;
margin: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.verify-header {
text-align: center;
}
.verify-tip {
color: #909399;
font-size: 14px;
margin-top: 8px;
}
.verify-button {
width: 100%;
margin-top: 20px;
}
:deep(.el-input-group__append) {
padding: 0;
width: 120px;
}
:deep(.el-input-group__append button) {
border: none;
height: 100%;
padding: 0;
width: 100%;
font-size: 13px;
}
.code-button {
white-space: nowrap;
}
</style>

16
vite.config.js Normal file
View file

@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
open: true
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})