Sfoglia il codice sorgente

fix: 补提交 App.vue/main.ts 导航启动逻辑 + 修部署脚本前端路径

根因:动态导航系统的核心驱动文件未提交,导致服务器构建出的产物
mine 页菜单为空、navigation store 被错误打进 mine chunk。

- App.vue: onLaunch 调 useNavigationStore().loadConfig() 加载导航配置;
  boot-splash 重试 UI;onShow 同步 tab 高亮。这个静态引用同时保证
  navigation store 被打进主 chunk(全局单例),而非 mine 页 chunk。
- main.ts: 在 #app 外创建 boot-splash DOM(绕过 uni H5 page HOC)
- deploy-auto.sh: FRONTEND_DIR 由 /data/ai/audio/frontend 修正为
  /data/ai/frontend(nginx 实际 root),修复 rsync mkdir 失败

Co-Authored-By: Claude <noreply@anthropic.com>
MyFramework User 2 mesi fa
parent
commit
8750880c77
3 ha cambiato i file con 303 aggiunte e 3 eliminazioni
  1. 274 1
      my-uniapp-vue3/src/App.vue
  2. 28 1
      my-uniapp-vue3/src/main.ts
  3. 1 1
      server/deploy-auto.sh

+ 274 - 1
my-uniapp-vue3/src/App.vue

@@ -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>

+ 28 - 1
my-uniapp-vue3/src/main.ts

@@ -7,7 +7,34 @@ import { useUserStore } from './store/user';
 import VConsole from 'vconsole';
 import VConsole from 'vconsole';
 // #endif
 // #endif
 
 
+/**
+ * 在 #app 之外创建启动态浮层 DOM(独立于 Vue 渲染树),
+ * App.vue 通过 watch 控制其显示/隐藏/内容。
+ *
+ * 原因:uni-app H5 的 page HOC 会吞掉 App.vue 模板里非 router-view 子节点,
+ *       把 splash 放在 Vue 渲染树里会导致 v-if 完全不生效。
+ */
+function ensureBootSplash() {
+  if (typeof document === 'undefined') return;
+  if (document.getElementById('boot-splash')) return;
+  const div = document.createElement('div');
+  div.id = 'boot-splash';
+  div.className = 'boot-splash';
+  div.style.display = 'none';
+  document.body.appendChild(div);
+}
+
+// H5 平台下立即创建(其它平台不需要此浮层)
+// #ifdef H5
+ensureBootSplash();
+// #endif
+
 export function createApp() {
 export function createApp() {
+  // 再次确保(main.ts 在不同入口可能被多次调用)
+  // #ifdef H5
+  ensureBootSplash();
+  // #endif
+
   const app = createSSRApp(App);
   const app = createSSRApp(App);
   const pinia = createPinia();
   const pinia = createPinia();
 
 
@@ -26,4 +53,4 @@ export function createApp() {
     app,
     app,
     pinia,
     pinia,
   };
   };
-}
+}

+ 1 - 1
server/deploy-auto.sh

@@ -6,7 +6,7 @@ set -e
 DEPLOY_LOG="/tmp/deploy-auto.log"
 DEPLOY_LOG="/tmp/deploy-auto.log"
 PROJECT_DIR="/data/ai/audio_codebuddy"
 PROJECT_DIR="/data/ai/audio_codebuddy"
 BACKEND_PORT="3100"
 BACKEND_PORT="3100"
-FRONTEND_DIR="/data/ai/audio/frontend"
+FRONTEND_DIR="/data/ai/frontend"
 
 
 log() {
 log() {
     echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$DEPLOY_LOG"
     echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$DEPLOY_LOG"