203 lines
12 KiB
Vue
203 lines
12 KiB
Vue
<template>
|
|
<el-card shadow="never" class="aws-panel">
|
|
<template #header>
|
|
<div class="panel-header">
|
|
<div class="panel-heading">
|
|
<span class="heading-icon"><el-icon><Cloudy /></el-icon></span>
|
|
<div>
|
|
<strong>AWS 专属信息</strong>
|
|
<span>履约状态与远端资源快照</span>
|
|
</div>
|
|
</div>
|
|
<div class="header-actions">
|
|
<el-button :icon="Refresh" :loading="loading" @click="refreshAll">刷新</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false" class="panel-alert">
|
|
<template #default><el-button link type="primary" @click="refresh">重新加载</el-button></template>
|
|
</el-alert>
|
|
|
|
<div v-loading="loading" class="panel-body">
|
|
<template v-if="instance">
|
|
<el-alert
|
|
v-if="instance.lastError || instance.last_error"
|
|
:title="instance.lastError || instance.last_error"
|
|
:description="instance.lastErrorCode || instance.last_error_code || undefined"
|
|
type="error"
|
|
show-icon
|
|
:closable="false"
|
|
class="panel-alert"
|
|
/>
|
|
|
|
<div class="instance-hero">
|
|
<div class="instance-identity">
|
|
<span class="instance-icon"><el-icon><Monitor /></el-icon></span>
|
|
<div>
|
|
<div class="instance-title-row">
|
|
<strong>{{ resource.name || 'AWS 云服务器' }}</strong>
|
|
<el-tag :type="statusType(provisionStatus)" effect="light">{{ statusText(provisionStatus) }}</el-tag>
|
|
<el-tag v-if="pendingAction" type="warning" effect="plain">正在{{ actionText(pendingAction) }}</el-tag>
|
|
</div>
|
|
<code>{{ remoteInstanceId || '等待 AWS 实例创建' }}</code>
|
|
</div>
|
|
</div>
|
|
<div class="instance-links">
|
|
<el-button v-if="instance.serviceId || instance.service_id" link type="primary" @click="openService">
|
|
服务 #{{ instance.serviceId || instance.service_id }}
|
|
</el-button>
|
|
<span>履约记录 #{{ instance.id }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="resource-grid">
|
|
<article>
|
|
<span class="resource-icon blue"><el-icon><Location /></el-icon></span>
|
|
<div><small>Region / 可用区</small><strong>{{ instance.region || resource.region || '-' }}</strong><span>{{ resource.availability_zone || instance.placementId || instance.placement_id || '-' }}</span></div>
|
|
</article>
|
|
<article>
|
|
<span class="resource-icon purple"><el-icon><Cpu /></el-icon></span>
|
|
<div><small>实例规格</small><strong>{{ resource.instance_type || requestSnapshot.instance_type || '-' }}</strong><span>{{ resource.image_id || requestSnapshot.image_id || '镜像信息待同步' }}</span></div>
|
|
</article>
|
|
<article>
|
|
<span class="resource-icon green"><el-icon><Connection /></el-icon></span>
|
|
<div><small>公网 IP</small><strong class="mono">{{ resource.public_ip_address || resource.public_ip || '-' }}</strong><span>私网 {{ resource.private_ip_address || resource.private_ip || '-' }}</span></div>
|
|
</article>
|
|
<article>
|
|
<span class="resource-icon orange"><el-icon><Timer /></el-icon></span>
|
|
<div><small>最近同步</small><strong>{{ formatTime(instance.lastSyncedAt || instance.last_synced_at || resource.synced_at) }}</strong><span>重试 {{ instance.retryCount ?? instance.retry_count ?? 0 }} 次</span></div>
|
|
</article>
|
|
</div>
|
|
|
|
<el-descriptions :column="4" border class="fulfillment-details">
|
|
<el-descriptions-item label="履约状态"><el-tag :type="statusType(provisionStatus)" size="small">{{ statusText(provisionStatus) }}</el-tag></el-descriptions-item>
|
|
<el-descriptions-item label="远端状态">{{ resource.state || resource.provision_status || '-' }}</el-descriptions-item>
|
|
<el-descriptions-item label="待执行操作">{{ pendingAction ? actionText(pendingAction) : '-' }}</el-descriptions-item>
|
|
<el-descriptions-item label="审核状态">{{ reviewText(instance.reviewStatus || instance.review_status) }}</el-descriptions-item>
|
|
<el-descriptions-item label="远端任务 ID" :span="2"><span class="mono breakable">{{ instance.remoteTaskId || instance.remote_task_id || resource.last_task_id || '-' }}</span></el-descriptions-item>
|
|
<el-descriptions-item label="创建时间">{{ formatTime(instance.CreatedAt || instance.createdAt || instance.created_at) }}</el-descriptions-item>
|
|
<el-descriptions-item label="完成时间">{{ formatTime(instance.finishedAt || instance.finished_at) }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
|
|
<div class="management-title">
|
|
<div><strong>实例管理</strong><span>履约审核、运行概览、网络安全、存储与操作中心</span></div>
|
|
</div>
|
|
<AwsInstanceDetail :key="instance.id" ref="managementRef" embedded :instance-id="Number(instance.id)" />
|
|
</template>
|
|
|
|
<el-empty v-else-if="!loading && !errorMessage" description="该用户商品尚未生成 AWS 履约实例" :image-size="90" />
|
|
</div>
|
|
</el-card>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, nextTick, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { Cloudy, Connection, Cpu, Location, Monitor, Refresh, Timer } from '@element-plus/icons-vue'
|
|
import { getAwsInstanceDetail } from '@/api/admin/awsService.js'
|
|
import AwsInstanceDetail from '@/views/aws/AwsInstanceDetail.vue'
|
|
|
|
const props = defineProps({
|
|
goodsId: { type: Number, required: true }
|
|
})
|
|
|
|
const router = useRouter()
|
|
const loading = ref(false)
|
|
const errorMessage = ref('')
|
|
const instance = ref(null)
|
|
const managementRef = ref(null)
|
|
|
|
const envelope = response => response?.data || response || {}
|
|
const field = (object, camel, snake) => object?.[camel] ?? object?.[snake]
|
|
const provisionStatus = computed(() => field(instance.value, 'provisionStatus', 'provision_status') || '')
|
|
const pendingAction = computed(() => field(instance.value, 'pendingAction', 'pending_action') || '')
|
|
const remoteInstanceId = computed(() => field(instance.value, 'remoteAwsInstanceId', 'remote_aws_instance_id') || field(instance.value, 'remoteInstanceId', 'remote_instance_id') || resource.value.instance_id || '')
|
|
const requestSnapshot = computed(() => parseObject(instance.value?.request || instance.value?.requestSnapshot || instance.value?.request_snapshot))
|
|
const snapshot = computed(() => parseObject(instance.value?.resourceSnapshot || instance.value?.resource_snapshot))
|
|
const resource = computed(() => snapshot.value?.resource?.instance || snapshot.value?.instance || snapshot.value?.resource || snapshot.value || {})
|
|
|
|
function parseObject(value) {
|
|
if (!value) return {}
|
|
if (typeof value === 'object') return value
|
|
try { return JSON.parse(value) } catch { return {} }
|
|
}
|
|
|
|
const statusLabels = { pending: '等待中', submitting: '提交中', provisioning: '创建中', active: '运行中', failed: '失败', stopping: '停止中', stopped: '已停止', terminating: '终止中', terminated: '已终止' }
|
|
const statusText = status => statusLabels[status] || status || '-'
|
|
const statusType = status => status === 'active' ? 'success' : status === 'failed' ? 'danger' : ['pending', 'submitting', 'provisioning', 'stopping', 'terminating'].includes(status) ? 'warning' : 'info'
|
|
const actionText = action => ({ create: '创建', start: '启动', stop: '停止', reboot: '重启', terminate: '终止' })[action] || action || '-'
|
|
const reviewText = value => ({ none: '无需审核', pending: '待审核', retrying: '重试中', resolved: '已解决', rejected: '已驳回' })[value] || value || '-'
|
|
const formatTime = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-'
|
|
|
|
async function refresh() {
|
|
if (!props.goodsId || loading.value) return
|
|
loading.value = true
|
|
errorMessage.value = ''
|
|
instance.value = null
|
|
try {
|
|
const response = await getAwsInstanceDetail({ user_goods_id: props.goodsId })
|
|
const body = envelope(response)
|
|
if (body.code !== 200) throw new Error(body.message || body.msg || 'AWS 履约信息加载失败')
|
|
instance.value = body.data?.instance || body.data || null
|
|
} catch (error) {
|
|
errorMessage.value = error?.response?.data?.message || error?.message || 'AWS 履约信息加载失败'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function openService() {
|
|
const id = instance.value?.serviceId || instance.value?.service_id
|
|
if (id) router.push({ path: '/aws/services', query: { id } })
|
|
}
|
|
async function refreshAll() {
|
|
await refresh()
|
|
await nextTick()
|
|
await managementRef.value?.refresh?.()
|
|
}
|
|
|
|
defineExpose({ refresh: refreshAll })
|
|
refresh()
|
|
</script>
|
|
|
|
<style scoped>
|
|
.aws-panel { margin-top: 16px; border: 1px solid #dce8f8; border-radius: 12px; }
|
|
.panel-header, .panel-heading, .header-actions, .instance-hero, .instance-identity, .instance-title-row, .instance-links, .failure-actions { display: flex; align-items: center; }
|
|
.panel-header, .instance-hero, .failure-actions { justify-content: space-between; gap: 18px; }
|
|
.panel-heading { gap: 12px; }
|
|
.heading-icon, .instance-icon, .resource-icon { display: grid; flex: 0 0 auto; place-items: center; }
|
|
.heading-icon { width: 38px; height: 38px; border-radius: 11px; color: #fff; background: linear-gradient(135deg, #409eff, #6b63f6); font-size: 20px; }
|
|
.panel-heading strong, .panel-heading span { display: block; }
|
|
.panel-heading strong { color: #263247; font-size: 16px; }
|
|
.panel-heading span { margin-top: 4px; color: #98a2b3; font-size: 12px; }
|
|
.header-actions { gap: 8px; }
|
|
.header-actions .el-button { margin: 0; }
|
|
.panel-body { min-height: 110px; }
|
|
.panel-alert { margin-bottom: 16px; }
|
|
.instance-hero { margin-bottom: 16px; padding: 18px 20px; border: 1px solid #dbe9fb; border-radius: 12px; background: linear-gradient(135deg, #f2f7ff, #fbfdff); }
|
|
.instance-identity { min-width: 0; gap: 13px; }
|
|
.instance-icon { width: 44px; height: 44px; border-radius: 13px; color: #4f7cff; background: #e6efff; font-size: 22px; }
|
|
.instance-title-row { flex-wrap: wrap; gap: 8px; margin-bottom: 6px; }
|
|
.instance-title-row strong { color: #1f2d3d; font-size: 17px; }
|
|
.instance-identity code { color: #667085; font: 12px Consolas, monospace; }
|
|
.instance-links { flex-wrap: wrap; justify-content: flex-end; gap: 10px; color: #98a2b3; font-size: 12px; }
|
|
.resource-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
|
|
.resource-grid article { display: flex; min-width: 0; align-items: center; gap: 12px; padding: 15px; border: 1px solid #e7edf5; border-radius: 11px; background: #fff; box-shadow: 0 5px 16px rgba(42, 61, 94, .04); }
|
|
.resource-icon { width: 38px; height: 38px; border-radius: 11px; font-size: 19px; }
|
|
.resource-icon.blue { color: #337ecc; background: #ecf5ff; }.resource-icon.purple { color: #6b57d2; background: #f2efff; }.resource-icon.green { color: #20a36a; background: #ebf8f2; }.resource-icon.orange { color: #e58a18; background: #fff4e6; }
|
|
.resource-grid article div { min-width: 0; }
|
|
.resource-grid small, .resource-grid strong, .resource-grid span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.resource-grid small { margin-bottom: 5px; color: #98a2b3; font-size: 11px; }.resource-grid strong { color: #344054; font-size: 13px; }.resource-grid span { margin-top: 4px; color: #7f8b9c; font-size: 11px; }
|
|
.mono { font-family: Consolas, monospace; }.breakable { word-break: break-all; }
|
|
.fulfillment-details { margin-top: 4px; }
|
|
.management-title { margin: 24px 0 4px; padding-top: 20px; border-top: 1px solid #e7edf5; }
|
|
.management-title strong, .management-title span { display: block; }
|
|
.management-title strong { color: #263247; font-size: 16px; }
|
|
.management-title span { margin-top: 5px; color: #98a2b3; font-size: 12px; }
|
|
.failure-actions { margin-top: 16px; padding: 13px 16px; border-radius: 9px; color: #b54708; background: #fffaeb; font-size: 13px; }
|
|
.failure-actions span { display: flex; align-items: center; gap: 7px; }
|
|
@media (max-width: 1200px) { .resource-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
@media (max-width: 760px) { .panel-header, .instance-hero, .failure-actions { align-items: flex-start; flex-direction: column; }.resource-grid { grid-template-columns: 1fr; }.fulfillment-details :deep(.el-descriptions__body) { overflow-x: auto; }.instance-links { justify-content: flex-start; } }
|
|
</style>
|