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">
|
|
|
|
|
|
<!-- 用户名称 -->
|
2026-06-12 14:55:09 +08:00
|
|
|
|
<a-dropdown v-model:visible="userDropdownVisible" placement="bottomRight" class="user-dropdown">
|
2026-06-10 15:38:37 +08:00
|
|
|
|
<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>
|
2026-06-12 14:55:09 +08:00
|
|
|
|
<ZhihuiIcon icon= "UpOutlined" v-if="userDropdownVisible"/>
|
|
|
|
|
|
<ZhihuiIcon icon= "DownOutlined" v-else/>
|
2026-06-10 15:38:37 +08:00
|
|
|
|
</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 14:55:09 +08:00
|
|
|
|
const userDropdownVisible = ref(false);
|
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 {
|
2026-06-14 18:33:33 +08:00
|
|
|
|
background: rgb(246 90 95 );
|
2026-06-10 15:38:37 +08:00
|
|
|
|
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;
|
2026-06-14 18:33:33 +08:00
|
|
|
|
padding-right: 15px;
|
2026-06-10 15:38:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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>
|