[Debug] 默认折叠过长的日志

[任务管理] 支持任务搜索和下载历史查看
[用户管理] 支持后台添加用户
修复一些其他问题
This commit is contained in:
root 2024-12-07 18:15:40 +08:00
parent 25191da5fa
commit 4f3622a553
6 changed files with 505 additions and 206 deletions

View file

@ -127,4 +127,14 @@ onMounted(async () => {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
/* 全局气泡提示样式 */
.el-popper {
z-index: 9999 !important;
}
/* 确保日志内容的气泡提示显示在最上层 */
.el-popper.is-pure.el-tooltip__popper {
z-index: 99999 !important;
}
</style> </style>

View file

@ -112,5 +112,24 @@ export const TaskAPI = {
*/ */
getConnectionPoolInfo() { getConnectionPoolInfo() {
return request.get('/Management/GetConnectionPoolInfo') return request.get('/Management/GetConnectionPoolInfo')
},
/**
* 获取任务下载历史
* @param {string} taskId 任务ID
*/
getDownloadHistory(taskId) {
return request.get(`/Management/GetDownloadHistory/${taskId}`)
},
/**
* 搜索任务
* @param {Object} params 搜索参数
* @param {string} params.keyword 搜索关键词
* @param {number} params.page 页码
* @param {number} params.pageSize 每页数量
*/
searchTasks(params) {
return request.get('/Management/SearchTasks', { params })
} }
} }

View file

@ -103,5 +103,18 @@ export const UserAPI = {
return request.post(`/user/updateNickname/${userId}`, { return request.post(`/user/updateNickname/${userId}`, {
newNickname newNickname
}) })
},
/**
* 添加用户
* @param {Object} data 用户信息
* @param {string} data.username 用户名
* @param {string} data.password 密码
* @param {string} data.nickname 昵称
* @param {string} data.email 邮箱
* @param {number} data.mask 权限掩码
*/
addUser(data) {
return request.post('/Management/AddUser', data)
} }
} }

View file

