Files
piaowu/ruoyi-uia/src/views/HomePage.vue
T

971 lines
30 KiB
Vue
Raw Normal View History

2026-06-10 15:38:37 +08:00
<template>
<!-- 主页布局 -->
<a-layout theme="light" style="height: 100%; overflow: hidden">
<!-- 左侧边栏 -->
<a-layout-sider theme="light" v-model:collapsed="collapsed" :trigger="null" collapsible>
<!-- 左侧边栏的顶部header -->
<a-layout-header class="logo" style=" padding: 0; height: 50px; line-height: 50px; text-align: center" mode="inline"> <!--background: #646ee3;-->
<a href="/" target="_self">
<img v-if="collapsed == false" src="/images/logo-piao.png" alt="智汇票务" style="height: 36px" />
<img v-if="collapsed == true" src="/images/logo-piao-one.png" alt="智汇票务" style="height: 36px" />
</a>
</a-layout-header>
<!-- 左侧边栏的菜单栏 -->
<a-menu v-model:selectedKeys="menuState.selectedMenuKeys" :open-keys="menuState.openedFaterMenuKeys" theme="light"
class="side-left-menu" mode="inline" style="height: 100%; overflow: scroll; padding-top: 4px"
@openChange="expandMenu">
<a-menu-item key="index" @click="clickMenuItem('HomePage', '/home', '主页', 'IndexPage', 'index', '工作台', 'index')">
<template #icon>
<ZhihuiIcon icon="HomeOutlined" />
</template>
<span>票务总台</span>
</a-menu-item>
<!-- 循环生成菜单菜单的key使用sidebarRouters中的path父菜单的key带/子菜单的key不带/ -->
<a-sub-menu v-for="router in sidebarRouters" :key="router.path" style="padding-top:0px;">
<template #icon>
<ZhihuiIcon :icon="router.meta.icon" />
</template>
<template #title>
<span style="display: flex; align-items: center; justify-content: flex-start; width: 100%;">
<span v-html="router.meta.link ? `<a href='${router.meta.link}' target='_blank'>${router.meta.title}</a>` : router.meta.title"></span>
<a-badge v-if="menuBadges[router.path] && menuBadges[router.path] > 0" :count="menuBadges[router.path]" size="small" />
</span>
</template>
<div v-for="children in router.children" :key="children.path" class="side-left-menu-sub">
<!-- 二级父菜单有子菜单 -->
<a-sub-menu v-if="children.children && children.children.length > 0" :key="children.path">
<template #icon>
<ZhihuiIcon :icon="children.meta.icon" />
</template>
<template #title>
<span style="display: flex; align-items: center; justify-content: flex-start; width: 100%;">
<span>{{ children.meta.title }}</span>
<a-badge v-if="menuBadges[children.path] && menuBadges[children.path] > 0" :count="menuBadges[children.path]" size="small" />
</span>
</template>
<!-- 三级菜单 -->
<a-menu-item
v-for="threeLevelChildren in children.children"
:key="threeLevelChildren.path"
@click="clickThreeLevelMenuItem(router.path, children.name, children.path, children.meta.title, threeLevelChildren.name, threeLevelChildren.path, threeLevelChildren.meta.title, threeLevelChildren.component)">
<template #icon>
<ZhihuiIcon :icon="threeLevelChildren.meta.icon" />
</template>
<!-- 使用条件判断来决定显示普通文本还是带badge的标题 -->
<template v-if="menuBadges[threeLevelChildren.path] && menuBadges[threeLevelChildren.path] > 0">
<span style="display: flex; align-items: center; justify-content: flex-start; width: 100%;">
<span>{{ threeLevelChildren.meta.title }}</span>
<a-badge :count="menuBadges[threeLevelChildren.path]" size="small" />
</span>
</template>
<template v-else>
<span>{{ threeLevelChildren.meta.title }}</span>
</template>
</a-menu-item>
</a-sub-menu>
<!-- 二级菜单无子菜单 -->
<a-menu-item v-else :key="children.path" @click="clickMenuItem(router.name, router.path, router.meta.title, children.name, children.path, children.meta.title, children.component)">
<template #icon>
<ZhihuiIcon :icon="children.meta.icon" />
</template>
<!-- 使用条件判断来决定显示普通文本还是带badge的标题 -->
<template v-if="menuBadges[children.path] && menuBadges[children.path] > 0">
<span style="display: flex; align-items: center; justify-content: flex-start; width: 100%;">
<span>{{ children.meta.title }}</span>
<a-badge :count="menuBadges[children.path]" size="small" />
</span>
</template>
<template v-else>
<span>{{ children.meta.title }}</span>
</template>
</a-menu-item>
</div>
</a-sub-menu>
</a-menu>
<MusicPlayer :collapsed="collapsed" />
</a-layout-sider>
<!-- 右侧布局 -->
<a-layout>
<!-- 右侧布局的顶部header -->
<a-layout-header class="navbar" style="padding: 0; height: 50px; line-height: 50px" mode="inline">
<a-row class="navbar-row">
<a-col class="navbar-col-menu">
<MenuUnfoldOutlined v-if="collapsed" @click="() => (collapsed = !collapsed)"
style="color: rgb(253, 253, 253); padding-left: 20px" />
<MenuFoldOutlined v-else @click="() => (collapsed = !collapsed)"
style="color: rgb(253, 253, 253); padding-left: 20px" />
<span class="navbar-title">{{ headerInfo.currentFatherMenuTitle }} / {{ headerInfo.currentMenuTitle }}</span>
</a-col>
<!-- 右侧功能区容器 -->
<div class="navbar-right-group">
<!-- 倒计时显示 -->
<a-col class="navbar-col-countdown">
<div style="display: flex; align-items: center; justify-content: flex-start; height: 50px; color: #fff;">
<a-badge :status="refreshStatus" />
<span v-if="refreshInterval !== 0" :class="['countdown-number', { 'countdown-urgent': remainingSeconds <= 3 }]">
<LoadingOutlined v-if="refreshStatus === 'processing'" />
<span v-else>{{ remainingSeconds }}</span>
</span>
<span v-else class="countdown-number">
非自动刷新
</span>
</div>
</a-col>
<a-col class="navbar-col-actions">
<a-space :size="16" class="navbar-buttons">
<a-button type="primary" @click="toggleAirModal" v-if="isAdmin" class="navbar-btn">
航空公司
<span v-if="airVisible"><UpOutlined /></span>
<span v-else><DownOutlined /></span>
</a-button>
<a href="http://2233.zhonghangtianxia.com/AirRule/tuigaiqian.aspx?Airlines=GS" target="_blank"
class="ant-btn ant-btn-primary navbar-btn">退改政策</a>
<a href="/setting/platform" class="ant-btn ant-btn-primary navbar-btn">平台消息</a>
</a-space>
</a-col>
<a-col class="navbar-col-user">
<!-- 用户名称 -->
<a-dropdown placement="bottomRight" class="user-dropdown">
<a style="color: #fffeee" class="user-dropdown-link">
2026-06-12 11:48:17 +08:00
<img :src="avatarSrc" class="avatarIcon" @error="onAvatarError" />
2026-06-10 15:38:37 +08:00
<span class="user-name">{{ vueStore.getters.nickName }}</span>
<ZhihuiIcon icon="DownOutlined" />
</a>
<template #overlay>
<a-menu>
<a-menu-item>
<ZhihuiIcon icon="UserOutlined" style="padding-right: 10px" />
<a @click="enterPersonalCenter" style="padding-right: 10px">个人中心</a>
</a-menu-item>
<template v-if="isAdmin">
<a-menu-item>
<ZhihuiIcon icon="SettingOutlined" style="padding-right: 10px" />
<a @click="changeTheme" style="padding-right: 10px">配置中心</a>
</a-menu-item>
</template>
<a-menu-item>
<ZhihuiIcon icon="LogoutOutlined" style="padding-right: 10px" />
<a @click="logout" style="padding-right: 10px">退出登录</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-col>
</div>
<a-modal class="air-modal" v-model:visible="airVisible" width="840px" title="国内航空公司列表"
:footer="null" :mask="true" :closable="false">
<div class="air-container">
<div class="air-list">
<a-row :gutter="[8, 1]">
<a-col :xs="12" :sm="8" :md="6" :lg="6" v-for="(airline, index) in airList" :key="airline.id" class="airline-col">
<a :href="airline.url" target="_blank" class="airline-link">
<span class="airline-info">{{ airline.name }}-{{ airline.code }}</span>
<img :src="`${airline.logo}`" alt="Logo" class="airline-logo" />
</a>
</a-col>
</a-row>
</div>
<a-spin size="middle" v-if="airLoading"></a-spin>
</div>
</a-modal>
</a-row>
</a-layout-header>
<!-- 右侧布局的主显示区 -->
<div class="main-area">
<a-layout-content class="content-area">
<div class="card-container">
<a-config-provider :locale="zhCN">
<router-view></router-view>
</a-config-provider>
</div>
</a-layout-content>
<AIChat />
</div>
</a-layout>
</a-layout>
<SettingDrawer ref="settingDrawer" />
</template>
<script setup>
import { ref, computed, reactive, onMounted, onUnmounted, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useStore, mapGetters } from 'vuex';
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UpOutlined,
DownOutlined,
LoadingOutlined,
} from '@ant-design/icons-vue';
import { ZhihuiIcon } from '@/utils/ZhihuiIcon.js';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import SettingDrawer from './SettingDrawer.vue';
import MusicPlayer from '@/components/MusicPlayer.vue';
import AIChat from '@/components/AIChat.vue';
import { message } from 'ant-design-vue';
import store from '@/store';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import { airlineApi } from '@/api/piao/airline';
import { PAGE_SIZE_OPTIONS } from '@/api/piao/constant';
import { messageApi } from '@/api/piao/message';
import zhihuiBus from '@/utils/zhihuiBus'; // 引入事件总线
import zhihuiAudio from '@/utils/zhihuiAudio'; // 引入音频播放器
dayjs.locale('zh-cn');
2026-06-12 11:48:17 +08:00
const avatarError = ref(false);
2026-06-10 15:38:37 +08:00
const avatarSrc = computed(() => {
2026-06-12 11:48:17 +08:00
if (avatarError.value) return "/images/defaultavatar.png";
2026-06-10 15:38:37 +08:00
return store.getters.avatar ? import.meta.env.VITE_API_DOMAIN + store.getters.avatar : "/images/defaultavatar.png";
});
2026-06-12 11:48:17 +08:00
const onAvatarError = () => { avatarError.value = true; };
2026-06-10 15:38:37 +08:00
const vueStore = useStore();
const vueRouter = useRouter();
const vueRoute = useRoute();
const isAdmin = computed(() => vueStore.state.user.roles?.includes('admin'));
// 左侧边栏是否折叠:移动端默认折叠
const isMobile = ref(window.innerWidth <= 950);
const collapsed = ref(isMobile.value);
// ========== 刷新相关 ==========
const refreshStatus = ref('success');
const remainingSeconds = ref(30);
const refreshInterval = ref(30000); // 默认30秒
let refreshTimer = null;
let countdownTimer = null;
let lastFetchTime = Date.now();
// ========== 提醒相关 ==========
const alertMode = ref(0); // 默认不提醒
let previousTotalBadges = 0; // 记录上一次的badge总数,用于检测是否有新增
const menuBadges = ref({
//一级显示
'/order': 0, // 订单列表显示5
// 订单系统下的二级菜单
'autoorder': 0, // 订单列表显示5
'autorefund': 0, // 我的进行中显示2
'changelist': 0, // 待出票显示3
'orderlist': 0, // 紧急出票显示1
'ordermine': 0, // 退票列表显示4
'orderurgent': 0, // 改期订单显示2
'orderwaiting': 0, // 自动出票显示7
'refundlist': 0 // 自动退票显示8
});
// 处理提醒模式变化
const handleAlertModeChange = (event) => {
if (event && event.detail) {
const newMode = event.detail.mode;
if (newMode !== undefined) {
alertMode.value = newMode;
zhihuiAudio.setAlertMode(newMode);
}
}
};
// 初始化音频播放器
const initAudioPlayer = () => {
// 预加载音频文件
zhihuiAudio.preloadAudio(1, '/audio/alert1.mp3');
zhihuiAudio.preloadAudio(2, '/audio/alert2.mp3');
// 预加载自定义音频文件
zhihuiAudio.preloadCustomAudio('alert-tui', '/audio/alert-tui.mp3');
zhihuiAudio.preloadCustomAudio('alert-gai', '/audio/alert-gai.mp3');
// 从 localStorage 加载保存的提醒模式
const savedAlertMode = localStorage.getItem('alertMode');
if (savedAlertMode !== null) {
const mode = parseInt(savedAlertMode);
alertMode.value = mode;
zhihuiAudio.setAlertMode(mode);
}
};
const fetchBadgeData = async () => {
refreshStatus.value = 'processing';
try {
// 从 localStorage 获取时间范围,默认10分钟
const savedTimeRange = localStorage.getItem('timeRange');
const timeRange = savedTimeRange ? parseInt(savedTimeRange) : 10;
const {data} = await messageApi.getMenuBadges({
...menuBadges.value,
timeRange: timeRange
});
if (data) {
// 合并数据,只更新存在的字段
menuBadges.value = {
...menuBadges.value,
...data
};
// 获取最近指定时间范围的新订单数量
const orderNewCount = data.orderNewCount || 0;
const refundNewCount = data.refundNewCount || 0;
const changeNewCount = data.changeNewCount || 0;
// 判断是否需要播放音乐:
// 1. 第一次加载(previousTotalBadges === 0)且有新订单
// 2. 或者最近指定时间范围有新订单
if ((previousTotalBadges === 0 && Object.values(data).reduce((sum, value) => sum + value, 0) > 0) ||
orderNewCount > 0 || refundNewCount > 0 || changeNewCount > 0) {
// 根据不同类型的新订单播放不同的音乐
if (orderNewCount > 0) {
zhihuiAudio.playAlert();
} else if (refundNewCount > 0) {
zhihuiAudio.playCustomAudio('alert-tui');
} else if (changeNewCount > 0) {
zhihuiAudio.playCustomAudio('alert-gai');
}
}
// 更新 previousTotalBadges 为当前总数
previousTotalBadges = Object.values(menuBadges.value).reduce((sum, value) => sum + value, 0);
}
refreshStatus.value = 'success';
console.log('Badge数据已更新:', menuBadges.value);
// 触发全局刷新事件,通知所有监听该事件的组件刷新
zhihuiBus.emit('global-refresh', { timestamp: Date.now() });
} catch (error) {
console.error('获取Badge数据失败:', error);
refreshStatus.value = 'warning';
} finally {
lastFetchTime = Date.now();
calculateRemainingSeconds();
}
};
// ========== 倒计时相关 ==========
const calculateRemainingSeconds = () => {
// 如果刷新间隔为0(不刷新),直接返回0并停止倒计时
if (refreshInterval.value === 0) {
remainingSeconds.value = 0;
return 0;
}
const elapsed = Date.now() - lastFetchTime;
const remaining = Math.max(0, Math.ceil((refreshInterval.value - elapsed) / 1000));
remainingSeconds.value = remaining;
return remaining;
};
const startCountdown = () => {
// 如果刷新间隔为0(不刷新),不启动倒计时
if (refreshInterval.value === 0) {
remainingSeconds.value = 0;
return;
}
if (countdownTimer) clearInterval(countdownTimer);
calculateRemainingSeconds();
countdownTimer = setInterval(() => {
const remaining = calculateRemainingSeconds();
if (remaining <= 0) {
fetchBadgeData();
}
}, 1000);
};
const stopCountdown = () => {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
};
// ========== 自动刷新 ==========
const startAutoRefresh = () => {
// 如果刷新间隔为0(不刷新),不启动定时器
if (refreshInterval.value === 0) {
return;
}
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = setInterval(() => {
fetchBadgeData();
}, refreshInterval.value);
};
const stopAutoRefresh = () => {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
};
// ========== 监听配置中心的间隔变化 ==========
const handleRefreshIntervalChange = (event) => {
if (event && event.detail) {
const newInterval = event.detail.interval;
if (newInterval !== undefined && !isNaN(newInterval)) {
refreshInterval.value = parseInt(newInterval);
// 保存到 localStorage
localStorage.setItem('preferredRefreshInterval', refreshInterval.value.toString());
// 先停止所有定时器
stopAutoRefresh();
stopCountdown();
// 如果新间隔不为0,重新启动定时器
if (refreshInterval.value !== 0) {
// 重置最后获取时间,避免立即触发
lastFetchTime = Date.now();
startAutoRefresh();
startCountdown();
} else {
// 设置为不刷新时,清除倒计时显示
remainingSeconds.value = 0;
refreshStatus.value = 'success';
}
}
}
};
// ========== 初始化 ==========
const initializeRefresh = () => {
// 从localStorage加载用户偏好的刷新间隔
const savedInterval = localStorage.getItem('preferredRefreshInterval');
if (savedInterval !== null) {
refreshInterval.value = parseInt(savedInterval);
}
// 立即获取一次数据
fetchBadgeData();
// 只有在刷新间隔不为0时才启动定时器
if (refreshInterval.value !== 0) {
startAutoRefresh();
startCountdown();
} else {
// 如果是不刷新模式,倒计时显示为0
remainingSeconds.value = 0;
}
};
// 菜单相关状态数据
const menuState = reactive({
// 所有一级菜单节点
rootSubmenuKeys: [],
// 展开的父菜单节点
openedFaterMenuKeys: [],
// 当前选中的菜单项
selectedMenuKeys: []
});
// 通过从后端获取的用户可以访问的菜单信息生产前端菜单列表
const sidebarRouters = computed(mapGetters(['sidebarRouters']).sidebarRouters.bind({ $store: vueStore }));
// console.log('sidebarRouters=', JSON.stringify(sidebarRouters.value));
// 设置一级父菜单
if (sidebarRouters.value && sidebarRouters.value.length > 0) {
sidebarRouters.value.forEach((element) => {
// console.log(element.path);
menuState.rootSubmenuKeys.push(element.path);
});
}
// 将工作台菜单添加到一级菜单节点数组中
menuState.rootSubmenuKeys.push('index');
const headerInfo = reactive({ currentFatherMenuTitle: '主页', currentMenuTitle: '票务总台' });
// alert(JSON.stringify(vueRoute));
// 刷新页面后重新选中对应的菜单
if (vueRoute.path) {
let arr = vueRoute.path.split('/');
// 如果有父菜单,获取父菜单key
if (arr.length > 2) {
menuState.openedFaterMenuKeys = [];
menuState.openedFaterMenuKeys.push('/' + arr[1]);
menuState.selectedMenuKeys = [];
menuState.selectedMenuKeys.push(arr[2]);
} else {
// 如果没有父菜单
if ('/home' === vueRoute.path) {
// 如果是主页路由,则选中工作台菜单
menuState.selectedMenuKeys.push('index');
} else {
menuState.openedFaterMenuKeys = [];
menuState.selectedMenuKeys = [];
menuState.selectedMenuKeys.push(arr[1]);
}
}
// 刷新页面后,重新设置顶部的菜单名称
if (sidebarRouters.value && sidebarRouters.value.length > 0) {
sidebarRouters.value.forEach((element) => {
if (element.path === '/' + arr[1]) {
headerInfo.currentFatherMenuTitle = element.meta.title;
if (element.children) {
element.children.forEach((child) => {
if (child.path === arr[2]) {
headerInfo.currentMenuTitle = child.meta.title;
}
});
}
}
});
}
}
// 展开菜单时,收起其他已展开的菜单
const expandMenu = (openedFaterMenuKeys) => {
const latestOpenKey = openedFaterMenuKeys.find((key) => menuState.openedFaterMenuKeys.indexOf(key) === -1);
if (menuState.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
menuState.openedFaterMenuKeys = openedFaterMenuKeys;
} else {
menuState.openedFaterMenuKeys = latestOpenKey ? [latestOpenKey] : [];
}
};
// 点击菜单项:跳转到对应的路由
const clickMenuItem = (fatherMenuName, fatherMenuPath, fatherTitle, menuName, menuPath, menuTitle, menuComponent) => {
if (menuPath === 'index') {
//如果点击的是工作台菜单,则关闭其他已展开的父菜单
menuState.openedFaterMenuKeys = [];
}
headerInfo.currentFatherMenuTitle = fatherTitle;
headerInfo.currentMenuTitle = menuTitle;
let routePathStr = fatherMenuPath + '/' + menuPath;
console.log('点击菜单后请求路由=' + routePathStr);
vueRouter.push({ path: routePathStr });
};
// 点击菜单项:跳转到对应的路由
const clickThreeLevelMenuItem = (greatFatherMenuPath, fatherMenuName, fatherMenuPath, fatherTitle, menuName, menuPath, menuTitle, menuComponent) => {
if (menuPath === 'index') {
//如果点击的是工作台菜单,则关闭其他已展开的父菜单
menuState.openedFaterMenuKeys = [];
}
headerInfo.currentFatherMenuTitle = fatherTitle;
headerInfo.currentMenuTitle = menuTitle;
let routePathStr = greatFatherMenuPath + '/' + fatherMenuPath + '/' + menuPath;
console.log('点击菜单后请求路由=' + routePathStr);
vueRouter.push({ path: routePathStr });
};
// 退出登录
const logout = () => {
vueStore.dispatch('LogOut_Action')
.then(() => {
// 登陆成功,跳转到主页
vueRouter.push({ path: '/' }).catch(() => {});
}).catch(() => {});
};
const router = useRouter();
// 进入个人中心
const enterPersonalCenter = () => {
router.push('/user/profile');
};
const settingDrawer = ref();
const changeTheme = () => {
if (settingDrawer.value) {
settingDrawer.value.showDrawer();
}
};
const airLoading = ref(false);
const airVisible = ref(false);
const airList = ref([]);
const airQuery = {
pageNum: 1,
pageSize: PAGE_SIZE_OPTIONS[8]
};
const toggleAirModal = async () => {
airVisible.value = !airVisible.value;
if (airVisible.value) {
try {
airLoading.value = true;
const airData = await airlineApi.queryPage(airQuery);
airList.value = airData.data.list;
console.log(JSON.stringify(airList.value));
} finally {
airLoading.value = false;
}
}
};
// 监听路由变化,同步菜单选中状态
watch(() => vueRoute.path, (newPath) => {
if (newPath) {
let arr = newPath.split('/');
if (arr.length > 2) {
menuState.openedFaterMenuKeys = ['/' + arr[1]];
menuState.selectedMenuKeys = [arr[2]];
} else {
if ('/home' === newPath) {
menuState.selectedMenuKeys = ['index'];
} else {
menuState.openedFaterMenuKeys = [];
menuState.selectedMenuKeys = arr.length > 1 ? [arr[1]] : [];
}
}
}
});
// ========== 生命周期 ==========
onMounted(() => {
initializeRefresh();
initAudioPlayer(); // 初始化音频播放器
// 添加事件监听
window.addEventListener('refresh-interval-change', handleRefreshIntervalChange);
window.addEventListener('alert-mode-change', handleAlertModeChange);
window.addEventListener('resize', handleResize);
});
function handleResize() {
const wasMobile = isMobile.value;
isMobile.value = window.innerWidth <= 950;
if (isMobile.value) {
collapsed.value = true;
} else if (wasMobile) {
// 从移动端变为桌面端,自动展开菜单
collapsed.value = false;
}
}
onUnmounted(() => {
stopAutoRefresh();
stopCountdown();
// 移除事件监听
window.removeEventListener('refresh-interval-change', handleRefreshIntervalChange);
window.removeEventListener('alert-mode-change', handleAlertModeChange);
window.removeEventListener('resize', handleResize);
// 停止音频播放
zhihuiAudio.stop();
});
</script>
<style lang="less">
// 对antd的tabs和menu部分样式进行了重写
@import '@/assets/styles/homePage/homePageStyles.less';
// 引入默认的全局样式
@import '@/assets/styles/globalStyles.less';
@import '@/assets/styles/skins.less';
.navbar-col-actions {
display: flex;
align-items: center;
flex: 0 0 auto;
padding-right: 120px;
}
@media (max-width: 850px) {
.navbar-col-actions {
display: none ;
overflow: hidden !important;
}
}
.air-modal .ant-modal-title {
text-align: center;
font-size: 20px;
}
.air-modal .air-list span {
padding: 3px;
margin: 1px;
vertical-align: middle;
line-height: 20px;
display: flex;
align-items: center;
justify-content: flex-start;
}
.air-modal .air-list span:hover {
/* border: solid 1px #62a3ef;*/
}
.airline-col {
display: flex;
align-items: center;
}
.airline-link {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
padding: 5px 8px;
border: 1px solid transparent;
border-radius: 4px;
transition: all 0.3s;
}
.airline-link:hover {
border-color: #62a3ef;
background-color: #fff;
}
.airline-info {
margin-right: 8px;
white-space: nowrap;
}
.airline-logo {
height: 25px;
width: 80px;
max-width: 70px;
margin: 3px;
object-fit: contain;
}
.avatarIcon {
2026-06-12 11:48:17 +08:00
height: 33px;
width: 33px;
2026-06-10 15:38:37 +08:00
border-radius: 50%;
2026-06-12 11:48:17 +08:00
background-color: #a8e4e4;
2026-06-10 15:38:37 +08:00
opacity: 0.9;
2026-06-12 11:48:17 +08:00
border: 1px solid #abcbe7;margin-right:5px;
2026-06-10 15:38:37 +08:00
}
.audio-player {
display: flex;
flex-direction: column;
align-items: center;
}
progress[value] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 20px;
background-color: #ddd;
}
progress[value]::-webkit-progress-bar {
background-color: #ddd;
}
progress[value]::-webkit-progress-value {
background-color: blue;
}
// 倒计时数字样式
.countdown-number {
margin-left: 4px;
padding: 2px 8px; /* 上下2px,左右8px,保持高度适中 */
//border-radius: 12px; /* 圆角 */
font-size: 13px; /* 字体大小 */
line-height: 1; /* 行高设为1,防止撑高 */
display: inline-block; /* 内联块元素 */
transition: all 0.3s ease;
}
// 3秒内紧急倒计时样式
.countdown-urgent {
background: rgba(255, 143, 145, 0.9);
color: #fff;
font-size: 15px;
font-weight: bold;
animation: urgent-pulse 0.5s infinite;
padding: 4px 14px;
line-height: 1;
display: inline-block;
border-radius: 12px;
box-shadow: 0 0 8px rgba(255, 77, 79, 0.6);
}
@keyframes urgent-pulse {
0% {
transform: scale(1);
opacity: 1;
box-shadow: 0 0 8px rgba(255, 77, 79, 0.6);
}
50% {
transform: scale(1.15);
opacity: 0.85;
box-shadow: 0 0 16px rgba(255, 77, 79, 0.9);
}
100% {
transform: scale(1);
opacity: 1;
box-shadow: 0 0 8px rgba(255, 77, 79, 0.6);
}
}
.ant-menu-title-content {
.ant-badge {
margin-left: 5px !important;
}
}
.main-area {
display: flex;
flex: 1;
overflow: hidden;
min-height: 0;
}
.content-area {
flex: 1;
padding: 0px;
min-height: 280px;
overflow: scroll;
background: white;
}
.content-area::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.content-area::-webkit-scrollbar-thumb {
background: #fbfbfb;
border-radius: 3px;
}
.content-area::-webkit-scrollbar-track {
background: transparent;
}
.content-area::-webkit-scrollbar-corner {
background: transparent;
}
.card-container {
padding: 10px;
}
// 响应式导航栏样式
.navbar-row {
width: 100%;
height: 100%;
margin: 0 !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
flex-direction: row !important;
}
.navbar-title {
color: rgb(253, 253, 253);
padding-left: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 0 0 auto !important;
}
.navbar-col-menu {
display: flex !important;
align-items: center !important;
flex: 0 0 auto !important;
}
.navbar-right-group {
display: flex !important;
align-items: center !important;
flex-direction: row !important;
justify-content: flex-end !important;
flex: 0 0 auto !important;
flex-wrap: nowrap !important;
}
.navbar-col-countdown {
display: flex !important;
align-items: center !important;
flex: 0 0 auto !important;
padding-right: 20px !important;
}
.navbar-col-user {
display: flex !important;
align-items: center !important;
flex: 0 0 auto !important;
padding-right: 10px;
}
.user-dropdown {
display: flex;
justify-content: flex-end;
}
.user-dropdown-link {
display: flex;
align-items: center;
}
.user-name {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 小屏幕适配
@media (max-width: 1200px) {
.navbar-title {
font-size: 14px;
padding-left: 10px;
}
.navbar-buttons .ant-btn {
padding-left: 8px !important;
padding-right: 8px !important;
}
}
@media (max-width: 992px) {
.navbar-title {
font-size: 13px;
padding-left: 8px;
}
.navbar-buttons .ant-btn-sm {
padding: 0 4px;
font-size: 12px;
}
}
@media (max-width: 768px) {
.navbar-title {
font-size: 12px;
padding-left: 5px;
}
.navbar-buttons .ant-btn {
margin-left: 2px !important;
margin-right: 2px !important;
}
.countdown-number {
font-size: 11px;
padding: 2px 4px;
}
.user-name {
display: none;
}
}
@media (max-width: 576px) {
.navbar-col-countdown {
padding-right: 0 !important;
}
.navbar-buttons .ant-btn {
margin-left: 1px !important;
margin-right: 1px !important;
font-size: 11px;
}
}
</style>