[Debug] 默认折叠过长的日志
[任务管理] 支持任务搜索和下载历史查看 [用户管理] 支持后台添加用户 修复一些其他问题
This commit is contained in:
parent
25191da5fa
commit
4f3622a553
6 changed files with 505 additions and 206 deletions
10
src/App.vue
10
src/App.vue
|
@ -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>
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -179,11 +179,24 @@
|
|||
<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' }">
|
||||
{{ row.message }}
|
||||
<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>
|
||||
|
@ -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) {
|
||||
|
|
|
@ -10,36 +10,33 @@
|
|||
</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"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#f5f7fa' }"
|
||||
border>
|
||||
|
||||
<!-- 修改表格数据源 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredTasks"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#f5f7fa' }"
|
||||
border>
|
||||
<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,66 +49,49 @@
|
|||
</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 ?
|
||||
(scope.row.queue_position === -1 ? ' 错误' : '#' + scope.row.queue_position)
|
||||
: ''
|
||||
{{ scope.row.status === 6 ?
|
||||
(scope.row.queue_position === -1 ? ' 错误' : '#' + scope.row.queue_position)
|
||||
: ''
|
||||
}}
|
||||
</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>
|
||||
|
||||
<el-button
|
||||
v-if="scope.row.status === 1"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleViewProcess(scope.row)">
|
||||
<el-icon><Monitor /></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 === 6"
|
||||
link
|
||||
type="success"
|
||||
@click="handlePrioritize(scope.row)">
|
||||
<el-icon><Top /></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="handleExecuteNow(scope.row)">
|
||||
<el-icon><VideoPlay /></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="[2, 7].includes(scope.row.status)"
|
||||
link
|
||||
type="success"
|
||||
@click="handleRetry(scope.row)">
|
||||
<el-icon><RefreshRight /></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
|
||||
link
|
||||
:type="scope.row.status === 1 ? 'warning' : 'danger'"
|
||||
<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'" />
|
||||
|
@ -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,68 +153,84 @@
|
|||
</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-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>
|
||||
<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>
|
||||
<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>
|
||||
</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,11 +239,11 @@
|
|||
</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 {
|
||||
Plus, Delete, Monitor, Search,
|
||||
import {
|
||||
Plus, Delete, Monitor, Search,
|
||||
InfoFilled, DocumentCopy, Download,
|
||||
VideoPlay, VideoPause, RefreshRight,
|
||||
Top, Connection
|
||||
|
@ -276,57 +257,61 @@ 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 filteredTasks = computed(() => tasks.value)
|
||||
|
||||
// 获取任务列表
|
||||
// 修改获取任务列表方法
|
||||
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) {
|
||||
let response
|
||||
if (searchQuery.value.trim()) {
|
||||
// 如果有搜索关键词,使用搜索接口
|
||||
response = await TaskAPI.searchTasks({
|
||||
keyword: searchQuery.value.trim(),
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
} else {
|
||||
// 否则使用普通列表接口
|
||||
response = await TaskAPI.getTaskList({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
}
|
||||
|
||||
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']
|
||||
|
@ -664,10 +680,10 @@ onUnmounted(() => {
|
|||
// 判断是否可以使用代理下载
|
||||
const canDownloadProxied = computed(() => {
|
||||
if (!currentTaskDetails.value) return false
|
||||
|
||||
|
||||
const validStatus = [3, 4] // 3: 已完成, 4: 已缓存
|
||||
return (
|
||||
validStatus.includes(currentTaskDetails.value.status) &&
|
||||
validStatus.includes(currentTaskDetails.value.status) &&
|
||||
currentTaskDetails.value.tag !== 'CLEANED'
|
||||
)
|
||||
})
|
||||
|
@ -675,10 +691,11 @@ const canDownloadProxied = computed(() => {
|
|||
// 添加代理下载方法
|
||||
const downloadProxiedFile = () => {
|
||||
if (!currentTaskDetails.value || !canDownloadProxied.value) return
|
||||
|
||||
|
||||
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;
|
||||
/* 移除脉冲动画 */
|
||||
}
|
||||
|
||||
/* 添加任务对话框样式 */
|
||||
|
@ -892,11 +913,12 @@ const downloadProxiedFile = () => {
|
|||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px;
|
||||
|
||||
|
||||
.el-pagination__total,
|
||||
.el-pagination__sizes,
|
||||
.el-pagination__jump {
|
||||
display: none; /* 在移动端隐藏这些元素 */
|
||||
display: none;
|
||||
/* 在移动端隐藏这些元素 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -904,7 +926,7 @@ const downloadProxiedFile = () => {
|
|||
.details-actions {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
|
@ -943,17 +965,18 @@ const downloadProxiedFile = () => {
|
|||
/* 优化表格在不同屏幕尺寸下的显示 */
|
||||
:deep(.el-table) {
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
/* UUID列宽度调整 */
|
||||
.el-table-column--tid {
|
||||
min-width: 120px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
|
||||
/* 状态列固定宽度 */
|
||||
.el-table-column--status {
|
||||
width: 15px !important;
|
||||
}
|
||||
|
||||
|
||||
/* 操作列自适应 */
|
||||
.el-table-column--operation {
|
||||
width: auto !important;
|
||||
|
@ -966,12 +989,12 @@ const downloadProxiedFile = () => {
|
|||
:deep(.el-dialog__body) {
|
||||
@media screen and (max-width: 768px) {
|
||||
padding: 15px;
|
||||
|
||||
|
||||
.el-descriptions__label {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
|
||||
.el-descriptions__content {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
@ -983,7 +1006,7 @@ const downloadProxiedFile = () => {
|
|||
@media screen and (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -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 = () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
.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>
|
|
@ -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>
|
Loading…
Reference in a new issue