@ -179,12 +179,25 @@
<el-table-column <el-table-column
prop="message" prop="message"
label="日志内容" label="日志内容"
min-width="300" min-width="300">
show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<div class="log-message" :class="{ 'error-message': row.level === 'Error' }"> <div class="log-message" :class="{ 'error-message': row.level === 'Error' }">
<div v-if="isLongMessage(row.message)" class="collapsible-message">
<div :class="{ 'collapsed': !expandedMessages[row.logId] }">
{{ row.message }} {{ row.message }}
</div> </div>
<el-button
link
type="primary"
size="small"
@click="toggleMessage(row.logId)">
{{ expandedMessages[row.logId] ? '收起' : '展开' }}
</el-button>
</div>
<div v-else>
{{ row.message }}
</div>
</div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -459,6 +472,19 @@ const showExceptionDialog = (exception) => {
currentException.value = exception currentException.value = exception
exceptionDialogVisible.value = true exceptionDialogVisible.value = true
} }
//
const expandedMessages = ref({})
//
const isLongMessage = (message) => {
return message && message.length > 100
}
//
const toggleMessage = (logId) => {
expandedMessages.value[logId] = !expandedMessages.value[logId]
}
</script> </script>
<style scoped> <style scoped>
@ -566,6 +592,18 @@ const showExceptionDialog = (exception) => {
overflow-y: auto; overflow-y: auto;
} }
.collapsible-message {
position: relative;
}
.collapsible-message .collapsed {
max-height: 3em; /* 显示约3行文本 */
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
/* 移动端样式 */ /* 移动端样式 */
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.debug-view { .debug-view {
@ -630,10 +668,13 @@ const showExceptionDialog = (exception) => {
z-index: 3000 !important; z-index: 3000 !important;
} }
/* 确保气泡提示在最上层 */
:deep(.el-popper) {
z-index: 9999 !important; /* 比下拉菜单更高 */
}
:deep(.el-select) { :deep(.el-select) {
z-index: 11; z-index: 11;
min-width: 120px; /* 设置最小宽度 */
width: auto !important; /* 允许自动扩展 */
} }
:deep(.el-date-editor) { :deep(.el-date-editor) {

View file

@ -10,23 +10,23 @@
</el-tag> </el-tag>
</div> </div>
<div class="header-right"> <div class="header-right">
<el-input <el-input v-model="searchQuery" placeholder="搜索任务..." class="search-input" clearable @clear="handleSearch">
v-model="searchQuery"
placeholder="搜索任务..."
class="search-input"
clearable
@clear="handleSearch">
<template #prefix> <template #prefix>
<el-icon><Search /></el-icon> <el-icon>
<Search />
</el-icon>
</template> </template>
</el-input> </el-input>
<el-button type="primary" @click="handleAddTask"> <el-button type="primary" @click="handleAddTask">
<el-icon><Plus /></el-icon>新建任务 <el-icon>
<Plus />
</el-icon>新建任务
</el-button> </el-button>
</div> </div>
</div> </div>
</template> </template>
<!-- 修改表格数据源 -->
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="filteredTasks" :data="filteredTasks"
@ -36,10 +36,7 @@
<el-table-column prop="tid" label="任务UUID" width="350" /> <el-table-column prop="tid" label="任务UUID" width="350" />
<el-table-column prop="client_ip" label="客户端IP" width="140"> <el-table-column prop="client_ip" label="客户端IP" width="140">
<template #default="{ row }"> <template #default="{ row }">
<el-link <el-link type="primary" :href="`https://ip138.com/iplookup.php?ip=${row.client_ip}`" target="_blank"
type="primary"
:href="`https://ip138.com/iplookup.php?ip=${row.client_ip}`"
target="_blank"
:underline="false"> :underline="false">
{{ row.client_ip }} {{ row.client_ip }}
</el-link> </el-link>
@ -52,9 +49,7 @@
</el-table-column> </el-table-column>
<el-table-column prop="status" label="状态" width="130"> <el-table-column prop="status" label="状态" width="130">
<template #default="scope"> <template #default="scope">
<el-tag <el-tag :type="getStatusType(scope.row.status, scope.row.queue_position)" effect="light"
:type="getStatusType(scope.row.status, scope.row.queue_position)"
effect="light"
:class="['status-tag', `status-${scope.row.status}`]"> :class="['status-tag', `status-${scope.row.status}`]">
{{ getStatusLabel(scope.row.status) }} {{ getStatusLabel(scope.row.status) }}
{{ scope.row.status === 6 ? {{ scope.row.status === 6 ?
@ -64,54 +59,39 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column label="操作" :width="isMobile ? '70' : '420'" fixed="right" class-name="operation-column">
label="操作"
:width="isMobile ? '70' : '420'"
fixed="right"
class-name="operation-column">
<template #default="scope"> <template #default="scope">
<el-button <el-button link type="primary" @click="handleViewDetails(scope.row)">
link <el-icon>
type="primary" <InfoFilled />
@click="handleViewDetails(scope.row)"> </el-icon>详情
<el-icon><InfoFilled /></el-icon>详情
</el-button> </el-button>
<el-button <el-button v-if="scope.row.status === 1" link type="primary" @click="handleViewProcess(scope.row)">
v-if="scope.row.status === 1" <el-icon>
link <Monitor />
type="primary" </el-icon>进程信息
@click="handleViewProcess(scope.row)">
<el-icon><Monitor /></el-icon>进程信息
</el-button> </el-button>
<el-button <el-button v-if="scope.row.status === 6" link type="success" @click="handlePrioritize(scope.row)">
v-if="scope.row.status === 6" <el-icon>
link <Top />
type="success" </el-icon>插至队首
@click="handlePrioritize(scope.row)">
<el-icon><Top /></el-icon>插至队首
</el-button> </el-button>
<el-button <el-button v-if="scope.row.status === 6" link type="success" @click="handleExecuteNow(scope.row)">
v-if="scope.row.status === 6" <el-icon>
link <VideoPlay />
type="success" </el-icon>立即执行
@click="handleExecuteNow(scope.row)">
<el-icon><VideoPlay /></el-icon>立即执行
</el-button> </el-button>
<el-button <el-button v-if="[2, 7].includes(scope.row.status)" link type="success" @click="handleRetry(scope.row)">
v-if="[2, 7].includes(scope.row.status)" <el-icon>
link <RefreshRight />
type="success" </el-icon>重试
@click="handleRetry(scope.row)">
<el-icon><RefreshRight /></el-icon>重试
</el-button> </el-button>
<el-button <el-button link :type="scope.row.status === 1 ? 'warning' : 'danger'"
link
:type="scope.row.status === 1 ? 'warning' : 'danger'"
@click="scope.row.status === 1 ? handleStop(scope.row) : handleDelete(scope.row)"> @click="scope.row.status === 1 ? handleStop(scope.row) : handleDelete(scope.row)">
<el-icon> <el-icon>
<component :is="scope.row.status === 1 ? 'VideoPause' : 'Delete'" /> <component :is="scope.row.status === 1 ? 'VideoPause' : 'Delete'" />
@ -123,23 +103,13 @@
</el-table> </el-table>
<div class="pagination-container"> <div class="pagination-container">
<el-pagination <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]"
v-model:current-page="currentPage" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
v-model:page-size="pageSize" @current-change="handlePageChange" />
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div> </div>
</el-card> </el-card>
<el-dialog <el-dialog v-model="processDialogVisible" title="进程信息" width="700px" destroy-on-close>
v-model="processDialogVisible"
title="进程信息"
width="700px"
destroy-on-close>
<div v-if="currentProcess" class="process-info"> <div v-if="currentProcess" class="process-info">
<el-descriptions :column="2" border> <el-descriptions :column="2" border>
<el-descriptions-item label="进程ID"> <el-descriptions-item label="进程ID">
@ -164,12 +134,7 @@
{{ currentProcess.workingDirectory }} {{ currentProcess.workingDirectory }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="命令行" :span="2"> <el-descriptions-item label="命令行" :span="2">
<el-input <el-input type="textarea" :rows="2" v-model="currentProcess.commandLine" readonly />
type="textarea"
:rows="2"
v-model="currentProcess.commandLine"
readonly
/>
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
@ -188,13 +153,11 @@
</div> </div>
</el-dialog> </el-dialog>
<el-dialog <el-dialog v-model="detailsDialogVisible" title="任务详细信息" width="700px" destroy-on-close>
v-model="detailsDialogVisible"
title="任务详细信息"
width="700px"
destroy-on-close>
<div v-if="currentTaskDetails" class="task-details"> <div v-if="currentTaskDetails" class="task-details">
<el-descriptions :column="2" border> <el-tabs class="details-tabs" v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="基本信息" name="info">
<el-descriptions :column="isMobile ? 1 : 2" border>
<el-descriptions-item label="任务ID" :span="2"> <el-descriptions-item label="任务ID" :span="2">
{{ currentTaskDetails.tid }} {{ currentTaskDetails.tid }}
</el-descriptions-item> </el-descriptions-item>
@ -220,36 +183,54 @@
{{ currentTaskDetails.hash }} {{ currentTaskDetails.hash }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="标签" :span="2"> <el-descriptions-item label="标签" :span="2">
<el-tag <el-tag v-if="currentTaskDetails.tag" :type="getTagType(currentTaskDetails.tag)" effect="light">
v-if="currentTaskDetails.tag"
:type="getTagType(currentTaskDetails.tag)"
effect="light">
{{ currentTaskDetails.tag }} {{ currentTaskDetails.tag }}
</el-tag> </el-tag>
<span v-else>-</span> <span v-else>-</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="下载地址" :span="2"> <el-descriptions-item label="下载地址" :span="2">
<el-input <el-input type="textarea" :rows="2" v-model="currentTaskDetails.url" readonly />
type="textarea"
:rows="2"
v-model="currentTaskDetails.url"
readonly
/>
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-tab-pane>
<el-tab-pane label="下载历史" name="history" lazy>
<div v-loading="loadingHistory">
<el-empty v-if="!downloadHistory.length" description="暂无下载记录" />
<el-table v-else :data="downloadHistory" style="width: 100%" size="small" border>
<el-table-column prop="time" label="下载时间" :width="isMobile ? '130' : '180'">
<template #default="{ row }">
{{ formatDateTime(row.time) }}
</template>
</el-table-column>
<el-table-column prop="clientIP" label="客户端IP" min-width="140">
<template #default="{ row }">
<el-link type="primary" :href="`https://ip138.com/iplookup.php?ip=${row.clientIP}`" target="_blank"
:underline="false">
{{ row.clientIP }}
</el-link>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
<div class="details-actions"> <div class="details-actions">
<el-button type="primary" @click="copyUrl"> <el-button type="primary" @click="copyUrl">
<el-icon><DocumentCopy /></el-icon>复制下载地址 <el-icon>
<DocumentCopy />
</el-icon>复制下载地址
</el-button> </el-button>
<el-button type="primary" @click="downloadFile" :disabled="!currentTaskDetails.url"> <el-button type="primary" @click="downloadFile" :disabled="!currentTaskDetails.url">
<el-icon><Download /></el-icon>下载文件 <el-icon>
<Download />
</el-icon>下载文件
</el-button> </el-button>
<el-button <el-button type="success" @click="downloadProxiedFile" :disabled="!canDownloadProxied">
type="success" <el-icon>
@click="downloadProxiedFile" <Connection />
:disabled="!canDownloadProxied"> </el-icon>下载文件(已代理)
<el-icon><Connection /></el-icon>下载文件(已代理)
</el-button> </el-button>
</div> </div>
</div> </div>
@ -258,7 +239,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted, watch } from 'vue' // watch
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { TaskAPI } from '../api/task' import { TaskAPI } from '../api/task'
import { import {
@ -276,57 +257,61 @@ const pageSize = ref(10)
const tasks = ref([]) const tasks = ref([])
const searchQuery = ref('') const searchQuery = ref('')
// //
const filteredTasks = computed(() => { const filteredTasks = computed(() => tasks.value)
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 () => { const fetchTasks = async () => {
loading.value = true loading.value = true
try { try {
console.log('Fetching tasks with params:', { let response
if (searchQuery.value.trim()) {
// 使
response = await TaskAPI.searchTasks({
keyword: searchQuery.value.trim(),
page: currentPage.value, page: currentPage.value,
pageSize: pageSize.value pageSize: pageSize.value
}) })
} else {
const response = await TaskAPI.getTaskList({ // 使
response = await TaskAPI.getTaskList({
page: currentPage.value, page: currentPage.value,
pageSize: pageSize.value pageSize: pageSize.value
}) })
console.log('Raw response:', response) }
if (response.data) { if (response.retcode === 0 && response.data) {
tasks.value = response.data.data || [] tasks.value = response.data.data || []
total.value = response.data.total || 0 total.value = response.data.total || 0
} }
} catch (error) { } catch (error) {
console.error('Error details:', { console.error('获取任务列表失败:', error)
message: error.message,
config: error.config,
response: error.response,
request: error.request
})
ElMessage.error('获取任务列表失败') ElMessage.error('获取任务列表失败')
} finally { } finally {
loading.value = false loading.value = false
} }
} }
// //
const handleSearch = () => { const handleSearch = debounce(() => {
// currentPage.value = 1 //
console.log('Searching for:', searchQuery.value) fetchTasks()
}, 500)
//
watch(searchQuery, () => {
handleSearch()
})
//
function debounce(fn, delay) {
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
} }
// //
@ -502,11 +487,40 @@ const getThreadStateLabel = (state) => {
const detailsDialogVisible = ref(false) const detailsDialogVisible = ref(false)
const currentTaskDetails = ref(null) const currentTaskDetails = ref(null)
const loadingHistory = ref(false)
const downloadHistory = ref([])
//
const fetchDownloadHistory = async (taskId) => {
loadingHistory.value = true
try {
const response = await TaskAPI.getDownloadHistory(taskId)
if (response.retcode === 0) {
downloadHistory.value = response.data || []
}
} catch (error) {
console.error('获取下载历史失败:', error)
} finally {
loadingHistory.value = false
}
}
const activeTab = ref('info')
//
const handleTabClick = async (tab) => {
if (tab.props.name === 'history' && currentTaskDetails.value) {
await fetchDownloadHistory(currentTaskDetails.value.tid)
}
}
//
const handleViewDetails = async (task) => { const handleViewDetails = async (task) => {
try { try {
const response = await TaskAPI.getTaskDetails(task.tid) const response = await TaskAPI.getTaskDetails(task.tid)
if (response.retcode === 0) { if (response.retcode === 0) {
currentTaskDetails.value = response.data currentTaskDetails.value = response.data
activeTab.value = 'info' //
detailsDialogVisible.value = true detailsDialogVisible.value = true
} }
} catch (error) { } catch (error) {
@ -514,6 +528,8 @@ const handleViewDetails = async (task) => {
} }
} }
// watch activeTab handleTabClick
const formatFileSize = (bytes) => { const formatFileSize = (bytes) => {
if (!bytes) return '0 B' if (!bytes) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB'] const units = ['B', 'KB', 'MB', 'GB', 'TB']
@ -679,6 +695,7 @@ const downloadProxiedFile = () => {
const downloadUrl = `${API_BASE_URL}/Download/${currentTaskDetails.value.tid}` const downloadUrl = `${API_BASE_URL}/Download/${currentTaskDetails.value.tid}`
window.open(downloadUrl, '_blank') window.open(downloadUrl, '_blank')
} }
</script> </script>
<style scoped> <style scoped>
@ -741,9 +758,11 @@ const downloadProxiedFile = () => {
0% { 0% {
opacity: 1; opacity: 1;
} }
50% { 50% {
opacity: 0.6; opacity: 0.6;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
@ -805,7 +824,8 @@ const downloadProxiedFile = () => {
display: flex; display: flex;
gap: 12px; gap: 12px;
justify-content: flex-end; justify-content: flex-end;
flex-wrap: wrap; /* 允许按钮换行 */ flex-wrap: wrap;
/* 允许按钮换行 */
} }
:deep(.el-descriptions__cell) { :deep(.el-descriptions__cell) {
@ -824,7 +844,8 @@ const downloadProxiedFile = () => {
/* 添加队列错误状态的样式 */ /* 添加队列错误状态的样式 */
.status-6.el-tag--danger { .status-6.el-tag--danger {
animation: none; /* 移除脉冲动画 */ animation: none;
/* 移除脉冲动画 */
} }
/* 添加任务对话框样式 */ /* 添加任务对话框样式 */
@ -896,7 +917,8 @@ const downloadProxiedFile = () => {
.el-pagination__total, .el-pagination__total,
.el-pagination__sizes, .el-pagination__sizes,
.el-pagination__jump { .el-pagination__jump {
display: none; /* 在移动端隐藏这些元素 */ display: none;
/* 在移动端隐藏这些元素 */
} }
} }
} }
@ -943,6 +965,7 @@ const downloadProxiedFile = () => {
/* 优化表格在不同屏幕尺寸下的显示 */ /* 优化表格在不同屏幕尺寸下的显示 */
:deep(.el-table) { :deep(.el-table) {
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
/* UUID列宽度调整 */ /* UUID列宽度调整 */
.el-table-column--tid { .el-table-column--tid {
min-width: 120px; min-width: 120px;
@ -992,10 +1015,12 @@ const downloadProxiedFile = () => {
/* 优化固定列的样式 */ /* 优化固定列的样式 */
:deep(.operation-column) { :deep(.operation-column) {
background-color: #fff; /* 确保背景色与表格一致 */ background-color: #fff;
/* 确保背景色与表格一致 */
.cell { .cell {
white-space: nowrap; /* 防止按钮换行 */ white-space: nowrap;
/* 防止按钮换行 */
} }
} }
@ -1006,13 +1031,16 @@ const downloadProxiedFile = () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
padding: 4px !important; /* 减小内边距 */ padding: 4px !important;
/* 减小内边距 */
.el-button { .el-button {
width: 100%; width: 100%;
justify-content: center; justify-content: center;
padding: 4px 0; /* 调整按钮内边距 */ padding: 4px 0;
margin: 0; /* 移除按钮外边距 */ /* 调整按钮内边距 */
margin: 0;
/* 移除按钮外边距 */
} }
} }
} }
@ -1034,4 +1062,30 @@ const downloadProxiedFile = () => {
} }
} }
} }
.details-tabs {
margin-top: 20px;
}
/* 移动端样式优化 */
@media screen and (max-width: 768px) {
.details-tabs {
:deep(.el-tabs__header) {
margin-bottom: 12px;
}
:deep(.el-tabs__item) {
padding: 0 12px;
font-size: 13px;
}
}
:deep(.el-table) {
font-size: 12px;
.el-table__cell {
padding: 6px;
}
}
}
</style> </style>

View file

@ -20,6 +20,9 @@
<el-icon><Search /></el-icon> <el-icon><Search /></el-icon>
</template> </template>
</el-input> </el-input>
<el-button type="primary" @click="showAddUserDialog">
<el-icon><Plus /></el-icon>添加用户
</el-button>
</div> </div>
</div> </div>
</template> </template>
@ -177,13 +180,66 @@
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
<!-- 添加用户对话框 -->
<el-dialog
v-model="addUserDialogVisible"
title="添加用户"
width="500px">
<el-form
ref="addUserFormRef"
:model="addUserForm"
:rules="addUserRules"
label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input
v-model="addUserForm.username"
placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input
v-model="addUserForm.nickname"
placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="addUserForm.email"
placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="addUserForm.password"
type="password"
show-password
placeholder="请输入密码" />
</el-form-item>
<el-form-item label="权限" prop="mask">
<el-select v-model="addUserForm.mask" placeholder="请选择权限">
<el-option :value="0" label="普通用户" />
<el-option :value="1" label="管理员" />
<el-option
v-if="isSuperAdmin"
:value="2"
label="超级管理员" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="addUserDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAddUser" :loading="adding">
确定
</el-button>
</span>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Edit, EditPen, Key } from '@element-plus/icons-vue' import { Search, Edit, EditPen, Key, Plus } from '@element-plus/icons-vue'
import { UserAPI } from '../api/user' import { UserAPI } from '../api/user'
const loading = ref(false) const loading = ref(false)
@ -476,6 +532,98 @@ onUnmounted(() => {
isMobile.value = window.innerWidth <= 768 isMobile.value = window.innerWidth <= 768
}) })
}) })
//
const addUserDialogVisible = ref(false)
const addUserFormRef = ref(null)
const adding = ref(false)
const addUserForm = ref({
username: '',
nickname: '',
email: '',
password: '',
mask: 0
})
//
const validateEmail = (rule, value, callback) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!value) {
callback(new Error('请输入电子邮件'))
} else if (!emailRegex.test(value)) {
callback(new Error('请输入有效的电子邮件地址'))
} else {
callback()
}
}
const addUserRules = {
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' }
],
email: [
{ required: true, validator: validateEmail, trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
],
mask: [
{ required: true, message: '请选择权限', trigger: 'change' }
]
}
//
const showAddUserDialog = () => {
addUserForm.value = {
username: '',
nickname: '',
email: '',
password: '',
mask: 0
}
addUserDialogVisible.value = true
}
//
const handleAddUser = async () => {
if (!addUserFormRef.value) return
await addUserFormRef.value.validate(async (valid) => {
if (valid) {
adding.value = true
try {
const response = await UserAPI.addUser(addUserForm.value)
if (response.retcode === 0) {
ElMessage.success('添加用户成功')
addUserDialogVisible.value = false
fetchUsers() //
}
} catch (error) {
console.error('添加用户失败:', error)
} finally {
adding.value = false
}
}
})
}
//
const userInfo = computed(() => {
try {
return JSON.parse(localStorage.getItem('userInfo'))
} catch {
return null
}
})
const isSuperAdmin = computed(() => userInfo.value?.mask === 2)
</script> </script>
<style scoped> <style scoped>
@ -627,4 +775,18 @@ onUnmounted(() => {
} }
} }
} }
/* 移动端适配 */
@media screen and (max-width: 768px) {
.header-right {
flex-direction: column;
gap: 8px;
width: 100%;
.el-button {
width: 100%;
justify-content: center;
}
}
}
</style> </style>