feat: Vue Router 集成 + 压缩密码编码修复
前端: - 引入 vue-router,HTML5 History 模式,干净 URL - ClientPage.vue 拆分为 5 个独立路由视图组件 - 提取共享状态到 composables(useLibraryRoots/useMediaPlayer/useViewMode/useModals) - 新增 DefaultLayout 统一布局 - 导航链接改为 router-link - FileCard 网格视图修复删除/压缩按钮 hover 不显示问题 后端: - 压缩密码编码修复:DotNetZip/SharpZipLib 不支持 UTF-8 密码 - 替换为 SharpZipLib + 中文字符替换为 # 的 ASCII 安全密码方案
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user