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 @@ + + + + + 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启用绑定 - - + + + +
+ +
选择
+ +
+ + + 绑定 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 @@ + + + + + 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 @@ - - - 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 @@ + + + + + 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 @@ + + + + + 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" >
@@ -608,40 +609,26 @@
- - - + +
+ + +
- +
+ + +
- +
+ + +
- +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
- + + -
启用后,用户购买/续费/升级此商品前须完成实名认证
- + + -
AWS 商品请同时将标签设为 aws,保存后到 AWS 商品绑定页配置目录规则
@@ -704,43 +704,33 @@ /> - +
+ + +
- + + -
开启后用户端将无法选购该商品
- - -
限制单用户最大购买数量,0 表示不限制
+ + +
+ + +
- - -
限制用户首次购买的最大时长(单位:月),0 表示不限制
+ + +
+ + +
@@ -754,44 +744,31 @@ inactive-text="禁用" /> - - - + +
+ + % +
- +
+ + % +
- + + -
用户购买后是否发送通知给管理员
@@ -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 @@ + + + + + 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 @@ + + + + + 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 @@ - + + @@ -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 @@