Commit 1a6d16c7 by linjinhong

feat:对接后端接口

parent 4fbb5e26
......@@ -4,8 +4,8 @@ import { app, protocol, BrowserWindow, globalShortcut } from "electron";
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
import { createServer } from "@/server/index.js";
import { autoUpdater } from "electron-updater";
const axios = require("axios");
const createDownloadQueue = require("@/utils/download-queue");
const { createBatchDownloadQueue } = require("@/utils/download-queue");
const fs = require("fs");
import path from "path"; // 引入 path 模块
......@@ -31,23 +31,32 @@ const winURL =
: `file://${__dirname}/index.html`;
// 全局唯一下载队列,服务启动时初始化一次
const downloadQueue = createDownloadQueue({
concurrency: 2,
const downloadQueue = createBatchDownloadQueue({
concurrency: 3,
retryTimes: 2,
// 根目录:系统下载目录/排版素材,可自行修改为固定盘符
basePath: path.join(app.getPath("downloads"), "自动排版素材"),
// 文件名规则,可自定义
fileNameRule: task => `${task.batchArrangeNum}_自动排版素材.zip`,
basePath: "",
// 单文件下载成功
onSuccess: task => {
console.log(`批次 ${task.batchArrangeNum} 下载完成`);
win.webContents.send("download:success", task.batchArrangeNum);
win?.webContents.send("download:file-success", {
batchArrangeNum: task.batchArrangeNum,
fileName: task.fileName
});
},
// 单文件下载失败
onError: (task, err) => {
console.error(`批次 ${task.batchArrangeNum} 下载失败:`, err.message);
win.webContents.send("download:error", {
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");
}
});
......@@ -504,31 +513,13 @@ async function createWindow() {
const { width, height } = win.getBounds();
win.webContents.send("window-size", { width, height });
});
// 1. 批量添加下载任务
ipcMain.on("download:addTasks", (event, tasks) => {
const BASE_URL = "";
// 给每个任务挂载获取下载地址的方法
const taskList = tasks.map(item => ({
...item,
fetchUrl: async () => {
const params = {};
const res = await axios({
method: "get",
url: `${BASE_URL}/material/getDownloadUrl`,
params
})({
batchArrangeNum: item.batchArrangeNum
});
return res.data.downloadUrl;
}
}));
downloadQueue.addTasks(taskList);
event.returnValue = { code: 200, msg: "已加入队列" };
// IPC 通信
ipcMain.on("download:submit-batch-list", (event, list) => {
downloadQueue.addBatchList(list);
event.returnValue = { code: 200, msg: "已处理" };
});
// 2. 查询队列运行状态
ipcMain.on("download:getStatus", event => {
ipcMain.on("download:get-status", event => {
event.returnValue = { isRunning: downloadQueue.isRunning };
});
}
......
......@@ -44,18 +44,6 @@
v-html="col.render(scope.row, scope.column, scope.$index)"
></span>
<!-- 2. tag标签 -->
<el-tag
v-else-if="col.type === 'tag'"
:type="col.getTagType ? col.getTagType(scope.row) : 'primary'"
:effect="col.tagEffect || 'light'"
size="mini"
>
{{
col.getTagText ? col.getTagText(scope.row) : scope.row[col.prop]
}}
</el-tag>
<!-- 3. 图片 -->
<el-image
v-else-if="col.type === 'image'"
......@@ -107,7 +95,7 @@
class="table-pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageInfo.pageNum"
:current-page="pageInfo.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageInfo.pageSize"
layout="total, sizes, prev, pager, next, jumper"
......@@ -170,12 +158,12 @@ export default {
},
/**
* 分页信息
* { pageNum: 1, pageSize: 10, total: 0 }
* { currentPage: 1, pageSize: 10, total: 0 }
*/
pageInfo: {
type: Object,
default: () => ({
pageNum: 1,
currentPage: 1,
pageSize: 10,
total: 0
})
......
......@@ -34,7 +34,36 @@ export const pathMap = {
},
checkInventory: {
OP: "factory/podOrderOperation/check-inventory"
}
},
//烫画
//获取生产配置
productionConfig: "api/factory/production-config",
//更新生产配置
productionConfigUpdate: "/api/factory/production-config/update",
//排版列表(分页)
productionSoftware:
"/api/factory/podOrderBatchDownload/page-production-software",
//查询该批次下的所有素材
listMaterialsByBatch:
"/api/factory/podOrderBatchDownload/list-materials-by-batch",
//重新合成排版
psReComposingDesignImages:
"/api/factory/podOrderBatchDownload/psReComposingDesignImages",
//根据批次分页查询操作单
listOperationByBatch:
"/api/factory/podOrderBatchDownload/listOperationByBatch"
};
// 操作单状态枚举
export const statusMap = {
待排单: "PENDING_SCHEDULE",
待拣胚: "PENDING_PICK",
待补胚: "PENDING_REPLENISH",
生产中: "IN_PRODUCTION",
待配货: "PENDING_PACKING",
配货完成: "PACKING_COMPLETED",
已完成: "COMPLETED",
已取消: "CANCELLED",
已归档: "ARCHIVE"
};
export function autoRegisterRouter(router, funcKey, handler) {
......
......@@ -6,7 +6,7 @@ import {
copySingleImage,
createBatchFolder
} from "@/server/utils";
const { downloadPngList } = require("@/utils/download-queue");
const {
cropImageTransparentEdges,
cropTransparentEdges,
......@@ -668,12 +668,119 @@ export default {
},
downloadMaterial: async (req, res) => {
try {
const { downloadDir, isArray, basePath } = req.body;
res.send();
const { urls, targetPath } = req.body;
const data = await downloadPngList(urls, targetPath);
res.send(data);
} catch (error) {
console.log("downloadMaterial", error);
res.json({ code: 500, msg: error });
}
},
//获取生产配置
productionConfig: async (req, res) => {
env = getHostApi().apiApiHost;
const token = req.headers["jwt-token"];
let url = pathMap["productionConfig"];
try {
let { data } = await axios.get(`${env}/${url}`, {
headers: { "jwt-token": token }
});
res.send(data);
} catch (error) {
console.log("checkInventory", error);
res.json({ code: 500, msg: error });
}
},
//更新生产配置
productionConfigUpdate: async (req, res) => {
env = getHostApi().apiApiHost;
const token = req.headers["jwt-token"];
const params = req.body;
let url = pathMap["productionConfigUpdate"];
try {
let { data } = await axios.post(`${env}/${url}`, {
headers: { "jwt-token": token },
params
});
res.send(data);
} catch (error) {
console.log("checkInventory", error);
res.json({ code: 500, msg: error });
}
},
//排版列表(分页)
productionSoftware: async (req, res) => {
env = getHostApi().apiApiHost;
const token = req.headers["jwt-token"];
const params = req.body;
let url = pathMap["productionSoftware"];
try {
let { data } = await axios.post(`${env}/${url}`, {
headers: { "jwt-token": token },
params
});
res.send(data);
} catch (error) {
console.log("checkInventory", error);
res.json({ code: 500, msg: error });
}
},
//查询该批次下的所有素材
listMaterialsByBatch: async (req, res) => {
env = getHostApi().apiApiHost;
const token = req.headers["jwt-token"];
const params = req.body;
let url = pathMap["listMaterialsByBatch"];
try {
let { data } = await axios.get(`${env}/${url}`, {
headers: { "jwt-token": token },
params
});
res.send(data);
} catch (error) {
console.log("checkInventory", error);
res.json({ code: 500, msg: error });
}
},
//重新合成排版
psReComposingDesignImages: async (req, res) => {
env = getHostApi().apiApiHost;
const token = req.headers["jwt-token"];
const params = req.body;
let url = pathMap["psReComposingDesignImages"];
try {
let { data } = await axios.get(`${env}/${url}`, {
headers: { "jwt-token": token },
params
});
res.send(data);
} catch (error) {
console.log("checkInventory", error);
res.json({ code: 500, msg: error });
}
},
//根据批次分页查询操作单
listOperationByBatch: async (req, res) => {
env = getHostApi().apiApiHost;
const token = req.headers["jwt-token"];
const params = req.body;
let url = pathMap["listOperationByBatch"];
try {
let { data } = await axios.post(`${env}/${url}`, {
headers: { "jwt-token": token },
params
});
res.send(data);
} catch (error) {
console.log("checkInventory", error);
res.json({ code: 500, msg: error });
}
}
};
......@@ -60,6 +60,19 @@ router.post("/downloadMaterial", fn.downloadMaterial);
//复制文件夹下文件
router.post("/copySingleImageFn", fn.copySingleImageFn);
//获取生产配置
router.post("/productionConfig", fn.productionConfig);
//更新生产配置
router.post("/productionConfigUpdate", fn.productionConfigUpdate);
//排版列表(分页)
router.post("/productionSoftware", fn.productionSoftware);
//查询该批次下的所有素材
router.post("/productionSoftware", fn.productionSoftware);
//重新合成排版
router.post("/psReComposingDesignImages", fn.psReComposingDesignImages);
//根据批次分页查询操作单
router.post("/listOperationByBatch", fn.listOperationByBatch);
// 根据生产单号查询详情
autoRegisterRouter(router, "findByPodProductionNo", fn.findByPodProductionNo);
......
......@@ -215,13 +215,13 @@ module.exports = {
},
getDTFSetting: () =>
store.get("DTFSetting") || {
overWidthMode: "autoScale",
widthStrategy: "1",
// 最大素材数模式:batch / custom
maxPicMode: "batch",
materialStrategy: "1",
// 自定义素材数量
customPicNum: 30,
materialCount: 30,
// 自动排版规格 40+2 / 60 / none
autoLayoutType: "none",
autoLayoutStrategy: "3",
// 默认下载目录
downloadDir: ""
},
......
const fs = require("fs");
const path = require("path");
const http = require("http");
const https = require("https");
const { URL } = require("url");
const { getDTFSetting } = require("@/server/utils/store");
function createDownloadQueue(options = {}) {
// 配置项
export function createBatchDownloadQueue(options = {}) {
// ========== 1. 配置项 ==========
const config = Object.assign(
{
concurrency: 2,
concurrency: 3,
retryTimes: 2,
basePath: "", // 根目录,外部传入
fileNameRule: task => `${task.batchArrangeNum}_素材.zip`, // 默认文件名规则:批次号_素材.zip
onSuccess: () => {},
onError: () => {},
onIdle: () => {}
onSuccess: () => {}, // 单文件下载成功
onError: () => {}, // 单文件下载失败
onBatchInvalid: () => {}, // 批次校验失败回调(返回异常批次列表+原因)
onIdle: () => {} // 全部任务完成
},
options
);
// 内部状态
// ========== 2. 内部状态 ==========
let pendingQueue = [];
let runningCount = 0;
const successSet = new Set();
const runningSet = new Set();
let isDestroyed = false;
// 工具方法
const hasFile = targetPath => fs.existsSync(targetPath);
const cleanFile = targetPath =>
hasFile(targetPath) && fs.unlinkSync(targetPath);
// ========== 3. 通用工具方法 ==========
const getRequestModule = url => {
try {
return new URL(url).protocol === "https:" ? https : http;
} catch (e) {
return https;
}
};
const isPngUrl = url => {
try {
const urlObj = new URL(url);
return urlObj.pathname.toLowerCase().endsWith(".png");
} catch (e) {
return false;
}
};
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`;
}
};
// 流式下载核心
const fileExists = targetPath => fs.existsSync(targetPath);
const cleanBadFile = targetPath =>
fileExists(targetPath) && fs.unlinkSync(targetPath);
// ========== 4. 流式下载核心 ==========
const streamDownload = (url, targetPath) =>
new Promise((resolve, reject) => {
const ws = fs.createWriteStream(targetPath);
const req = https.get(url, res => {
// 处理重定向
const requestModule = getRequestModule(url);
const writeStream = fs.createWriteStream(targetPath);
const request = requestModule.get(url, res => {
if ([301, 302, 307, 308].includes(res.statusCode)) {
ws.close();
writeStream.close();
streamDownload(res.headers.location, targetPath)
.then(resolve)
.catch(reject);
return;
}
if (res.statusCode !== 200) {
ws.close();
cleanFile(targetPath);
reject(new Error(`状态码 ${res.statusCode}`));
writeStream.close();
cleanBadFile(targetPath);
reject(new Error(`响应状态码 ${res.statusCode}`));
return;
}
res.pipe(ws);
res.pipe(writeStream);
});
ws.on("finish", () => {
ws.close();
writeStream.on("finish", () => {
writeStream.close();
resolve();
});
ws.on("error", err => {
ws.close();
cleanFile(targetPath);
writeStream.on("error", err => {
writeStream.close();
cleanBadFile(targetPath);
reject(err);
});
req.on("error", err => {
ws.close();
cleanFile(targetPath);
request.on("error", err => {
writeStream.close();
cleanBadFile(targetPath);
reject(err);
});
req.setTimeout(30000, () => {
req.destroy();
ws.close();
cleanFile(targetPath);
request.setTimeout(30000, () => {
request.destroy();
writeStream.close();
cleanBadFile(targetPath);
reject(new Error("下载超时"));
});
});
// 队列消费
// ========== 5. 队列消费逻辑 ==========
const consume = () => {
if (isDestroyed) return;
if (runningCount >= config.concurrency || pendingQueue.length === 0) {
......@@ -85,16 +116,11 @@ function createDownloadQueue(options = {}) {
runningCount++;
(async () => {
try {
// 自动创建当前批次的目录(仅创建需要下载的批次目录,不批量预创建)
const targetDir = path.dirname(task.targetPath);
if (!hasFile(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
// 获取下载地址
const url =
typeof task.fetchUrl === "function"
? await task.fetchUrl(task)
: task.downloadUrl;
await streamDownload(url, 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) {
......@@ -112,36 +138,94 @@ function createDownloadQueue(options = {}) {
})();
};
// 对外方法
// ========== 6. 对外暴露方法 ==========
return {
addTasks(tasks = []) {
if (isDestroyed) return;
const validTasks = tasks
.map(row => {
// 按规则拼接完整路径:根目录 / 批次号:xxx / 自动排版素材 / 文件名
const dir = path.join(
config.basePath,
`批次号:${row.batchArrangeNum}`,
"自动排版素材"
);
const fileName = config.fileNameRule(row);
const targetPath = path.join(dir, fileName);
return { ...row, targetPath, remainRetry: config.retryTimes };
})
// 三重过滤:本地已存在、已下载成功、正在队列中
.filter(
task =>
!hasFile(task.targetPath) &&
!successSet.has(task.targetPath) &&
!runningSet.has(task.targetPath)
);
addBatchList(list = []) {
if (isDestroyed || !Array.isArray(list)) return;
if (validTasks.length === 0) return;
validTasks.forEach(t => {
runningSet.add(t.targetPath);
pendingQueue.push(t);
// 第一步:筛选 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;
}
// 校验2:URL 为空字符串/纯空格
if (batch.url.trim() === "") {
invalidBatchList.push({
batchArrangeNum: batchNo,
reason: "下载链接为空"
});
return;
}
// 第三步:拆分多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格式下载链接"
});
return;
}
// 第四步:生成合法下载任务,去重过滤
const dir = path.join(
getDTFSetting().downloadDir,
`批次号:${batchNo}`,
"自动排版素材"
);
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
});
}
});
});
consume();
// 有异常批次,通过回调抛出
if (invalidBatchList.length > 0) {
config.onBatchInvalid(invalidBatchList);
}
// 启动下载队列
if (fileTasks.length > 0) {
fileTasks.forEach(task => {
runningSet.add(task.targetPath);
pendingQueue.push(task);
});
consume();
}
},
get isRunning() {
......@@ -158,4 +242,158 @@ function createDownloadQueue(options = {}) {
};
}
module.exports = createDownloadQueue;
/**
* 批量下载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
};
return new Promise(resolve => {
// 参数合法性校验
if (!Array.isArray(urls) || urls.length === 0) {
resolve({
success: [],
failed: [{ url: "", error: "下载链接数组为空" }]
});
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;
// 从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);
});
writeStream.on("finish", () => {
console.log("downloadPngList---finish");
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("下载超时"));
});
});
};
// 带重试的单任务执行
const runTask = async url => {
const fileName = getPngFileName(url);
const filePath = path.join(targetDir, fileName);
// 本地文件已存在,直接跳过
if (fs.existsSync(filePath)) {
successList.push(filePath);
return;
}
console.log(`开始下载:${fileName}`);
let remainRetry = config.retryTimes;
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();
});
}
// 所有任务执行完毕,返回结果
if (runningCount === 0 && currentIndex >= total) {
console.log(
`全部任务执行完成,成功:${successList.length} 个,失败:${failedList.length} 个`
);
resolve({ success: successList, failed: failedList });
}
};
next();
});
}
<script>
import { mapState } from "vuex";
const { getDesktopDevice, getDTFSetting } = require("@/server/utils/store");
const { getDTFSetting } = require("@/server/utils/store");
const { ipcRenderer } = require("electron");
export default {
......@@ -18,39 +17,13 @@ export default {
return {
dialogVisible: false,
dtfSetting: getDTFSetting(),
radio: ""
radio: "",
searchForm: {}
};
},
computed: {
...mapState([
"defaultProportion",
"countryList",
"orderType",
"grid",
"imgList"
])
},
mounted() {
this.$nextTick(() => {
this.$refs.searchRef?.focus();
if (this.desktopDevice !== 3) {
this.selectGridIndex = 0;
this.setting.gridValue = 0;
} else {
this.selectGridIndex = 5;
this.setting.gridValue = 5;
}
});
this.$store.commit("changeDesktopDevice", getDesktopDevice());
},
created() {
localStorage.setItem("desktoVersion", "print");
if (this.$dataStore.get("setting")) {
this.setting = this.$dataStore.get("setting");
} else {
this.$dataStore.set("setting", this.setting);
}
},
computed: {},
mounted() {},
created() {},
methods: {
dropdownCommand(v) {
......@@ -87,13 +60,10 @@ export default {
goBack() {
this.$router.push({ name: "design-dtf" });
},
goSetting() {
this.$router.push({
path: "/DTFsetting",
query: { id: 123 }
});
goSetting() {},
getDataInfo() {
this.$emit("search", this.searchForm);
},
getDataInfo() {},
productionCompleted() {
this.$confirm("确认完成生产?", "提示", {
confirmButtonText: "确定",
......@@ -106,13 +76,12 @@ export default {
changeTypesetting() {
this.dialogVisible = true;
},
submit() {
console.log(this.radio);
async submit() {
if (!this.radio) {
return this.$message.error("请选择排版宽度");
}
if (this.dtfSetting.overWidthMode === "biggerSize" && this.radio == 60) {
if (this.dtfSetting.widthStrategy == 3 && this.radio == 60) {
this.$confirm(
"由于排版设置中选择“按更大规格排版”仅适用于40+2,选60时,默认超60的规格不排版",
"提示",
......@@ -120,13 +89,19 @@ export default {
confirmButtonText: "确定",
type: "warning"
}
).then(() => {
this.dialogVisible = false;
this.$message(
"排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看"
);
);
await this.$api.post("/psReComposingDesignImages", {
batchArrangeNum: this.templateBatchArrangeNum,
templateWidth: this.radio
});
this.$message(
"排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看"
);
} else {
await this.$api.post("/psReComposingDesignImages", {
batchArrangeNum: this.templateBatchArrangeNum,
templateWidth: this.radio
});
this.$message(
"排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看"
);
......@@ -161,8 +136,26 @@ export default {
>返回</el-button
></el-form-item
>
<el-form-item> <el-input></el-input></el-form-item>
<el-form-item label="操作单状态"> <el-input></el-input></el-form-item>
<el-form-item>
<el-input
placeholder="请扫描操作单"
v-model="searchForm.operationNo"
></el-input
></el-form-item>
<el-form-item label="操作单状态">
<el-select v-model="searchForm.status" placeholder="请选择" clearable>
<el-option
v-for="(value, label) in statusMap"
:key="value"
:label="label"
:value="value"
></el-option> </el-select
></el-form-item>
<el-form-item label="操作单排版状态">
<el-select v-model="searchForm.layout" placeholder="请选择">
<el-option label="是" :value="true"></el-option>
<el-option label="否" :value="false"> </el-option> </el-select
></el-form-item>
<el-form-item>
<el-button @click="getDataInfo" type="primary"
......
......@@ -18,12 +18,7 @@ export default {
}),
type: Object
},
factoryType: { type: String, default: "CN" },
cardConfig: {
imageSrc: "./src/assets/template-bg-1.jpg",
title: "title",
time: "time"
}
factoryType: { type: String, default: "CN" }
},
data() {
return {
......@@ -31,7 +26,7 @@ export default {
detailVisible: false,
logList: [],
pageInfo: {
pageNum: 1,
currentPage: 1,
pageSize: 10,
total: 2
},
......@@ -49,14 +44,14 @@ export default {
};
}),
detailRow: {
name: { label: "批次号", value: 111 },
name1: { label: "操作单数量", value: 111 },
name2: { label: "素材数量", value: 111 },
name3: { label: "工艺类型", value: 111 },
name6: { label: "规范素材", value: 111 },
name4: { label: "创建时间", value: 111 },
name5: { label: "排版状态", value: 111 },
name7: { label: "操作单状态", value: 111 }
batchArrangeNumber: { label: "批次号", value: 111 },
operationNum: { label: "操作单数量", value: 111 },
materialNum: { label: "素材数量", value: 111 },
craftType: { label: "工艺类型", value: 111 },
standardDesignImage: { label: "规范素材", value: 111 },
createTime: { label: "创建时间", value: 111 },
layoutStatus: { label: "排版状态", value: 111 },
status: { label: "操作单状态", value: 111 }
}
};
},
......@@ -180,30 +175,25 @@ export default {
{{ item.operationNo }}
</span>
</template>
<template #top_left>
<template #top_right>
<el-tooltip
v-if="item.outOfStock"
v-if="item.layout"
effect="light"
content="缺货"
content="排版完成"
placement="bottom"
>
<div
style="
background-color: #f56c6c;
background-color: #f0f9eb;
color: #fff;
padding: 2px 4px;
border-radius: 4px;
font-size: 12px;
"
>
</div>
</el-tooltip>
<span v-if="item.tagsId" class="flex-row flex-row-gap6">
<el-tag v-for="tag in getItemTags(item.tagsId)" :key="tag.id">
{{ tag.name }}
</el-tag>
</span>
</template>
<template #images>
......@@ -291,9 +281,9 @@ export default {
<div class="card-info-row full">
<span
class="info-value ellipsis"
:title="item.productName || ''"
:title="item.operationNo || ''"
>
{{ item.productName }}
{{ item.operationNo }}
</span>
</div>
<div class="card-info-row">
......
<script>
import { mapState } from "vuex";
const { getDesktopDevice } = require("@/server/utils/store");
export default {
props: {
user: {
......@@ -13,38 +11,13 @@ export default {
factoryType: { type: String, default: "CN" }
},
data() {
return {};
},
computed: {
...mapState([
"defaultProportion",
"countryList",
"orderType",
"grid",
"imgList"
])
},
mounted() {
this.$nextTick(() => {
this.$refs.searchRef?.focus();
if (this.desktopDevice !== 3) {
this.selectGridIndex = 0;
this.setting.gridValue = 0;
} else {
this.selectGridIndex = 5;
this.setting.gridValue = 5;
}
});
this.$store.commit("changeDesktopDevice", getDesktopDevice());
},
created() {
localStorage.setItem("desktoVersion", "print");
if (this.$dataStore.get("setting")) {
this.setting = this.$dataStore.get("setting");
} else {
this.$dataStore.set("setting", this.setting);
}
return {
searchForm: { batchArrangeNumber: "", layoutStatus: "", startTime: "" }
};
},
computed: {},
mounted() {},
created() {},
methods: {
dropdownCommand(v) {
......@@ -78,14 +51,7 @@ export default {
return "";
},
goBack() {
// Vue路由返回
this.$router.push({
name: "design"
});
// 如果需要关闭当前弹窗/子窗口,搭配:
// ipcRenderer.send("close-current-win");
},
goSetting() {
this.$router.push({
name: "dtf-setting"
......@@ -96,7 +62,9 @@ export default {
name: "dtf-batch-detail"
});
},
getDataInfo() {}
getDataInfo() {
this.$emit("search", this.searchForm);
}
}
};
</script>
......@@ -104,10 +72,39 @@ export default {
<template>
<div>
<div class="page-header">
<el-form style="display: flex;flex-wrap: nowrap;" inline>
<el-form-item label="批次号"> <el-input></el-input></el-form-item>
<el-form-item label="排版状态"> <el-input></el-input></el-form-item>
<el-form-item label="创建时间"> <el-input></el-input></el-form-item>
<el-form
style="display: flex;flex-wrap: nowrap;"
inline
:model="searchForm"
>
<el-form-item label="批次号">
<el-input
v-model="searchForm.batchArrangeNumber"
placeholder="请输入批次号"
></el-input
></el-form-item>
<el-form-item label="排版状态">
<el-select
v-model="searchForm.layoutStatus"
placeholder="请选择排版状态"
>
<el-option label="待排版" :value="1"></el-option>
<el-option label="排版中" :value="2"> </el-option>
<el-option label="排版成功" :value="3"> </el-option>
<el-option label="排版失败" :value="4"> </el-option>
<el-option label="部分成功" :value="5"> </el-option>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="searchForm.startTime"
type="datetime"
placeholder="选择创建时间"
format="yyyy 年 MM 月 dd 日"
value-format="yyyy-MM-dd"
>
</el-date-picker
></el-form-item>
<el-form-item>
<el-button @click="getDataInfo" type="primary"
>查询
......@@ -118,12 +115,6 @@ export default {
>DTF排版设置
</el-button></el-form-item
>
<el-form-item>
<el-button @click="goSetting1" type="success"
>详情页
</el-button></el-form-item
>
</el-form>
<div class="right-user">
......
......@@ -3,6 +3,7 @@ import BaseTable from "../../components/BaseTable.vue";
const { remote, ipcRenderer } = require("electron");
const { getDTFSetting } = require("@/server/utils/store");
const { Menu } = remote;
const path = require("path");
import DTFhead from "./components/head.vue";
export default {
......@@ -23,30 +24,20 @@ export default {
dialogVisible: false,
selectionList: [],
radio: "",
templateBatchArrangeNum: "",
dtfSetting: getDTFSetting(),
pageInfo: {
pageNum: 1,
currentPage: 1,
pageSize: 10,
total: 2
},
tableList: Array.from({ length: 50 }, (_, index) => {
const isDtg = index % 2 === 0;
return {
id: index + 1,
batchArrangeNum: index + 1,
status: index % 3 === 0 ? 0 : 1,
img: isDtg ? "/static/dtg.jpg" : "/static/dtf.jpg",
subOrderNumber: index % 4 !== 0,
num: isDtg ? 120 + index * 2 : 86 + index,
failedReason: ""
};
}),
tableList: [],
tableConfig: [
{
prop: "batchArrangeNum",
label: "批次号",
width: 100,
slotName: "batchNo"
slotName: "batchArrangeNum"
},
{
prop: "operationNum",
......@@ -68,7 +59,13 @@ export default {
prop: "standardDesignImage",
label: "规范素材",
width: 100,
slotName: "standardDesignImage"
render(row) {
return row.standardDesignImage == 0
? "否"
: row.standardDesignImage == 1
? "是"
: "混合";
}
},
{
label: "创建时间",
......@@ -77,7 +74,8 @@ export default {
{
label: "排版状态",
width: 140,
prop: "status"
prop: "layoutStatus",
slotName: "layoutStatus"
},
{
prop: "failReason",
......@@ -87,7 +85,7 @@ export default {
},
{
label: "排版参数",
prop: "layoutParameters",
prop: "composingParam",
width: 300
},
{
......@@ -95,68 +93,88 @@ export default {
width: 260,
slotName: "operation"
}
]
],
layoutStatus: {
1: { label: "待排版", type: "warning" },
2: { label: "排版中", type: "info" },
3: { label: "排版成功", type: "success" },
4: { label: "排版失败", type: "danger" },
5: { label: "部分成功", type: "success" }
}
};
},
mounted() {
this.fetchBatchList();
// 开启5分钟轮询
this.pollTimer = setInterval(() => {
this.fetchBatchList();
}, 5 * 60 * 1000);
this.pollTimer = setInterval(() => this.fetchList(), 300000);
// 监听下载结果
ipcRenderer.on("download:success", (_, batchNum) => {
this.$message.success(`批次 ${batchNum} 素材下载完成`);
// 单文件下载成功
ipcRenderer.on("download:file-success", (_, data) => {
console.log(`批次 ${data.batchArrangeNum}${data.fileName} 下载完成`);
});
ipcRenderer.on("download:error", (_, { batchArrangeNum, msg }) => {
this.$message.error(`批次 ${batchArrangeNum} 下载失败:${msg}`);
// 单文件下载失败
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() {
clearInterval(this.pollTimer);
ipcRenderer.removeAllListeners("download:success");
ipcRenderer.removeAllListeners("download:error");
ipcRenderer.removeAllListeners("download:file-success");
ipcRenderer.removeAllListeners("download:file-error");
ipcRenderer.removeAllListeners("download:batch-invalid");
ipcRenderer.removeAllListeners("download:all-finish");
},
methods: {
async fetchBatchList() {
//定时任务
async fetchBatchList(value) {
// 先判断:有下载任务在跑,本轮直接跳过
const status = ipcRenderer.sendSync("download:getStatus");
const status = ipcRenderer.sendSync("download:get-status");
if (status.isRunning) return;
let params = {
currentPage: this.pageInfo.currentPage,
pageSize: this.pageInfo.pageSize
};
if (value) {
params = { ...params, ...value };
}
try {
// const res = await getBatchListApi();
let res;
const res = await await this.$api.post("/productionSoftware", params);
if (res.code === 200) {
this.tableData = res.data || [];
this.autoHandleDownload();
this.tableData = res.data.records || [];
this.pageInfo.total = res.data.total;
ipcRenderer.sendSync("download:submit-batch-list", this.tableData);
}
} catch (err) {
console.error("批次列表拉取失败", err);
}
},
autoHandleDownload() {
// 1. 过滤出 status 为“是”的批次
const finishList = this.tableData.filter(item => item.status === "是");
if (finishList.length === 0) return;
// 2. 直接发给主进程,主进程会自动校验本地文件、过滤已存在的批次
ipcRenderer.sendSync("download:addTasks", finishList);
},
handleSelect(rows) {
console.log("选中行", rows);
this.selectionList = rows;
},
handleSizeChange(size) {
this.pageInfo.pageSize = size;
this.pageInfo.pageNum = 1;
this.pageInfo.currentPage = 1;
this.loadData();
},
handleCurrentChange(page) {
this.pageInfo.pageNum = page;
this.pageInfo.currentPage = page;
this.loadData();
},
//右键选择器
openMenu(e) {
e.preventDefault();
const template = [
......@@ -174,12 +192,14 @@ export default {
const menu = Menu.buildFromTemplate(template);
menu.popup({ x: e.clientX, y: e.clientY });
},
//查看排版文件
async goFliePage(value, type) {
console.log(value);
let { data } = await this.$api.post("/createBatchFolder", {
basePath: this.dtfSetting.downloadDir,
downloadDir: value.batchNo,
downloadDir: value.batchArrangeNum,
isArray: false
});
console.log("goFliePage", data);
......@@ -187,22 +207,26 @@ export default {
ipcRenderer.send("open-folder-only", data);
}
},
changeTypesetting() {
if (this.dtfSetting.autoLayoutType == "none") {
//打开手动排版弹窗
changeTypesetting(value) {
if (this.dtfSetting.autoLayoutStrategy == 3) {
this.dialogVisible = true;
this.templateBatchArrangeNum = value;
} else {
this.$message(
"排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看"
);
}
},
submit() {
console.log(this.radio);
//提交手动排版
async submit() {
if (!this.radio) {
return this.$message.error("请选择排版宽度");
}
if (this.dtfSetting.overWidthMode === "biggerSize" && this.radio == 60) {
if (this.dtfSetting.widthStrategy == 3 && this.radio == 60) {
this.$confirm(
"由于排版设置中选择“按更大规格排版”仅适用于40+2,选60时,默认超60的规格不排版",
"提示",
......@@ -210,27 +234,56 @@ export default {
confirmButtonText: "确定",
type: "warning"
}
).then(() => {
this.$message(
"排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看"
);
);
await this.$api.post("/psReComposingDesignImages", {
batchArrangeNum: this.templateBatchArrangeNum,
templateWidth: this.radio
});
this.$message(
"排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看"
);
} else {
await this.$api.post("/psReComposingDesignImages", {
batchArrangeNum: this.templateBatchArrangeNum,
templateWidth: this.radio
});
this.$message(
"排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看"
);
}
},
//跳转批次详情页
toDetail(value) {
this.$router.push({
name: "dtf-batch-detail",
params: {
batchNo: value
batchArrangeNum: value
}
});
},
downloadFile() {
this.$message("下载进行中,下载完成的素材将在文件夹原素材中显示");
//下载批次原素材
async downloadFile(value) {
try {
const { data } = await this.$api.post("/listMaterialsByBatch", {
batchArrangeNum: value
});
const res = await this.$api.post("/downloadMaterial", {
urls: data.urls,
targetPath: path.join(
getDTFSetting().downloadDir,
`批次号:${value}`,
"原素材"
)
});
console.log("listMaterialsByBatch-", data);
console.log("downloadMaterial", res);
this.$message("下载进行中,下载完成的素材将在文件夹原素材中显示");
} catch (error) {
console.log(error);
}
}
}
};
......@@ -238,7 +291,11 @@ export default {
<template>
<div class="page" @contextmenu.prevent="openMenu">
<DTFhead :user="user" :factoryType="factoryType"></DTFhead>
<DTFhead
:user="user"
:factoryType="factoryType"
@search="fetchBatchList"
></DTFhead>
<BaseTable
v-if="tableList.length"
ref="tableRefs"
......@@ -256,11 +313,16 @@ export default {
size="mini"
>
<!-- 自定义插槽示例 -->
<template #batchNo="scope">
<a href="javascript:;" @click="toDetail(scope.row.batchNo)">{{
scope.row.batchNo
<template #batchArrangeNum="scope">
<a href="javascript:;" @click="toDetail(scope.row.batchArrangeNum)">{{
scope.row.batchArrangeNum
}}</a>
</template>
<template #layoutStatus="scope">
<el-tag :type="layoutStatus[scope.row?.layoutStatus].type">{{
layoutStatus[scope.row?.layoutStatus].label
}}</el-tag>
</template>
<template #failReason="scope">
<div style="white-space: pre-line">
<span v-html="scope.row?.failReason"></span>
......@@ -272,21 +334,21 @@ export default {
<el-button
type="text"
class="btn flex btnBox"
@click="downloadFile(scope.row)"
@click="downloadFile(scope.row.batchArrangeNum)"
>
<img src="../../assets/download.png" width="30" height="30" />
<div>下载素材</div></el-button
>
<el-button
type="text"
disabled
:disabled="scope.row.layoutStatus == 2"
class="btn flex btnBox"
@click="changeTypesetting(scope.row)"
@click="changeTypesetting(scope.row.batchArrangeNum)"
><img
src="../../assets/typesetting.png"
width="30"
height="30"
:class="{ disabled: false }"
:class="{ disabled: scope.row.layoutStatus == 2 }"
/>
<div>排版</div>
</el-button>
......
......@@ -21,26 +21,24 @@
<div class="setting-group">
<div class="group-title">素材超排版宽度时:</div>
<el-radio-group v-model="form.overWidthMode">
<el-radio label="autoScale">自动缩小素材,适配烫画模宽度</el-radio>
<el-radio label="skip">超出排版宽度不排版</el-radio>
<el-radio label="biggerSize"
>超出排版宽度,使用更大规格排版</el-radio
>
<el-radio-group v-model="form.widthStrategy">
<el-radio :label="1">自动缩小素材,适配烫画模宽度</el-radio>
<el-radio :label="2">超出排版宽度不排版</el-radio>
<el-radio :label="3">超出排版宽度,使用更大规格排版</el-radio>
</el-radio-group>
</div>
<div class="setting-group">
<div class="group-title">单张排版最大素材数:</div>
<el-radio-group v-model="form.maxPicMode">
<el-radio label="batch">批次素材数</el-radio>
<el-radio-group v-model="form.materialStrategy">
<el-radio :label="1">批次素材数</el-radio>
<div class="custom-num-row">
<el-radio label="custom">自定义素材数(建议数量30):</el-radio>
<el-radio :label="2">自定义素材数(建议数量30):</el-radio>
<el-input
v-model.number="form.customPicNum"
v-model.number="form.materialCount"
placeholder="请输入素材数"
style="width: 180px; margin-left: 8px"
:disabled="form.maxPicMode !== 'custom'"
:disabled="form.materialStrategy !== '2'"
/>
</div>
</el-radio-group>
......@@ -48,10 +46,10 @@
<div class="setting-group">
<div class="group-title">自动排版设置</div>
<el-radio-group v-model="form.autoLayoutType">
<el-radio label="40+2">40+2</el-radio>
<el-radio label="60">60</el-radio>
<el-radio label="none">不执行自动排版</el-radio>
<el-radio-group v-model="form.autoLayoutStrategy">
<el-radio :label="1">40+2</el-radio>
<el-radio :label="2">60</el-radio>
<el-radio :label="3">不执行自动排版</el-radio>
</el-radio-group>
</div>
......@@ -78,25 +76,19 @@
<script>
const { ipcRenderer } = require("electron");
const {
getProductType,
setProductType,
setDTFSetting,
getDTFSetting
} = require("@/server/utils/store");
const { setDTFSetting, getDTFSetting } = require("@/server/utils/store");
export default {
name: "dtf-setting",
data() {
return {
// 表单配置,可从本地配置文件读取初始化
form: { ...getDTFSetting() },
activeName: getProductType()
form: { ...getDTFSetting() }
};
},
mounted() {
// 页面加载读取本地配置回填表单
this.loadLocalConfig();
// this.loadLocalConfig();
},
methods: {
// 返回上一页
......@@ -105,12 +97,13 @@ export default {
},
// 读取本地配置
async loadLocalConfig() {
ipcRenderer.send("read-layout-config");
ipcRenderer.once("layout-config-data", (_, cfg) => {
if (cfg) {
this.form = Object.assign({}, this.form, cfg);
}
});
try {
const { data } = await this.$api.post("/productionConfig");
this.form = { ...data };
setDTFSetting({ ...this.form });
} catch (error) {
console.log(error);
}
},
// 选择文件夹弹窗
selectFolder() {
......@@ -120,11 +113,8 @@ export default {
});
},
// 保存配置到本地文件
saveConfig() {
if (
this.form.autoLayoutType == "60" &&
this.form.overWidthMode === "biggerSize"
) {
async saveConfig() {
if (this.form.autoLayoutStrategy == 2 && this.form.widthStrategy == 3) {
this.$confirm(
"由于排版设置中选择“按更大规格排版”仅适用于40+2,选60时,默认超60的规格不排版",
"提示",
......@@ -132,17 +122,11 @@ export default {
confirmButtonText: "确定",
type: "warning"
}
).then(() => {
this.$message.success("排版设置保存成功!");
});
} else {
this.$message.success("排版设置保存成功!");
);
}
await this.$api.post("/productionConfigUpdate", this.form);
setDTFSetting({ ...this.form });
},
tabClick(tab) {
if (tab.name == "DTG") this.goBack();
setProductType(tab.name);
this.$message.success("排版设置保存成功!");
}
}
};
......
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