发布 0.1.0:通知渠道、告警规则与多项体验修复
CI / test (push) Successful in 27s
Release / release (push) Successful in 26s

This commit is contained in:
Wang Defa
2026-07-10 17:31:19 +08:00
parent 9d0c116ce3
commit 5464d2dfea
68 changed files with 1604 additions and 359 deletions
+3 -2
View File
@@ -1,13 +1,14 @@
<script setup lang="ts">
import { NButton, useMessage } from 'naive-ui'
import { NButton } from 'naive-ui'
import { computed, ref } from 'vue'
import { listAiChannels, listAiKeys, listAiModels } from '@/api/aigateway'
import AiChannelPanel from '@/components/ai/AiChannelPanel.vue'
import AiKeyPanel from '@/components/ai/AiKeyPanel.vue'
import { useAsync } from '@/composables/useAsync'
import { useToast } from '@/composables/useToast'
const message = useMessage()
const message = useToast()
const keys = useAsync(listAiKeys)
const channels = useAsync(listAiChannels)
+2 -2
View File
@@ -7,7 +7,6 @@ import {
NInputGroupLabel,
NPopconfirm,
NSelect,
useMessage,
type DataTableColumns,
} from 'naive-ui'
import { computed, h, ref, watch } from 'vue'
@@ -24,6 +23,7 @@ import { fmtTime, shortAd, truncOcid, vpusLabel } from '@/composables/useFormat'
import { useAsync } from '@/composables/useAsync'
import { useScopeStore } from '@/stores/scope'
import type { BootVolume } from '@/types/api'
import { useToast } from '@/composables/useToast'
interface BvRow extends BootVolume {
cfgId: number
@@ -31,7 +31,7 @@ interface BvRow extends BootVolume {
/** 租户 / 区域 / 区间来自侧栏全局作用域 */
const scope = useScopeStore()
const message = useMessage()
const message = useToast()
/** 当前作用域租户失联时整页替换占位,不发起资源请求 */
const isDead = computed(() => scope.currentConfig?.aliveStatus === 'dead')
+56 -12
View File
@@ -6,7 +6,6 @@ import {
NPopconfirm,
NSpin,
useDialog,
useMessage,
type DataTableColumns,
} from 'naive-ui'
import { computed, h, ref, watch } from 'vue'
@@ -49,10 +48,11 @@ import {
useTransientPoll,
} from '@/composables/useTransientPoll'
import type { BootVolume, ConsoleConnection, PowerAction, VolumeAttachment } from '@/types/api'
import { useToast } from '@/composables/useToast'
const route = useRoute()
const router = useRouter()
const message = useMessage()
const message = useToast()
const dialog = useDialog()
const { alias: regionAlias } = useRegionAlias()
@@ -133,10 +133,23 @@ const isStopped = computed(() => instance.data.value?.lifecycleState === 'STOPPE
const isEnded = computed(() =>
['TERMINATED', 'TERMINATING'].includes(instance.data.value?.lifecycleState ?? ''),
)
// 区块数据首次加载中禁用对应头部按钮(loading && !data:静默轮询刷新不闪禁用态)
const bvLoading = computed(
() => (bvAtts.loading.value || bvDetails.loading.value) && !bvDetails.data.value,
)
const volLoading = computed(() => volAtts.loading.value && !volAtts.data.value)
const consolesLoading = computed(() => consoles.loading.value && !consoles.data.value)
const attachedVolumeIds = computed(() => (volAtts.data.value ?? []).map((a) => a.volumeId))
/** 创建时若选了随机密码,密码写在 TAG RootPassword 中,这里回显 */
const rootPassword = computed(() => instance.data.value?.freeformTags?.RootPassword ?? '')
// 密码默认掩码(S-01),点击查看后本页会话内保持可见
const pwdRevealed = ref(false)
function revealRootPassword() {
pwdRevealed.value = true
}
async function copyRootPassword() {
await navigator.clipboard.writeText(rootPassword.value)
@@ -181,7 +194,6 @@ function openSerial() {
instanceId: instanceId.value,
region: region.value,
instanceName: instance.data.value?.displayName,
rootPassword: rootPassword.value,
})
}
@@ -492,10 +504,28 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
>
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">ROOT 密码</div>
<div class="flex min-w-0 flex-1 items-center gap-1.5">
<code class="mono truncate text-[13px]" :title="rootPassword">{{ rootPassword }}</code>
<NButton size="tiny" quaternary type="primary" @click="copyRootPassword">
复制
</NButton>
<template v-if="pwdRevealed">
<code class="mono truncate text-[13px]" :title="rootPassword">{{ rootPassword }}</code>
<NButton size="tiny" quaternary type="primary" @click="copyRootPassword">
复制
</NButton>
</template>
<template v-else>
<code class="mono text-[13px] tracking-widest select-none"></code>
<NButton
size="tiny"
quaternary
type="primary"
title="验证身份后查看"
@click="revealRootPassword"
>
<svg class="mr-1 h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
查看
</NButton>
</template>
</div>
</div>
<div class="flex gap-3 py-2.5 max-md:flex-col max-md:gap-0.5">
@@ -593,7 +623,7 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
<div class="text-sm font-semibold">引导卷</div>
<NButton
size="small"
:disabled="!isStopped || isEnded"
:disabled="!isStopped || isEnded || bvLoading"
:title="isStopped ? '' : '替换引导卷须实例处于 STOPPED'"
@click="showReplaceBv = true"
>
@@ -661,7 +691,12 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
<div class="panel">
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
<div class="text-sm font-semibold">块存储卷</div>
<NButton size="small" type="primary" :disabled="isEnded" @click="showAttachVol = true">
<NButton
size="small"
type="primary"
:disabled="isEnded || volLoading"
@click="showAttachVol = true"
>
附加块卷
</NButton>
</div>
@@ -682,11 +717,20 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
<div class="text-sm font-semibold">控制台连接(VNC / 串口)</div>
<div class="flex items-center gap-2">
<NButton size="small" type="primary" :disabled="isEnded" @click="openSerial">
<NButton
size="small"
type="primary"
:disabled="isEnded || consolesLoading"
@click="openSerial"
>
串行终端
</NButton>
<NButton size="small" :disabled="isEnded" @click="openVnc">网页 VNC</NButton>
<NButton size="small" :disabled="isEnded" @click="showConsole = true">创建连接</NButton>
<NButton size="small" :disabled="isEnded || consolesLoading" @click="openVnc">
网页 VNC
</NButton>
<NButton size="small" :disabled="isEnded || consolesLoading" @click="showConsole = true">
创建连接
</NButton>
</div>
</div>
<NDataTable
+2 -2
View File
@@ -6,7 +6,6 @@ import {
NInputGroup,
NInputGroupLabel,
NSelect,
useMessage,
type DataTableColumns,
} from 'naive-ui'
import { computed, h, ref, watch } from 'vue'
@@ -22,13 +21,14 @@ import { useAsync } from '@/composables/useAsync'
import { INSTANCE_TRANSIENT_STATES, useTransientPoll } from '@/composables/useTransientPoll'
import { useScopeStore } from '@/stores/scope'
import type { Instance, PowerAction } from '@/types/api'
import { useToast } from '@/composables/useToast'
interface InstanceRow extends Instance {
cfgId: number
cfgAlias: string
}
const message = useMessage()
const message = useToast()
const { alias } = useRegionAlias()
/** 租户 / 区域 / 区间来自侧栏全局作用域,页内只做区间切换与搜索 */
+27 -1
View File
@@ -8,12 +8,15 @@ import { ApiError } from '@/api/request'
import AppLogo from '@/components/AppLogo.vue'
import FullPageBackdrop from '@/components/FullPageBackdrop.vue'
import OauthProviderIcon from '@/components/OauthProviderIcon.vue'
import { useThemeRipple } from '@/composables/useThemeRipple'
import { useAuthStore } from '@/stores/auth'
import type { OauthProviderInfo } from '@/types/api'
const auth = useAuthStore()
const route = useRoute()
const router = useRouter()
// 点击 logo 切换明暗主题,新主题自 logo 向外扩散
const { toggleDarkFrom } = useThemeRipple()
const username = ref('admin')
const password = ref('')
@@ -123,7 +126,15 @@ async function oauthLogin(provider: string) {
<div class="fpb-ring fpb-ring-dash" style="width: 430px; height: 430px; left: calc(50% - 215px); top: calc(50% - 215px)"></div>
</div>
<div class="flex items-center gap-3">
<span class="floaty flex-none"><AppLogo :size="58" /></span>
<button
type="button"
class="floaty logo-btn flex-none"
title="切换明暗主题"
aria-label="切换明暗主题"
@click="toggleDarkFrom"
>
<AppLogo :size="58" />
</button>
<span>
<span class="block font-serif text-2xl font-semibold">OCI Portal</span>
<span class="mt-px block text-[12.5px] text-ink-3">自托管 · Self-hosted</span>
@@ -240,6 +251,21 @@ async function oauthLogin(provider: string) {
height: 7px;
border-radius: 50%;
}
/* logo 即主题开关:去按钮底色,悬停轻微放大提示可点 */
.logo-btn {
cursor: pointer;
border: none;
background: none;
padding: 0;
line-height: 0;
transition: scale 0.25s ease;
}
.logo-btn:hover {
scale: 1.06;
}
.logo-btn:active {
scale: 0.96;
}
@media (prefers-reduced-motion: no-preference) {
.floaty {
animation: login-fl 7s ease-in-out infinite;
+9 -2
View File
@@ -6,6 +6,7 @@ import { getOverview, listConfigs } from '@/api/configs'
import { listTaskLogs, listTasks } from '@/api/tasks'
import CostChart from '@/components/CostChart.vue'
import EmptyCard from '@/components/EmptyCard.vue'
import { aliveMeta } from '@/composables/useAliveStatus'
import { fmtRelative } from '@/composables/useFormat'
import { useRegionAlias } from '@/composables/useRegionAlias'
import { useAsync } from '@/composables/useAsync'
@@ -175,9 +176,15 @@ const currencySymbol = computed(() => {
<div class="flex w-14 flex-none items-center gap-1.5 text-[12.5px]">
<span
class="h-1.5 w-1.5 rounded-full"
:class="c.aliveStatus === 'alive' ? 'bg-ok' : 'bg-err'"
:class="
c.aliveStatus === 'alive'
? 'bg-ok'
: c.aliveStatus === 'suspended'
? 'bg-warn'
: 'bg-err'
"
/>
{{ c.aliveStatus === 'alive' ? '存活' : '失联' }}
{{ aliveMeta(c.aliveStatus).label }}
</div>
<div class="w-18 flex-none text-right text-xs text-ink-3">{{ fmtRelative(c.lastVerifiedAt) }}</div>
</div>
+46 -6
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { NButton, NInput, NSpin, NSwitch, NTabPane, NTabs, useMessage } from 'naive-ui'
import { NButton, NInput, NSpin, NSwitch, NTabPane, NTabs } from 'naive-ui'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@@ -8,6 +8,7 @@ import {
getSecuritySetting,
getTaskSetting,
getTelegramSetting,
listNotifyChannels,
listNotifyTemplates,
testTelegram,
updateNotifyEvents,
@@ -20,27 +21,47 @@ import FootNote from '@/components/FootNote.vue'
import FormField from '@/components/FormField.vue'
import AboutTab from '@/components/settings/AboutTab.vue'
import AccountSecurityCard from '@/components/settings/AccountSecurityCard.vue'
import NotifyChannelCard from '@/components/settings/NotifyChannelCard.vue'
import NotifyTemplateModal from '@/components/settings/NotifyTemplateModal.vue'
import { useAsync } from '@/composables/useAsync'
import type { NotifyEventsSetting, NotifyTemplateItem, SecuritySetting } from '@/types/api'
import type {
NotifyChannelItem,
NotifyEventsSetting,
NotifyTemplateItem,
SecuritySetting,
} from '@/types/api'
import { useAuthStore } from '@/stores/auth'
import { useToast } from '@/composables/useToast'
const message = useMessage()
const message = useToast()
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
// 设置页以 tab 组织:通知(管理 + 方式)/ 任务(抢机熔断阈值)/ 安全(WAF/网络/账号)
const tab = ref('notify')
// OAuth 绑定回跳:成功/失败提示后清理 query,并停在安全 tab
// OAuth 绑定回跳:成功/失败提示后清理 query,并停在安全 tab;
// 绑定使令牌版本递增,fragment 携带的新 token 须先落地,否则后续请求 401
onMounted(() => {
const { oauth, oauthError } = route.query
if (oauth !== 'bound' && !oauthError) return
consumeBindToken()
tab.value = 'security'
if (oauth === 'bound') message.success('外部身份绑定成功')
if (typeof oauthError === 'string' && oauthError) message.error(oauthError)
void router.replace({ query: {} })
})
/** 消费绑定回跳 fragment 中的新会话 token(无感换发,不进服务端日志) */
function consumeBindToken() {
const hash = window.location.hash
if (!hash.startsWith('#oauthToken=')) return
const token = decodeURIComponent(hash.slice('#oauthToken='.length))
history.replaceState(null, '', window.location.pathname + window.location.search)
if (token) auth.setSession(token, new Date(Date.now() + 24 * 3600e3).toISOString())
}
// ---- 通知管理:按事件类型的推送开关 ----
const events = useAsync(getNotifyEvents)
const eventForm = ref<NotifyEventsSetting>({
@@ -138,6 +159,14 @@ async function saveEvents() {
}
}
// ---- 通知方式:多渠道(webhook/ntfy/bark/smtp)----
const channels = useAsync(listNotifyChannels)
/** 渠道保存接口回全渠道最新视图,就地覆盖免二次拉取 */
function onChannelsSaved(items: NotifyChannelItem[]) {
channels.data.value = items
}
// ---- 通知方式:Telegram 渠道配置 ----
const setting = useAsync(getTelegramSetting)
const enabled = ref(false)
@@ -431,13 +460,24 @@ async function sendTest() {
拉进群 浏览器打开 api.telegram.org/bot&lt;token&gt;/getUpdates
message.chat.id 即为 chat_id
</FootNote>
<!-- 多渠道: Telegram 并存,可同时启用任意多个 -->
<div class="border-t border-line-soft">
<NotifyChannelCard
v-for="ch in channels.data.value ?? []"
:key="ch.type"
:item="ch"
@saved="onChannelsSaved"
/>
</div>
</div>
<div class="panel px-4.5 py-3.5">
<div class="text-[13px] font-semibold">送达规则</div>
<div class="mt-1.5 text-xs leading-relaxed text-ink-3">
发送为异步10 秒超时失败仅记后端日志不影响任务执行同一事件按状态变化去重
会刷屏云端事件另受日志回传链路约束未建链路的租户不产生该类通知
事件同时投递到全部已启用渠道发送为异步10 秒超时单渠道失败仅记后端日志
影响其他渠道与任务执行同一事件按状态变化去重不会刷屏
云端事件另受日志回传链路约束未建链路的租户不产生该类通知
</div>
</div>
</div>
+3 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { NButton, NSpin, useDialog, useMessage } from 'naive-ui'
import { NButton, NSpin, useDialog } from 'naive-ui'
import { computed, onUnmounted, ref } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
@@ -12,10 +12,11 @@ import { parseSnatch, scopeCfgIds, STATUS_META, TYPE_LABEL } from '@/components/
import { fmtRelative, fmtTime, regionCity } from '@/composables/useFormat'
import { useAsync } from '@/composables/useAsync'
import { useScopeStore } from '@/stores/scope'
import { useToast } from '@/composables/useToast'
const route = useRoute()
const router = useRouter()
const message = useMessage()
const message = useToast()
const dialog = useDialog()
const scope = useScopeStore()
+3 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { NButton, NSpin, useDialog, useMessage } from 'naive-ui'
import { NButton, NSpin, useDialog } from 'naive-ui'
import { computed, onUnmounted, ref } from 'vue'
import { deleteTask, listTasks, runTask, updateTask } from '@/api/tasks'
@@ -10,8 +10,9 @@ import { parseSnatch } from '@/components/task/taskDisplay'
import { useAsync } from '@/composables/useAsync'
import { useScopeStore } from '@/stores/scope'
import type { Task } from '@/types/api'
import { useToast } from '@/composables/useToast'
const message = useMessage()
const message = useToast()
const dialog = useDialog()
const scope = useScopeStore()
+58 -15
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import { NButton, NPopconfirm, NSpin, NTabPane, NTabs, NTooltip, useMessage } from 'naive-ui'
import { computed, ref } from 'vue'
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'
@@ -15,15 +16,17 @@ 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 = useMessage()
const message = useToast()
const { alias: regionAlias } = useRegionAlias()
const cfgId = computed(() => Number(route.params.id))
@@ -32,8 +35,33 @@ const verifying = ref(false)
const config = useAsync(() => getConfig(cfgId.value))
/** 失联租户各 tab 拉不到云端数据,内容区整体换失联占位 */
const isDead = computed(() => config.data.value?.aliveStatus === 'dead')
/** 失联/暂停租户各 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
@@ -53,7 +81,7 @@ async function verify() {
async function remove() {
try {
await deleteConfig(cfgId.value)
message.success('已删除(仅移除本面板配置,不影响云端资源)')
message.success('已删除租户及关联本地数据,OCI 云端资源未受影响')
void scope.refreshConfigs()
void router.push({ name: 'tenants' })
} catch (e) {
@@ -88,19 +116,22 @@ async function remove() {
<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"
v-if="isBlocked && config.data.value.lastError"
trigger="hover"
placement="top-start"
>
<template #trigger>
<StatusBadge kind="term" label="失联" />
<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="config.data.value.aliveStatus === 'alive' ? 'run' : 'term'"
:label="config.data.value.aliveStatus === 'alive' ? '存活' : '失联'"
:kind="aliveMeta(config.data.value.aliveStatus).kind"
:label="aliveMeta(config.data.value.aliveStatus).label"
/>
<AccountTypeBadge :type="config.data.value.accountType" />
<div class="flex-1" />
@@ -109,7 +140,8 @@ async function remove() {
<template #trigger>
<NButton size="small" type="error" ghost>删除租户</NButton>
</template>
删除{{ config.data.value.alias }}仅移除本面板配置与快照不影响云端资源
删除{{ config.data.value.alias }}将清理本面板中的关联任务快照回传事件与 Webhook
密钥告警及 AI 渠道数据OCI 云端资源不会被删除云端日志回传链路请先在其他页销毁
</NPopconfirm>
</div>
@@ -140,19 +172,30 @@ async function remove() {
<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="isDead" :last-error="config.data.value.lastError" />
<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" />
<IdpTab v-else-if="tab === 'idp'" :cfg-id="cfgId" />
<UsersTab v-else-if="tab === 'users'" :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 :cfg-id="cfgId" />
<MiscTab v-else :key="`misc-${domainId ?? ''}`" :cfg-id="cfgId" :domain-id="domainId" />
</template>
</template>
</div>
+11 -10
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { NButton, NDataTable, NInput, NInputGroup, NInputGroupLabel, NPopconfirm, NPopover, NSelect, NTooltip, useMessage, type DataTableColumns } from 'naive-ui'
import { NButton, NDataTable, NInput, NInputGroup, NInputGroupLabel, NPopconfirm, NPopover, NSelect, NTooltip, type DataTableColumns } from 'naive-ui'
import { computed, h, ref, type VNodeChild } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
@@ -11,13 +11,15 @@ import EditConfigModal from '@/components/config/EditConfigModal.vue'
import ImportConfigModal from '@/components/config/ImportConfigModal.vue'
import StatusBadge from '@/components/StatusBadge.vue'
import { parseSnatch, scopeCfgIds, STATUS_META, TYPE_LABEL } from '@/components/task/taskDisplay'
import { aliveMeta } from '@/composables/useAliveStatus'
import { fmtRelative, fmtTime } from '@/composables/useFormat'
import { useRegionAlias } from '@/composables/useRegionAlias'
import { useAsync } from '@/composables/useAsync'
import { useScopeStore } from '@/stores/scope'
import type { AccountType, OciConfigSummary, Task } from '@/types/api'
import { useToast } from '@/composables/useToast'
const message = useMessage()
const message = useToast()
const router = useRouter()
const { alias: regionAlias } = useRegionAlias()
const scope = useScopeStore()
@@ -104,7 +106,7 @@ function setGlobal(row: OciConfigSummary) {
async function remove(row: OciConfigSummary) {
try {
await deleteConfig(row.id)
message.success(`已删除「${row.alias}(仅移除本面板配置,不影响云端资源)`)
message.success(`已删除「${row.alias}及关联本地数据,OCI 云端资源未受影响`)
reloadConfigs()
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败')
@@ -125,13 +127,11 @@ function renderTenant(row: OciConfigSummary): VNodeChild {
])
}
/** 状态列:仅徽标;失联且有错误详情时 hover 徽标以 tooltip 展示完整 lastError */
/** 状态列:仅徽标;失联/暂停且有错误详情时 hover 徽标以 tooltip 展示完整 lastError */
function renderStatus(row: OciConfigSummary): VNodeChild {
const badge = h(StatusBadge, {
kind: row.aliveStatus === 'alive' ? 'run' : row.aliveStatus === 'dead' ? 'term' : 'check',
label: row.aliveStatus === 'alive' ? '存活' : row.aliveStatus === 'dead' ? '失联' : '未测',
})
if (row.aliveStatus !== 'dead' || !row.lastError) return badge
const meta = aliveMeta(row.aliveStatus)
const badge = h(StatusBadge, { kind: meta.kind, label: meta.label })
if (!['dead', 'suspended'].includes(row.aliveStatus) || !row.lastError) return badge
return h(
NTooltip,
{ trigger: 'hover', placement: 'top-start' },
@@ -221,7 +221,8 @@ function renderActions(row: OciConfigSummary): VNodeChild {
{
trigger: () =>
h(NButton, { size: 'tiny', quaternary: true, type: 'error' }, { default: () => '删除' }),
default: () => `删除「${row.alias}」?仅移除本面板配置与快照,不影响云端资源。`,
default: () =>
`删除「${row.alias}」?将清理本面板中的关联任务、快照、回传事件与 Webhook 密钥、告警及 AI 渠道数据;OCI 云端资源不会被删除(云端日志回传链路请先在“其他”页销毁)。`,
},
),
])
+2 -2
View File
@@ -4,7 +4,6 @@ import {
NDataTable,
NPopconfirm,
NSpin,
useMessage,
type DataTableColumns,
} from 'naive-ui'
import { computed, h, ref } from 'vue'
@@ -18,10 +17,11 @@ import StatusBadge from '@/components/StatusBadge.vue'
import { fmtTime } from '@/composables/useFormat'
import { useAsync } from '@/composables/useAsync'
import type { Ipv6Step, SecurityList, Subnet } from '@/types/api'
import { useToast } from '@/composables/useToast'
const route = useRoute()
const router = useRouter()
const message = useMessage()
const message = useToast()
const cfgId = computed(() => Number(route.params.cfgId))
const vcnId = computed(() => String(route.params.vcnId))