Kaynağa Gözat

增加 阿里云 ecs信息展示

dodo hold 1 yıl önce
ebeveyn
işleme
d392bc1302
2 değiştirilmiş dosya ile 395 ekleme ve 5 silme
  1. 395 5
      src/views/settings/aliyun.vue
  2. 0 0
      {{ -i

+ 395 - 5
src/views/settings/aliyun.vue

@@ -48,6 +48,9 @@
                             </template>
                             {{ $t('searchTable.operation.create') }}
                         </a-button>
+                        <a-checkbox v-model="showECSInfo" @change="onECSInfoChange">
+                            显示ECS主机信息
+                        </a-checkbox>
                     </a-space>
                 </a-col>
                 <a-col :span="12" style="
@@ -108,7 +111,9 @@
 
             <a-table row-key="id" :loading="loading" :pagination="pagination"
                 :columns="(cloneColumns as TableColumnData[])" :data="renderData" :bordered="{ headerCell: true }"
-                :size="size" @page-change="onPageChange">
+                :size="size" @page-change="onPageChange"
+                :expandable="tableExpandable" :row-class="getRowClass"
+                :class="{ 'show-ecs-info': showECSInfo }">
                 <template #index="{ record }">
                     {{ record.id }}
                 </template>
@@ -141,7 +146,16 @@
                         record.balance_alert_threshold }}
                     </a-tag>
                     <a-tag color="blue" v-else>{{ record.balance_alert_threshold }} </a-tag>
+                </template>
 
+                <template #ecs="{ record }">
+                    <span v-if="record.ecs_list && record.ecs_list.length > 0">
+                        <a-tag color="green">{{ record.ecs_list.length }} 台</a-tag>
+                        <a-button type="text" size="mini" @click="expandECSDetails(record)">
+                            <icon-eye />
+                        </a-button>
+                    </span>
+                    <a-tag v-else color="gray">无</a-tag>
                 </template>
 
                 <template #status="{ record }">
@@ -164,6 +178,70 @@
                         <div>更新:{{ dayjs(record.last_time).format('MM-DD HH:mm') }}</div>
                     </div>
                 </template>
+                <template #expandIcon="{ record }">
+                    <a-button type="text" size="mini" @click="toggleRowExpand(record.id)">
+                        <icon-plus v-if="!expandedRows.includes(record.id)" />
+                        <icon-minus v-else />
+                    </a-button>
+                </template>
+                <template #expand-row="{ record }">
+                    <div v-if="!record.ecs_list || record.ecs_list.length === 0" class="no-ecs-message">
+                        该账号下无ECS实例
+                    </div>
+                    <div v-else class="ecs-table-wrapper">
+                        <a-table
+                            :data="record.ecs_list"
+                            :pagination="false"
+                            bordered="cell"
+                            size="mini"
+                            stripe
+                            row-key="instance_id"
+                        >
+                            <template #columns>
+                                <a-table-column title="实例ID" data-index="instance_id" :width="170" />
+                                <a-table-column title="主机名" data-index="host_name" :width="150" />
+                                <a-table-column title="系统" data-index="os_name" :width="120" />
+                                <a-table-column title="配置" :width="100">
+                                    <template #cell="{ record }">
+                                        {{ record.cpu }}核 {{ record.memory }}GB
+                                    </template>
+                                </a-table-column>
+                                <a-table-column title="IP地址" :width="350">
+                                    <template #cell="{ record }">
+                                        <div v-if="!record.addressIps || record.addressIps.length === 0">-</div>
+                                        <div v-else class="ip-pairs-container">
+                                            <div v-for="(ip, index) in record.addressIps" :key="index" class="ip-pair">
+                                                <a-tag class="ip-tag private-ip" color="red">{{ ip.privateIp }}</a-tag>
+                                                <template v-if="ip.publicIp">
+                                                    <span class="ip-arrow">→</span>
+                                                    <a-tag class="ip-tag public-ip" color="green">{{ ip.publicIp }}</a-tag>
+                                                </template>
+                                            </div>
+                                        </div>
+                                    </template>
+                                </a-table-column>
+                                <a-table-column title="到期时间" :width="150">
+                                    <template #cell="{ record }">
+                                        <!-- 计算到期时间 -->
+                                        <div v-if="getDaysLeft(record.expired_time) <= 0" class="expired-critical">
+                                            <span>已过期</span>
+                                            <div class="expired-date">{{ formatDate(record.expired_time) }}</div>
+                                        </div>
+                                        <div v-else-if="getDaysLeft(record.expired_time) <= 7" class="expired-urgent">
+                                            <span>剩余{{ getDaysLeft(record.expired_time) }}天</span>
+                                            <div class="expired-date">{{ formatDate(record.expired_time) }}</div>
+                                        </div>
+                                        <div v-else-if="getDaysLeft(record.expired_time) <= 30" class="expired-warning">
+                                            <span>剩余{{ getDaysLeft(record.expired_time) }}天</span>
+                                            <div class="expired-date">{{ formatDate(record.expired_time) }}</div>
+                                        </div>
+                                        <span v-else>{{ formatDate(record.expired_time) }}</span>
+                                    </template>
+                                </a-table-column>
+                            </template>
+                        </a-table>
+                    </div>
+                </template>
             </a-table>
         </a-card>
     </div>
