Commit c9be6378 by wusiyi

Merge branch 'dev_heat_transfer_printing' into 'master'

Dev heat transfer printing

See merge request !2
parents e2fcbfe9 7506e6b5
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -2,7 +2,7 @@
"name": "JomallProductionAssistant",
"productName": "JomallProductionAssistant",
"description": "",
"version": "1.0.39",
"version": "1.0.40",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
......@@ -26,6 +26,7 @@
"bignumber.js": "^9.2.1",
"compressing": "^1.10.1",
"core-js": "^3.6.4",
"dayjs": "^1.11.21",
"electron-builder": "^24.13.3",
"electron-dialogs": "^1.4.0",
"electron-icon-builder": "^2.0.1",
......@@ -38,6 +39,7 @@
"express": "^4.17.1",
"fabric": "^6.7.0",
"html2canvas": "^1.4.1",
"lodash": "^4.18.1",
"lodash-id": "^0.14.0",
"log4js": "^6.9.1",
"moment": "^2.30.1",
......@@ -75,6 +77,5 @@
"pug-plain-loader": "^1.0.0",
"vue-cli-plugin-electron-builder": "^1.4.6",
"vue-template-compiler": "^2.6.11"
},
"__npminstall_done": false
}
}
......@@ -4,7 +4,11 @@ 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 registerDownloadIpc = require("@/utils/download-file.js");
import downloadQueue from "@/utils/download-queue";
const {
registerTypesettingDownloadIpc
} = require("@/utils/download-typesetting");
const fs = require("fs");
import path from "path"; // 引入 path 模块
......@@ -14,7 +18,7 @@ const { setVersion, getHostApi, setApi } = require("@/server/utils/store");
Object.defineProperty(app, "isPackaged", {
get() {
return true;
},
}
});
createServer();
const isDevelopment = process.env.NODE_ENV !== "production";
......@@ -22,13 +26,15 @@ const isDevelopment = process.env.NODE_ENV !== "production";
let win;
let newWindow;
protocol.registerSchemesAsPrivileged([
{ scheme: "app", privileges: { secure: true, standard: true } },
{ scheme: "app", privileges: { secure: true, standard: true } }
]);
const winURL =
process.env.NODE_ENV === "development"
? "http://localhost:8050"
: `file://${__dirname}/index.html`;
registerDownloadIpc();
registerTypesettingDownloadIpc();
async function createWindow() {
win = new BrowserWindow({
width: 1500,
......@@ -39,9 +45,9 @@ async function createWindow() {
webSecurity: false,
nodeIntegration: true,
contextIsolation: false, // ^1.4.0 版本必须关闭
contextIsolation: false // ^1.4.0 版本必须关闭
},
show: false,
show: false
});
win.once("ready-to-show", () => {
win.maximize();
......@@ -49,62 +55,6 @@ async function createWindow() {
});
const template = [
{
label: "文件",
submenu: [
{
label: "新建",
accelerator: "CmdOrCtrl+N",
click: () => {
console.log("新建文件");
// 这里可以添加创建新文件的逻辑
},
},
{
label: "打开",
accelerator: "CmdOrCtrl+O",
click: () => {
// 打开文件对话框
dialog
.showOpenDialog({
properties: ["openFile"],
})
.then((result) => {
if (!result.canceled) {
console.log("打开文件:", result.filePaths[0]);
}
});
},
},
{
label: "保存",
accelerator: "CmdOrCtrl+S",
click: () => {
console.log("保存文件");
},
},
{
label: "另存为",
accelerator: "CmdOrCtrl+Shift+S",
click: () => {
console.log("另存为");
},
},
{ type: "separator" },
{
label: "打印",
accelerator: "CmdOrCtrl+P",
click: () => {
const win = BrowserWindow.getFocusedWindow();
if (win) {
win.webContents.print();
}
},
},
{ type: "separator" },
],
},
{
label: "编辑",
submenu: [
{ label: "撤销", role: "undo" },
......@@ -115,8 +65,8 @@ async function createWindow() {
{ label: "粘贴", role: "paste" },
{ label: "删除", role: "delete" },
{ type: "separator" },
{ label: "全选", role: "selectAll" },
],
{ label: "全选", role: "selectAll" }
]
},
{
......@@ -130,8 +80,8 @@ async function createWindow() {
{ label: "放大", role: "zoomin" },
{ label: "缩小", role: "zoomout" },
{ type: "separator" },
{ label: "全屏切换", role: "togglefullscreen" },
],
{ label: "全屏切换", role: "togglefullscreen" }
]
},
{
......@@ -139,8 +89,8 @@ async function createWindow() {
submenu: [
{ label: "最小化", role: "minimize" },
{ label: "缩放", role: "zoom" },
{ label: "关闭", role: "close" },
],
{ label: "关闭", role: "close" }
]
},
{
label: "设置",
......@@ -159,7 +109,7 @@ async function createWindow() {
type: "error",
title: "错误",
message: "无可用窗口",
buttons: ["确定"],
buttons: ["确定"]
});
return;
}
......@@ -183,8 +133,8 @@ async function createWindow() {
contextIsolation: false,
sandbox: false,
enableRemoteModule: true,
webSecurity: false,
},
webSecurity: false
}
});
// 3. 打开开发者工具(排查用,上线后删除)
......@@ -194,7 +144,7 @@ async function createWindow() {
if (process.env.NODE_ENV === "development") {
exePath = path.join(
__dirname,
"../html/domain-prompt.html",
"../html/domain-prompt.html"
);
} else {
const newpath = path
......@@ -243,31 +193,46 @@ async function createWindow() {
}
try {
let configObj = JSON.parse(data);
configObj.apiApiHost = `https://${newDomain}/api`;
configObj.fileApiUrl = `https://${newDomain}/upload/factory`;
configObj.configPath = configPath;
const newData = JSON.stringify(configObj); // 使用2个空格缩进
const isTest = process.env.VUE_APP_ENV === "test";
console.log("isTest", isTest);
console.log("configObj", configObj);
const config = isTest ? configObj.test : configObj.prod;
config.apiApiHost = isTest
? `http://${newDomain}/api`
: `https://${newDomain}/api`;
config.fileApiUrl = isTest
? `http://${newDomain}/upload/factory`
: `https://${newDomain}/upload/factory`;
config.configPath = configPath;
// const newData = JSON.stringify(config); // 使用2个空格缩进
// console.log("修改后的数据:", newData);
configObj[isTest ? "test" : "prod"] = config;
const newConfigObj = JSON.stringify(configObj);
// 4. 写回文件
fs.writeFile(configPath, newData, "utf-8", (writeErr) => {
if (writeErr) {
console.error("写入配置文件失败:", writeErr);
this.$message.error(
`写入配置文件失败: ${writeErr.message}`,
);
return;
fs.writeFile(
configPath,
newConfigObj,
"utf-8",
writeErr => {
if (writeErr) {
console.error("写入配置文件失败:", writeErr);
this.$message.error(
`写入配置文件失败: ${writeErr.message}`
);
return;
}
console.log("配置文件更新成功", config);
setApi(config);
}
console.log("配置文件更新成功", configObj);
setApi(configObj);
});
);
} catch (parseError) {
console.error("解析 JSON 失败:", parseError);
this.$message.error(
`配置文件格式错误: ${parseError.message}`,
`配置文件格式错误: ${parseError.message}`
);
}
});
......@@ -278,7 +243,7 @@ async function createWindow() {
console.error("自定义弹窗出错:", error);
}
})();
},
}
},
{
label: "清除缓存",
......@@ -293,7 +258,7 @@ async function createWindow() {
type: "error",
title: "错误",
message: "无可用窗口",
buttons: ["确定"],
buttons: ["确定"]
});
return;
}
......@@ -317,8 +282,8 @@ async function createWindow() {
contextIsolation: false,
sandbox: false,
enableRemoteModule: true,
webSecurity: false,
},
webSecurity: false
}
});
// 3. 打开开发者工具(排查用,上线后删除)
......@@ -372,34 +337,34 @@ async function createWindow() {
console.error("自定义弹窗出错:", error);
}
})();
},
},
],
},
{
label: "帮助",
submenu: [
{
label: "帮助文档",
click: async () => {
await shell.openExternal("");
},
},
{
label: "检查更新",
click: () => {
console.log("检查更新");
},
},
{ type: "separator" },
}
}
]
}
{
label: "关于",
click: () => {},
},
],
},
// {
// label: "帮助",
// submenu: [
// {
// label: "帮助文档",
// click: async () => {
// await shell.openExternal("");
// },
// },
// {
// label: "检查更新",
// click: () => {
// console.log("检查更新");
// },
// },
// { type: "separator" },
// {
// label: "关于",
// click: () => {},
// },
// ],
// },
];
// 构建菜单
const menu = Menu.buildFromTemplate(template);
......@@ -415,23 +380,31 @@ async function createWindow() {
if (newWindow) newWindow.webContents.send("getProductionNoInfo", v);
});
ipcMain.on("request-select-folder", (event) => {
ipcMain.on("request-select-folder", (event, currentPath) => {
// 打开文件夹选择对话框(Electron 6.x 回调写法,更稳定)
dialog.showOpenDialog(
{
properties: ["openDirectory"], // 仅允许选择文件夹
title: "选择下载保存位置",
defaultPath: currentPath || ""
},
(result) => {
result => {
// 处理结果并返回给渲染进程
console.log(438, result);
if (result.length) {
event.reply("select-folder-result", result[0]);
}
},
}
);
});
// 监听窗口的 reload 事件
win.webContents.on("did-finish-load", () => {
const { width, height } = win.getBounds();
win.webContents.send("window-size", { width, height });
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();
......@@ -444,43 +417,40 @@ async function createWindow() {
win.webContents.send("sendWebsockt", obj);
});
win.on("closed", () => {
// win && win.removeAllListeners();
// win = null;
// newWindow && newWindow.close();
// newWindow = null;
ipcMain.on("open-folder-only", async (event, dirPath) => {
console.log(938, dirPath);
const result = shell.openItem(dirPath);
return result;
});
win.on("closed", () => {
// 确保窗口还未销毁,才执行清理操作
if (win && !win.isDestroyed()) {
win.removeAllListeners();
win = null;
}
// // 确保 newWindow 还未销毁,才执行关闭操作
// if (newWindow && !newWindow.isDestroyed()) {
// newWindow.close();
// newWindow = null;
// }
});
// 自动更新逻辑开始
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false; // 应用退出后自动安装
autoUpdater.on("download-progress", (progress) => {
autoUpdater.on("download-progress", progress => {
// 下载进度监听
if (win.isDestroyed()) return;
win.webContents.send("update", {
speed: Math.ceil(progress.bytesPerSecond / 1000), // 网速
percent: Math.ceil(progress.percent), // 百分比
percent: Math.ceil(progress.percent) // 百分比
});
});
autoUpdater.on("error", (err) => {
autoUpdater.on("error", err => {
// 下载失败监听
if (win.isDestroyed()) return;
win.webContents.send("update-error", err);
});
autoUpdater.on("update-downloaded", (info) => {
autoUpdater.on("update-downloaded", info => {
// 下载完成监听
if (win.isDestroyed()) return;
console.log(info, "info");
......@@ -497,9 +467,9 @@ async function createWindow() {
if (win.isDestroyed()) return;
autoUpdater.setFeedURL({
provider: "generic",
url: item.url,
url: item.url
});
autoUpdater.checkForUpdates().then((res) => {
autoUpdater.checkForUpdates().then(res => {
console.log(res);
autoUpdater.downloadUpdate();
});
......@@ -516,7 +486,7 @@ async function createWindow() {
path.dirname(app.getPath("exe")),
"resources",
"scripts",
"script.ps1",
"script.ps1"
);
}
......@@ -535,11 +505,42 @@ async function createWindow() {
win.on("resize", () => {
const { width, height } = win.getBounds();
win.webContents.send("window-size", { width, height });
console.log(888);
});
// 监听窗口的 reload 事件
win.webContents.on("did-finish-load", () => {
const { width, height } = win.getBounds();
win.webContents.send("window-size", { width, height });
ipcMain.on("get-install-path", event => {
const desktopPath = app.getPath("desktop");
const dtfDir = path.join(desktopPath, "JomallProductionAssistant-DTF");
let resPath = desktopPath;
try {
fs.mkdirSync(dtfDir, { recursive: true });
resPath = dtfDir;
} catch (err) {
console.error("创建文件夹失败", err);
}
event.reply("get-install-path-res", resPath);
});
// ========== 新增3:窗口关闭时停止定时任务 ==========
win.on("closed", () => {
downloadQueue.stopAutoDownloadTask();
});
}
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on("second-instance", () => {
// 第二次双击时,唤起已有的窗口
const allWindows = BrowserWindow.getAllWindows();
const mainWin = allWindows[0];
if (mainWin) {
if (mainWin.isMinimized()) mainWin.restore();
mainWin.show();
mainWin.focus();
}
});
}
......@@ -573,10 +574,16 @@ app.on("will-quit", () => {
globalShortcut.unregister("F5");
globalShortcut.unregister("CommandOrControl+R");
});
app.on("window-all-closed", () => {
downloadQueue.stopAutoDownloadTask();
if (process.platform !== "darwin") {
app.quit();
}
});
if (isDevelopment) {
if (process.platform === "win32") {
process.on("message", (data) => {
process.on("message", data => {
if (data === "graceful-exit") {
app.quit();
}
......
<template>
<div class="commodity-card" :class="{ active: active }">
<div class="commodity-card-image">
<div class="before" :style="{ paddingTop: paddingTop }"></div>
<div class="image">
<img :src="mainImageSrc" />
</div>
<!-- 左上角 -->
<div class="img_top_left">
<span v-if="showSelectIcon" class="select-icon"></span>
<slot name="top_left"></slot>
</div>
<!-- 右上角 -->
<div class="img_top_right">
<slot name="top_right"></slot>
</div>
<!-- 左下角 -->
<div class="img_bottom_left">
<slot name="bottom_left"></slot>
</div>
<!-- 右下角 -->
<div class="img_bottom_right">
<slot name="operations"></slot>
</div>
</div>
<div class="commodity-card-info">
<!-- 颜色图片列表 -->
<div v-if="hasImageList" class="color-image flex flex-gap-10">
<div
v-for="(img, index) in imageList"
:key="index"
class="color-image-item"
>
<img :src="getItemImagePath(img)" />
</div>
</div>
<!-- 时间信息插槽 -->
<slot name="time"></slot>
<!-- 名称和价格 -->
<!-- <div
v-if="cardItem?.productName"
class="commodity-card-name-price flex flex-justify-space-between"
>
<slot name="productName">
<span>{{ cardItem?.productName }}</span>
</slot>
<slot name="price"></slot>
</div> -->
<!-- SKU信息 -->
<div
v-if="cardItem?.sku"
class="commodity-card-sku"
@click.stop="copy(String(cardItem?.sku))"
>
<!-- <img src="../assets/images/id.png" height="20" /> -->
<span>{{ cardItem?.sku }}</span>
</div>
<slot name="images"></slot>
<!-- 自定义信息 -->
<slot name="info"></slot>
</div>
</div>
</template>
<script>
/**
* 通用卡片封装 BaseCard
* 基于ElementUI el-card二次封装,配置驱动 + 多插槽拓展
* @prop {Object} cardConfig 卡片核心配置对象
* @prop {String} bodyPadding 卡片body内边距,默认 0px
*/
export default {
name: "BaseCard",
props: {
cardItem: {
type: Object,
default: () => ({})
},
active: {
type: Boolean,
default: false
},
showSelectIcon: {
type: Boolean,
default: true
},
showSku: {
type: Boolean,
default: false
},
showProductInfo: {
type: Boolean,
default: false
},
showImageList: {
type: Boolean,
default: false
},
imageField: {
type: String,
default: "variantImage"
},
imageListField: {
type: String,
default: "imageList"
},
imagePathField: {
type: String,
default: "imagePath"
},
paddingTop: {
type: String,
default: "120%"
}
},
data() {
return { imageList: [] };
},
computed: {
hasImageList() {
return true;
},
mainImageSrc() {
const item = this.cardItem;
// 使用索引访问避免联合类型的属性访问问题
if (
this.imageField === "variantImage" &&
typeof item[this.imageField] === "string"
) {
return item[this.imageField];
}
if (
this.imageField === "mainImage" &&
typeof item[this.imageField] === "string"
) {
return item[this.imageField];
}
// 默认返回空字符串
return "";
}
},
methods: {}
};
</script>
<style lang="less" scoped>
.commodity-card {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.commodity-card-image {
position: relative;
border: 1px solid #eee;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
.img_top_left {
position: absolute;
top: 5px;
left: 5px;
display: flex;
align-items: center;
gap: 4px;
}
.img_top_right {
position: absolute;
top: 0px;
right: 0px;
}
.img_bottom_left {
position: absolute;
bottom: 5px;
left: 5px;
}
.img_bottom_right {
position: absolute;
bottom: 5px;
right: 5px;
display: flex;
align-items: center;
:deep(.svg-icon) {
width: 2em;
height: 2em;
}
}
}
.before {
height: 0;
}
.image {
position: absolute;
// position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
img {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
}
}
.color-image {
.color-image-item {
width: 50px;
height: 50px;
}
img {
width: 100%;
height: 100%;
}
}
.commodity-card-info {
flex: 1;
padding: 10px;
font-size: 14px;
background: linear-gradient(
90deg,
rgba(228, 228, 228, 0.4) 0%,
rgba(255, 255, 255, 1) 100%
);
}
.commodity-card-name-price {
height: 30px;
line-height: 30px;
}
.commodity-card-sku {
display: flex;
align-items: center;
gap: 5px;
background-color: #fff;
cursor: pointer;
}
.select-icon {
position: relative;
display: block;
width: 16px;
height: 16px;
border: 1px solid #cccccc;
box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.4) inset;
border-radius: 8px;
background: #fff;
}
.commodity-card.active .select-icon {
background: #4168ff;
position: relative;
border-color: #4168ff;
box-shadow: none;
font-size: 18px;
top: -1px;
left: -1px;
}
.commodity-card.active .select-icon::after {
position: absolute;
content: "";
top: 0px;
left: 4px;
width: 4px;
height: 8px;
border-width: 2px;
border-style: solid;
border-color: transparent #fff #fff transparent;
transform: rotate(45deg) scale(0.8);
}
</style>
<template>
<div class="base-table-wrap">
<!-- ElementUI 原生表格,所有属性透传 -->
<el-table
ref="tableRef"
:data="tableData"
v-bind="$attrs"
v-on="$listeners"
@selection-change="handleSelectionChange"
>
<!-- 多选框列 -->
<el-table-column
v-if="hasSelection"
type="selection"
width="55"
align="center"
/>
<!-- 序号列 -->
<el-table-column
v-if="hasIndex"
type="index"
label="序号"
width="50"
align="center"
/>
<!-- 动态循环渲染列配置 -->
<el-table-column
v-for="col in tableConfig"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:width="col.width"
:min-width="col.minWidth"
:fixed="col.fixed"
:align="col.align || 'center'"
:header-align="col.headerAlign || col.align || 'center'"
:sortable="col.sortable"
:show-overflow-tooltip="col.showOverflowTooltip ?? true"
>
<template slot-scope="scope">
<!-- 1. 自定义render -->
<span
v-if="col.render"
v-html="col.render(scope.row, scope.column, scope.$index)"
></span>
<!-- 3. 图片 -->
<el-image
v-else-if="col.type === 'image'"
:src="scope.row[col.prop]"
style="width: 40px; height: 40px"
fit="cover"
:preview-src-list="[scope.row[col.prop]]"
/>
<!-- 4. 开关 -->
<el-switch
v-else-if="col.type === 'switch'"
v-model="scope.row[col.prop]"
@change="col.onSwitchChange && col.onSwitchChange(scope.row)"
active-text="开启"
inactive-text="关闭"
/>
<!-- 5. 操作按钮 -->
<div v-else-if="col.type === 'button'" class="table-btn-group">
<el-button
v-for="btn in col.btns"
:key="btn.key"
size="mini"
:type="btn.type || 'primary'"
:icon="btn.icon"
:disabled="btn.disabled ? btn.disabled(scope.row) : false"
@click="btn.click(scope.row, scope.$index)"
>
{{ btn.label }}
</el-button>
</div>
<!-- 6. 外部自定义插槽 -->
<slot v-else-if="col.slotName" :name="col.slotName" v-bind="scope" />
<!-- 7. 普通文本兜底(现在一定能拿到scope) -->
<span v-else>{{ scope.row[col.prop] ?? "" }}</span>
</template>
<!-- 7. 默认文本展示 -->
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
small
v-if="showPagination"
class="table-pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageInfo.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageInfo.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageInfo.total"
/>
</div>
</template>
<script>
export default {
name: "BaseTable",
props: {
/**
* 表格数据源
*/
tableData: {
type: Array,
default: () => []
},
/**
* 列配置数组【核心】
* @type Array<Object>
* 字段说明:
* prop: 绑定字段名
* label: 表头文字
* width/minWidth: 列宽
* fixed: 固定列 left/right
* align/headerAlign: 对齐方式
* sortable: 是否可排序
* showOverflowTooltip: 文字溢出悬浮提示
* type: tag/image/switch/button 特殊类型渲染
* render: 自定义渲染函数(row, column, index) => html字符串
* slotName: 自定义插槽名称
* btns: 操作按钮数组(type=button时生效)
*/
tableConfig: {
type: Array,
required: true
},
/**
* 是否显示多选框
*/
hasSelection: {
type: Boolean,
default: false
},
/**
* 是否显示序号列
*/
hasIndex: {
type: Boolean,
default: false
},
/**
* 是否展示分页
*/
showPagination: {
type: Boolean,
default: true
},
/**
* 分页信息
* { currentPage: 1, pageSize: 10, total: 0 }
*/
pageInfo: {
type: Object,
default: () => ({
currentPage: 1,
pageSize: 10,
total: 0
})
}
},
data() {
return {
// 选中行存储
selectedRows: []
};
},
methods: {
/**
* 多选框选中回调
*/
handleSelectionChange(val) {
this.selectedRows = val;
this.$emit("selection", val);
},
/**
* 获取当前选中的所有行数据
*/
getSelectedRows() {
return this.selectedRows;
},
/**
* 清空多选框选中状态
*/
clearSelection() {
this.$refs.tableRef.clearSelection();
this.selectedRows = [];
},
/**
* 每页条数改变
*/
handleSizeChange(size) {
this.$emit("page-size-change", size);
},
/**
* 页码改变
*/
handleCurrentChange(page) {
this.$emit("page-current-change", page);
},
toggleAllSelection(value) {
this.$refs.tableRef.toggleAllSelection(value);
}
},
mounted() {
// console.log("tableConfig", this.tableConfig);
// console.log("tableData", this.tableData);
}
};
</script>
<style scoped>
.el-table {
max-height: calc(100% - 75px);
}
.base-table-wrap {
width: 100%;
height: 100%;
/* overflow-y: scroll; */
}
.table-pagination {
margin-top: 10px;
text-align: center;
}
.table-btn-group {
display: flex;
gap: 4px;
flex-wrap: wrap;
justify-content: center;
}
</style>
<template>
<div class="log-list">
<div v-for="l in logList" :key="l.id" class="log-item flex">
<div class="log-item-icon">
<el-icon name="a-2labadianji3x" />
</div>
<div class="log-item-time">
<span>{{ l.local && `${l.local}:` }}{{ l.createTime }}</span>
</div>
<div v-if="l.employeeName" class="log-item-name">
<span>{{ l.employeeName }}</span>
</div>
<div class="log-item-content" :title="l.description">
<span>{{ l.description }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LogList",
props: {
logList: {
default: () => [],
type: Array
}
}
};
</script>
<style lang="less" scoped>
.log-list {
height: 500px;
overflow: auto;
font-size: 14px;
}
.log-item {
line-height: 26px;
white-space: pre-wrap;
word-break: break-all;
}
.log-item div:not(:last-child) {
margin-right: 6px;
}
.log-item-time {
// width: 220px;
}
.log-item-name {
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.log-item-content {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
// white-space: nowrap;
}
</style>
<template>
<el-dialog
:visible.sync="visible"
width="92%"
top="5vh"
:before-close="handleClose"
:show-close="true"
>
<div class="order-detail-container" style="display: flex;height: 800px;">
<!-- 左侧图片区域 -->
<div class="left-images" style="width: 50%; text-align: center;">
<el-carousel
v-if="parseImageAry(info.imageAry).length > 0"
style="height: 100%"
:autoplay="false"
>
<el-carousel-item
v-for="(item, index) in parseImageAry(info.imageAry)"
:key="index"
style="height: 100%"
>
<div class="left-image">
<b v-show="item?.title && item?.url">
{{ item.title }}
<span
v-if="item?.id"
style="
text-decoration: underline;
cursor: pointer;
color: blue;
"
>
(DID:{{ item.id }}
</span>
</b>
<img :src="item.url" alt="" />
</div>
</el-carousel-item>
</el-carousel>
</div>
<!-- 右侧信息区域 -->
<div class="right-info" style="flex: 1;">
<!-- 客户+订单号栏 -->
<div
class="customer-order-row"
style="display: flex; justify-content: space-between; border: 1px solid #e4e7ed; padding: 15px 20px; margin-bottom: 18px;"
>
<div class="text-label">
<span>客户:</span>
<span style="color: #f56c6c;font-size: 20px;font-weight: 700;">{{
info.userMark
}}</span>
</div>
<div class="text-label">
<span>订单号:</span>
<span style="color: #f56c6c;font-size: 20px;font-weight: 700;">{{
info.factoryOrderNumber
}}</span>
</div>
</div>
<!-- 操作单信息区块 -->
<div class="div-text"><b>操作单信息</b></div>
<div
class="info-box"
style="border: 1px solid #e4e7ed; padding: 18px 22px; margin-bottom: 26px;"
>
<div
style="display: grid; grid-template-columns: 1fr 1fr; row-gap: 14px;"
>
<div class="text-label">
<span>操作单号:</span>{{ info.operationNo }}
</div>
<div class="text-label">
<span>生产工艺:</span>{{ info.craftName }}
</div>
<div class="text-label"><span>基版:</span>{{ info.baseSku }}</div>
<div class="text-label">
<span>变体SKU:</span>{{ info.variantSku }}
</div>
<div class="text-label"><span>数量:</span>{{ info.quantity }}</div>
<div class="text-label"><span>尺寸:</span>{{ info.size }}</div>
<div class="text-label">
<span>店铺单号:</span>{{ info.shopNumber }}
</div>
<div class="text-label">
<span>创建时间:</span>{{ info.createTime }}
</div>
</div>
</div>
<!-- 底部按钮 -->
<div style="text-align: right;">
<el-button type="primary" size="medium" @click="downloadFn"
>下载素材</el-button
>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import bus from "@/bus";
export default {
name: "OrderDetailDialog",
props: {
// 弹窗显示控制
show: {
type: Boolean,
default: false
},
// 订单详情数据
info: {
type: Object,
default: () => ({})
}
},
computed: {
visible: {
get() {
return this.show;
},
set(val) {
this.$emit("update:show", val);
}
}
},
data() {
return {
scanDownloadCheck: true
};
},
methods: {
handleClose() {
this.$emit("update:show", false);
},
parseImageAry(imageAry) {
if (typeof imageAry !== "string" || !imageAry.trim()) return [];
const trimmed = imageAry.trim();
if (trimmed.startsWith("http")) {
return trimmed
.split(",")
.map(url => ({ url: url.trim() }))
.filter(item => item.url);
}
try {
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed)) return [{ url: trimmed }];
const result = [];
for (const item of parsed) {
if (typeof item === "string" && item) {
result.push({ url: item });
} else if (item && typeof item === "object" && "url" in item) {
const url = item.url;
if (url) {
result.push({
url,
title: item.title
});
}
}
}
return result;
} catch {
return [{ url: trimmed }];
}
},
downloadFn() {
bus.$emit("downLoadFile", this.info);
}
}
};
</script>
<style scoped lang="less">
.left-images {
display: flex;
width: 95%;
height: 100%;
flex-direction: column;
b {
color: black;
text-align: center;
margin-bottom: 15px;
}
.left-image {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
img {
height: auto;
width: 100%;
max-height: 90%;
object-fit: contain;
}
b {
color: black;
font-size: 18px;
margin-bottom: 15px;
}
}
}
.div-text {
b {
position: relative;
background: white;
top: 9px;
left: 13px;
padding: 0 10px;
font-size: 22px;
color: black;
z-index: 3;
}
}
.text-label {
flex: 50%;
font-size: 16px;
}
::v-deep.el-carousel {
.el-carousel__container {
height: 100% !important;
}
}
</style>
......@@ -19,9 +19,9 @@
:draggable="true"
:resizable="true"
:rotatable="true"
@dragstop="(a) => dragStop(a, item)"
@resizestop="(a) => resizeStop(a, item)"
@rotatestop="(a) => rotating(a, item)"
@dragstop="a => dragStop(a, item)"
@resizestop="a => resizeStop(a, item)"
@rotatestop="a => rotating(a, item)"
:angle="item.r"
>
<div
......@@ -45,7 +45,7 @@
:class="{ 'no-border-grid': systemSetting.gridShow !== 1 }"
:style="{
width: gridWH.w + 'px',
height: gridWH.h + 'px',
height: gridWH.h + 'px'
}"
class="grid"
>
......@@ -89,9 +89,9 @@ export default {
// console.log(data);
this.$set(item, "r", data.angle);
this.imgHistoryList.push(JSON.parse(JSON.stringify(this.imgList)));
},
}
},
mounted() {},
mounted() {}
};
</script>
<style lang="less">
......@@ -105,7 +105,7 @@ export default {
#line {
position: absolute;
left: 5%;
top: 50%;
top: 46%;
transform: translate(0, -50%);
background-color: #f0f2f6;
}
......
......@@ -21,7 +21,8 @@ export const pathMap = {
CN: "factory/podJomallOrderProductCn/completeDelivery",
US: "factory/podJomallOrderProductUs/completeDelivery",
GC: "factory/podJomallOrderProduct/completeDelivery",
OP: "factory/podOrderOperation/printerCompleteDelivery"
OP: "factory/podOrderOperation/printerCompleteDelivery",
DTF: "factory/podOrderOperation/completeDelivery"
},
//惠立彩素材下载接口
processTransparentBackground: {
......@@ -34,14 +35,57 @@ export const pathMap = {
},
checkInventory: {
OP: "factory/podOrderOperation/check-inventory"
}
},
//烫画
//获取生产配置
productionConfig: "factory/production-config/get",
//更新生产配置
productionConfigUpdate: "factory/production-config/update",
//排版列表(分页)
productionSoftware: "factory/podOrderBatchDownload/page-production-software",
//查询该批次下的所有素材
listMaterialsByBatch: "factory/podOrderBatchDownload/list-materials-by-batch",
//重新合成排版
psReComposingDesignImages:
"factory/podOrderBatchDownload/psReComposingDesignImages",
//根据批次分页查询操作单
listOperationByBatch: "factory/podOrderBatchDownload/listOperationByBatch",
//排版
replenishmentComposingDesignImages:
"factory/podOrderOperation/softwareProduction-replenishmentComposingDesignImages",
//获取单个批次号信息
podOrderBatchDownload: "factory/podOrderBatchDownload/get",
//操作日志
getPodOrderLog: "factory/podOrderLog/getPodOrderLog"
};
// 操作单状态枚举
export const statusMap = {
待排单: "PENDING_SCHEDULE",
待拣胚: "PENDING_PICK",
待补胚: "PENDING_REPLENISH",
生产中: "IN_PRODUCTION",
待配货: "PENDING_PACKING",
配货完成: "PACKING_COMPLETED",
已完成: "COMPLETED",
已取消: "CANCELLED",
已归档: "ARCHIVE"
};
export function autoRegisterRouter(router, funcKey, handler) {
const configGroup = pathMap[funcKey];
// 遍历配置自动注册路由
Object.values(configGroup).forEach(el => {
router.post(`/${el}`, (req, res) => {
const pathConfig = pathMap[funcKey];
// 根据类型收集所有需要注册的接口路径
let pathList = [];
if (typeof pathConfig === "string") {
// 字符串类型:单一路径(如 productionConfig)
pathList = [pathConfig];
} else if (typeof pathConfig === "object" && pathConfig !== null) {
// 对象类型:多站点路径集合(如 findByPodProductionNo)
pathList = Object.values(pathConfig);
}
// 批量注册路由
pathList.forEach(path => {
router.post(`/${path}`, (req, res) => {
handler(req, res);
});
});
......
[
{
"name": "Shopify",
"lowercase": "shopify",
"type": "SHOPIFY",
"icon": "@/assets/images/icon/logo-1.png",
"cycle": "today",
"route": "store_profits_shopify"
},
{
"name": "AliExpress",
"lowercase": "aliExpress",
"type": "AE",
"icon": "@/assets/images/icon/logo-3.png",
"cycle": "today",
"route": "store_profits_aliexpress"
},
{
"name": "Amazon",
"lowercase": "amazon",
"type": "AMAZON",
"icon": "@/assets/images/icon/logo-2.png",
"cycle": "yesterday",
"route": "store_profits_amazon"
},
{
"name": "Magento",
"lowercase": "magento",
"type": "MAGENTO",
"icon": "@/assets/images/icon/logo-6.png",
"cycle": "today",
"route": "store_profits_magento"
},
{
"name": "SHOPLINE",
"lowercase": "shopline",
"type": "SHOPLINE",
"cycle": "today",
"icon": "@/assets/images/icon/logo-4.png",
"route": "store_profits_shopline"
},
{
"name": "Shoplazza",
"lowercase": "shoplazza",
"type": "SHOPLAZZA",
"cycle": "today",
"icon": "@/assets/images/icon/logo-5.png",
"route": "store_profits_shoplazza"
},
{
"name": "temu",
"lowercase": "temu",
"type": "TEMU",
"cycle": "today",
"icon": "@/assets/images/icon/temu.png",
"route": "store_profits_temu"
},
{
"name": "eBay",
"lowercase": "ebay",
"type": "EBAY",
"cycle": "today",
"icon": "@/assets/images/icon/eBay.png",
"route": "store_profits_ebay"
},
{
"name": "Etsy",
"lowercase": "etsy",
"type": "ETSY",
"cycle": "today",
"icon": "@/assets/images/icon/etsy.png",
"route": "store_profits_etsy"
},
{
"name": "Alibaba",
"lowercase": "alibaba",
"type": "ALIBABA",
"cycle": "today",
"icon": "@/assets/images/icon/alibaba.png",
"route": "store_profits_alibaba"
},
{
"name": "walmart",
"lowercase": "walmart",
"type": "WALMART",
"cycle": "today",
"icon": "@/assets/images/icon/walmart.png",
"route": "store_profits_walmart"
},
{
"name": "tiktokshop",
"lowercase": "tiktok",
"type": "TIKTOK",
"cycle": "today",
"icon": "@/assets/images/icon/tiktokshop.png",
"route": "store_profits_tiktokshop"
},
{
"name": "Customize",
"lowercase": "customize",
"type": "CUSTOMIZE",
"cycle": "today",
"icon": "@/assets/images/icon/customize.png",
"route": "store_profits_customize"
},
{
"name": "1688",
"type": "ALIBABA_DOMESTIC",
"cycle": "today",
"icon": "@/assets/images/icon/1688.png",
"route": "store_profits_1688"
}
]
......@@ -8,38 +8,62 @@ const routes = [
path: "/",
name: "login",
meta: {
title: "登录",
title: "登录"
},
component: (resolve) => require(["../views/login/index.vue"], resolve),
component: resolve => require(["../views/login/index.vue"], resolve)
},
{
path: "/design",
name: "design",
meta: {
title: "设计页面",
title: "设计页面"
},
component: (resolve) => require(["../views/design/index.vue"], resolve),
},
{
path: "/design-detail",
name: "design",
meta: {
title: "设计详情页面",
},
component: (resolve) =>
require(["../views/design/detail/index.vue"], resolve),
component: resolve => require(["../views/design/tabIndex.vue"], resolve),
redirect: "/design/dtg",
children: [
// DTG
{
path: "dtg",
name: "design-dtg",
meta: { title: "DTG生产" },
component: resolve => require(["../views/design/index.vue"], resolve)
},
// DTF 列表
{
path: "dtf",
name: "design-dtf",
meta: { title: "DTF生产" },
component: resolve => require(["@/views/dtf-list/index.vue"], resolve)
},
// DTF 排版设置
{
path: "dtf/setting",
name: "dtf-setting",
meta: { title: "DTF排版设置" },
component: resolve =>
require(["@/views/dtf-setting/index.vue"], resolve)
},
// DTF 批次详情
{
path: "dtf/batch-detail",
name: "dtf-batch-detail",
meta: { title: "DTF批次详情" },
component: resolve =>
require(["@/views/dtf-batch-detail/detail.vue"], resolve)
}
]
},
// 删掉原来独立的 /dtf-setting、/dtf-batch-detail、/dtf-file
{
path: "*",
redirect: "/",
},
redirect: "/"
}
];
const router = new VueRouter({
mode: "hash",
base: process.env.BASE_URL,
routes,
routes
});
router.beforeEach((to, from, next) => {
let user = Vue.prototype.$dataStore.get("user");
......@@ -57,6 +81,8 @@ router.beforeEach((to, from, next) => {
if (user) {
return next();
} else {
console.log("router", user);
return next("/");
}
}
......
......@@ -3,8 +3,11 @@ import {
downloadOtherImage,
toSend,
writeProfileXml,
copySingleImage
copySingleImage,
createBatchFolder,
downloadDTFFiles
} from "@/server/utils";
const {
cropImageTransparentEdges,
cropTransparentEdges,
......@@ -72,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);
......@@ -153,6 +156,7 @@ export default {
env = getHostApi().apiApiHost;
fileEnv = getHostApi().fileApiUrl;
const params = req.body;
const targetDir = params.targetDir || "";
const token = req.headers["jwt-token"];
const forceProduction = params.forceProduction || false;
const downloadBySubOrderNumber = pathMap["downloadBySubOrderNumber"];
......@@ -194,20 +198,23 @@ export default {
let files = [path];
files = files.map(el => ({ url: `${fileEnv}${el}` }));
const downloadFunc =
params.device === 2 ? downloadOtherImage : downloadImage;
const result = await downloadFunc(files, {
processDesignA,
processDesignB
});
let result;
if (params.device === 4) {
result = await downloadDTFFiles(files[0].url, targetDir);
} else {
const downloadFunc =
params.device === 2 ? downloadOtherImage : downloadImage;
result = await downloadFunc(files, {
processDesignA,
processDesignB
});
}
console.log("result200", result);
res.json({ code: 200, data: result });
} catch (err) {
console.error("Download error:", err);
res.status(500).json({ code: 500, msg: err });
res.status(500).json({ code: 500, msg: err.message });
}
},
findByPodProductionNo: async (req, res) => {
......@@ -265,7 +272,8 @@ export default {
podJomallOrderUsId: params.podJomallOrderUsId || ""
},
GC: { id: params.id },
OP: { ...params.data }
OP: { ...params.data },
DTF: params.ids
};
let url = pathMap["completeDelivery"][params.orderType];
......@@ -383,7 +391,6 @@ export default {
try {
env = getHostApi().apiApiHost;
console.log(`${env}/factory/login`);
let { data } = await axios.post(`${env}/factory/login`, req.body);
res.send(data);
} catch (err) {
......@@ -557,7 +564,8 @@ export default {
//获取国家名称及代码
getAllCountry: async (req, res) => {
const token = req.headers["jwt-token"];
let url = "https://factory.jomalls.com/api/logisticsAddress/getAllCountry";
env = getHostApi().apiApiHost;
let url = `${env}/logisticsAddress/getAllCountry`;
try {
let { data } = await axios.get(url, { headers: { "jwt-token": token } });
......@@ -653,5 +661,188 @@ export default {
console.log("checkInventory", error);
res.json({ code: 500, msg: error });
}
},
createBatchFolder: async (req, res) => {
try {
const { downloadDir, isArray, basePath } = req.body;
const fn = isArray ? createBatchFolder : createBatchFolder;
const data = await fn(basePath, downloadDir);
res.send(data);
} catch (error) {
console.log("createBatchFolder", 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("productionConfig", 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}`, params, {
headers: { "jwt-token": token }
});
res.send(data);
} catch (error) {
console.log("productionConfigUpdate", 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}`, params, {
headers: { "jwt-token": token }
});
res.send(data);
} catch (error) {
console.log("productionSoftware", 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("listMaterialsByBatch", 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("psReComposingDesignImages", 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}`, params, {
headers: { "jwt-token": token }
});
res.send(data);
} catch (error) {
console.log("listOperationByBatch", error);
res.json({ code: 500, msg: error });
}
},
replenishmentComposingDesignImages: async (req, res) => {
env = getHostApi().apiApiHost;
const token = req.headers["jwt-token"];
const { ids, type, templateWidth, targetDir } = req.body;
let url = pathMap["replenishmentComposingDesignImages"];
try {
let { data } = await axios.post(
`${env}/${url}?type=${type}&templateWidth=${templateWidth}`,
ids,
{
headers: { "jwt-token": token }
}
);
if (data.code !== 200) {
return res.json(data);
}
console.log("data", data);
let path = data.message;
// 转义中文
if (path) {
path = encodeURIComponent(path);
}
let files = [path];
files = files.map(el => ({ url: `${fileEnv}${el}` }));
const result = await downloadDTFFiles(files[0].url, targetDir);
res.json({ code: 200, data: result });
} catch (error) {
console.log("replenishmentComposingDesignImages", error);
res.json({ code: 500, msg: error });
}
},
podOrderBatchDownload: async (req, res) => {
env = getHostApi().apiApiHost;
const token = req.headers["jwt-token"];
const { id } = req.body;
let url = pathMap["podOrderBatchDownload"];
try {
let { data } = await axios.get(`${env}/${url}?id=${id}`, {
headers: { "jwt-token": token }
});
res.json({ code: 200, data });
} catch (error) {
console.log("podOrderBatchDownload", error);
res.json({ code: 500, msg: error });
}
},
getPodOrderLog: async (req, res) => {
env = getHostApi().apiApiHost;
const token = req.headers["jwt-token"];
const { id } = req.body;
let url = pathMap["getPodOrderLog"];
console.log(req.body);
try {
let { data } = await axios.get(`${env}/${url}?id=${id}`, {
headers: { "jwt-token": token }
});
res.json({ code: 200, data });
} catch (error) {
console.log("getPodOrderLog", error);
res.json({ code: 500, msg: error });
}
}
};
......@@ -13,7 +13,7 @@ export const createServer = () => {
webApp.use(logMiddleWare());
webApp.use(express.json());
webApp.use(express.urlencoded({ extended: false }));
webApp.use("/", router);
webApp.use("/", router);
// catch 404
webApp.use((req, res) => {
res.status(404).send("Sorry! 404 Error.");
......@@ -29,7 +29,7 @@ export const createServer = () => {
res.status(err.status || 500);
res.json({
message: err.message,
error: err,
error: err
});
});
......
......@@ -51,10 +51,39 @@ router.post("/getAllCountry", fn.getAllCountry);
//根据图片地址保存文件到本地
router.post("/saveImgByUrl", fn.saveImgByUrl);
//获取目标路径文件夹下所以文件
router.post("/createBatchFolder", fn.createBatchFolder);
//复制文件夹下文件
router.post("/copySingleImageFn", fn.copySingleImageFn);
//获取生产配置
autoRegisterRouter(router, "productionConfig", fn.productionConfig);
//更新生产配置
autoRegisterRouter(router, "productionConfigUpdate", fn.productionConfigUpdate);
//排版列表(分页)
autoRegisterRouter(router, "productionSoftware", fn.productionSoftware);
//查询该批次下的所有素材
autoRegisterRouter(router, "listMaterialsByBatch", fn.listMaterialsByBatch);
//重新合成排版
autoRegisterRouter(
router,
"psReComposingDesignImages",
fn.psReComposingDesignImages
);
//排版
autoRegisterRouter(
router,
"replenishmentComposingDesignImages",
fn.replenishmentComposingDesignImages
);
//排版
autoRegisterRouter(router, "podOrderBatchDownload", fn.podOrderBatchDownload);
//根据批次分页查询操作单
autoRegisterRouter(router, "listOperationByBatch", fn.listOperationByBatch);
//操作日志
autoRegisterRouter(router, "getPodOrderLog", fn.getPodOrderLog);
// 根据生产单号查询详情
autoRegisterRouter(router, "findByPodProductionNo", fn.findByPodProductionNo);
......
import { exec } from "child_process";
var fs = require("fs");
const https = require("https");
const http = require("http");
var path = require("path");
var request = require("request");
var uuid = require("uuid");
......@@ -84,9 +85,6 @@ export const downloadImage = (list, isForcedProduction) => {
if (downloadItems.length === 0) {
return resolve(list);
}
console.log("list1111", list);
console.log("downloadItems", downloadItems);
// 并行处理所有下载
const downloadPromises = downloadItems.map(item => {
return new Promise(async itemResolve => {
......@@ -494,19 +492,10 @@ export const writeProfileXml = b => {
const fsPromises = fs.promises; // 明确获取 promises 异步 API
export async function copySingleImage(sourceImagePath, targetDir) {
// 先打印传入的原始路径(方便验证是否和预期一致)
// console.log(`传入参数验证:
// 源图片路径:${sourceImagePath}
// 目标文件夹路径:${targetDir}`);
try {
// 步骤1:路径规范化(修正潜在的分隔符/隐形字符问题)
const normalizedSourcePath = path.resolve(sourceImagePath);
const normalizedTargetDir = path.resolve(targetDir);
// console.log(`路径规范化后:
// 源图片路径:${normalizedSourcePath}
// 目标文件夹路径:${normalizedTargetDir}`);
// 步骤2:校验源文件是否为有效图片文件
let fileStat;
try {
......@@ -521,8 +510,6 @@ export async function copySingleImage(sourceImagePath, targetDir) {
if (!fileStat.isFile()) {
throw new Error(`源路径不是有效文件,是文件夹:${normalizedSourcePath}`);
}
// console.log("校验通过:源路径是有效文件");
// 2.2 校验是否为图片格式(可选,过滤非图片文件)
const validImageExts = [".jpg", ".jpeg", ".png", ".gif", ".bmp"];
const fileExt = path.extname(normalizedSourcePath).toLowerCase();
......@@ -533,8 +520,6 @@ export async function copySingleImage(sourceImagePath, targetDir) {
)})`
);
}
// console.log(`校验通过:源文件是图片格式(${fileExt})`);
// 步骤3:获取文件名 + 拼接目标图片完整路径
const imageFileName = path.basename(normalizedSourcePath);
const targetImagePath = path.join(normalizedTargetDir, imageFileName);
......@@ -564,3 +549,236 @@ export async function copySingleImage(sourceImagePath, targetDir) {
throw err;
}
}
/**
* 创建批次目录:基础路径/批次号:xxx/三个素材子目录
* @param {string} basePath 父目录基础路径
* @param {string} batchNo 批次编号
*/
export function createBatchFolder(basePath, batchNo) {
try {
// 拼接外层批次文件夹名称
const batchDirName = `批次号:${batchNo}`;
const batchPath = path.join(basePath, batchDirName);
// 固定子文件夹列表
const subDirList = ["原素材", "自动排版素材", "手动排版素材"];
// 1. 判断批次目录是否已存在
if (fs.existsSync(batchPath)) {
return { data: batchPath, message: "success" };
}
// 2. 创建外层批次目录,recursive自动补全上级目录
fs.mkdirSync(batchPath, { recursive: true });
// 3. 批量创建内部三个子目录,存在自动跳过
subDirList.forEach(subName => {
const subPath = path.join(batchPath, subName);
fs.mkdirSync(subPath, { recursive: true });
});
// 目录为本次新建
return { data: batchPath, message: "success" };
} catch (error) {
return { message: error.message };
}
}
export async function createBatchList(basePath, batchNoList) {
const fs = require("fs").promises;
const path = require("path");
const subFolders = ["原素材", "自动排版素材", "手动排版素材"];
let successCount = 0;
let skipCount = 0;
for (const batchNo of batchNoList) {
const str = `批次号:${batchNo}`;
const batchPath = path.join(basePath, str);
try {
// 已存在就跳过
try {
await fs.access(batchPath);
skipCount++;
continue;
} catch {
// 不存在,继续创建
}
// 创建批次文件夹
await fs.mkdir(batchPath, { recursive: true });
// 创建三个子文件夹
await Promise.all(
subFolders.map(name =>
fs.mkdir(path.join(batchPath, name), { recursive: true })
)
);
successCount++;
} catch (e) {
console.error(`批次 ${batchNo} 创建失败:`, e);
}
}
console.error(`创建完成:成功 ${successCount} 个,跳过 ${skipCount} 个`);
return true;
}
/**
* 格式化当前时间为 YYYY-MM-DD HH-mm-ss
* @returns {string}
*/
function formatTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hour = String(now.getHours()).padStart(2, "0");
const minute = String(now.getMinutes()).padStart(2, "0");
const second = String(now.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day}_${hour}.${minute}.${second}`;
}
/**
* 下载文件(图片/zip)到指定目录,zip格式自动解压
* @param {string} fileUrl - 文件链接(支持png/jpg/zip等)
* @param {string} targetDir - 目标文件夹绝对路径
* @param {object} options - 可选配置
* @param {boolean} options.deleteZipAfterUnzip - 解压后是否删除原zip文件,默认false
* @param {boolean} options.unzipToSubdir - zip是否解压到同名子文件夹,默认false(直接散列到目标目录)
* @returns {Promise<{success: boolean, type: string, filePath: string, fileName: string, unzipDir?: string}>}
*/
export function downloadDTFFiles(fileUrl, targetDir, options = {}) {
const { deleteZipAfterUnzip = true, unzipToSubdir = false } = options;
return new Promise((resolve, reject) => {
try {
// 1. 解析URL,判断协议
const urlObj = new URL(fileUrl);
const requestLib = urlObj.protocol === "https:" ? https : http;
// 2. 递归创建目标目录
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
// 3. 提取文件名(去除参数/哈希,无后缀默认补.jpg)
let cleanPath = urlObj.pathname;
try {
// 先解码URL编码字符,还原 / 和 \
cleanPath = decodeURIComponent(urlObj.pathname);
} catch (e) {
// 解码失败则使用原始路径兜底
cleanPath = urlObj.pathname;
}
// 统一所有反斜杠为正斜杠,兼容Windows路径格式
cleanPath = cleanPath.replace(/\\/g, "/");
// 提取最后一段:纯文件名(含后缀)
const originalFileName = path.basename(cleanPath);
// 最终命名:时间 + 原文件名,兜底空文件名
const fileName = originalFileName
? `${formatTime()} ${originalFileName}`
: `${formatTime()}.png`;
const savePath = path.join(targetDir, fileName);
// 4. 发起网络请求
const request = requestLib.get(fileUrl, response => {
// 处理301/302重定向
if ([301, 302, 307, 308].includes(response.statusCode)) {
const redirectUrl = response.headers.location;
const finalRedirectUrl = redirectUrl.startsWith("http")
? redirectUrl
: new URL(redirectUrl, fileUrl).href;
console.log("downloadDTFFiles---------", finalRedirectUrl, targetDir);
downloadDTFFiles(finalRedirectUrl, targetDir, options)
.then(resolve)
.catch(reject);
response.resume();
return;
}
if (response.statusCode !== 200) {
reject(new Error(`下载失败,HTTP状态码:${response.statusCode}`));
return;
}
// 5. 流式写入文件
const writeStream = fs.createWriteStream(savePath);
response.pipe(writeStream);
// 写入完成:判断格式,zip自动解压
writeStream.on("finish", () => {
writeStream.close(async () => {
const fileExt = path.extname(fileName).toLowerCase();
// 非zip文件:直接返回
if (fileExt !== ".zip") {
resolve({
success: true,
type: "image",
filePath: savePath,
fileName: fileName
});
return;
}
// zip文件:执行解压
try {
// 解压目标目录:直接解压到目标目录 / 或解压到同名子文件夹
let unzipTargetDir = targetDir;
if (unzipToSubdir) {
const dirName = path.basename(fileName, ".zip");
unzipTargetDir = path.join(targetDir, dirName);
if (!fs.existsSync(unzipTargetDir)) {
fs.mkdirSync(unzipTargetDir, { recursive: true });
}
}
// 执行解压
await compressing.zip.uncompress(savePath, unzipTargetDir);
// 可选:解压完成后删除原zip文件
if (deleteZipAfterUnzip) {
fs.unlinkSync(savePath);
}
resolve({
success: true,
type: "zip",
filePath: savePath,
fileName: fileName,
unzipDir: unzipTargetDir
});
} catch (unzipErr) {
reject(new Error(`ZIP解压失败:${unzipErr.message}`));
}
});
});
// 写入异常:删除残留文件
writeStream.on("error", err => {
if (fs.existsSync(savePath)) {
fs.unlink(savePath, () => {});
}
reject(new Error(`文件写入失败:${err.message}`));
});
});
// 网络请求异常
request.on("error", err => {
reject(new Error(`网络请求失败:${err.message}`));
});
// 超时控制
request.setTimeout(15000, () => {
request.destroy();
reject(new Error("下载超时,请检查网络或链接有效性"));
});
} catch (err) {
reject(new Error(`参数校验失败:${err.message}`));
}
});
}
......@@ -7,7 +7,6 @@ const MAX_LIMIT_RECORD_KEY = "originalIdMaxLimitRecord"; // 永久上限标记
// ===== 2. 核心内存缓存 =====
let orderCacheMap = new Map(); // key格式:前缀-数字(如AAAF...-1),value:原始订单对象
// ===== 3. 永久上限标记方法(保留你的原有核心逻辑,无改动) =====
/**
* 获取已达永久上限的原始newId Map
......@@ -197,11 +196,47 @@ module.exports = {
store.set("desktoBoard", version);
},
getBoard: () => store.get("desktoBoard") || 3,
setIsOver: value => {
store.set("isOver", value);
},
getIsOver: () => store.get("isOver") || false,
//储存用户信息
setUser: value => {
store.set("userData", value);
},
getUser: () => store.get("userData"),
//本地储存 DTG、DTF生产类型
setProductType: value => {
store.set("productType", value);
},
getProductType: () => store.get("productType") || "DTG",
//DTF生产设置
setDTFSetting: value => {
store.set("DTFSetting", value);
},
getDTFSetting: () =>
store.get("DTFSetting") || {
widthStrategy: "1",
// 最大素材数模式:batch / custom
materialStrategy: "1",
// 自定义素材数量
materialCount: 30,
// 自动排版规格 40+2 / 60 / none
autoLayoutStrategy: "3",
// 默认下载目录
downloadDir: ""
},
//本地储存 文件批次号
setBatchNumbers: value => {
store.set("batchNumbers", value);
},
getBatchNumbers: () => store.get("batchNumbers") || [],
setLocation: (version, locationKey) => {
if (!locationKey) return;
store.set(locationKey, version);
......
......@@ -26,6 +26,7 @@ function endLoading() {
service.interceptors.request.use(
config => {
const user = Vue.prototype.$dataStore.get("user");
if (user) {
config.headers["jwt-token"] = user.token;
}
......@@ -82,8 +83,26 @@ service.interceptors.response.use(
},
error => {
endLoading();
// do something with response error
return Promise.reject(error);
let errMsg = "请求失败,请稍后重试";
// 1. 有服务端响应:提取后端返回的 msg
if (error.response && error.response.data) {
const data = error.response.data;
// 优先取后端返回的 msg(适配你后端 {code:500, msg:'xxx'} 的格式)
errMsg =
data.msg || data.message || `请求错误(${error.response.status})`;
}
// 2. 无响应:网络层面错误
else if (error.message) {
if (error.message.includes("timeout")) {
errMsg = "请求超时,请检查网络";
} else if (error.message.includes("Network Error")) {
errMsg = "网络连接异常,请检查网络";
} else {
errMsg = error.message;
}
}
return Promise.reject(new Error(errMsg));
}
);
......
const { ipcRenderer } = window.require("electron");
// ========== 原素材下载 API(原有保留) ==========
/**
* 提交下载任务
* @param {Object} params { urls, targetPath, batchArrangeNum }
* @returns {Promise<Object>}
*/
export function submitDownload(params) {
return new Promise(resolve => {
ipcRenderer.once("download:submit-reply", (_, res) => resolve(res));
ipcRenderer.send("download:submit", params);
});
}
/**
* 获取当前下载状态
* @returns {Promise<Object>}
*/
export function getDownloadStatus() {
return new Promise(resolve => {
ipcRenderer.once("download:getStatus-reply", (_, res) => resolve(res));
ipcRenderer.send("download:getStatus");
});
}
/**
* 监听下载状态变更
* @param {Function} callback
* @returns {Function} 取消监听函数
*/
export function onDownloadStatusChange(callback) {
const handler = (_, data) => callback(data);
ipcRenderer.on("download:status", handler);
return () => ipcRenderer.removeListener("download:status", handler);
}
// ========== 新增:手动排版下载 API ==========
/**
* 提交手动排版下载任务
* @param {Object} params { urlStr: 逗号分隔URL字符串, targetPath: 保存目录 }
* @returns {Promise<Object>}
*/
export function submitTypesettingDownload(params) {
return new Promise(resolve => {
ipcRenderer.once("download-typesetting:submit-reply", (_, res) =>
resolve(res)
);
ipcRenderer.send("download-typesetting:submit", params);
});
}
/**
* 查询手动排版下载状态
* @returns {Promise<Object>}
*/
export function getTypesettingDownloadStatus() {
return new Promise(resolve => {
ipcRenderer.once("download-typesetting:getStatus-reply", (_, res) =>
resolve(res)
);
ipcRenderer.send("download-typesetting:getStatus");
});
}
/**
* 监听手动排版下载状态变更
* @param {Function} callback
* @returns {Function} 取消监听函数
*/
export function onTypesettingStatusChange(callback) {
const handler = (_, data) => callback(data);
ipcRenderer.on("download-typesetting:status", handler);
return () =>
ipcRenderer.removeListener("download-typesetting:status", handler);
}
const path = require("path");
const fs = require("fs");
const http = require("http");
const https = require("https");
const url = require("url");
const { setBatchNumbers, getBatchNumbers } = require("@/server/utils/store");
/* ===================== 基础工具函数 ===================== */
/**
* 从URL提取文件名
*/
function getFileName(urlStr) {
const formatUrl = urlStr.replace(/\\/g, "/");
const lastName = path.basename(formatUrl);
console.log("lastName", lastName);
// 先分割拿到数组
const nameArr = lastName.split("-");
// 判断是否存在横线分割成两段
if (nameArr.length >= 2) {
// OP260630368_test_1361_11852.png
return `${nameArr[1]}_${nameArr[0]}`;
}
// 没有横线直接返回原名
return lastName;
}
/**
* 判断是否为绝对URL
*/
function isAbsoluteUrl(urlStr) {
return /^https?:\/\//i.test(urlStr);
}
/**
* 补全相对路径为完整URL
*/
function resolveUrl(urlStr, baseUrl) {
if (isAbsoluteUrl(urlStr)) return urlStr;
let url = urlStr.replace(/^\//, "");
const nameArr = url.split("-");
if (nameArr.length >= 2) url = nameArr[0];
console.log("resolveUrl", `${baseUrl.replace(/\/$/, "")}/${url}`);
return `${baseUrl.replace(/\/$/, "")}/${url}`;
}
/**
* 递归创建目录
*/
async function ensureDir(dirPath) {
await fs.promises.mkdir(dirPath, { recursive: true });
}
/**
* 判断文件是否存在
*/
async function isFileExists(filePath) {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
return true;
} catch (e) {
return false;
}
}
/**
* 并发限制器
*/
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();
});
}
};
return fn => {
return new Promise((resolve, reject) => {
queue.push(() =>
fn()
.then(resolve)
.catch(reject)
);
next();
});
};
}
/* ===================== 核心下载能力 ===================== */
/**
* 单文件流式下载(支持重定向、超时、自动清理残文件)
* @param {string} fileUrl 完整下载链接
* @param {string} filePath 本地保存路径
* @param {number} timeout 超时时间ms
* @param {number} redirectCount 已重定向次数
*/
function streamDownload(fileUrl, filePath, timeout = 30000, redirectCount = 0) {
return new Promise((resolve, reject) => {
try {
console.log("fileUrl", fileUrl);
const requestModule = fileUrl.startsWith("https") ? https : http;
const req = requestModule.get(fileUrl, { timeout }, res => {
// 重定向处理,最多3次防死循环
if ([301, 302, 307, 308].includes(res.statusCode)) {
if (redirectCount >= 3) {
res.resume();
reject(new Error("重定向次数超过3次"));
return;
}
const redirectUrl = url.resolve(fileUrl, res.headers.location);
streamDownload(redirectUrl, filePath, timeout, redirectCount + 1)
.then(resolve)
.catch(reject);
return;
}
if (res.statusCode !== 200) {
res.resume();
reject(new Error(`HTTP状态码异常: ${res.statusCode}`));
return;
}
let writeStream;
try {
writeStream = fs.createWriteStream(filePath);
} catch (err) {
res.resume();
reject(new Error(`路径创建失败:${err.message}`));
return;
}
res.pipe(writeStream);
writeStream.on("finish", () => {
writeStream.close(() => resolve(filePath));
});
writeStream.on("error", err => {
writeStream.destroy();
fs.promises.unlink(filePath).catch(() => {});
reject(err);
});
res.on("error", err => {
writeStream.destroy();
fs.promises.unlink(filePath).catch(() => {});
reject(new Error(`网络响应异常: ${err.message}`));
});
});
req.on("error", err => reject(new Error(`网络请求失败: ${err.message}`)));
req.on("timeout", () => {
req.destroy();
reject(new Error("下载超时"));
});
} catch (syncErr) {
reject(new Error(`请求初始化失败: ${syncErr.message}`));
}
});
}
/**
* 带失败重试的单文件下载
*/
async function downloadWithRetry(
fileUrl,
filePath,
retryTimes = 0,
timeout = 30000
) {
const attempt = async remain => {
try {
return await streamDownload(fileUrl, filePath, timeout);
} catch (err) {
if (remain > 0) {
console.log(`[下载重试] 剩余 ${remain} 次:${path.basename(filePath)}`);
return attempt(remain - 1);
}
throw err;
}
};
return attempt(retryTimes);
}
/* ===================== 批量下载封装 ===================== */
/**
* 批量下载(URL数组版本)
* @param {string[]} urlList URL数组
* @param {string} targetDir 目标目录
* @param {Object} options 配置
* @returns {Promise<{success: string[], failed: Array<{url:string, error:string}>}>}
*/
async function batchDownloadByList(urlList, targetDir, options = {}) {
const {
concurrency = 5,
timeout = 30000,
retryTimes = 0,
baseUrl = "",
skipExists = true,
fileNameFormatter = null, // 新增:自定义文件名回调,
isCheck = false,
batchId = "" // 新增:批次唯一标识,isCheck 模式下使用
} = options;
const success = [];
const failed = [];
if (isCheck) {
// 批次已处理过则直接跳过整个批次,不执行后续任何下载逻辑
const batchNumbers = getBatchNumbers() || [];
if (batchNumbers.includes(batchId)) {
console.log(`该批次已执行过,直接跳过 | 批次号: ${batchId}`);
return {
success: [],
failed: [],
skipped: true,
batchId
};
}
}
// 1. 创建目标目录
try {
await ensureDir(targetDir);
} catch (dirErr) {
const errMsg = `目标目录创建失败:${dirErr.message}`;
console.error(`[批量下载][路径错误] ${errMsg} | 目录: ${targetDir}`);
return {
success: [],
failed: [{ url: "", error: errMsg }]
};
}
// 2. 并发执行下载
const limit = pLimit(concurrency);
const tasks = urlList.map(urlStr =>
limit(async () => {
const fullUrl = baseUrl ? resolveUrl(urlStr, baseUrl) : urlStr;
const fileName = fileNameFormatter
? fileNameFormatter(urlStr, baseUrl)
: getFileName(urlStr);
const savePath = path.join(targetDir, fileName);
try {
// 已存在则跳过
if (skipExists && (await isFileExists(savePath))) {
console.log(`[批量下载][跳过] 文件已存在: ${fileName}`);
success.push(savePath);
return;
}
await downloadWithRetry(fullUrl, savePath, retryTimes, timeout);
console.log(`[批量下载][成功] ${fileName}`);
success.push(savePath);
} catch (err) {
// 识别路径类错误
const isPathError =
["ENOENT", "EINVAL", "ENAMETOOLONG", "EPERM", "EACCES"].includes(
err.code
) || /路径|path|file/i.test(err.message);
const errorType = isPathError ? "路径错误" : "下载失败";
console.error(
`[批量下载][${errorType}] 跳过文件: ${fileName} | 链接: ${fullUrl} | 原因: ${err.message}`
);
failed.push({ url: fullUrl, error: err.message });
}
})
);
if (isCheck && batchId) {
setBatchNumbers([...getBatchNumbers(), batchId]);
console.log(`批量下载 批次标记已标记批次为已处理 | 批次号: ${batchId}`);
}
await Promise.all(tasks);
console.log(`批量下载完成,成功: ${success.length},失败: ${failed.length}`);
return { success, failed };
}
/**
* 批量下载(逗号分隔URL字符串版本)
* @param {string} urlStr 逗号分隔的URL字符串
* @param {string} targetDir 目标目录
* @param {Object} options 配置
*/
async function batchDownloadByStr(urlStr, targetDir, options = {}) {
// 参数校验
if (!urlStr || typeof urlStr !== "string") {
console.error("[批量下载][参数错误] 传入的URL字符串为空或格式无效");
return { success: [], failed: [{ url: "", error: "URL字符串无效" }] };
}
// 拆分URL
const urlList = urlStr
.split(",")
.map(u => u.trim())
.filter(u => u);
if (urlList.length === 0) {
console.warn("[批量下载][无有效链接] 拆分后无可用URL,直接跳过");
return { success: [], failed: [] };
}
return batchDownloadByList(urlList, targetDir, options);
}
module.exports = {
// 工具函数
getFileName,
isAbsoluteUrl,
resolveUrl,
ensureDir,
isFileExists,
pLimit,
// 核心下载
streamDownload,
downloadWithRetry,
// 批量下载
batchDownloadByList,
batchDownloadByStr
};
const { ipcMain, BrowserWindow } = require("electron");
const { getHostApi } = require("@/server/utils/store");
const { batchDownloadByList } = require("./download-core");
/* ========== 任务管理模块(全局互斥锁 + 状态广播) ========== */
const taskState = {
isRunning: false,
currentBatchNo: "",
latestResult: { success: [], failed: [] }
};
function broadcastStatus() {
const allWindows = BrowserWindow.getAllWindows();
allWindows.forEach(win => {
if (!win.isDestroyed()) {
win.webContents.send("download:status", {
isRunning: taskState.isRunning,
currentBatchNo: taskState.currentBatchNo,
result: taskState.latestResult
});
}
});
}
function submitTask(params) {
const { urls, targetPath, batchArrangeNum } = params;
if (taskState.isRunning) {
return {
code: 501,
msg: `当前正在下载批次【${taskState.currentBatchNo}】,请等待完成后再操作`,
data: { isRunning: true }
};
}
if (!Array.isArray(urls) || urls.length === 0) {
return { code: 500, msg: "下载链接数组为空" };
}
taskState.isRunning = true;
taskState.currentBatchNo = batchArrangeNum || "未知批次";
taskState.latestResult = { success: [], failed: [] };
broadcastStatus();
// 调用核心下载能力
batchDownloadByList(urls, targetPath, {
concurrency: 2,
timeout: 30000,
retryTimes: 1,
baseUrl: getHostApi().fileApiUrl
})
.then(result => {
taskState.latestResult = result;
})
.catch(err => {
taskState.latestResult = {
success: [],
failed: [{ url: "", error: err.message || "下载任务异常中断" }]
};
})
.finally(() => {
taskState.isRunning = false;
taskState.currentBatchNo = "";
broadcastStatus();
});
return {
code: 200,
msg: "下载进行中,下载完成的素材将在文件夹原素材中显示",
data: { isRunning: true }
};
}
function getStatus() {
return {
code: 200,
data: {
isRunning: taskState.isRunning,
currentBatchNo: taskState.currentBatchNo,
result: taskState.latestResult
}
};
}
/* ========== IPC 注册 ========== */
function registerDownloadIpc() {
ipcMain.on("download:submit", (event, params) => {
const result = submitTask(params);
event.sender.send("download:submit-reply", result);
});
ipcMain.on("download:getStatus", event => {
const result = getStatus();
event.sender.send("download:getStatus-reply", result);
});
}
module.exports = registerDownloadIpc;
const path = require("path");
const { pathMap } = require("@/config/index.js");
const { getDTFSetting, getHostApi } = require("@/server/utils/store");
import api from "./request";
const { batchDownloadByStr } = require("./download-core.js");
/* ========== 全局状态 ========== */
const taskState = {
currentPage: 1,
pageSize: 100,
layoutStatus: "3,5",
isDownloading: false,
timer: null,
total: 0,
interval: 1 * 60 * 1000,
maxBatchConcurrency: 3,
maxImageConcurrency: 10
};
/* ========== 工具函数(业务专属,保留) ========== */
function logDownloadError(type, batchNo, url, message) {
console.error(
`[自动下载失败][${type}] 批次号: ${batchNo || "未知"} | 链接: ${url ||
"-"} | 详情: ${message}`
);
}
// 复用core的并发控制器,也可以直接用core导出的pLimit
const { pLimit } = require("./download-core");
/* ========== 业务逻辑 ========== */
async function processBatchItem(item) {
const { batchArrangeNum, spUrl: urlStr } = item;
if (!batchArrangeNum) {
logDownloadError(
"参数无效",
batchArrangeNum,
"",
"缺少批次号batchArrangeNum"
);
return;
}
if (!urlStr || typeof urlStr !== "string") {
logDownloadError(
"参数无效",
batchArrangeNum,
urlStr,
"url字段为空或非字符串格式"
);
return;
}
// 构建保存目录
let saveDir;
try {
const downloadRoot = getDTFSetting().downloadDir;
if (!downloadRoot) throw new Error("下载根目录downloadDir配置为空");
saveDir = path.join(
downloadRoot,
`批次号:${batchArrangeNum}`,
"自动排版素材"
);
} catch (dirErr) {
logDownloadError("目录创建失败", batchArrangeNum, "", dirErr.message);
return;
}
// 调用核心批量下载
await batchDownloadByStr(urlStr, saveDir, {
concurrency: taskState.maxImageConcurrency,
timeout: 30000,
baseUrl: getHostApi().fileApiUrl,
isCheck: true,
batchId: batchArrangeNum
});
}
async function runTask() {
if (taskState.isDownloading) {
console.log("当前有下载任务进行中,跳过本轮定时任务");
return;
}
try {
taskState.isDownloading = true;
console.log(`=== 开始执行定时任务,当前页码:${taskState.currentPage} ===`);
// 获取排版策略
const settingRes = await api.post(pathMap["productionConfig"]);
if (settingRes.data.autoLayoutStrategy === 3) {
console.log("不自动排版,跳过本次下载");
return;
}
// 拉取排版数据
const layoutRes = await api.post(pathMap["productionSoftware"], {
currentPage: taskState.currentPage,
pageSize: taskState.pageSize,
layoutStatus: taskState.layoutStatus
});
const list = layoutRes.data.records || [];
taskState.total = layoutRes.data.total || 0;
if (list.length === 0) {
console.log("当前页无数据,本轮任务结束");
return;
}
console.log(layoutRes);
// 多批次并行下载
const batchLimit = pLimit(taskState.maxBatchConcurrency);
const batchPromises = list.map(item =>
batchLimit(() => processBatchItem(item))
);
await Promise.all(batchPromises);
console.log(`=== 第 ${taskState.currentPage} 页全部处理完成 ===`);
// 更新页码
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("本轮任务结束,锁已释放");
}
}
/* ========== 对外启停方法 ========== */
function startAutoDownloadTask() {
if (taskState.timer) return;
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("自动下载定时任务已停止");
}
}
export default {
startAutoDownloadTask,
stopAutoDownloadTask
};
const { ipcMain, BrowserWindow } = require("electron");
const path = require("path");
const { getHostApi } = require("@/server/utils/store");
const { batchDownloadByStr } = require("./download-core");
/* ===================== 原有命名逻辑(完全保留) ===================== */
function formatTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hour = String(now.getHours()).padStart(2, "0");
const minute = String(now.getMinutes()).padStart(2, "0");
const second = String(now.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day}_${hour}.${minute}.${second}`;
}
function customFileNameFormatter(urlStr) {
let cleanPath = urlStr;
try {
cleanPath = decodeURIComponent(urlStr);
} catch (e) {
cleanPath = urlStr;
}
cleanPath = cleanPath.replace(/\\/g, "/");
const originalFileName = path.basename(cleanPath);
const timePrefix = formatTime();
return originalFileName
? `${timePrefix} ${originalFileName}`
: `${timePrefix}.png`;
}
/**
* 原有纯函数调用方式(保留,兼容现有代码)
*/
async function batchDownloadUrls(urlStr, targetDir, options = {}) {
return batchDownloadByStr(urlStr, targetDir, {
baseUrl: getHostApi().fileApiUrl,
fileNameFormatter: customFileNameFormatter,
...options
});
}
/* ===================== 新增:IPC 任务管理模块(独立锁,与原素材隔离) ===================== */
const taskState = {
isRunning: false,
latestResult: { success: [], failed: [], currentBatchNo: "" }
};
/**
* 向所有窗口广播状态
*/
function broadcastStatus() {
const allWindows = BrowserWindow.getAllWindows();
allWindows.forEach(win => {
if (!win.isDestroyed()) {
win.webContents.send("download-typesetting:status", {
isRunning: taskState.isRunning,
result: taskState.latestResult
});
}
});
}
/**
* 提交下载任务(异步执行,立即返回)
*/
function submitTask(params) {
const { urlStr, targetPath, batchArrangeNum } = params;
taskState.currentBatchNo = batchArrangeNum;
// 全局互斥:有任务在跑直接拒绝
if (taskState.isRunning) {
return {
code: 500,
msg: "当前正在下载手动排版素材,请等待完成后再操作",
data: { isRunning: true }
};
}
if (!urlStr || typeof urlStr !== "string") {
return { code: 500, msg: "下载链接为空或格式无效" };
}
// 加锁
taskState.isRunning = true;
taskState.latestResult = { success: [], failed: [] };
broadcastStatus();
// 后台异步执行下载,不阻塞 IPC 返回
batchDownloadUrls(urlStr, targetPath)
.then(result => {
taskState.latestResult = result;
})
.catch(err => {
taskState.latestResult = {
success: [],
failed: [{ url: "", error: err.message || "下载任务异常中断" }]
};
})
.finally(() => {
// 释放锁
taskState.isRunning = false;
broadcastStatus();
});
return {
code: 200,
msg: "排版进行中,请查看处理状态,排版完成的素材至文件夹排版素材查看",
data: { isRunning: true }
};
}
/**
* 查询当前下载状态
*/
function getStatus() {
return {
code: 0,
data: {
isRunning: taskState.isRunning,
result: taskState.latestResult
}
};
}
/* ===================== 新增:IPC 事件注册 ===================== */
/**
* 注册手动排版下载 IPC 事件
* 在主进程入口调用一次即可
*/
function registerTypesettingDownloadIpc() {
// 提交下载任务
ipcMain.on("download-typesetting:submit", (event, params) => {
const result = submitTask(params);
event.sender.send("download-typesetting:submit-reply", result);
});
// 查询下载状态
ipcMain.on("download-typesetting:getStatus", event => {
const result = getStatus();
event.sender.send("download-typesetting:getStatus-reply", result);
});
}
/* ===================== 导出(兼容两种使用方式) ===================== */
module.exports = {
// 原有纯函数,可直接在主进程内调用
batchDownloadUrls,
// IPC 注册函数,主进程入口调用
registerTypesettingDownloadIpc
};
......@@ -62,7 +62,7 @@ export const cutArea = (canvasEl, mousewheel) => {
boundaryLeft,
boundaryTop,
boundaryWidth,
boundaryHeight,
boundaryHeight
} = getRealBoundary(canvasEl, mousewheel);
//存储正方形边界
......@@ -70,7 +70,7 @@ export const cutArea = (canvasEl, mousewheel) => {
boundaryLeft,
boundaryTop,
boundaryWidth,
boundaryHeight,
boundaryHeight
});
cutCtx.clearRect(boundaryLeft, boundaryTop, boundaryWidth, boundaryHeight);
......@@ -92,7 +92,7 @@ export const cutArea = (canvasEl, mousewheel) => {
canvasEl.set({
cutWidth: width,
cutHeight: height,
cutHeight: height
});
//设置蒙版图片
......@@ -119,7 +119,7 @@ export const cutArea = (canvasEl, mousewheel) => {
canvasEl.maskDiv.appendChild(mask_img);
insertAfter(
canvasEl.maskDiv,
document.getElementById(canvasEl.cutCanvas.id),
document.getElementById(canvasEl.cutCanvas.id)
);
}
......@@ -177,7 +177,7 @@ const getRealBoundary = function(canvasEl, mousewheel) {
if (!canvasEl.oldCutWidth) {
canvasEl.set({
oldCutWidth: cutWidth,
oldCutHeight: cutHeight,
oldCutHeight: cutHeight
});
}
}
......@@ -218,7 +218,7 @@ const getRealBoundary = function(canvasEl, mousewheel) {
boundaryLeft,
boundaryTop,
boundaryWidth,
boundaryHeight,
boundaryHeight
};
};
......
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;
......@@ -1144,7 +1144,7 @@ export default {
z-index: 4;
bottom: 0;
height: calc(100% - 50px);
height: calc(100% - 75px);
.print-content {
background: #fff;
......
<script>
import PHead from "./head/index.vue";
import PMain from "./main/index.vue";
import UpdateDialog from "@/views/design/updateDialog.vue";
const { getUser } = require("@/server/utils/store");
export default {
components: { PHead, PMain, UpdateDialog },
name: "design-dtg",
components: { PHead, PMain },
data() {
return {
user: {},
factoryType: "CN",
userData: getUser()
};
},
mounted() {
this.user = this.$dataStore.get("user");
if (this.user.factory && this.user.factory.countryCode) {
this.factoryType = this.user.factory.countryCode;
}
// console.log(17, this.user);
this.$refs.updateDialog.checkUpdate().then((data) => {
// console.log(17, data);
if (data) {
// 有新版本更新
this.$refs.updateDialog.open(data);
}
});
},
mounted() {},
methods: {}
};
</script>
<template>
<div class="page">
<p-head :user="user" :factoryType="factoryType" />
<p-head :user="userData.user" :factoryType="userData.factoryType" />
<p-main :factoryType="factoryType" />
<update-dialog ref="updateDialog" />
</div>
</template>
<style scoped>
<style scoped lang="less">
.page {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
// border-top: 1px solid #dcdfe6;
}
</style>
......@@ -18,33 +18,33 @@ export default {
y: 0,
w: 0,
h: 0,
r: 0,
},
r: 0
}
};
},
computed: {
...mapState(["isPreView"]),
...mapState(["isPreView"])
},
props: {
visible: {
type: Boolean,
default: false,
default: false
},
item: {
type: Object,
default: () => {},
default: () => {}
},
newDesktopDevice: {
type: [String, Number],
default: "1",
},
default: "1"
}
},
watch: {
visible: {
handler() {
this.drawerShow = this.visible;
},
immediate: true,
immediate: true
},
item: {
handler(val) {
......@@ -67,14 +67,14 @@ export default {
y: 0,
w: 0,
h: 0,
r: 0,
r: 0
};
}
// console.log("item", this.item);
},
immediate: true,
deep: true,
},
deep: true
}
},
methods: {
pxToUnit,
......@@ -87,7 +87,7 @@ export default {
this.item.x = item.x - item.w / 2;
this.$dataStore.set(
"position_before_px",
JSON.parse(JSON.stringify(this.item)),
JSON.parse(JSON.stringify(this.item))
);
console.log("this.item", this.item);
......@@ -107,7 +107,7 @@ export default {
this.form.r = this.item.r;
this.$dataStore.set(
`position_after_px`,
JSON.parse(JSON.stringify(this.form)),
JSON.parse(JSON.stringify(this.form))
);
},
formChange(t) {
......@@ -169,8 +169,8 @@ export default {
if (this.form[f] < 0) this.$set(this.form, f, 0);
}
this.formChange();
},
},
}
}
};
</script>
......@@ -370,14 +370,14 @@ export default {
width: 500px;
// bottom: 0;
// top: 50px;
padding: 10px;
// padding: 10px;
// z-index: 99;
background: white;
// box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
// 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12);
// left: 0;
// position: fixed;
border-right: 1px solid #ececec;
// border-right: 1px solid #ececec;
// height: calc(100vh - 50px);
display: flex;
flex-direction: column;
......
......@@ -38,22 +38,18 @@ export default {
},
created() {
this.$store.commit("getNewGrid");
let proportion;
ipcRenderer.on("window-size", (event, size) => {
const { width, height } = size;
console.log(`接收到窗口尺寸:${width}x${height}`);
let proportion;
proportion = 1.4;
// } else {
// proportion = 1.5;
// }
this.$store.commit("setWHproportion", proportion);
this.$store.commit("setWindows", { width, height });
this.$store.commit("setDefaultproportion", proportion);
this.$store.commit("setGrid");
});
this.$store.commit("setDefaultproportion", proportion);
this.$store.commit("setGrid");
if (!this.countryList.length) {
this.getAllCountryFn();
}
......@@ -1593,7 +1589,7 @@ export default {
<div class="item-label">工厂:</div>
<div class="item-value">{{ detail.factoryCode }}</div>
</div>
<div :title="detail.shopNumber" class="div-item" style="flex: 50%;">
<div :title="detail.shopNumber" class="div-item">
<div>
店铺单号:
</div>
......@@ -1601,11 +1597,7 @@ export default {
{{ detail.shopNumber }}
</div>
</div>
<div
:title="detail.endProductId"
class="div-item"
style="flex: 50%;"
>
<div :title="detail.endProductId" class="div-item">
<div>
客户保存记录ID:
</div>
......@@ -1613,19 +1605,11 @@ export default {
{{ detail.endProductId || "" }}
</div>
</div>
<div
:title="detail.createTime"
class="div-item"
style="flex: 100%;"
>
<div :title="detail.createTime" class="div-item">
<div>创建时间:</div>
<div>{{ detail.createTime }}</div>
</div>
<div
:title="detail.startStockingTime"
class="div-item"
style="flex: 100%;"
>
<div :title="detail.startStockingTime" class="div-item">
<div>
生产确认时间:
</div>
......@@ -2033,6 +2017,7 @@ export default {
flex-shrink: 0;
overflow: hidden;
box-sizing: border-box;
height: 100%;
}
.main-bg {
......@@ -2049,7 +2034,7 @@ export default {
// z-index: 2;
left: 5%;
top: 50%;
top: 46%;
transform: translate(0, -50%);
background-color: #f0f2f6;
}
......
<script>
import UpdateDialog from "@/views/design/updateDialog.vue";
import { Loading } from "element-ui";
const {
getProductType,
setProductType,
setUser
} = require("@/server/utils/store");
export default {
components: { UpdateDialog },
data() {
return {
user: {},
factoryType: "CN",
activeName: getProductType(),
loading: null
};
},
mounted() {
this.user = this.$dataStore.get("user");
if (this.user.factory && this.user.factory.countryCode) {
this.factoryType = this.user.factory.countryCode;
}
this.$refs.updateDialog?.checkUpdate().then(data => {
if (data) {
// 有新版本更新
this.$refs.updateDialog?.open(data);
}
});
setUser({ user: this.user, factoryType: this.factoryType });
const routerName = this.$router.currentRoute.name;
const routerMap = { DTG: "design-dtg", DTF: "design-dtf" };
if (routerName !== routerMap[getProductType()]) {
if (getProductType() === "DTG") {
this.$router.push({ name: "design-dtg" });
} else if (getProductType() === "DTF") {
this.$router.push({ name: "design-dtf" });
}
}
},
methods: {
tabClick(tab) {
const targetName = tab.name === "DTG" ? "design-dtg" : "design-dtf";
// 如果已经在目标路由,直接返回,不跳转
if (this.$route.name === targetName) {
return;
}
this.startLoading();
try {
setProductType(tab.name);
if (tab.name === "DTG") {
this.$router.push({ name: "design-dtg" });
} else if (tab.name === "DTF") {
this.$router.push({ name: "design-dtf" });
}
} catch (error) {
console.log(error);
} finally {
this.endLoading();
}
},
startLoading() {
this.loading = Loading.service({
lock: true,
text: "拼命加载中...",
spinner: "el-icon-loading",
background: "rgba(0,0,0,.7)"
});
},
endLoading() {
this.loading.close();
}
}
};
</script>
<template>
<div class="page">
<el-tabs type="border-card" v-model="activeName" @tab-click="tabClick">
<el-tab-pane label="DTG生产" name="DTG">
<router-view v-if="activeName === 'DTG'"></router-view>
</el-tab-pane>
<el-tab-pane label="DTF生产" name="DTF">
<router-view v-if="activeName === 'DTF'"></router-view>
</el-tab-pane>
</el-tabs>
<update-dialog ref="updateDialog" />
</div>
</template>
<style scoped lang="less">
.page {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
// border-top: 1px solid #dcdfe6;
}
::v-deep {
.el-tabs {
height: 100%;
border: none;
}
.el-tabs__header {
margin: 0;
background-color: #c6c6c6;
// height: 20px;
}
.el-tabs__header .el-tabs__item {
font-size: 12px;
line-height: 25px;
height: 25px;
// border-bottom: none;
}
.el-tabs--border-card > .el-tabs__content {
padding: 0;
height: 100%;
}
.el-tab-pane {
height: 100%;
}
.el-tabs--border-card > .el-tabs__header .el-tabs__item.is-active {
border: none;
background-color: #243151;
color: #fff;
}
.el-tabs--border-card > .el-tabs__header .el-tabs__item {
border: none;
}
.el-tabs--border-card
> .el-tabs__header
.el-tabs__item:not(.is-disabled):hover {
color: #fff;
// background-color: #5b647c;
}
}
</style>
<script>
const { getDTFSetting } = require("@/server/utils/store");
const { ipcRenderer } = require("electron");
const { statusMap } = require("@/config/index.js");
const { pathMap } = require("@/config/index.js");
const { getUser } = require("@/server/utils/store");
var path = require("path");
import bus from "@/bus";
export default {
props: {
selection: { type: Array, default: () => [] },
podOrderProductIds: { type: Array, default: () => [] }
},
data() {
return {
dialogVisible: false,
dtfSetting: getDTFSetting(),
radio: "",
curBatchArrangeNum: "",
searchForm: {},
statusMap: statusMap,
userData: getUser()
};
},
mounted() {
const params = this.$route.params;
this.curBatchArrangeNum = params.batchArrangeNum;
bus.$on("downLoadFile", async v => {
let { podOrderProductId, batchArrangeNumber } = v;
console.log(31, v);
try {
let res = await this.$api.post(
pathMap["downloadBySubOrderNumber"]["OP"],
{
ids: [podOrderProductId],
device: 4,
orderType: "OP",
targetDir: path.join(
getDTFSetting().downloadDir,
`批次号:${batchArrangeNumber}`,
"原素材"
)
}
);
console.log(77, res);
} catch (error) {
this.$message.error(error);
}
});
},
created() {},
methods: {
dropdownCommand(v) {
switch (v) {
case "logout":
this.$confirm("是否退出登录?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$dataStore.delete("user");
this.$router.push("/");
})
.catch(() => {});
break;
case "cache":
this.checkList = [];
this.cacheVisible = true;
break;
}
},
getCountryName(code) {
if (code) {
const item = this.countryList?.find(el => el.countryCode == code);
if (item) {
return `(${item.nameCn})`;
}
}
return "";
},
goBack() {
this.$router.push({ name: "design-dtf" });
},
async downLoadFn() {
if (!this.podOrderProductIds.length) {
return this.$message.warning("请选择操作单");
}
try {
let res = await this.$api.post(
pathMap["downloadBySubOrderNumber"]["OP"],
{
ids: this.podOrderProductIds,
device: 4,
orderType: "OP",
targetDir: path.join(
getDTFSetting().downloadDir,
`批次号:${this.curBatchArrangeNum}`,
"原素材"
)
}
);
console.log(77, res);
} catch (error) {
this.$message.error(error);
}
},
getDataInfo() {
this.$emit("search", this.searchForm);
},
productionCompleted() {
if (!this.selection.length) {
return this.$message.warning("请选择操作单");
}
this.$confirm(`确定生产完成?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await this.$api.post(pathMap.completeDelivery["DTF"], {
orderType: "DTF",
ids: this.selection
});
});
},
changeTypesetting() {
if (!this.selection.length) {
return this.$message.warning("请选择操作单");
}
this.dialogVisible = true;
this.radio = "";
},
async submit() {
try {
if (!this.radio) {
return this.$message.error("请选择排版宽度");
}
const payload = {
ids: this.selection,
type: "png",
templateWidth: this.radio,
targetDir: path.join(
getDTFSetting().downloadDir,
`批次号:${this.curBatchArrangeNum}`,
"手动排版素材"
)
};
if (this.dtfSetting.widthStrategy == 3 && this.radio == 60) {
this.$confirm(
"由于排版设置中选择“按更大规格排版”仅适用于40+2,选60时,默认超60的规格不排版",
"提示",
{
confirmButtonText: "确定",
type: "warning"
}
);
const { data } = await this.$api.post(
pathMap["replenishmentComposingDesignImages"],
payload
);
console.log("submit", data);
this.$message.success("DTF排版成功,请到手动排版素材中查看");
} else {
const { data } = await this.$api.post(
pathMap["replenishmentComposingDesignImages"],
payload
);
console.log("submit", data);
this.$message.success("DTF排版成功,请到手动排版素材中查看");
}
this.dialogVisible = false;
this.getDataInfo();
} catch (error) {
console.log("submit", error);
}
},
async goFliePage() {
let { data } = await this.$api.post("/createBatchFolder", {
basePath: this.dtfSetting.downloadDir,
downloadDir: this.curBatchArrangeNum,
isArray: false
});
if (!data) {
return this.$message.error("下载文件目录未配置或者目录错误");
}
ipcRenderer.send("open-folder-only", data);
}
}
};
</script>
<template>
<div>
<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"
>返回</el-button
></el-form-item
>
<el-form-item>
<el-input
placeholder="请扫描操作单"
v-model="searchForm.operationNo"
clearable
></el-input
></el-form-item>
<el-form-item label="操作单状态">
<el-select
style="width: 120px;"
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="请选择"
clearable
style="width: 120px;"
>
<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"
>查询
</el-button></el-form-item
>
<el-form-item>
<el-button @click="productionCompleted" type="success"
>生产完成
</el-button></el-form-item
>
<el-form-item>
<el-button @click="downLoadFn" type="success"
>下载素材
</el-button></el-form-item
>
<el-form-item>
<el-button @click="changeTypesetting" type="success"
>排版
</el-button></el-form-item
>
<el-form-item>
<el-button @click="goFliePage" type="success"
>查看排版文件
</el-button></el-form-item
>
</el-form>
<div class="right-user">
<div
v-if="userData && userData.factory"
style="font-weight: 700;margin-left: 8px;"
>
{{
userData.user.countryCode
? `${userData.user.countryCode}${getCountryName(
userData.user.countryCode
)}`
: "CN(中国)"
}}
</div>
<p v-if="userData.user && userData.user.factory">
{{ userData.user.factory.title }}
</p>
<el-dropdown @command="dropdownCommand">
<div style="margin-right: 20px">
<b style="cursor:pointer;">{{ userData.user.account }}</b>
<i class="el-icon-arrow-down"></i>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<el-dialog title="排版宽度" :visible.sync="dialogVisible" width="25%">
<el-radio-group v-model="radio">
<el-radio :label="42">40+2cm</el-radio>
<el-radio :label="60">60cm</el-radio>
</el-radio-group>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submit"> </el-button>
</span>
</el-dialog>
</div>
</template>
<style lang="less" scoped>
.right-user {
display: flex;
align-items: center;
b {
margin: 0 3px;
display: inline-block;
color: black;
font-size: 15px;
font-weight: bold;
}
p {
margin: 0 10px;
font-weight: bold;
display: inline-block;
font-size: 15px;
color: #e6a23c;
}
}
.page-header {
display: flex;
justify-content: space-between;
background-color: #ececec;
padding: 5px;
.el-form-item {
margin-bottom: 0;
}
}
</style>
<script>
import BaseCard from "@/components/BaseCard.vue";
import DTFhead from "./components/detailHead.vue";
import DetailDialog from "@/components/detailDialog.vue";
const { remote } = require("electron");
const { pathMap } = require("@/config/index.js");
import platformJson from "@/json/platform.json";
import LogList from "@//components/LogList.vue";
const { Menu } = remote;
import dayjs from "dayjs";
export default {
name: "dtf-batch-detail",
components: { BaseCard, DTFhead, DetailDialog, LogList },
props: {
user: {
default: () => ({
avatar: "",
factory: {}
}),
type: Object
},
factoryType: { type: String, default: "CN" }
},
data() {
return {
logVisible: false,
detailVisible: false,
curData: {},
logList: [],
selection: [],
podOrderProductIds: [],
pageInfo: {
currentPage: 1,
pageSize: 10,
total: 2
},
cardList: [],
timer: null,
curBatchArrangeNum: "",
detailRow: {
batchArrangeNum: { label: "批次号", value: "" },
operationNum: { label: "操作单数量", value: "" },
materialNum: { label: "素材数量", value: "" },
craftType: { label: "工艺类型", value: "" },
standardDesignImage: { label: "规范素材", value: "" },
createTime: { label: "创建时间", value: "" },
layoutStatus: { label: "排版状态", value: "" },
status: { label: "操作单状态", value: "" }
},
platformJson: platformJson,
layoutStatus: {
1: { label: "待排版", type: "warning" },
2: { label: "排版中", type: "info" },
3: { label: "排版成功", type: "success" },
4: { label: "排版失败", type: "danger" },
5: { label: "部分成功", type: "success" }
}
};
},
mounted() {
this.loadRowData();
this.timer = setInterval(() => this.getCardList(), 5 * 60 * 1000);
this.curData = {};
this.selection = [];
this.podOrderProductIds = [];
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer);
}
},
methods: {
async loadRowData() {
try {
const params = this.$route.params;
this.curBatchArrangeNum = params.batchArrangeNum;
Object.keys(this.detailRow).forEach(key => {
// 路由里有对应值就赋值,没有则保留默认值
if (params[key] !== undefined) {
this.detailRow[key].value = params[key];
}
});
await this.getCardList();
} catch (error) {
console.log(error);
}
},
async getCardList(value) {
this.selection = [];
let params = {
currentPage: this.pageInfo.currentPage,
pageSize: this.pageInfo.pageSize,
batchArrangeNumber: this.curBatchArrangeNum
};
if (value) {
params = { ...params, ...value };
}
try {
const { data } = await this.$api.post(
pathMap["listOperationByBatch"],
params
);
this.cardList = data.records;
this.pageInfo.total = data.total;
console.log("listOperationByBatch", data);
} catch (error) {
console.log(error);
}
},
handleSelect(rows) {
console.log("选中行", rows);
},
handleSizeChange(size) {
this.pageInfo.pageSize = size;
this.pageInfo.pageNum = 1;
this.loadData();
},
handleCurrentChange(page) {
this.pageInfo.pageNum = page;
this.loadData();
},
copyText(text) {
const { clipboard } = window.require("electron").remote;
// 复制文本,同步方法,调用即生效
clipboard.writeText(text);
this.$message.success(`复制成功`);
},
getOrderDelayText(createTime) {
if (!createTime) return "--";
const createAt = dayjs(createTime);
if (!createAt.isValid()) return "--";
const diffMinutes = Math.max(0, dayjs().diff(createAt, "minute"));
const baseHours = Math.floor(diffMinutes / 60);
const remainMinutes = diffMinutes % 60;
const hours = baseHours + (remainMinutes > 30 ? 1 : 0);
return `${hours}h`;
},
openMenu(e) {
e.preventDefault();
const template = [
{
label: "全部选择",
click: () => {
const cardIds = this.cardList.map(el => el.id);
const cardProIds = this.cardList.map(el => el.podOrderProductId);
this.podOrderProductIds = Array.from(
new Set([...this.podOrderProductIds, ...cardProIds])
);
this.selection = Array.from(
new Set([...this.selection, ...cardIds])
);
}
},
{
label: "取消选择",
click: () => {
const cardIdSet = new Set(this.cardList.map(el => el.id));
const cardProIdsSet = new Set(
this.cardList.map(el => el.podOrderProductId)
);
this.selection = this.selection.filter(id => !cardIdSet.has(id));
this.podOrderProductIds = this.selection.filter(
id => !cardProIdsSet.has(id)
);
}
}
];
const menu = Menu.buildFromTemplate(template);
menu.popup({ x: e.clientX, y: e.clientY });
},
parseImageAry(imageAry) {
if (typeof imageAry !== "string" || !imageAry.trim()) return [];
const trimmed = imageAry.trim();
if (trimmed.startsWith("http")) {
return trimmed
.split(",")
.map(url => ({ url: url.trim() }))
.filter(item => item.url);
}
try {
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed)) return [{ url: trimmed }];
const result = [];
for (const item of parsed) {
if (typeof item === "string" && item) {
result.push({ url: item });
} else if (item && typeof item === "object" && "url" in item) {
const url = item.url;
if (url) {
result.push({
url,
title: item.title
});
}
}
}
return result;
} catch {
return [{ url: trimmed }];
}
},
async openLogDialog(item) {
try {
const { data } = await this.$api.post(pathMap["getPodOrderLog"], {
id: item.id
});
this.logList = data;
this.logVisible = true;
} catch (error) {
console.log(error);
}
},
changeActive(item) {
const index = this.selection.indexOf(item.id);
const podIndex = this.podOrderProductIds.indexOf(item.podOrderProductId);
if (podIndex > -1) {
this.podOrderProductIds.splice(podIndex, 1);
} else {
this.podOrderProductIds.push(item.podOrderProductId);
}
if (index > -1) {
this.selection.splice(index, 1);
} else {
this.selection.push(item.id);
}
},
isSelected(item) {
return this.selection.includes(item.id);
},
handleViewDetail(item) {
try {
this.detailVisible = true;
this.curData = item;
} catch (e) {
console.error(e);
}
},
getPlatformImg(code) {
const lowerCode = code.toLowerCase();
const item = platformJson.find(el =>
el.type.toLowerCase().includes(lowerCode)
);
if (item) {
// console.log(item.icon);
return item.icon.split("/").pop();
}
return "";
}
}
};
</script>
<template>
<div style="height:100%" @contextmenu.prevent="openMenu">
<div class="page">
<DTFhead
:user="user"
:factoryType="factoryType"
@search="getCardList"
:selection="selection"
:podOrderProductIds="podOrderProductIds"
></DTFhead>
<div class="rowBox">
<div class="rowItem" v-for="(item, index) in detailRow" :key="index">
<div
style="display: flex;
flex-direction: column; align-items: center;"
>
<div style="min-height: 22px;">{{ item.label }}</div>
<div style="min-height: 22px;" v-if="item.label == '排版状态'">
<el-tag :type="layoutStatus[item.value]?.type">{{
layoutStatus[item.value]?.label
}}</el-tag>
</div>
<div v-else style="min-height: 22px;">
{{ item.value }}
</div>
</div>
</div>
</div>
<div class="card-content">
<div class="card-grid">
<div
class="card-grid-item"
v-for="(item, index) in cardList"
:key="index"
@click="changeActive(item)"
>
<BaseCard
style="cursor: pointer;"
:card-item="item"
:active="isSelected(item)"
:show-sku="false"
:show-product-info="false"
:image-field="'variantImage'"
:padding-top="'100%'"
>
<template #bottom_left>
<span
class="operation-number"
:title="`操作单号:${item.operationNo}`"
@click.stop="copyText(item.operationNo || '')"
>
{{ item.operationNo }}
</span>
</template>
<template #top_right>
<el-tooltip
v-if="item.layout"
effect="light"
content="排版完成"
placement="bottom"
>
<div
style="
background-color: #f0f9eb;
color: #6fc844;
border-radius: 4px;
padding:5px;
font-size: 20px;
"
>
</div>
</el-tooltip>
</template>
<template #images>
<div class="flex-between">
<div class="images-position">
<div class="images-container">
<div
v-for="(img, index) in parseImageAry(item.imageAry)"
:key="index"
:title="img.title"
class="item-image"
>
<el-image
:src="img.url"
height="50"
:preview-src-list="
parseImageAry(item.imageAry).map(i => i.url)
"
:initial-index="index"
/>
</div>
</div>
</div>
<div class="flex-row flex-row-gap15">
<el-tooltip
class="item"
effect="dark"
:content="'用户编码: ' + item?.userMark"
placement="bottom"
>
<span
style="
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
display: inline-block;
margin-right: 8px;
"
@click.stop="copyText(String(item?.userMark || ''))"
>
{{ item?.userMark || "AAAF" }}
</span>
</el-tooltip>
<img
v-if="item.platform"
:src="`/images/icon/${getPlatformImg(item.platform)}`"
style="width: 30px"
/>
</div>
</div>
</template>
<template #operations>
<div class="flex-row flex-row-gap6">
<div
class="quantity-badge multiple-quantity-badge-color"
:title="
`类型:${item.customizedQuantity === 1 ? '单面' : '双面'}`
"
>
{{ item.customizedQuantity === 1 ? "单" : "多" }}
</div>
<i
class="el-icon-tickets quantity-badge"
style="background-color: #41c792;"
@click.stop="openLogDialog(item)"
title="操作日志"
>
</i>
<i
style="background-color: #2183fe;"
class="el-icon-search quantity-badge"
@click.stop="handleViewDetail(item)"
title="查看详情"
>
</i>
</div>
</template>
<template #info>
<div class="card-info-grid">
<div class="card-info-row full">
<span
class="info-value ellipsis"
:title="item.productName || ''"
>
{{ item.productName }}
</span>
</div>
<div class="card-info-row">
<span
class="info-value clickable ellipsis"
:title="`变体SKU:${item.variantSku || ''}`"
@click.stop="copyText(item.variantSku || '')"
>
{{ item.variantSku }}
</span>
</div>
<div class="card-info-row">
<span class="info-label">工艺:</span>
<span class="info-value">{{ item.craftName }}</span>
</div>
<div class="card-info-row">
<span
class="info-value clickable ellipsis"
:title="`库存SKU:${item.thirdSkuCode || ''}`"
@click.stop="copyText(item.thirdSkuCode || '')"
>
{{ item.thirdSkuCode }}
</span>
</div>
<div class="card-info-row">
<span class="info-label">店铺单号:</span>
<span
class="info-value clickable ellipsis"
:title="`店铺单号:${item.shopNumber || ''}`"
@click.stop="copyText(item.shopNumber || '')"
>{{ item.shopNumber }}</span
>
</div>
<div class="card-info-row">
<span class="info-label">款号:</span>
<span
class="info-value clickable ellipsis"
:title="`款号:${item.supplierProductNo || ''}`"
@click.stop="copyText(item.supplierProductNo || '')"
>{{ item.supplierProductNo }}</span
>
</div>
<div class="card-info-row">
<span class="info-label">客户单号:</span>
<span
class="info-value clickable ellipsis"
:title="`客户单号:${item.thirdOrderNumber || ''}`"
@click.stop="copyText(item.thirdOrderNumber || '')"
>{{ item.thirdOrderNumber }}</span
>
</div>
<div class="card-info-row">
<span class="info-label">批次号:</span>
<span
class="info-value clickable ellipsis"
:title="`批次号:${item.batchArrangeNumber || ''}`"
@click.stop="copyText(item.batchArrangeNumber || '')"
>{{ item.batchArrangeNumber }}</span
>
</div>
<div class="card-info-row" style="color: red">
<span class="info-label">状态:</span>
<span class="info-value">{{ item.statusName }}</span>
</div>
<div class="card-info-row full delay-time-row">
<span
class="delay-time-value"
:title="
`订单延期:${getOrderDelayText(item.orderCreateTime)}`
"
>
{{ getOrderDelayText(item.orderCreateTime) }}
</span>
</div>
</div>
</template>
</BaseCard>
</div>
</div>
</div>
<div class="table-pagination">
<el-pagination
small
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageInfo.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageInfo.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageInfo.total"
/>
</div>
</div>
<el-dialog
:visible.sync="logVisible"
title="操作日志"
width="1000px"
:close-on-click-modal="false"
>
<LogList v-if="logList.length" :log-list="logList" />
<div v-else class="empty-content">暂无数据</div>
</el-dialog>
<DetailDialog :show.sync="detailVisible" :info="curData"></DetailDialog>
</div>
</template>
<style scoped lang="less">
.page {
display: flex;
flex-direction: column;
height: calc(100% - 25px);
}
.table-pagination {
margin: 10px 0;
text-align: center;
}
.rowBox {
display: grid;
grid-template-columns: repeat(8, 1fr);
justify-items: center;
align-items: center;
background-color: #f3f3f3;
padding: 10px 0;
.rowItem {
width: 100%;
border-right: 1px solid #c6c6c6;
:nth-last-child {
border: none;
}
}
}
.card-content {
height: 100%;
flex: 1;
overflow-y: auto;
}
.card-pagination {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
}
.card-selected-info {
font-size: 14px;
color: #606266;
.highlight {
color: red;
font-weight: bold;
}
}
.card-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
grid-template-rows: max-content;
gap: 10px;
padding: 5px;
}
.operation-number {
font-size: 12px;
cursor: pointer;
background: rgba(255, 255, 255, 0.75);
padding: 2px 6px;
border-radius: 4px;
max-width: 100%;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.quantity-badge {
width: 28px;
height: 28px;
color: #fff;
border-radius: 4px;
font-size: 16px;
line-height: 28px;
text-align: center;
margin-left: 5px;
}
.multiple-quantity-badge-color {
background-color: #e6a23c;
}
.product-mark-badge {
border: 2px solid #f56c6c;
color: #f56c6c;
font-weight: 700;
height: 28px;
width: 28px;
text-align: center;
border-radius: 6px;
font-size: 16px;
}
.card-info-grid {
display: grid;
grid-template-columns: auto auto;
gap: 4px;
font-size: 12px;
margin-top: 6px;
.full {
grid-column: span 2;
}
}
.card-info-row {
display: flex;
overflow: hidden;
}
.delay-time-row {
justify-content: flex-end;
margin-top: 2px;
}
.delay-time-value {
color: red;
font-weight: 600;
}
.info-label {
flex-shrink: 0;
color: #909399;
}
.info-value {
flex: 1;
min-width: 0;
&.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&.clickable {
cursor: pointer;
color: #409eff;
&:hover {
text-decoration: underline;
}
}
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
overflow: hidden;
gap: 6px;
.images-position {
flex: 1;
overflow: auto hidden;
.images-container {
width: 100%;
display: flex;
gap: 6px;
}
.item-image {
width: 50px;
min-width: 50px;
height: 50px;
border: 1px solid #eee;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
b {
margin-right: 5px;
font-size: 15px;
}
}
.flex-row {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.flex-row-gap15 {
gap: 15px;
}
.flex-row-gap6 {
gap: 6px;
}
@media (max-width: 1920px) {
.card-grid {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
}
@media (max-width: 1750px) {
.card-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
// .card-info-grid {
// grid-template-columns: 1fr 180px;
// }
}
@media (max-width: 1550px) {
.card-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
// .card-info-grid {
// grid-template-columns: 1fr 200px;
// }
}
</style>
<script>
const { getUser } = require("@/server/utils/store");
export default {
data() {
return {
searchForm: { batchArrangeNumber: "", layoutStatus: "", startTime: "" },
userData: getUser()
};
},
computed: {},
mounted() {},
created() {},
methods: {
dropdownCommand(v) {
switch (v) {
case "logout":
this.$confirm("是否退出登录?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$dataStore.delete("user");
this.$router.push("/");
})
.catch(() => {});
break;
case "cache":
this.checkList = [];
this.cacheVisible = true;
break;
}
},
getCountryName(code) {
if (code) {
const item = this.countryList?.find(el => el.countryCode == code);
if (item) {
return `(${item.nameCn})`;
}
}
return "";
},
goSetting() {
this.$router.push({
name: "dtf-setting"
});
},
goSetting1() {
this.$router.push({
name: "dtf-batch-detail"
});
},
getDataInfo() {
this.$emit("search", this.searchForm);
}
}
};
</script>
<template>
<div>
<div class="page-header">
<el-form
style="display: flex;flex-wrap: nowrap;"
inline
:model="searchForm"
>
<el-form-item label="批次号">
<el-input
v-model="searchForm.batchArrangeNumber"
placeholder="请输入批次号"
clearable
></el-input
></el-form-item>
<el-form-item label="排版状态">
<el-select
v-model="searchForm.layoutStatus"
placeholder="请选择排版状态"
clearable
>
<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
style="width: 380px;"
clearable
v-model="searchForm.timeRange"
type="datetimerange"
placeholder="选择时间"
format="yyyy-MM-dd HH:mm:ss"
value-format="yyyy-MM-dd HH:mm:ss"
>
</el-date-picker
></el-form-item>
<el-form-item>
<el-button @click="getDataInfo" type="primary"
>查询
</el-button></el-form-item
>
<el-form-item>
<el-button @click="goSetting" type="success"
>DTF排版设置
</el-button></el-form-item
>
</el-form>
<div class="right-user">
<div
v-if="userData.user && userData.user.factory"
style="font-weight: 700;margin-left: 8px;"
>
{{
userData.user.countryCode
? `${userData.user.countryCode}${getCountryName(
userData.user.countryCode
)}`
: "CN(中国)"
}}
</div>
<p v-if="userData.user && userData.user.factory">
{{ userData.user.factory.title }}
</p>
<el-dropdown @command="dropdownCommand">
<div style="margin-right: 20px">
<b style="cursor:pointer;">{{ userData.user.account }}</b>
<i class="el-icon-arrow-down"></i>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.right-user {
display: flex;
align-items: center;
b {
margin: 0 3px;
display: inline-block;
color: black;
font-size: 15px;
font-weight: bold;
}
p {
margin: 0 10px;
font-weight: bold;
display: inline-block;
font-size: 15px;
color: #e6a23c;
}
}
.page-header {
display: flex;
justify-content: space-between;
background-color: #ececec;
padding: 5px;
.el-form-item {
margin-bottom: 0;
}
}
</style>
<script>
import BaseTable from "../../components/BaseTable.vue";
const { remote, ipcRenderer } = require("electron");
const { getDTFSetting, setDTFSetting } = require("@/server/utils/store");
const { Menu } = remote;
const path = require("path");
const { pathMap } = require("@/config/index.js");
import DTFhead from "./components/head.vue";
import debounce from "lodash/debounce";
import {
submitDownload,
getDownloadStatus,
onDownloadStatusChange,
submitTypesettingDownload,
getTypesettingDownloadStatus,
onTypesettingStatusChange
} from "@/utils/download-api";
export default {
name: "design-dtf",
components: { BaseTable, DTFhead },
props: {
user: {
default: () => ({
avatar: "",
factory: {}
}),
type: Object
},
factoryType: { type: String, default: "CN" }
},
data() {
return {
dialogVisible: false,
selectionList: [],
searchForm: {},
radio: "",
templateBatchArrangeNum: "",
templateId: "",
isDownloading: false,
TypeDownloading: false,
dtfSetting: getDTFSetting(),
removeStatusListener: null, // 保存取消监听函数
pageInfo: {
currentPage: 1,
pageSize: 10,
total: 2
},
tableList: [],
tableConfig: [
{
prop: "batchArrangeNum",
label: "批次号",
width: 140,
slotName: "batchArrangeNum"
},
{
prop: "operationNum",
label: "操作单数量",
width: 100
},
{
prop: "materialNum",
label: "素材数量",
width: 100
},
{
prop: "craftType",
label: "工艺类型",
width: 100
},
{
prop: "standardDesignImage",
label: "规范素材",
width: 100,
render(row) {
return row.standardDesignImage == 0
? "否"
: row.standardDesignImage == 1
? "是"
: "混合";
}
},
{
label: "创建时间",
prop: "createTime"
},
{
label: "排版状态",
width: 140,
prop: "layoutStatus",
slotName: "layoutStatus"
},
{
prop: "failReason",
label: "失败原因",
width: 300,
slotName: "failReason"
},
{
label: "排版参数",
prop: "composingParam",
width: 300
},
{
label: "操作",
width: 260,
slotName: "operation",
prop: "operation"
}
],
layoutStatus: {
1: { label: "待排版", type: "warning" },
2: { label: "排版中", type: "info" },
3: { label: "排版成功", type: "success" },
4: { label: "排版失败", type: "danger" },
5: { label: "部分成功", type: "success" }
}
};
},
async mounted() {
this.loadLocalConfig();
this.fetchBatchList();
this.removeStatusListener = onDownloadStatusChange(status => {
this.handleDownloadStatus("material", status);
});
this.removeListener = onTypesettingStatusChange(status => {
this.handleDownloadStatus("typesetting", status);
});
// 初始化同步状态
await this.syncStatus();
},
beforeDestroy() {
if (this.removeStatusListener) this.removeStatusListener();
if (this.removeListener) this.removeListener();
// 取消待执行的防抖任务,避免销毁后执行报错
this.downloadFile.cancel?.();
},
methods: {
async loadLocalConfig() {
try {
if (!getDTFSetting().downloadDir) {
ipcRenderer.send("get-install-path");
ipcRenderer.once("get-install-path-res", (_, defaultPath) => {
setDTFSetting({ downloadDir: defaultPath });
});
}
} catch (error) {
console.log(error);
}
},
//定时任务
async fetchBatchList(value) {
try {
let params = {
currentPage: this.pageInfo.currentPage,
pageSize: this.pageInfo.pageSize
};
if (value) {
params = { ...params, ...value };
}
const res = await this.$api.post(pathMap["productionSoftware"], params);
if (res.code === 200) {
this.tableList = res.data.records || [];
this.pageInfo.total = res.data.total;
}
} catch (err) {
console.error("批次列表拉取失败", err);
}
},
handleSelect(rows) {
console.log("选中行", rows);
this.selectionList = rows;
},
handleSizeChange(size) {
this.pageInfo.pageSize = size;
this.pageInfo.currentPage = 1;
this.fetchBatchList(this.searchForm);
},
handleCurrentChange(page) {
this.pageInfo.currentPage = page;
this.fetchBatchList(this.searchForm);
},
//右键选择器
openMenu(e) {
e.preventDefault();
const template = [
{
label: "全部选择",
click: () => {
this.$refs.tableRefs?.toggleAllSelection(true);
}
},
{
label: "取消选择",
click: () => this.$refs.tableRefs?.toggleAllSelection(false)
}
];
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.batchArrangeNum,
isArray: false
});
if (!data) {
return this.$message.error("下载文件目录未配置或者目录错误");
}
if (type == "check") {
ipcRenderer.send("open-folder-only", data);
}
},
//打开手动排版弹窗
changeTypesetting(value) {
if (![2].includes(value.layoutStatus)) {
this.dialogVisible = true;
this.radio = "";
this.templateBatchArrangeNum = value.batchArrangeNum;
this.templateId = value.id;
}
},
//提交手动排版
async submit() {
if (this.TypeDownloading) {
this.$message.warning("其他排版下载中,请等待完成后再操作");
return;
}
if (!this.radio) {
return this.$message.error("请选择排版宽度");
}
try {
this.TypeDownloading = true;
if (this.dtfSetting.widthStrategy == 3 && this.radio == 60) {
this.$confirm(
"由于排版设置中选择“按更大规格排版”仅适用于40+2,选60时,默认超60的规格不排版",
"提示",
{
confirmButtonText: "确定",
type: "warning"
}
);
await this.$api.post(pathMap["psReComposingDesignImages"], {
batchArrangeNum: this.templateBatchArrangeNum,
templateWidth: this.radio
});
} else {
await this.$api.post(pathMap["psReComposingDesignImages"], {
batchArrangeNum: this.templateBatchArrangeNum,
templateWidth: this.radio
});
}
const { data } = await this.$api.post(
pathMap["podOrderBatchDownload"],
{
id: this.templateId
}
);
const res = await submitTypesettingDownload({
urlStr: data.data.spUrl,
targetPath: path.join(
getDTFSetting().downloadDir,
`批次号:${this.templateBatchArrangeNum}`,
"手动排版素材"
),
batchArrangeNum: this.templateBatchArrangeNum
});
console.log(res);
if (res.code == 200) {
this.$message(res.msg);
} else {
this.TypeDownloading = false;
this.$message.error(res.msg);
}
this.dialogVisible = false;
this.fetchBatchList(this.searchForm);
} catch (error) {
console.log(error);
this.TypeDownloading = false;
}
},
//跳转批次详情页
toDetail(value) {
this.$router.push({
name: "dtf-batch-detail",
params: {
...value
}
});
},
downloadFile: debounce(
function(value) {
this._downloadFile(value);
},
300,
{
leading: true,
trailing: false
}
),
//下载批次原素材
async _downloadFile(value) {
if (this.isDownloading) {
this.$message.warning("正在下载中,请等待完成后再操作");
return;
}
try {
this.isDownloading = true;
const { data } = await this.$api.post(pathMap["listMaterialsByBatch"], {
batchArrangeNum: value
});
// const newUrls = data.flatMap(el => el.urls);
const newUrls = data.flatMap(({ operationNo, urls }) =>
urls.map(url => `${url}-${operationNo}`)
);
console.log(newUrls);
if (newUrls.length === 0) {
this.$message.warning("该批次暂无素材可下载");
this.isDownloading = false;
return;
}
const res = await submitDownload({
urls: newUrls,
targetPath: path.join(
this.dtfSetting.downloadDir,
`批次号:${value}`,
"原素材"
),
batchArrangeNum: value
});
if (res.code == 200) {
this.$message(res.msg);
} else {
this.isDownloading = false;
this.$message.error(res.msg);
}
} catch (error) {
console.error("下载提交失败", error);
this.$message.error("下载提交失败,请重试");
// 仅提交失败时释放锁
this.isDownloading = false;
}
},
/**
* 状态变更回调
*/
handleDownloadStatus(type, status) {
const statusKey =
type === "material" ? "isDownloading" : "TypeDownloading";
this[statusKey] = status.isRunning;
if (!status.isRunning && status.result) {
const { success, failed, currentBatchNo } = status.result;
this.$message.success(
`批次号:${currentBatchNo},下载完成:成功 ${success.length} 个,失败 ${failed.length} 个`
);
if (failed.length > 0) {
console.warn("下载失败明细:", failed);
}
}
},
/**
* 同步当前状态
*/
async syncStatus() {
try {
const res = await getDownloadStatus();
const resTypesetting = await getTypesettingDownloadStatus();
this.isDownloading = res.data.isRunning;
this.TypeDownloading = resTypesetting.data.isRunning;
} catch (err) {
console.error("同步下载状态失败", err);
}
},
searchFn(value) {
this.searchForm = value;
if (value.timeRange?.length) {
this.searchForm.startTime = value.timeRange[0];
this.searchForm.endTime = value.timeRange[1];
} else {
this.searchForm.startTime = "";
this.searchForm.endTime = "";
}
this.fetchBatchList(this.searchForm);
}
}
};
</script>
<template>
<div class="page" @contextmenu.prevent="openMenu">
<DTFhead
:user="user"
:factoryType="factoryType"
@search="searchFn"
></DTFhead>
<BaseTable
v-if="tableList.length"
ref="tableRefs"
:table-data="tableList"
:table-config="tableConfig"
:has-selection="true"
:has-index="true"
:page-info="pageInfo"
@page-size-change="handleSizeChange"
@page-current-change="handleCurrentChange"
@selection="handleSelect"
border
stripe
height="100%"
size="mini"
>
<!-- 自定义插槽示例 -->
<template #batchArrangeNum="scope">
<a href="javascript:;" @click="toDetail(scope.row)">{{
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>
</div>
</template>
<template #operation="scope">
<div class="flex" style="justify-content: space-between;">
<el-button
type="text"
class="btn flex btnBox"
@click="downloadFile(scope.row.batchArrangeNum)"
>
<img src="../../assets/download.png" width="30" height="30" />
<div>下载素材</div></el-button
>
<el-button
type="text"
:disabled="scope.row.layoutStatus == 2"
class="btn flex btnBox"
@click="changeTypesetting(scope.row)"
><img
src="../../assets/typesetting.png"
width="30"
height="30"
:class="{ disabled: scope.row.layoutStatus == 2 }"
/>
<div>排版</div>
</el-button>
<el-button
type="text"
class="btn flex btnBox"
@click="goFliePage(scope.row, 'check')"
>
<img src="../../assets/file.png" width="30" height="30" />
<div>查看排版文件</div>
</el-button>
</div>
</template>
</BaseTable>
<el-empty
style="height: 100%; "
v-else
:image-size="500"
:image="require('@/assets/JOMALL.png')"
description="好像什么都没有"
></el-empty>
<el-dialog title="排版宽度" :visible.sync="dialogVisible" width="25%">
<el-radio-group v-model="radio">
<el-radio :label="40">40+2cm</el-radio>
<el-radio :label="60">60cm</el-radio>
</el-radio-group>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submit">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<style scoped lang="less">
.page {
height: "100%";
}
.flex {
display: flex;
}
.btnBox {
flex-direction: column;
align-items: center;
}
.btn {
padding: 0;
}
::v-deep .el-empty__image img {
filter: grayscale(100%);
opacity: 0.3;
}
::v-deep.el-empty__description p {
font-size: 50px;
}
.el-button {
div {
margin-top: 5px;
font-size: 12px;
}
}
::v-deep.disabled {
filter: grayscale(100%);
opacity: 0.3;
}
</style>
<template>
<div style="height:100%">
<div class="dtf-setting-page">
<!-- 返回按钮 -->
<div class="page-top">
<el-button icon="el-icon-arrow-left" @click="goBack">
返回
</el-button>
</div>
<el-card class="setting-card">
<template #header>
<div class="card-header">
<i
class="el-icon-setting"
style="color: #568cf9;font-size: 24px;margin-right: 8px;"
></i>
<span>DTF排版设置</span>
</div>
</template>
<div class="setting-group">
<div class="group-title">素材超排版宽度时:</div>
<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.materialStrategy">
<el-radio :label="1">批次素材数</el-radio>
<div class="custom-num-row">
<el-radio :label="2">自定义素材数(建议数量30):</el-radio>
<el-input
v-model.number="form.materialCount"
placeholder="请输入素材数"
style="width: 180px; margin-left: 8px"
:disabled="form.materialStrategy !== 2"
/>
</div>
</el-radio-group>
</div>
<div class="setting-group">
<div class="group-title">自动排版设置</div>
<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>
<div class="setting-group">
<div class="group-title">下载文件目录</div>
<div class="dir-input-row">
<el-input v-model="form.downloadDir" readonly style="flex: 1">
<template slot="prepend">素材默认下载位置:</template>
</el-input>
<el-button
icon="el-icon-folder-opened"
@click="selectFolder"
></el-button>
</div>
</div>
<!-- <div class="btn-wrap">
<el-button type="primary" @click="saveConfig">保存设置</el-button>
</div> -->
</el-card>
</div>
</div>
</template>
<script>
const { ipcRenderer } = require("electron");
const { setDTFSetting, getDTFSetting } = require("@/server/utils/store");
const { pathMap } = require("@/config/index.js");
import debounce from "lodash/debounce";
export default {
name: "dtf-setting",
data() {
return {
// 表单配置,可从本地配置文件读取初始化
form: {},
isInitLoad: true
};
},
mounted() {
// 页面加载读取本地配置回填表单
this.loadLocalConfig();
this.debounceSaveConfig = debounce(() => {
this.saveConfig();
}, 300);
},
watch: {
form: {
handler() {
console.log(999);
if (this.isInitLoad) return;
this.debounceSaveConfig();
},
deep: true
}
},
beforeDestroy() {
// 组件销毁清除防抖,防止内存泄漏
this.debounceSaveConfig.cancel();
},
methods: {
// 返回上一页
goBack() {
this.$router.push({ name: "design-dtf" });
},
// 读取本地配置
async loadLocalConfig() {
this.isFist = true;
try {
const { data } = await this.$api.post(pathMap["productionConfig"]);
this.form = { ...getDTFSetting(), ...data };
setDTFSetting({ ...this.form });
this.$nextTick(() => (this.isInitLoad = false));
} catch (error) {
console.log(error);
}
},
// 选择文件夹弹窗
selectFolder() {
ipcRenderer.removeAllListeners("select-folder-result");
ipcRenderer.send("request-select-folder", this.form.downloadDir);
ipcRenderer.once("select-folder-result", (_, path) => {
if (path) {
this.form = {
...this.form,
downloadDir: path
};
}
});
// console.log("selectFolder", this.form);
},
// 保存配置到本地文件
async saveConfig() {
console.log(999);
if (this.form.autoLayoutStrategy == 2 && this.form.widthStrategy == 3) {
await this.$confirm(
"由于排版设置中选择“按更大规格排版”仅适用于40+2,选60时,默认超60的规格不排版",
"提示",
{
confirmButtonText: "确定",
type: "warning"
}
);
}
await this.$api.post(pathMap["productionConfigUpdate"], this.form);
setDTFSetting({ ...this.form });
console.log(getDTFSetting());
console.log(this.form);
this.$message.success("排版设置保存成功!");
}
}
};
</script>
<style lang="less" scoped>
.dtf-setting-page {
padding: 16px;
width: 100%;
height: 100%;
box-sizing: border-box;
}
.page-top {
margin-bottom: 12px;
}
.setting-card {
width: 100%;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 20px;
font-weight: 700;
}
.setting-group {
margin-bottom: 24px;
}
.group-title {
font-size: 20px;
color: #303133;
margin-bottom: 10px;
}
.el-radio-group {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 16px;
border: 1px solid #e4e7ed;
border-radius: 4px;
}
.custom-num-row {
display: flex;
align-items: center;
}
.dir-input-row {
display: flex;
align-items: center;
gap: 8px;
}
.btn-wrap {
margin-top: 30px;
text-align: left;
}
.setting-card {
::v-deep {
.el-card__header {
border-bottom: none;
}
.el-card__body {
padding: 10px 200px;
}
}
}
.el-radio-group {
.el-radio {
padding: 10px 0;
font-size: 18px;
}
}
</style>
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