feat: peercard accept drag event

This commit is contained in:
2026-02-10 22:44:18 +08:00
parent 7c65daeb89
commit ea40aa76d0
8 changed files with 211 additions and 48 deletions

View File

@@ -2,5 +2,6 @@
// This file is automatically generated. DO NOT EDIT
export {
File,
FilesDroppedEvent
} from "./models.js";

View File

@@ -5,8 +5,33 @@
// @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime";
export class File {
"name": string;
"path": string;
/** Creates a new File instance. */
constructor($$source: Partial<File> = {}) {
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("path" in $$source)) {
this["path"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new File instance from a string or object.
*/
static createFrom($$source: any = {}): File {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new File($$parsedSource as Partial<File>);
}
}
export class FilesDroppedEvent {
"files": string[];
"files": File[];
"target": string;
/** Creates a new FilesDroppedEvent instance. */
@@ -25,7 +50,7 @@ export class FilesDroppedEvent {
* Creates a new FilesDroppedEvent instance from a string or object.
*/
static createFrom($$source: any = {}): FilesDroppedEvent {
const $$createField0_0 = $$createType0;
const $$createField0_0 = $$createType1;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("files" in $$parsedSource) {
$$parsedSource["files"] = $$createField0_0($$parsedSource["files"]);
@@ -35,4 +60,5 @@ export class FilesDroppedEvent {
}
// Private type creation functions
const $$createType0 = $Create.Array($Create.Any);
const $$createType0 = File.createFrom;
const $$createType1 = $Create.Array($$createType0);

View File

@@ -150,12 +150,24 @@ const handleCleanFinished = async () => {
<v-container fluid class="pa-4">
<!-- 发现视图 -->
<div v-show="activeKey === 'discover'">
<div v-if="peers.length > 0" class="peer-grid">
<div v-for="peer in peers" :key="peer.id">
<PeerCard
:peer="peer"
@transferStarted="activeKey = 'transfers'"
/>
<div v-if="peers.length > 0">
<v-alert
icon="mdi-information-outline"
density="compact"
variant="tonal"
color="primary"
class="mb-4 text-body-2"
closable
>
{{ t("discover.dragDropHint") }}
</v-alert>
<div class="peer-grid">
<div v-for="peer in peers" :key="peer.id">
<PeerCard
:peer="peer"
@transferStarted="activeKey = 'transfers'"
/>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
// --- Vue 核心 ---
import { computed, ref, watch, onMounted } from "vue";
import { computed, ref, watch, onMounted, onUnmounted } from "vue";
import { useI18n } from "vue-i18n";
// --- 组件 ---
@@ -8,7 +8,7 @@ import FileSendModal from "./modals/FileSendModal.vue";
import TextSendModal from "./modals/TextSendModal.vue";
// --- Wails & 后端绑定 ---
import { Dialogs, Clipboard } from "@wailsio/runtime";
import { Dialogs, Clipboard, Events } from "@wailsio/runtime";
import {
SendFolder,
SendText,
@@ -19,14 +19,24 @@ import {
AddTrust,
RemoveTrust,
} from "../../bindings/mesh-drop/internal/config/config";
import { File } from "bindings/mesh-drop/models";
// --- 生命周期 ---
const droppedFiles = ref<File[]>([]);
onMounted(async () => {
try {
isTrusted.value = await IsTrusted(props.peer.id);
} catch (err) {
console.error("Failed to check trusted peer status:", err);
}
Events.On("files-dropped", (event) => {
droppedFiles.value = event.data.files;
showFileModal.value = true;
});
});
onUnmounted(() => {
Events.Off("files-dropped");
});
// --- 属性 & 事件 ---
@@ -171,7 +181,14 @@ const handleUntrust = () => {
</script>
<template>
<v-card hover link class="peer-card pa-2" :ripple="false">
<v-card
hover
link
class="peer-card pa-2"
:ripple="false"
data-file-drop-target
:id="`drop-zone-peer-${peer.id}`"
>
<template #title>
<div class="d-flex align-center">
<v-icon :icon="osIcon" size="24" class="mr-2"></v-icon>
@@ -219,6 +236,16 @@ const handleUntrust = () => {
{{ t("discover.noRoute") }}
</v-chip>
</div>
<!-- 拖放提示覆盖层 -->
<div class="drag-drop-overlay">
<v-icon
icon="mdi-file-upload-outline"
size="48"
color="primary"
style="opacity: 0.8"
></v-icon>
</div>
</template>
<v-card-actions>
@@ -304,6 +331,7 @@ const handleUntrust = () => {
v-model="showFileModal"
:peer="peer"
:selectedIp="selectedIp"
:files="droppedFiles"
@transferStarted="emit('transferStarted')"
/>
@@ -314,3 +342,70 @@ const handleUntrust = () => {
@transferStarted="emit('transferStarted')"
/>
</template>
<style scoped>
.peer-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.peer-card::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgb(var(--v-theme-primary));
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
z-index: 1;
}
.peer-card.file-drop-target-active {
transform: translateY(-4px);
box-shadow: 0 8px 24px -4px rgba(var(--v-theme-primary), 0.24) !important;
border-color: rgb(var(--v-theme-primary)) !important;
}
.peer-card.file-drop-target-active::after {
opacity: 0.12;
}
.drag-drop-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
background: rgba(var(--v-theme-surface), 0.8);
backdrop-filter: blur(2px);
}
.peer-card.file-drop-target-active .drag-drop-overlay {
opacity: 1;
}
.drag-drop-content {
color: rgb(var(--v-theme-primary));
font-weight: 500;
display: flex;
flex-direction: column;
align-items: center;
transform: translateY(10px);
transition: transform 0.3s ease;
}
.peer-card.file-drop-target-active .drag-drop-content {
transform: translateY(0);
}
</style>

View File

@@ -1,18 +1,22 @@
<script setup lang="ts">
// --- Vue 核心 ---
import { computed, ref, watch } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
// --- Wails & 后端绑定 ---
import { Events, Dialogs } from "@wailsio/runtime";
import { Events, Dialogs, Window } from "@wailsio/runtime";
import { SendFiles } from "../../../bindings/mesh-drop/internal/transfer/service";
import { Peer } from "../../../bindings/mesh-drop/internal/discovery/models";
import { File } from "bindings/mesh-drop/models";
onMounted(() => {});
// --- 属性 & 事件 ---
const props = defineProps<{
modelValue: boolean;
peer: Peer;
selectedIp: string;
files: File[];
}>();
const emit = defineEmits<{
@@ -22,7 +26,6 @@ const emit = defineEmits<{
// --- 状态 ---
const { t } = useI18n();
const fileList = ref<{ name: string; path: string }[]>([]);
// --- 计算属性 ---
const show = computed({
@@ -34,19 +37,15 @@ const show = computed({
watch(show, (newVal) => {
if (newVal) {
Events.On("files-dropped", (event) => {
const files: string[] = event.data.files || [];
const files: File[] = event.data.files || [];
files.forEach((f) => {
if (!fileList.value.find((existing) => existing.path === f)) {
fileList.value.push({
name: f.split(/[\/]/).pop() || f,
path: f,
});
if (!props.files.find((existing) => existing.path === f.path)) {
props.files.push(f);
}
});
});
} else {
Events.Off("files-dropped");
fileList.value = [];
}
});
@@ -60,17 +59,17 @@ const openFileDialog = async () => {
if (files) {
if (Array.isArray(files)) {
files.forEach((f) => {
if (!fileList.value.find((existing) => existing.path === f)) {
fileList.value.push({
name: f.split(/[\\/]/).pop() || f,
if (!props.files.find((existing) => existing.path === f)) {
props.files.push({
name: f.split(/[\/]/).pop() || f,
path: f,
});
}
});
} else {
const f = files as string;
if (!fileList.value.find((existing) => existing.path === f)) {
fileList.value.push({
if (!props.files.find((existing) => existing.path === f)) {
props.files.push({
name: f.split(/[\\/]/).pop() || f,
path: f,
});
@@ -80,12 +79,12 @@ const openFileDialog = async () => {
};
const handleRemoveFile = (index: number) => {
fileList.value.splice(index, 1);
props.files.splice(index, 1);
};
const handleSendFiles = async () => {
if (fileList.value.length === 0 || !props.selectedIp) return;
const paths = fileList.value.map((f) => f.path);
if (props.files.length === 0 || !props.selectedIp) return;
const paths = props.files.map((f) => f.path);
try {
await SendFiles(props.peer, props.selectedIp, paths);
@@ -103,10 +102,11 @@ const handleSendFiles = async () => {
<v-card :title="$t('modal.fileSend.title')">
<v-card-text>
<div
v-if="fileList.length === 0"
class="drop-zone pa-10 text-center rounded-lg border-dashed"
v-if="props.files.length === 0"
class="drop-zone pa-10 text-center rounded-lg"
@click="openFileDialog"
data-file-drop-target
id="drop-zone-area"
>
<v-icon
icon="mdi-cloud-upload"
@@ -127,9 +127,10 @@ const handleSendFiles = async () => {
max-height="400"
style="overflow-y: auto"
data-file-drop-target
id="drop-zone-list"
>
<v-list-item
v-for="(file, index) in fileList"
v-for="(file, index) in props.files"
:key="file.path"
:title="file.name"
:subtitle="file.path"
@@ -149,8 +150,7 @@ const handleSendFiles = async () => {
<v-btn
block
variant="outlined"
style="border-style: dashed"
variant="tonal"
prepend-icon="mdi-plus"
@click="openFileDialog"
class="mt-2"
@@ -168,10 +168,10 @@ const handleSendFiles = async () => {
<v-btn
color="primary"
@click="handleSendFiles"
:disabled="fileList.length === 0"
:disabled="props.files.length === 0"
>
{{ $t("modal.fileSend.sendSrc") }}
{{ fileList.length > 0 ? `(${fileList.length})` : "" }}
{{ props.files.length > 0 ? `(${props.files.length})` : "" }}
</v-btn>
</v-card-actions>
</v-card>
@@ -180,17 +180,32 @@ const handleSendFiles = async () => {
<style scoped>
.drop-zone {
border: 2px dashed #666; /* Use a darker color or theme var */
border: 2px solid transparent;
border-radius: 12px;
background-color: rgba(var(--v-theme-on-surface), 0.04);
cursor: pointer;
transition: all 0.3s;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.drop-zone:hover {
border-color: #38bdf8;
background-color: rgba(56, 189, 248, 0.05);
background-color: rgba(var(--v-theme-primary), 0.08);
}
.drop-zone.file-drop-target-active {
border-color: #38bdf8;
background-color: rgba(56, 189, 248, 0.1);
border-color: rgb(var(--v-theme-primary));
background-color: rgba(var(--v-theme-primary), 0.12);
transform: scale(1.01);
box-shadow: 0 4px 12px rgba(var(--v-theme-primary), 0.15);
}
#drop-zone-list {
transition: all 0.3s ease;
}
#drop-zone-list.file-drop-target-active {
box-shadow: inset 0 0 0 2px rgb(var(--v-theme-primary));
background-color: rgba(var(--v-theme-primary), 0.04);
}
</style>

View File

@@ -33,7 +33,8 @@
"trustPeer": "Trust Peer",
"untrustPeer": "Untrust Peer",
"sendFolderFailed": "Failed to send folder: {error}",
"sendClipboardFailed": "Failed to send clipboard: {error}"
"sendClipboardFailed": "Failed to send clipboard: {error}",
"dragDropHint": "Drag and drop files here to send"
},
"transfers": {
"noTransfers": "No transfers yet",

View File

@@ -33,7 +33,8 @@
"trustPeer": "信任设备",
"untrustPeer": "取消信任",
"sendFolderFailed": "发送文件夹失败: {error}",
"sendClipboardFailed": "发送剪贴板失败: {error}"
"sendClipboardFailed": "发送剪贴板失败: {error}",
"dragDropHint": "拖放文件到此处快速发送"
},
"transfers": {
"noTransfers": "暂无传输记录",