aliyun.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869
  1. <template>
  2. <div class="container">
  3. <Breadcrumb :items="['menu.jd', 'menu.jd.list']" />
  4. <a-card class="general-card" :title="$t('menu.jd.list')">
  5. <a-row>
  6. <a-col :flex="1">
  7. <a-form :model="formModel" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
  8. label-align="left">
  9. <a-row :gutter="16">
  10. <a-col :span="8">
  11. <a-form-item field="name" label="账号名">
  12. <a-input v-model="formModel.name" placeholder="输入关键词搜索" />
  13. </a-form-item>
  14. </a-col>
  15. <a-col :span="8">
  16. <a-form-item field="lastTime" :label="$t('searchTable.form.lastTime')">
  17. <a-range-picker v-model="formModel.lastTime" style="width: 100%" />
  18. </a-form-item>
  19. </a-col>
  20. <a-col :span="8">
  21. <a-form-item field="status" :label="$t('searchTable.form.status')">
  22. <a-select v-model="formModel.status" :options="statusOptions" :placeholder="$t('searchTable.form.selectDefault')
  23. " />
  24. </a-form-item>
  25. </a-col>
  26. </a-row>
  27. </a-form>
  28. </a-col>
  29. <a-divider style="height: 38px" direction="vertical" />
  30. <a-col :flex="'86px'" style="text-align: right">
  31. <a-space direction="vertical" :size="18">
  32. <a-button type="primary" @click="search">
  33. <template #icon>
  34. <icon-search />
  35. </template>
  36. {{ $t('searchTable.form.search') }}
  37. </a-button>
  38. </a-space>
  39. </a-col>
  40. </a-row>
  41. <a-divider style="margin-top: 0" />
  42. <a-row style="margin-bottom: 16px">
  43. <a-col :span="12">
  44. <a-space>
  45. <a-button type="primary">
  46. <template #icon>
  47. <icon-plus />
  48. </template>
  49. {{ $t('searchTable.operation.create') }}
  50. </a-button>
  51. <a-checkbox v-model="showECSInfo" @change="onECSInfoChange">
  52. 显示ECS主机信息
  53. </a-checkbox>
  54. </a-space>
  55. </a-col>
  56. <a-col :span="12" style="
  57. display: flex;
  58. align-items: center;
  59. justify-content: end;
  60. ">
  61. <a-tooltip :content="$t('searchTable.actions.refresh')">
  62. <div class="action-icon" @click="search"><icon-refresh size="18" /></div>
  63. </a-tooltip>
  64. <a-dropdown @select="handleSelectDensity">
  65. <a-tooltip :content="$t('searchTable.actions.density')">
  66. <div class="action-icon"><icon-line-height size="18" /></div>
  67. </a-tooltip>
  68. <template #content>
  69. <a-doption v-for="item in densityList" :key="item.value" :value="item.value"
  70. :class="{ active: item.value === size }">
  71. <span>{{ item.name }}</span>
  72. </a-doption>
  73. </template>
  74. </a-dropdown>
  75. <a-tooltip :content="$t('searchTable.actions.columnSetting')">
  76. <a-popover trigger="click" position="bl" @popup-visible-change="popupVisibleChange">
  77. <div class="action-icon"><icon-settings size="18" /></div>
  78. <template #content>
  79. <div id="tableSetting">
  80. <div v-for="(item, index) in showColumns" :key="item.dataIndex" class="setting">
  81. <div style="
  82. margin-right: 4px;
  83. cursor: move;
  84. ">
  85. <icon-drag-arrow />
  86. </div>
  87. <div>
  88. <a-checkbox v-model="item.checked" @change="
  89. handleChange(
  90. $event,
  91. item as TableColumnData,
  92. index
  93. )
  94. ">
  95. </a-checkbox>
  96. </div>
  97. <div class="title">
  98. {{
  99. item.title === '#'
  100. ? '序列号'
  101. : item.title
  102. }}
  103. </div>
  104. </div>
  105. </div>
  106. </template>
  107. </a-popover>
  108. </a-tooltip>
  109. </a-col>
  110. </a-row>
  111. <a-table row-key="id" :loading="loading" :pagination="pagination"
  112. :columns="(cloneColumns as TableColumnData[])" :data="renderData" :bordered="{ headerCell: true }"
  113. :size="size" @page-change="onPageChange"
  114. :expandable="tableExpandable" :row-class="getRowClass"
  115. :class="{ 'show-ecs-info': showECSInfo }">
  116. <template #index="{ record }">
  117. {{ record.id }}
  118. </template>
  119. <template #name="{ record }">
  120. <div>{{ record.name }}</div>
  121. <a-typography-paragraph :ellipsis="{ rows: 2 }">
  122. <a-typography-text disabled>
  123. {{ record.description }}
  124. </a-typography-text>
  125. </a-typography-paragraph>
  126. </template>
  127. <template #company="{ record }">
  128. <div>{{ record.company }}</div>
  129. <a-typography-text disabled>
  130. {{ record.mobile }}
  131. </a-typography-text>
  132. </template>
  133. <template #balance="{ record }">
  134. <a-tag color="red" v-if="record.balance <= record.balance_alert_threshold"
  135. @click="changeAttr(record.id, 'balance', true)">{{ record.balance }}
  136. </a-tag>
  137. <a-tag color="blue" v-else @click="changeAttr(record.id, 'balance', true)">{{ record.balance }}
  138. </a-tag>
  139. </template>
  140. <template #balance_alert_threshold="{ record }">
  141. <a-tag color="red" v-if="record.balance <= record.balance_alert_threshold">{{
  142. record.balance_alert_threshold }}
  143. </a-tag>
  144. <a-tag color="blue" v-else>{{ record.balance_alert_threshold }} </a-tag>
  145. </template>
  146. <template #ecs="{ record }">
  147. <span v-if="record.ecs_list && record.ecs_list.length > 0">
  148. <a-tag color="green">{{ record.ecs_list.length }} 台</a-tag>
  149. <a-button type="text" size="mini" @click="expandECSDetails(record)">
  150. <icon-eye />
  151. </a-button>
  152. </span>
  153. <a-tag v-else color="gray">无</a-tag>
  154. </template>
  155. <template #status="{ record }">
  156. <a-tag v-if="record.status" bordered color="green">
  157. <template #icon>
  158. <icon-check />
  159. </template>
  160. 正常
  161. </a-tag>
  162. <a-tag v-else bordered color="">
  163. <template #icon>
  164. <icon-stop />
  165. </template>
  166. 禁用
  167. </a-tag>
  168. </template>
  169. <template #time="{ record }">
  170. <div style="color:gray;">
  171. <div>添加:{{ dayjs(record.create_time).format('MM-DD HH:mm') }}</div>
  172. <div>更新:{{ dayjs(record.last_time).format('MM-DD HH:mm') }}</div>
  173. </div>
  174. </template>
  175. <template #expandIcon="{ record }">
  176. <a-button type="text" size="mini" @click="toggleRowExpand(record.id)">
  177. <icon-plus v-if="!expandedRows.includes(record.id)" />
  178. <icon-minus v-else />
  179. </a-button>
  180. </template>
  181. <template #expand-row="{ record }">
  182. <div v-if="!record.ecs_list || record.ecs_list.length === 0" class="no-ecs-message">
  183. 该账号下无ECS实例
  184. </div>
  185. <div v-else class="ecs-table-wrapper">
  186. <a-table
  187. :data="record.ecs_list"
  188. :pagination="false"
  189. bordered="cell"
  190. size="mini"
  191. stripe
  192. row-key="instance_id"
  193. >
  194. <template #columns>
  195. <a-table-column title="实例ID" data-index="instance_id" :width="170" />
  196. <a-table-column title="主机名" data-index="host_name" :width="150" />
  197. <a-table-column title="系统" data-index="os_name" :width="120" />
  198. <a-table-column title="配置" :width="100">
  199. <template #cell="{ record }">
  200. {{ record.cpu }}核 {{ record.memory }}GB
  201. </template>
  202. </a-table-column>
  203. <a-table-column title="IP地址" :width="350">
  204. <template #cell="{ record }">
  205. <div v-if="!record.addressIps || record.addressIps.length === 0">-</div>
  206. <div v-else class="ip-pairs-container">
  207. <div v-for="(ip, index) in record.addressIps" :key="index" class="ip-pair">
  208. <a-tag class="ip-tag private-ip" color="red">{{ ip.privateIp }}</a-tag>
  209. <template v-if="ip.publicIp">
  210. <span class="ip-arrow">→</span>
  211. <a-tag class="ip-tag public-ip" color="green">{{ ip.publicIp }}</a-tag>
  212. </template>
  213. </div>
  214. </div>
  215. </template>
  216. </a-table-column>
  217. <a-table-column title="到期时间" :width="150">
  218. <template #cell="{ record }">
  219. <!-- 计算到期时间 -->
  220. <div v-if="getDaysLeft(record.expired_time) <= 0" class="expired-critical">
  221. <span>已过期</span>
  222. <div class="expired-date">{{ formatDate(record.expired_time) }}</div>
  223. </div>
  224. <div v-else-if="getDaysLeft(record.expired_time) <= 7" class="expired-urgent">
  225. <span>剩余{{ getDaysLeft(record.expired_time) }}天</span>
  226. <div class="expired-date">{{ formatDate(record.expired_time) }}</div>
  227. </div>
  228. <div v-else-if="getDaysLeft(record.expired_time) <= 30" class="expired-warning">
  229. <span>剩余{{ getDaysLeft(record.expired_time) }}天</span>
  230. <div class="expired-date">{{ formatDate(record.expired_time) }}</div>
  231. </div>
  232. <span v-else>{{ formatDate(record.expired_time) }}</span>
  233. </template>
  234. </a-table-column>
  235. </template>
  236. </a-table>
  237. </div>
  238. </template>
  239. </a-table>
  240. </a-card>
  241. </div>
  242. </template>
  243. <script lang="ts" setup>
  244. import { useRouter } from 'vue-router';
  245. import { computed, ref, reactive, watch, nextTick, onMounted } from 'vue';
  246. import { useI18n } from 'vue-i18n';
  247. import dayjs from 'dayjs';
  248. import useLoading from '@/hooks/loading';
  249. import { Pagination } from '@/types/global';
  250. import { Modal, Message } from '@arco-design/web-vue';
  251. import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
  252. import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
  253. import cloneDeep from 'lodash/cloneDeep';
  254. import Sortable from 'sortablejs';
  255. import { numberFormat, amountFormat } from '@/utils/util';
  256. import WorkHourModel from '@/components/WorkHourModel.vue';
  257. import { h } from 'vue';
  258. import { Table as ATable } from '@arco-design/web-vue';
  259. import {
  260. AliyunAccountParams, queryAccountList, UpdateAccountModel, UpdateAccount
  261. } from '@/api/aliyun';
  262. type SizeProps = 'mini' | 'small' | 'medium' | 'large';
  263. type Column = TableColumnData & { checked?: true };
  264. interface ECSInstance {
  265. id: number;
  266. account_id: number;
  267. name: string;
  268. description: string;
  269. create_time: string;
  270. last_time: string;
  271. instance_id: string;
  272. instance_name: string;
  273. host_name: string;
  274. os_name: string;
  275. region_id: string;
  276. instance_type: string;
  277. cpu: number;
  278. memory: number;
  279. expired_time: string;
  280. creation_time: string;
  281. public_ip?: string;
  282. addressIps: {
  283. networkInterfaceId: string;
  284. privateIp: string;
  285. }[];
  286. }
  287. type ExtendedAliyunAccountRecord = import('@/api/aliyun').AliyunAccountRecord & {
  288. ecs_list?: ECSInstance[];
  289. };
  290. const generateFormModel = () => {
  291. return {
  292. name: '',
  293. lastTime: [],
  294. status: '',
  295. sort: 'status',
  296. order: 'descending',
  297. };
  298. };
  299. const router = useRouter();
  300. const { loading, setLoading } = useLoading(true);
  301. const { t } = useI18n();
  302. const renderData = ref<ExtendedAliyunAccountRecord[]>([]);
  303. const formModel = ref(generateFormModel());
  304. const cloneColumns = ref<Column[]>([]);
  305. const showColumns = ref<Column[]>([]);
  306. const size = ref<SizeProps>('small');
  307. const basePagination: Pagination = {
  308. current: 1,
  309. pageSize: 50,
  310. };
  311. const pagination = reactive({
  312. ...basePagination,
  313. });
  314. const densityList = computed(() => [
  315. {
  316. name: t('searchTable.size.mini'),
  317. value: 'mini',
  318. },
  319. {
  320. name: t('searchTable.size.small'),
  321. value: 'small',
  322. },
  323. {
  324. name: t('searchTable.size.medium'),
  325. value: 'medium',
  326. },
  327. {
  328. name: t('searchTable.size.large'),
  329. value: 'large',
  330. },
  331. ]);
  332. const columns = computed<any[]>(() => [
  333. {
  334. title: '',
  335. width: 50,
  336. slotName: 'expandIcon',
  337. },
  338. {
  339. title: '#',
  340. dataIndex: 'index',
  341. slotName: 'index',
  342. width: 50,
  343. },
  344. {
  345. title: '账号名',
  346. slotName: 'name',
  347. width: 250,
  348. },
  349. {
  350. title: '认证主体',
  351. slotName: 'company',
  352. width: 250,
  353. },
  354. {
  355. title: '账户余额',
  356. slotName: 'balance',
  357. align: 'right',
  358. width: 60,
  359. },
  360. {
  361. title: '余额警戒线',
  362. slotName: 'balance_alert_threshold',
  363. align: 'right',
  364. width: 60,
  365. },
  366. {
  367. title: 'ECS数量',
  368. dataIndex: 'ecs_count',
  369. width: 80,
  370. render: ({ record }: any) => {
  371. const count = record.ecs_list?.length || 0;
  372. return count > 0
  373. ? h('a-tag', { color: 'green' }, `${count} 台`)
  374. : h('a-tag', { color: 'gray' }, '无');
  375. },
  376. },
  377. {
  378. title: '状态',
  379. slotName: 'status',
  380. width: 100,
  381. },
  382. {
  383. title: '时间',
  384. slotName: 'time',
  385. width: 170,
  386. },
  387. ]);
  388. const statusOptions = computed<SelectOptionData[]>(() => [
  389. {
  390. label: '全部',
  391. value: '',
  392. },
  393. {
  394. label: '正常',
  395. value: '1',
  396. },
  397. {
  398. label: '禁用',
  399. value: '0',
  400. },
  401. ]);
  402. const fetchData = async (
  403. params: AliyunAccountParams = {
  404. current: 1,
  405. pageSize: 50,
  406. sort: 'status',
  407. order: 'descending',
  408. getTotal: true,
  409. }
  410. ) => {
  411. setLoading(true);
  412. try {
  413. const { data } = await queryAccountList(params);
  414. if (!data) return;
  415. console.log('API返回数据:', data.list);
  416. renderData.value = data.list;
  417. pagination.current = data.page;
  418. pagination.total = data.count;
  419. } catch (err) {
  420. console.log(err);
  421. } finally {
  422. setLoading(false);
  423. }
  424. };
  425. const changeAttr = async (id: number, name: string, val: any) => {
  426. const args = {
  427. id,
  428. name,
  429. val
  430. } as UpdateAccountModel;
  431. Modal.confirm({
  432. title: '确认提示',
  433. content: '确定更新操作吗?',
  434. okText: '确认',
  435. cancelText: '取消',
  436. async onOk() {
  437. setLoading(true);
  438. try {
  439. const { data } = await UpdateAccount(args);
  440. console.log(data, data.msg);
  441. const msg = data.msg || '更新成功!';
  442. if (data.success) {
  443. Message.success(msg);
  444. fetchData();
  445. } else {
  446. Message.error(data.msg);
  447. }
  448. } catch (e) { console.log(e) }
  449. setLoading(false);
  450. }
  451. });
  452. };
  453. const search = () => {
  454. fetchData({
  455. ...basePagination,
  456. ...formModel.value,
  457. } as unknown as AliyunAccountParams);
  458. };
  459. const onPageChange = (current: number) => {
  460. fetchData({ ...basePagination, current });
  461. };
  462. fetchData();
  463. const reset = () => {
  464. formModel.value = generateFormModel();
  465. };
  466. const handleSelectDensity = (
  467. val: string | number | Record<string, any> | undefined,
  468. e: Event
  469. ) => {
  470. size.value = val as SizeProps;
  471. };
  472. const handleChange = (
  473. checked: boolean | (string | boolean | number)[],
  474. column: Column,
  475. index: number
  476. ) => {
  477. if (!checked) {
  478. cloneColumns.value = showColumns.value.filter(
  479. (item) => item.dataIndex !== column.dataIndex
  480. );
  481. } else {
  482. cloneColumns.value.splice(index, 0, column);
  483. }
  484. };
  485. const exchangeArray = <T extends Array<any>>(
  486. array: T,
  487. beforeIdx: number,
  488. newIdx: number,
  489. isDeep = false
  490. ): T => {
  491. const newArray = isDeep ? cloneDeep(array) : array;
  492. if (beforeIdx > -1 && newIdx > -1) {
  493. // 先替换后面的,然后拿到替换的结果替换前面的
  494. newArray.splice(
  495. beforeIdx,
  496. 1,
  497. newArray.splice(newIdx, 1, newArray[beforeIdx]).pop()
  498. );
  499. }
  500. return newArray;
  501. };
  502. const popupVisibleChange = (val: boolean) => {
  503. if (val) {
  504. nextTick(() => {
  505. const el = document.getElementById(
  506. 'tableSetting'
  507. ) as HTMLElement;
  508. const sortable = new Sortable(el, {
  509. onEnd(e: any) {
  510. const { oldIndex, newIndex } = e;
  511. exchangeArray(cloneColumns.value, oldIndex, newIndex);
  512. exchangeArray(showColumns.value, oldIndex, newIndex);
  513. },
  514. });
  515. });
  516. }
  517. };
  518. watch(
  519. () => columns.value,
  520. (val) => {
  521. cloneColumns.value = cloneDeep(val);
  522. cloneColumns.value.forEach((item, index) => {
  523. item.checked = true;
  524. });
  525. showColumns.value = cloneDeep(cloneColumns.value);
  526. },
  527. { deep: true, immediate: true }
  528. );
  529. // 新增状态来跟踪行的展开状态
  530. const expandedRows = ref<number[]>([]);
  531. // 切换行的展开状态
  532. const toggleRowExpand = (id: number) => {
  533. const index = expandedRows.value.indexOf(id);
  534. if (index === -1) {
  535. expandedRows.value.push(id);
  536. } else {
  537. expandedRows.value.splice(index, 1);
  538. }
  539. };
  540. // 监听expandedRows变化
  541. watch(expandedRows, (newVal) => {
  542. console.log('expandedRows变化:', newVal);
  543. }, { deep: true });
  544. // 在mounted钩子中添加调试信息
  545. onMounted(() => {
  546. console.log('初始showECSInfo状态:', showECSInfo.value);
  547. nextTick(() => {
  548. if (renderData.value.length > 0 && showECSInfo.value) {
  549. expandedRows.value = renderData.value.map(item => item.id);
  550. console.log('初始化设置展开行:', expandedRows.value);
  551. }
  552. });
  553. });
  554. // 在表格数据加载后展开所有行
  555. watch(() => renderData.value, (newVal) => {
  556. if (newVal.length > 0 && showECSInfo.value) {
  557. expandedRows.value = newVal.map(item => item.id);
  558. }
  559. }, { deep: true });
  560. // 控制显示/隐藏ECS信息的状态
  561. const showECSInfo = ref(localStorage.getItem('aliyun_show_ecs') === 'true');
  562. // 监听状态变化,更新localStorage
  563. watch(showECSInfo, (value) => {
  564. localStorage.setItem('aliyun_show_ecs', String(value));
  565. });
  566. // 添加用于日期计算的辅助函数
  567. const getDaysLeft = (dateStr: string) => {
  568. const expiredTime = dayjs(dateStr);
  569. const now = dayjs();
  570. return expiredTime.diff(now, 'day');
  571. };
  572. const formatDate = (dateStr: string) => {
  573. return dayjs(dateStr).format('YYYY-MM-DD');
  574. };
  575. // 修改expandable配置
  576. const tableExpandable = computed(() => {
  577. console.log('计算expandable属性,当前showECSInfo:', showECSInfo.value, '当前expandedRows:', expandedRows.value);
  578. return {
  579. expandedRowKeys: expandedRows.value,
  580. // 不使用expandRowByClick,避免与单击事件冲突
  581. expandRowByClick: false,
  582. // 确保即使showECSInfo为false时也能展开,但默认状态是根据showECSInfo来设置
  583. rowExpandable: () => true
  584. };
  585. });
  586. const onECSInfoChange = (val: boolean) => {
  587. console.log('ECS信息显示状态改变:', val);
  588. localStorage.setItem('aliyun_show_ecs', String(val));
  589. // 强制渲染更新
  590. nextTick(() => {
  591. if (val) {
  592. // 如果选中了显示ECS信息,则展开所有行
  593. expandedRows.value = renderData.value.map(item => item.id);
  594. console.log('已设置展开所有行:', expandedRows.value);
  595. } else {
  596. // 如果取消了显示ECS信息,则收起所有行
  597. expandedRows.value = [];
  598. console.log('已设置收起所有行');
  599. }
  600. });
  601. };
  602. const getRowClass = (record: any) => {
  603. console.log('getRowClass called for:', record.id, 'isECSInfo:', record.isECSInfo);
  604. if (record.isECSInfo) {
  605. return 'ecs-info-row';
  606. }
  607. return '';
  608. };
  609. const expandECSDetails = (record: any) => {
  610. console.log('点击展开按钮,记录ID:', record.id);
  611. // 使用对象解构获取event
  612. const { event } = window;
  613. if (event) {
  614. event.stopPropagation();
  615. }
  616. // 确保ID存在
  617. if (!record || !record.id) return;
  618. // 切换展开状态
  619. const index = expandedRows.value.indexOf(record.id);
  620. if (index === -1) {
  621. expandedRows.value.push(record.id);
  622. console.log('展开行:', record.id, '当前展开行:', expandedRows.value);
  623. } else {
  624. expandedRows.value.splice(index, 1);
  625. console.log('收起行:', record.id, '当前展开行:', expandedRows.value);
  626. }
  627. };
  628. </script>
  629. <script lang="ts">
  630. export default {
  631. name: 'SearchTable',
  632. };
  633. </script>
  634. <style scoped lang="less">
  635. .container {
  636. padding: 0 20px 20px 20px;
  637. }
  638. :deep(.arco-table-th) {
  639. &:last-child {
  640. .arco-table-th-item-title {
  641. margin-left: 16px;
  642. }
  643. }
  644. }
  645. .action-icon {
  646. margin-left: 12px;
  647. cursor: pointer;
  648. }
  649. .active {
  650. color: #0960bd;
  651. background-color: #e3f4fc;
  652. }
  653. .setting {
  654. display: flex;
  655. align-items: center;
  656. width: 200px;
  657. .title {
  658. margin-left: 12px;
  659. cursor: pointer;
  660. }
  661. }
  662. :deep(.arco-table-tr-expand) {
  663. background-color: #f0f9ff;
  664. }
  665. :deep(.arco-table-expanded-row-cell) {
  666. padding: 12px 20px;
  667. border: 1px solid #d9ecff;
  668. border-radius: 4px;
  669. margin: 8px 0;
  670. }
  671. .no-ecs-message {
  672. color: #999;
  673. padding: 12px 0;
  674. font-size: 14px;
  675. }
  676. .no-ecs-message.error {
  677. color: #f56c6c;
  678. }
  679. :deep(.arco-table-container) {
  680. border-radius: 4px;
  681. }
  682. :deep(.arco-table-stripe) .arco-table-tr:nth-child(even) {
  683. background-color: #fafafa;
  684. }
  685. /* 更改展开行单元格样式 */
  686. :deep(.arco-table-expand-content) {
  687. padding: 16px;
  688. background-color: #f9f9f9;
  689. }
  690. :deep(.arco-table-td.arco-table-expand-cell) {
  691. padding: 0;
  692. }
  693. :deep(.arco-table-expanded-row) {
  694. .arco-table-td {
  695. border-bottom: 1px solid #e9e9e9;
  696. }
  697. }
  698. /* 确保嵌套表格样式正确 */
  699. .ecs-table-wrapper {
  700. margin: 0;
  701. width: 100%;
  702. .arco-table {
  703. margin-bottom: 0;
  704. }
  705. }
  706. /* 改变展开行的背景颜色,让它与主表区分开 */
  707. :deep(.arco-table-tr-expand) {
  708. background-color: #f0f9ff;
  709. }
  710. /* 确保展开行中的表格占满整行 */
  711. :deep(.arco-table-expanded-row-cell) {
  712. padding: 0 !important;
  713. /* 重要:这会使展开行内容横跨整行 */
  714. display: table-cell;
  715. width: 100%;
  716. }
  717. /* 控制展开行的显示/隐藏 */
  718. :deep(.arco-table-expanded-row) {
  719. display: v-bind('showECSInfo ? "table-row" : "none"');
  720. }
  721. /* 控制ECS信息行的显示/隐藏 - 更简单的版本 */
  722. :deep(.ecs-info-row) {
  723. display: none;
  724. }
  725. :deep(.show-ecs-info .ecs-info-row) {
  726. display: table-row;
  727. }
  728. /* 改进IP地址显示样式 */
  729. .ip-pairs-container {
  730. display: flex;
  731. flex-direction: column;
  732. gap: 8px;
  733. }
  734. .ip-pair {
  735. display: flex;
  736. align-items: center;
  737. gap: 8px;
  738. }
  739. .ip-tag {
  740. font-family: monospace; /* 使用等宽字体更适合显示IP地址 */
  741. min-width: 120px;
  742. text-align: center;
  743. padding: 2px 8px;
  744. }
  745. .ip-arrow {
  746. color: #909399;
  747. font-weight: bold;
  748. }
  749. /* 修改到期时间样式,移除动画效果 */
  750. .expired-critical {
  751. color: #f56c6c; /* 红色 */
  752. font-size: 14px;
  753. font-weight: bold;
  754. text-align: center;
  755. /* 移除 animation: expired-pulse 1s infinite; */
  756. }
  757. .expired-urgent {
  758. color: #f56c6c; /* 红色 */
  759. font-weight: bold;
  760. text-align: center;
  761. /* 移除 animation: expired-soon-pulse 1.5s infinite; */
  762. }
  763. .expired-warning {
  764. color: #e6a23c; /* 橙色/黄色 */
  765. font-weight: bold;
  766. text-align: center;
  767. }
  768. .expired-date {
  769. font-size: 12px;
  770. margin-top: 4px;
  771. }
  772. /* 删除这些动画关键帧定义,因为不再需要 */
  773. /*
  774. @keyframes expired-pulse {
  775. 0% { transform: scale(1); opacity: 1; }
  776. 50% { transform: scale(1.05); opacity: 0.7; }
  777. 100% { transform: scale(1); opacity: 1; }
  778. }
  779. @keyframes expired-soon-pulse {
  780. 0% { opacity: 1; }
  781. 50% { opacity: 0.6; }
  782. 100% { opacity: 1; }
  783. }
  784. */
  785. </style>