first commit
This commit is contained in:
parent
30da3bd709
commit
55725f3f0f
24 changed files with 4863 additions and 0 deletions
1
.env.development
Normal file
1
.env.development
Normal file
|
@ -0,0 +1 @@
|
||||||
|
VITE_API_BASE_URL=http://localhost:5098
|
58
.gitignore
vendored
Normal file
58
.gitignore
vendored
Normal 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
13
index.html
Normal 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
1082
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
package.json
Normal file
22
package.json
Normal 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
14
src/App.vue
Normal 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
75
src/api/request.js
Normal 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
95
src/api/task.js
Normal 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
107
src/api/user.js
Normal 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
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
8
src/config/api.config.js
Normal 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
196
src/layout/Layout.vue
Normal 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
17
src/main.js
Normal 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
119
src/router/index.js
Normal 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
247
src/views/ConfigView.vue
Normal 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
305
src/views/Dashboard.vue
Normal 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
208
src/views/LoginView.vue
Normal 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
201
src/views/RegisterView.vue
Normal 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>
|
780
src/views/TaskManagement.vue
Normal file
780
src/views/TaskManagement.vue
Normal 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
249
src/views/UserEvents.vue
Normal 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>
|
523
src/views/UserManagement.vue
Normal file
523
src/views/UserManagement.vue
Normal 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
335
src/views/UserProfile.vue
Normal 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
192
src/views/VerifyAdmin.vue
Normal 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
16
vite.config.js
Normal 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in a new issue