feat: 离线状态体验增强

This commit is contained in:
nap0o committed 2026-02-28 18:19:33 +08:00
1 parent face59cdd7
commit 247d25b213
4 files changed
+50 -52

No files matched your search

+4 -1
View File
@@ -91,7 +91,9 @@ jobs:
OAUTH_GOOGLE_REDIRECT_URI: ${{ secrets.OAUTH_GOOGLE_REDIRECT_URI }}
run: |
SECRETS="OAUTH_ALLOWED_USERS
SECRETS="
OAUTH_ALLOW_ALL
OAUTH_ALLOWED_USERS
ENCRYPTION_KEY
JWT_SECRET"
@@ -125,6 +127,7 @@ jobs:
command: deploy --minify
secrets: ${{ steps.secret_list.outputs.secrets }}
env:
OAUTH_ALLOW_ALL: ${{ secrets.OAUTH_ALLOW_ALL || 'false' }}
OAUTH_ALLOWED_USERS: ${{ secrets.OAUTH_ALLOWED_USERS }}
OAUTH_GITHUB_CLIENT_ID: ${{ secrets.OAUTH_GITHUB_CLIENT_ID }}
OAUTH_GITHUB_CLIENT_SECRET: ${{ secrets.OAUTH_GITHUB_CLIENT_SECRET }}
+16 -1
View File
@@ -1,6 +1,7 @@
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { RouterView, useRoute } from 'vue-router'
import { Warning } from '@element-plus/icons-vue'
import TheHeader from '@/shared/components/theHeader.vue'
import TheFooter from '@/shared/components/theFooter.vue'
import { useLayoutStore } from '@/shared/stores/layoutStore'
@@ -12,18 +13,32 @@ const checkMobile = () => {
layoutStore.isMobile = window.innerWidth < 768
}
const isOffline = ref(false)
const setOnline = () => { isOffline.value = false }
const setOffline = () => { isOffline.value = true }
onMounted(() => {
checkMobile()
isOffline.value = !navigator.onLine
window.addEventListener('resize', checkMobile)
window.addEventListener('online', setOnline)
window.addEventListener('offline', setOffline)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', checkMobile)
window.removeEventListener('online', setOnline)
window.removeEventListener('offline', setOffline)
})
</script>
<template>
<div class="app-container">
<!-- 方案A: 全局离线横幅 -->
<div v-if="isOffline" class="global-offline-banner" style="background-color: #fdf6ec; color: #e6a23c; padding: 10px 15px; text-align: center; font-size: 14px; display: flex; align-items: center; justify-content: center; gap: 6px; border-bottom: 1px solid #faecd8; position: sticky; top: 0; z-index: 9999; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
<el-icon><Warning /></el-icon> <span>网络已断开,当前应用正处于离线缓存模式</span>
</div>
<!-- 登录页通常不显示头部,可以通过路由 meta 控制,这里简单示例默认显示 -->
<TheHeader v-if="!route.meta.hideHeader" />
@@ -16,12 +16,19 @@
<!-- 3. 数据列表 (已解锁) -->
<div v-else class="vault-content">
<div class="toolbar" style="margin-bottom: 20px; display: flex; gap: 15px; align-items: center; justify-content: space-between; flex-wrap: wrap;">
<el-input
v-model="searchQuery"
placeholder="🔍 搜索服务名称、账号或分类..."
clearable
style="max-width: 400px; flex: 1;"
/>
<div style="display: flex; align-items: center; gap: 10px; flex: 1;">
<el-input
v-model="searchQuery"
placeholder="搜索服务名称、账号或分类..."
clearable
style="max-width: 400px;"
>
<template #prefix>
<el-icon v-if="isFetching && searchQuery" class="is-loading"><Loading /></el-icon>
<el-icon v-else><Search /></el-icon>
</template>
</el-input>
</div>
<div class="batch-actions" style="display: flex; align-items: center; gap: 10px;">
<template v-if="selectedIds.length > 0">
@@ -165,7 +172,7 @@
<script setup>
import { ref, shallowRef, onMounted, onUnmounted, watch, computed, defineAsyncComponent, nextTick, triggerRef } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { MoreFilled, Edit, Delete, Picture, View, Hide, CopyDocument, Loading } from '@element-plus/icons-vue'
import { MoreFilled, Edit, Delete, Picture, View, Hide, CopyDocument, Loading, Search } from '@element-plus/icons-vue'
import QRCode from 'qrcode'
import { request } from '@/shared/utils/request'
import { useLayoutStore } from '@/shared/stores/layoutStore'
@@ -199,7 +206,6 @@ const showSecret = ref(false)
const qrCodeUrl = ref('')
let globalTimer = null
let loadingMessageHandle = null
// --- 定时更新 (提升定义顺序) ---
// 更新倒计时与验证码
@@ -251,54 +257,20 @@ const updateVaultStatus = async (targetList) => {
// --- 工具函数 ---
let searchTimer = null
// --- Loading 提示控制 ---
const showTopLoading = () => {
// 仅在已解锁状态下显示 Loading
if (!vaultStore.isUnlocked) return
// 防抖:如果已经在显示或正在等待显示,则不重复触发
if (loadingMessageHandle || loadingTimer) return
loadingMessageHandle = ElMessage({
message: '数据正在加载...',
icon: Loading,
duration: 0, // 设为 0 则不会自动关闭
type: 'info',
grouping: true
})
}
let loadingTimer = null
const hideTopLoading = () => {
if (loadingTimer) clearTimeout(loadingTimer) // 清除等待中的 Loading
loadingTimer = null
if (loadingMessageHandle) {
loadingMessageHandle.close()
loadingMessageHandle = null
}
}
// --- Vue Query 集成 ---
// 1. 定义数据获取函数
const fetchVaultPage = async ({ pageParam = 1 }) => {
// 如果未解锁,直接返回空(理论上 enabled 会控制不执行,但做个兜底)
// 如果未解锁,直接返回空
if (!vaultStore.isUnlocked) return { vault: [], pagination: { totalPages: 0 } }
// 如果是第一页且没有搜索,显示顶部 Loading
if (pageParam === 1 && !searchQuery.value) showTopLoading()
const query = new URLSearchParams({
const query = new URLSearchParams({
page: pageParam,
limit: pageSize.value,
search: searchQuery.value
}).toString()
search: searchQuery.value
}).toString()
const data = await request(`/api/vault?${query}`)
// 隐藏 Loading (仅第一页)
if (pageParam === 1) hideTopLoading()
return data
return await request(`/api/vault?${query}`)
}
// 2. 使用 useInfiniteQuery
@@ -403,8 +375,7 @@ defineExpose({
})
watch(searchQuery, () => {
// Vue Query 会自动处理,这里只需要处理 Loading UI
if (searchQuery.value) showTopLoading()
// Vue Query 会自动处理列表更新
})
// 批量选择
@@ -528,6 +499,5 @@ onMounted(() => {
onUnmounted(() => {
if (globalTimer) clearInterval(globalTimer)
if (searchTimer) clearTimeout(searchTimer)
hideTopLoading()
})
</script>
+10
View File
@@ -22,6 +22,16 @@ export async function request(url, options = {}) {
headers['X-CSRF-Token'] = csrfToken;
}
// --- 离线拦截逻辑 ---
// 拦截破坏性/写操作,在无网络时提供温和提示并短路执行
const method = (options.method || 'GET').toUpperCase();
if (!navigator.onLine && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
if (!options.silent) {
ElMessage.warning('当前处于离线模式,无法进行此操作');
}
throw new Error('Offline: Cannot perform mutative action');
}
try {
const response = await fetch(url, { ...options, headers, credentials: 'include' })