+
#{{instance.userGoodsId}}
#{{instance.orderId}}
@@ -16,29 +16,32 @@
{{instance.reviewStatus||'-'}}{{statusText(instance.previousProvisionStatus)}}{{instance.remoteTaskId||'-'}}{{instance.retryCount||0}}{{instance.lastErrorCode||'-'}}{{instance.failureStage||'-'}}{{instance.lastError||'-'}}{{instance.reviewNote||'-'}}
- 创建请求
{{pretty(baseDetail?.request)}}
+ 开通配置
{{item.label}}{{item.value}}
-
+
- 实时详情
{{pretty(runtimeDetails)}}监控聚合
{{pretty(monitoring)}}
- {{portText(row)}}删除
- {{pretty(eip)}}
+ 安全组名称{{security.group_name||security.name||'-'}}{{security.group_id||security.id||'-'}}
所属 VPC{{security.vpc_id||'-'}}
规则统计{{(security.ingress_rules||[]).length}} 条入站 · {{(security.egress_rules||[]).length}} 条出站
AWS 托管
+
+ {{directionText(row.direction)}}{{protocolText(row.protocol)}}{{portText(row)}}删除
+
+ 公网 / 私网地址{{eipCurrent.public_ip||'-'}}{{eipCurrent.private_ip||'-'}}
Allocation ID{{eipCurrent.allocation_id||'-'}}
网络接口{{eipCurrent.network_interface_id||'-'}}
PTR 记录{{eipCurrent.ptr_record||'未设置'}}
已关联
+
- 扩容/修改挂载卸载删除
+ 扩容系统盘扩容/修改挂载卸载删除
运行期异步操作刷新
- {{operationText(row.status)}}详情同步重试确认解决驳回
+ {{operationTypeText(row.operationType)}}#{{row.orderId}}-{{operationText(row.status)}}{{row.targetRenewPrice==null?'-':`¥${cents(row.targetRenewPrice)}`}}{{time(row.nextRetryAt)}}详情同步重试确认解决驳回
@@ -60,8 +63,10 @@
- 取消确认
- 取消提交
- {{pretty(currentOperation)}}
+ 取消提交
+ 取消确认重装
+ {{trafficLimit?.enabled?'已启用':'未启用'}}{{trafficLimit?.limit_bytes?bytes(trafficLimit.limit_bytes):'-'}}取消提交变更
+ {{operationTypeText(currentOperation.operationType)}}
操作记录 #{{currentOperation.id}}
{{operationText(currentOperation.status)}} #{{currentOperation.instanceId||'-'}}#{{currentOperation.userGoodsId}}-#{{currentOperation.orderId}}-{{currentOperation.targetRenewPrice==null?'-':`¥${cents(currentOperation.targetRenewPrice)}`}}{{time(currentOperation.nextRetryAt)}}{{time(currentOperation.finishedAt)}}{{currentOperation.retryCount||0}}{{time(currentOperation.UpdatedAt)}}{{currentOperation.errorMessage}}
@@ -69,37 +74,50 @@
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
-import { InfoFilled, User } from '@element-plus/icons-vue'
+import { Connection, InfoFilled, Lock, Operation, User } from '@element-plus/icons-vue'
import * as api from '@/api/admin/awsService.js'
import AwsRuntimeVisualization from './AwsRuntimeVisualization.vue'
const props=defineProps({embedded:{type:Boolean,default:false},instanceId:{type:Number,default:0}})
-const route=useRoute(),embedded=computed(()=>props.embedded),instanceId=props.instanceId||Number(route.params.instanceId),loading=ref(false),submitting=ref(false),pageError=ref(''),tab=ref('fulfillment')
-const baseDetail=ref(null),runtimeDetails=ref(null),monitoring=ref(null),security=ref(null),eip=ref(null),volumes=ref([]),operations=ref([]),operationPage=ref(1),operationTotal=ref(0)
-const loginDialog=ref(false),loginUsers=ref([]),loginRetryAfter=ref(0),retryTimer=ref(null),revealed=reactive({}),ruleDialog=ref(false),volumeDialog=ref(false),volumeMode=ref('create'),operationDialog=ref(false),currentOperation=ref(null),pollTimer=ref(null),pollStarted=ref(0)
-const protocols=['tcp','udp','icmp','icmpv6','-1'],volumeTypes=['gp3','gp2','io1','io2','st1','sc1','standard'],volumeTitles={create:'创建并挂载数据卷',modify:'扩容或修改数据卷',attach:'挂载数据卷'}
+const route=useRoute(),embedded=computed(()=>props.embedded),instanceId=props.instanceId||Number(route.params.instanceId),loading=ref(false),submitting=ref(false),pageError=ref(''),tab=ref(props.embedded?'runtime':'fulfillment')
+const baseDetail=ref(null),runtimeDetails=ref(null),monitoring=ref(null),security=ref(null),eip=ref(null),volumes=ref([]),operations=ref([]),operationPage=ref(1),operationTotal=ref(0),trafficLimit=ref(null)
+const loginDialog=ref(false),loginUsers=ref([]),loginRetryAfter=ref(0),retryTimer=ref(null),revealed=reactive({}),ruleDialog=ref(false),volumeDialog=ref(false),volumeMode=ref('create'),rootCurrentSize=ref(0),operationDialog=ref(false),currentOperation=ref(null),pollTimer=ref(null),pollStarted=ref(0),rebuildDialog=ref(false),rebuildImageId=ref(''),trafficDialog=ref(false)
+const protocols=['tcp','udp','icmp','icmpv6','-1'],volumeTypes=['gp3','gp2','io1','io2','st1','sc1','standard'],volumeTitles={create:'创建并挂载数据卷',modify:'扩容或修改数据卷',attach:'挂载数据卷',root_expand:'扩容系统盘'}
const rule=reactive({direction:'ingress',protocol:'tcp',from_port:22,to_port:22,cidr:'0.0.0.0/0',description:''})
const volumeForm=reactive({volume_id:'',name:'',device_name:'/dev/sdf',size_gib:20,type:'gp3',iops:0,throughput:0,encrypted:true,kms_key_id:'',delete_on_termination:false})
+const trafficForm=reactive({enabled:true,limit_gib:100})
const envelope=r=>r?.data||r||{},dataOf=r=>envelope(r)?.data,message=(e,f)=>e?.response?.data?.message||e?.response?.data?.msg||envelope(e)?.message||e?.message||f
const instance=computed(()=>baseDetail.value?.instance||null)
-const snapshotText=computed(()=>{const raw=instance.value?.resourceSnapshot;if(!raw)return'暂无资源快照';try{return JSON.stringify(typeof raw==='string'?JSON.parse(raw):raw,null,2)}catch{return String(raw)}})
const securityRules=computed(()=>{const d=security.value||{};if(Array.isArray(d.rules))return d.rules;return[...(d.ingress_rules||d.ingressRules||[]).map(v=>({...v,direction:v.direction||'ingress'})),...(d.egress_rules||d.egressRules||[]).map(v=>({...v,direction:v.direction||'egress'}))]})
-const pretty=v=>JSON.stringify(v??{},null,2),mask=v=>v?'••••••••':'-',statusText=s=>({pending:'等待中',submitting:'提交中',provisioning:'创建中',active:'运行中',failed:'失败',stopping:'停止中',stopped:'已停止',terminating:'终止中',terminated:'已终止'})[s]||s||'-'
+const publicManagementRisk=computed(()=>securityRules.value.some(r=>r.direction==='ingress'&&r.protocol==='tcp'&&(r.from_port??r.fromPort)<=22&&(r.to_port??r.toPort)>=22&&['0.0.0.0/0','::/0'].includes(r.cidr)))
+const eipCurrent=computed(()=>eip.value?.current||(!eip.value?.available&&eip.value?.public_ip?eip.value:null))
+const requestRows=computed(()=>{const r=baseDetail.value?.request||{},root=r.root_volume||{};return[{label:'实例规格',value:r.instance_type||'-'},{label:'镜像 ID',value:r.image_id||'-'},{label:'系统盘',value:root.size_gib?`${root.size_gib} GiB · ${String(root.type||'').toUpperCase()}`:'-'},{label:'磁盘加密',value:root.encrypted?'已启用':'未启用'},{label:'弹性公网 IP',value:r.associate_eip?'自动关联':'不关联'},{label:'详细监控',value:r.monitoring?'已启用':'未启用'},{label:'管理 Agent',value:r.install_agent?'自动安装':'不安装'},{label:'安全组模式',value:r.security_group_mode==='common_ports'?'常用端口':r.security_group_mode||'-'},{label:'流量额度',value:r.traffic_limit_gib?`${r.traffic_limit_gib} GiB`:'未限制'},{label:'关机行为',value:r.shutdown_behavior==='stop'?'停止实例':r.shutdown_behavior||'-'}]})
+const mask=v=>v?'••••••••':'-',statusText=s=>({pending:'等待中',submitting:'提交中',provisioning:'创建中',active:'运行中',failed:'失败',stopping:'停止中',stopped:'已停止',terminating:'终止中',terminated:'已终止'})[s]||s||'-'
const isPrivateKey=method=>method?.credential?.kind==='private_key'||String(method?.type||'').includes('private_key')||Boolean(method?.credential?.private_key)
const isPassword=method=>method?.credential?.kind==='password'||String(method?.type||'').includes('password')||Boolean(method?.credential?.password)
const credentialType=method=>isPrivateKey(method)?'SSH 私钥':isPassword(method)?'SSH 密码':method?.type||'登录凭据'
const sourceText=source=>({ec2_key_pair:'EC2 密钥对',agent_managed:'Agent 托管'})[source]||source||'未知来源'
const downloadPrivateKey=(value,name)=>{if(!value){ElMessage.warning('暂无可下载的私钥');return}const safeName=String(name||'aws-private-key').replace(/[^\w.-]/g,'_');const url=URL.createObjectURL(new Blob([value],{type:'application/x-pem-file'}));const link=document.createElement('a');link.href=url;link.download=safeName.endsWith('.pem')?safeName:`${safeName}.pem`;link.click();URL.revokeObjectURL(url)}
const statusTag=s=>s==='active'?'success':s==='failed'?'danger':['pending','submitting','provisioning','stopping','terminating'].includes(s)?'warning':'info'
-const operationText=s=>({queued:'排队中',running:'执行中',succeeded:'成功',failed:'失败',rejected:'已驳回'})[s]||s||'-',operationTag=s=>s==='succeeded'?'success':s==='failed'?'danger':s==='rejected'?'info':'warning'
+const operationText=s=>({awaiting_payment:'待支付',queued:'排队中',running:'执行中',succeeded:'成功',failed:'失败',rejected:'已驳回'})[s]||s||'-',operationTag=s=>s==='succeeded'?'success':s==='failed'?'danger':s==='rejected'?'info':'warning'
+const operationTypeText=value=>({
+ 'details.refresh':'刷新实例详情','security_group.rule.add':'添加安全组规则','security_group.rule.delete':'删除安全组规则','eip.replace':'更换弹性 IP','eip.disassociate':'解绑弹性 IP','eip.release':'释放弹性 IP','eip.reverse_dns':'更新 PTR 记录','volume.create':'创建数据卷','volume.modify':'修改数据卷','root_volume.expand':'扩容系统盘','volume.attach':'挂载数据卷','volume.detach':'卸载数据卷','volume.delete':'删除数据卷','password.reset':'重置实例密码','instance.rebuild':'重装系统','traffic_limit.update':'修改流量上限'
+})[value]||value||'-'
+const actorText=value=>({user:'用户',admin:'管理员',system:'系统'})[value]||value||'-'
+const time=value=>value?new Date(value).toLocaleString('zh-CN',{hour12:false}):'-'
+const cents=value=>(Number(value||0)/100).toFixed(2)
+function bytes(value){const n=Number(value||0);if(!n)return'0 B';const units=['B','KB','MB','GB','TB'],i=Math.min(Math.floor(Math.log(n)/Math.log(1024)),4);return`${(n/1024**i).toFixed(i?2:0)} ${units[i]}`}
const portText=r=>r.protocol==='-1'?'全部':`${r.from_port??r.fromPort??'-'}-${r.to_port??r.toPort??'-'}`
+const directionText=value=>({ingress:'入站',egress:'出站'})[value]||value||'-'
+const protocolText=value=>value==='-1'?'全部':String(value||'-').toUpperCase()
async function safe(call,fallback){try{const r=await call();const b=envelope(r);if(b.code!==200)throw new Error(b.message||fallback);return dataOf(r)}catch(e){ElMessage.error(message(e,fallback));return null}}
-async function loadBase(){loading.value=true;pageError.value='';try{const v=await safe(()=>api.getAwsInstanceDetail(instanceId),'加载履约详情失败');if(v)baseDetail.value=v;else pageError.value='AWS 履约实例不存在或加载失败'}finally{loading.value=false}}
+async function loadBase(){loading.value=true;pageError.value='';try{const v=await safe(()=>api.getAwsInstanceDetail(instanceId),'加载履约详情失败');if(v){baseDetail.value=v;if(embedded.value&&tab.value==='runtime')await Promise.all([runtimeDetails.value?null:loadRuntime(),monitoring.value?null:loadMonitoring()])}else pageError.value='AWS 履约实例不存在或加载失败'}finally{loading.value=false}}
async function loadRuntime(){const v=await safe(()=>api.getAdminAwsRuntimeDetails(instanceId),'读取实时详情失败');if(v!==null)runtimeDetails.value=v}
async function loadMonitoring(){const v=await safe(()=>api.getAdminAwsMonitoring(instanceId),'读取监控失败');if(v!==null)monitoring.value=v}
+async function loadTraffic(){const v=await safe(()=>api.getAdminAwsTrafficLimit(instanceId),'读取流量上限失败');if(v!==null)trafficLimit.value=v}
async function loadNetwork(){const [s,p]=await Promise.all([safe(()=>api.getAdminAwsSecurityGroup(instanceId),'读取安全组失败'),safe(()=>api.getAdminAwsEip(instanceId),'读取 EIP 失败')]);if(s!==null)security.value=s;if(p!==null)eip.value=p}
async function loadVolumes(){const v=await safe(()=>api.getAdminAwsVolumes(instanceId),'读取数据卷失败');if(v!==null)volumes.value=Array.isArray(v)?v:(v.list||v.volumes||[])}
async function loadOperations(){const v=await safe(()=>api.getAdminAwsOperations(instanceId,{page:operationPage.value,count:20}),'读取操作记录失败');if(v){operations.value=v.list||[];operationTotal.value=v.all_count||0}}
-function tabChanged(name){if(name==='runtime'&&!runtimeDetails.value)loadRuntime();if(name==='network'&&!security.value)loadNetwork();if(name==='storage'&&!volumes.value.length)loadVolumes();if(name==='operations')loadOperations()}
+function tabChanged(name){if(name==='runtime'){if(!runtimeDetails.value)loadRuntime();if(!monitoring.value)loadMonitoring();if(!trafficLimit.value)loadTraffic()}if(name==='network'&&!security.value)loadNetwork();if(name==='storage'&&!volumes.value.length)loadVolumes();if(name==='operations')loadOperations()}
async function promptNote(title){try{return(await ElMessageBox.prompt('请输入操作备注(可留空)',title,{inputValue:''})).value}catch{return null}}
async function fulfillmentAction(type){const note=await promptNote({resync:'重新同步履约',retry:'审核重试',resolved:'确认解决',reject:'驳回履约'}[type]);if(note===null)return;let call;if(type==='resync')call=()=>api.resyncAwsInstance(instanceId,note);else if(type==='retry')call=()=>api.retryAwsInstance(instanceId,note);else call=()=>api.resolveAwsInstance(instanceId,type,note);const v=await safe(call,'履约操作失败');if(v!==null){ElMessage.success('操作成功');loadBase()}}
function stopPoll(){clearTimeout(pollTimer.value);pollTimer.value=null}
@@ -117,9 +135,12 @@ async function deleteRule(row){try{await ElMessageBox.confirm(`确认删除 ${ro
async function ensureEip(){const allocation=await promptNote('确保或更换 EIP(可填写 Allocation ID)');if(allocation===null)return;runAsync(()=>api.replaceAdminAwsEip({instance_id:instanceId,allocation_id:allocation}),loadNetwork)}
async function setPtr(){const domain=await promptNote('更新 EIP PTR(填写域名,留空清除)');if(domain===null)return;runAsync(()=>api.updateAdminAwsEipReverseDns({instance_id:instanceId,domain_name:domain}),loadNetwork)}
async function dangerEip(type){try{await ElMessageBox.confirm(type==='release'?'释放 EIP 后可能无法恢复,确认继续?':'确认解绑当前 EIP?',type==='release'?'释放 EIP':'解绑 EIP',{type:'error'})}catch{return}runAsync(()=>type==='release'?api.releaseAdminAwsEip(instanceId):api.disassociateAdminAwsEip(instanceId),loadNetwork)}
-function openVolume(mode,row={}){volumeMode.value=mode;Object.assign(volumeForm,{volume_id:row.volume_id||'',name:row.name||'',device_name:row.device_name||'/dev/sdf',size_gib:Number(row.size_gib)||20,type:row.volume_type||'gp3',iops:Number(row.iops)||0,throughput:Number(row.throughput)||0,encrypted:row.encrypted!==false,kms_key_id:row.kms_key_id||'',delete_on_termination:!!row.delete_on_termination});volumeDialog.value=true}
-function submitVolume(){if(['create','modify'].includes(volumeMode.value)&&(volumeForm.size_gib<1||volumeForm.size_gib>16384))return ElMessage.warning('容量范围为 1-16384 GiB');if(['create','attach'].includes(volumeMode.value)&&!volumeForm.device_name)return ElMessage.warning('请输入设备名');const data={instance_id:instanceId,...volumeForm};const call=volumeMode.value==='create'?()=>api.createAdminAwsVolume(data):volumeMode.value==='modify'?()=>api.modifyAdminAwsVolume(data):()=>api.attachAdminAwsVolume(data);runAsync(call,async()=>{volumeDialog.value=false;await loadVolumes()})}
+function openVolume(mode,row={}){volumeMode.value=mode;rootCurrentSize.value=mode==='root_expand'?Number(row.size_gib||0):0;Object.assign(volumeForm,{volume_id:row.volume_id||'',name:row.name||'',device_name:row.device_name||'/dev/sdf',size_gib:mode==='root_expand'?rootCurrentSize.value+1:Number(row.size_gib)||20,type:row.volume_type||'gp3',iops:Number(row.iops)||0,throughput:Number(row.throughput)||0,encrypted:row.encrypted!==false,kms_key_id:row.kms_key_id||'',delete_on_termination:!!row.delete_on_termination});volumeDialog.value=true}
+function submitVolume(){if(['create','modify','root_expand'].includes(volumeMode.value)&&(volumeForm.size_gib<1||volumeForm.size_gib>16384))return ElMessage.warning('容量范围为 1-16384 GiB');if(volumeMode.value==='root_expand'&&Number(volumeForm.size_gib)<=rootCurrentSize.value)return ElMessage.warning('目标容量必须大于当前系统盘容量');if(['create','attach'].includes(volumeMode.value)&&!volumeForm.device_name)return ElMessage.warning('请输入设备名');const data={instance_id:instanceId,...volumeForm};const call=volumeMode.value==='create'?()=>api.createAdminAwsVolume(data):volumeMode.value==='modify'?()=>api.modifyAdminAwsVolume(data):volumeMode.value==='root_expand'?()=>api.expandAdminAwsRootVolume(instanceId,Number(volumeForm.size_gib)):()=>api.attachAdminAwsVolume(data);runAsync(call,async()=>{volumeDialog.value=false;await loadVolumes()})}
async function volumeDanger(type,row){try{await ElMessageBox.confirm(type==='delete'?'删除数据卷不可恢复,确认继续?':'卸载可能导致实例内文件系统异常,确认继续?',type==='delete'?'删除数据卷':'卸载数据卷',{type:'error'})}catch{return}const call=type==='delete'?()=>api.deleteAdminAwsVolume({instance_id:instanceId,volume_id:row.volume_id,force:false}):()=>api.detachAdminAwsVolume({instance_id:instanceId,volume_id:row.volume_id,force:false});runAsync(call,loadVolumes)}
+async function submitRebuild(){if(!rebuildImageId.value)return ElMessage.warning('请输入镜像 ID');try{await ElMessageBox.confirm('重装将覆盖系统盘,确认继续?','确认重装',{type:'error'})}catch{return}runAsync(()=>api.rebuildAdminAwsInstance(instanceId,rebuildImageId.value),async()=>{rebuildDialog.value=false;tab.value='operations';await loadOperations()})}
+async function openTraffic(){await loadTraffic();const gib=Math.round(Number(trafficLimit.value?.limit_bytes||0)/1024**3);Object.assign(trafficForm,{enabled:trafficLimit.value?.enabled!==false,limit_gib:gib||100});trafficDialog.value=true}
+function submitTraffic(){if(trafficForm.enabled&&Number(trafficForm.limit_gib)<=0)return ElMessage.warning('请输入有效流量额度');runAsync(()=>api.updateAdminAwsTrafficLimit(instanceId,{enabled:trafficForm.enabled,limit_gib:Number(trafficForm.limit_gib)}),async()=>{trafficDialog.value=false;await Promise.all([loadTraffic(),loadRuntime()])})}
async function showOperation(id){const v=await safe(()=>api.getAdminAwsOperationDetail(id),'读取操作详情失败');if(v){currentOperation.value=v;operationDialog.value=true}}
async function operationReview(type,row){const note=type==='resync'?'':await promptNote({retry:'重试运行期操作',resolve:'确认操作已解决',reject:'驳回失败操作'}[type]);if(note===null)return;if(type==='resync'){const v=await safe(()=>api.resyncAdminAwsOperation(row.id),'同步操作失败');if(v!==null){ElMessage.success('同步完成');loadOperations()}return}if(type==='retry')runAsync(()=>api.retryAdminAwsOperation(row.id,note),loadOperations);else{const v=await safe(()=>type==='resolve'?api.resolveAdminAwsOperation(row.id,note):api.rejectAdminAwsOperation(row.id,note),'审核操作失败');if(v!==null){ElMessage.success('审核完成');loadOperations()}}}
defineExpose({refresh:loadBase})
@@ -129,4 +150,6 @@ onMounted(loadBase);onBeforeUnmount(()=>{stopPoll();clearInterval(retryTimer.val
diff --git a/src/views/aws/AwsRuntimeVisualization.vue b/src/views/aws/AwsRuntimeVisualization.vue
index 0e75dca..4791ca5 100644
--- a/src/views/aws/AwsRuntimeVisualization.vue
+++ b/src/views/aws/AwsRuntimeVisualization.vue
@@ -1,5 +1,6 @@
+
实例状态{{ stateText(instance.state || instance.provision_status) }}{{ instance.instance_id || '-' }}
公网 / 私网 IP{{ instance.public_ip_address || '-' }}{{ instance.private_ip_address || '-' }}
@@ -10,6 +11,10 @@
实例与费用{{ time(instance.synced_at) }}{{ r[0] }}{{ r[1] || '-' }}
本月{{ money(instance.month_cost) }}
预计本月{{ money(instance.projected_month_cost) }}
完整月{{ money(instance.full_month_cost) }}
流量与资源{{ limit.measurement_source === 'agent' ? 'Agent 计量' : 'AWS 计量' }}{{ bytes(limit.current_usage_bytes) }}/ {{ bytes(limit.limit_bytes) }}
{{ details.security_groups?.length || 0 }} 安全组{{ details.storage?.volumes?.length || 0 }} 数据卷{{ details.network?.elastic_ips?.length || 0 }} EIP
+
+
计算规格{{ sourceText(instanceType.detail_source) }}{{instanceType.display_name||instanceType.instance_type||instance.instance_type||'-'}}
{{instanceType.description||'暂无规格说明'}}
{{instanceType.vcpus||'-'}} vCPU{{memory(instanceType.memory_mib)}} 内存{{instanceType.network_performance||'-'}} 网络{{instanceType.gpu_count}} GPU
+
系统镜像{{ sourceText(imageDetails.detail_source) }}{{imageDetails.display_name||imageDetails.name||imageDetails.image_id||instance.image_id||'-'}}
{{imageDetails.display_intro||imageDetails.description||'暂无镜像说明'}}
{{imageDetails.os_family||imageDetails.platform||'-'}} 系统{{imageDetails.architecture||'-'}} 架构{{imageDetails.source_name||imageDetails.owner_alias||'-'}} 来源
+
CPU{{ percent(metrics.cpu_percent) }}内存{{ percent(ratio(metrics.memory_used_bytes,metrics.memory_total_bytes)) }}{{ bytes(metrics.memory_used_bytes) }} / {{ bytes(metrics.memory_total_bytes) }}磁盘{{ percent(ratio(metrics.disk_used_bytes,metrics.disk_total_bytes)) }}{{ bytes(metrics.disk_used_bytes) }} / {{ bytes(metrics.disk_total_bytes) }}运行时长{{ duration(metrics.uptime_seconds) }}
网络吞吐趋势{{ monitoring.traffic?.bucket_seconds || 60 }} 秒聚合
@@ -21,14 +26,18 @@
diff --git a/src/views/aws/components/AwsGoodsPolicyForm.vue b/src/views/aws/components/AwsGoodsPolicyForm.vue
new file mode 100644
index 0000000..e5830de
--- /dev/null
+++ b/src/views/aws/components/AwsGoodsPolicyForm.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/src/views/aws/components/AwsVolumeEditor.vue b/src/views/aws/components/AwsVolumeEditor.vue
new file mode 100644
index 0000000..9ef04e6
--- /dev/null
+++ b/src/views/aws/components/AwsVolumeEditor.vue
@@ -0,0 +1,20 @@
+
+
+
+ GiB
+
+
+ MiB/s
+ 加密随实例删除
+
+
+
+
+
+
+
diff --git a/src/views/product/ProductGroup.vue b/src/views/product/ProductGroup.vue
index f8d3658..5ae8e37 100644
--- a/src/views/product/ProductGroup.vue
+++ b/src/views/product/ProductGroup.vue
@@ -526,8 +526,9 @@
ref="productFormRef"
:model="productForm"
:rules="productRules"
- label-position="top"
- class="product-form"
+ label-position="right"
+ label-width="116px"
+ class="product-form goods-form"
>
@@ -754,44 +744,31 @@
inactive-text="禁用"
/>
-
-
- 推荐返还(%)
-
-
+
+
+
+ %
+
- 续费推介返还(%,0 沿用推荐返还)
+ 续费推介返还
-
+
+
+ %
+
-
+
+ 购买通知
- 用户购买后是否发送通知给管理员
@@ -856,7 +833,7 @@
import { ref, reactive, computed, onMounted, onActivated, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
-import { Plus, Refresh, Search, Folder, ArrowRight, Loading, Grid, List, Document, Picture, Delete, CollectionTag } from '@element-plus/icons-vue'
+import { Plus, Refresh, Search, Folder, ArrowRight, Loading, Grid, List, Document, Picture, Delete, CollectionTag, QuestionFilled } from '@element-plus/icons-vue'
import {
getProductGroupList,
createProductGroup,
@@ -973,6 +950,8 @@ const productForm = reactive({
sold_out: false,
max_per_user: 0,
max_first_purchase_duration: 0,
+ max_renew_duration: 0,
+ renew_before_days: 0,
send_notice: false,
renew_price: 0,
renew_recommend_rebate: 0,
@@ -1634,6 +1613,8 @@ const handleAddProduct = () => {
remote_goods: false,
max_per_user: 0,
max_first_purchase_duration: 0,
+ max_renew_duration: 0,
+ renew_before_days: 0,
recommend_words: ''
})
@@ -1673,6 +1654,8 @@ const handleEditProduct = (product, parentGroupId) => {
sold_out: !!product.soldOut,
max_per_user: product.maxPerUser ?? product.max_per_user ?? 0,
max_first_purchase_duration: product.maxFirstPurchaseDuration ?? product.max_first_purchase_duration ?? 0,
+ max_renew_duration: product.maxRenewDuration ?? product.max_renew_duration ?? 0,
+ renew_before_days: product.renewBeforeDays ?? product.renew_before_days ?? 0,
send_notice: !!product.sendNotice,
renew_price: (product.renewPrice ?? product.renew_price ?? 0) / 100,
renew_recommend_rebate: product.renewRecommendRebate ?? product.renew_recommend_rebate ?? 0,
@@ -1707,6 +1690,8 @@ const submitProductForm = () => {
sold_out: productForm.sold_out === true,
max_per_user: Number(productForm.max_per_user) || 0,
max_first_purchase_duration: Number(productForm.max_first_purchase_duration) || 0,
+ max_renew_duration: Number(productForm.max_renew_duration) || 0,
+ renew_before_days: Number(productForm.renew_before_days) || 0,
send_notice: productForm.send_notice === true,
renew_price: Number(productForm.renew_price) || 0,
renew_recommend_rebate: Number(productForm.renew_recommend_rebate) || 0,
@@ -2140,8 +2125,9 @@ watch(() => route.query.good_id, (newId) => {
margin-top: 4px;
}
-.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
-.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
+.unit-input-row { display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0; }
+.unit-input-row :deep(.el-input-number) { width: 100%; min-width: 0; flex: 1; }
+.unit-text { width: 22px; color: #606266; font-size: 13px; flex-shrink: 0; white-space: nowrap; }
.recommend-user-selector {
display: flex;
@@ -2168,6 +2154,41 @@ watch(() => route.query.good_id, (newId) => {
--section-gap: 20px;
}
+.goods-form :deep(.el-form-item__label) {
+ display: inline-flex;
+ height: 32px;
+ align-items: center;
+ justify-content: flex-end;
+ padding-right: 12px;
+ color: #303133;
+ line-height: 32px;
+}
+
+.goods-form :deep(.el-form-item__content) {
+ min-width: 0;
+ line-height: 32px;
+}
+
+.form-label-with-help {
+ display: inline-flex;
+ min-width: 0;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 4px;
+ white-space: nowrap;
+}
+
+.form-label-with-help .el-icon {
+ color: #909399;
+ font-size: 14px;
+ cursor: help;
+ transition: color .2s ease;
+}
+
+.form-label-with-help .el-icon:hover {
+ color: #409eff;
+}
+
.product-form-header {
display: flex;
gap: 24px;
@@ -2269,6 +2290,10 @@ watch(() => route.query.good_id, (newId) => {
margin-bottom: 12px;
}
+.goods-form .basic-fields :deep(.el-form-item) {
+ align-items: flex-start;
+}
+
.basic-fields :deep(.el-form-item:last-child) {
margin-bottom: 0;
}
@@ -2393,6 +2418,11 @@ watch(() => route.query.good_id, (newId) => {
margin-bottom: 12px;
}
+.goods-form .form-grid :deep(.el-form-item) {
+ min-width: 0;
+ align-items: flex-start;
+}
+
.form-grid .arg-type-item {
grid-column: span 2;
}
diff --git a/src/views/product/components/AwsUserGoodsDetail.css b/src/views/product/components/AwsUserGoodsDetail.css
new file mode 100644
index 0000000..2a770e9
--- /dev/null
+++ b/src/views/product/components/AwsUserGoodsDetail.css
@@ -0,0 +1,417 @@
+.aws-overview {
+ margin-top: 16px;
+}
+
+.aws-overview .overview-body {
+ min-height: 160px;
+}
+
+.aws-overview .panel-alert {
+ margin: 0 0 16px;
+}
+
+.aws-overview .instance-hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+ padding: 24px 26px;
+ overflow: hidden;
+ border: 1px solid #dce7f8;
+ border-radius: 16px;
+ background: radial-gradient(circle at 85% 10%, rgba(121, 92, 246, .13), transparent 27%), linear-gradient(135deg, #eef5ff 0%, #f8fbff 55%, #fff 100%);
+ box-shadow: 0 10px 30px rgba(40, 70, 120, .06);
+}
+
+.aws-overview .hero-main,
+.aws-overview .hero-title,
+.aws-overview .hero-identifiers,
+.aws-overview .hero-actions,
+.aws-overview .surface-header,
+.aws-overview .surface-header > div,
+.aws-overview .management-heading > div {
+ display: flex;
+ align-items: center;
+}
+
+.aws-overview .hero-main {
+ min-width: 0;
+ gap: 17px;
+}
+
+.aws-overview .aws-mark {
+ display: grid;
+ width: 62px;
+ height: 62px;
+ flex: none;
+ border-radius: 17px;
+ color: #fff;
+ background: linear-gradient(145deg, #252f3e, #4b5568);
+ box-shadow: 0 9px 22px rgba(37, 47, 62, .22);
+ font: 700 20px Arial, sans-serif;
+ letter-spacing: -.7px;
+ place-items: center;
+}
+
+.aws-overview .aws-mark::after {
+ width: 27px;
+ height: 5px;
+ margin-top: -15px;
+ border-bottom: 2px solid #ffad22;
+ border-radius: 50%;
+ content: '';
+}
+
+.aws-overview .hero-copy {
+ min-width: 0;
+}
+
+.aws-overview .hero-title {
+ flex-wrap: wrap;
+ gap: 9px;
+}
+
+.aws-overview .hero-title h2 {
+ margin: 0;
+ color: #1d2939;
+ font-size: 21px;
+}
+
+.aws-overview .status-dot {
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ margin-right: 5px;
+ border-radius: 50%;
+ background: currentColor;
+ box-shadow: 0 0 0 3px rgba(255, 255, 255, .28);
+}
+
+.aws-overview .hero-identifiers {
+ flex-wrap: wrap;
+ gap: 7px 17px;
+ margin-top: 10px;
+ color: #667085;
+ font-size: 12px;
+}
+
+.aws-overview .hero-identifiers span {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.aws-overview .hero-actions {
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+.aws-overview .hero-actions .el-button {
+ margin: 0;
+}
+
+.aws-overview .stat-grid {
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 13px;
+ margin-top: 16px;
+}
+
+.aws-overview .stat-card {
+ display: flex;
+ position: relative;
+ min-width: 0;
+ align-items: center;
+ gap: 11px;
+ padding: 16px;
+ border: 1px solid #e5ebf4;
+ border-radius: 13px;
+ background: #fff;
+ box-shadow: 0 5px 17px rgba(42, 61, 94, .04);
+}
+
+.aws-overview .stat-icon,
+.aws-overview .surface-icon,
+.aws-overview .management-icon,
+.aws-overview .volume-icon {
+ display: grid;
+ flex: none;
+ place-items: center;
+}
+
+.aws-overview .stat-icon {
+ width: 41px;
+ height: 41px;
+ border-radius: 12px;
+ font-size: 20px;
+}
+
+.aws-overview .blue { color: #4f7cff; background: #edf3ff; }
+.aws-overview .cyan { color: #0e9fba; background: #eaf9fc; }
+.aws-overview .green { color: #16a57a; background: #eaf8f3; }
+.aws-overview .purple { color: #8264df; background: #f2effd; }
+.aws-overview .orange { color: #e38a1c; background: #fff4e8; }
+
+.aws-overview .stat-card > div {
+ min-width: 0;
+}
+
+.aws-overview .stat-card small,
+.aws-overview .stat-card strong,
+.aws-overview .stat-card p {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.aws-overview .stat-card small { color: #98a2b3; font-size: 11px; }
+.aws-overview .stat-card strong { margin-top: 5px; color: #344054; font-size: 14px; }
+.aws-overview .stat-card p { margin: 4px 0 0; color: #8490a3; font-size: 11px; }
+.aws-overview .copy-button { position: absolute; top: 8px; right: 8px; }
+
+.aws-overview .visual-grid {
+ display: grid;
+ grid-template-columns: 1.2fr 1fr;
+ gap: 14px;
+ margin-top: 14px;
+}
+
+.aws-overview .surface {
+ min-width: 0;
+ padding: 19px;
+ border: 1px solid #e5ebf4;
+ border-radius: 14px;
+ background: #fff;
+ box-shadow: 0 5px 18px rgba(42, 61, 94, .035);
+}
+
+.aws-overview .surface-header {
+ justify-content: space-between;
+ gap: 14px;
+}
+
+.aws-overview .surface-header > div {
+ min-width: 0;
+ gap: 10px;
+}
+
+.aws-overview .surface-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 10px;
+ font-size: 18px;
+}
+
+.aws-overview .surface-header h3,
+.aws-overview .management-heading h3 {
+ margin: 0;
+ color: #29364a;
+ font-size: 15px;
+}
+
+.aws-overview .surface-header p,
+.aws-overview .management-heading p {
+ margin: 4px 0 0;
+ color: #98a2b3;
+ font-size: 11px;
+}
+
+.aws-overview .cost-content,
+.aws-overview .traffic-content {
+ display: flex;
+ align-items: center;
+ min-height: 218px;
+}
+
+.aws-overview .cost-chart { width: 48%; height: 205px; }
+.aws-overview .traffic-chart { width: 55%; height: 205px; }
+.aws-overview .cost-legend { width: 52%; }
+
+.aws-overview .cost-legend > div {
+ display: flex;
+ align-items: center;
+ padding: 7px 0;
+ border-bottom: 1px dashed #edf0f5;
+ font-size: 12px;
+}
+
+.aws-overview .cost-legend i { width: 8px; height: 8px; margin-right: 8px; border-radius: 50%; }
+.aws-overview .cost-legend span { flex: 1; color: #667085; }
+.aws-overview .cost-legend b { color: #344054; font-family: Consolas, monospace; }
+.aws-overview .cost-legend .hourly-rate { margin-top: 5px; border-bottom: 0; }
+.aws-overview .cost-legend .hourly-rate span { color: #98a2b3; }
+.aws-overview .cost-legend .hourly-rate b { color: #f59e0b; }
+
+.aws-overview .traffic-summary {
+ display: grid;
+ width: 45%;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.aws-overview .traffic-summary div { padding: 10px; border-radius: 9px; background: #f7f9fc; }
+.aws-overview .traffic-summary span,
+.aws-overview .traffic-summary b { display: block; }
+.aws-overview .traffic-summary span { color: #98a2b3; font-size: 11px; }
+.aws-overview .traffic-summary b { margin-top: 5px; overflow: hidden; color: #344054; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
+
+.aws-overview .detail-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 14px;
+ margin-top: 14px;
+}
+
+.aws-overview .surface-header.compact {
+ margin-bottom: 15px;
+ padding-bottom: 13px;
+ border-bottom: 1px solid #edf1f6;
+}
+
+.aws-overview .property-list {
+ display: grid;
+ margin: 0;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0 22px;
+}
+
+.aws-overview .property-list > div {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 10px 0;
+ border-bottom: 1px dashed #edf0f5;
+}
+
+.aws-overview .property-list dt { flex: none; color: #98a2b3; font-size: 12px; }
+.aws-overview .property-list dd { min-width: 0; margin: 0; overflow: hidden; color: #344054; font-size: 12px; font-weight: 600; text-align: right; text-overflow: ellipsis; white-space: nowrap; }
+.aws-overview .mono { font-family: Consolas, 'SFMono-Regular', monospace; }
+
+.aws-overview .volume-list > div {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 0;
+ border-bottom: 1px dashed #edf0f5;
+}
+
+.aws-overview .volume-icon { width: 34px; height: 34px; border-radius: 9px; color: #e58a18; background: #fff4e8; }
+.aws-overview .volume-list > div > div { min-width: 0; }
+.aws-overview .volume-list strong,
+.aws-overview .volume-list small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.aws-overview .volume-list strong { color: #344054; font-size: 12px; }
+.aws-overview .volume-list small { margin-top: 3px; color: #98a2b3; font-size: 10px; }
+.aws-overview .volume-meta { flex: 1; text-align: right; }
+.aws-overview .volume-meta b,
+.aws-overview .volume-meta span { display: block; }
+.aws-overview .volume-meta b { color: #344054; font-size: 12px; }
+.aws-overview .volume-meta span { margin-top: 3px; color: #98a2b3; font-size: 10px; }
+
+.aws-overview .health-list > div {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 0;
+ border-bottom: 1px dashed #edf0f5;
+}
+
+.aws-overview .health-dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: #b8c0cc; box-shadow: 0 0 0 4px #f2f4f7; }
+.aws-overview .health-dot.fresh { background: #23b583; box-shadow: 0 0 0 4px #e8f8f2; }
+.aws-overview .health-dot.stale,
+.aws-overview .health-dot.syncing,
+.aws-overview .health-dot.warning { background: #f3a33b; box-shadow: 0 0 0 4px #fff4e5; }
+.aws-overview .health-dot.failed { background: #ef6262; box-shadow: 0 0 0 4px #ffeded; }
+.aws-overview .health-list div > div { min-width: 0; flex: 1; }
+.aws-overview .health-list strong,
+.aws-overview .health-list small { display: block; }
+.aws-overview .health-list strong { color: #344054; font-size: 12px; }
+.aws-overview .health-list small { margin-top: 3px; color: #98a2b3; font-size: 10px; }
+
+.aws-overview .management-heading {
+ margin: 22px 0 10px;
+ padding: 18px 20px;
+ border: 1px solid #dce7f8;
+ border-radius: 13px;
+ background: linear-gradient(135deg, #f3f7ff, #fbfdff);
+}
+
+.aws-overview .management-heading > div { gap: 11px; }
+.aws-overview .management-icon { width: 38px; height: 38px; border-radius: 11px; color: #fff; background: linear-gradient(135deg, #4f7cff, #7b61e8); font-size: 19px; }
+
+.aws-overview .operations-shell {
+ margin-top: 14px;
+ overflow: hidden;
+ border: 1px solid #e5ebf4;
+ border-radius: 14px;
+ background: #fff;
+ box-shadow: 0 5px 18px rgba(42, 61, 94, .035);
+}
+
+.aws-overview .operations-shell .page.embedded {
+ padding: 0;
+ background: #fff;
+}
+
+.aws-overview .operations-shell .tabs {
+ margin: 0;
+ padding: 0 20px 20px;
+ border-radius: 0;
+}
+
+.aws-overview .operations-shell .el-tabs__header {
+ margin: 0 -20px 18px;
+ padding: 0 20px;
+ border-bottom: 1px solid #e8eef6;
+ background: linear-gradient(135deg, #f8faff, #fff);
+}
+
+.aws-overview .operations-shell .el-tabs__nav-wrap::after {
+ display: none;
+}
+
+.aws-overview .operations-shell .el-tabs__item {
+ height: 54px;
+ color: #667085;
+ font-weight: 500;
+}
+
+.aws-overview .operations-shell .el-tabs__item.is-active {
+ color: #4f7cff;
+ font-weight: 600;
+}
+
+.aws-overview .operations-shell .toolbar {
+ margin: 0 0 16px;
+ padding: 12px 14px;
+ border-radius: 10px;
+ background: #f7f9fc;
+}
+
+@media (max-width: 1400px) {
+ .aws-overview .stat-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+}
+
+@media (max-width: 1100px) {
+ .aws-overview .visual-grid,
+ .aws-overview .detail-grid { grid-template-columns: 1fr; }
+ .aws-overview .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+
+@media (max-width: 760px) {
+ .aws-overview .instance-hero { align-items: flex-start; flex-direction: column; }
+ .aws-overview .hero-actions { justify-content: flex-start; }
+ .aws-overview .stat-grid { grid-template-columns: 1fr; }
+ .aws-overview .cost-content,
+ .aws-overview .traffic-content { align-items: stretch; flex-direction: column; }
+ .aws-overview .cost-chart,
+ .aws-overview .traffic-chart,
+ .aws-overview .cost-legend,
+ .aws-overview .traffic-summary { width: 100%; }
+ .aws-overview .traffic-summary { margin-top: -20px; }
+ .aws-overview .property-list { grid-template-columns: 1fr; }
+}
diff --git a/src/views/product/components/AwsUserGoodsDetail.vue b/src/views/product/components/AwsUserGoodsDetail.vue
index e6a1e98..747da80 100644
--- a/src/views/product/components/AwsUserGoodsDetail.vue
+++ b/src/views/product/components/AwsUserGoodsDetail.vue
@@ -1,202 +1,238 @@
-
-
-
-
-
+
重新加载
-
+
-
-
-
-
-
-
-
{{ resource.name || 'AWS 云服务器' }}
-
{{ statusText(provisionStatus) }}
-
正在{{ actionText(pendingAction) }}
+
+
aws
+
+
+
{{ resource.name || 'AWS EC2 云服务器' }}
+ {{ statusText(displayStatus) }}
+ Agent {{ agent.status === 'online' ? '在线' : agent.status }}
+
+
+ {{ remoteInstanceId || '等待实例创建' }}
+ {{ resource.region || instance.region || '-' }}
+ 同步于 {{ formatTime(lastSyncedAt) }}
-
{{ remoteInstanceId || '等待 AWS 实例创建' }}
-
-
- 服务 #{{ instance.serviceId || instance.service_id }}
-
-
履约记录 #{{ instance.id }}
+
+ 订单 #{{ orderId }}
+ 服务 #{{ serviceId }}
+ 刷新资源
-
-
-
- Region / 可用区{{ instance.region || resource.region || '-' }}{{ resource.availability_zone || instance.placementId || instance.placement_id || '-' }}
+
+
+
+
+
+ 实例规格{{ resource.instance_type || requestSnapshot.instance_type || '-' }}{{ resource.image_id || requestSnapshot.image_id || '镜像待同步' }}
-
-
- 实例规格{{ resource.instance_type || requestSnapshot.instance_type || '-' }}{{ resource.image_id || requestSnapshot.image_id || '镜像信息待同步' }}
+
+
+ Region / 可用区{{ resource.region || instance.region || '-' }}{{ resource.availability_zone || instance.placementId || instance.placement_id || '-' }}
-
-
- 公网 IP{{ resource.public_ip_address || resource.public_ip || '-' }}私网 {{ resource.private_ip_address || resource.private_ip || '-' }}
+
+
+ 公网 / 私网 IP{{ resource.public_ip_address || '-' }}{{ resource.private_ip_address || '-' }}
+
-
-
- 最近同步{{ formatTime(instance.lastSyncedAt || instance.last_synced_at || resource.synced_at) }}重试 {{ instance.retryCount ?? instance.retry_count ?? 0 }} 次
+
+
+ 预计本月费用{{ money(resource.projected_month_cost) }}完整月 {{ money(resource.full_month_cost) }}
+
+
+
+ 云盘 / 安全组{{ volumes.length }} 块云盘{{ securityGroups.length }} 个安全组 · {{ elasticIps.length }} 个 EIP
-
- {{ statusText(provisionStatus) }}
- {{ resource.state || resource.provision_status || '-' }}
- {{ pendingAction ? actionText(pendingAction) : '-' }}
- {{ reviewText(instance.reviewStatus || instance.review_status) }}
- {{ instance.remoteTaskId || instance.remote_task_id || resource.last_task_id || '-' }}
- {{ formatTime(instance.CreatedAt || instance.createdAt || instance.created_at) }}
- {{ formatTime(instance.finishedAt || instance.finished_at) }}
-
+
+
+
+
+
+
+
{{ item.label }}{{ money(item.value) }}
+
实例时价{{ money(resource.hourly_rate, 4) }} / 小时
+
+
+
-
-
实例管理履约审核、运行概览、网络安全、存储与操作中心
+
+
+
+
+
+
已使用{{ bytes(trafficLimit.current_usage_bytes) }}
+
总额度{{ bytes(trafficLimit.limit_bytes) }}
+
剩余额度{{ bytes(trafficRemaining) }}
+
计费截止{{ formatDate(trafficLimit.period_end) }}
+
+
+
+
+
+
+
+
+
+ - 远端实例 ID
- {{ remoteInstanceId || '-' }}
+ - 资源记录 ID
- {{ resource.id || instance.remoteInstanceId || '-' }}
+ - 实例规格
- {{ resource.instance_type || requestSnapshot.instance_type || '-' }}
+ - 镜像 ID
- {{ resource.image_id || requestSnapshot.image_id || '-' }}
+ - 系统 / 架构
- {{ agent.os || '-' }} / {{ agent.architecture || '-' }}
+ - 主机名
- {{ agent.hostname || '-' }}
+ - 密钥名称
- {{ resource.key_name || '-' }}
+ - 启动时间
- {{ formatTime(resource.launch_time) }}
+
+
+
+
+
+
+ - VPC
- {{ network.vpc?.vpc_id || resource.vpc_id || '-' }}
+ - VPC 网段
- {{ network.vpc?.cidr || '-' }}
+ - 子网
- {{ network.subnet?.cidr || '-' }}
+ - 公网 IP
- {{ resource.public_ip_address || '-' }}
+ - 私网 IP
- {{ resource.private_ip_address || '-' }}
+ - 弹性 IP
- {{ elasticIps.length ? `${elasticIps.length} 个已绑定` : '未绑定' }}
+ - 安全组
- {{ securityGroups.length }} 个
+ - 入站规则
- {{ ingressRuleCount }} 条
+
+
+
+
+
+
+
+
+
{{ volume.name || (volume.root ? '系统盘' : '数据盘') }}{{ volume.volume_id }}
+
{{ volume.size_gib }} GiB{{ volume.volume_type }} · {{ volume.iops || 0 }} IOPS
+
{{ volume.state || '-' }}
+
+
+
+
+
+
+
+
+
{{ item.label }}{{ formatTime(item.time) }}
{{ cacheStatusText(item.status) }}
+
履约状态重试 {{ instance.retryCount ?? instance.retry_count ?? 0 }} 次
{{ statusText(displayStatus) }}
+
+
+
+
+
-
-
+
重新检查
-
+
diff --git a/src/views/user/UserDetail.vue b/src/views/user/UserDetail.vue
index 538ea67..b01d69c 100644
--- a/src/views/user/UserDetail.vue
+++ b/src/views/user/UserDetail.vue
@@ -508,8 +508,17 @@
-
+ handleGroupSelectVisible(visible, 'user')"
+ >
+
@@ -642,6 +651,8 @@
style="width: 100%"
:loading="adminGroupLoading"
filterable
+ popper-class="user-detail-admin-group-popper"
+ @visible-change="visible => handleGroupSelectVisible(visible, 'admin')"
>
+
@@ -663,7 +675,7 @@