refactor: 租户切换器useTenantOptions收拢三页

新建 composables/useTenantOptions.ts:会话/配置/统计三页各自的
tenants/tenantsLoading/tenantsError/loadTenants 拷贝收拢为单一
composable。两种形态:
- 默认(会话/配置页):loadTenants 成功选中首个租户 +
  bootstrapTenantContext()(原两页 onMounted 公共体:超管拉列表/
  租户管理员固定自己/无 slug 防御不动),选中后的 watch 仍留各页
  (重置逻辑页面各异);
- selectFirst: false(统计页):纯下拉数据源,activeSlug 以
  tenantFilter 别名解构('' = 全部租户合并视图),模板零改动。

dashboard 控制台是租户卡片墙(非下拉切换),不在此列。
composable 显式 import vue API——vitest 不跑 unplugin-auto-import。
新增 8 个 composable 测试(mock api/auth store,不装 pinia)。

vitest 164 passed(156+8);build 通过;dts 行尾漂移已还原。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-14 10:36:30 +08:00
parent a50ed866ed
commit b02326036f
5 changed files with 245 additions and 77 deletions
+13 -29
View File
@@ -488,7 +488,8 @@
<script setup lang="ts">
import { useAuthStore } from '@/store/modules/auth'
import { fetchTenantDetail, fetchTenantList, type TenantDetailResponse, type TenantListItem } from '@/api/tenants'
import { fetchTenantDetail, type TenantDetailResponse } from '@/api/tenants'
import { useTenantOptions } from '@/composables/useTenantOptions'
import {
fetchTenantConfig,
normalizeBrandGroup,
@@ -560,31 +561,19 @@
() => GROUP_NAV.find((g) => g.key === activeGroup.value)?.label ?? ''
)
// ---- 租户上下文(超管可切换;租户管理员固定自己——会话页同款) ------------------------
// ---- 租户上下文(超管可切换;租户管理员固定自己——useTenantOptions 收拢) ----------------
const authStore = useAuthStore()
const isSuper = computed(() => authStore.role === 'super')
const tenants = ref<TenantListItem[]>([])
const tenantsLoading = ref(false)
const tenantsError = ref('')
const activeSlug = ref('')
async function loadTenants(): Promise<void> {
tenantsLoading.value = true
tenantsError.value = ''
try {
const res = await fetchTenantList()
tenants.value = res.items
if (res.items.length > 0) {
activeSlug.value = res.items[0].slug
}
} catch (error) {
tenantsError.value = error instanceof Error ? error.message : '租户列表加载失败'
} finally {
tenantsLoading.value = false
}
}
const {
tenants,
tenantsLoading,
tenantsError,
activeSlug,
loadTenants,
isSuper,
bootstrapTenantContext
} = useTenantOptions()
// ---- 配置状态(品牌表单 + 整份快照——Task 7b 起提示词/词表 pane 消费快照组) ----------
@@ -902,12 +891,7 @@
})
onMounted(() => {
if (isSuper.value) {
loadTenants() // 成功后置 activeSlug → watch 触发首载
} else if (authStore.tenantSlug) {
activeSlug.value = authStore.tenantSlug // watch 触发首载
}
// 租户管理员无 tenantSlug:模板已出防御告警,不加载
bootstrapTenantContext()
})
</script>
+12 -30
View File
@@ -370,7 +370,7 @@
<script setup lang="ts">
import { ElMessageBox } from 'element-plus'
import { useAuthStore } from '@/store/modules/auth'
import { fetchTenantList, type TenantListItem } from '@/api/tenants'
import { useTenantOptions } from '@/composables/useTenantOptions'
import {
INTENT_OPTIONS,
PUSH_STATUS_OPTIONS,
@@ -394,30 +394,17 @@
defineOptions({ name: 'ConversationList' })
const authStore = useAuthStore()
const isSuper = computed(() => authStore.role === 'super')
// ---- 租户上下文(超管可切换;租户管理员固定自己) --------------------------------
const tenants = ref<TenantListItem[]>([])
const tenantsLoading = ref(false)
const tenantsError = ref('')
const activeSlug = ref('')
async function loadTenants(): Promise<void> {
tenantsLoading.value = true
tenantsError.value = ''
try {
const res = await fetchTenantList()
tenants.value = res.items
// 默认选中第一个租户(选中后由 watch(activeSlug) 触发首载)
if (res.items.length > 0) {
activeSlug.value = res.items[0].slug
}
} catch (error) {
tenantsError.value = error instanceof Error ? error.message : '租户列表加载失败'
} finally {
tenantsLoading.value = false
}
}
// ---- 租户上下文(超管可切换;租户管理员固定自己——useTenantOptions 收拢) --------------
const {
tenants,
tenantsLoading,
tenantsError,
activeSlug,
loadTenants,
isSuper,
bootstrapTenantContext
} = useTenantOptions()
// ---- 页签 -----------------------------------------------------------------------
const activeTab = ref<'list' | 'pushLogs'>('list')
@@ -663,12 +650,7 @@
})
onMounted(() => {
if (isSuper.value) {
loadTenants() // 成功后置 activeSlug → watch 触发首载
} else if (authStore.tenantSlug) {
activeSlug.value = authStore.tenantSlug // watch 触发首载
}
// 租户管理员无 tenantSlug:模板已出防御告警,不加载
bootstrapTenantContext()
})
</script>
+11 -18
View File
@@ -198,7 +198,7 @@
import type { EChartsOption } from '@/plugins/echarts'
import { useAuthStore } from '@/store/modules/auth'
import { fetchPlatformStats, fetchTenantStats, type StatsResponse } from '@/api/stats'
import { fetchTenantList, type TenantListItem } from '@/api/tenants'
import { useTenantOptions } from '@/composables/useTenantOptions'
import { conversationSourceLabel } from '@/api/conversations'
import { STATS_RANGE_OPTIONS, resolveStatsRange, type StatsRangeFilter } from '@/utils/statsRange'
import {
@@ -215,7 +215,6 @@
defineOptions({ name: 'StatsOverview' })
const authStore = useAuthStore()
const isSuper = computed(() => authStore.role === 'super')
// ---- 筛选:时间档 + 自定义区间 + 租户(超管) --------------------------------------
@@ -224,22 +223,16 @@
/** 当前筛选 → 查询参数;null = 自定义区间不完整(不发请求,保留上一份数据) */
const params = computed(() => resolveStatsRange(rangeFilter.value, customRange.value))
const tenantFilter = ref('') // '' = 全部租户合并视图
const tenants = ref<TenantListItem[]>([])
const tenantsLoading = ref(false)
const tenantsError = ref('')
async function loadTenants(): Promise<void> {
tenantsLoading.value = true
tenantsError.value = ''
try {
tenants.value = (await fetchTenantList()).items
} catch (error) {
tenantsError.value = error instanceof Error ? error.message : '租户列表加载失败'
} finally {
tenantsLoading.value = false
}
}
// 纯下拉数据源(selectFirst: false):tenantFilter = activeSlug 改名,
// '' = 全部租户合并视图,切换只进 reload 的查询参数——useTenantOptions 收拢
const {
tenants,
tenantsLoading,
tenantsError,
loadTenants,
isSuper,
activeSlug: tenantFilter
} = useTenantOptions({ selectFirst: false })
// ---- 统计取数(epoch 防筛选快速切换的过期响应竞写) --------------------------------