|
@@ -1,7 +1,9 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
|
|
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
|
|
|
|
|
+import { watch } from 'vue';
|
|
|
import { createPinia } from 'pinia';
|
|
import { createPinia } from 'pinia';
|
|
|
import { useUserStore } from './store/user';
|
|
import { useUserStore } from './store/user';
|
|
|
|
|
+import { useNavigationStore } from './store/navigation';
|
|
|
import { initDebug } from './utils/debug';
|
|
import { initDebug } from './utils/debug';
|
|
|
import { initAnalytics } from './utils/analytics';
|
|
import { initAnalytics } from './utils/analytics';
|
|
|
import MiniPlayer from './components/MiniPlayer.vue';
|
|
import MiniPlayer from './components/MiniPlayer.vue';
|
|
@@ -110,8 +112,22 @@ async function handleWechatOAuthCallback() {
|
|
|
// #endif
|
|
// #endif
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-onLaunch(() => {
|
|
|
|
|
|
|
+onLaunch(async () => {
|
|
|
console.log('App Launch');
|
|
console.log('App Launch');
|
|
|
|
|
+
|
|
|
|
|
+ // ============ 第一件事:加载导航配置 ============
|
|
|
|
|
+ // 业务方要求:首次打开必须先获取底部 tab 导航接口,
|
|
|
|
|
+ // 否则不允许加载任何业务内容。
|
|
|
|
|
+ const navStore = useNavigationStore();
|
|
|
|
|
+ try {
|
|
|
|
|
+ await navStore.loadConfig({ throwOnError: true, timeout: 8000 });
|
|
|
|
|
+ console.log('[App] 导航配置加载成功,tabs:', navStore.tabList.length);
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[App] 导航配置加载失败,阻塞启动:', err?.message || err);
|
|
|
|
|
+ // 不再继续后续初始化,等用户在重试 UI 上点击重试
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const userStore = useUserStore();
|
|
const userStore = useUserStore();
|
|
|
userStore.initUser();
|
|
userStore.initUser();
|
|
|
|
|
|
|
@@ -195,12 +211,28 @@ async function triggerWechatOAuth() {
|
|
|
|
|
|
|
|
onShow(() => {
|
|
onShow(() => {
|
|
|
console.log('App Show');
|
|
console.log('App Show');
|
|
|
|
|
+ // 同步当前 tab 高亮(处理热更新/外部跳转等场景)
|
|
|
|
|
+ syncCurrentTabFromPageStack();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
onHide(() => {
|
|
onHide(() => {
|
|
|
console.log('App Hide');
|
|
console.log('App Hide');
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+// 从页面栈同步当前 tab 高亮
|
|
|
|
|
+function syncCurrentTabFromPageStack() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const navStore = useNavigationStore();
|
|
|
|
|
+ if (!navStore.isReady) return;
|
|
|
|
|
+ const pages = getCurrentPages();
|
|
|
|
|
+ const last = pages[pages.length - 1] as any;
|
|
|
|
|
+ const route = (last && last.route) ? String(last.route) : '';
|
|
|
|
|
+ if (route) navStore.syncCurrentTabByPagePath(route);
|
|
|
|
|
+ } catch (_) {
|
|
|
|
|
+ // 非 H5 环境 getCurrentPages 可能不可用
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 应用夜间模式
|
|
// 应用夜间模式
|
|
|
function applyDarkMode() {
|
|
function applyDarkMode() {
|
|
|
try {
|
|
try {
|
|
@@ -212,6 +244,91 @@ function applyDarkMode() {
|
|
|
console.error('应用夜间模式失败:', error);
|
|
console.error('应用夜间模式失败:', error);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 启动态:导航配置重试 ====================
|
|
|
|
|
+const navStore = useNavigationStore();
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 控制启动浮层 (#boot-splash) 的显示与内容。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 该 DOM 元素由 main.ts 在 #app 之外创建,目的是绕过 uni-app H5 的 page HOC
|
|
|
|
|
+ * (HOC 会吞掉 App.vue 模板里 router-view 之外的子节点,导致 v-if 不生效)。
|
|
|
|
|
+ * 我们 watch navStore.isReady / loading / loadError 来同步浮层状态。
|
|
|
|
|
+ */
|
|
|
|
|
+function renderBootSplash() {
|
|
|
|
|
+ if (typeof document === 'undefined') return;
|
|
|
|
|
+ const splash = document.getElementById('boot-splash');
|
|
|
|
|
+ if (!splash) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (navStore.isReady) {
|
|
|
|
|
+ splash.classList.add('is-hidden');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 显示 splash
|
|
|
|
|
+ splash.classList.remove('is-hidden');
|
|
|
|
|
+
|
|
|
|
|
+ const showError = !navStore.loading && !!navStore.loadError;
|
|
|
|
|
+
|
|
|
|
|
+ const loadingHtml = `
|
|
|
|
|
+ <div class="boot-spinner"></div>
|
|
|
|
|
+ <div class="boot-loading-text">正在加载导航…</div>
|
|
|
|
|
+ <div class="boot-loading-hint">${navStore.loading ? '请稍候' : '准备中'}</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ const errorHtml = `
|
|
|
|
|
+ <div class="boot-error-icon">⚠️</div>
|
|
|
|
|
+ <div class="boot-error-title">导航服务连接失败</div>
|
|
|
|
|
+ <div class="boot-error-message"></div>
|
|
|
|
|
+ <button class="boot-retry-btn" id="boot-retry-btn">重新连接</button>
|
|
|
|
|
+ <div class="boot-error-hint">网络异常时,请检查后端服务后重试</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ splash.innerHTML = `
|
|
|
|
|
+ <div class="boot-logo"><span class="boot-logo-icon">📚</span></div>
|
|
|
|
|
+ <div class="boot-app-name">AI 有声书</div>
|
|
|
|
|
+ <div class="boot-state">${showError ? errorHtml : loadingHtml}</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ if (showError) {
|
|
|
|
|
+ const msgEl = splash.querySelector('.boot-error-message');
|
|
|
|
|
+ if (msgEl) msgEl.textContent = String(navStore.loadError || '');
|
|
|
|
|
+ const btn = splash.querySelector('#boot-retry-btn');
|
|
|
|
|
+ if (btn) {
|
|
|
|
|
+ btn.addEventListener('click', () => {
|
|
|
|
|
+ handleNavRetry();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+if (typeof window !== 'undefined') {
|
|
|
|
|
+ watch(
|
|
|
|
|
+ () => [navStore.isReady, navStore.loading, navStore.loadError],
|
|
|
|
|
+ () => {
|
|
|
|
|
+ renderBootSplash();
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: true, deep: true }
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleNavRetry() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await navStore.loadConfig({ throwOnError: true, timeout: 8000 });
|
|
|
|
|
+ // 加载成功后,补充执行原本被阻塞的初始化
|
|
|
|
|
+ const userStore = useUserStore();
|
|
|
|
|
+ if (!userStore.token) {
|
|
|
|
|
+ userStore.initUser();
|
|
|
|
|
+ }
|
|
|
|
|
+ applyDarkMode();
|
|
|
|
|
+ initAnalytics();
|
|
|
|
|
+ // #ifdef H5
|
|
|
|
|
+ handleWechatOAuthCallback().then(() => checkAutoLogin());
|
|
|
|
|
+ // #endif
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ // 留 store 内的 loadError 给 UI 显示
|
|
|
|
|
+ console.error('[App] 导航配置重试失败:', err?.message || err);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
@@ -465,4 +582,160 @@ text {
|
|
|
padding-bottom: constant(safe-area-inset-bottom);
|
|
padding-bottom: constant(safe-area-inset-bottom);
|
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* ============================================================
|
|
|
|
|
+ 隐藏 uni-app 内置 tab bar(由 CustomTabBar 替代)
|
|
|
|
|
+ ============================================================ */
|
|
|
|
|
+uni-app .uni-tabbar,
|
|
|
|
|
+uni-app uni-tabbar,
|
|
|
|
|
+.uni-app--showtabbar .uni-tabbar {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============================================================
|
|
|
|
|
+ 启动阻塞 UI(导航未就绪时显示)
|
|
|
|
|
+ 使用 div 选择器确保生效
|
|
|
|
|
+ ============================================================ */
|
|
|
|
|
+.boot-splash.is-hidden {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-splash {
|
|
|
|
|
+ position: fixed !important;
|
|
|
|
|
+ top: 0 !important;
|
|
|
|
|
+ left: 0 !important;
|
|
|
|
|
+ right: 0 !important;
|
|
|
|
|
+ bottom: 0 !important;
|
|
|
|
|
+ display: flex !important;
|
|
|
|
|
+ flex-direction: column !important;
|
|
|
|
|
+ align-items: center !important;
|
|
|
|
|
+ justify-content: center !important;
|
|
|
|
|
+ background: linear-gradient(160deg, #4f46e5 0%, #7c3aed 60%, #a855f7 100%) !important;
|
|
|
|
|
+ color: #ffffff !important;
|
|
|
|
|
+ padding: 60rpx 80rpx !important;
|
|
|
|
|
+ z-index: 99999 !important;
|
|
|
|
|
+ box-sizing: border-box !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-logo {
|
|
|
|
|
+ width: 160rpx;
|
|
|
|
|
+ height: 160rpx;
|
|
|
|
|
+ border-radius: 40rpx;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.18);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ margin-bottom: 28rpx;
|
|
|
|
|
+ box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.18);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-logo-icon {
|
|
|
|
|
+ font-size: 96rpx;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-app-name {
|
|
|
|
|
+ font-size: 44rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 2rpx;
|
|
|
|
|
+ margin-bottom: 80rpx;
|
|
|
|
|
+ text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-state {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-loading {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-spinner {
|
|
|
|
|
+ width: 56rpx;
|
|
|
|
|
+ height: 56rpx;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ border: 4rpx solid rgba(255, 255, 255, 0.25);
|
|
|
|
|
+ border-top-color: #ffffff;
|
|
|
|
|
+ animation: bootSpin 0.9s linear infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes bootSpin {
|
|
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-loading-text {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-loading-hint {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.7);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-error {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-width: 560rpx;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
+ border-radius: 28rpx;
|
|
|
|
|
+ padding: 48rpx 40rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+ box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.18);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-error-icon {
|
|
|
|
|
+ font-size: 80rpx;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-error-title {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-error-message {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ margin-bottom: 32rpx;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-retry-btn {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 88rpx;
|
|
|
|
|
+ line-height: 88rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ border-radius: 44rpx;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+ box-shadow: 0 4rpx 16rpx rgba(79, 70, 229, 0.4);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-retry-btn::after {
|
|
|
|
|
+ border: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.boot-error-hint {
|
|
|
|
|
+ font-size: 20rpx;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|