12451 b553c4e519 feat: API 统一 + 设备详情补全 + H5 导航优化
- API 路由统一: 移除 /api/h5/,统一 /api/pms/ 入口
- 二进制改名: api-server -> pms-api(与服务名一致)
- nginx: 合并 /api/ + /pms/api/ 为 /api/pms/
- 新增 docs/API.md 接口文档
- 后端: CabinetDetail 增加 rssi/pow_fail/address/soc 等字段
- Redis 管道批量读取仓体实时数据(SOC/电压/电流/功率)
- DB: 所有 TIMESTAMP 列改为 DATETIME(修复类型不匹配)
- 用户管理: 新增创建时间/修改时间列,状态改用 Switch
- 角色管理/用户管理: 操作按钮改为图标
- PC 设备详情: 基本信息补全 RSSI/ICCID/电源/地址
- PC 卡片: 通道统计改用 Lucide 图标,颜色统一
- PC 二维码: canvas 渲染,含编号文字,支持右键下载
- H5: 按 uni-app 规范修正导航(navigateTo 替代 reLaunch)
- H5: 详情页加返回导航条
- H5: 扫码支持摄像头(jsQR)+ ?id= 参数
- H5: 底部 tabBar 在所有页面常显
- H5: 首页统计改为 6 项(设备/通道/空闲/充电/充满/故障)
- H5: 下拉刷新 / 上拉加载
- 产品首页: 添加业务联系
- 文档: 清理过时文件,补充缺失规范章节
2026-07-04 04:03:02 +08:00

3.5 KiB

Suspense Only Reverts to Pending on Root Node Replacement

Rule

Once resolved, <Suspense> only reverts to a pending state if the root node of the #default slot is replaced. New async dependencies nested deeper in the tree will NOT cause <Suspense> to revert to a pending state.

Why This Matters

This is a common source of confusion. Developers expect Suspense to show the fallback whenever any async component in the tree starts loading, but it only triggers for root-level changes. Nested async updates complete silently without showing any loading indicator.

Bad Code

<script setup>
import { ref } from 'vue'

const activeTab = ref('dashboard')
</script>

<template>
  <Suspense>
    <!-- This wrapper is always the same component -->
    <TabContainer>
      <!-- Tab changes won't trigger Suspense fallback! -->
      <AsyncDashboard v-if="activeTab === 'dashboard'" />
      <AsyncSettings v-else-if="activeTab === 'settings'" />
      <AsyncProfile v-else-if="activeTab === 'profile'" />
    </TabContainer>

    <template #fallback>
      Loading... <!-- Only shows on initial load -->
    </template>
  </Suspense>
</template>

Good Code

Solution 1: Use :key to Force Root Replacement

<script setup>
import { ref, defineAsyncComponent } from 'vue'

const activeTab = ref('dashboard')

const tabs = {
  dashboard: defineAsyncComponent(() => import('./Dashboard.vue')),
  settings: defineAsyncComponent(() => import('./Settings.vue')),
  profile: defineAsyncComponent(() => import('./Profile.vue'))
}
</script>

<template>
  <Suspense>
    <!-- Key change forces root replacement, triggering Suspense -->
    <component :is="tabs[activeTab]" :key="activeTab" />

    <template #fallback>
      Loading...
    </template>
  </Suspense>
</template>

Solution 2: Nested Suspense with suspensible Prop (Vue 3.3+)

<template>
  <Suspense>
    <TabContainer>
      <!-- Inner Suspense handles nested async components -->
      <Suspense suspensible>
        <AsyncDashboard v-if="activeTab === 'dashboard'" />
        <AsyncSettings v-else-if="activeTab === 'settings'" />
        <AsyncProfile v-else-if="activeTab === 'profile'" />

        <template #fallback>
          <TabLoadingSpinner />
        </template>
      </Suspense>
    </TabContainer>

    <template #fallback>
      <PageLoadingSkeleton />
    </template>
  </Suspense>
</template>

Solution 3: Manual Loading State for Nested Content

<script setup>
import { ref, watch } from 'vue'

const activeTab = ref('dashboard')
const isTabLoading = ref(false)

watch(activeTab, () => {
  isTabLoading.value = true
})

const onTabLoaded = () => {
  isTabLoading.value = false
}
</script>

<template>
  <Suspense>
    <TabContainer>
      <div v-if="isTabLoading" class="tab-loading">
        Loading tab...
      </div>
      <AsyncTab
        v-else
        :tab="activeTab"
        @vue:mounted="onTabLoaded"
      />
    </TabContainer>

    <template #fallback>
      Initial loading...
    </template>
  </Suspense>
</template>

Key Points

  1. Suspense tracks the root node identity, not the entire subtree
  2. Use :key to force root node replacement when you need Suspense to re-trigger
  3. For Vue 3.3+, nested <Suspense> with suspensible prop handles deeply nested async components
  4. Consider manual loading states for fine-grained control over nested content
  5. Without the suspensible prop, inner Suspense is treated as sync and causes multiple patching cycles

References