diff --git a/.gitea/workflows/build-service-server.yaml b/.gitea/workflows/build-service-server.yaml
index c1b1f9a..10f09d5 100644
--- a/.gitea/workflows/build-service-server.yaml
+++ b/.gitea/workflows/build-service-server.yaml
@@ -14,7 +14,7 @@ jobs:
- name: Install pnpm
run: |
- npm install -g pnpm
+ npm install -g pnpm@8.15.5
- name: Install dependencies
run: |
diff --git a/.gitea/workflows/build-test-server.yaml b/.gitea/workflows/build-test-server.yaml
index 55cbc5c..d80d1dd 100644
--- a/.gitea/workflows/build-test-server.yaml
+++ b/.gitea/workflows/build-test-server.yaml
@@ -14,7 +14,7 @@ jobs:
- name: Install pnpm
run: |
- npm install -g pnpm
+ npm install -g pnpm@8.15.5
- name: Install dependencies
run: |
diff --git a/package.json b/package.json
index 349177d..4c0c20a 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
"name": "007ui",
"private": true,
"version": "0.1.0",
+ "packageManager": "pnpm@8.15.5",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/src/api/admin/awsService.js b/src/api/admin/awsService.js
index bd40ad3..e28cf57 100644
--- a/src/api/admin/awsService.js
+++ b/src/api/admin/awsService.js
@@ -1,6 +1,13 @@
import { http2 } from '@/utils/request.js'
const base = '/api/v1/admin/server/aws_service'
+const runtimeBase = `${base}/instances/runtime`
+
+export const createAwsIdempotencyKey = (prefix = 'aws') => {
+ const random = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`
+ return `${prefix}-${random}`.slice(0, 128)
+}
+const withKey = (key) => ({ headers: { 'Idempotency-Key': key || createAwsIdempotencyKey() } })
export const getAwsServiceList = (params) => http2.get(`${base}/list`, { params })
export const getAwsServiceDetail = (id) => http2.get(`${base}/detail`, { params: { id } })
@@ -15,5 +22,52 @@ export const createAwsGoods = (data) => http2.post(`${base}/goods/create`, data)
export const updateAwsGoods = (data) => http2.post(`${base}/goods/update`, data)
export const deleteAwsGoods = (id) => http2.delete(`${base}/goods/delete`, { params: { id } })
-export const getAwsInstanceList = (params) => http2.get(`${base}/instances/list`, { params })
-export const retryAwsInstance = (instanceId) => http2.post(`${base}/instances/retry`, null, { params: { instance_id: instanceId } })
+export const getAwsInstanceDetail = (identifier) => http2.get(`${base}/instances/detail`, {
+ params: typeof identifier === 'object' ? identifier : { instance_id: identifier }
+})
+export const actionAdminAwsInstance = (instanceId, action, reason) => http2.post(`${base}/instances/action`, {
+ instance_id: instanceId,
+ action,
+ ...(action === 'suspend' ? { reason } : {})
+})
+export const retryAwsInstance = (instanceId, note = '') => http2.post(`${base}/instances/retry`, { instance_id: instanceId, note })
+export const resyncAwsInstance = (instanceId, note = '') => http2.post(`${base}/instances/resync`, { instance_id: instanceId, note })
+export const resolveAwsInstance = (instanceId, resolution, note = '') => http2.post(`${base}/instances/resolve`, { instance_id: instanceId, resolution, note })
+
+export const getAdminAwsRuntimeDetails = instanceId => http2.get(`${runtimeBase}/details`, { params: { instance_id: instanceId } })
+export const refreshAdminAwsRuntimeDetails = (instanceId, key) => http2.post(`${runtimeBase}/details/refresh`, { instance_id: instanceId }, withKey(key))
+export const getAdminAwsLoginInfo = instanceId => http2.post(`${runtimeBase}/login_info`, { instance_id: instanceId })
+export const resetAdminAwsPassword = (instanceId, key) => http2.post(`${runtimeBase}/password/reset`, { instance_id: instanceId }, withKey(key))
+export const getAdminAwsSshKeys = instanceId => http2.get(`${runtimeBase}/ssh_keys`, { params: { instance_id: instanceId } })
+export const addAdminAwsSshKey = (instanceId, data, key) => http2.post(`${runtimeBase}/ssh_keys/add`, { instance_id: instanceId, ...data }, withKey(key))
+export const rotateAdminAwsSshKey = (instanceId, data, key) => http2.post(`${runtimeBase}/ssh_keys/rotate`, { instance_id: instanceId, ...data }, withKey(key))
+export const revealAdminAwsSshPrivateKey = (instanceId, keyId) => http2.post(`${runtimeBase}/ssh_keys/reveal`, { instance_id: instanceId, key_id: keyId })
+export const getAdminAwsMonitoring = instanceId => http2.get(`${runtimeBase}/monitoring`, { params: { instance_id: instanceId } })
+export const getAdminAwsSecurityGroup = instanceId => http2.get(`${runtimeBase}/security_group`, { params: { instance_id: instanceId } })
+export const addAdminAwsSecurityGroupRule = (instanceId, rule, key) => http2.post(`${runtimeBase}/security_group/rules/add`, { instance_id: instanceId, rule }, withKey(key))
+export const deleteAdminAwsSecurityGroupRule = (instanceId, rule, key) => http2.post(`${runtimeBase}/security_group/rules/delete`, { instance_id: instanceId, rule }, withKey(key))
+export const getAdminAwsVolumes = instanceId => http2.get(`${runtimeBase}/volumes`, { params: { instance_id: instanceId } })
+export const createAdminAwsVolume = (data, key) => http2.post(`${runtimeBase}/volumes/create`, data, withKey(key))
+export const modifyAdminAwsVolume = (data, key) => http2.post(`${runtimeBase}/volumes/modify`, data, withKey(key))
+export const expandAdminAwsRootVolume = (instanceId, sizeGib, key) => http2.post(`${runtimeBase}/root_volume/expand`, { instance_id: instanceId, size_gib: sizeGib }, withKey(key))
+export const attachAdminAwsVolume = (data, key) => http2.post(`${runtimeBase}/volumes/attach`, data, withKey(key))
+export const detachAdminAwsVolume = (data, key) => http2.post(`${runtimeBase}/volumes/detach`, data, withKey(key))
+export const deleteAdminAwsVolume = (data, key) => http2.post(`${runtimeBase}/volumes/delete`, data, withKey(key))
+export const getAdminAwsEip = instanceId => http2.get(`${runtimeBase}/eip`, { params: { instance_id: instanceId } })
+export const replaceAdminAwsEip = (data, key) => http2.post(`${runtimeBase}/eip/replace`, data, withKey(key))
+export const disassociateAdminAwsEip = (instanceId, key) => http2.post(`${runtimeBase}/eip/disassociate`, { instance_id: instanceId }, withKey(key))
+export const releaseAdminAwsEip = (instanceId, key) => http2.post(`${runtimeBase}/eip/release`, { instance_id: instanceId }, withKey(key))
+export const updateAdminAwsEipReverseDns = (data, key) => http2.post(`${runtimeBase}/eip/reverse_dns`, data, withKey(key))
+export const getAdminAwsOperations = (instanceId, params = {}) => http2.get(`${runtimeBase}/operations`, { params: { instance_id: instanceId, ...params } })
+export const getAdminAwsOperationDetail = operationId => http2.get(`${runtimeBase}/operations/detail`, { params: { operation_id: operationId } })
+export const retryAdminAwsOperation = (operationId, note = '', key) => http2.post(`${runtimeBase}/operations/retry`, { operation_id: operationId, note }, withKey(key))
+export const resyncAdminAwsOperation = operationId => http2.post(`${runtimeBase}/operations/resync`, null, { params: { operation_id: operationId } })
+export const resolveAdminAwsOperation = (operationId, note = '') => http2.post(`${runtimeBase}/operations/resolve`, { operation_id: operationId, note })
+export const rejectAdminAwsOperation = (operationId, note = '') => http2.post(`${runtimeBase}/operations/reject`, { operation_id: operationId, note })
+export const rebuildAdminAwsInstance = (instanceId, imageId, key) => http2.post(`${runtimeBase}/rebuild`, { instance_id: instanceId, image_id: imageId }, withKey(key))
+export const getAdminAwsRebuildConfiguration = (goodId, region) => http2.post('/api/v1/tool/good/remote/configuration', {
+ good_id: goodId,
+ remote_args: JSON.stringify({ region })
+})
+export const getAdminAwsTrafficLimit = instanceId => http2.get(`${runtimeBase}/traffic_limit`, { params: { instance_id: instanceId } })
+export const updateAdminAwsTrafficLimit = (instanceId, changes, key) => http2.post(`${runtimeBase}/traffic_limit/update`, { instance_id: instanceId, ...changes }, withKey(key))
diff --git a/src/api/admin/userVm.js b/src/api/admin/userVm.js
index 5527691..0cc3d8c 100644
--- a/src/api/admin/userVm.js
+++ b/src/api/admin/userVm.js
@@ -114,6 +114,16 @@ export const renewUserGoods = (data) => http2.post(`${GOODS_BASE}/renew`, fd(dat
export const getUserVmMetricsHistory = (params) => http2.get(`${BASE}/metrics_history`, { params })
export const getUserVmTrafficHourly = (params) => http2.get(`${BASE}/traffic_hourly`, { params })
+// ========== 虚拟机专用操作日志 ==========
+const OPERATION_RECORD_BASE = '/api/v1/admin/host_service/point/vm/operation_record'
+export const getVmOperationRecordList = (params) => http2.get(`${OPERATION_RECORD_BASE}/list`, { params })
+export const getVmOperationRecordDetail = (id) => http2.get(`${OPERATION_RECORD_BASE}/detail`, { params: { id } })
+export const refreshVmOperationRecord = (id) => http2.post(
+ `${OPERATION_RECORD_BASE}/refresh`,
+ fd({ id }),
+ { headers: { 'Content-Type': 'multipart/form-data' } }
+)
+
// ========== 流量策略 ==========
// 测试未通过(接口新增,待联调)
export const getUserVmTrafficPolicy = (params) => http2.get(`${BASE}/traffic_policy`, { params })
diff --git a/src/components/admin/ProductGoodsTreeSelector.vue b/src/components/admin/ProductGoodsTreeSelector.vue
new file mode 100644
index 0000000..f98affa
--- /dev/null
+++ b/src/components/admin/ProductGoodsTreeSelector.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+ {{ data.name }}
+ {{ data.type==='group'?'商品组':'商品' }}
+ #{{ data.id }}
+
+
+
+
+
+ 当前选择
+
+ {{ selected.type==='group'?'商品组':'商品' }}
+ {{ selected.name }}
+ #{{ selected.id }}
+
+ 尚未选择
+
+
+ 取消
+ 确定选择
+
+
+
+
+
+
+
diff --git a/src/config/menus.js b/src/config/menus.js
index e2eff65..17fa551 100644
--- a/src/config/menus.js
+++ b/src/config/menus.js
@@ -186,8 +186,7 @@ export const menus = [
icon: 'Cloudy',
children: [
{ path: '/aws/services', title: '服务配置' },
- { path: '/aws/goods', title: '商品绑定' },
- { path: '/aws/instances', title: '履约实例' }
+ { path: '/aws/goods', title: '商品绑定' }
]
},
{
diff --git a/src/router/index.js b/src/router/index.js
index 27c992c..7d2a034 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -689,12 +689,6 @@ const routes = [
name: 'AwsGoods',
component: () => import('../views/aws/AwsGoods.vue'),
meta: { title: '商品绑定' }
- },
- {
- path: 'instances',
- name: 'AwsInstances',
- component: () => import('../views/aws/AwsInstances.vue'),
- meta: { title: '履约实例' }
}
]
},
diff --git a/src/views/aws/AwsGoods.vue b/src/views/aws/AwsGoods.vue
index 01d099c..a1df370 100644
--- a/src/views/aws/AwsGoods.vue
+++ b/src/views/aws/AwsGoods.vue
@@ -10,52 +10,102 @@
EIP加密系统盘Agent
+ {{cap}}全部关闭
{{row.enabled?'启用':'停用'}}
编辑删除
-
+
-
-
-
-
-
-
-
- 绑定 EIP加密系统盘安装 Agent启用绑定
-
-
+
+
+
+
+
+ 选择
+
+
+
+
+
绑定 EIP加密系统盘安装 Agent启用绑定
+
+
+
+
+
+
+ 登录凭据密码重置监控安全组数据卷读取SSH 密钥管理
新增 EIP更换 EIPPTR 反向解析数据盘变更系统盘扩容系统重装流量提升
启用每次更换附加费元 / 次计入用户 EIP 更换实时报价
+
+
+
+
取消保存
+
diff --git a/src/views/aws/AwsInstanceDetail.vue b/src/views/aws/AwsInstanceDetail.vue
new file mode 100644
index 0000000..29da311
--- /dev/null
+++ b/src/views/aws/AwsInstanceDetail.vue
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+ #{{instance.userGoodsId}}
+ #{{instance.orderId}}
+ #{{instance.serviceId}}
+ {{statusText(instance.provisionStatus)}}
+ {{instance.region||'-'}}{{instance.placementId||'-'}}{{instance.remoteAwsInstanceId||instance.remoteInstanceId||'-'}}{{instance.pendingAction||'-'}}
+
+
+
+
+
+ 暂停原因:{{ adminPauseInfo.stopReasonDetail || '未提供暂停原因' }}
+ 暂停时间:{{ time(adminPauseInfo.stoppedAt) }}
+
+
+
+
+
+
+
+
+ 取消确认暂停
+
+
+
+
+
+ {{instance.reviewStatus||'-'}}{{statusText(instance.previousProvisionStatus)}}{{instance.remoteTaskId||'-'}}{{instance.retryCount||0}}{{instance.lastErrorCode||'-'}}{{instance.failureStage||'-'}}{{instance.lastError||'-'}}{{instance.reviewNote||'-'}}
+ 开通配置
{{item.label}}{{item.value}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{user.username||'未知用户'}}
{{(user.methods||[]).length}} 种登录方式
+
+ {{credentialType(method)}}
{{method.credential?.available===false?'不可用':'可用'}}
+
+
+ 登录密码
{{revealed[`${ui}-${mi}-p`]?method.credential.password:mask(method.credential.password)}}
{{revealed[`${ui}-${mi}-p`]?'隐藏':'展示'}}复制重置密码
+ SSH 私钥
{{method.credential?.key_name||'EC2 Key Pair'}}
下载复制安全轮换
+
+
+
+
+
+ SSH 密钥绑定在线生成的私钥只在敏感信息弹窗中展示,请及时下载并妥善保存。
+
+ {{row.status||'-'}}读取私钥轮换
+
+
+
+
+ 资源指标与网络趋势刷新监控
+
+
+
+
+
+ 安全组名称{{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||'未设置'}}
已关联
+
+
+
+
+
+ 扩容系统盘扩容/修改挂载卸载删除
+
+
+
+ 运行期异步操作刷新
+ {{operationTypeText(row.operationType)}}#{{row.orderId}}-{{operationText(row.status)}}{{row.targetRenewPrice==null?'-':`¥${cents(row.targetRenewPrice)}`}}{{time(row.nextRetryAt)}}详情同步重试确认解决驳回
+
+
+
+
+
- 取消确认
+
取消提交
+
+
+
+ 取消提交
+
+
{{privateKey.key_name||'-'}}{{privateKey.username||'-'}}{{privateKey.fingerprint||'-'}}关闭复制下载 .pem
+
取消确认重装
+
{{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}}
+
+
+
+
+
+
diff --git a/src/views/aws/AwsInstances.vue b/src/views/aws/AwsInstances.vue
deleted file mode 100644
index 0329e6f..0000000
--- a/src/views/aws/AwsInstances.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
重置
-
-
- {{row.userGoodsId}}
- {{row.orderId}}
- {{row.serviceId}}
-
-
- {{statusText(row.provisionStatus)}}
-
-
-
- {{formatTime(row.lastSyncedAt)}}
- 详情重试
-
-
-
- {{current.id}}{{statusText(current.provisionStatus)}}{{current.remoteAwsInstanceId||'-'}}{{current.remoteTaskId||'-'}}{{current.lastErrorCode||'-'}}{{formatTime(current.nextRetryAt)}}{{current.lastError||'-'}}资源快照
{{snapshotText}}
-
-
-
-
-
diff --git a/src/views/aws/AwsRuntimeVisualization.vue b/src/views/aws/AwsRuntimeVisualization.vue
new file mode 100644
index 0000000..4791ca5
--- /dev/null
+++ b/src/views/aws/AwsRuntimeVisualization.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
实例状态{{ stateText(instance.state || instance.provision_status) }}{{ instance.instance_id || '-' }}
+
公网 / 私网 IP{{ instance.public_ip_address || '-' }}{{ instance.private_ip_address || '-' }}
+
规格 / 可用区{{ instance.instance_type || '-' }}{{ instance.availability_zone || '-' }}
+
Agent{{ agent.status === 'online' ? '在线' : agent.status || '未连接' }}{{ agent.os || '-' }} · {{ agent.version || '-' }}
+
+
+
实例与费用{{ 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 }} 秒聚合
+
+
+
+
+
+
+
+
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/UserGoodsDetail.vue b/src/views/product/UserGoodsDetail.vue
index 8aad036..b2e164b 100644
--- a/src/views/product/UserGoodsDetail.vue
+++ b/src/views/product/UserGoodsDetail.vue
@@ -210,6 +210,8 @@
+
+
@@ -336,6 +338,7 @@ import { extractApiError } from '@/utils/kvmErrorUtil'
import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue'
import KvmServiceSelector from '@/components/admin/KvmServiceSelector.vue'
import UserVmDetail from '@/views/user-vm/UserVmDetail.vue'
+import AwsUserGoodsDetail from './components/AwsUserGoodsDetail.vue'
import dayjs from 'dayjs'
const route = useRoute()
@@ -345,6 +348,7 @@ const goodsId = computed(() => parseInt(route.params.id) || 0)
const loading = ref(false)
const refreshing = ref(false)
const vmDetailRef = ref(null)
+const awsDetailRef = ref(null)
const submitLoading = ref(false)
const detail = ref(null)
const detailExpanded = ref(false)
@@ -387,6 +391,10 @@ const expireStatusClass = computed(() => {
const goBack = () => router.push('/user-goods/list')
const currentTag = computed(() => (detail.value?.tag || detail.value?.good?.tag || '').toLowerCase())
+const isAwsGoods = computed(() => {
+ const tag = currentTag.value.replace(/\s+/g, '')
+ return tag === 'aws' || tag.includes('aws云服务器') || tag.includes('aws服务器')
+})
// 是否已删除(后端返回 deleteAt 字段,有值即已删除)
const isDeleted = computed(() => !!(detail.value?.deleteAt || detail.value?.DeleteAt || detail.value?.deleted_at))
@@ -458,6 +466,10 @@ const refreshDetail = async () => {
await nextTick()
await vmDetailRef.value?.refresh?.()
}
+ if (isAwsGoods.value && !isDeleted.value) {
+ await nextTick()
+ await awsDetailRef.value?.refresh?.()
+ }
} finally {
refreshing.value = false
}
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
new file mode 100644
index 0000000..f5f4783
--- /dev/null
+++ b/src/views/product/components/AwsUserGoodsDetail.vue
@@ -0,0 +1,182 @@
+
+
+
+ 重新加载
+
+
+
+
+
+
+
aws
+
+
+
{{ resource.name || 'AWS EC2 云服务器' }}
+ {{ statusText(displayStatus) }}
+ Agent {{ agent.status === 'online' ? '在线' : agent.status }}
+
+
+ {{ remoteInstanceId || '等待实例创建' }}
+ {{ resource.region || instance.region || '-' }}
+ 同步于 {{ formatTime(lastSyncedAt) }}
+
+
+
+
+ 订单 #{{ orderId }}
+ 服务 #{{ serviceId }}
+ 刷新资源
+
+
+
+
+
+
+
+
+ 实例规格{{ 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.private_ip_address || '-' }}
+
+
+
+
+ 预计本月费用{{ money(resource.projected_month_cost) }}完整月 {{ money(resource.full_month_cost) }}
+
+
+
+ 云盘 / 安全组{{ volumes.length }} 块云盘{{ securityGroups.length }} 个安全组 · {{ elasticIps.length }} 个 EIP
+
+
+
+
+
+
+
+
+
+
+
- 远端实例 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) }}
+
{{ volume.name || (volume.root ? '系统盘' : '数据盘') }}{{ volume.volume_id }}
{{ volume.size_gib }} GiB{{ volume.volume_type }} · {{ volume.iops || 0 }} IOPS
{{ volume.state || '-' }}
+
+
+
+
+
+
- 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 }} 条
+
已使用{{ bytes(trafficLimit.current_usage_bytes) }}
总额度{{ bytes(trafficLimit.limit_bytes) }}
剩余额度{{ bytes(trafficRemaining) }}
计费截止{{ formatDate(trafficLimit.period_end) }}
+
+
+
+
+
+
{{ item.label }}{{ money(item.value) }}
实例时价{{ money(resource.hourly_rate, 4) }} / 小时
+
{{ item.label }}{{ formatTime(item.time) }}
{{ cacheStatusText(item.status) }}履约状态重试 {{ instance.retryCount ?? instance.retry_count ?? 0 }} 次
{{ statusText(displayStatus) }}
+
+
+
+
+
+
+
+
+
重新检查
+
+
+
+
+
+
+
diff --git a/src/views/user-vm/UserVmDetail.vue b/src/views/user-vm/UserVmDetail.vue
index 89fdceb..c683908 100644
--- a/src/views/user-vm/UserVmDetail.vue
+++ b/src/views/user-vm/UserVmDetail.vue
@@ -564,6 +564,9 @@
+
+
+
@@ -1215,6 +1218,7 @@ import UserSelector from '@/components/UserSelector/index.vue'
import UserVmSecurityGroupSelector from '@/components/admin/UserVmSecurityGroupSelector.vue'
import UserVmVolumeSelector from '@/components/admin/UserVmVolumeSelector.vue'
import UserVmNetworkSelector from '@/components/admin/UserVmNetworkSelector.vue'
+import VmOperationRecords from './components/VmOperationRecords.vue'
import dayjs from 'dayjs'
import * as echarts from 'echarts'
diff --git a/src/views/user-vm/components/VmOperationRecords.vue b/src/views/user-vm/components/VmOperationRecords.vue
new file mode 100644
index 0000000..a2819ad
--- /dev/null
+++ b/src/views/user-vm/components/VmOperationRecords.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+ 查询刷新
+
+
+
+ {{ operationLabel(row.operation_type) }}{{ row.note || row.operation_type || '-' }}
+ {{ operatorLabel(row.operator_type) }}#{{ row.operator_id || '-' }}
+ {{ statusLabel(row.status) }}
+
+ {{ formatTime(row.started_at) }}
+ {{ duration(row) }}
+
+ 详情核对状态
+
+
+
+
+
+ #{{ detail.id }}{{ statusLabel(detail.status) }}
+ {{ operationLabel(detail.operation_type) }}{{ operatorLabel(detail.operator_type) }} #{{ detail.operator_id || '-' }}
+ #{{ detail.user_good_id || '-' }}#{{ detail.vm_id || '-' }}
+ #{{ detail.service_id || '-' }} / #{{ detail.host_id || '-' }}{{ detail.task_id || '-' }}
+ {{ detail.method || '-' }} {{ detail.route || '-' }}
+ {{ detail.note || '-' }}{{ detail.error_message || '-' }}
+ {{ formatTime(detail.started_at) }}{{ formatTime(detail.finished_at) }}
+
+ 请求快照
{{ pretty(detail.request_snapshot) }}结果快照
{{ pretty(detail.result_snapshot) }}
+
+
+
+
+
+
+
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 @@