|
|
|
|
<template>
|
|
|
|
|
<div class="app-container">
|
|
|
|
|
<!-- 传输管理器卡片 -->
|
|
|
|
|
<el-card>
|
|
|
|
|
<!-- 头部:标题 + 清空按钮 -->
|
|
|
|
|
<div slot="header">
|
|
|
|
|
<span>传输管理器</span>
|
|
|
|
|
<el-button
|
|
|
|
|
style="float: right; padding: 3px 0"
|
|
|
|
|
type="text"
|
|
|
|
|
icon="el-icon-delete"
|
|
|
|
|
:disabled="groupedRecords.length === 0"
|
|
|
|
|
@click="handleClearAll"
|
|
|
|
|
>
|
|
|
|
|
清空记录
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-loading="loading" class="container">
|
|
|
|
|
<template v-if="groupedRecords.length > 0">
|
|
|
|
|
<div v-for="(group, index) in groupedRecords" :key="index">
|
|
|
|
|
<!-- 日期分割线 -->
|
|
|
|
|
<el-divider>
|
|
|
|
|
<el-tag type="info">
|
|
|
|
|
{{ group.date }} ({{ group.items.length }})
|
|
|
|
|
</el-tag>
|
|
|
|
|
</el-divider>
|
|
|
|
|
<!-- 传输记录表格 -->
|
|
|
|
|
<el-table :data="group.items" :show-header="false">
|
|
|
|
|
<el-table-column label="缩略图标" width="80" align="center">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
<div class="file-thumb">
|
|
|
|
|
<i :class="getFileIconClass(scope.row.file_name)" />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column
|
|
|
|
|
prop="file_name"
|
|
|
|
|
label="文件名"
|
|
|
|
|
show-overflow-tooltip
|
|
|
|
|
/>
|
|
|
|
|
<el-table-column
|
|
|
|
|
label="文件大小"
|
|
|
|
|
width="100"
|
|
|
|
|
align="center"
|
|
|
|
|
show-overflow-tooltip
|
|
|
|
|
>
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ formatFileSize(scope.row.file_size) }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="进度" width="150" align="center">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
<div class="progress-container">
|
|
|
|
|
<el-progress
|
|
|
|
|
:percentage="
|
|
|
|
|
scope.row.is_success
|
|
|
|
|
? 100
|
|
|
|
|
: scope.row.upload_percent || 0
|
|
|
|
|
"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column
|
|
|
|
|
label="上传时间"
|
|
|
|
|
width="80"
|
|
|
|
|
align="center"
|
|
|
|
|
show-overflow-tooltip
|
|
|
|
|
>
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ formatTime(scope.row.time) }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="状态" width="100" align="center">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
<el-tag
|
|
|
|
|
:type="getStatusType(scope.row.transfer_type, scope.row)"
|
|
|
|
|
size="mini"
|
|
|
|
|
>
|
|
|
|
|
{{ getStatusText(scope.row.transfer_type, scope.row) }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<!-- 操作按钮 -->
|
|
|
|
|
<el-table-column label="操作" width="220" align="center">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
<el-button
|
|
|
|
|
v-if="scope.row.transfer_type === 3 && scope.row.file_url"
|
|
|
|
|
type="text"
|
|
|
|
|
icon="el-icon-view"
|
|
|
|
|
@click="handleDownload(scope.row)"
|
|
|
|
|
>
|
|
|
|
|
打开
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button
|
|
|
|
|
v-if="scope.row.transfer_type === 3 && scope.row.file_url"
|
|
|
|
|
type="text"
|
|
|
|
|
icon="el-icon-folder-opened"
|
|
|
|
|
@click="handleOpenLocation(scope.row)"
|
|
|
|
|
>
|
|
|
|
|
打开位置
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button
|
|
|
|
|
type="text"
|
|
|
|
|
icon="el-icon-delete"
|
|
|
|
|
style="color: #f56c6c"
|
|
|
|
|
@click="handleDeleteItem(scope.row)"
|
|
|
|
|
>
|
|
|
|
|
删除
|
|
|
|
|
</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-empty v-else description="暂无文件传输记录" />
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<script>
|
|
|
|
|
const STORAGE_KEY = "file_transfer_records";
|
|
|
|
|
export default {
|
|
|
|
|
name: "UtilityPage",
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
loading: false,
|
|
|
|
|
transferRecords: [],
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
// 按日期分组,与 Qt DisplayFiletransferinfo 逻辑一致
|
|
|
|
|
groupedRecords() {
|
|
|
|
|
const groups = {};
|
|
|
|
|
for (const item of this.transferRecords) {
|
|
|
|
|
const date = this.formatDate(item.time);
|
|
|
|
|
if (!groups[date]) {
|
|
|
|
|
groups[date] = { date, items: [] };
|
|
|
|
|
}
|
|
|
|
|
groups[date].items.push(item);
|
|
|
|
|
}
|
|
|
|
|
// 按日期降序,同一天内按时间降序
|
|
|
|
|
return Object.values(groups).sort((a, b) => {
|
|
|
|
|
return new Date(b.date) - new Date(a.date);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
this.loadTransferRecords();
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
// 从 localStorage 加载文件传输记录(替代 Qt 的 SQLite 查询)
|
|
|
|
|
loadTransferRecords() {
|
|
|
|
|
this.loading = true;
|
|
|
|
|
try {
|
|
|
|
|
const data = localStorage.getItem(STORAGE_KEY);
|
|
|
|
|
if (data) {
|
|
|
|
|
this.transferRecords = JSON.parse(data);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("加载文件传输记录失败", e);
|
|
|
|
|
this.transferRecords = [];
|
|
|
|
|
} finally {
|
|
|
|
|
this.loading = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 保存到 localStorage
|
|
|
|
|
saveTransferRecords() {
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.transferRecords));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("保存文件传输记录失败", e);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 格式化日期,与 Qt "yyyy-MM-dd" 一致
|
|
|
|
|
formatDate(timeStr) {
|
|
|
|
|
if (!timeStr) return "";
|
|
|
|
|
const date = new Date(timeStr);
|
|
|
|
|
const y = date.getFullYear();
|
|
|
|
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
const d = String(date.getDate()).padStart(2, "0");
|
|
|
|
|
return `${y}-${m}-${d}`;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 格式化时间,与 Qt "hh:mm" 一致
|
|
|
|
|
formatTime(timeStr) {
|
|
|
|
|
if (!timeStr) return "";
|
|
|
|
|
const date = new Date(timeStr);
|
|
|
|
|
const h = String(date.getHours()).padStart(2, "0");
|
|
|
|
|
const m = String(date.getMinutes()).padStart(2, "0");
|
|
|
|
|
return `${h}:${m}`;
|
|
|
|
|
},
|
|
|
|
|
// 格式化文件大小,与 Qt getSendFileSize 一致
|
|
|
|
|
formatFileSize(size) {
|
|
|
|
|
if (!size || size < 0) return "0.00B";
|
|
|
|
|
if (size >= 1024 * 1024) {
|
|
|
|
|
return (size / (1024.0 * 1024.0)).toFixed(2) + "M";
|
|
|
|
|
} else if (size >= 1024) {
|
|
|
|
|
return (size / 1024.0).toFixed(2) + "K";
|
|
|
|
|
}
|
|
|
|
|
return size.toFixed(2) + "B";
|
|
|
|
|
},
|
|
|
|
|
// 获取文件图标类名,与 Qt getFileIconPath 逻辑一致
|
|
|
|
|
getFileIconClass(fileName) {
|
|
|
|
|
if (!fileName) return "el-icon-document";
|
|
|
|
|
const suffix = fileName.split(".").pop().toLowerCase();
|
|
|
|
|
const iconMap = {
|
|
|
|
|
txt: "el-icon-tickets",
|
|
|
|
|
pdf: "el-icon-document-copy",
|
|
|
|
|
mp3: "el-icon-headset",
|
|
|
|
|
wav: "el-icon-headset",
|
|
|
|
|
avi: "el-icon-video-camera",
|
|
|
|
|
mp4: "el-icon-video-camera",
|
|
|
|
|
wmv: "el-icon-video-camera",
|
|
|
|
|
mov: "el-icon-video-camera",
|
|
|
|
|
zip: "el-icon-folder-opened",
|
|
|
|
|
rar: "el-icon-folder-opened",
|
|
|
|
|
ppt: "el-icon-data-board",
|
|
|
|
|
pps: "el-icon-data-board",
|
|
|
|
|
xls: "el-icon-s-grid",
|
|
|
|
|
xlsx: "el-icon-s-grid",
|
|
|
|
|
doc: "el-icon-document",
|
|
|
|
|
docx: "el-icon-document",
|
|
|
|
|
jpg: "el-icon-picture",
|
|
|
|
|
jpeg: "el-icon-picture",
|
|
|
|
|
jpe: "el-icon-picture",
|
|
|
|
|
bmp: "el-icon-picture",
|
|
|
|
|
gif: "el-icon-picture",
|
|
|
|
|
png: "el-icon-picture",
|
|
|
|
|
tif: "el-icon-picture",
|
|
|
|
|
tiff: "el-icon-picture",
|
|
|
|
|
};
|
|
|
|
|
return iconMap[suffix] || "el-icon-document";
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 获取状态文字
|
|
|
|
|
getStatusText(type, row) {
|
|
|
|
|
const isSuccess = row.is_success;
|
|
|
|
|
const isUploading = row.is_uploading;
|
|
|
|
|
if (type === 3) {
|
|
|
|
|
// 知识库上传类型
|
|
|
|
|
if (isUploading && !isSuccess) return "上传中";
|
|
|
|
|
if (!isSuccess) return "上传失败";
|
|
|
|
|
return "上传成功";
|
|
|
|
|
}
|
|
|
|
|
// 其他类型
|
|
|
|
|
if (!isSuccess) return "失败";
|
|
|
|
|
return "成功";
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 获取状态标签类型(Element UI 样式)
|
|
|
|
|
getStatusType(type, row) {
|
|
|
|
|
const isSuccess = row.is_success;
|
|
|
|
|
const isUploading = row.is_uploading;
|
|
|
|
|
if (isUploading && !isSuccess) return "warning";
|
|
|
|
|
if (!isSuccess) return "danger";
|
|
|
|
|
return "success";
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 下载文件
|
|
|
|
|
handleDownload(item) {
|
|
|
|
|
if (item.file_url) {
|
|
|
|
|
window.open(item.file_url, "_blank");
|
|
|
|
|
} else {
|
|
|
|
|
this.$message.warning("文件链接不可用");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 打开文件位置
|
|
|
|
|
handleOpenLocation(item) {
|
|
|
|
|
if (item.file_url) {
|
|
|
|
|
this.$modal.msg("浏览器安全限制:请下载文件后在下载列表中打开文件夹");
|
|
|
|
|
// 先触发下载
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.href = item.file_url;
|
|
|
|
|
a.download = item.file_name;
|
|
|
|
|
a.target = "_blank";
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
} else {
|
|
|
|
|
this.$message.warning("文件路径不可用");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 删除单条记录
|
|
|
|
|
handleDeleteItem(item) {
|
|
|
|
|
this.$confirm("确认删除该传输记录?", "提示", { type: "warning" })
|
|
|
|
|
.then(() => {
|
|
|
|
|
this.transferRecords = this.transferRecords.filter(
|
|
|
|
|
(r) => r.id !== item.id
|
|
|
|
|
);
|
|
|
|
|
this.saveTransferRecords();
|
|
|
|
|
this.$message.success("删除成功");
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
},
|
|
|
|
|
// 清空所有记录,与 Qt on_clearButton_clicked 逻辑一致
|
|
|
|
|
handleClearAll() {
|
|
|
|
|
this.$modal
|
|
|
|
|
.confirm("是否要清除本地传输记录?")
|
|
|
|
|
.then(() => {
|
|
|
|
|
this.transferRecords = [];
|
|
|
|
|
this.saveTransferRecords();
|
|
|
|
|
this.$message.success("记录已清空");
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
},
|
|
|
|
|
// 添加一条新的传输记录(供外部调用,与 Qt UpdateToolsWidgetSlot 对应)
|
|
|
|
|
addTransferRecord(record) {
|
|
|
|
|
const newRecord = {
|
|
|
|
|
id: record.id || Date.now(),
|
|
|
|
|
user_id: record.user_id || 0,
|
|
|
|
|
file_name: record.file_name || "",
|
|
|
|
|
local_path: record.local_path || "",
|
|
|
|
|
file_size: record.file_size || 0,
|
|
|
|
|
transfer_type: record.transfer_type || 9,
|
|
|
|
|
is_success: record.is_success || false,
|
|
|
|
|
time: record.time || new Date().toISOString(),
|
|
|
|
|
file_url: record.file_url || "",
|
|
|
|
|
};
|
|
|
|
|
// 插入到列表顶部(与 Qt InsertToolsItemWidget 对应)
|
|
|
|
|
this.transferRecords.unshift(newRecord);
|
|
|
|
|
this.saveTransferRecords();
|
|
|
|
|
},
|
|
|
|
|
// 更新传输状态(供外部调用,与 Qt UpdateFinishedToolsWidgetSlot 对应)
|
|
|
|
|
updateTransferStatus(id, isSuccess) {
|
|
|
|
|
const record = this.transferRecords.find((r) => r.id === id);
|
|
|
|
|
if (record) {
|
|
|
|
|
record.is_success = isSuccess;
|
|
|
|
|
record.is_uploading = false;
|
|
|
|
|
this.saveTransferRecords();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 更新上传进度(供外部调用)
|
|
|
|
|
updateUploadProgress(id, percent) {
|
|
|
|
|
const record = this.transferRecords.find((r) => r.id === id);
|
|
|
|
|
if (record) {
|
|
|
|
|
record.upload_percent = percent;
|
|
|
|
|
record.is_uploading = percent < 100;
|
|
|
|
|
this.saveTransferRecords();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.container {
|
|
|
|
|
height: calc(100vh - 230px);
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
.file-thumb {
|
|
|
|
|
width: 42px;
|
|
|
|
|
height: 42px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background: #655dd4;
|
|
|
|
|
color: #fff;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|