refactor(app): 重构应用初始化和骨架屏逻辑

- 移除了不必要的 preload 脚本- 优化了 i18n 和 websocket 插件的实现- 简化了 app.vue 中的逻辑
- 新增了独立的骨架屏组件
This commit is contained in:
xingyy 2025-02-27 16:25:31 +08:00
parent 6cb7a282f6
commit 812135c5bb
7 changed files with 200 additions and 225 deletions

View File

@ -1,55 +1,23 @@
<script setup>
import {useI18n} from 'vue-i18n'
import {message} from '@/components/x-message/useMessage.js'
import {hideMinWindow1} from "@/components/floatingBubble/floating.js";
import AppSkeleton from '@/components/app-skeleton/index.vue'
// message.success('success')
const {t} = useI18n()
useHead({
title: useI18n().t('appSetting.appName'),
title: t('appSetting.appName'),
meta: [
{name: 'description', content: useI18n().t('appSetting.appDescription')},
{name: 'keywords', content: useI18n().t('appSetting.appKeyWords')},
{name: 'description', content: t('appSetting.appDescription')},
{name: 'keywords', content: t('appSetting.appKeyWords')},
],
})
//
const router = useRouter()
const route = useRoute()
const slideDirection = ref('slide-left')
const { locale } = useI18n()
//
const routeHistory = ref([])
//
const nuxtApp = useNuxtApp()
const isSkeletonLoading = ref(true)
//
onMounted(() => {
console.log('app.vue 已挂载,检查骨架屏状态')
//
if (nuxtApp.$appSkeleton) {
console.log('找到骨架屏插件,当前状态:', nuxtApp.$appSkeleton.isLoading.value)
isSkeletonLoading.value = nuxtApp.$appSkeleton.isLoading.value
//
watch(() => nuxtApp.$appSkeleton.isLoading.value, (newValue) => {
console.log('骨架屏状态变化:', newValue)
isSkeletonLoading.value = newValue
}, { immediate: true })
} else {
console.log('未找到骨架屏插件,将使用备用机制')
// 3
setTimeout(() => {
isSkeletonLoading.value = false
console.log('备用机制:骨架屏已隐藏')
}, 3000)
}
})
router.beforeEach((to, from) => {
//
routeHistory.value.push(from.path)
@ -69,12 +37,12 @@ if (to.path==='/'){
//
provide('slideDirection', slideDirection)
</script>
<template>
<client-only>
<!-- 骨架屏组件 -->
<AppSkeleton v-if="isSkeletonLoading" />
<VanConfigProvider>
<NuxtLoadingIndicator

View File

@ -0,0 +1,194 @@
<script setup>
//
</script>
<template>
<transition name="fade">
<div class="app-skeleton">
<!-- 顶部导航栏骨架 -->
<div class="skeleton-header">
<div class="skeleton-avatar"></div>
<div class="skeleton-title"></div>
</div>
<!-- 内容区域骨架 -->
<div class="skeleton-content">
<!-- 轮播图骨架 -->
<div class="skeleton-banner"></div>
<!-- 菜单项骨架 -->
<div class="skeleton-menu">
<div class="skeleton-menu-item" v-for="i in 4" :key="i"></div>
</div>
<!-- 列表项骨架 -->
<div class="skeleton-list">
<div class="skeleton-list-item" v-for="i in 3" :key="i">
<div class="skeleton-list-image"></div>
<div class="skeleton-list-content">
<div class="skeleton-list-title"></div>
<div class="skeleton-list-desc"></div>
<div class="skeleton-list-price"></div>
</div>
</div>
</div>
</div>
<!-- 底部导航栏骨架 -->
<div class="skeleton-tabbar">
<div class="skeleton-tab-item" v-for="i in 4" :key="i"></div>
</div>
</div>
</transition>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.app-skeleton {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
z-index: 9999;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 骨架屏动画 */
@keyframes skeleton-loading {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
.skeleton-header, .skeleton-avatar, .skeleton-title, .skeleton-banner,
.skeleton-menu-item, .skeleton-list-image, .skeleton-list-title,
.skeleton-list-desc, .skeleton-list-price, .skeleton-tab-item {
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%);
background-size: 400% 100%;
animation: skeleton-loading 1.4s ease infinite;
}
/* 顶部导航栏 */
.skeleton-header {
height: 44px;
padding: 0 16px;
display: flex;
align-items: center;
border-bottom: 1px solid #f5f5f5;
}
.skeleton-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
}
.skeleton-title {
width: 120px;
height: 16px;
margin-left: 12px;
border-radius: 4px;
}
/* 内容区域 */
.skeleton-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.skeleton-banner {
width: 100%;
height: 150px;
border-radius: 8px;
margin-bottom: 20px;
}
/* 菜单项 */
.skeleton-menu {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.skeleton-menu-item {
width: 60px;
height: 60px;
border-radius: 8px;
}
/* 列表项 */
.skeleton-list-item {
display: flex;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #f5f5f5;
}
.skeleton-list-image {
width: 80px;
height: 80px;
border-radius: 4px;
flex-shrink: 0;
}
.skeleton-list-content {
flex: 1;
margin-left: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.skeleton-list-title {
height: 16px;
width: 80%;
border-radius: 4px;
margin-bottom: 8px;
}
.skeleton-list-desc {
height: 12px;
width: 60%;
border-radius: 4px;
margin-bottom: 8px;
}
.skeleton-list-price {
height: 16px;
width: 40%;
border-radius: 4px;
}
/* 底部导航栏 */
.skeleton-tabbar {
height: 50px;
display: flex;
justify-content: space-around;
align-items: center;
border-top: 1px solid #f5f5f5;
padding: 0 16px;
}
.skeleton-tab-item {
width: 24px;
height: 24px;
border-radius: 4px;
}
</style>

View File

@ -38,15 +38,5 @@ export default defineNuxtPlugin(() => {
const systemLang = getSystemLanguage()
setLocale(systemLang as TypeLocale)
Locale.use(systemLang)
// 监听语言变化,当语言变化时,如果有活跃的 WebSocket 连接,则重新连接
watch(() => i18n.locale.value, (newLocale) => {
// 如果 WebSocket 插件已加载并且有活跃连接
if (nuxtApp.$ws) {
// 使用 refreshConnection 方法刷新 WebSocket 连接
console.log('语言已更改为:', newLocale, '正在更新 WebSocket 连接')
nuxtApp.$ws.refreshConnection()
}
})
}
})

View File

@ -1,117 +0,0 @@
import {authStore} from "@/stores/auth";
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const { token } = authStore()
const i18n = useNuxtApp().$i18n
const ws = reactive({
instance: null as WebSocket | null,
isConnected: false,
currentPath: '', // 保存当前连接的路径
currentData: null as Record<string, any> | null, // 保存当前连接的数据
// 修改 connect 方法接收路径和数据对象
connect(path: string, data?: Record<string, any>) {
// 保存当前连接的路径和数据,以便后续重连使用
this.currentPath = path
this.currentData = data || null
if (this.instance?.readyState === WebSocket.OPEN) {
this.instance.close()
}
// 构建查询字符串
const queryString =data
? '?' + Object.entries({
token: token.value,
'accept-language': i18n.locale.value, // 添加当前语言作为 accept-language
...data
})
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
: '?' + Object.entries({
token: token.value,
'accept-language': i18n.locale.value // 即使没有其他数据,也添加语言
})
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
// 构建完整的 WebSocket URL
const wsUrl = `${config.public.NUXT_PUBLIC_SOCKET_URL}${path}${queryString}`
this.instance = new WebSocket(wsUrl)
this.instance.onopen = () => {
this.isConnected = true
console.log('WebSocket 已连接')
}
this.instance.onclose = () => {
this.isConnected = false
console.log('WebSocket 已断开')
/* this.reconnect(path, data)*/
}
this.instance.onerror = (error) => {
console.error('WebSocket 错误:', error)
}
this.instance.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
this.handleMessage(data)
} catch (error) {
console.error('消息解析错误:', error)
}
}
},
// 更新重连方法以支持数据对象
reconnect(path?: string, data?: Record<string, any>) {
setTimeout(() => {
console.log('尝试重新连接...')
// 如果提供了新的路径和数据,则使用新的;否则使用保存的当前路径和数据
this.connect(path || this.currentPath, data || this.currentData || undefined)
}, 3000)
},
// 使用当前保存的路径和数据重新连接
refreshConnection() {
if (this.currentPath) {
console.log('刷新 WebSocket 连接,使用当前语言:', i18n.locale.value)
this.connect(this.currentPath, this.currentData || undefined)
return true
}
return false // 如果没有当前连接信息,返回 false
},
// 发送消息
send(data: any) {
if (this.instance?.readyState === WebSocket.OPEN) {
this.instance.send(JSON.stringify(data))
} else {
console.warn('WebSocket 未连接,无法发送消息')
}
},
// 关闭连接
disconnect() {
if (this.instance) {
this.instance.close()
this.instance = null
}
},
// 消息处理
handleMessage(data: any) {
// 触发自定义事件,让组件可以监听
window.dispatchEvent(new CustomEvent('ws-message', { detail: data }))
}
})
return {
provide: {
ws
}
}
})

