滑动续期与多标签同步、列表报错提示、分页排序体验改进
This commit is contained in:
@@ -11,6 +11,7 @@ import { computed, h, ref, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { deleteBootVolume, listBootVolumes } from '@/api/bootVolumes'
|
||||
import { ApiError } from '@/api/request'
|
||||
import CompartmentSelect from '@/components/CompartmentSelect.vue'
|
||||
import ConfirmPop from '@/components/ConfirmPop.vue'
|
||||
import DeadPlaceholder from '@/components/DeadPlaceholder.vue'
|
||||
@@ -33,6 +34,10 @@ interface BvRow extends BootVolume {
|
||||
const scope = useScopeStore()
|
||||
const message = useToast()
|
||||
|
||||
function showListError(error: unknown) {
|
||||
if (error instanceof ApiError && error.status === 502) message.error(error.message)
|
||||
}
|
||||
|
||||
/** 当前作用域租户失联时整页替换占位,不发起资源请求 */
|
||||
const isDead = computed(() => scope.currentConfig?.aliveStatus === 'dead')
|
||||
|
||||
@@ -47,7 +52,8 @@ const rows = useAsync<BvRow[]>(async () => {
|
||||
scope.region || undefined,
|
||||
)
|
||||
return (list ?? []).map((item) => ({ ...item, cfgId: cfg.id }))
|
||||
} catch {
|
||||
} catch (error) {
|
||||
showListError(error)
|
||||
return []
|
||||
}
|
||||
}, false)
|
||||
|
||||
@@ -82,6 +82,25 @@ const consoles = useAsync(() =>
|
||||
listConsoleConnections(cfgId.value, instanceId.value, region.value),
|
||||
)
|
||||
|
||||
/** 数据在手时的加载失败走 toast、页面继续展示旧数据;首载失败仍由页内占位卡承接。
|
||||
* 轮询期间同一错误反复出现只提示一次,成功一次后解除,避免 toast 刷屏 */
|
||||
let instanceErrorToasted = ''
|
||||
watch(
|
||||
() => instance.error.value,
|
||||
(err) => {
|
||||
if (err && instance.data.value && err !== instanceErrorToasted) {
|
||||
instanceErrorToasted = err
|
||||
message.error('实例刷新失败', err)
|
||||
}
|
||||
},
|
||||
)
|
||||
watch(
|
||||
() => instance.data.value,
|
||||
() => {
|
||||
instanceErrorToasted = ''
|
||||
},
|
||||
)
|
||||
|
||||
/** 按 imageId 补镜像名称,查询失败时回退 OCID 展示 */
|
||||
const image = useAsync(
|
||||
() => {
|
||||
@@ -432,7 +451,7 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
|
||||
>
|
||||
<NSpin size="small" />
|
||||
</div>
|
||||
<div v-else-if="instance.error.value" class="panel">
|
||||
<div v-else-if="instance.error.value && !instance.data.value" class="panel">
|
||||
<EmptyCard title="实例加载失败" :note="instance.error.value" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
terminatingInstances,
|
||||
terminatingKey,
|
||||
} from '@/api/instances'
|
||||
import { ApiError } from '@/api/request'
|
||||
import CompartmentSelect from '@/components/CompartmentSelect.vue'
|
||||
import ConfirmPop from '@/components/ConfirmPop.vue'
|
||||
import DeadPlaceholder from '@/components/DeadPlaceholder.vue'
|
||||
@@ -39,6 +40,10 @@ interface InstanceRow extends Instance {
|
||||
|
||||
const message = useToast()
|
||||
|
||||
function showListError(error: unknown) {
|
||||
if (error instanceof ApiError && error.status === 502) message.error(error.message)
|
||||
}
|
||||
|
||||
/** 租户 / 区域 / 区间来自侧栏全局作用域,页内只做区间切换与搜索 */
|
||||
const scope = useScopeStore()
|
||||
/** ≤767px 表格换卡片列表(设计稿 B2) */
|
||||
@@ -54,7 +59,8 @@ const rows = useAsync<InstanceRow[]>(async () => {
|
||||
try {
|
||||
const list = await listInstances(cfg.id, scope.region || undefined, compartmentId)
|
||||
return list.map((item) => ({ ...item, cfgId: cfg.id, cfgAlias: cfg.alias }))
|
||||
} catch {
|
||||
} catch (error) {
|
||||
showListError(error)
|
||||
return []
|
||||
}
|
||||
}, false)
|
||||
|
||||
@@ -10,6 +10,7 @@ import { computed, h, ref, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { listSubnets, listVcns } from '@/api/networks'
|
||||
import { ApiError } from '@/api/request'
|
||||
import CompartmentSelect from '@/components/CompartmentSelect.vue'
|
||||
import DeadPlaceholder from '@/components/DeadPlaceholder.vue'
|
||||
import LifecycleBadge from '@/components/LifecycleBadge.vue'
|
||||
@@ -18,6 +19,7 @@ import ReservedIpPanel from '@/components/network/ReservedIpPanel.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import { useRegionAlias } from '@/composables/useRegionAlias'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useScopeStore } from '@/stores/scope'
|
||||
import type { Vcn } from '@/types/api'
|
||||
|
||||
@@ -30,8 +32,13 @@ interface VcnRow extends Vcn {
|
||||
/** 租户 / 区域 / 区间来自侧栏全局作用域 */
|
||||
const scope = useScopeStore()
|
||||
const { alias: regionAlias } = useRegionAlias()
|
||||
const message = useToast()
|
||||
const showCreate = ref(false)
|
||||
|
||||
function showListError(error: unknown) {
|
||||
if (error instanceof ApiError && error.status === 502) message.error(error.message)
|
||||
}
|
||||
|
||||
/** 当前作用域租户失联时整页替换占位,不发起资源请求 */
|
||||
const isDead = computed(() => scope.currentConfig?.aliveStatus === 'dead')
|
||||
|
||||
@@ -53,7 +60,8 @@ const rows = useAsync<VcnRow[]>(async () => {
|
||||
}
|
||||
}),
|
||||
)
|
||||
} catch {
|
||||
} catch (error) {
|
||||
showListError(error)
|
||||
return []
|
||||
}
|
||||
}, false)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NSelect, NSpin, NTabPane, NTabs, NTooltip } from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { deleteConfig, getConfig, verifyConfig } from '@/api/configs'
|
||||
@@ -43,6 +43,32 @@ const heroExpanded = ref(false)
|
||||
|
||||
const config = useAsync(() => getConfig(cfgId.value))
|
||||
|
||||
function refreshAfterKeyActivation() {
|
||||
void config.run()
|
||||
void scope.refreshConfigs()
|
||||
}
|
||||
|
||||
/** 新 key 激活后的 OCI 公钥全球传播窗口:窗口内显示提示横幅,到点自动消失 */
|
||||
const KEY_PROPAGATION_MS = 15 * 60 * 1000
|
||||
const propagationNow = ref(Date.now())
|
||||
let propagationTimer = 0
|
||||
const keyPropagating = computed(() => {
|
||||
const at = config.data.value?.keyActivatedAt
|
||||
return !!at && propagationNow.value - new Date(at).getTime() < KEY_PROPAGATION_MS
|
||||
})
|
||||
watch(
|
||||
() => config.data.value?.keyActivatedAt,
|
||||
(at) => {
|
||||
window.clearTimeout(propagationTimer)
|
||||
if (!at) return
|
||||
const left = new Date(at).getTime() + KEY_PROPAGATION_MS - Date.now()
|
||||
if (left > 0)
|
||||
propagationTimer = window.setTimeout(() => (propagationNow.value = Date.now()), left + 1000)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
onUnmounted(() => window.clearTimeout(propagationTimer))
|
||||
|
||||
/** 失联/暂停租户各 tab 拉不到云端数据,内容区整体换占位 */
|
||||
const isBlocked = computed(() =>
|
||||
['dead', 'suspended'].includes(config.data.value?.aliveStatus ?? ''),
|
||||
@@ -158,6 +184,26 @@ async function remove() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="keyPropagating"
|
||||
class="mt-3 flex items-start gap-2 rounded-lg border border-warn/30 bg-warn/10 px-3 py-2.5 text-xs leading-relaxed"
|
||||
>
|
||||
<svg
|
||||
class="mt-0.5 h-[14px] w-[14px] flex-none text-warn"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 8v4M12 16h.01" />
|
||||
</svg>
|
||||
<span>
|
||||
新 API Key 正在全球生效中(约需数分钟):期间部分区域可能间歇报
|
||||
NotAuthenticated,属预期现象,请稍后重试
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="isMobile"
|
||||
class="mt-2 flex cursor-pointer items-center gap-1 text-xs text-ink-3"
|
||||
@@ -231,7 +277,13 @@ async function remove() {
|
||||
<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" />
|
||||
<UsersTab
|
||||
v-else-if="tab === 'users'"
|
||||
:key="`users-${domainId ?? ''}`"
|
||||
:cfg-id="cfgId"
|
||||
:domain-id="domainId"
|
||||
@key-activated="refreshAfterKeyActivation"
|
||||
/>
|
||||
<CostTab v-else-if="tab === 'cost'" :cfg-id="cfgId" />
|
||||
<InvoiceTab v-else-if="tab === 'invoice'" :cfg-id="cfgId" />
|
||||
<SubsTab v-else-if="tab === 'subs'" :cfg-id="cfgId" :account-type="config.data.value.accountType" />
|
||||
|
||||
@@ -16,6 +16,7 @@ import { parseSnatch, scopeCfgIds, STATUS_META, TYPE_LABEL } from '@/components/
|
||||
import { aliveMeta } from '@/composables/useAliveStatus'
|
||||
import { fmtRelative, fmtTime } from '@/composables/useFormat'
|
||||
import { useIsMobile } from '@/composables/useIsMobile'
|
||||
import { usePersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||
import { useRegionAlias } from '@/composables/useRegionAlias'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import { useScopeStore } from '@/stores/scope'
|
||||
@@ -81,9 +82,10 @@ const filtered = computed(() => {
|
||||
})
|
||||
|
||||
/** 客户端分页:字面量对象会在重渲染时被重建导致状态丢失,须用 reactive 受控 */
|
||||
const tenantsPageSize = usePersistedPageSize('tenants')
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSize: tenantsPageSize.load(),
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
onUpdatePage: (p: number) => {
|
||||
@@ -92,6 +94,7 @@ const pagination = reactive({
|
||||
onUpdatePageSize: (ps: number) => {
|
||||
pagination.pageSize = ps
|
||||
pagination.page = 1
|
||||
tenantsPageSize.save(ps)
|
||||
},
|
||||
})
|
||||
/** 搜索或分组过滤变化后回第 1 页,避免停留在越界页码 */
|
||||
|
||||
Reference in New Issue
Block a user