Files
oci-portal-dash/src/views/TenantDetailView.vue
T
wangdefa 2ea5b73de2
CI / test (push) Successful in 25s
Release / release (push) Successful in 26s
移除告警规则界面、恢复 Chat 端点并优化列表
2026-07-13 10:08:08 +08:00

216 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { NButton, NPopconfirm, NSelect, NSpin, NTabPane, NTabs, NTooltip } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { deleteConfig, getConfig, verifyConfig } from '@/api/configs'
import { listIdentityDomains } from '@/api/tenant'
import AccountTypeBadge from '@/components/AccountTypeBadge.vue'
import AuditLogTab from '@/components/tenant/AuditLogTab.vue'
import CostTab from '@/components/tenant/CostTab.vue'
import DeadPlaceholder from '@/components/DeadPlaceholder.vue'
import IdpTab from '@/components/tenant/IdpTab.vue'
import MiscTab from '@/components/tenant/MiscTab.vue'
import OcidText from '@/components/OcidText.vue'
import QuotaTab from '@/components/tenant/QuotaTab.vue'
import StatusBadge from '@/components/StatusBadge.vue'
import SubsTab from '@/components/tenant/SubsTab.vue'
import UsersTab from '@/components/tenant/UsersTab.vue'
import { aliveMeta } from '@/composables/useAliveStatus'
import { fmtTime } from '@/composables/useFormat'
import { useRegionAlias } from '@/composables/useRegionAlias'
import { useAsync } from '@/composables/useAsync'
import { useScopeStore } from '@/stores/scope'
import { useToast } from '@/composables/useToast'
const scope = useScopeStore()
const route = useRoute()
const router = useRouter()
const message = useToast()
const { alias: regionAlias } = useRegionAlias()
const cfgId = computed(() => Number(route.params.id))
const tab = ref('quota')
const verifying = ref(false)
const config = useAsync(() => getConfig(cfgId.value))
/** 失联/暂停租户各 tab 拉不到云端数据,内容区整体换占位 */
const isBlocked = computed(() =>
['dead', 'suspended'].includes(config.data.value?.aliveStatus ?? ''),
)
// ---- 身份域:用户 / IDP / 其他 三个 tab 共享的域上下文 ----
// 拉取失败(无权限/老租户)静默降级:不显示选择器,各 tab 走缺省 Default 路径
const domains = useAsync(async () => {
try {
return await listIdentityDomains(cfgId.value)
} catch {
return []
}
})
const domainId = ref<string | undefined>(undefined)
watch(domains.data, (list) => {
if (!list?.length) return
const preferred = list.find((d) => d.displayName === 'Default') ?? list[0]
domainId.value ??= preferred.id
})
const domainOptions = computed(
() => domains.data.value?.map((d) => ({ label: d.displayName, value: d.id })) ?? [],
)
/** 域选择器只在多域租户的域相关 tab 显示 */
const showDomainPicker = computed(
() => (domains.data.value?.length ?? 0) > 1 && ['users', 'idp', 'misc'].includes(tab.value),
)
async function verify() {
verifying.value = true
try {
const result = await verifyConfig(cfgId.value)
const changed = Object.keys(result.changes).length
message.success(changed ? `测活完成,${changed} 个字段有变更` : '测活完成,无变更')
void config.run()
void scope.refreshConfigs()
} catch (e) {
message.error(e instanceof Error ? e.message : '测活失败')
} finally {
verifying.value = false
}
}
async function remove() {
try {
await deleteConfig(cfgId.value)
message.success('已删除租户及关联本地数据,OCI 云端资源未受影响')
void scope.refreshConfigs()
void router.push({ name: 'tenants' })
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败')
}
}
</script>
<template>
<div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
<div
v-if="config.loading.value && !config.data.value"
class="panel flex items-center justify-center py-20"
>
<NSpin size="small" />
</div>
<div
v-else-if="config.error.value"
class="panel px-5 py-10 text-center text-[13px] text-ink-3"
>
{{ config.error.value }}
</div>
<div v-if="config.data.value" class="panel px-5 pt-4 pb-0">
<RouterLink
class="mb-2 inline-flex items-center gap-1.5 text-[13px] text-ink-3 hover:text-ink"
:to="{ name: 'tenants' }"
>
返回租户列表
</RouterLink>
<div class="flex flex-wrap items-center gap-3">
<h1 class="page-title">{{ config.data.value.alias }}</h1>
<NTooltip
v-if="isBlocked && config.data.value.lastError"
trigger="hover"
placement="top-start"
>
<template #trigger>
<StatusBadge
:kind="aliveMeta(config.data.value.aliveStatus).kind"
:label="aliveMeta(config.data.value.aliveStatus).label"
/>
</template>
<div class="max-w-80 text-xs">{{ config.data.value.lastError }}</div>
</NTooltip>
<StatusBadge
v-else
:kind="aliveMeta(config.data.value.aliveStatus).kind"
:label="aliveMeta(config.data.value.aliveStatus).label"
/>
<AccountTypeBadge :type="config.data.value.accountType" />
<div class="flex-1" />
<NButton size="small" :loading="verifying" @click="verify">立即测活</NButton>
<NPopconfirm negative-text="取消" positive-text="删除" @positive-click="remove">
<template #trigger>
<NButton size="small" type="error" ghost>删除租户</NButton>
</template>
<div class="max-w-80 text-[13px] leading-relaxed">
<div class="font-medium">删除{{ config.data.value.alias }}」?</div>
<div class="mt-1 text-xs text-ink-3">
将清理面板内关联数据后台任务测活 / 成本快照回传事件Webhook 密钥与 AI 渠道
</div>
<div class="mt-0.5 text-xs text-ink-3">
OCI 云端资源不受影响云端日志回传链路请先在其他页销毁
</div>
</div>
</NPopconfirm>
</div>
<div class="mt-3 grid grid-cols-4 gap-4 pb-4 max-lg:grid-cols-2 max-md:grid-cols-1">
<div class="min-w-0">
<div class="text-xs text-ink-3">租户名</div>
<div class="mono mt-0.5 truncate text-[13px]">{{ config.data.value.tenancyName }}</div>
</div>
<div class="min-w-0">
<div class="text-xs text-ink-3">Tenancy OCID</div>
<div class="mt-0.5"><OcidText :ocid="config.data.value.tenancyOcid" /></div>
</div>
<div class="min-w-0">
<div class="text-xs text-ink-3">主区域</div>
<div class="mt-0.5 text-[13px]">{{ regionAlias(config.data.value.region) }}</div>
</div>
<div class="min-w-0">
<div class="text-xs text-ink-3">导入时间</div>
<div class="mt-0.5 text-[13px]">{{ fmtTime(config.data.value.createdAt).slice(0, 10) }}</div>
</div>
</div>
<NTabs v-model:value="tab" type="line" class="tenant-tabs">
<NTabPane name="quota" tab="配额" />
<NTabPane name="idp" tab="IDP" />
<NTabPane name="users" tab="用户" />
<NTabPane name="cost" tab="成本" />
<NTabPane name="subs" tab="订阅" />
<NTabPane name="audit" tab="审计日志" />
<NTabPane name="misc" tab="其他" />
<template #suffix>
<!-- 多域租户:用户/IDP/其他 tab 按选中域读写 -->
<div v-if="showDomainPicker" class="w-64 max-md:w-44">
<NSelect v-model:value="domainId" size="small" :options="domainOptions" placeholder="身份域" />
</div>
</template>
</NTabs>
</div>
<template v-if="config.data.value">
<DeadPlaceholder
v-if="isBlocked"
:last-error="config.data.value.lastError"
:suspended="config.data.value.aliveStatus === 'suspended'"
/>
<template v-else>
<QuotaTab v-if="tab === 'quota'" :cfg-id="cfgId" />
<!-- 域相关 tab domainId key:切域整组件重挂载,状态与数据全量刷新 -->
<IdpTab v-else-if="tab === 'idp'" :key="`idp-${domainId ?? ''}`" :cfg-id="cfgId" :domain-id="domainId" />
<UsersTab v-else-if="tab === 'users'" :key="`users-${domainId ?? ''}`" :cfg-id="cfgId" :domain-id="domainId" />
<CostTab v-else-if="tab === 'cost'" :cfg-id="cfgId" />
<SubsTab v-else-if="tab === 'subs'" :cfg-id="cfgId" :account-type="config.data.value.accountType" />
<AuditLogTab v-else-if="tab === 'audit'" :cfg-id="cfgId" />
<MiscTab v-else :key="`misc-${domainId ?? ''}`" :cfg-id="cfgId" :domain-id="domainId" />
</template>
</template>
</div>
</template>
<style scoped>
.tenant-tabs :deep(.n-tabs-nav) {
overflow-x: auto;
}
</style>