[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); }
|
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>
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in a new issue