初始提交:OCI 面板前端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:29 +08:00
commit db45a669e3
135 changed files with 25220 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
<script setup lang="ts">
import { NButton, NPopconfirm, NSpin, NTabPane, NTabs, NTooltip, useMessage } from 'naive-ui'
import { computed, ref } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { deleteConfig, getConfig, verifyConfig } from '@/api/configs'
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 { fmtTime } from '@/composables/useFormat'
import { useRegionAlias } from '@/composables/useRegionAlias'
import { useAsync } from '@/composables/useAsync'
import { useScopeStore } from '@/stores/scope'
const scope = useScopeStore()
const route = useRoute()
const router = useRouter()
const message = useMessage()
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 isDead = computed(() => config.data.value?.aliveStatus === 'dead')
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('已删除(仅移除本面板配置,不影响云端资源)')
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="isDead && config.data.value.lastError"
trigger="hover"
placement="top-start"
>
<template #trigger>
<StatusBadge kind="term" label="失联" />
</template>
<div class="max-w-80 text-xs">{{ config.data.value.lastError }}</div>
</NTooltip>
<StatusBadge
v-else
:kind="config.data.value.aliveStatus === 'alive' ? 'run' : 'term'"
:label="config.data.value.aliveStatus === 'alive' ? '存活' : '失联'"
/>
<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>
删除{{ config.data.value.alias }}仅移除本面板配置与快照不影响云端资源
</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="其他" />
</NTabs>
</div>
<template v-if="config.data.value">
<DeadPlaceholder v-if="isDead" :last-error="config.data.value.lastError" />
<template v-else>
<QuotaTab v-if="tab === 'quota'" :cfg-id="cfgId" />
<IdpTab v-else-if="tab === 'idp'" :cfg-id="cfgId" />
<UsersTab v-else-if="tab === 'users'" :cfg-id="cfgId" />
<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 :cfg-id="cfgId" />
</template>
</template>
</div>
</template>
<style scoped>
.tenant-tabs :deep(.n-tabs-nav) {
overflow-x: auto;
}
</style>