初始提交:饿了么 Token 管理工具

This commit is contained in:
97694732@qq.com 2026-06-22 18:09:17 +08:00
commit 295b46acd7
30 changed files with 9194 additions and 0 deletions

2
.env.development Normal file
View File

@ -0,0 +1,2 @@
# 开发环境 API 基础路径
VITE_APP_BASE_API=

2
.env.production Normal file
View File

@ -0,0 +1,2 @@
# 生产环境 API 基础路径
VITE_APP_BASE_API=

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
.DS_Store
*.local

13
index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

8
src/App.vue Normal file
View File

@ -0,0 +1,8 @@
<!-- src/App.vue -->
<template>
<router-view />
</template>
<script setup>
// router-view
</script>

236
src/api/shopTask.js Normal file
View 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
View 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'
})
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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

File diff suppressed because it is too large Load Diff

581
src/page/test.vue Normal file
View File

@ -0,0 +1,581 @@
<template>
<!-- 搜索条 -->
<SearchBar v-model="searchForm" :isShow="true" @search="searchFunc"></SearchBar>
<!-- 搜索条 -->
<div class="mb-[10px]">
<h6 style="color: red;">Tip1闲鱼如果发送其他类目可能会出现无法获取到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()
// URLurlType
const urlType = route.query.type as string || ''
// URLtoken
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 {
// tokenlocalStoragestore
localStorage.setItem('token', urlToken)
// storesetTokenstore
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
// userIdstore
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
View 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
View 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
View 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
View 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
View 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
}
}
}
})