前端: - 引入 vue-router,HTML5 History 模式,干净 URL - ClientPage.vue 拆分为 5 个独立路由视图组件 - 提取共享状态到 composables(useLibraryRoots/useMediaPlayer/useViewMode/useModals) - 新增 DefaultLayout 统一布局 - 导航链接改为 router-link - FileCard 网格视图修复删除/压缩按钮 hover 不显示问题 后端: - 压缩密码编码修复:DotNetZip/SharpZipLib 不支持 UTF-8 密码 - 替换为 SharpZipLib + 中文字符替换为 # 的 ASCII 安全密码方案
89 lines
2.5 KiB
Vue
89 lines
2.5 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import ClientHeader from '../components/client/ClientHeader.vue'
|
|
import QrCodeModal from '../components/QrCodeModal.vue'
|
|
import ConfirmModal from '../components/ConfirmModal.vue'
|
|
import { useLibraryRoots } from '../composables/useLibraryRoots'
|
|
import { useMediaPlayer } from '../composables/useMediaPlayer'
|
|
import { useModals } from '../composables/useModals'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const { activeRoots, loadRoots } = useLibraryRoots()
|
|
const { selectedFile, flushVideoProgress } = useMediaPlayer()
|
|
const { confirmModal, qrModal } = useModals()
|
|
|
|
const searchQuery = ref('')
|
|
const errorMessage = ref('')
|
|
|
|
const isSearching = computed(() => route.name === 'search')
|
|
const isBrowsingRoots = computed(() => {
|
|
const name = route.name as string
|
|
return name === 'home' || name === 'recent-added' || name === 'recent-played'
|
|
})
|
|
|
|
const activeTab = computed<'recent-added' | 'recent-played' | 'libraries'>(() => {
|
|
const name = route.name as string
|
|
if (name === 'recent-added') return 'recent-added'
|
|
if (name === 'recent-played') return 'recent-played'
|
|
return 'libraries'
|
|
})
|
|
|
|
const clientTitle = computed(() => {
|
|
if (activeTab.value === 'recent-added') return '最近添加'
|
|
if (activeTab.value === 'recent-played') return '最近播放'
|
|
if (isBrowsingRoots.value) return '文件库'
|
|
return '文件'
|
|
})
|
|
|
|
async function doSearch() {
|
|
const keyword = searchQuery.value.trim()
|
|
if (!keyword) return
|
|
flushVideoProgress()
|
|
router.push({ name: 'search', query: { q: keyword } })
|
|
}
|
|
|
|
function backToRoots() {
|
|
flushVideoProgress()
|
|
router.push('/')
|
|
}
|
|
|
|
function exitSearch() {
|
|
flushVideoProgress()
|
|
searchQuery.value = ''
|
|
router.push('/')
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('beforeunload', flushVideoProgress)
|
|
if (activeRoots.value.length === 0) loadRoots()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<main class="client-shell">
|
|
<ClientHeader
|
|
v-model:search-query="searchQuery"
|
|
:title="clientTitle"
|
|
:active-tab="activeTab"
|
|
:recent-count="0"
|
|
:active-roots-count="activeRoots.length"
|
|
:is-browsing-roots="isBrowsingRoots"
|
|
:is-searching="isSearching"
|
|
:browse-data="null"
|
|
@search="doSearch"
|
|
@back="backToRoots"
|
|
@exit-search="exitSearch"
|
|
@open-qr="qrModal?.open()"
|
|
/>
|
|
|
|
<p v-if="errorMessage" class="error-banner">{{ errorMessage }}</p>
|
|
|
|
<router-view />
|
|
</main>
|
|
|
|
<QrCodeModal ref="qrModal" />
|
|
<ConfirmModal ref="confirmModal" />
|
|
</template>
|