Files
SoonWorker/src/renderer/components/files/file.vue
T
24kycj 04ca328a67 修复提交逻辑:完善所有拷贝类型支持,修复电子光盘和禁拷U盘提交卡死问题
- 修复 Files 组件中 file_form=1(电子光盘) 和 file_form=4(禁拷U盘) 的空处理分支
- 添加完成回调触发逻辑,避免提交流程卡死
- 完善 upload()、upload_over()、performSubmit() 的状态判断
- 严格对齐 work-副本.vue 的验证逻辑和参数构建顺序
- 优化标签上传判断,避免无标签时的不必要API调用
- 添加 ISO/ZIP 验证(仅在有内容文件时)
- 清理所有调试日志
2026-01-05 04:05:37 +08:00

516 lines
15 KiB
Vue

<template>
<div class="files">
<div class="btn-group" v-show="false">
<div class="btn" @click="addFolder()" style="color: #67c23a">
<i class="el-icon-folder" />
{{ $t("file.addFolder") }}
</div>
<div class="btn" @click="addFile()" style="color: #67c23a">
<i class="el-icon-files" />
{{ $t("file.addFile") }}
</div>
</div>
<fileEmpty v-if="allNumber == 0" />
<fileList v-else :list="filesList" :del="delFile" :key="key" />
<div v-if="progressVisible">
<el-dialog :close-on-click-modal="false" width="500px" :visible.sync="progressVisible" :append-to-body="true"
class="prodialog">
<progressdialog :list="filesList" :overNumber="overNumber" :allNumber="allNumber"
:changevisiable="changeProgressvisible" :changestate="changestate" :isSucess="isSucess" ref="progressdialog">
</progressdialog>
</el-dialog>
</div>
<div v-if="archiverVisible">
<el-dialog :close-on-click-modal="false" width="500px" :visible.sync="archiverVisible" :append-to-body="true"
class="archiverDialog" :show-close="false">
<archiverdialog :isOver="archiverIsover" :isFalse="archiverIsfalse" :changevisiable="changeArchivervisible">
</archiverdialog>
</el-dialog>
</div>
<slot></slot>
</div>
</template>
<script>
const { dialog } = require("@electron/remote");
const fs = require("fs");
const path = require("path");
import fileEmpty from "./fileEmpty";
import fileList from "./fileList";
import progressdialog from "./progressdialog";
import archiverdialog from "./archiverdialog";
import { calcSize, getFileName, isFolder } from "./calc";
import { copy } from "./copy";
import { zip } from "./archiver";
// 网络路径检测工具函数
const isNetworkPath = (path) => {
// 检测是否为网络路径
// Windows 网络路径: \\hostname\path 或 \\IP\path
// Unix 网络路径: //hostname/path 或 //IP/path
return path.startsWith('\\\\') || path.startsWith('//') ||
(path.includes(':') && !path.includes('\\') && !path.includes('/'));
};
const extractHostName = (path) => {
if (path.startsWith('\\\\')) {
// Windows 网络路径 \\hostname\path
const parts = path.substring(2).split('\\')
return parts[0]
} else if (path.startsWith('//')) {
// Unix 网络路径 //hostname/path
const parts = path.substring(2).split('/')
return parts[0]
} else if (path.includes(':')) {
// 可能包含 IP 地址
const match = path.match(/^([^\\\/:]+)/)
return match ? match[1] : 'unknown'
}
return 'unknown'
};
export default {
name: "Files",
props: {
onSizechange: {
type: Function,
},
complete: {
type: Function,
},
saveWorkList: {
type: Object | undefined,
},
},
components: {
fileEmpty,
fileList,
progressdialog,
archiverdialog,
},
data() {
return {
filesList: {},
key: 0,
allSize: 0,
progressVisible: false,
archiverVisible: false,
overNumber: 0,
allNumber: 0,
nowstate: true,
archiverIsover: false,
archiverIsfalse: false,
zip_path: "",
isCopy: false,
isSucess: true,
copyPath: "",
networkPaths: [], // 存储检测到的网络路径
};
},
mounted() {
// this.$set(this, filesList, this.saveWorkList)
const _this = this;
this._handleDrop = (e) => {
e.preventDefault();
// e.stopPropagation();
for (const f of e.dataTransfer.files) {
const isFolder = _this.dropFolderCheck(f);
_this.insertList({
name: getFileName(f.path),
path: f.path,
size: isFolder ? -1 : f.size,
folder: isFolder,
});
if (isFolder) {
calcSize(f.path, _this.folderCalcCallback);
}
}
};
this._handleDragOver = (e) => {
e.preventDefault();
// e.stopPropagation();
};
document.addEventListener("drop", this._handleDrop);
document.addEventListener("dragover", this._handleDragOver);
},
beforeDestroy() {
if (this._handleDrop) {
document.removeEventListener("drop", this._handleDrop);
this._handleDrop = null;
}
if (this._handleDragOver) {
document.removeEventListener("dragover", this._handleDragOver);
this._handleDragOver = null;
}
},
methods: {
reset() {
this.filesList = {};
this.key++;
this.allSize = 0;
this.progressVisible = false;
this.archiverVisible = false;
this.overNumber = 0;
this.allNumber = 0;
this.nowstate = true;
this.archiverIsover = false;
this.archiverIsfalse = false;
this.zip_path = "";
this.isCopy = false;
this.isSucess = true;
this.copyPath = "";
this.networkPaths = [];
this.$emit('network-paths-changed', []);
},
insertList(f) {
if (!this.filesList[f.path]) {
this.allNumber++;
this.$set(this.filesList, f.path, f);
if (f.size != -1) {
// 是文件夹
this.sizeChange(f.size);
}
// 检测是否为网络路径
if (isNetworkPath(f.path)) {
this.addNetworkPath(f.path);
}
return true;
} else {
//有了,不用加入
return false;
}
},
addFile() {
const _this = this;
dialog
.showOpenDialog({
properties: ["multiSelections"],
})
.then(async (res) => {
console.log(res)
for (const item of res.filePaths) {
await fs.stat(item, function (err, res) {
if (err) {
return false;
}
_this.insertList({
name: getFileName(item),
path: item,
size: res.size,
folder: false,
});
});
}
});
},
addFolder() {
const _this = this;
dialog
.showOpenDialog({
properties: ["openDirectory", "multiSelections"],
})
.then((res) => {
console.log(res)
for (const item of res.filePaths) {
const result = _this.insertList({
name: getFileName(item),
path: item,
size: -1,
folder: true,
});
if (result) {
calcSize(item, _this.folderCalcCallback);
}
}
});
},
folderCalcCallback(err, res, path) {
if (this.filesList[path]) {
this.filesList[path].size = res;
this.$set(this.filesList, path, this.filesList[path]);
this.sizeChange(this.filesList[path].size);
}
},
delFile(path) {
this.sizeChange(-this.filesList[path].size);
delete this.filesList[path];
this.key++;
this.allNumber--;
// 若为网络路径,则同步按主机清理认证项
if (isNetworkPath(path)) {
const hostName = extractHostName(path)
this.networkPaths = this.networkPaths.filter(p => p.hostName !== hostName)
}
this.$emit('network-paths-changed', this.getNetworkPaths());
},
dropFolderCheck(f) {
//T是文件夹 F不是文件夹
//拖放无法从参数判断是否为文件夹,需要额外处理
if (f.size != 0 && f.size != 4096) {
//返回大小不是0,则不是文件夹
return false;
}
if (f.type != "") {
//如果type不是空,则不是文件夹
return false;
}
return isFolder(f.path);
},
sizeChange(size) {
this.allSize = this.allSize + size;
if (this.allSize <= 0) {
this.allSize = 0;
}
this.onSizechange(this.allSize);
},
fileBack(fileList, Sucess) {
if (Sucess) {
if (this.nowstate) {
this.isSucess = true;
fileList.state = true;
this.overNumber++;
if (this.overNumber == this.allNumber) {
setTimeout(() => {
this.changeProgressvisible(false);
this.complete();
}, 500);
}
} else {
console.log("终止!!!!");
}
} else {
this.isSucess = false;
if (this.nowstate) {
fileList.state = false;
} else {
console.log("终止!!!!");
}
}
},
archiverBack(state) {
if (state) {
this.archiverIsover = true;
this.complete();
} else {
this.archiverIsfalse = true;
}
setTimeout(() => {
this.changeArchivervisible(false);
}, 500);
},
async resume(file_form) {
// 统一走优化流程
const entries = Object.values(this.filesList || {});
if (entries.length === 0) {
return;
}
if (this.isCopy) {
// 拷贝到共享目录:确保目标目录存在,顺序执行避免过多并发导致卡顿
try {
if (!this.copyPath || typeof this.copyPath !== 'string') {
console.warn('Invalid copyPath');
// 回退为直接完成,避免卡住
entries.forEach((f) => this.fileBack(f, true));
return;
}
// 确保目录以分隔符结尾拼接正确
const targetDir = this.copyPath;
try {
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
} catch (e) {
console.error('ensure target dir failed', e);
}
this.overNumber = 0;
this.changeProgressvisible(true);
for (const file of entries) {
const dest = path.join(targetDir, file.name);
await new Promise((resolve) => {
copy(
file.path,
dest,
file.folder,
(fl, ok) => {
this.fileBack(fl, ok);
resolve();
},
file
);
});
}
} catch (err) {
console.error('copy flow error', err);
// 出错时也推进为完成,避免停滞
entries.forEach((f) => this.fileBack(f, false));
}
} else if (file_form == 1) {
// 电子光盘:不需要拷贝,直接标记为完成
this.overNumber = 0;
for (const file of entries) {
this.fileBack(file, true);
}
} else if (file_form == 2) {
//zip
this.archiverIsover = false;
this.archiverIsfalse = false;
this.zip_path = "D:/archivertest/1.zip";
zip(this.filesList, this.zip_path, this.archiverBack, false);
} else if (file_form == 3) {
//加密zip
this.archiverIsover = false;
this.archiverIsfalse = false;
this.zip_path = "D:/archivertest/2.zip";
let password = "123456";
zip(this.filesList, this.zip_path, this.archiverBack, true, password);
} else if (file_form == 4) {
// 禁拷贝U盘:不需要拷贝,直接标记为完成
this.overNumber = 0;
for (const file of entries) {
this.fileBack(file, true);
}
} else {
// 默认路径上传:不实际拷贝,仅回调推进流程
this.overNumber = 0;
for (const file of entries) {
this.fileBack(file, true);
}
}
},
changeProgressvisible(visiable) {
this.progressVisible = visiable;
},
changeArchivervisible(visiable) {
this.archiverVisible = visiable;
},
changestate(state) {
this.nowstate = state;
},
getLists() {
return this.filesList;
},
// 添加网络路径到列表(按主机去重,仅认证根目录,如 \\\\NAS)
addNetworkPath(path) {
const hostName = extractHostName(path);
const existingIndex = this.networkPaths.findIndex(p => p.hostName === hostName);
if (existingIndex === -1) {
// 尝试从本地存储获取已保存的认证信息
let userName = '';
let password = '';
try {
const stored = localStorage.getItem('networkCredentials');
if (stored) {
const credentials = JSON.parse(stored);
if (credentials[hostName]) {
userName = credentials[hostName].userName;
password = credentials[hostName].password;
}
}
} catch (error) {
console.log('无法获取已保存的认证信息');
}
// 仅保存根路径用于展示与认证
const rootPath = `\\\\${hostName}`;
this.networkPaths.push({
path: rootPath,
hostName: hostName,
userName: userName,
password: password
});
this.$emit('network-paths-changed', this.getNetworkPaths());
}
},
// 获取网络路径列表(按主机去重,返回根路径)
getNetworkPaths() {
const uniqueByHost = {};
for (const item of this.networkPaths) {
if (!uniqueByHost[item.hostName]) {
uniqueByHost[item.hostName] = {
path: `\\\\${item.hostName}`,
hostName: item.hostName,
userName: item.userName || '',
password: item.password || ''
};
}
}
return Object.values(uniqueByHost);
},
// 检查是否有网络路径
hasNetworkPaths() {
return this.networkPaths.length > 0;
},
haveFolderIsCalc() { },
},
computed: {},
watch: {},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="less" scoped>
.prodialog {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.archiverDialog {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
/deep/.el-dialog__header {
padding: 10px;
}
}
.files {
width: 100%;
background-color: transparent;
margin: 0;
font-size: 12px;
box-shadow: none;
position: relative;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.btn-group {
height: 30px;
background-color: #e7e7ec;
.btn {
color: #53809f;
display: inline-block;
height: 30px;
line-height: 30px;
font-size: 14px;
padding-left: 10px;
padding-right: 10px;
cursor: pointer;
&:hover {
background-color: #dfdfe4;
}
&:active {
background-color: #d1d1d7;
}
}
}
</style>