refactor: replace naive-ui with vuetify
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user