View File

@ -1,44 +0,0 @@
/**
* 格式化价格
* @param {number} price - 价格数值
* @param {string} currency - 货币符号默认为 ¥
* @returns {string} 格式化后的价格字符串
*/
export const formatPrice = (price, currency = '¥') => {
if (price == null || isNaN(price)) return `${currency}0`
// 将价格转换为数字
const numPrice = Number(price)
// 处理小数点,保留两位小数
const formattedPrice = numPrice.toFixed(2)
// 添加千位分隔符
const parts = formattedPrice.toString().split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return `${currency}${parts.join('.')}`
}
/**
* 格式化数字
* @param {number} num - 需要格式化的数字
* @returns {string} 格式化后的数字字符串
*/
export const formatNumber = (num) => {
if (num == null || isNaN(num)) return '0'
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
/**
* 格式化百分比
* @param {number} value - 需要格式化的值
* @param {number} decimals - 小数位数默认为 2
* @returns {string} 格式化后的百分比字符串
*/
export const formatPercent = (value, decimals = 2) => {
if (value == null || isNaN(value)) return '0%'
return `${(Number(value) * 100).toFixed(decimals)}%`
}

View File

@ -1,10 +0,0 @@
export default function preload() {
return `
;(function() {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const setting = localStorage.getItem('nuxt-color-mode') || 'auto';
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('van-theme-dark', true);
})()
`
}

View File

@ -1,7 +1,5 @@
import dotenv from 'dotenv'
import process from 'node:process'
import preload from './app/utils/preload'
import skeletonPreload from './app/utils/skeleton-preload'
import { currentLocales } from './i18n/i18n'
const envFile = process.env.ENV_FILE || '.env.test'
dotenv.config({ path: `./env/${envFile}` })
@ -81,10 +79,6 @@ export default defineNuxtConfig({
{ name: 'theme-color', media: '(prefers-color-scheme: light)', content: '#ffffff' },
{ name: 'theme-color', media: '(prefers-color-scheme: dark)', content: '#222222' },
],
script: [
{ innerHTML: skeletonPreload(), type: 'text/javascript', tagPosition: 'head' },
{ innerHTML: preload(), type: 'text/javascript', tagPosition: 'head' },
],
},
},
nitro: {