Commit 26c55257 by linjinhong

feat:修改定时任务方法

parent 98eefb15
......@@ -5,7 +5,7 @@ import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
import { createServer } from "@/server/index.js";
import { autoUpdater } from "electron-updater";
const { createBatchDownloadQueue } = require("@/utils/download-queue");
import downloadQueue from "@/utils/download-queue";
const fs = require("fs");
import path from "path"; // 引入 path 模块
......@@ -30,36 +30,6 @@ const winURL =
? "http://localhost:8050"
: `file://${__dirname}/index.html`;
// 全局唯一下载队列,服务启动时初始化一次
const downloadQueue = createBatchDownloadQueue({
concurrency: 3,
retryTimes: 2,
basePath: "",
// 单文件下载成功
onSuccess: task => {
win.webContents.send("download:file-success", {
batchArrangeNum: task.batchArrangeNum,
fileName: task.fileName
});
},
// 单文件下载失败
onError: (task, err) => {
win.webContents.send("download:file-error", {
batchArrangeNum: task.batchArrangeNum,
fileName: task.fileName,
msg: err.message
});
},
// 批次校验失败(新增:返回所有异常批次)
onBatchInvalid: invalidList => {
win.webContents.send("download:batch-invalid", invalidList);
},
// 全部任务完成
onIdle: () => {
win.webContents.send("download:all-finish");
}
});
async function createWindow() {
win = new BrowserWindow({
width: 1500,
......@@ -408,6 +378,15 @@ async function createWindow() {
);
});
// 监听窗口的 reload 事件
win.webContents.on("did-finish-load", () => {
const { width, height } = win.getBounds();
win.webContents.send("window-size", { width, height });
console.log(99999);
// downloadQueue.startAutoDownloadTask();
});
if (process.env.WEBPACK_DEV_SERVER_URL) {
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
if (!process.env.IS_TEST) win.webContents.openDevTools();
......@@ -508,17 +487,7 @@ async function createWindow() {
win.on("resize", () => {
const { width, height } = win.getBounds();
win.webContents.send("window-size", { width, height });
});
// 监听窗口的 reload 事件
win.webContents.on("did-finish-load", () => {
const { width, height } = win.getBounds();
win.webContents.send("window-size", { width, height });
});
// IPC 通信
ipcMain.on("download:submit-batch-list", (event, list) => {
downloadQueue.addBatchList(list);
event.returnValue = { code: 200, msg: "已处理" };
console.log(888);
});
ipcMain.on("get-install-path", event => {
......@@ -542,18 +511,9 @@ async function createWindow() {
}
});
ipcMain.on("download:get-status", event => {
try {
// 无论正常异常都保证回复,避免前端挂起
event.reply("download:status-reply", {
isRunning: downloadQueue.isRunning
});
} catch (err) {
event.reply("download:status-reply", {
isRunning: false,
error: err.message
});
}
// ========== 新增3:窗口关闭时停止定时任务 ==========
win.on("closed", () => {
downloadQueue.stopAutoDownloadTask();
});
}
......@@ -587,8 +547,11 @@ app.on("will-quit", () => {
globalShortcut.unregister("F5");
globalShortcut.unregister("CommandOrControl+R");
});
app.on("before-quit", () => {
downloadQueue.destroy();
app.on("window-all-closed", () => {
downloadQueue.stopAutoDownloadTask();
if (process.platform !== "darwin") {
app.quit();
}
});
if (isDevelopment) {
......
......@@ -75,7 +75,7 @@ function readEnv() {
fileEnv = config.fileApiUrl;
env = config.apiApiHost;
visionUrl = config.visionUrl;
console.log("配置加载完成:", { fileEnv, env, visionUrl });
// console.log("配置加载完成:", { fileEnv, env, visionUrl });
} catch (err) {
console.error("读取配置文件失败:", err);
......
const fs = require("fs");
// ===================== 1. 依赖引入(最顶层,按依赖顺序) =====================
const path = require("path");
const fs = require("fs");
const http = require("http");
const https = require("https");
const { URL } = require("url");
const { getDTFSetting } = require("@/server/utils/store");
export function createBatchDownloadQueue(options = {}) {
// ========== 1. 配置项 ==========
const config = Object.assign(
{
concurrency: 3,
retryTimes: 2,
onSuccess: () => {}, // 单文件下载成功
onError: () => {}, // 单文件下载失败
onBatchInvalid: () => {}, // 批次校验失败回调(返回异常批次列表+原因)
onIdle: () => {} // 全部任务完成
},
options
);
// ========== 2. 内部状态 ==========
let pendingQueue = [];
let runningCount = 0;
const successSet = new Set();
const runningSet = new Set();
let isDestroyed = false;
const { pathMap } = require("@/config/index.js");
const { getDTFSetting, getHostApi } = require("@/server/utils/store");
import api from "./request";
// ===================== 2. 全局状态与可配置参数 =====================
const taskState = {
currentPage: 1, // 当前拉取页码,从第1页开始
pageSize: 100, // 每页拉取100条数据,与后端约定一致
layoutStatus: "3,5", // 部分成功,排版成功
isDownloading: false, // 任务互斥锁:防止5分钟到点后重复执行
timer: null, // 定时器实例,用于启停控制
total: 0, // 数据总条数,从接口实时同步
interval: 5 * 60 * 1000, // 定时周期:5分钟执行一次
maxBatchConcurrency: 3, // 同时处理的最大批次数量(总并发维度1)
maxImageConcurrency: 10 // 单个批次内同时下载的最大图片数(总并发维度2)
// 总并发峰值 = 3 × 10 = 30,生产环境安全稳定
};
// ===================== 3. 基础工具函数 =====================
// ========== 3. 通用工具方法 ==========
const getRequestModule = url => {
try {
return new URL(url).protocol === "https:" ? https : http;
} catch (e) {
return https;
}
};
/**
* 统一错误日志格式化输出
* @param {string} type 错误类型
* @param {string} batchNo 批次号
* @param {string} url 出错的图片链接
* @param {string} message 错误详情
*/
function logDownloadError(type, batchNo, url, message) {
console.error(
`[自动下载失败][${type}] 批次号: ${batchNo || "未知"} | 链接: ${url ||
"-"} | 详情: ${message}`
);
}
const isPngUrl = url => {
try {
const urlObj = new URL(url);
return urlObj.pathname.toLowerCase().endsWith(".png");
} catch (e) {
return false;
/**
* 简易并发限制器(无第三方依赖,兼容Node 12)
* 作用:保证同时运行的异步任务不超过指定数量,完成一个自动补上下一个
* @param {number} concurrency 最大并发数
* @returns {Function} 接收异步函数的包装器
*/
function pLimit(concurrency) {
let activeCount = 0; // 当前正在执行的任务数
const queue = []; // 等待执行的任务队列
// 尝试启动下一个任务
const next = () => {
if (queue.length > 0 && activeCount < concurrency) {
activeCount++;
const task = queue.shift();
task().finally(() => {
activeCount--;
next(); // 任务完成后自动启动下一个
});
}
};
const getPngFileName = url => {
try {
const urlObj = new URL(url);
const name = path.basename(urlObj.pathname);
return name.toLowerCase().endsWith(".png")
? name
: `${name || `img_${Date.now()}`}.png`;
} catch (e) {
return `img_${Date.now()}.png`;
}
return fn => {
return new Promise((resolve, reject) => {
// 加入等待队列
queue.push(() =>
fn()
.then(resolve)
.catch(reject)
);
next();
});
};
}
/**
* 递归创建目录(异步,不阻塞主线程)
* @param {string} dirPath 目录路径
*/
async function ensureDir(dirPath) {
await fs.promises.mkdir(dirPath, { recursive: true });
}
/**
* 从URL中提取原始文件名(完全沿用你指定的逻辑)
* @param {string} url 图片链接
* @returns {string} 原始文件名
*/
const getPngFileName = url => {
let filePath = url.replace(/\\/g, "/"); // 统一反斜杠为正斜杠
const originalFileName = path.basename(filePath);
return originalFileName;
};
/**
* 异步判断文件是否已存在
* @param {string} filePath 文件完整路径
* @returns {boolean} true=存在,false=不存在
*/
async function isFileExists(filePath) {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
return true;
} catch (e) {
return false;
}
}
const fileExists = targetPath => fs.existsSync(targetPath);
const cleanBadFile = targetPath =>
fileExists(targetPath) && fs.unlinkSync(targetPath);
// ===================== 4. 核心下载能力 =====================
// ========== 4. 流式下载核心 ==========
const streamDownload = (url, targetPath) =>
new Promise((resolve, reject) => {
const requestModule = getRequestModule(url);
const writeStream = fs.createWriteStream(targetPath);
const request = requestModule.get(url, res => {
/**
* 单文件流式下载(全错误捕获 + 重定向控制)
* 流式写入:边下载边写盘,内存占用极低,不阻塞主线程
* @param {string} url 图片下载链接
* @param {string} savePath 本地保存完整路径
* @param {number} redirectCount 已重定向次数(内部递归用,外部不传)
* @returns {Promise<string>} 保存成功的文件路径
*/
function downloadFile(url, savePath, redirectCount = 0) {
return new Promise((resolve, reject) => {
try {
// 根据链接协议自动选择 http/https 模块
const request = url.startsWith("https") ? https : http;
const req = request.get(url, { timeout: 30000 }, res => {
// 处理重定向:最多3次,防止死循环
if ([301, 302, 307, 308].includes(res.statusCode)) {
writeStream.close();
streamDownload(res.headers.location, targetPath)
if (redirectCount >= 3) {
res.resume(); // 消耗响应体,释放连接
reject(new Error("重定向次数超过3次,终止下载"));
return;
}
// 递归执行下载,重定向次数+1
downloadFile(res.headers.location, savePath, redirectCount + 1)
.then(resolve)
.catch(reject);
return;
}
// HTTP状态码非200直接报错
if (res.statusCode !== 200) {
writeStream.close();
cleanBadFile(targetPath);
reject(new Error(`响应状态码 ${res.statusCode}`));
res.resume();
reject(new Error(`HTTP状态码异常: ${res.statusCode}`));
return;
}
// 创建写入流,通过管道将网络响应写入文件
const writeStream = fs.createWriteStream(savePath);
res.pipe(writeStream);
});
writeStream.on("finish", () => {
writeStream.close();
resolve();
});
writeStream.on("error", err => {
writeStream.close();
cleanBadFile(targetPath);
reject(err);
});
request.on("error", err => {
writeStream.close();
cleanBadFile(targetPath);
reject(err);
});
request.setTimeout(30000, () => {
request.destroy();
writeStream.close();
cleanBadFile(targetPath);
reject(new Error("下载超时"));
// 写入完成
writeStream.on("finish", () => {
writeStream.close(() => resolve(savePath));
});
// 写入失败:销毁流并清理损坏文件
writeStream.on("error", err => {
writeStream.destroy();
fs.promises.unlink(savePath).catch(() => {});
reject(new Error(`文件写入失败: ${err.message}`));
});
// 响应流异常
res.on("error", err => {
writeStream.destroy();
fs.promises.unlink(savePath).catch(() => {});
reject(new Error(`网络响应异常: ${err.message}`));
});
});
});
// ========== 5. 队列消费逻辑 ==========
const consume = () => {
if (isDestroyed) return;
if (runningCount >= config.concurrency || pendingQueue.length === 0) {
runningCount === 0 && config.onIdle();
return;
// 请求级错误(网络不通、DNS失败等)
req.on("error", err => reject(new Error(`网络请求失败: ${err.message}`)));
// 请求超时
req.on("timeout", () => {
req.destroy();
reject(new Error("下载超时(30s)"));
});
} catch (syncErr) {
// 捕获URL格式错误等同步异常
reject(new Error(`请求初始化失败: ${syncErr.message}`));
}
});
}
const task = pendingQueue.shift();
runningCount++;
(async () => {
try {
const targetDir = path.dirname(task.targetPath);
if (!fileExists(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
await streamDownload(task.downloadUrl, task.targetPath);
successSet.add(task.targetPath);
config.onSuccess(task);
} catch (err) {
if (task.remainRetry > 0) {
task.remainRetry--;
pendingQueue.unshift(task);
} else {
config.onError(task, err);
}
} finally {
runningCount--;
runningSet.delete(task.targetPath);
consume();
}
})();
};
// ===================== 6. 核心业务逻辑 =====================
// ========== 6. 对外暴露方法 ==========
return {
addBatchList(list = []) {
if (isDestroyed || !Array.isArray(list)) return;
/**
* 处理单个批次的图片下载
* 单张图片失败仅跳过当前文件,不影响同批次其他图片
* @param {object} item 单条排版数据(含batchArrangeNum、url字段)
*/
async function processBatchItem(item) {
const { batchArrangeNum, url: urlStr } = item;
// ---------- 基础参数校验 ----------
if (!batchArrangeNum) {
logDownloadError(
"参数无效",
batchArrangeNum,
"",
"缺少批次号batchArrangeNum"
);
return;
}
if (!urlStr || typeof urlStr !== "string") {
logDownloadError(
"参数无效",
batchArrangeNum,
urlStr,
"url字段为空或非字符串格式"
);
return;
}
// ---------- 1. 创建批次保存目录 ----------
let saveDir;
try {
const downloadRoot = getDTFSetting().downloadDir;
if (!downloadRoot) throw new Error("下载根目录downloadDir配置为空");
// 目录结构:根目录/批次号:xxx/自动排版素材
saveDir = path.join(
downloadRoot,
`批次号:${batchArrangeNum}`,
"自动排版素材"
);
await ensureDir(saveDir);
} catch (dirErr) {
// 目录创建失败:整个批次跳过
logDownloadError("目录创建失败", batchArrangeNum, "", dirErr.message);
return;
}
// ---------- 2. 拆分图片链接 ----------
// 支持逗号分隔的多图字符串,过滤空值与无效值
const urlList = urlStr
.split(",")
.map(u => u.trim())
.filter(u => {
if (!u || typeof u !== "string") {
logDownloadError("链接无效", batchArrangeNum, u, "空链接或非字符串");
return false;
}
return true;
});
// 第一步:筛选 layoutStatus = 3 或 5 的批次
const targetBatch = list.filter(item =>
[3, 5].includes(item.layoutStatus)
);
const invalidBatchList = []; // 收集异常批次
const fileTasks = [];
// 第二步:逐个校验批次URL合法性
targetBatch.forEach(batch => {
const batchNo = batch.batchArrangeNum || "未知批次";
// 校验1:URL 不是字符串类型
if (typeof batch.url !== "string") {
invalidBatchList.push({
batchArrangeNum: batchNo,
reason: "下载链接格式错误,非字符串类型"
});
return;
}
if (urlList.length === 0) {
logDownloadError("无有效链接", batchArrangeNum, "", "拆分后无可用图片链接");
return;
}
// 校验2:URL 为空字符串/纯空格
if (batch.url.trim() === "") {
invalidBatchList.push({
batchArrangeNum: batchNo,
reason: "下载链接为空"
});
return;
}
// ---------- 3. 并发下载批次内所有图片 ----------
const imageLimit = pLimit(taskState.maxImageConcurrency);
const fileEnv = getHostApi().fileApiUrl;
const downloadPromises = urlList.map(url =>
imageLimit(async () => {
const newUrl = `${fileEnv}${url}`;
// 第三步:拆分多URL,过滤空值,校验PNG格式
const urlList = batch.url
.split(",")
.map(u => u.trim())
.filter(Boolean);
const validPngUrls = urlList.filter(url => isPngUrl(url));
// 校验3:拆分后无有效PNG链接
if (validPngUrls.length === 0) {
invalidBatchList.push({
batchArrangeNum: batchNo,
reason: "无有效PNG格式下载链接"
});
try {
const fileName = getPngFileName(url);
const savePath = path.join(saveDir, fileName);
// 文件已存在则直接跳过,不重复下载
const exists = await isFileExists(savePath);
if (exists) {
console.log(
`[跳过下载] 文件已存在 | 批次: ${batchArrangeNum} | 文件: ${fileName}`
);
return;
}
// 第四步:生成合法下载任务,去重过滤
const dir = path.join(
getDTFSetting().downloadDir,
`批次号:${batchNo}`,
"自动排版素材"
// 执行下载
await downloadFile(newUrl, savePath);
console.log(`[下载成功] 批次: ${batchArrangeNum} | 文件: ${fileName}`);
} catch (downloadErr) {
// 单张下载失败:记录日志,跳过当前文件
logDownloadError(
"文件下载失败",
batchArrangeNum,
newUrl,
downloadErr.message
);
validPngUrls.forEach(url => {
const fileName = getPngFileName(url);
const targetPath = path.join(dir, fileName);
if (
!fileExists(targetPath) &&
!successSet.has(targetPath) &&
!runningSet.has(targetPath)
) {
fileTasks.push({
batchArrangeNum: batchNo,
downloadUrl: `https://factory.jomalls.com${url}`,
targetPath,
fileName,
remainRetry: config.retryTimes
});
}
});
});
// 有异常批次,通过回调抛出
if (invalidBatchList.length > 0) {
config.onBatchInvalid(invalidBatchList);
}
})
);
// 启动下载队列
if (fileTasks.length > 0) {
fileTasks.forEach(task => {
runningSet.add(task.targetPath);
pendingQueue.push(task);
});
consume();
}
},
get isRunning() {
return runningCount > 0 || pendingQueue.length > 0;
},
destroy() {
isDestroyed = true;
pendingQueue = [];
runningCount = 0;
successSet.clear();
runningSet.clear();
}
};
// 等待当前批次所有图片处理完成
await Promise.all(downloadPromises);
}
/**
* 批量下载PNG图片到指定文件夹
* @param {string[]} urls - PNG图片链接数组
* @param {string} targetDir - 目标文件夹绝对路径
* @param {Object} [options] - 可选配置
* @param {number} [options.concurrency=2] - 同时下载数量
* @param {number} [options.timeout=30000] - 单文件超时时间(ms)
* @param {number} [options.retryTimes=1] - 单文件失败重试次数
* @returns {Promise<{success: string[], failed: {url: string, error: string}[]}>}
* 执行一次完整的定时任务
* 流程:获取设置 → 判断策略 → 拉取列表 → 批量下载 → 更新页码
*/
export function downloadPngList(urls, targetDir, options = {}) {
const config = {
concurrency: 2,
timeout: 30000,
retryTimes: 1,
...options
};
async function runTask() {
// 互斥锁:上一轮任务未完成则直接跳过本轮
if (taskState.isDownloading) {
console.log("当前有下载任务进行中,跳过本轮定时任务");
return;
}
return new Promise(resolve => {
// 参数合法性校验
if (!Array.isArray(urls) || urls.length === 0) {
resolve({
success: [],
failed: [{ url: "", error: "下载链接数组为空" }]
});
try {
taskState.isDownloading = true;
console.log(`=== 开始执行定时任务,当前页码:${taskState.currentPage} ===`);
// 步骤1:调用获取设置接口
const settingRes = await api.post(pathMap["productionConfig"]);
const autoLayoutStrategy = settingRes.data.autoLayoutStrategy;
// 步骤2:策略判断:不等于3才继续下载
if (autoLayoutStrategy === 3) {
console.log("不自动排版,跳过本次下载");
return;
}
console.log(
`开始批量下载,共 ${urls.length} 个链接,目标目录:${targetDir}`
);
// 自动创建目标目录(多级递归创建,不需要可注释掉)
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const successList = []; // 下载成功的文件路径
const failedList = []; // 下载失败的链接+原因
let currentIndex = 0; // 当前处理到第几个链接
let runningCount = 0; // 当前正在下载的数量
const total = urls.length;
// 步骤3:调用排版数据接口,拉取当前页数据
const layoutRes = await api.post(pathMap["productionSoftware"], {
currentPage: taskState.currentPage,
pageSize: taskState.pageSize,
layoutStatus: taskState.layoutStatus
});
console.log(layoutRes);
// 从URL中提取PNG文件名,无后缀自动补.png
const getPngFileName = url => {
try {
const urlObj = new URL(url);
const name = path.basename(urlObj.pathname);
return name.toLowerCase().endsWith(".png") ? name : `${name}.png`;
} catch (e) {
return `img_${Date.now()}_${Math.random()
.toString(36)
.slice(2, 6)}.png`;
}
};
// 单文件流式下载核心
const streamDownload = (url, filePath) => {
return new Promise((resolve, reject) => {
const requestModule = url.startsWith("https:") ? https : http;
const writeStream = fs.createWriteStream(filePath);
const request = requestModule.get(url, res => {
// 自动处理301/302/307/308重定向
if ([301, 302, 307, 308].includes(res.statusCode)) {
writeStream.close();
streamDownload(res.headers.location, filePath)
.then(resolve)
.catch(reject);
return;
}
if (res.statusCode !== 200) {
writeStream.close();
fs.existsSync(filePath) && fs.unlinkSync(filePath);
reject(new Error(`HTTP状态码 ${res.statusCode}`));
return;
}
res.pipe(writeStream);
});
// return;
const list = layoutRes.data.records || [];
const total = layoutRes.data.total || 0;
taskState.total = total;
writeStream.on("finish", () => {
console.log("downloadPngList---finish");
if (list.length === 0) {
console.log("当前页无数据,本轮任务结束");
return;
}
writeStream.close();
resolve();
});
writeStream.on("error", err => {
writeStream.close();
fs.existsSync(filePath) && fs.unlinkSync(filePath);
reject(err);
});
request.on("error", err => {
writeStream.close();
fs.existsSync(filePath) && fs.unlinkSync(filePath);
reject(err);
});
// 超时兜底
request.setTimeout(config.timeout, () => {
request.destroy();
writeStream.close();
fs.existsSync(filePath) && fs.unlinkSync(filePath);
reject(new Error("下载超时"));
});
});
};
// 步骤4:多批次并行下载
const batchLimit = pLimit(taskState.maxBatchConcurrency);
const batchPromises = list.map(item =>
batchLimit(() => processBatchItem(item))
);
await Promise.all(batchPromises);
// 带重试的单任务执行
const runTask = async url => {
const fileName = getPngFileName(url);
const filePath = path.join(targetDir, fileName);
console.log(`=== 第 ${taskState.currentPage} 页全部处理完成 ===`);
// 本地文件已存在,直接跳过
if (fs.existsSync(filePath)) {
successList.push(filePath);
return;
}
console.log(`开始下载:${fileName}`);
let remainRetry = config.retryTimes;
// 步骤5:更新页码,超过总数则从第1页循环
taskState.currentPage++;
if ((taskState.currentPage - 1) * taskState.pageSize >= taskState.total) {
taskState.currentPage = 1;
console.log("已遍历全部数据,下轮任务从第1页重新开始");
}
} catch (err) {
// 整体任务异常(接口挂了等),记录日志
console.error("定时任务执行异常:", err.message);
} finally {
// 无论成功失败,最终释放锁,避免死锁
taskState.isDownloading = false;
console.log("本轮任务结束,锁已释放");
}
}
try {
await streamDownload(url, filePath);
console.log(`下载成功:${fileName}`);
successList.push(filePath);
} catch (err) {
if (remainRetry > 0) {
remainRetry--;
} else {
failedList.push({ url, error: err.message });
}
}
};
// 并发调度器
const next = () => {
// 并发数未满且还有未处理任务,继续启动
while (runningCount < config.concurrency && currentIndex < total) {
runningCount++;
const url = urls[currentIndex].trim();
currentIndex++;
runTask(url).finally(() => {
runningCount--;
next();
});
}
// ===================== 7. 对外暴露的启停方法 =====================
// 所有任务执行完毕,返回结果
if (runningCount === 0 && currentIndex >= total) {
console.log(
`全部任务执行完成,成功:${successList.length} 个,失败:${failedList.length} 个`
);
resolve({ success: successList, failed: failedList });
}
};
/**
* 启动自动下载定时任务
* 调用后立即启动定时器,5分钟后首次执行任务
*/
function startAutoDownloadTask() {
// 防止重复启动
if (taskState.timer) return;
next();
});
console.log("自动下载定时任务已启动,5分钟后首次执行");
taskState.timer = setInterval(runTask, taskState.interval);
}
/**
* 停止自动下载定时任务
* 窗口关闭、应用退出时调用,清理定时器与状态
*/
function stopAutoDownloadTask() {
if (taskState.timer) {
clearInterval(taskState.timer);
taskState.timer = null;
taskState.isDownloading = false;
console.log("自动下载定时任务已停止");
}
}
// ===================== 8. 模块导出 =====================
export default {
startAutoDownloadTask,
stopAutoDownloadTask
};
import axios from "axios";
import Store from "electron-store"; // 主进程用 electron-store 存取用户信息,和渲染进程的 $dataStore 数据互通
const dataStore = new Store();
// 纯请求实例:无任何 UI 依赖,仅处理 token 注入和错误格式化
const service = axios.create({
baseURL: "http://localhost:3000",
timeout: 12600000
});
// 请求拦截:仅注入 token
service.interceptors.request.use(
config => {
const user = dataStore.get("user");
if (user.token) {
config.headers["jwt-token"] = user.token;
}
return config;
},
error => Promise.reject(error)
);
// 响应拦截:仅处理数据和错误信息,不做任何 UI 操作
service.interceptors.response.use(
response => response.data,
error => {
let errMsg = "请求失败,请稍后重试";
if (error.response.data) {
const data = error.response.data;
errMsg =
data.msg || data.message || `请求错误(${error.response.status})`;
} else if (error.message) {
errMsg = error.message.includes("timeout")
? "请求超时,请检查网络"
: error.message;
}
return Promise.reject(new Error(errMsg));
}
);
export default service;
......@@ -179,10 +179,7 @@ export default {
<div class="page-header">
<el-form style="display: flex;flex-wrap: nowrap;" inline>
<el-form-item>
<el-button
icon="el-icon-arrow-left"
@click="goBack"
style="margin-right:12px;"
<el-button icon="el-icon-arrow-left" @click="goBack"
>返回</el-button
></el-form-item
>
......@@ -194,7 +191,12 @@ export default {
></el-input
></el-form-item>
<el-form-item label="操作单状态">
<el-select v-model="searchForm.status" placeholder="请选择" clearable>
<el-select
style="width: 120px;"
v-model="searchForm.status"
placeholder="请选择"
clearable
>
<el-option
v-for="(value, label) in statusMap"
:key="value"
......@@ -203,7 +205,12 @@ export default {
></el-option> </el-select
></el-form-item>
<el-form-item label="操作单排版状态">
<el-select v-model="searchForm.layout" placeholder="请选择" clearable>
<el-select
v-model="searchForm.layout"
placeholder="请选择"
clearable
style="width: 120px;"
>
<el-option label="排版成功" :value="true"></el-option>
<el-option label="排版失败" :value="false"> </el-option> </el-select
></el-form-item>
......
<script>
import BaseTable from "../../components/BaseTable.vue";
const { remote, ipcRenderer } = require("electron");
const { getDTFSetting } = require("@/server/utils/store");
const { getDTFSetting, setDTFSetting } = require("@/server/utils/store");
const { Menu } = remote;
const path = require("path");
const { pathMap } = require("@/config/index.js");
......@@ -26,7 +26,7 @@ export default {
searchForm: {},
radio: "",
templateBatchArrangeNum: "",
pollTimer: null,
dtfSetting: getDTFSetting(),
isFetching: false,
pageInfo: {
......@@ -107,63 +107,25 @@ export default {
};
},
mounted() {
this.loadLocalConfig();
this.fetchBatchList();
this.pollTimer = setInterval(() => this.fetchBatchList(), 300000);
// 单文件下载成功
ipcRenderer.on("download:file-success", (_, data) => {
console.log(`批次 ${data.batchArrangeNum}${data.fileName} 下载完成`);
});
// 单文件下载失败
ipcRenderer.on("download:file-error", (_, data) => {
this.$message.error(
`批次 ${data.batchArrangeNum}${data.fileName} 下载失败,${data.msg}`
);
});
// 新增:批次校验失败,批量提示
ipcRenderer.on("download:batch-invalid", (_, invalidList) => {
const msg = invalidList
.map(item => `【${item.batchArrangeNum}${item.reason}`)
.join(";");
this.$message.warning(`存在异常批次:${msg}`);
});
},
beforeDestroy() {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
ipcRenderer.removeAllListeners("download:status-reply");
ipcRenderer.removeAllListeners("download:file-success");
ipcRenderer.removeAllListeners("download:file-error");
ipcRenderer.removeAllListeners("download:batch-invalid");
ipcRenderer.removeAllListeners("download:all-finish");
},
methods: {
getDownloadStatus() {
return new Promise(resolve => {
// 只监听一次回复,避免重复绑定
ipcRenderer.once("download:status-reply", (_, status) => {
resolve(status);
});
// 发送请求
ipcRenderer.send("download:get-status");
});
async loadLocalConfig() {
try {
if (!getDTFSetting().downloadDir) {
const path = ipcRenderer.sendSync("get-install-path");
setDTFSetting({ downloadDir: path });
}
} catch (error) {
console.log(error);
}
},
//定时任务
async fetchBatchList(value) {
// 先判断:有下载任务在跑,本轮直接跳过
// console.log("this.isFetching", this.isFetching);
if (this.isFetching) return;
this.isFetching = true;
try {
const status = await this.getDownloadStatus();
// console.log("status", status);
if (status.isRunning) return;
let params = {
currentPage: this.pageInfo.currentPage,
pageSize: this.pageInfo.pageSize
......@@ -179,12 +141,9 @@ export default {
this.tableList = res.data.records || [];
this.pageInfo.total = res.data.total;
ipcRenderer.sendSync("download:submit-batch-list", this.tableList);
}
} catch (err) {
console.error("批次列表拉取失败", err);
} finally {
this.isFetching = false;
}
},
......@@ -195,7 +154,7 @@ export default {
handleSizeChange(size) {
this.pageInfo.pageSize = size;
this.pageInfo.currentPage = 1;
this.loadData();
this.settingRes();
},
handleCurrentChange(page) {
this.pageInfo.currentPage = page;
......@@ -245,6 +204,7 @@ export default {
[3, 5].includes(value.layoutStatus)
) {
this.dialogVisible = true;
this.radio = "";
this.templateBatchArrangeNum = value.batchArrangeNum;
} else {
this.$message(
......@@ -305,15 +265,18 @@ export default {
const { data } = await this.$api.post(pathMap["listMaterialsByBatch"], {
batchArrangeNum: value
});
const newUrls = data.flatMap(el => el.urls);
// console.log(311, newUrls);
// return;
const res = await this.$api.post("/downloadMaterial", {
urls: data.urls,
urls: newUrls,
targetPath: path.join(
getDTFSetting().downloadDir,
`批次号:${value}`,
"原素材"
)
});
console.log("listMaterialsByBatch-", data);
// console.log("listMaterialsByBatch-", data);
console.log("downloadMaterial", res);
this.$message("下载进行中,下载完成的素材将在文件夹原素材中显示");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment