[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); }
100% { transform: rotate(360deg); }
}
/* 全局气泡提示样式 */
.el-popper {
z-index: 9999 !important;
}
/* 确保日志内容的气泡提示显示在最上层 */
.el-popper.is-pure.el-tooltip__popper {
z-index: 99999 !important;
}
</style>

View file

@ -112,5 +112,24 @@ export const TaskAPI = {
*/
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}`, {
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
prop="message"
label="日志内容"
min-width="300"
show-overflow-tooltip>
min-width="300">
<template #default="{ row }">
<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 }}
</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>
</el-table-column>
</el-table>
@ -459,6 +472,19 @@ const showExceptionDialog = (exception) => {
currentException.value = exception
exceptionDialogVisible.value = true
}
//
const expandedMessages = ref({})
//
const isLongMessage = (message) => {
return message && message.length > 100
}
//
const toggleMessage = (logId) => {
expandedMessages.value[logId] = !expandedMessages.value[logId]
}
</script>
<style scoped>
@ -566,6 +592,18 @@ const showExceptionDialog = (exception) => {
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) {
.debug-view {
@ -630,10 +668,13 @@ const showExceptionDialog = (exception) => {
z-index: 3000 !important;
}
/* 确保气泡提示在最上层 */
:deep(.el-popper) {
z-index: 9999 !important; /* 比下拉菜单更高 */
}
:deep(.el-select) {
z-index: 11;
min-width: 120px; /* 设置最小宽度 */
width: auto !important; /* 允许自动扩展 */
}
:deep(.el-date-editor) {

View file

@ -10,23 +10,23 @@
</el-tag>
</div>
<div class="header-right">
<el-input
v-model="searchQuery"
placeholder="搜索任务..."
class="search-input"
clearable
@clear="handleSearch">
<el-input v-model="searchQuery" placeholder="搜索任务..." class="search-input" clearable @clear="handleSearch">
<template #prefix>
<el-icon><Search /></el-icon>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="handleAddTask">
<el-icon><Plus /></el-icon>新建任务
<el-icon>
<Plus />
</el-icon>新建任务
</el-button>
</div>
</div>
</template>
<!-- 修改表格数据源 -->
<el-table
v-loading="loading"
:data="filteredTasks"
@ -36,10 +36,7 @@
<el-table-column prop="tid" label="任务UUID" width="350" />
<el-table-column prop="client_ip" label="客户端IP" width="140">
<template #default="{ row }">
<el-link
type="primary"
:href="`https://ip138.com/iplookup.php?ip=${row.client_ip}`"
target="_blank"
<el-link type="primary" :href="`https://ip138.com/iplookup.php?ip=${row.client_ip}`" target="_blank"
:underline="false">
{{ row.client_ip }}
</el-link>
@ -52,9 +49,7 @@
</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"
<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 ?
@ -64,54 +59,39 @@
</el-tag>
</template>
</el-table-column>
<el-table-column
label="操作"
:width="isMobile ? '70' : '420'"
fixed="right"
class-name="operation-column">
<el-table-column label="操作" :width="isMobile ? '70' : '420'" fixed="right" class-name="operation-column">
<template #default="scope">
<el-button
link
type="primary"
@click="handleViewDetails(scope.row)">
<el-icon><InfoFilled /></el-icon>详情
<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 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 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 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 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'"
<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'" />
@ -123,23 +103,13 @@
</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"
/>
<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>
<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">
@ -164,12 +134,7 @@
{{ currentProcess.workingDirectory }}
</el-descriptions-item>
<el-descriptions-item label="命令行" :span="2">
<el-input
type="textarea"
:rows="2"
v-model="currentProcess.commandLine"
readonly
/>
<el-input type="textarea" :rows="2" v-model="currentProcess.commandLine" readonly />
</el-descriptions-item>
</el-descriptions>
@ -188,13 +153,11 @@
</div>
</el-dialog>
<el-dialog
v-model="detailsDialogVisible"
title="任务详细信息"
width="700px"
destroy-on-close>
<el-dialog v-model="detailsDialogVisible" title="任务详细信息" width="700px" destroy-on-close>
<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">
{{ currentTaskDetails.tid }}
</el-descriptions-item>
@ -220,36 +183,54 @@
{{ currentTaskDetails.hash }}
</el-descriptions-item>
<el-descriptions-item label="标签" :span="2">
<el-tag
v-if="currentTaskDetails.tag"
:type="getTagType(currentTaskDetails.tag)"
effect="light">
<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-input type="textarea" :rows="2" v-model="currentTaskDetails.url" readonly />
</el-descriptions-item>
</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">
<el-button type="primary" @click="copyUrl">
<el-icon><DocumentCopy /></el-icon>复制下载地址
<el-icon>
<DocumentCopy />
</el-icon>复制下载地址
</el-button>
<el-button type="primary" @click="downloadFile" :disabled="!currentTaskDetails.url">
<el-icon><Download /></el-icon>下载文件
<el-icon>
<Download />
</el-icon>下载文件
</el-button>
<el-button
type="success"
@click="downloadProxiedFile"
:disabled="!canDownloadProxied">
<el-icon><Connection /></el-icon>下载文件(已代理)
<el-button type="success" @click="downloadProxiedFile" :disabled="!canDownloadProxied">
<el-icon>
<Connection />
</el-icon>下载文件(已代理)
</el-button>
</div>
</div>
@ -258,7 +239,7 @@
</template>
<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 { TaskAPI } from '../api/task'
import {
@ -276,57 +257,61 @@ const pageSize = ref(10)
const tasks = ref([])
const searchQuery = ref('')
//
const filteredTasks = computed(() => {
if (!searchQuery.value) return tasks.value
//
const filteredTasks = computed(() => 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:', {
let response
if (searchQuery.value.trim()) {
// 使
response = await TaskAPI.searchTasks({
keyword: searchQuery.value.trim(),
page: currentPage.value,
pageSize: pageSize.value
})
const response = await TaskAPI.getTaskList({
} else {
// 使
response = await TaskAPI.getTaskList({
page: currentPage.value,
pageSize: pageSize.value
})
console.log('Raw response:', response)
}
if (response.data) {
if (response.retcode === 0 && 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
})
console.error('获取任务列表失败:', error)
ElMessage.error('获取任务列表失败')
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
//
console.log('Searching for:', searchQuery.value)
//
const handleSearch = debounce(() => {
currentPage.value = 1 //
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 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) => {
try {
const response = await TaskAPI.getTaskDetails(task.tid)
if (response.retcode === 0) {
currentTaskDetails.value = response.data
activeTab.value = 'info' //
detailsDialogVisible.value = true
}
} catch (error) {
@ -514,6 +528,8 @@ const handleViewDetails = async (task) => {
}
}
// watch activeTab handleTabClick
const formatFileSize = (bytes) => {
if (!bytes) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
@ -679,6 +695,7 @@ const downloadProxiedFile = () => {
const downloadUrl = `${API_BASE_URL}/Download/${currentTaskDetails.value.tid}`
window.open(downloadUrl, '_blank')
}
</script>
<style scoped>
@ -741,9 +758,11 @@ const downloadProxiedFile = () => {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
@ -805,7 +824,8 @@ const downloadProxiedFile = () => {
display: flex;
gap: 12px;
justify-content: flex-end;
flex-wrap: wrap; /* 允许按钮换行 */
flex-wrap: wrap;
/* 允许按钮换行 */
}
:deep(.el-descriptions__cell) {
@ -824,7 +844,8 @@ const downloadProxiedFile = () => {
/* 添加队列错误状态的样式 */
.status-6.el-tag--danger {
animation: none; /* 移除脉冲动画 */
animation: none;
/* 移除脉冲动画 */
}
/* 添加任务对话框样式 */
@ -896,7 +917,8 @@ const downloadProxiedFile = () => {
.el-pagination__total,
.el-pagination__sizes,
.el-pagination__jump {
display: none; /* 在移动端隐藏这些元素 */
display: none;
/* 在移动端隐藏这些元素 */
}
}
}
@ -943,6 +965,7 @@ const downloadProxiedFile = () => {
/* 优化表格在不同屏幕尺寸下的显示 */
:deep(.el-table) {
@media screen and (max-width: 768px) {
/* UUID列宽度调整 */
.el-table-column--tid {
min-width: 120px;
@ -992,10 +1015,12 @@ const downloadProxiedFile = () => {
/* 优化固定列的样式 */
:deep(.operation-column) {
background-color: #fff; /* 确保背景色与表格一致 */
background-color: #fff;
/* 确保背景色与表格一致 */
.cell {
white-space: nowrap; /* 防止按钮换行 */
white-space: nowrap;
/* 防止按钮换行 */
}
}
@ -1006,13 +1031,16 @@ const downloadProxiedFile = () => {
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px !important; /* 减小内边距 */
padding: 4px !important;
/* 减小内边距 */
.el-button {
width: 100%;
justify-content: center;
padding: 4px 0; /* 调整按钮内边距 */
margin: 0; /* 移除按钮外边距 */
padding: 4px 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>

View file

@ -20,6 +20,9 @@
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="showAddUserDialog">
<el-icon><Plus /></el-icon>添加用户
</el-button>
</div>
</div>
</template>
@ -177,13 +180,66 @@
</span>
</template>
</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>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
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'
const loading = ref(false)
@ -476,6 +532,98 @@ onUnmounted(() => {
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>
<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>