p2p-explorer-web/src/pages/file/index.vue
2025-01-03 10:53:05 +08:00

411 lines
8.5 KiB
Vue

<template>
<div class="container">
<!-- 连接状态 -->
<div class="status-bar">
<div class="peer-info">
我的ID: <span class="id-text">{{ myId || "等待连接..." }}</span>
<Button type="primary" @click="copyId" v-if="myId">复制</Button>
<Button type="link" @click="shareUrl" v-if="myId">分享</Button>
</div>
<!-- 显示流量 丢包率-->
<div class="status-info">
<div class="status-item">
<span>流量:</span>
<span>{{ formatSize(transInfo.bytes) }}/s</span>
</div>
<div class="status-item">
<span>包数:</span>
<span>{{ transInfo.packets }}/s</span>
</div>
</div>
<div class="connect-section">
<input
v-model="targetId"
:placeholder="!myId ? '请稍后...' : '输入对方ID'"
:disabled="isConnected || !myId"
/>
<Button @click="handleConnect" :disabled="!targetId || isConnected">
{{ isConnected ? "已连接" : "连接" }}
</Button>
</div>
</div>
<!-- 文件传输区域 -->
<div class="file-transfer" :class="{ 'mobile-layout': isPhone }">
<!-- 本地文件区域 -->
<FileView
v-if="!isPhone"
:isRemote="false"
:selectedFiles="selectedLocalFiles"
/>
<!-- 传输控制 -->
<div
v-if="!isPhone"
class="transfer-controls"
:class="{ 'mobile-controls': isPhone }"
>
<Clipboard v-if="isConnected" />
<Button
type="primary"
:disabled="!isConnected || selectedLocalFiles.length === 0"
@click="handleSend"
>
发送
</Button>
<Button
type="primary"
:disabled="!isConnected || selectedRemoteFiles.length === 0"
@click="handleReceive"
:loading="receiveLoading"
>
接收
</Button>
</div>
<!-- 远程文件区域 -->
<FileView :isRemote="true" :selectedFiles="selectedRemoteFiles" />
</div>
</div>
<FileTransferView />
</template>
<script setup lang="ts">
import Clipboard from "./item/clipboard.vue";
import { ref, onMounted } from "vue";
import { peer } from "./utils/peer";
import FileView from "./item/fileView.vue";
import { notification } from "ant-design-vue";
import { fileMgrInstance, type FileData } from "./utils/fileMgr";
import { Button } from "ant-design-vue";
import {
formatSize,
getUrlParam,
localCurrentFile,
remoteCurrentFile,
} from "./utils/common";
import FileTransferView from "./item/fileTranserView.vue";
const isPhone = ref(false);
const receiveLoading = ref(false);
const myId = ref("");
const targetId = ref("");
const isConnected = ref(false);
// 文件系统相关
const selectedLocalFiles = ref<string[]>([]);
const selectedRemoteFiles = ref<string[]>([]);
//根据文档宽度
onMounted(() => {
isPhone.value = document.body.clientWidth < 768;
});
const shareUrl = async () => {
if (myId.value) {
const url =
window.location.origin + window.location.pathname + "?sign=" + myId.value;
await copyToClipboard(url);
notification.success({
message: "链接已复制",
description: "已成功复制到剪贴板",
});
}
};
// 复制ID
const copyId = async () => {
if (myId.value) {
try {
await copyToClipboard(myId.value);
notification.success({
message: "ID已复制",
description: "已成功复制到剪贴板",
});
} catch (error) {
notification.error({
message: "复制失败",
icon: "error",
});
}
}
};
//复制到剪贴板
const copyToClipboard = async (text: string) => {
await navigator.clipboard.writeText(text);
};
// 连接处理
const handleConnect = () => {
if (!targetId.value) return;
peer.connect(targetId.value);
};
let lastBytes = 0;
let lastPackets = 0;
const transInfo = ref({
bytes: 0,
packets: 0,
});
const checkBytes = () => {
transInfo.value.bytes = peer.transbytesNum - lastBytes;
transInfo.value.packets = peer.transpackNum - lastPackets;
lastBytes = peer.transbytesNum;
lastPackets = peer.transpackNum;
setTimeout(() => {
checkBytes();
}, 1000);
};
const handleSend = async () => {
notification.info({
message: "未完成",
description: "开发中...",
});
};
const handleReceive = async () => {
receiveLoading.value = true;
const rootFile = await fileMgrInstance.getRootFile();
console.log(localCurrentFile.value.path);
try {
let getFileHandles: Promise<FileData | FileData[]>[] = [];
selectedRemoteFiles.value.forEach((filepath) => {
getFileHandles.push(
fileMgrInstance.remoteRootFile
.getFileInfo(filepath)
.getFile(
false,
localCurrentFile.value.path.replace(rootFile.path, ""),
remoteCurrentFile.value.path
)
);
});
await Promise.all(getFileHandles);
} catch (err) {
notification.error({
message: "接收文件失败",
});
}
receiveLoading.value = false;
await localCurrentFile.value.loadLocalDirectory();
};
// 事件监听
onMounted(() => {
peer.init();
peer.on("open", ((event: CustomEvent) => {
myId.value = event.detail;
const sign = getUrlParam("sign");
if (sign) {
targetId.value = sign;
handleConnect();
}
}) as EventListener);
peer.on("connection-open", ((event: CustomEvent) => {
if (event.detail.peer == peer.remoteConnection?.peer) {
checkBytes();
isConnected.value = peer.remoteConnection?.open;
fileMgrInstance.remoteRootFile.loadLocalDirectory();
notification.success({
message: "连接成功",
});
} else {
notification.success({
message: "出现新的连接",
});
}
}) as EventListener);
peer.on("peer-disconnected", ((event: CustomEvent) => {
isConnected.value = peer.remoteConnection?.open;
notification.error({
message: event.detail.peer + "连接已断开",
});
}) as EventListener);
peer.on("error", ((event: CustomEvent) => {
notification.error({
message: "发生错误",
});
console.error("连接错误:", event.detail);
}) as EventListener);
});
</script>
<style scoped>
.container {
padding: 20px;
display: flex;
flex-direction: column;
height: calc(100vh - 75px);
gap: 20px;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: #f5f5f5;
border-radius: 8px;
}
.peer-info {
display: flex;
align-items: center;
gap: 10px;
}
.connect-section {
display: flex;
gap: 10px;
}
.file-transfer {
display: flex;
flex: 1;
gap: 20px;
min-height: 0;
}
.file-transfer.mobile-layout {
flex-direction: column;
}
.file-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.panel-header {
padding: 10px;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.path-nav {
display: flex;
align-items: center;
gap: 10px;
margin-top: 5px;
}
.file-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.file-item {
display: flex;
align-items: center;
padding: 8px;
cursor: pointer;
border-radius: 4px;
gap: 10px;
}
.file-item:hover {
background: #f5f5f5;
}
.file-item.selected {
background: #e3f2fd;
}
.file-icon {
font-size: 24px;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: 500;
}
.file-meta {
font-size: 12px;
color: #666;
}
.panel-footer {
padding: 10px;
background: #f5f5f5;
border-top: 1px solid #ddd;
display: flex;
gap: 10px;
}
.transfer-controls {
display: flex;
flex-direction: column;
justify-content: center;
gap: 10px;
}
.transfer-progress {
padding: 10px;
background: #f5f5f5;
border-radius: 8px;
}
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
}
.progress-bar {
height: 4px;
background: #ddd;
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #007aff;
transition: width 0.3s ease;
}
input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
input:disabled {
background: #f5f5f5;
}
.id-text {
font-weight: bold;
color: #007aff;
}
/* 移动端适配样式 */
@media screen and (max-width: 768px) {
.status-bar {
flex-direction: column;
gap: 10px;
}
.connect-section {
width: 100%;
}
.connect-section input {
flex: 1;
}
.status-info {
width: 100%;
display: flex;
justify-content: space-around;
}
}
</style>