refactor: replace naive-ui with vuetify

This commit is contained in:
2026-02-04 22:41:22 +08:00
parent a4173c327d
commit f7a881358f
26 changed files with 2853 additions and 1379 deletions

View File

@@ -1,28 +1,7 @@
<script lang="ts" setup>
import { onMounted, ref, computed, h } from "vue";
import { onMounted, ref, computed } from "vue";
import PeerCard from "./PeerCard.vue";
import TransferItem from "./TransferItem.vue";
import {
NLayout,
NLayoutHeader,
NLayoutContent,
NLayoutSider,
NSpace,
NText,
NEmpty,
NMenu,
NBadge,
NButton,
NIcon,
} from "naive-ui";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {
faSatelliteDish,
faInbox,
faBars,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
import { type MenuOption } from "naive-ui";
import { Peer } from "../../bindings/mesh-drop/internal/discovery/models";
import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
import { GetPeers } from "../../bindings/mesh-drop/internal/discovery/service";
@@ -32,7 +11,7 @@ import { GetTransferList } from "../../bindings/mesh-drop/internal/transfer/serv
const peers = ref<Peer[]>([]);
const transferList = ref<Transfer[]>([]);
const activeKey = ref("discover");
const showMobileMenu = ref(false);
const drawer = ref(true); // Control drawer visibility
const isMobile = ref(false);
// 监听窗口大小变化更新 isMobile
@@ -43,49 +22,20 @@ onMounted(async () => {
transferList.value = (
(list || []).filter((t) => t !== null) as Transfer[]
).sort((a, b) => b.create_time - a.create_time);
if (isMobile.value) {
drawer.value = false;
}
});
const checkMobile = () => {
isMobile.value = window.innerWidth < 768;
if (!isMobile.value) showMobileMenu.value = false;
const mobile = window.innerWidth < 768;
if (mobile !== isMobile.value) {
isMobile.value = mobile;
drawer.value = !mobile;
}
};
// --- 菜单选项 ---
const renderIcon = (icon: any) => {
return () => h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon }) });
};
const menuOptions = computed<MenuOption[]>(() => [
{
label: "Discover",
key: "discover",
icon: renderIcon(faSatelliteDish),
},
{
label: () =>
h(
"div",
{
style:
"display: flex; align-items: center; justify-content: space-between; width: 100%",
},
[
"Transfers",
pendingCount.value > 0 ?
h(NBadge, {
style: "display: inline-flex; align-items: center",
value: pendingCount.value,
max: 99,
type: "error",
})
: null,
],
),
key: "transfers",
icon: renderIcon(faInbox),
},
]);
// --- 后端集成 ---
onMounted(async () => {
peers.value = await GetPeers();
@@ -111,146 +61,134 @@ const pendingCount = computed(() => {
).length;
});
const menuItems = computed(() => [
{
title: "Discover",
value: "discover",
icon: "mdi-radar",
},
{
title: "Transfers",
value: "transfers",
icon: "mdi-inbox",
badge: pendingCount.value > 0 ? pendingCount.value : null,
},
]);
// --- 操作 ---
const handleMenuUpdate = (key: string) => {
const handleMenuClick = (key: string) => {
activeKey.value = key;
showMobileMenu.value = false;
if (isMobile.value) {
drawer.value = false;
}
};
</script>
<template>
<!-- 小尺寸头部 -->
<n-layout-header v-if="isMobile" bordered class="mobile-header">
<n-space
align="center"
justify="space-between"
style="height: 100%; padding: 0 16px">
<n-text class="logo">Mesh Drop</n-text>
<n-button
text
style="font-size: 24px"
@click="showMobileMenu = !showMobileMenu">
<n-icon>
<FontAwesomeIcon :icon="showMobileMenu ? faXmark : faBars" />
</n-icon>
</n-button>
</n-space>
</n-layout-header>
<v-layout>
<!-- App Bar for Mobile -->
<v-app-bar v-if="isMobile" border flat>
<v-toolbar-title class="text-primary font-weight-bold"
>Mesh Drop</v-toolbar-title
>
<template v-slot:append>
<v-btn icon="mdi-menu" @click="drawer = !drawer"></v-btn>
</template>
</v-app-bar>
<!-- 小尺寸抽屉菜单 -->
<n-drawer
v-model:show="showMobileMenu"
placement="top"
height="200"
v-if="isMobile">
<n-drawer-content>
<n-menu
:value="activeKey"
:options="menuOptions"
@update:value="handleMenuUpdate" />
</n-drawer-content>
</n-drawer>
<n-layout
has-sider
position="absolute"
:style="{ top: isMobile ? '64px' : '0' }">
<!-- 桌面端侧边栏 -->
<n-layout-sider
v-if="!isMobile"
bordered
width="240"
content-style="padding: 24px;">
<div class="desktop-logo">
<n-text class="logo">Mesh Drop</n-text>
<!-- Navigation Drawer -->
<v-navigation-drawer v-model="drawer" :permanent="!isMobile">
<div class="pa-4" v-if="!isMobile">
<div class="text-h6 text-primary font-weight-bold">Mesh Drop</div>
</div>
<n-menu
:value="activeKey"
:options="menuOptions"
@update:value="handleMenuUpdate" />
</n-layout-sider>
<n-layout-content class="content">
<div class="content-container">
<!-- 发现页视图 -->
<v-list nav>
<v-list-item
v-for="item in menuItems"
:key="item.value"
:value="item.value"
:active="activeKey === item.value"
@click="handleMenuClick(item.value)"
rounded="xl"
color="primary"
>
<template v-slot:prepend>
<v-icon :icon="item.icon"></v-icon>
</template>
<v-list-item-title>
{{ item.title }}
<v-badge
v-if="item.badge"
:content="item.badge"
color="error"
inline
class="ml-2"
></v-badge>
</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
<!-- Main Content -->
<v-main>
<v-container fluid class="pa-4">
<!-- Discover View -->
<div v-show="activeKey === 'discover'">
<n-space vertical size="large" v-if="peers.length > 0">
<div class="peer-grid">
<div v-for="peer in peers" :key="peer.id">
<PeerCard
:peer="peer"
@transferStarted="activeKey = 'transfers'" />
</div>
<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>
</n-space>
</div>
<div v-else class="empty-state">
<n-empty description="Scanning for peers...">
<template #icon>
<n-icon class="radar-icon">
<FontAwesomeIcon :icon="faSatelliteDish" />
</n-icon>
</template>
</n-empty>
<div
v-else
class="empty-state d-flex flex-column justify-center align-center"
>
<v-icon
icon="mdi-radar"
size="100"
color="primary"
class="mb-4 radar-icon"
style="opacity: 0.5"
></v-icon>
<div class="text-grey">Scanning for peers...</div>
</div>
</div>
<!-- 传输列表视图 -->
<!-- Transfers View -->
<div v-show="activeKey === 'transfers'">
<div v-if="transferList.length > 0">
<TransferItem
v-for="transfer in transferList"
:key="transfer.id"
:transfer="transfer" />
:transfer="transfer"
/>
</div>
<div v-else class="empty-state">
<n-empty style="user-select: none" description="No transfers yet">
<template #icon>
<n-icon>
<FontAwesomeIcon :icon="faInbox" />
</n-icon>
</template>
</n-empty>
<div
v-else
class="empty-state d-flex flex-column justify-center align-center"
>
<v-icon icon="mdi-inbox" size="100" class="mb-4 text-grey"></v-icon>
<div class="text-grey">No transfers yet</div>
</div>
</div>
</div>
</n-layout-content>
</n-layout>
</v-container>
</v-main>
</v-layout>
</template>
<style scoped>
.mobile-header {
height: 64px;
z-index: 1000;
}
.desktop-logo {
margin-bottom: 24px;
padding-left: 8px;
}
.logo {
font-size: 1.25rem;
font-weight: 700;
color: #38bdf8;
}
.content-container {
padding: 24px;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 90vh;
height: 80vh;
}
.radar-icon {
animation: spin 3s linear infinite;
color: #38bdf8;
opacity: 0.5;
}
@keyframes spin {
@@ -271,7 +209,7 @@ const handleMenuUpdate = (key: string) => {
}
}
@media (min-width: 700px) {
@media (min-width: 960px) {
.peer-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}