- 保存请求执行期间禁止切换:organization store 新增 saving/busy 与 beginSaving/endSaving,切换守卫与切换器禁用条件均由 switching 改为 busy - 切换提交时 abort 旧读请求:commitOrganizationId 内调用 abortReadRequests, 不再只在 resetOrganizationContext 里取消 - validateCandidate 仅将 403 视为候选失效,网络异常/500 等向上抛出; initialize 置 loadError,App 据此区分“加载失败”与“请先选择公司”并支持重试 - 补竞态与中止测试;需要写请求时旧响应仍被版本丢弃的用例改用 POST 覆盖
102 lines
3.1 KiB
Vue
102 lines
3.1 KiB
Vue
<template>
|
||
<div id="app">
|
||
<template v-if="authStore.isLoggedIn">
|
||
<AppHeader />
|
||
<div class="app-container">
|
||
<AppSidebar :active-menu="currentRoute" @menu-select="handleMenuSelect" />
|
||
<main class="app-main">
|
||
<!-- 公司尚未确定时用屏障占位,不挂载业务页面,避免组件 onMounted 发出缺少组织头的请求 -->
|
||
<div v-if="organizationBarrier" class="organization-barrier">
|
||
<el-empty v-if="organizationStore.loadError" description="公司信息加载失败,请重试">
|
||
<el-button type="primary" :loading="retrying" @click="retryInitialize">
|
||
重新加载
|
||
</el-button>
|
||
</el-empty>
|
||
<el-empty v-else description="请先选择要操作的公司" />
|
||
</div>
|
||
<router-view v-else :key="viewKey" />
|
||
</main>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<router-view />
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { useAuthStore } from './stores/auth'
|
||
import { useOrganizationStore } from './stores/organization'
|
||
import AppHeader from './components/layout/AppHeader.vue'
|
||
import AppSidebar from './components/layout/AppSidebar.vue'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const authStore = useAuthStore()
|
||
const organizationStore = useOrganizationStore()
|
||
|
||
// 加载失败后用户手动重试;重试本身不应被误认为公司切换,故用独立标记。
|
||
const retrying = ref(false)
|
||
|
||
const currentRoute = computed(() => route.path)
|
||
|
||
// 组织级页面在公司未确定时不挂载:由 AppHeader 的切换器引导用户先选公司。
|
||
const organizationBarrier = computed(
|
||
() => route.meta.organizationScoped === true && organizationStore.currentOrganizationId == null
|
||
)
|
||
|
||
// 组织级页面用“公司 ID + 上下文版本”作为挂载 key:切换公司即重新挂载,旧页面状态随之清空。
|
||
// 全局的用户、组织、角色管理页不作为公司数据页,key 固定、不随公司变化。
|
||
const viewKey = computed(() => {
|
||
if (route.meta.organizationScoped !== true) return 'global'
|
||
return `organization-${organizationStore.currentOrganizationId}-${organizationStore.contextVersion}`
|
||
})
|
||
|
||
function handleMenuSelect(path) {
|
||
router.push(path)
|
||
}
|
||
|
||
// 手动重试公司初始化:成功后 currentOrganizationId 变为非空,屏障自动撤下;
|
||
// 仍失败时保持“加载失败”提示,等待用户再次重试。
|
||
async function retryInitialize() {
|
||
retrying.value = true
|
||
try {
|
||
await organizationStore.initialize()
|
||
} catch {
|
||
// 错误已由 axios 拦截器提示,且 store 已置 loadError,这里不再重复弹提示。
|
||
} finally {
|
||
retrying.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
#app {
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.app-container {
|
||
display: flex;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
.app-sidebar {
|
||
width: 200px;
|
||
flex-shrink: 0;
|
||
}
|
||
.app-main {
|
||
flex: 1;
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
}
|
||
.organization-barrier {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
}
|
||
</style>
|