| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869 |
- <template>
- <div class="container">
- <Breadcrumb :items="['menu.jd', 'menu.jd.list']" />
- <a-card class="general-card" :title="$t('menu.jd.list')">
- <a-row>
- <a-col :flex="1">
- <a-form :model="formModel" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
- label-align="left">
- <a-row :gutter="16">
- <a-col :span="8">
- <a-form-item field="name" label="账号名">
- <a-input v-model="formModel.name" placeholder="输入关键词搜索" />
- </a-form-item>
- </a-col>
- <a-col :span="8">
- <a-form-item field="lastTime" :label="$t('searchTable.form.lastTime')">
- <a-range-picker v-model="formModel.lastTime" style="width: 100%" />
- </a-form-item>
- </a-col>
- <a-col :span="8">
- <a-form-item field="status" :label="$t('searchTable.form.status')">
- <a-select v-model="formModel.status" :options="statusOptions" :placeholder="$t('searchTable.form.selectDefault')
- " />
- </a-form-item>
- </a-col>
- </a-row>
- </a-form>
- </a-col>
- <a-divider style="height: 38px" direction="vertical" />
- <a-col :flex="'86px'" style="text-align: right">
- <a-space direction="vertical" :size="18">
- <a-button type="primary" @click="search">
- <template #icon>
- <icon-search />
- </template>
- {{ $t('searchTable.form.search') }}
- </a-button>
- </a-space>
- </a-col>
- </a-row>
- <a-divider style="margin-top: 0" />
- <a-row style="margin-bottom: 16px">
- <a-col :span="12">
- <a-space>
- <a-button type="primary">
- <template #icon>
- <icon-plus />
- </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="
- display: flex;
- align-items: center;
- justify-content: end;
- ">
- <a-tooltip :content="$t('searchTable.actions.refresh')">
- <div class="action-icon" @click="search"><icon-refresh size="18" /></div>
- </a-tooltip>
- <a-dropdown @select="handleSelectDensity">
- <a-tooltip :content="$t('searchTable.actions.density')">
- <div class="action-icon"><icon-line-height size="18" /></div>
- </a-tooltip>
- <template #content>
- <a-doption v-for="item in densityList" :key="item.value" :value="item.value"
- :class="{ active: item.value === size }">
- <span>{{ item.name }}</span>
- </a-doption>
- </template>
- </a-dropdown>
- <a-tooltip :content="$t('searchTable.actions.columnSetting')">
- <a-popover trigger="click" position="bl" @popup-visible-change="popupVisibleChange">
- <div class="action-icon"><icon-settings size="18" /></div>
- <template #content>
- <div id="tableSetting">
- <div v-for="(item, index) in showColumns" :key="item.dataIndex" class="setting">
- <div style="
- margin-right: 4px;
- cursor: move;
- ">
- <icon-drag-arrow />
- </div>
- <div>
- <a-checkbox v-model="item.checked" @change="
- handleChange(
- $event,
- item as TableColumnData,
- index
- )
- ">
- </a-checkbox>
- </div>
- <div class="title">
- {{
- item.title === '#'
- ? '序列号'
- : item.title
- }}
- </div>
- </div>
- </div>
- </template>
- </a-popover>
- </a-tooltip>
- </a-col>
- </a-row>
- <a-table row-key="id" :loading="loading" :pagination="pagination"
- :columns="(cloneColumns as TableColumnData[])" :data="renderData" :bordered="{ headerCell: true }"
- :size="size" @page-change="onPageChange"
- :expandable="tableExpandable" :row-class="getRowClass"
- :class="{ 'show-ecs-info': showECSInfo }">
- <template #index="{ record }">
- {{ record.id }}
- </template>
- <template #name="{ record }">
- <div>{{ record.name }}</div>
- <a-typography-paragraph :ellipsis="{ rows: 2 }">
- <a-typography-text disabled>
- {{ record.description }}
- </a-typography-text>
- </a-typography-paragraph>
- </template>
- <template #company="{ record }">
- <div>{{ record.company }}</div>
- <a-typography-text disabled>
- {{ record.mobile }}
- </a-typography-text>
- </template>
- <template #balance="{ record }">
- <a-tag color="red" v-if="record.balance <= record.balance_alert_threshold"
- @click="changeAttr(record.id, 'balance', true)">{{ record.balance }}
- </a-tag>
- <a-tag color="blue" v-else @click="changeAttr(record.id, 'balance', true)">{{ record.balance }}
- </a-tag>
- </template>
- <template #balance_alert_threshold="{ record }">
- <a-tag color="red" v-if="record.balance <= record.balance_alert_threshold">{{
- 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 }">
- <a-tag v-if="record.status" bordered color="green">
- <template #icon>
- <icon-check />
- </template>
- 正常
- </a-tag>
- <a-tag v-else bordered color="">
- <template #icon>
- <icon-stop />
- </template>
- 禁用
- </a-tag>
- </template>
- <template #time="{ record }">
- <div style="color:gray;">
- <div>添加:{{ dayjs(record.create_time).format('MM-DD HH:mm') }}</div>
- <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>
- </template>
- <script lang="ts" setup>
- import { useRouter } from 'vue-router';
- import { computed, ref, reactive, watch, nextTick, onMounted } from 'vue';
- import { useI18n } from 'vue-i18n';
- import dayjs from 'dayjs';
- import useLoading from '@/hooks/loading';
- import { Pagination } from '@/types/global';
- import { Modal, Message } from '@arco-design/web-vue';
- import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
- import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
- 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, 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 {
- name: '',
- lastTime: [],
- status: '',
- sort: 'status',
- order: 'descending',
- };
- };
- const router = useRouter();
- const { loading, setLoading } = useLoading(true);
- const { t } = useI18n();
- const renderData = ref<ExtendedAliyunAccountRecord[]>([]);
- const formModel = ref(generateFormModel());
- const cloneColumns = ref<Column[]>([]);
- const showColumns = ref<Column[]>([]);
- const size = ref<SizeProps>('small');
- const basePagination: Pagination = {
- current: 1,
- pageSize: 50,
- };
- const pagination = reactive({
- ...basePagination,
- });
- const densityList = computed(() => [
- {
- name: t('searchTable.size.mini'),
- value: 'mini',
- },
- {
- name: t('searchTable.size.small'),
- value: 'small',
- },
- {
- name: t('searchTable.size.medium'),
- value: 'medium',
- },
- {
- name: t('searchTable.size.large'),
- value: 'large',
- },
- ]);
- const columns = computed<any[]>(() => [
- {
- title: '',
- width: 50,
- slotName: 'expandIcon',
- },
- {
- title: '#',
- dataIndex: 'index',
- slotName: 'index',
- width: 50,
- },
- {
- title: '账号名',
- slotName: 'name',
- width: 250,
- },
- {
- title: '认证主体',
- slotName: 'company',
- width: 250,
- },
- {
- title: '账户余额',
- slotName: 'balance',
- align: 'right',
- width: 60,
- },
- {
- title: '余额警戒线',
- slotName: 'balance_alert_threshold',
- 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',
- width: 100,
- },
- {
- title: '时间',
- slotName: 'time',
- width: 170,
- },
- ]);
- const statusOptions = computed<SelectOptionData[]>(() => [
- {
- label: '全部',
- value: '',
- },
- {
- label: '正常',
- value: '1',
- },
- {
- label: '禁用',
- value: '0',
- },
- ]);
- const fetchData = async (
- params: AliyunAccountParams = {
- current: 1,
- pageSize: 50,
- sort: 'status',
- order: 'descending',
- getTotal: true,
- }
- ) => {
- setLoading(true);
- 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;
- } catch (err) {
- console.log(err);
- } finally {
- setLoading(false);
- }
- };
- const changeAttr = async (id: number, name: string, val: any) => {
- const args = {
- id,
- name,
- val
- } as UpdateAccountModel;
- Modal.confirm({
- title: '确认提示',
- content: '确定更新操作吗?',
- okText: '确认',
- cancelText: '取消',
- async onOk() {
- setLoading(true);
- try {
- const { data } = await UpdateAccount(args);
- console.log(data, data.msg);
- const msg = data.msg || '更新成功!';
- if (data.success) {
- Message.success(msg);
- fetchData();
- } else {
- Message.error(data.msg);
- }
- } catch (e) { console.log(e) }
- setLoading(false);
- }
- });
- };
- const search = () => {
- fetchData({
- ...basePagination,
- ...formModel.value,
- } as unknown as AliyunAccountParams);
- };
- const onPageChange = (current: number) => {
- fetchData({ ...basePagination, current });
- };
- fetchData();
- const reset = () => {
- formModel.value = generateFormModel();
- };
- const handleSelectDensity = (
- val: string | number | Record<string, any> | undefined,
- e: Event
- ) => {
- size.value = val as SizeProps;
- };
- const handleChange = (
- checked: boolean | (string | boolean | number)[],
- column: Column,
- index: number
- ) => {
- if (!checked) {
- cloneColumns.value = showColumns.value.filter(
- (item) => item.dataIndex !== column.dataIndex
- );
- } else {
- cloneColumns.value.splice(index, 0, column);
- }
- };
- const exchangeArray = <T extends Array<any>>(
- array: T,
- beforeIdx: number,
- newIdx: number,
- isDeep = false
- ): T => {
- const newArray = isDeep ? cloneDeep(array) : array;
- if (beforeIdx > -1 && newIdx > -1) {
- // 先替换后面的,然后拿到替换的结果替换前面的
- newArray.splice(
- beforeIdx,
- 1,
- newArray.splice(newIdx, 1, newArray[beforeIdx]).pop()
- );
- }
- return newArray;
- };
- const popupVisibleChange = (val: boolean) => {
- if (val) {
- nextTick(() => {
- const el = document.getElementById(
- 'tableSetting'
- ) as HTMLElement;
- const sortable = new Sortable(el, {
- onEnd(e: any) {
- const { oldIndex, newIndex } = e;
- exchangeArray(cloneColumns.value, oldIndex, newIndex);
- exchangeArray(showColumns.value, oldIndex, newIndex);
- },
- });
- });
- }
- };
- watch(
- () => columns.value,
- (val) => {
- cloneColumns.value = cloneDeep(val);
- cloneColumns.value.forEach((item, index) => {
- item.checked = true;
- });
- showColumns.value = cloneDeep(cloneColumns.value);
- },
- { 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">
- export default {
- name: 'SearchTable',
- };
- </script>
- <style scoped lang="less">
- .container {
- padding: 0 20px 20px 20px;
- }
- :deep(.arco-table-th) {
- &:last-child {
- .arco-table-th-item-title {
- margin-left: 16px;
- }
- }
- }
- .action-icon {
- margin-left: 12px;
- cursor: pointer;
- }
- .active {
- color: #0960bd;
- background-color: #e3f4fc;
- }
- .setting {
- display: flex;
- align-items: center;
- width: 200px;
- .title {
- margin-left: 12px;
- 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>
|