初始提交:饿了么 Token 管理工具
This commit is contained in:
commit
295b46acd7
2
.env.development
Normal file
2
.env.development
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# 开发环境 API 基础路径
|
||||||
|
VITE_APP_BASE_API=
|
||||||
2
.env.production
Normal file
2
.env.production
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# 生产环境 API 基础路径
|
||||||
|
VITE_APP_BASE_API=
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
*.local
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3386
package-lock.json
generated
Normal file
3386
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "vue3_cli_default",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:test": "vite build --mode test",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"preview:test": "vite preview --mode test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"axios": "^1.13.5",
|
||||||
|
"element-plus": "^2.13.2",
|
||||||
|
"qs": "^6.15.0",
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-router": "^4.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.5.0",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
|
"vite": "^4.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/kfz-CRAXgFfE.png
Normal file
BIN
public/kfz-CRAXgFfE.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
BIN
public/pdd-BySt7T7H.png
Normal file
BIN
public/pdd-BySt7T7H.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
BIN
public/tb-8B6DW9L3.png
Normal file
BIN
public/tb-8B6DW9L3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
public/xy-D-YVxkSv.png
Normal file
BIN
public/xy-D-YVxkSv.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
8
src/App.vue
Normal file
8
src/App.vue
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<!-- src/App.vue -->
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// 无需手动导入组件,router-view 会自动渲染路由匹配的组件
|
||||||
|
</script>
|
||||||
236
src/api/shopTask.js
Normal file
236
src/api/shopTask.js
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取存活状态
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含操作结果
|
||||||
|
*/
|
||||||
|
export function GetAlive() {
|
||||||
|
return request({
|
||||||
|
url: `/alive/get`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取任务列表
|
||||||
|
* @param {number} page - 页码(分页参数),从1开始
|
||||||
|
* @param {number} size - 每页显示条数(分页参数)
|
||||||
|
* @param {string|number} [taskId] - 任务ID(可选),用于精准查询特定任务
|
||||||
|
* @param {string} [taskType] - 任务类型(可选),用于模糊查询相关任务
|
||||||
|
* @param {string} [shopName] - 店铺名称(可选),用于模糊查询相关任务
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含任务列表数据
|
||||||
|
*/
|
||||||
|
export function GetTaskAdmin(page, size, taskId, taskType, shopName) {
|
||||||
|
return request({
|
||||||
|
url: `/task/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
task_id: taskId,
|
||||||
|
task_type: taskType,
|
||||||
|
shop_name: shopName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取任务列表
|
||||||
|
* @param {number} page - 页码(分页参数),从1开始
|
||||||
|
* @param {number} size - 每页显示条数(分页参数)
|
||||||
|
* @param {string|number} [userId] - 用户ID 用于精准查询指定用户任务
|
||||||
|
* @param {string|number} [taskId] - 任务ID(可选),用于精准查询特定任务
|
||||||
|
* @param {string|number} [taskType] - 任务类型(可选),用于区分任务
|
||||||
|
* @param {string} [shopName] - 店铺名称(可选),用于模糊查询相关任务
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含任务列表数据
|
||||||
|
*/
|
||||||
|
export function GetTask(page, size,userId, taskId, taskType, shopName) {
|
||||||
|
return request({
|
||||||
|
url: `/task/getByUserId`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
user_id:userId,
|
||||||
|
task_id: taskId,
|
||||||
|
task_type: taskType,
|
||||||
|
shop_name: shopName,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停指定ID的任务
|
||||||
|
* @param {string|number} taskId - 要暂停的任务ID
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含操作结果
|
||||||
|
*/
|
||||||
|
export function PauseTask(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/task/pause/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复指定ID的暂停任务
|
||||||
|
* @param {string|number} taskId - 要恢复的任务ID
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含操作结果
|
||||||
|
*/
|
||||||
|
export function ResumeTask(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/task/resume/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止指定ID的任务(终止任务执行)
|
||||||
|
* @param {string|number} taskId - 要停止的任务ID
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含操作结果
|
||||||
|
*/
|
||||||
|
export function StopTask(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/task/stop/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID导出任务详情
|
||||||
|
* @param {string|number} taskId - 要导出的任务ID
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含操作结果
|
||||||
|
*/
|
||||||
|
export function ExportTaskDetailByTaskId(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/task/export/exportTaskDetail/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取导出任务列表
|
||||||
|
* @param {number} page - 页码(分页参数),从1开始
|
||||||
|
* @param {number} size - 每页显示条数(分页参数)
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含任务列表数据
|
||||||
|
*/
|
||||||
|
export function GetTaskExportAdmin(page, size) {
|
||||||
|
return request({
|
||||||
|
url: `/task/export/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取导出任务列表
|
||||||
|
* @param {number} page - 页码(分页参数),从1开始
|
||||||
|
* @param {number} size - 每页显示条数(分页参数)
|
||||||
|
* @param {string|number} [userId] - 用户ID 用于精准查询指定用户任务
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含任务列表数据
|
||||||
|
*/
|
||||||
|
export function GetTaskExport(page, size, userId) {
|
||||||
|
return request({
|
||||||
|
url: `/task/export/get/${userId}`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据任务id获取详情
|
||||||
|
* @param {string|number} taskId - 任务ID
|
||||||
|
* @param {number} page - 页码(分页参数),从1开始
|
||||||
|
* @param {number} size - 每页显示条数(分页参数)
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含任务列表数据
|
||||||
|
*/
|
||||||
|
export function GetTaskDetailByTaskId(taskId, page, size) {
|
||||||
|
return request({
|
||||||
|
url: `/task/getOver/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID删除任务详情
|
||||||
|
* @param {string|number} taskId - 要删除的任务ID
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含操作结果
|
||||||
|
*/
|
||||||
|
export function DelTaskDetailByTaskId(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/task/del/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取任务header
|
||||||
|
* @param {string|number} taskId - 要删除的任务ID
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含操作结果
|
||||||
|
*/
|
||||||
|
export function GetHeaderByTaskId(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/task/header/get/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取删除任务
|
||||||
|
* @param {number} page - 页码(分页参数),从1开始
|
||||||
|
* @param {number} size - 每页显示条数(分页参数)
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含操作结果
|
||||||
|
*/
|
||||||
|
export function GetDeleteTaskAdmin( page, size) {
|
||||||
|
return request({
|
||||||
|
url: `/deltask/getDelTask`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取删除任务-用户
|
||||||
|
* @param {number} page - 页码(分页参数),从1开始
|
||||||
|
* @param {number} size - 每页显示条数(分页参数)
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含操作结果
|
||||||
|
*/
|
||||||
|
export function GetDeleteTask( page, size, userId) {
|
||||||
|
return request({
|
||||||
|
url: `/deltask/getDelTaskByUserId/${userId}`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取删除任务详情
|
||||||
|
* @param {number} page - 页码(分页参数),从1开始
|
||||||
|
* @param {number} size - 每页显示条数(分页参数)
|
||||||
|
* @param {string|number} taskId - 要删除的任务ID
|
||||||
|
* @returns {Promise} 返回请求Promise对象,resolve时包含操作结果
|
||||||
|
*/
|
||||||
|
export function GetDeleteTaskDetailAdmin( page, size, taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/deltask/getDelTaskDetail/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
13
src/api/user.js
Normal file
13
src/api/user.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param {string} token - 用户token
|
||||||
|
* @returns {Promise} 用户信息
|
||||||
|
*/
|
||||||
|
export function GetUserInfo(token) {
|
||||||
|
return request({
|
||||||
|
url: 'https://api.buzhiyushu.cn/zhishu/userInfo/getUserId?token='+token,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
794
src/components/taskList/Debugger.vue
Normal file
794
src/components/taskList/Debugger.vue
Normal file
@ -0,0 +1,794 @@
|
|||||||
|
<template>
|
||||||
|
<div class="task-header-container">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading" class="loading-state">
|
||||||
|
<el-skeleton :rows="3" animated />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误状态 -->
|
||||||
|
<div v-else-if="error" class="error-state">
|
||||||
|
<el-alert type="error" :title="error" show-icon :closable="false" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据展示 -->
|
||||||
|
<div v-else-if="taskData" class="task-content">
|
||||||
|
<!-- 基本信息卡片 -->
|
||||||
|
<el-card class="info-card compact" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">基本信息</span>
|
||||||
|
<el-tag :type="getStatusType(taskData.status)" size="small">
|
||||||
|
{{ getStatusText(taskData.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-row :gutter="12">
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">任务ID:</span>
|
||||||
|
<el-text copyable size="small">{{ taskData.task_id }}</el-text>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">任务类型:</span>
|
||||||
|
<el-tag size="small">{{ getTaskTypeText(taskData.task_type) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">更新方式:</span>
|
||||||
|
<span>{{ getUpdateTypeText(taskData.update_type) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">店铺:</span>
|
||||||
|
<span>{{ taskData.shop_name }}</span>
|
||||||
|
<span class="info-sub">({{ taskData.shop_id }})</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">平台:</span>
|
||||||
|
<span>{{ getShopTypeText(taskData.shop_type) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="3">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">图片:</span>
|
||||||
|
<span>{{ getImgTypeText(taskData.img_type) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">创建时间:</span>
|
||||||
|
<span>{{ formatTimestamp(taskData.task_create_at) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4" v-if="taskData.task_over_at">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">结束时间:</span>
|
||||||
|
<span>{{ formatTimestamp(taskData.task_over_at) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">QPM限制:</span>
|
||||||
|
<span>{{ taskData.task_qpm || '无' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">最后索引:</span>
|
||||||
|
<span>{{ taskData.last_index || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 店铺配置卡片 -->
|
||||||
|
<el-card class="info-card compact" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">店铺配置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-row :gutter="12">
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">店铺别名:</span>
|
||||||
|
<span>{{ taskData.shop_msg.shop_alias_name || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">Token:</span>
|
||||||
|
<span>{{ taskData.shop_msg.token || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">标题前缀:</span>
|
||||||
|
<span>{{ taskData.shop_msg.goods_name_prefix || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">标题后缀:</span>
|
||||||
|
<span>{{ taskData.shop_msg.goods_name_suffix || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">空格字符:</span>
|
||||||
|
<span>{{ taskData.shop_msg.space_character === '1' ? '使用' : '不使用' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">水印位置:</span>
|
||||||
|
<span>{{ getWatermarkPositionText(taskData.shop_msg.watermark_position) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">规格名称:</span>
|
||||||
|
<span>{{ taskData.shop_msg.spec_name || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">子规格:</span>
|
||||||
|
<span>{{ taskData.shop_msg.spec_child_name || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">规格组成:</span>
|
||||||
|
<span>{{ getSpecComposeText(taskData.shop_msg.spec_child_name) || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">规格前缀:</span>
|
||||||
|
<span>{{ taskData.shop_msg.spec_prefix || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">规格后缀:</span>
|
||||||
|
<span>{{ taskData.shop_msg.spec_suffix || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">【闲鱼】一级类目:</span>
|
||||||
|
<span>{{ getPublishTypeText(taskData.shop_msg.publish_type) || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">【闲鱼】类目:</span>
|
||||||
|
<span>{{ taskData.shop_msg.category_id || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">假一赔十:</span>
|
||||||
|
<el-tag :type="taskData.shop_msg.is_fotl ? 'success' : 'info'" size="small">{{ taskData.shop_msg.is_fotl ? '支持' : '不支持' }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">预售:</span>
|
||||||
|
<el-tag :type="taskData.shop_msg.is_pre_sale ? 'warning' : 'info'" size="small">{{ taskData.shop_msg.is_pre_sale ? '是' : '否' }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">7天退货:</span>
|
||||||
|
<el-tag :type="taskData.shop_msg.is_refundable ? 'success' : 'danger'" size="small">{{ taskData.shop_msg.is_refundable ? '支持' : '不支持' }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">二手:</span>
|
||||||
|
<el-tag :type="taskData.shop_msg.is_second_hand ? 'warning' : 'success'" size="small">{{ taskData.shop_msg.is_second_hand ? '是' : '否' }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">发货时限:</span>
|
||||||
|
<span>{{ formatSeconds(taskData.shop_msg.shipment_limit_second) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">运费模板:</span>
|
||||||
|
<span>{{ taskData.shop_msg.cost_template_id || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">默认库存:</span>
|
||||||
|
<span>{{ taskData.shop_msg.def_stock || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">两件折扣:</span>
|
||||||
|
<span>{{ taskData.shop_msg.two_discount }}%</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 图片配置区域 -->
|
||||||
|
<div class="image-section compact">
|
||||||
|
<div class="section-title">图片配置</div>
|
||||||
|
<div class="image-row">
|
||||||
|
<div class="image-group">
|
||||||
|
<div class="image-label">水印图</div>
|
||||||
|
<el-image
|
||||||
|
v-if="taskData.shop_msg.watermark_img_url"
|
||||||
|
:src="taskData.shop_msg.watermark_img_url"
|
||||||
|
fit="cover"
|
||||||
|
:preview-src-list="[taskData.shop_msg.watermark_img_url]"
|
||||||
|
:preview-teleported="true"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
:zoom-rate="1.2"
|
||||||
|
class="preview-image-small"
|
||||||
|
/>
|
||||||
|
<span v-else class="no-image">无</span>
|
||||||
|
</div>
|
||||||
|
<div class="image-group">
|
||||||
|
<div class="image-label">SKU水印</div>
|
||||||
|
<el-image
|
||||||
|
v-if="taskData.shop_msg.sku_watermark_img_url"
|
||||||
|
:src="taskData.shop_msg.sku_watermark_img_url"
|
||||||
|
fit="cover"
|
||||||
|
:preview-src-list="[taskData.shop_msg.sku_watermark_img_url]"
|
||||||
|
:preview-teleported="true"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
:zoom-rate="1.2"
|
||||||
|
class="preview-image-small"
|
||||||
|
/>
|
||||||
|
<span v-else class="no-image">无</span>
|
||||||
|
</div>
|
||||||
|
<div class="image-group" v-if="taskData.shop_msg.carouse_last_img_url_array?.length">
|
||||||
|
<div class="image-label">轮播图末尾</div>
|
||||||
|
<div class="image-list-horizontal">
|
||||||
|
<el-image
|
||||||
|
v-for="(url, idx) in taskData.shop_msg.carouse_last_img_url_array.slice(0, 3)"
|
||||||
|
:key="idx"
|
||||||
|
:src="url"
|
||||||
|
fit="cover"
|
||||||
|
:preview-src-list="taskData.shop_msg.carouse_last_img_url_array"
|
||||||
|
:preview-teleported="true"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
:zoom-rate="1.2"
|
||||||
|
class="preview-image-tiny"
|
||||||
|
/>
|
||||||
|
<span v-if="taskData.shop_msg.carouse_last_img_url_array.length > 3" class="image-more">+{{ taskData.shop_msg.carouse_last_img_url_array.length - 3 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-group" v-if="taskData.shop_msg.goods_detail_first_img_url_array?.length">
|
||||||
|
<div class="image-label">详情首图</div>
|
||||||
|
<div class="image-list-horizontal">
|
||||||
|
<el-image
|
||||||
|
v-for="(url, idx) in taskData.shop_msg.goods_detail_first_img_url_array.slice(0, 3)"
|
||||||
|
:key="idx"
|
||||||
|
:src="url"
|
||||||
|
fit="cover"
|
||||||
|
:preview-src-list="taskData.shop_msg.goods_detail_first_img_url_array"
|
||||||
|
:preview-teleported="true"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
:zoom-rate="1.2"
|
||||||
|
class="preview-image-tiny"
|
||||||
|
/>
|
||||||
|
<span v-if="taskData.shop_msg.goods_detail_first_img_url_array.length > 3" class="image-more">+{{ taskData.shop_msg.goods_detail_first_img_url_array.length - 3 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-group" v-if="taskData.shop_msg.goods_detail_last_img_url_array?.length">
|
||||||
|
<div class="image-label">详情末尾</div>
|
||||||
|
<div class="image-list-horizontal">
|
||||||
|
<el-image
|
||||||
|
v-for="(url, idx) in taskData.shop_msg.goods_detail_last_img_url_array.slice(0, 3)"
|
||||||
|
:key="idx"
|
||||||
|
:src="url"
|
||||||
|
fit="cover"
|
||||||
|
:preview-src-list="taskData.shop_msg.goods_detail_last_img_url_array"
|
||||||
|
:preview-teleported="true"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
:zoom-rate="1.2"
|
||||||
|
class="preview-image-tiny"
|
||||||
|
/>
|
||||||
|
<span v-if="taskData.shop_msg.goods_detail_last_img_url_array.length > 3" class="image-more">+{{ taskData.shop_msg.goods_detail_last_img_url_array.length - 3 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 任务统计卡片 -->
|
||||||
|
<el-card class="info-card compact" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">任务统计</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="stat-row">
|
||||||
|
<div class="stat-item-compact">
|
||||||
|
<span class="stat-value-compact">{{ taskData.task_count }}</span>
|
||||||
|
<span class="stat-label-compact">总数</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item-compact">
|
||||||
|
<span class="stat-value-compact">{{ taskData.task_count_true }}</span>
|
||||||
|
<span class="stat-label-compact">真实</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item-compact">
|
||||||
|
<span class="stat-value-compact">{{ taskData.task_count_wait }}</span>
|
||||||
|
<span class="stat-label-compact">等待</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item-compact">
|
||||||
|
<span class="stat-value-compact">{{ taskData.task_count_over }}</span>
|
||||||
|
<span class="stat-label-compact">结束</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item-compact">
|
||||||
|
<span class="stat-value-compact text-success">{{ taskData.task_count_success }}</span>
|
||||||
|
<span class="stat-label-compact">成功</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item-compact">
|
||||||
|
<span class="stat-value-compact text-danger">{{ taskData.task_count_error }}</span>
|
||||||
|
<span class="stat-label-compact">错误</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 价格模版卡片 -->
|
||||||
|
<el-card class="info-card compact" shadow="never" v-if="taskData.price_mod && taskData.price_mod.length">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">价格模版</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="taskData.price_mod" border size="small">
|
||||||
|
<el-table-column prop="min" label="最小值" width="100">
|
||||||
|
<template #default="{ row }">¥{{ row.min / 100 }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="max" label="最大值" width="100">
|
||||||
|
<template #default="{ row }">¥{{ row.max / 100 }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="markup_rate" label="加价比例" width="100">
|
||||||
|
<template #default="{ row }">{{ row?.markup_rate ?? 0 }}%</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="markup_value" label="加价值">
|
||||||
|
<template #default="{ row }">¥{{ row?.markup_value ? (row.markup_value / 100).toFixed(2) : '0.00' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 运费模版 -->
|
||||||
|
<el-card class="info-card compact" shadow="never" v-if="taskData.ship_price_mod">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">运费模版</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="ship-price-mod-compact">{{ taskData.ship_price_mod }}</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { GetHeaderByTaskId } from '../../api/shopTask.js'
|
||||||
|
import { s } from 'vue-router/dist/router-CWoNjPRp.mjs'
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
interface DistrictMsg {
|
||||||
|
district_id: number
|
||||||
|
district_type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShopMsg {
|
||||||
|
id: number
|
||||||
|
shop_alias_name: string
|
||||||
|
shop_name: string
|
||||||
|
token: string
|
||||||
|
goods_name_prefix: string
|
||||||
|
goods_name_suffix: string
|
||||||
|
title_consist_of: string
|
||||||
|
space_character: string
|
||||||
|
watermark_img_url: string
|
||||||
|
watermark_position: string
|
||||||
|
carouse_last_img_url_array: string[]
|
||||||
|
goods_detail_first_img_url_array: string[]
|
||||||
|
goods_detail_last_img_url_array: string[]
|
||||||
|
spec_name: string
|
||||||
|
spec_id: number
|
||||||
|
spec_child_name: string
|
||||||
|
spec_prefix: string
|
||||||
|
spec_suffix: string
|
||||||
|
is_fotl: boolean
|
||||||
|
is_pre_sale: boolean
|
||||||
|
is_refundable: boolean
|
||||||
|
is_second_hand: boolean
|
||||||
|
shipment_limit_second: number
|
||||||
|
cost_template_id: number
|
||||||
|
def_stock: number
|
||||||
|
two_discount: number
|
||||||
|
district_msg: DistrictMsg
|
||||||
|
shop_context: string
|
||||||
|
sku_watermark_img_url: string
|
||||||
|
publish_type: string
|
||||||
|
category_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PriceMod {
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
markup_rate: number
|
||||||
|
markup_value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskData {
|
||||||
|
task_id: string
|
||||||
|
task_type: number
|
||||||
|
shop_id: number
|
||||||
|
shop_name: string
|
||||||
|
shop_type: string
|
||||||
|
shop_msg: ShopMsg
|
||||||
|
price_mod: PriceMod[] | null
|
||||||
|
ship_price_mod: string
|
||||||
|
task_count: number
|
||||||
|
task_count_true: number
|
||||||
|
task_count_wait: number
|
||||||
|
task_count_over: number
|
||||||
|
task_count_success: number
|
||||||
|
task_count_error: number
|
||||||
|
status: number
|
||||||
|
task_qpm: number
|
||||||
|
task_create_at: number
|
||||||
|
task_over_at: number
|
||||||
|
last_index: number
|
||||||
|
img_type: number
|
||||||
|
update_type: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
taskId: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
const taskData = ref<TaskData | null>(null)
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status: number): string => {
|
||||||
|
const statusMap: Record<number, string> = {
|
||||||
|
1: '运行中',
|
||||||
|
2: '已暂停',
|
||||||
|
3: '已停止',
|
||||||
|
4: '已完成',
|
||||||
|
10: '推送中'
|
||||||
|
}
|
||||||
|
return statusMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
const getStatusType = (status: number): 'success' | 'danger' | 'warning' | 'info' => {
|
||||||
|
const typeMap: Record<number, 'success' | 'danger' | 'warning' | 'info'> = {
|
||||||
|
1: 'warning',
|
||||||
|
2: 'info',
|
||||||
|
3: 'danger',
|
||||||
|
4: 'success',
|
||||||
|
10: 'info'
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务类型文本
|
||||||
|
const getTaskTypeText = (taskType: number): string => {
|
||||||
|
const typeMap: Record<number, string> = {
|
||||||
|
1: '核价发布',
|
||||||
|
2: '表格发布',
|
||||||
|
3: '拉取商品',
|
||||||
|
4: '拉取商品详情'
|
||||||
|
}
|
||||||
|
return typeMap[taskType] || `未知`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取更新方式文本
|
||||||
|
const getUpdateTypeText = (taskType: number): string => {
|
||||||
|
const typeMap: Record<number, string> = {
|
||||||
|
0: '未知',
|
||||||
|
1: '过滤重复',
|
||||||
|
2: '全新上传'
|
||||||
|
}
|
||||||
|
return typeMap[taskType] || `未知`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取店铺类型文本
|
||||||
|
const getShopTypeText = (shopType: string): string => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
'1': '拼多多',
|
||||||
|
'2': '孔夫子',
|
||||||
|
'5': '闲鱼',
|
||||||
|
'6': '淘宝',
|
||||||
|
}
|
||||||
|
return typeMap[shopType] || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片类型文本
|
||||||
|
const getImgTypeText = (imgType: number): string => {
|
||||||
|
const typeMap: Record<number, string> = {
|
||||||
|
1: '仅官图',
|
||||||
|
2: '实拍图',
|
||||||
|
3: '优先官图',
|
||||||
|
4: '优先实拍图'
|
||||||
|
}
|
||||||
|
return typeMap[imgType] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取水印位置文本
|
||||||
|
const getWatermarkPositionText = (position: string): string => {
|
||||||
|
const posMap: Record<string, string> = {
|
||||||
|
'0': '全部',
|
||||||
|
'1': '第一张'
|
||||||
|
}
|
||||||
|
return posMap[position] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取规格组成文本
|
||||||
|
const getSpecComposeText = (specCompose: string): string => {
|
||||||
|
const typeMap: Record<number, string> = {
|
||||||
|
"1": '自定义',
|
||||||
|
"2": 'ISBN',
|
||||||
|
"3": '书名',
|
||||||
|
"4": '货号'
|
||||||
|
}
|
||||||
|
return typeMap[specCompose] || `未知`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取是否使用图书类目文本
|
||||||
|
const getPublishTypeText = (publishType: string): string => {
|
||||||
|
const typeMap: Record<number, string> = {
|
||||||
|
"0": '使用',
|
||||||
|
"1": '不使用'
|
||||||
|
}
|
||||||
|
return typeMap[publishType] || `未知`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间戳
|
||||||
|
const formatTimestamp = (timestamp: number): string => {
|
||||||
|
if (!timestamp) return '-'
|
||||||
|
const date = new Date(timestamp * 1000)
|
||||||
|
return `${date.getMonth()+1}/${date.getDate()} ${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化秒数
|
||||||
|
const formatSeconds = (seconds: number): string => {
|
||||||
|
if (!seconds) return '-'
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
if (hours > 0) return `${hours}h`
|
||||||
|
const days = Math.floor(seconds / 86400)
|
||||||
|
if (days > 0) return `${days}d`
|
||||||
|
return `${seconds}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取详情数据
|
||||||
|
const getHeader = async (taskId: string) => {
|
||||||
|
if (!taskId) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await GetHeaderByTaskId(taskId) as any
|
||||||
|
if (response.code === '200' && response.data) {
|
||||||
|
taskData.value = response.data
|
||||||
|
} else {
|
||||||
|
error.value = response.msg || '获取数据失败'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : '网络请求失败'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.taskId, (newVal) => {
|
||||||
|
if (newVal) getHeader(newVal)
|
||||||
|
}, { immediate: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.task-header-container {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state, .error-state {
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.compact :deep(.el-card__header) {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.compact :deep(.el-card__body) {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-sub {
|
||||||
|
color: #95a5a6;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-section.compact {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-group {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #95a5a6;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image-small {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
cursor: pointer;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image-tiny {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
cursor: pointer;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-list-horizontal {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-more {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #3498db;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-image {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item-compact {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px 0;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value-compact {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label-compact {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-success {
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-price-mod-compact {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏图片预览器的关闭按钮和工具栏 */
|
||||||
|
:deep(.el-image-viewer__btn) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-image-viewer__close) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-image-viewer__actions) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-image-viewer__mask) {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
351
src/components/taskList/DelCenter.vue
Normal file
351
src/components/taskList/DelCenter.vue
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" title="删除中心" width="1440">
|
||||||
|
<div v-loading="downloadLoading">
|
||||||
|
<div v-if="dowCenterData && dowCenterData.length" class="detail-container">
|
||||||
|
<el-table :data="dowCenterData" border style="margin-top: 10px">
|
||||||
|
<!-- 可展开行:任务详情 -->
|
||||||
|
<el-table-column type="expand">
|
||||||
|
<template #default="item">
|
||||||
|
<DelCenterDetail :task-id="item.row.task_id" ></DelCenterDetail>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="任务id" prop="task_id" width="200" align="center"/>
|
||||||
|
<el-table-column label="店铺名称" prop="shop_name" width="200" align="center"/>
|
||||||
|
<el-table-column label="类型" align="center" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag
|
||||||
|
size="small" effect="light">
|
||||||
|
{{ getTaskTypeText(scope.row.task_type) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" align="center" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getStatusType(scope.row.status)"
|
||||||
|
size="small" effect="light">
|
||||||
|
{{ getStatusText(scope.row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="进度" align="center" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-progress :percentage="Math.round((scope.row.task_count_over / scope.row.task_count) * 100) || 0"
|
||||||
|
:status="getDownloadProgressStatus(scope.row.status)" :stroke-width="15" :text-inside="true"
|
||||||
|
:format="() => `${scope.row.task_count_over}/${scope.row.task_count}`" color="#67c23a"
|
||||||
|
class="custom-progress" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="暂停时间" prop="pause_at" width="180" align="center"/>
|
||||||
|
<el-table-column label="终止时间" prop="stop_at" width="180" align="center"/>
|
||||||
|
<el-table-column label="创建时间" prop="create_at" width="180" align="center"/>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="totalCount"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
style="margin-top: 10px; justify-content: flex-end;"
|
||||||
|
size="small"
|
||||||
|
background
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else description="暂无下载任务" />
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { GetDeleteTaskAdmin,GetDeleteTask } from '../../api/shopTask.js'
|
||||||
|
import DelCenterDetail from '../taskList/DelCenterDetail.vue'
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps({
|
||||||
|
currentUserId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
isAdmin: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义响应数据类型接口
|
||||||
|
interface DelTaskResponse {
|
||||||
|
code: string
|
||||||
|
msg?: string
|
||||||
|
data: {
|
||||||
|
list: DelTaskItem[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface DelTaskItem {
|
||||||
|
task_id: string
|
||||||
|
shop_name: string
|
||||||
|
task_type: number
|
||||||
|
status: number
|
||||||
|
task_count:number
|
||||||
|
task_count_over:number
|
||||||
|
pause_at: string
|
||||||
|
stop_at: string
|
||||||
|
create_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义类型接口
|
||||||
|
interface DowList {
|
||||||
|
task_id: string
|
||||||
|
shop_name: string
|
||||||
|
task_type: number
|
||||||
|
status: number
|
||||||
|
task_count:number
|
||||||
|
task_count_over:number
|
||||||
|
pause_at: string
|
||||||
|
stop_at: string
|
||||||
|
create_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogVisible = ref(false) // 默认关闭,通过父组件控制打开
|
||||||
|
|
||||||
|
const downloadLoading = ref(false) // 下载列表加载状态
|
||||||
|
const dowCenterData = ref<DowList[]>([]) // 当前页数据
|
||||||
|
const totalCount = ref(0) // 总数据条数
|
||||||
|
|
||||||
|
// 分页相关变量
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
|
// ==================== 方法组 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开弹窗(供父组件调用)
|
||||||
|
*/
|
||||||
|
const openDialog = () => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
getDeleteTaskList() // 打开时重新获取数据
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭弹窗
|
||||||
|
*/
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 可选:重置分页状态
|
||||||
|
currentPage.value = 1
|
||||||
|
pageSize.value = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期时间
|
||||||
|
* @param dateTimeStr - ISO格式的日期时间字符串 (如: 2026-04-09T15:20:43Z)
|
||||||
|
* @returns 格式化后的日期时间字符串 (年-月-日 时:分:秒)
|
||||||
|
*/
|
||||||
|
const formatDateTime = (dateTimeStr: string): string => {
|
||||||
|
if (!dateTimeStr || dateTimeStr === '-') return '-'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(dateTimeStr)
|
||||||
|
// 检查日期是否有效
|
||||||
|
if (isNaN(date.getTime())) return dateTimeStr
|
||||||
|
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
} catch (error) {
|
||||||
|
console.error('日期格式化失败:', error)
|
||||||
|
return dateTimeStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取删除任务列表数据(支持分页)
|
||||||
|
*/
|
||||||
|
const getDeleteTaskList = async () => {
|
||||||
|
downloadLoading.value = true
|
||||||
|
try {
|
||||||
|
let response: DelTaskResponse
|
||||||
|
if (props.isAdmin) {
|
||||||
|
response = await GetDeleteTaskAdmin(
|
||||||
|
currentPage.value,
|
||||||
|
pageSize.value,
|
||||||
|
)
|
||||||
|
}else{
|
||||||
|
// 普通用户,传递 userId
|
||||||
|
response = await GetDeleteTask(
|
||||||
|
currentPage.value,
|
||||||
|
pageSize.value,
|
||||||
|
props.currentUserId // 使用 props 中的 userId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建表格数据数组
|
||||||
|
const tableData: DowList[] = []
|
||||||
|
if (response && response.data.list && Array.isArray(response.data.list)) {
|
||||||
|
for (let i = 0; i < response.data.list.length; i++) {
|
||||||
|
const item = response.data.list[i]
|
||||||
|
tableData.push({
|
||||||
|
task_id: item.task_id || '-',
|
||||||
|
shop_name: item.shop_name || '-',
|
||||||
|
task_type: item.task_type,
|
||||||
|
status: item.status,
|
||||||
|
task_count:item.task_count,
|
||||||
|
task_count_over:item.task_count_over,
|
||||||
|
pause_at: formatDateTime(item.pause_at),
|
||||||
|
stop_at: formatDateTime(item.stop_at),
|
||||||
|
create_at: formatDateTime(item.create_at), // 格式化创建时间
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 赋值给 dowCenterData
|
||||||
|
dowCenterData.value = tableData
|
||||||
|
// 更新总数(根据后端返回的实际total字段)
|
||||||
|
totalCount.value = response.data.total || 0
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取下载列表失败:', error)
|
||||||
|
ElMessage.error('获取下载列表失败,请重试')
|
||||||
|
dowCenterData.value = []
|
||||||
|
totalCount.value = 0
|
||||||
|
} finally {
|
||||||
|
downloadLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页大小改变时的处理
|
||||||
|
* @param val 新的每页条数
|
||||||
|
*/
|
||||||
|
const handleSizeChange = (val: number) => {
|
||||||
|
pageSize.value = val
|
||||||
|
currentPage.value = 1 // 重置到第一页
|
||||||
|
// 分页参数变化会自动触发 watch,重新获取数据
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前页改变时的处理
|
||||||
|
* @param val 新的页码
|
||||||
|
*/
|
||||||
|
const handleCurrentChange = (val: number) => {
|
||||||
|
currentPage.value = val
|
||||||
|
// 分页参数变化会自动触发 watch,重新获取数据
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助函数:从URL中提取文件名
|
||||||
|
* @param url 文件URL
|
||||||
|
* @returns 文件名
|
||||||
|
*/
|
||||||
|
const getFileNameFromUrl = (url : string) => {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url, window.location.origin)
|
||||||
|
const pathname = urlObj.pathname
|
||||||
|
return pathname.substring(pathname.lastIndexOf('/') + 1) || 'download'
|
||||||
|
} catch (e) {
|
||||||
|
return 'download'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态对应的标签类型
|
||||||
|
* @param status - 状态码 (1:处理中, 2:已完成, 3:失败)
|
||||||
|
* @returns 标签类型
|
||||||
|
*/
|
||||||
|
const getStatusType = (status : number) => {
|
||||||
|
const typeMap : Record<number, string> = {
|
||||||
|
0: '', // 待执行
|
||||||
|
1: 'primary', // 执行中
|
||||||
|
2: 'warning', // 暂停
|
||||||
|
3: 'success' // 完成
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本
|
||||||
|
* @param status - 状态码
|
||||||
|
* @returns 状态文本
|
||||||
|
*/
|
||||||
|
const getStatusText = (status : number) => {
|
||||||
|
const textMap : Record<number, string> = {
|
||||||
|
0: '待执行',
|
||||||
|
1: '处理中',
|
||||||
|
2: '暂停',
|
||||||
|
3: '完成'
|
||||||
|
}
|
||||||
|
return textMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取类型文本
|
||||||
|
* @param status - 状态码
|
||||||
|
* @returns 状态文本
|
||||||
|
*/
|
||||||
|
const getTaskTypeText = (status : number) => {
|
||||||
|
const textMap : Record<number, string> = {
|
||||||
|
0: '常规删除',
|
||||||
|
1: '常规删除',
|
||||||
|
2: '数量删除',
|
||||||
|
3: '时间删除'
|
||||||
|
}
|
||||||
|
return textMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取执行进度条状态
|
||||||
|
* @param status - 状态码
|
||||||
|
* @returns 进度条状态
|
||||||
|
*/
|
||||||
|
const getDownloadProgressStatus = (status : number) => {
|
||||||
|
if (status === 3) return 'success' // 完成
|
||||||
|
return '' // 处理中
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听分页参数变化,重新请求数据
|
||||||
|
watch([currentPage, pageSize], () => {
|
||||||
|
// 只在弹窗打开时才请求数据
|
||||||
|
if (dialogVisible.value) {
|
||||||
|
getDeleteTaskList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法给父组件调用
|
||||||
|
defineExpose({
|
||||||
|
openDialog
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-progress {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修正进度条文字使其可以纵向居中 */
|
||||||
|
>>> .el-progress-bar__innerText > span{
|
||||||
|
display: block;
|
||||||
|
height: 16px;
|
||||||
|
line-height: 12px;
|
||||||
|
color: #000;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
/* 修正进度条文字使其可以纵向居中 */
|
||||||
|
</style>
|
||||||
182
src/components/taskList/DelCenterDetail.vue
Normal file
182
src/components/taskList/DelCenterDetail.vue
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<div v-loading="loading">
|
||||||
|
<div v-if="detailData && detailData.data && detailData.data.length" class="detail-container">
|
||||||
|
<!-- 详情列表 - 直接显示数据,不需要前端切片 -->
|
||||||
|
<el-table :data="detailData.data" border size="small" style="margin-top: 10px">
|
||||||
|
<el-table-column label="序号" type="index" width="50" align="center"/>
|
||||||
|
<el-table-column label="商品编码" prop="isbn" width="150" align="center"/>
|
||||||
|
<el-table-column label="商品名称" prop="book_name" width="450" align="center"/>
|
||||||
|
<el-table-column label="执行状态" prop="status" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="row.status === 1 ? 'success' : row.status === 2 ? 'danger' : 'info'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ row.status === 1 ? '成功' : row.status === 2 ? '失败' : '待执行' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="错误信息" v-if="showErrorMsg" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.err || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[1, 20, 50, 100]"
|
||||||
|
:total="totalCount"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
style="margin-top: 10px; justify-content: flex-end;"
|
||||||
|
size="small"
|
||||||
|
background
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else description="暂无详情数据" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
import { GetDeleteTaskDetailAdmin } from '../../api/shopTask.js'
|
||||||
|
|
||||||
|
interface DetailItem {
|
||||||
|
isbn: string
|
||||||
|
book_name: string
|
||||||
|
goods_id: string
|
||||||
|
status: string
|
||||||
|
err: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskDetail {
|
||||||
|
data: DetailItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
taskId: string,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const detailData = ref<TaskDetail | null>(null)
|
||||||
|
const showErrorMsg = ref(true)
|
||||||
|
|
||||||
|
// 分页相关变量
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const totalCount = ref(0)
|
||||||
|
|
||||||
|
// 分页大小改变时的处理
|
||||||
|
const handleSizeChange = (val: number) => {
|
||||||
|
pageSize.value = val
|
||||||
|
currentPage.value = 1 // 重置到第一页
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页改变时的处理
|
||||||
|
const handleCurrentChange = (val: number) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期时间
|
||||||
|
* @param dateTimeStr - ISO格式的日期时间字符串 (如: 2026-04-09T15:20:43Z)
|
||||||
|
* @returns 格式化后的日期时间字符串 (年-月-日 时:分:秒)
|
||||||
|
*/
|
||||||
|
const formatDateTime = (dateTimeStr: string): string => {
|
||||||
|
if (!dateTimeStr || dateTimeStr === '-') return '-'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(dateTimeStr)
|
||||||
|
// 检查日期是否有效
|
||||||
|
if (isNaN(date.getTime())) return dateTimeStr
|
||||||
|
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
} catch (error) {
|
||||||
|
console.error('日期格式化失败:', error)
|
||||||
|
return dateTimeStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取详情数据
|
||||||
|
const fetchDetail = async (taskId: string) => {
|
||||||
|
if (!taskId) {
|
||||||
|
console.warn('taskId is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await GetDeleteTaskDetailAdmin(currentPage.value, pageSize.value, taskId)
|
||||||
|
|
||||||
|
// 构建表格数据数组
|
||||||
|
const tableData: DetailItem[] = []
|
||||||
|
|
||||||
|
if (response && response.data.list && Array.isArray(response.data.list)) {
|
||||||
|
for (let i = 0; i < response.data.list.length; i++) {
|
||||||
|
console.log(response.data.list[i].err)
|
||||||
|
tableData.push({
|
||||||
|
isbn: response.data.list[i]?.isbn || '-',
|
||||||
|
book_name: response.data.list[i]?.book_name || '-',
|
||||||
|
goods_id: response.data.list[i]?.goods_id || '-',
|
||||||
|
status: response.data.list[i]?.status,
|
||||||
|
err: response.data.list[i]?.err || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 赋值给 detailData
|
||||||
|
detailData.value = {
|
||||||
|
data: tableData
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新总数
|
||||||
|
totalCount.value = response.data.total
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务详情失败:', error)
|
||||||
|
detailData.value = null
|
||||||
|
totalCount.value = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 taskId 变化
|
||||||
|
watch(() => props.taskId, (newTaskId, oldTaskId) => {
|
||||||
|
console.log('taskId 变化:', { old: oldTaskId, new: newTaskId })
|
||||||
|
if (newTaskId) {
|
||||||
|
fetchDetail(newTaskId)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 监听分页参数变化,重新请求数据
|
||||||
|
watch([currentPage, pageSize], () => {
|
||||||
|
if (props.taskId) {
|
||||||
|
fetchDetail(props.taskId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
154
src/components/taskList/Detail.vue
Normal file
154
src/components/taskList/Detail.vue
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<div v-loading="loading">
|
||||||
|
<div v-if="detailData && detailData.data && detailData.data.length" class="detail-container">
|
||||||
|
<!-- 详情列表 - 直接显示数据,不需要前端切片 -->
|
||||||
|
<el-table :data="detailData.data" border size="small" style="margin-top: 10px">
|
||||||
|
<el-table-column label="序号" type="index" width="50" align="center"/>
|
||||||
|
<el-table-column label="商品编码" prop="isbn" width="150" align="center"/>
|
||||||
|
<el-table-column label="商品名称" prop="book_name" width="450" align="center"/>
|
||||||
|
<el-table-column label="执行状态" prop="status" width="80" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.status === 1 ? '成功' : '失败' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="错误信息" v-if="showErrorMsg" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.error || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="totalCount"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
style="margin-top: 10px; justify-content: flex-end;"
|
||||||
|
size="small"
|
||||||
|
background
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else description="暂无详情数据" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
import { GetTaskDetailByTaskId } from '../../api/shopTask.js'
|
||||||
|
|
||||||
|
interface DetailItem {
|
||||||
|
isbn: string
|
||||||
|
book_name: string
|
||||||
|
status: string
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskDetail {
|
||||||
|
data: DetailItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
taskId: string,
|
||||||
|
taskType: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const detailData = ref<TaskDetail | null>(null)
|
||||||
|
const showErrorMsg = ref(true)
|
||||||
|
|
||||||
|
// 分页相关变量
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const totalCount = ref(0)
|
||||||
|
|
||||||
|
// 分页大小改变时的处理
|
||||||
|
const handleSizeChange = (val: number) => {
|
||||||
|
pageSize.value = val
|
||||||
|
currentPage.value = 1 // 重置到第一页
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页改变时的处理
|
||||||
|
const handleCurrentChange = (val: number) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取详情数据
|
||||||
|
const fetchDetail = async (taskId: string) => {
|
||||||
|
if (!taskId) {
|
||||||
|
console.warn('taskId is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await GetTaskDetailByTaskId(taskId, currentPage.value, pageSize.value)
|
||||||
|
|
||||||
|
// 构建表格数据数组
|
||||||
|
const tableData: DetailItem[] = []
|
||||||
|
|
||||||
|
if (response && response.data.list && Array.isArray(response.data.list)) {
|
||||||
|
for (let i = 0; i < response.data.list.length; i++) {
|
||||||
|
let error = response.data.list[i].detail?.error
|
||||||
|
if (props.taskType == "3"){
|
||||||
|
error = ""
|
||||||
|
}
|
||||||
|
tableData.push({
|
||||||
|
isbn: response.data.list[i].book_info?.isbn || '-',
|
||||||
|
book_name: response.data.list[i].book_info?.book_name || '-',
|
||||||
|
status: response.data.list[i].detail?.status,
|
||||||
|
error: error || ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 赋值给 detailData
|
||||||
|
detailData.value = {
|
||||||
|
data: tableData
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新总数
|
||||||
|
totalCount.value = response.data.total
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务详情失败:', error)
|
||||||
|
detailData.value = null
|
||||||
|
totalCount.value = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 taskId 变化
|
||||||
|
watch(() => props.taskId, (newTaskId, oldTaskId) => {
|
||||||
|
console.log('taskId 变化:', { old: oldTaskId, new: newTaskId })
|
||||||
|
if (newTaskId) {
|
||||||
|
fetchDetail(newTaskId)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 监听分页参数变化,重新请求数据
|
||||||
|
watch([currentPage, pageSize], () => {
|
||||||
|
if (props.taskId) {
|
||||||
|
fetchDetail(props.taskId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
387
src/components/taskList/DowCenter.vue
Normal file
387
src/components/taskList/DowCenter.vue
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" title="下载中心" width="1180">
|
||||||
|
<div v-loading="downloadLoading">
|
||||||
|
<div v-if="dowCenterData && dowCenterData.length" class="detail-container">
|
||||||
|
<el-table :data="dowCenterData" border style="margin-top: 10px">
|
||||||
|
<el-table-column label="任务编码" prop="task_id" width="180" align="center"/>
|
||||||
|
<el-table-column label="店铺名称" prop="shop_name" width="200" align="center"/>
|
||||||
|
<el-table-column label="状态" align="center" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getStatusType(scope.row.status)"
|
||||||
|
size="small" effect="light">
|
||||||
|
{{ getStatusText(scope.row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="进度" align="center" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-progress :percentage="Math.round((scope.row.complete / scope.row.total) * 100) || 0"
|
||||||
|
:status="getDownloadProgressStatus(scope.row.status)" :stroke-width="15" :text-inside="true"
|
||||||
|
:format="() => `${scope.row.complete}/${scope.row.total}`" color="#67c23a"
|
||||||
|
class="custom-progress" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="完成时间" prop="complete_at" width="180" align="center"/>
|
||||||
|
<el-table-column label="创建时间" prop="create_at" width="180" align="center"/>
|
||||||
|
<el-table-column label="操作" align="center" fixed="right" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button v-if="scope.row.status === 2" type="primary" link
|
||||||
|
@click="handleDownloadFile(scope.row)">
|
||||||
|
下载
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="totalCount"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
style="margin-top: 10px; justify-content: flex-end;"
|
||||||
|
size="small"
|
||||||
|
background
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else description="暂无下载任务" />
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { GetTaskExportAdmin, GetTaskExport } from '../../api/shopTask.js'
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps({
|
||||||
|
currentUserId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
isAdmin: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义响应数据类型接口
|
||||||
|
interface ExportResponse {
|
||||||
|
code: string
|
||||||
|
msg?: string
|
||||||
|
data: {
|
||||||
|
list: ExportItem[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface ExportItem {
|
||||||
|
task_id: string
|
||||||
|
shop_name: string
|
||||||
|
status: number
|
||||||
|
complete: number
|
||||||
|
total: number
|
||||||
|
complete_at: string
|
||||||
|
create_at: string
|
||||||
|
file_url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义类型接口
|
||||||
|
interface DowList {
|
||||||
|
task_id: string
|
||||||
|
shop_name: string
|
||||||
|
status: number
|
||||||
|
complete: number
|
||||||
|
total: number
|
||||||
|
complete_at: string
|
||||||
|
create_at: string
|
||||||
|
fileUrl?: string // 添加文件URL字段
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogVisible = ref(false) // 默认关闭,通过父组件控制打开
|
||||||
|
|
||||||
|
const downloadLoading = ref(false) // 下载列表加载状态
|
||||||
|
const dowCenterData = ref<DowList[]>([]) // 当前页数据
|
||||||
|
const totalCount = ref(0) // 总数据条数
|
||||||
|
|
||||||
|
// 分页相关变量
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
|
// ==================== 方法组 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开弹窗(供父组件调用)
|
||||||
|
*/
|
||||||
|
const openDialog = () => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
getDownloadList() // 打开时重新获取数据
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭弹窗
|
||||||
|
*/
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 可选:重置分页状态
|
||||||
|
currentPage.value = 1
|
||||||
|
pageSize.value = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期时间
|
||||||
|
* @param dateTimeStr - ISO格式的日期时间字符串 (如: 2026-04-09T15:20:43Z)
|
||||||
|
* @returns 格式化后的日期时间字符串 (年-月-日 时:分:秒)
|
||||||
|
*/
|
||||||
|
const formatDateTime = (dateTimeStr: string): string => {
|
||||||
|
if (!dateTimeStr || dateTimeStr === '-') return '-'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(dateTimeStr)
|
||||||
|
// 检查日期是否有效
|
||||||
|
if (isNaN(date.getTime())) return dateTimeStr
|
||||||
|
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
} catch (error) {
|
||||||
|
console.error('日期格式化失败:', error)
|
||||||
|
return dateTimeStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载列表数据(支持分页)
|
||||||
|
*/
|
||||||
|
const getDownloadList = async () => {
|
||||||
|
downloadLoading.value = true
|
||||||
|
try {
|
||||||
|
let response: ExportResponse
|
||||||
|
if (props.isAdmin) {
|
||||||
|
response = await GetTaskExportAdmin(
|
||||||
|
currentPage.value,
|
||||||
|
pageSize.value,
|
||||||
|
)
|
||||||
|
}else{
|
||||||
|
// 普通用户,传递 userId
|
||||||
|
response = await GetTaskExport(
|
||||||
|
currentPage.value,
|
||||||
|
pageSize.value,
|
||||||
|
props.currentUserId // 使用 props 中的 userId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建表格数据数组
|
||||||
|
const tableData: DowList[] = []
|
||||||
|
if (response && response.data.list && Array.isArray(response.data.list)) {
|
||||||
|
for (let i = 0; i < response.data.list.length; i++) {
|
||||||
|
const item = response.data.list[i]
|
||||||
|
tableData.push({
|
||||||
|
task_id: item.task_id || '-',
|
||||||
|
shop_name: item.shop_name || '-',
|
||||||
|
status: item.status,
|
||||||
|
complete: item.complete,
|
||||||
|
total: item.total,
|
||||||
|
complete_at: formatDateTime(item.complete_at), // 格式化完成时间
|
||||||
|
create_at: formatDateTime(item.create_at), // 格式化创建时间
|
||||||
|
fileUrl: item.file_url || '', // 假设后端返回文件URL字段
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 赋值给 dowCenterData
|
||||||
|
dowCenterData.value = tableData
|
||||||
|
// 更新总数(根据后端返回的实际total字段)
|
||||||
|
totalCount.value = response.data.total || 0
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取下载列表失败:', error)
|
||||||
|
ElMessage.error('获取下载列表失败,请重试')
|
||||||
|
dowCenterData.value = []
|
||||||
|
totalCount.value = 0
|
||||||
|
} finally {
|
||||||
|
downloadLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页大小改变时的处理
|
||||||
|
* @param val 新的每页条数
|
||||||
|
*/
|
||||||
|
const handleSizeChange = (val: number) => {
|
||||||
|
pageSize.value = val
|
||||||
|
currentPage.value = 1 // 重置到第一页
|
||||||
|
// 分页参数变化会自动触发 watch,重新获取数据
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前页改变时的处理
|
||||||
|
* @param val 新的页码
|
||||||
|
*/
|
||||||
|
const handleCurrentChange = (val: number) => {
|
||||||
|
currentPage.value = val
|
||||||
|
// 分页参数变化会自动触发 watch,重新获取数据
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件
|
||||||
|
* @param row 下载项数据
|
||||||
|
*/
|
||||||
|
const handleDownloadFile = async (row : DowList) => {
|
||||||
|
if (!row.fileUrl) {
|
||||||
|
ElMessage.warning('文件不存在或尚未生成')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success(`开始下载: ${row.task_id || '任务导出文件'}`)
|
||||||
|
|
||||||
|
// 构建完整的文件URL
|
||||||
|
const fileUrl = row.fileUrl.startsWith('http')
|
||||||
|
? row.fileUrl
|
||||||
|
: `${import.meta.env.VITE_APP_BASE_API}/api/${row.fileUrl}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 fetch 获取文件
|
||||||
|
const response = await fetch(fileUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('下载失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件名
|
||||||
|
let fileName = getFileNameFromUrl(fileUrl)
|
||||||
|
const contentDisposition = response.headers.get('content-disposition')
|
||||||
|
if (contentDisposition) {
|
||||||
|
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
|
||||||
|
if (match && match[1]) {
|
||||||
|
fileName = match[1].replace(/['"]/g, '')
|
||||||
|
fileName = decodeURIComponent(fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将响应转换为blob
|
||||||
|
const blob = await response.blob()
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = blobUrl
|
||||||
|
link.download = fileName
|
||||||
|
link.style.display = 'none'
|
||||||
|
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败:', error)
|
||||||
|
ElMessage.error('下载失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助函数:从URL中提取文件名
|
||||||
|
* @param url 文件URL
|
||||||
|
* @returns 文件名
|
||||||
|
*/
|
||||||
|
const getFileNameFromUrl = (url : string) => {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url, window.location.origin)
|
||||||
|
const pathname = urlObj.pathname
|
||||||
|
return pathname.substring(pathname.lastIndexOf('/') + 1) || 'download'
|
||||||
|
} catch (e) {
|
||||||
|
return 'download'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态对应的标签类型
|
||||||
|
* @param status - 状态码 (1:处理中, 2:已完成, 3:失败)
|
||||||
|
* @returns 标签类型
|
||||||
|
*/
|
||||||
|
const getStatusType = (status : number) => {
|
||||||
|
const typeMap : Record<number, string> = {
|
||||||
|
1: 'warning', // 处理中
|
||||||
|
2: 'success', // 已完成
|
||||||
|
3: 'danger' // 失败
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本
|
||||||
|
* @param status - 状态码
|
||||||
|
* @returns 状态文本
|
||||||
|
*/
|
||||||
|
const getStatusText = (status : number) => {
|
||||||
|
const textMap : Record<number, string> = {
|
||||||
|
1: '处理中',
|
||||||
|
2: '已完成',
|
||||||
|
3: '失败'
|
||||||
|
}
|
||||||
|
return textMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载进度条状态
|
||||||
|
* @param status - 状态码
|
||||||
|
* @returns 进度条状态
|
||||||
|
*/
|
||||||
|
const getDownloadProgressStatus = (status : number) => {
|
||||||
|
if (status === 2) return 'success' // 完成
|
||||||
|
if (status === 3) return 'exception' // 失败
|
||||||
|
return '' // 处理中
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听分页参数变化,重新请求数据
|
||||||
|
watch([currentPage, pageSize], () => {
|
||||||
|
// 只在弹窗打开时才请求数据
|
||||||
|
if (dialogVisible.value) {
|
||||||
|
getDownloadList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法给父组件调用
|
||||||
|
defineExpose({
|
||||||
|
openDialog
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-progress {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修正进度条文字使其可以纵向居中 */
|
||||||
|
>>> .el-progress-bar__innerText > span{
|
||||||
|
display: block;
|
||||||
|
height: 16px;
|
||||||
|
line-height: 12px;
|
||||||
|
color: #000;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
/* 修正进度条文字使其可以纵向居中 */
|
||||||
|
</style>
|
||||||
109
src/components/taskList/SearchBar.vue
Normal file
109
src/components/taskList/SearchBar.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
|
||||||
|
<div v-show="isShow" class="mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="localModelSubmit" :inline="true">
|
||||||
|
<el-form-item label="任务ID" prop="taskId">
|
||||||
|
<el-input v-model="localModelSubmit.taskId" placeholder="请输入任务ID" clearable @keyup.enter="handleSubmit" @update:modelValue="emitUpdate" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="任务类型" prop="taskType">
|
||||||
|
<el-select style="width: 160px;" v-model="localModelSubmit.taskType" placeholder="请选择任务类型" clearable @update:modelValue="emitUpdate">
|
||||||
|
<el-option label="请选择任务类型" value="" />
|
||||||
|
<el-option v-for="item in taskTypeList" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="店铺名称" prop="shopName">
|
||||||
|
<el-input v-model="localModelSubmit.shopName" placeholder="请输入店铺" clearable @keyup.enter="handleSubmit" @update:modelValue="emitUpdate" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="Search" @click="handleSubmit">搜索</el-button>
|
||||||
|
<el-button icon="Refresh" @click="handleResetQueryParams">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
|
||||||
|
// 定义类型接口
|
||||||
|
interface QueryParams {
|
||||||
|
taskId: string;
|
||||||
|
taskType: string;
|
||||||
|
shopName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: QueryParams // v-model 绑定的数据
|
||||||
|
isShow: boolean // 是否显示搜索栏
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 定义 emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: QueryParams): void // 更新外部数据
|
||||||
|
(e: 'search', value: QueryParams): void // 搜索事件
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// ==================== 本地数据 ====================
|
||||||
|
|
||||||
|
// 初始化 动画参数 动画: {搜索动画: {表示元素`进入`时要应用的 CSS 类名: 'animated fadeIn',表示元素`离开`时要应用的 CSS 类名: 'animated fadeOut'}
|
||||||
|
const animate = {searchAnimate: {enter: 'animated fadeIn',leave: 'animated fadeOut'}}
|
||||||
|
|
||||||
|
// 复制外部数据到本地,用于表单双向绑定
|
||||||
|
const localModelSubmit = ref<QueryParams>({ ...props.modelValue })
|
||||||
|
|
||||||
|
// 监听外部 modelValue 的变化,同步到本地(例如父组件重置时)
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
localModelSubmit.value = { ...newVal }
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// ==================== 本地数据 ====================
|
||||||
|
|
||||||
|
// ==================== 静态数据 ====================
|
||||||
|
// 任务类型字典
|
||||||
|
const taskTypeList = ref([
|
||||||
|
{ value: '1', label: '核价发布' },
|
||||||
|
{ value: '2', label: '表格发布' },
|
||||||
|
{ value: '3', label: '拉取商品' },
|
||||||
|
{ value: '4', label: '拉取商品详情' },
|
||||||
|
{ value: '5', label: '上下架、改库存、改价格' },
|
||||||
|
{ value: '6', label: '核价表格发布' },
|
||||||
|
{ value: '7', label: '增量库存' },
|
||||||
|
{ value: '8', label: '自营书品发布' },
|
||||||
|
{ value: '9', label: '核价改价' },
|
||||||
|
])
|
||||||
|
// ==================== 静态数据 ====================
|
||||||
|
|
||||||
|
// ==================== 方法组 ====================
|
||||||
|
|
||||||
|
// 当本地数据变化时,立即触发 update:modelValue 事件,实现双向绑定
|
||||||
|
const emitUpdate = () => {
|
||||||
|
emit('update:modelValue', { ...localModelSubmit.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法 重置 搜索条件
|
||||||
|
const handleResetQueryParams = () : void => {
|
||||||
|
localModelSubmit.value = {
|
||||||
|
taskId: '',
|
||||||
|
taskType: '',
|
||||||
|
shopName: ''
|
||||||
|
}
|
||||||
|
// 立即同步到外部
|
||||||
|
emitUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交搜索:触发 search 事件,传递当前表单数据
|
||||||
|
const handleSubmit = () => {
|
||||||
|
emit('search', { ...localModelSubmit.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 方法组 ====================
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
405
src/components/taskList/Table.vue
Normal file
405
src/components/taskList/Table.vue
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 主表格:任务列表 -->
|
||||||
|
<el-table v-loading="localLoading" :data="localModelTableData" row-key="id" border @selection-change="handleSelectionChange" :expand-row-keys="expandRowKeys" @row-click="handleRowClick" @cell-mouse-enter="handleCellMouseEnter" @cell-mouse-leave="handleCellMouseLeave">
|
||||||
|
<!-- 可展开行:任务详情 -->
|
||||||
|
<el-table-column type="expand">
|
||||||
|
<template #default="item">
|
||||||
|
<div style="margin: 0px 4px;background-color: var(--el-border-color-lighter);">
|
||||||
|
<el-collapse v-model="activeNames" style="padding: 10px;" accordion expand-icon-position="left" @change="handleChange">
|
||||||
|
<el-collapse-item v-if="isAdmin" :name="item.row.id + '-debuger'" >
|
||||||
|
<template #title>
|
||||||
|
<span>任务快照</span>
|
||||||
|
</template>
|
||||||
|
<div style="padding: 5px;border-top: 1px solid #ebeef5;">
|
||||||
|
<Debugger :task-id="item.row.id"></Debugger>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
<el-collapse-item :name="item.row.id + '-detail'">
|
||||||
|
<template #title>
|
||||||
|
<span>任务详情 - 任务ID:{{ item.row.id }}</span>
|
||||||
|
<el-tag size="small" type="info" class="m-l-10">任务总数:{{ item.row.totalCount || 0 }}</el-tag>
|
||||||
|
<el-tag size="small" type="success" class="m-l-10">正常:{{ item.row.successCount || 0 }}</el-tag>
|
||||||
|
<el-tag size="small" type="danger" class="m-l-10">错误:{{ item.row.errorCount || 0 }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<div style="padding: 5px;border-top: 1px solid #ebeef5;">
|
||||||
|
<Detail :task-id="item.row.id" :task-type="item.row.taskType"></Detail>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 表格列定义 -->
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column label="任务编码" align="center" prop="id" min-width="160" />
|
||||||
|
<el-table-column label="任务类型" align="center" prop="taskType" min-width="140">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ taskTypeToChinese(scope.row.taskType) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="店铺名称" align="left" min-width="200">
|
||||||
|
<template #default="item">
|
||||||
|
<el-image v-if="item.row.shopType === '1'" class="taskTableShopTypeImg" src="https://erp.buzhiyushu.cn/assets/pdd-BySt7T7H.png"></el-image>
|
||||||
|
<el-image v-else-if="item.row.shopType === '2'" class="taskTableShopTypeImg" src="https://erp.buzhiyushu.cn/assets/kfz-CRAXgFfE.png"></el-image>
|
||||||
|
<el-image v-else-if="item.row.shopType === '5'" class="taskTableShopTypeImg" src="https://erp.buzhiyushu.cn/assets/xy-D-YVxkSv.png"></el-image>
|
||||||
|
<el-image v-else class="taskTableShopTypeImg" src="/tb-8B6DW9L3.png"></el-image>
|
||||||
|
{{ item.row.shopName }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="执行进度" align="center" width="140">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-progress
|
||||||
|
:percentage="Math.round((parseInt(scope.row.overCount) / (parseInt(scope.row.totalCount || 0))) * 100) || 0"
|
||||||
|
:stroke-width="15" :text-inside="true"
|
||||||
|
:format="() => `${parseInt(scope.row.overCount || 0)}/${parseInt(scope.row.totalCount || 0)}`"
|
||||||
|
color="#67c23a" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="任务状态" align="center" min-width="160">
|
||||||
|
<template #default="item">
|
||||||
|
<!-- 暂停按钮 - 状态1显示且可点击 -->
|
||||||
|
<el-button v-if="item.row.taskStatus == '1'" size="small" class="taskTableStatus" type="success" :icon="VideoPlay" text title="暂停" @click="handlePause(item.row.id)">运行中...点击暂停</el-button>
|
||||||
|
<!-- 恢复按钮 - 状态2显示且可点击 -->
|
||||||
|
<el-button v-if="item.row.taskStatus == '2'" size="small" class="taskTableStatus" type="warning" :icon="VideoPause" text title="启动" @click="handleResume(item.row.id)">已暂停...点击恢复</el-button>
|
||||||
|
<!-- 终止 - 状态3显示 -->
|
||||||
|
<el-button v-if="item.row.taskStatus == '3'" size="small" class="taskTableStatus" type="danger" :icon="SwitchButton" text title="已终止">已终止</el-button>
|
||||||
|
<!-- 完成 - 状态4显示 -->
|
||||||
|
<el-button v-if="item.row.taskStatus == '4'" size="small" class="taskTableStatus" type="primary" :icon="Select" text title="已完成">已完成</el-button>
|
||||||
|
<!-- 推送中 - 状态10 显示 -->
|
||||||
|
<el-button v-if="item.row.taskStatus == '10'" size="small" class="taskTableStatus" type="primary" :icon="VideoPause" text title="推送中">推送中</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime" min-width="160">
|
||||||
|
<template #default="item"><span>{{ formatDate(item.row.createTime) }}</span></template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 操作列:单个任务的操作按钮 -->
|
||||||
|
<el-table-column label="操作" align="center" fixed="right" width="220">
|
||||||
|
<template #default="item">
|
||||||
|
<!--
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button class="taskTableHandle" size="small" type="danger" :disabled="item.row.taskStatus != 1" :icon="SwitchButton" text title="终止" @click="handleStop(item.row.id)">终止</el-button>
|
||||||
|
<el-button class="taskTableHandle" size="small" :icon="Delete" text title="删除" @click="handleDelete(item.row.id)">删除</el-button>
|
||||||
|
<el-button class="taskTableHandle" size="small" :icon="Notification" text title="导出" @click="handleExp(item.row.id,item.row.taskStatus)">导出</el-button>
|
||||||
|
</div> -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button size="small" type="warning" @click="handleStop(item.row.id)">终止</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="handleDelete(item.row.id)">删除</el-button>
|
||||||
|
<el-button size="small" type="info" @click="handleExp(item.row.id,item.row.taskStatus)">导出</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<el-pagination
|
||||||
|
style="margin-top: 10px;justify-content: flex-end;"
|
||||||
|
size="small"
|
||||||
|
v-model:current-page="localPagination.currentPage"
|
||||||
|
v-model:page-size="localPagination.pageSize"
|
||||||
|
:page-sizes="paginationConfig.sizes"
|
||||||
|
:disabled="localLoading"
|
||||||
|
background
|
||||||
|
:layout="paginationConfig.layout"
|
||||||
|
:total="localPagination.total"
|
||||||
|
@current-change="handlePaginationCurrentPageChange"
|
||||||
|
@size-change="handlePaginationPageSizeChange"
|
||||||
|
/>
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
|
||||||
|
<!-- 全局提示组件 -->
|
||||||
|
<el-tooltip
|
||||||
|
:visible="tooltipVisible"
|
||||||
|
:content="tooltipContent"
|
||||||
|
placement="top"
|
||||||
|
:show-after="200"
|
||||||
|
:hide-after="0"
|
||||||
|
:popper-class="'xy-task-tooltip'"
|
||||||
|
:teleported="true"
|
||||||
|
effect="dark"
|
||||||
|
>
|
||||||
|
<div style="position: fixed; pointer-events: none; z-index: 9999;" :style="tooltipStyle"></div>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { VideoPause, VideoPlay, SwitchButton, Delete, Notification, Select } from '@element-plus/icons-vue'
|
||||||
|
import Detail from '../../components/taskList/Detail.vue'
|
||||||
|
import Debugger from '../../components/taskList/Debugger.vue'
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
|
||||||
|
// 定义类型接口什
|
||||||
|
interface TaskList {
|
||||||
|
id : string; // id
|
||||||
|
taskType : string; // 任务类型
|
||||||
|
shopName : string; // 店铺名称
|
||||||
|
shopType : string; // 店铺类型
|
||||||
|
taskStatus : string; // 任务状态
|
||||||
|
|
||||||
|
waitCount : string; // 等待数量
|
||||||
|
successCount : string; // 成功数量
|
||||||
|
errorCount : string; // 错误数量
|
||||||
|
totalCount : string; // 总计数量
|
||||||
|
|
||||||
|
createTime : string; // 创建时间
|
||||||
|
bookDetail : string; // 书籍详情
|
||||||
|
isExport: string, // 是否导出
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
interface Pagination {
|
||||||
|
currentPage : number // 当前页
|
||||||
|
pageSize : number // 页大小
|
||||||
|
total : number // 总数
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps<{
|
||||||
|
tableData: TaskList[] // v-model 绑定的数据
|
||||||
|
loading : boolean
|
||||||
|
pagination : Pagination // 分页组件
|
||||||
|
isAdmin?: boolean // 是否为管理员模式
|
||||||
|
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 定义 emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'pause', value: string): void // 暂停
|
||||||
|
(e: 'resume', value: string): void // 恢复
|
||||||
|
(e: 'stop', value: string): void // 终止
|
||||||
|
(e: 'delete', value: string): void // 删除
|
||||||
|
(e: 'exp', value: string): void // 导出
|
||||||
|
(e: 'paginationCurrentPageChange', value: number): void // 修改分页的当前页
|
||||||
|
(e: 'paginationPageSizeChange', value: number): void // 修改分页的每页大小
|
||||||
|
(e: 'selectionChange', value: TaskList[]): void // 新增:选中项变化事件
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 提示相关状态
|
||||||
|
const tooltipVisible = ref(false)
|
||||||
|
const tooltipContent = ref('')
|
||||||
|
const tooltipStyle = ref({ top: '0px', left: '0px' })
|
||||||
|
let hoverTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
// 闲鱼店铺提示内容
|
||||||
|
const xyTipContent = 'Tip:闲鱼店铺拉取商品任务进度条数量为闲管家中所有店铺的销售中商品总和,先获取到所有商品后再归纳到ERP的各个店铺商品中。'
|
||||||
|
|
||||||
|
// 检查是否需要显示提示
|
||||||
|
const shouldShowTip = (row: TaskList): boolean => {
|
||||||
|
return row.shopType === '5' && row.taskType === '3'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标进入单元格
|
||||||
|
const handleCellMouseEnter = (row: TaskList, column: any, cell: HTMLElement, event: MouseEvent) => {
|
||||||
|
|
||||||
|
if (!shouldShowTip(row)) return
|
||||||
|
|
||||||
|
console.log("进入单元格!")
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (hoverTimer) {
|
||||||
|
clearTimeout(hoverTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟显示提示,避免快速移动时闪烁
|
||||||
|
hoverTimer = setTimeout(() => {
|
||||||
|
const rect = cell.getBoundingClientRect()
|
||||||
|
tooltipStyle.value = {
|
||||||
|
top: `${rect.top + window.scrollY}px`,
|
||||||
|
left: `${rect.left + window.scrollX}px`,
|
||||||
|
width: `${rect.width}px`,
|
||||||
|
height: `${rect.height}px`
|
||||||
|
}
|
||||||
|
tooltipContent.value = xyTipContent
|
||||||
|
tooltipVisible.value = true
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标离开单元格
|
||||||
|
const handleCellMouseLeave = () => {
|
||||||
|
if (hoverTimer) {
|
||||||
|
clearTimeout(hoverTimer)
|
||||||
|
hoverTimer = null
|
||||||
|
}
|
||||||
|
tooltipVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制外部数据到本地,用于表单双向绑定
|
||||||
|
const localModelTableData = ref<TaskList[]>(Array.isArray(props.tableData) ? [...props.tableData] : [])
|
||||||
|
// 监听外部 modelValue 的变化,同步到本地(例如父组件重置时)
|
||||||
|
watch(() => props.tableData, (newVal : TaskList[]) => {
|
||||||
|
localModelTableData.value = Array.isArray(newVal) ? [ ...newVal ] : []
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 复制外部数据到本地,用于表单双向绑定
|
||||||
|
const localLoading = ref<boolean>(props.loading)
|
||||||
|
// 监听外部 modelValue 的变化,同步到本地(例如父组件重置时)
|
||||||
|
watch(() => props.loading, (newVal : boolean) => {
|
||||||
|
localLoading.value = newVal
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
|
||||||
|
// 复制外部数据到本地,用于表单双向绑定
|
||||||
|
const localPagination = ref<Pagination>({ ...props.pagination })
|
||||||
|
// 监听外部 modelValue 的变化,同步到本地(例如父组件重置时)
|
||||||
|
watch(() => props.pagination, (newVal : Pagination) => {
|
||||||
|
localPagination.value = { ...newVal }
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 是否为管理员模式
|
||||||
|
const isAdmin = ref<boolean>(props.isAdmin || false)
|
||||||
|
// 监听外部 isAdmin 的变化
|
||||||
|
watch(() => props.isAdmin, (newVal : boolean) => {
|
||||||
|
isAdmin.value = newVal
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeNames = ref([])
|
||||||
|
|
||||||
|
const handleChange = (val: CollapseModelValue) => {
|
||||||
|
console.log(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页组件配置
|
||||||
|
const paginationConfig = ref({
|
||||||
|
sizes:[10, 20, 50, 100],
|
||||||
|
layout:"total, sizes, prev, pager, next, jumper"
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加展开行的控制
|
||||||
|
const expandRowKeys = ref<string[]>([])
|
||||||
|
// ==================== 方法组 ====================
|
||||||
|
// 处理选中项变化
|
||||||
|
const handleSelectionChange = (selection: TaskList[]) => {
|
||||||
|
emit('selectionChange', selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂停
|
||||||
|
const handlePause = (id: string) => { emit('pause',id) }
|
||||||
|
|
||||||
|
// 恢复
|
||||||
|
const handleResume = (id: string) => { emit('resume',id) }
|
||||||
|
|
||||||
|
// 停止
|
||||||
|
const handleStop = (id: string) => { emit('stop',id) }
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = (id: string) => { emit('delete',id) }
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
const handleExp = (id: string) => { emit('exp',id) }
|
||||||
|
|
||||||
|
// 修改分页的当前页
|
||||||
|
const handlePaginationCurrentPageChange = (val: number) => { emit('paginationCurrentPageChange',val) }
|
||||||
|
|
||||||
|
// 修改分页的每页大小
|
||||||
|
const handlePaginationPageSizeChange = (val: number) => { emit('paginationPageSizeChange',val) }
|
||||||
|
|
||||||
|
// 处理行点击展开/收起
|
||||||
|
const handleRowClick = (row: TaskList) => {
|
||||||
|
const index = expandRowKeys.value.indexOf(row.id)
|
||||||
|
if (index === -1) {
|
||||||
|
// 如果当前行未展开,则展开它(如果之前有其他展开的行,需要根据需求决定是否只保留一个)
|
||||||
|
expandRowKeys.value = [row.id]
|
||||||
|
} else {
|
||||||
|
// 如果已展开,则收起
|
||||||
|
expandRowKeys.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具函数 ====================
|
||||||
|
const taskTypeToChinese = (taskType : string) => {
|
||||||
|
switch(taskType){
|
||||||
|
case '1': return '核价发布';
|
||||||
|
case '2': return '表格发布';
|
||||||
|
case '3': return '拉取商品';
|
||||||
|
case '4': return '拉取商品详情';
|
||||||
|
case '5': return '上下架、改库存、改价格';
|
||||||
|
case '6': return '核价表格发布';
|
||||||
|
case '7': return '增量库存';
|
||||||
|
case '8': return '自营书品发布';
|
||||||
|
case '9': return '核价改价';
|
||||||
|
default: return '未知分类'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期时间戳为可读字符串
|
||||||
|
* @param {number|string} date - 日期时间戳(秒级或毫秒级)
|
||||||
|
* @returns {string} 格式化后的日期时间,格式:YYYY-MM-DD HH:mm:ss
|
||||||
|
*/
|
||||||
|
const formatDate = (date: string) => {
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
const timestamp = typeof date === 'string' ? parseInt(date, 10) : date;
|
||||||
|
const dateObj = new Date(timestamp < 1e12 ? timestamp * 1000 : timestamp);
|
||||||
|
|
||||||
|
const year = dateObj.getFullYear();
|
||||||
|
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(dateObj.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(dateObj.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(dateObj.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(dateObj.getSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.m-l-10{
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
/* 任务状态按钮-样式 */
|
||||||
|
>>> .el-button.taskTableStatus{
|
||||||
|
padding: 0px !important;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
>>> .el-button.taskTableStatus > span{
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
/* 任务状态按钮-样式 */
|
||||||
|
|
||||||
|
|
||||||
|
/* 任务店铺类型-样式 */
|
||||||
|
>>>.el-image.taskTableShopTypeImg{
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: clip;
|
||||||
|
}
|
||||||
|
/* 任务店铺类型-样式 */
|
||||||
|
|
||||||
|
/* 操作按钮-样式 */
|
||||||
|
>>> .el-button.taskTableHandle{
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 0px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
>>> .el-button.taskTableHandle > span{
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
/* 操作按钮-样式 */
|
||||||
|
|
||||||
|
/* 修正进度条文字使其可以纵向居中 */
|
||||||
|
>>> .el-progress-bar__innerText > span{
|
||||||
|
display: block;
|
||||||
|
height: 16px;
|
||||||
|
line-height: 12px;
|
||||||
|
color: #000;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
/* 修正进度条文字使其可以纵向居中 */
|
||||||
|
/* 全局样式 - 提示框样式 */
|
||||||
|
.xy-task-tooltip {
|
||||||
|
max-width: 400px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
303
src/components/taskList/Tools.vue
Normal file
303
src/components/taskList/Tools.vue
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
<template>
|
||||||
|
<div class="m-t-b-10">
|
||||||
|
<el-row :gutter="10" align="middle">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-button type="success" plain @click="handleBatchRun">运行</el-button>
|
||||||
|
<el-button type="warning" plain @click="handleBatchPause">暂停</el-button>
|
||||||
|
<el-button type="danger" plain @click="handleBatchStop">终止</el-button>
|
||||||
|
<el-button type="primary" plain @click="handleDowCenter">下载中心</el-button>
|
||||||
|
<el-button type="danger" plain @click="handleDelCenter">删除中心</el-button>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="header-right">
|
||||||
|
<!-- 服务状态显示 -->
|
||||||
|
<div v-if="isAdmin && isShowAliveService" class="service-status-container">
|
||||||
|
<div class="service-status">
|
||||||
|
<div v-for="item in aliveServices" :key="item.name" class="service-item">
|
||||||
|
<span class="service-name">{{ item.name }}</span>
|
||||||
|
<span class="service-times">[{{ item.times }}]</span>
|
||||||
|
<span class="service-dot" :class="getStatusDotClass(item.status)"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="simple-toolbar">
|
||||||
|
<el-tooltip content="刷新" placement="top"><el-button icon="Refresh" circle plain @click="handleRefresh"/></el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { GetAlive } from '../../api/shopTask.js'
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps<{
|
||||||
|
isShowAliveService: boolean // 是否显示搜索栏
|
||||||
|
isAdmin?: boolean // 是否为管理员模式
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 定义 emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'batchRun'): void // 批量运行事件
|
||||||
|
(e: 'batchPause'): void // 批量暂停事件
|
||||||
|
(e: 'batchStop'): void // 批量停止事件
|
||||||
|
(e: 'refresh'): void // 刷新
|
||||||
|
(e: 'dowCenter'): void // 下载中心事件
|
||||||
|
(e: 'delCenter'): void // 删除中心事件
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 服务状态列表 */
|
||||||
|
const aliveServices = ref<Array<{ name : string, status : number, times : number, msg? : string }>>([])
|
||||||
|
|
||||||
|
// 定时器引用
|
||||||
|
let pollingInterval: number | null = null
|
||||||
|
|
||||||
|
// 记录已显示过的消息
|
||||||
|
const shownMessages = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务存活状态
|
||||||
|
*/
|
||||||
|
const getAliveList = async () : Promise<void> => {
|
||||||
|
try {
|
||||||
|
const ret = await GetAlive()
|
||||||
|
if (ret.code === "200") {
|
||||||
|
// 初始化
|
||||||
|
const data = ret.data || []
|
||||||
|
// 排序后装入
|
||||||
|
aliveServices.value = [...data].sort((a, b) => a.name.length - b.name.length)
|
||||||
|
|
||||||
|
// 检查是否有服务的 msg 不为空
|
||||||
|
const servicesWithMsg = data.filter((service: any) =>
|
||||||
|
service.msg && service.msg.trim() !== ''
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果有服务包含消息,处理弹窗
|
||||||
|
if (servicesWithMsg.length > 0) {
|
||||||
|
const newMessages: Array<{name: string, msg: string, status: number}> = []
|
||||||
|
const currentMessages = new Set<string>()
|
||||||
|
|
||||||
|
servicesWithMsg.forEach((service: any) => {
|
||||||
|
const messageKey = `${service.name}_${service.msg}`
|
||||||
|
currentMessages.add(messageKey)
|
||||||
|
|
||||||
|
// 如果是新消息,记录下来
|
||||||
|
if (!shownMessages.value.has(messageKey)) {
|
||||||
|
newMessages.push({
|
||||||
|
name: service.name,
|
||||||
|
msg: service.msg,
|
||||||
|
status: service.status
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新已显示消息集合
|
||||||
|
shownMessages.value = currentMessages
|
||||||
|
|
||||||
|
// 如果有新消息,弹窗显示
|
||||||
|
if (newMessages.length > 0) {
|
||||||
|
// 构建弹窗内容
|
||||||
|
const messageContent = newMessages.map(service =>
|
||||||
|
`【${service.name}】: ${service.msg}`
|
||||||
|
).join('\n')
|
||||||
|
|
||||||
|
// 根据状态确定消息类型
|
||||||
|
const hasError = newMessages.some(s => s.status === 2)
|
||||||
|
const hasWarning = newMessages.some(s => s.status === 1)
|
||||||
|
let messageType: 'warning' | 'error' | 'info' = 'info'
|
||||||
|
if (hasError) messageType = 'error'
|
||||||
|
else if (hasWarning) messageType = 'warning'
|
||||||
|
|
||||||
|
// 使用消息框弹窗
|
||||||
|
ElMessageBox.alert(messageContent, '服务状态提醒', {
|
||||||
|
confirmButtonText: '知道了',
|
||||||
|
type: messageType,
|
||||||
|
dangerouslyUseHTMLString: false
|
||||||
|
}).catch(() => {}) // 忽略关闭时的错误
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有服务有消息,清空记录(下次相同消息会重新弹窗)
|
||||||
|
shownMessages.value.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取存活状态错误')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务状态点样式类
|
||||||
|
* @param status 状态码
|
||||||
|
* @returns 样式类名
|
||||||
|
*/
|
||||||
|
const getStatusDotClass = (status : number) : string => {
|
||||||
|
const classMap : Record<number, string> = {
|
||||||
|
0: 'status-dot-normal', // 正常
|
||||||
|
1: 'status-dot-warning', // 警告
|
||||||
|
2: 'status-dot-error' // 错误
|
||||||
|
}
|
||||||
|
return classMap[status] || 'status-dot-offline'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动服务状态轮询
|
||||||
|
*/
|
||||||
|
const startServiceStatusPolling = () : void => {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (pollingInterval !== null) {
|
||||||
|
clearInterval(pollingInterval)
|
||||||
|
pollingInterval = null
|
||||||
|
}
|
||||||
|
// 获取服务存活状态
|
||||||
|
getAliveList()
|
||||||
|
// 装入定时器
|
||||||
|
pollingInterval = window.setInterval(() => {
|
||||||
|
getAliveList()
|
||||||
|
}, 10000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止服务状态轮询
|
||||||
|
*/
|
||||||
|
const stopServiceStatusPolling = () : void => {
|
||||||
|
if (pollingInterval !== null) {
|
||||||
|
clearInterval(pollingInterval)
|
||||||
|
pollingInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== 方法组 ====================
|
||||||
|
|
||||||
|
// 批量运行
|
||||||
|
const handleBatchRun = () => {
|
||||||
|
emit('batchRun')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量暂停
|
||||||
|
const handleBatchPause = () => {
|
||||||
|
emit('batchPause')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量停止
|
||||||
|
const handleBatchStop = () => {
|
||||||
|
emit('batchStop')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
const handleRefresh = () => {
|
||||||
|
emit('refresh')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载中心
|
||||||
|
const handleDowCenter = () =>{
|
||||||
|
emit('dowCenter')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除中心
|
||||||
|
const handleDelCenter = () =>{
|
||||||
|
emit('delCenter')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 方法组 ====================
|
||||||
|
|
||||||
|
// 监听 isAdmin 和 isShowAliveService 的变化
|
||||||
|
watch(() => [props.isAdmin, props.isShowAliveService], ([newIsAdmin, newIsShowAliveService]) => {
|
||||||
|
if (newIsAdmin && newIsShowAliveService) {
|
||||||
|
startServiceStatusPolling()
|
||||||
|
} else {
|
||||||
|
stopServiceStatusPolling()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onMounted(async () => {
|
||||||
|
if (props.isAdmin && props.isShowAliveService) {
|
||||||
|
startServiceStatusPolling()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清除定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopServiceStatusPolling()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.m-t-b-10{
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
服务状态样式
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.service-status-container {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #409eff;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-times {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态点颜色 */
|
||||||
|
.status-dot-normal {
|
||||||
|
background-color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot-warning {
|
||||||
|
background-color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot-error {
|
||||||
|
background-color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot-offline {
|
||||||
|
background-color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
服务状态样式
|
||||||
|
============================================================================ */
|
||||||
|
</style>
|
||||||
28
src/main.js
Normal file
28
src/main.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// src/main.js
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
import store from './store'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 注册 Element Plus 图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用路由插件
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
// 使用 Element Plus 插件
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn
|
||||||
|
})
|
||||||
|
|
||||||
|
app.provide('store', store)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
1793
src/page/shopTask.vue
Normal file
1793
src/page/shopTask.vue
Normal file
File diff suppressed because it is too large
Load Diff
581
src/page/test.vue
Normal file
581
src/page/test.vue
Normal file
@ -0,0 +1,581 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 搜索条 -->
|
||||||
|
<SearchBar v-model="searchForm" :isShow="true" @search="searchFunc"></SearchBar>
|
||||||
|
<!-- 搜索条 -->
|
||||||
|
|
||||||
|
<div class="mb-[10px]">
|
||||||
|
<h6 style="color: red;">Tip:1、闲鱼如果发送其他类目可能会出现无法获取到isbn的情况,会影响去重复、违规检测。2、拉取任务进度条数量一般会比实际任务数大一些,以ERP店铺商品中实际的数量为准。3、闲鱼店铺只能拉取到半年内更新过的商品</h6>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工具条 -->
|
||||||
|
<Tools :isAdmin="urlType === 'admin'" :isShowAliveService="true" @batchRun="batchRunFunc" @batchPause="batchPauseFunc" @batchStop="batchStopFunc" @refresh="refreshFunc" @dowCenter="dowCenterFunc" @delCenter="delCenterFunc"></Tools>
|
||||||
|
<!-- 工具条 -->
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<TaskTable :isAdmin="urlType === 'admin'" :loading="tableLoading" :tableData="taskList" :pagination="pagination" @pause="pauseFunc" @resume="resumeFunc" @stop="stopFunc" @delete="deleteFunc" @exp="expFunc" @paginationCurrentPageChange="paginationCurrentPageChangeFunc" @paginationPageSizeChange="paginationPageSizeChangeFunc" @selectionChange="handleSelectionChange"></TaskTable>
|
||||||
|
<!-- 表格 -->
|
||||||
|
|
||||||
|
<!-- 下载中心 -->
|
||||||
|
<DowCenter ref="dowCenterRef" :currentUserId="currentUserId" :isAdmin="urlType === 'admin'"></DowCenter>
|
||||||
|
<!-- 下载中心 -->
|
||||||
|
|
||||||
|
<!-- 删除中心 -->
|
||||||
|
<DelCenter ref="delCenterRef" :currentUserId="currentUserId" :isAdmin="urlType === 'admin'"></DelCenter>
|
||||||
|
<!-- 删除中心 -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import SearchBar from '../components/taskList/SearchBar.vue'
|
||||||
|
import Tools from '../components/taskList/Tools.vue'
|
||||||
|
import TaskTable from '../components/taskList/Table.vue'
|
||||||
|
import DowCenter from '../components/taskList/DowCenter.vue'
|
||||||
|
import DelCenter from '../components/taskList/DelCenter.vue'
|
||||||
|
import { GetTaskAdmin, ExportTaskDetailByTaskId, StopTask, DelTaskDetailByTaskId, PauseTask, ResumeTask, GetTask } from '../api/shopTask.js'
|
||||||
|
import { GetUserInfo } from '../api/user.js'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import store from '../store/index.js'
|
||||||
|
|
||||||
|
// ==================== 引入 ====================
|
||||||
|
|
||||||
|
// 定义类型接口
|
||||||
|
interface SearchForm {
|
||||||
|
taskId: string;
|
||||||
|
taskType: string;
|
||||||
|
shopName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义响应式表单数据
|
||||||
|
const searchForm = ref<SearchForm>({
|
||||||
|
taskId: '',
|
||||||
|
taskType: '',
|
||||||
|
shopName: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格的 loading 参数
|
||||||
|
const tableLoading = ref(false)
|
||||||
|
|
||||||
|
// 存储选中的任务
|
||||||
|
const selectedTasks = ref<TaskList[]>([])
|
||||||
|
|
||||||
|
// 下载中心组件引用
|
||||||
|
const dowCenterRef = ref<InstanceType<typeof DowCenter>>()
|
||||||
|
|
||||||
|
// 删除中心组件引用
|
||||||
|
const delCenterRef = ref<InstanceType<typeof DelCenter>>()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 从URL参数中获取urlType
|
||||||
|
const urlType = route.query.type as string || ''
|
||||||
|
|
||||||
|
// 从URL参数中获取token
|
||||||
|
const urlToken = route.query.token as string || ''
|
||||||
|
|
||||||
|
// 存储当前登录用户ID
|
||||||
|
const currentUserId = ref('')
|
||||||
|
|
||||||
|
|
||||||
|
// ============ 测试数据 ==============
|
||||||
|
|
||||||
|
// 定义类型接口
|
||||||
|
interface TaskList {
|
||||||
|
id : string; // id
|
||||||
|
taskType : string; // 任务类型
|
||||||
|
shopName : string; // 店铺名称
|
||||||
|
shopType : string; // 店铺类型
|
||||||
|
taskStatus : string; // 任务状态
|
||||||
|
|
||||||
|
waitCount : string; // 等待数量
|
||||||
|
successCount : string; // 成功数量
|
||||||
|
errorCount : string; // 错误数量
|
||||||
|
totalCount : string; // 总计数量
|
||||||
|
overCount : string; // 完成数量
|
||||||
|
|
||||||
|
createTime : string; // 创建时间
|
||||||
|
bookDetail : string; // 书籍详情
|
||||||
|
isExport: string, // 是否导出
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
interface Pagination {
|
||||||
|
currentPage : number // 当前页
|
||||||
|
pageSize : number // 页大小
|
||||||
|
total : number // 总数
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义 - 响应式表单数据
|
||||||
|
const taskList = ref<TaskList[]>([
|
||||||
|
{
|
||||||
|
id : "1", // id
|
||||||
|
taskType : "PDD_GOODS_RELEASR", // 任务类型
|
||||||
|
shopName : "测试店铺", // 店铺名称
|
||||||
|
shopType : "0",
|
||||||
|
taskStatus : "1", // 任务状态
|
||||||
|
|
||||||
|
waitCount : "100", // 等待数量
|
||||||
|
successCount : "90", // 成功数量
|
||||||
|
errorCount : "10", // 错误数量
|
||||||
|
totalCount : "250", // 总计数量
|
||||||
|
overCount :"10", //完成数量
|
||||||
|
|
||||||
|
createTime : "0", // 创建时间
|
||||||
|
bookDetail : "", // 书籍详情
|
||||||
|
isExport: "0", // 是否导出
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 定义 - 分页数据
|
||||||
|
const pagination = ref<Pagination>({
|
||||||
|
currentPage : 1, // 当前页
|
||||||
|
pageSize : 10, // 页大小
|
||||||
|
total : 0, // 总数
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理表格选中项变化
|
||||||
|
const handleSelectionChange = (selection: TaskList[]) => {
|
||||||
|
selectedTasks.value = selection
|
||||||
|
console.log('选中的任务:', selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 测试数据 ==============
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const searchFunc = (params) : void =>{
|
||||||
|
pagination.value.currentPage = 1 // 搜索时重置到第一页
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量操作-运行
|
||||||
|
const batchRunFunc = async () => {
|
||||||
|
if (selectedTasks.value.length === 0) {
|
||||||
|
ElMessage.warning('请先选择要运行的任务')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要运行选中的 ${selectedTasks.value.length} 个任务吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedIds = selectedTasks.value.map(task => task.id)
|
||||||
|
let successCount = 0
|
||||||
|
let failCount = 0
|
||||||
|
|
||||||
|
// 开始批量运行(注意:你需要有对应的运行API)
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
try {
|
||||||
|
// 假设你有 ResumeTask 或专门的运行接口
|
||||||
|
const ret = await ResumeTask(id) // 或者使用其他运行接口
|
||||||
|
if (ret.code === "200") {
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
failCount++
|
||||||
|
console.error(`运行任务 ${id} 失败: ${ret.msg}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failCount++
|
||||||
|
console.error(`运行任务 ${id} 失败:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
ElMessage.success(`成功运行 ${successCount} 个任务${failCount > 0 ? `,${failCount} 个失败` : ''}`)
|
||||||
|
} else if (failCount > 0) {
|
||||||
|
ElMessage.error(`运行失败,${failCount} 个任务操作失败`)
|
||||||
|
}
|
||||||
|
|
||||||
|
getTaskList()
|
||||||
|
selectedTasks.value = []
|
||||||
|
|
||||||
|
} catch (error : any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('批量运行操作失败,请重试')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量操作-暂停
|
||||||
|
const batchPauseFunc = async () => {
|
||||||
|
if (selectedTasks.value.length === 0) {
|
||||||
|
ElMessage.warning('请先选择要暂停的任务')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要暂停选中的 ${selectedTasks.value.length} 个任务吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedIds = selectedTasks.value.map(task => task.id)
|
||||||
|
let successCount = 0
|
||||||
|
let failCount = 0
|
||||||
|
|
||||||
|
// 开始批量暂停
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
try {
|
||||||
|
const ret = await PauseTask(id)
|
||||||
|
if (ret.code === "200") {
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
failCount++
|
||||||
|
console.error(`暂停任务 ${id} 失败: ${ret.msg}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failCount++
|
||||||
|
console.error(`暂停任务 ${id} 失败:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示批量操作结果
|
||||||
|
if (successCount > 0) {
|
||||||
|
ElMessage.success(`成功暂停 ${successCount} 个任务${failCount > 0 ? `,${failCount} 个失败` : ''}`)
|
||||||
|
} else if (failCount > 0) {
|
||||||
|
ElMessage.error(`暂停失败,${failCount} 个任务操作失败`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新列表并清空选中状态
|
||||||
|
getTaskList()
|
||||||
|
selectedTasks.value = []
|
||||||
|
|
||||||
|
} catch (error : any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('批量暂停操作失败,请重试')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量操作-停止
|
||||||
|
const batchStopFunc = async () => {
|
||||||
|
if (selectedTasks.value.length === 0) {
|
||||||
|
ElMessage.warning('请先选择要停止的任务')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要停止选中的 ${selectedTasks.value.length} 个任务吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedIds = selectedTasks.value.map(task => task.id)
|
||||||
|
let successCount = 0
|
||||||
|
let failCount = 0
|
||||||
|
|
||||||
|
// 开始批量停止
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
try {
|
||||||
|
const ret = await StopTask(id)
|
||||||
|
if (ret.code === "200") {
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
failCount++
|
||||||
|
console.error(`停止任务 ${id} 失败: ${ret.msg}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failCount++
|
||||||
|
console.error(`停止任务 ${id} 失败:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
ElMessage.success(`成功停止 ${successCount} 个任务${failCount > 0 ? `,${failCount} 个失败` : ''}`)
|
||||||
|
} else if (failCount > 0) {
|
||||||
|
ElMessage.error(`停止失败,${failCount} 个任务操作失败`)
|
||||||
|
}
|
||||||
|
|
||||||
|
getTaskList()
|
||||||
|
selectedTasks.value = []
|
||||||
|
|
||||||
|
} catch (error : any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('批量停止操作失败,请重试')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
const refreshFunc = () => {
|
||||||
|
getTaskList() // 刷新时重新获取数据
|
||||||
|
selectedTasks.value = [] // 清空选中状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂停
|
||||||
|
const pauseFunc = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要暂停任务 ${id} 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const ret = await PauseTask(id)
|
||||||
|
if (ret.code !== "200") {
|
||||||
|
ElMessage.error(`暂停任务失败: ${ret.msg}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.success('任务暂停成功')
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
} catch (error : any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('暂停任务失败,请重试')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复
|
||||||
|
const resumeFunc = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要恢复任务 ${id} 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const ret = await ResumeTask(id)
|
||||||
|
if (ret.code !== "200") {
|
||||||
|
ElMessage.error(`恢复任务失败: ${ret.msg}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.success('任务恢复成功')
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
} catch (error : any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('恢复任务失败,请重试')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 终止
|
||||||
|
const stopFunc = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要终止任务 ${id} 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const ret = await StopTask(id)
|
||||||
|
if (ret.code !== "200") {
|
||||||
|
ElMessage.error(`终止任务失败: ${ret.msg}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.success('任务终止成功')
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
} catch (error : any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('终止任务失败,请重试')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const deleteFunc = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要删除任务 ${id} 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const ret = await DelTaskDetailByTaskId(id)
|
||||||
|
if (ret.code !== "200") {
|
||||||
|
ElMessage.error(`删除任务失败: ${ret.msg}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.success('任务删除成功')
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
} catch (error : any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('删除任务失败,请重试')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
const expFunc = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要导出任务 ${id} 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const ret = await ExportTaskDetailByTaskId(id)
|
||||||
|
if (ret.code !== "200") {
|
||||||
|
ElMessage.error(`导出任务失败: ${ret.msg}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.success('导出任务成功')
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
} catch (error : any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('任务导出失败,请重试')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载中心
|
||||||
|
const dowCenterFunc = (id) => {
|
||||||
|
console.log('父组件-触发-下载中心-方法')
|
||||||
|
// 调用子组件方法打开弹窗
|
||||||
|
if (dowCenterRef.value) {
|
||||||
|
dowCenterRef.value.openDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除中心
|
||||||
|
const delCenterFunc = (id) => {
|
||||||
|
console.log('父组件-触发-删除中心-方法')
|
||||||
|
// 调用子组件方法打开弹窗
|
||||||
|
if (delCenterRef.value) {
|
||||||
|
delCenterRef.value.openDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改分页的当前页
|
||||||
|
const paginationCurrentPageChangeFunc = (page) => {
|
||||||
|
pagination.value.currentPage = page // 更新当前页
|
||||||
|
selectedTasks.value = [] // 清空选中状态
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改分页的每页大小
|
||||||
|
const paginationPageSizeChangeFunc = (pageSize) => {
|
||||||
|
pagination.value.pageSize = pageSize // 关键:更新每页大小
|
||||||
|
pagination.value.currentPage = 1 // 改变每页大小时重置到第一页
|
||||||
|
selectedTasks.value = [] // 清空选中状态
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============= 启动函数 ============= //
|
||||||
|
onMounted(async () => {
|
||||||
|
//判断是否admin
|
||||||
|
if(urlType == "admin"){
|
||||||
|
getTaskList()
|
||||||
|
}else{
|
||||||
|
// 先获取用户信息,再加载任务列表
|
||||||
|
const success = await getUserInfoFromToken()
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 初始化用户信息 ====================
|
||||||
|
/**
|
||||||
|
* 从token获取用户信息
|
||||||
|
*/
|
||||||
|
const getUserInfoFromToken = async () => {
|
||||||
|
// 如果没有token,跳转到登录页
|
||||||
|
if (!urlToken) {
|
||||||
|
ElMessage.error('未找到token,请重新登录')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 保存token到localStorage和store
|
||||||
|
localStorage.setItem('token', urlToken)
|
||||||
|
|
||||||
|
// 如果store有setToken方法,也设置到store中
|
||||||
|
if (store && typeof store.commit === 'function') {
|
||||||
|
store.commit('user/SET_TOKEN', urlToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用接口获取用户信息
|
||||||
|
const response = await GetUserInfo(urlToken)
|
||||||
|
|
||||||
|
const userId = response.replace(/^"|"$/g, '')
|
||||||
|
|
||||||
|
// 保存userId到响应式变量
|
||||||
|
currentUserId.value = userId
|
||||||
|
|
||||||
|
// 保存userId到store
|
||||||
|
if (store && typeof store.commit === 'function') {
|
||||||
|
store.commit('user/SET_USER_ID', userId)
|
||||||
|
store.commit('user/SET_USER_INFO', userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error)
|
||||||
|
ElMessage.error('获取用户信息失败,请重新登录')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 主要函数 ============= //
|
||||||
|
/**
|
||||||
|
* 获取任务列表(包装函数)
|
||||||
|
*/
|
||||||
|
const getTaskList = async () => {
|
||||||
|
tableLoading.value = true
|
||||||
|
|
||||||
|
// 调用 - 获取列表接口
|
||||||
|
if(urlType == "admin"){
|
||||||
|
var req = await GetTaskAdmin(pagination.value.currentPage,pagination.value.pageSize,searchForm.value.taskId,searchForm.value.taskType,searchForm.value.shopName)
|
||||||
|
}else{
|
||||||
|
var req = await GetTask(pagination.value.currentPage,pagination.value.pageSize,currentUserId.value,searchForm.value.taskId,searchForm.value.taskType,searchForm.value.shopName)
|
||||||
|
}
|
||||||
|
try{
|
||||||
|
if (req.code == "200") {
|
||||||
|
// 重置 表格数据
|
||||||
|
taskList.value = []
|
||||||
|
// 置数据 到 列表数据
|
||||||
|
var list = req.data?.list || []
|
||||||
|
// 置数据 到 列表数据
|
||||||
|
pagination.value.total = req.data?.total || 0
|
||||||
|
|
||||||
|
for(let i = 0; i < list.length; i++){
|
||||||
|
const item = {
|
||||||
|
id: list[i].header.task_id,
|
||||||
|
taskType: list[i].header.task_type.toString(),
|
||||||
|
shopName: list[i].header.shop_name,
|
||||||
|
shopType: list[i].header.shop_type.toString(),
|
||||||
|
taskStatus: list[i].header.status,
|
||||||
|
createTime: list[i].header.task_create_at.toString(),
|
||||||
|
waitCount : list[i].footer.task_count_wait.toString(), // 等待数量
|
||||||
|
successCount : list[i].footer.task_count_success.toString(), // 成功数量
|
||||||
|
errorCount : list[i].footer.task_count_error.toString(), // 错误数量
|
||||||
|
totalCount : list[i].footer.task_count_true.toString(), // 总计数量
|
||||||
|
overCount : list[i].footer.task_count_over.toString(), // 完成数量
|
||||||
|
} as TaskList;
|
||||||
|
console.log(item.shopType)
|
||||||
|
taskList.value.push(item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(`获取数据失败: ${req.msg || '未知错误'}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GetShopTask error:', error)
|
||||||
|
ElMessage.error('获取任务列表失败,请重试')
|
||||||
|
}finally{
|
||||||
|
tableLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
46
src/router/index.js
Normal file
46
src/router/index.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
import Index from '../page/shopTask.vue'
|
||||||
|
import Test from '../page/test.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: '首页',
|
||||||
|
component: Test
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test',
|
||||||
|
name: '测试',
|
||||||
|
component: Test
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(), // 使用 HTML5 模式
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 全局前置守卫:路由跳转前检查登录状态
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
// 1. 获取用户的登录 Token(从 localStorage 中)
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
// 2. 判断当前路由是否需要登录权限
|
||||||
|
const requiresAuth = to.meta.requiresAuth;
|
||||||
|
|
||||||
|
if (requiresAuth) {
|
||||||
|
// 3. 需要登录的路由:检查 Token 是否存在
|
||||||
|
if (token) {
|
||||||
|
// 已登录,放行
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
console.log("未登录")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不需要登录的路由(如登录页),直接放行
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default router
|
||||||
90
src/store/index.js
Normal file
90
src/store/index.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
// 定义本地存储的键名(统一管理,方便维护)
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
user: 'app_user_info',
|
||||||
|
token: 'app_token'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 封装本地存储工具函数(内置,无需额外文件)
|
||||||
|
const storage = {
|
||||||
|
// 设置存储
|
||||||
|
set(key, value) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`存储${key}失败:`, e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取存储
|
||||||
|
get(key) {
|
||||||
|
try {
|
||||||
|
const value = localStorage.getItem(key)
|
||||||
|
return value ? JSON.parse(value) : null
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`获取${key}失败:`, e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 移除存储
|
||||||
|
remove(key) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化状态:优先从本地存储恢复
|
||||||
|
const initState = () => {
|
||||||
|
return reactive({
|
||||||
|
// 从本地存储恢复用户信息
|
||||||
|
user: storage.get(STORAGE_KEYS.user),
|
||||||
|
// 从本地存储恢复token
|
||||||
|
token: storage.get(STORAGE_KEYS.token)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
// 初始化响应式状态(刷新后自动恢复)
|
||||||
|
state: initState(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户信息(同时持久化到本地存储)
|
||||||
|
* @param {Object} user - 用户信息对象
|
||||||
|
*/
|
||||||
|
setUser(user) {
|
||||||
|
this.state.user = user
|
||||||
|
// 持久化到localStorage
|
||||||
|
storage.set(STORAGE_KEYS.user, user)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置token(同时持久化到本地存储)
|
||||||
|
* @param {String} token - 登录令牌
|
||||||
|
*/
|
||||||
|
setToken(token) {
|
||||||
|
this.state.token = token
|
||||||
|
// 持久化到localStorage
|
||||||
|
storage.set(STORAGE_KEYS.token, token)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除登录状态(内存+本地存储)
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
// 清空内存状态
|
||||||
|
this.state.user = null
|
||||||
|
this.state.token = null
|
||||||
|
// 清空本地存储
|
||||||
|
storage.remove(STORAGE_KEYS.user)
|
||||||
|
storage.remove(STORAGE_KEYS.token)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已登录
|
||||||
|
* @returns {Boolean} 是否登录
|
||||||
|
*/
|
||||||
|
isLogin() {
|
||||||
|
return !!this.state.token && !!this.state.user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
163
src/utils/date.js
Normal file
163
src/utils/date.js
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
// src/utils/date.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期时间为 YYYY-MM-DD HH:mm:ss
|
||||||
|
* 自动处理秒级(10位)和毫秒级(13位)时间戳
|
||||||
|
* @param {string|number|Date} timestamp - 时间戳或日期字符串
|
||||||
|
* @returns {string} 格式化后的日期时间字符串
|
||||||
|
*/
|
||||||
|
export const formatDateTime = (timestamp) => {
|
||||||
|
// 处理空值
|
||||||
|
if (timestamp === undefined || timestamp === null || timestamp === '') {
|
||||||
|
// console.warn('时间戳为空')
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let date
|
||||||
|
|
||||||
|
// 根据不同类型处理
|
||||||
|
if (typeof timestamp === 'number') {
|
||||||
|
const timestampStr = String(timestamp)
|
||||||
|
|
||||||
|
// 判断是秒级(10位)还是毫秒级(13位)时间戳
|
||||||
|
if (timestampStr.length === 10) {
|
||||||
|
// 秒级时间戳
|
||||||
|
date = new Date(timestamp * 1000)
|
||||||
|
} else if (timestampStr.length === 13) {
|
||||||
|
// 毫秒级时间戳
|
||||||
|
date = new Date(timestamp)
|
||||||
|
} else {
|
||||||
|
// 其他长度尝试直接转换
|
||||||
|
date = new Date(timestamp)
|
||||||
|
}
|
||||||
|
} else if (typeof timestamp === 'string') {
|
||||||
|
// 处理 ISO 格式字符串 (如: 2026-03-02T14:25:06Z)
|
||||||
|
if (timestamp.includes('T') && (timestamp.includes('Z') || timestamp.includes('+'))) {
|
||||||
|
// ISO 格式可以直接被 Date 解析
|
||||||
|
date = new Date(timestamp)
|
||||||
|
}
|
||||||
|
// 检查是否是数字字符串
|
||||||
|
else if (/^\d+$/.test(timestamp)) {
|
||||||
|
// 纯数字字符串
|
||||||
|
const num = parseInt(timestamp)
|
||||||
|
if (String(num).length === 10) {
|
||||||
|
date = new Date(num * 1000)
|
||||||
|
} else {
|
||||||
|
date = new Date(num)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 普通日期字符串,替换 - 为 / 兼容 Safari
|
||||||
|
date = new Date(timestamp.replace(/-/g, '/'))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Date 对象或其他
|
||||||
|
date = new Date(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查日期是否有效
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
console.warn('无效的时间戳:', timestamp)
|
||||||
|
return String(timestamp) // 返回原始字符串而不是空字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
} catch (error) {
|
||||||
|
console.error('日期格式化错误:', error)
|
||||||
|
return String(timestamp) // 出错时返回原始字符串
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期为 YYYY-MM-DD
|
||||||
|
* @param {string|number|Date} timestamp
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const formatDate = (timestamp) => {
|
||||||
|
if (!timestamp) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
if (isNaN(date.getTime())) return ''
|
||||||
|
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间为 HH:mm:ss
|
||||||
|
* @param {string|number|Date} timestamp
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const formatTime = (timestamp) => {
|
||||||
|
if (!timestamp) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
if (isNaN(date.getTime())) return ''
|
||||||
|
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${hours}:${minutes}:${seconds}`
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取相对时间(刚刚、几分钟前等)
|
||||||
|
* @param {string|number|Date} timestamp
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const timeAgo = (timestamp) => {
|
||||||
|
if (!timestamp) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
if (isNaN(date.getTime())) return ''
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const diff = now - date.getTime()
|
||||||
|
|
||||||
|
const seconds = Math.floor(diff / 1000)
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
const days = Math.floor(hours / 24)
|
||||||
|
const months = Math.floor(days / 30)
|
||||||
|
const years = Math.floor(months / 12)
|
||||||
|
|
||||||
|
if (years > 0) return `${years}年前`
|
||||||
|
if (months > 0) return `${months}个月前`
|
||||||
|
if (days > 0) return `${days}天前`
|
||||||
|
if (hours > 0) return `${hours}小时前`
|
||||||
|
if (minutes > 0) return `${minutes}分钟前`
|
||||||
|
if (seconds > 10) return `${seconds}秒前`
|
||||||
|
return '刚刚'
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认导出所有方法
|
||||||
|
export default {
|
||||||
|
formatDateTime,
|
||||||
|
formatDate,
|
||||||
|
formatTime,
|
||||||
|
timeAgo
|
||||||
|
}
|
||||||
88
src/utils/request.js
Normal file
88
src/utils/request.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// 创建 axios 实例
|
||||||
|
const service = axios.create({
|
||||||
|
// 优先使用环境变量,无则用 /api 作为兜底
|
||||||
|
baseURL: import.meta.env.VITE_APP_BASE_API || '/api',
|
||||||
|
timeout: 10000, // 请求超时时间
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json;charset=utf-8' // 统一设置默认请求头
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器:添加 token、处理请求配置
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// 从本地存储获取 token 并添加到请求头
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 请求发送前的错误处理
|
||||||
|
console.error('请求拦截器错误:', error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器:统一处理返回数据、错误状态码
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
const res = response.data
|
||||||
|
|
||||||
|
// 优化:仅判断 HTTP 状态码不够,建议结合后端自定义 code
|
||||||
|
// 如果后端仅用 HTTP 200 表示成功,这部分可简化
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error('请求失败:', res.message || '接口返回非 200 状态')
|
||||||
|
return Promise.reject(new Error(res.message || '请求失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常返回数据
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 响应错误处理(401/404/500 等)
|
||||||
|
console.error('响应拦截器错误:', error)
|
||||||
|
|
||||||
|
// 处理不同状态码的业务逻辑
|
||||||
|
if (error.response) {
|
||||||
|
const { status } = error.response
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
// 未授权:清除 token 并跳转到登录页
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
// 适配 Vue Router 或直接跳转(根据你的项目选择)
|
||||||
|
// router.push('/login') // 如果用 Vue Router,需先引入
|
||||||
|
window.location.href = '/login' // 原生跳转,兼容性更好
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
console.error('接口不存在:', error.response.config.url)
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
console.error('服务器内部错误')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.error(`请求错误,状态码:${status}`)
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
// 请求已发送但无响应(网络问题)
|
||||||
|
console.error('网络异常,请检查网络连接')
|
||||||
|
} else {
|
||||||
|
// 请求配置错误
|
||||||
|
console.error('请求配置错误:', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 导出常用请求方法(简化调用)
|
||||||
|
export const get = (url, params = {}) => service.get(url, { params })
|
||||||
|
export const post = (url, data = {}) => service.post(url, data)
|
||||||
|
export const put = (url, data = {}) => service.put(url, data)
|
||||||
|
export const del = (url) => service.delete(url)
|
||||||
|
|
||||||
|
// 导出默认实例
|
||||||
|
export default service
|
||||||
32
vite.config.js
Normal file
32
vite.config.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: true,
|
||||||
|
proxy: { // proxy 需要放在 server 对象内部
|
||||||
|
// 代理所有以 /api 开头的请求
|
||||||
|
'/api': {
|
||||||
|
// target: 'http://36.212.7.246:8283/', // 后端服务地址
|
||||||
|
target: 'http://192.168.101.156:8080', // 后端服务地址
|
||||||
|
changeOrigin: true, // 支持跨域
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, '') // 重写路径,去掉 /api 前缀
|
||||||
|
},
|
||||||
|
// 可以配置多个代理
|
||||||
|
'/upload': {
|
||||||
|
// target: 'http://36.212.7.246:8283/',
|
||||||
|
target: 'http:///192.168.101.156:8080',
|
||||||
|
changeOrigin: true
|
||||||
|
// 如果不需要重写路径,可以不加 rewrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user