@@ -171,7 +249,7 @@
 
 <script lang="ts" setup>
 import { useRouter } from 'vue-router';
-import { computed, ref, reactive, watch, nextTick } from 'vue';
+import { computed, ref, reactive, watch, nextTick, onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
 import dayjs from 'dayjs';
 import useLoading from '@/hooks/loading';
@@ -183,15 +261,44 @@ import cloneDeep from 'lodash/cloneDeep';
 import Sortable from 'sortablejs';
 import { numberFormat, amountFormat } from '@/utils/util';
 import WorkHourModel from '@/components/WorkHourModel.vue';
+import { h } from 'vue';
+import { Table as ATable } from '@arco-design/web-vue';
 
 import {
-    AliyunAccountParams, AliyunAccountRecord, queryAccountList, UpdateAccountModel, UpdateAccount
+    AliyunAccountParams, queryAccountList, UpdateAccountModel, UpdateAccount
 } from '@/api/aliyun';
 
 
 type SizeProps = 'mini' | 'small' | 'medium' | 'large';
 type Column = TableColumnData & { checked?: true };
 
+interface ECSInstance {
+    id: number;
+    account_id: number;
+    name: string;
+    description: string;
+    create_time: string;
+    last_time: string;
+    instance_id: string;
+    instance_name: string;
+    host_name: string;
+    os_name: string;
+    region_id: string;
+    instance_type: string;
+    cpu: number;
+    memory: number;
+    expired_time: string;
+    creation_time: string;
+    public_ip?: string;
+    addressIps: {
+        networkInterfaceId: string;
+        privateIp: string;
+    }[];
+}
+
+type ExtendedAliyunAccountRecord = import('@/api/aliyun').AliyunAccountRecord & {
+    ecs_list?: ECSInstance[];
+};
 
 const generateFormModel = () => {
     return {
@@ -206,7 +313,7 @@ const router = useRouter();
 
 const { loading, setLoading } = useLoading(true);
 const { t } = useI18n();
-const renderData = ref<AliyunAccountRecord[]>([]);
+const renderData = ref<ExtendedAliyunAccountRecord[]>([]);
 const formModel = ref(generateFormModel());
 const cloneColumns = ref<Column[]>([]);
 const showColumns = ref<Column[]>([]);
@@ -239,7 +346,12 @@ const densityList = computed(() => [
     },
 ]);
 
-const columns = computed<TableColumnData[]>(() => [
+const columns = computed<any[]>(() => [
+    {
+        title: '',
+        width: 50,
+        slotName: 'expandIcon',
+    },
     {
         title: '#',
         dataIndex: 'index',
@@ -268,6 +380,17 @@ const columns = computed<TableColumnData[]>(() => [
         align: 'right',
         width: 60,
     },
+    {
+        title: 'ECS数量',
+        dataIndex: 'ecs_count',
+        width: 80,
+        render: ({ record }: any) => {
+            const count = record.ecs_list?.length || 0;
+            return count > 0 
+                ? h('a-tag', { color: 'green' }, `${count} 台`)
+                : h('a-tag', { color: 'gray' }, '无');
+        },
+    },
     {
         title: '状态',
         slotName: 'status',
@@ -308,6 +431,7 @@ const fetchData = async (
     try {
         const { data } = await queryAccountList(params);
         if (!data) return;
+        console.log('API返回数据:', data.list);
         renderData.value = data.list;
         pagination.current = data.page;
         pagination.total = data.count;
@@ -435,6 +559,123 @@ watch(
     },
     { deep: true, immediate: true }
 );
+
+// 新增状态来跟踪行的展开状态
+const expandedRows = ref<number[]>([]);
+
+// 切换行的展开状态
+const toggleRowExpand = (id: number) => {
+    const index = expandedRows.value.indexOf(id);
+    if (index === -1) {
+        expandedRows.value.push(id);
+    } else {
+        expandedRows.value.splice(index, 1);
+    }
+};
+
+// 监听expandedRows变化
+watch(expandedRows, (newVal) => {
+    console.log('expandedRows变化:', newVal);
+}, { deep: true });
+
+// 在mounted钩子中添加调试信息
+onMounted(() => {
+    console.log('初始showECSInfo状态:', showECSInfo.value);
+    
+    nextTick(() => {
+        if (renderData.value.length > 0 && showECSInfo.value) {
+            expandedRows.value = renderData.value.map(item => item.id);
+            console.log('初始化设置展开行:', expandedRows.value);
+        }
+    });
+});
+
+// 在表格数据加载后展开所有行
+watch(() => renderData.value, (newVal) => {
+    if (newVal.length > 0 && showECSInfo.value) {
+        expandedRows.value = newVal.map(item => item.id);
+    }
+}, { deep: true });
+
+// 控制显示/隐藏ECS信息的状态
+const showECSInfo = ref(localStorage.getItem('aliyun_show_ecs') === 'true');
+
+// 监听状态变化,更新localStorage
+watch(showECSInfo, (value) => {
+    localStorage.setItem('aliyun_show_ecs', String(value));
+});
+
+// 添加用于日期计算的辅助函数
+const getDaysLeft = (dateStr: string) => {
+    const expiredTime = dayjs(dateStr);
+    const now = dayjs();
+    return expiredTime.diff(now, 'day');
+};
+
+const formatDate = (dateStr: string) => {
+    return dayjs(dateStr).format('YYYY-MM-DD');
+};
+
+// 修改expandable配置
+const tableExpandable = computed(() => {
+    console.log('计算expandable属性,当前showECSInfo:', showECSInfo.value, '当前expandedRows:', expandedRows.value);
+    return {
+        expandedRowKeys: expandedRows.value,
+        // 不使用expandRowByClick,避免与单击事件冲突
+        expandRowByClick: false,
+        // 确保即使showECSInfo为false时也能展开,但默认状态是根据showECSInfo来设置
+        rowExpandable: () => true
+    };
+});
+
+const onECSInfoChange = (val: boolean) => {
+    console.log('ECS信息显示状态改变:', val);
+    localStorage.setItem('aliyun_show_ecs', String(val));
+    
+    // 强制渲染更新
+    nextTick(() => {
+        if (val) {
+            // 如果选中了显示ECS信息,则展开所有行
+            expandedRows.value = renderData.value.map(item => item.id);
+            console.log('已设置展开所有行:', expandedRows.value);
+        } else {
+            // 如果取消了显示ECS信息,则收起所有行
+            expandedRows.value = [];
+            console.log('已设置收起所有行');
+        }
+    });
+};
+
+const getRowClass = (record: any) => {
+    console.log('getRowClass called for:', record.id, 'isECSInfo:', record.isECSInfo);
+    if (record.isECSInfo) {
+        return 'ecs-info-row';
+    }
+    return '';
+};
+
+const expandECSDetails = (record: any) => {
+    console.log('点击展开按钮,记录ID:', record.id);
+    
+    // 使用对象解构获取event
+    const { event } = window;
+    if (event) {
+        event.stopPropagation();
+    }
+    
+    // 确保ID存在
+    if (!record || !record.id) return;
+    
+    // 切换展开状态
+    const index = expandedRows.value.indexOf(record.id);
+    if (index === -1) {
+        expandedRows.value.push(record.id);
+        console.log('展开行:', record.id, '当前展开行:', expandedRows.value);
+    } else {
+        expandedRows.value.splice(index, 1);
+        console.log('收起行:', record.id, '当前展开行:', expandedRows.value);
+    }
+};
 </script>
 
 <script lang="ts">
@@ -476,4 +717,153 @@ export default {
         cursor: pointer;
     }
 }
+
+:deep(.arco-table-tr-expand) {
+    background-color: #f0f9ff;
+}
+
+:deep(.arco-table-expanded-row-cell) {
+    padding: 12px 20px;
+    border: 1px solid #d9ecff;
+    border-radius: 4px;
+    margin: 8px 0;
+}
+
+.no-ecs-message {
+    color: #999;
+    padding: 12px 0;
+    font-size: 14px;
+}
+
+.no-ecs-message.error {
+    color: #f56c6c;
+}
+
+:deep(.arco-table-container) {
+    border-radius: 4px;
+}
+
+:deep(.arco-table-stripe) .arco-table-tr:nth-child(even) {
+    background-color: #fafafa;
+}
+
+/* 更改展开行单元格样式 */
+:deep(.arco-table-expand-content) {
+    padding: 16px;
+    background-color: #f9f9f9;
+}
+
+:deep(.arco-table-td.arco-table-expand-cell) {
+    padding: 0;
+}
+
+:deep(.arco-table-expanded-row) {
+    .arco-table-td {
+        border-bottom: 1px solid #e9e9e9;
+    }
+}
+
+/* 确保嵌套表格样式正确 */
+.ecs-table-wrapper {
+    margin: 0;
+    width: 100%;
+    
+    .arco-table {
+        margin-bottom: 0;
+    }
+}
+
+/* 改变展开行的背景颜色,让它与主表区分开 */
+:deep(.arco-table-tr-expand) {
+    background-color: #f0f9ff;
+}
+
+/* 确保展开行中的表格占满整行 */
+:deep(.arco-table-expanded-row-cell) {
+    padding: 0 !important;
+    /* 重要:这会使展开行内容横跨整行 */
+    display: table-cell;
+    width: 100%;
+}
+
+/* 控制展开行的显示/隐藏 */
+:deep(.arco-table-expanded-row) {
+    display: v-bind('showECSInfo ? "table-row" : "none"');
+}
+
+/* 控制ECS信息行的显示/隐藏 - 更简单的版本 */
+:deep(.ecs-info-row) {
+    display: none;
+}
+
+:deep(.show-ecs-info .ecs-info-row) {
+    display: table-row;
+}
+
+/* 改进IP地址显示样式 */
+.ip-pairs-container {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+
+.ip-pair {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.ip-tag {
+    font-family: monospace; /* 使用等宽字体更适合显示IP地址 */
+    min-width: 120px;
+    text-align: center;
+    padding: 2px 8px;
+}
+
+.ip-arrow {
+    color: #909399;
+    font-weight: bold;
+}
+
+/* 修改到期时间样式,移除动画效果 */
+.expired-critical {
+    color: #f56c6c;     /* 红色 */
+    font-size: 14px;
+    font-weight: bold;
+    text-align: center;
+    /* 移除 animation: expired-pulse 1s infinite; */
+}
+
+.expired-urgent {
+    color: #f56c6c;     /* 红色 */
+    font-weight: bold;
+    text-align: center;
+    /* 移除 animation: expired-soon-pulse 1.5s infinite; */
+}
+
+.expired-warning {
+    color: #e6a23c;     /* 橙色/黄色 */
+    font-weight: bold;
+    text-align: center;
+}
+
+.expired-date {
+    font-size: 12px;
+    margin-top: 4px;
+}
+
+/* 删除这些动画关键帧定义,因为不再需要 */
+/*
+@keyframes expired-pulse {
+    0% { transform: scale(1); opacity: 1; }
+    50% { transform: scale(1.05); opacity: 0.7; }
+    100% { transform: scale(1); opacity: 1; }
+}
+
+@keyframes expired-soon-pulse {
+    0% { opacity: 1; }
+    50% { opacity: 0.6; }
+    100% { opacity: 1; }
+}
+*/
 </style>

+ 0 - 0
{{ -i