ylj20011123 1a1cb38f1a update
2025-12-04 15:36:09 +08:00

956 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<page-meta :page-style="pageStyle"></page-meta>
<view class="digital-dashboard">
<!-- Tab切换区域 -->
<scroll-view scroll-x class="tab-container" :scroll-with-animation="true" :scroll-left="tabScrollPosition"
show-scrollbar="false" :scroll-animation-duration="300">
<view class="tab-list">
<view v-for="(tab, index) in tabList" :key="index" :id="`tab-${index}`" class="tab-item"
:class="{ active: activeTab === index }" @click="switchTab(index)">
<text class="tab-text">{{ tab.name }}</text>
<view class="tab-indicator" v-if="activeTab === index"></view>
</view>
</view>
</scroll-view>
<!-- 右侧悬浮导航栏 -->
<view class="side-navigation" v-if="currentNavItems && currentNavItems.length > 0">
<view class="nav-rail" :class="{ compact: isNavCollapsed }">
<view class="rail-body" :class="{ hidden: isNavCollapsed }">
<view class="rail-header">
<text class="rail-title">
<text style="display: block;">快速</text>
<text style="display: block;">导航</text>
</text>
<view class="rail-toggle" @click="toggleNavCollapse">
<text class="toggle-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="rail-track" show-scrollbar="false">
<view v-for="item in currentNavItems" :key="item.id" class="rail-item"
:class="{ active: activeNavItem === item.id }" @click="scrollToComponent(item.id)">
<view class="rail-dot" :style="{ borderColor: navAccentColor }">
<text class="rail-dot-text">{{ (item.name) }}</text>
</view>
</view>
</scroll-view>
<view class="rail-footer" @click.stop="scrollToTop">
<view class="rail-back">
<text></text>
<text>TOP</text>
</view>
</view>
</view>
<view class="rail-compact" :class="{ visible: isNavCollapsed }" @click="toggleNavCollapse">
<text class="compact-icon"></text>
<text class="compact-text">导航</text>
</view>
</view>
</view>
<swiper class="tab-swiper" :current="activeTab" @change="handleSwiperChange">
<swiper-item v-for="(tab, index) in tabList" :key="tab.key">
<scroll-view class="tab-scroll-view" scroll-y @scroll="handleScroll"
:scroll-into-view="scrollIntoViewMap[tab.key]" :scroll-with-animation="true"
:scroll-y="allowScroll">
<view :id="`top-${tab.key}`"></view>
<view class="content-container">
<!-- 时间选择 -->
<view class="timeBox">
<picker mode="date" @change="bindDateChange" :value="selectTime" fields="month">
统计时间{{ selectTime }}
</picker>
</view>
<!-- 实时运营监控中心 -->
<view v-if="tab.key === 'business'" class="tab-content">
<view id="overview-of-serviceArea" class="section-anchor"></view>
<OverviewOfServiceArea />
<view id="trading-alert" class="section-anchor"></view>
<TradingAlert />
<view id="trend-of-trafficFlow" class="section-anchor"></view>
<TrendOfTrafficFlow :selectTime="selectTime" />
<view id="vehicles-entering" class="section-anchor"></view>
<VehiclesEntering :selectTime="selectTime" />
<view id="vehicle-model-stay" class="section-anchor"></view>
<VehicleModelStay :selectTime="selectTime" />
</view>
<!-- 客群画像与消费行为分析 -->
<view v-else-if="tab.key === 'customerProfile'" class="tab-content">
<view id="customer-age-group" class="section-anchor"></view>
<CustomerAgeGroup :selectTime="selectTime" />
<view id="gender-customer-group" class="section-anchor"></view>
<GenderCustomerGroup :selectTime="selectTime" />
<view id="preference-type" class="section-anchor"></view>
<PreferenceType :selectTime="selectTime" />
<view id="customer-group" class="section-anchor"></view>
<CustomerGroup :selectTime="selectTime" />
<view id="customer-consumption-preferences" class="section-anchor"></view>
<CustomerConsumptionPreferences :selectTime="selectTime" />
<view id="consumption-conversion" class="section-anchor"></view>
<ConsumptionConversion :selectTime="selectTime" />
<view id="consumption-level" class="section-anchor"></view>
<ConsumptionLevel :selectTime="selectTime" />
<view id="consumption-period" class="section-anchor"></view>
<ConsumptionPeriod :selectTime="selectTime" />
<view id="brand-consumption-level" class="section-anchor"></view>
<BrandConsumptionLevel :selectTime="selectTime" />
</view>
<!-- 多维度经营数据分析 -->
<view v-else-if="tab.key === 'businessRevenue'" class="tab-content">
<view id="business-case" class="section-anchor"></view>
<BusinessCase :selectTime="selectTime" />
<view id="regional-revenue" class="section-anchor"></view>
<RegionalRevenue :selectTime="selectTime" />
<view id="business-structure" class="section-anchor"></view>
<BusinessStructure :selectTime="selectTime" />
<view id="festival-revenue-sum-info" class="section-anchor"></view>
<FestivalRevenueSumInfo />
</view>
<!-- 供应链生态全景 -->
<view v-else-if="tab.key === 'supplierAnalysis'" class="tab-content">
<view id="supplier-overview" class="section-anchor"></view>
<SupplierOverview :selectTime="selectTime" />
<view id="supplier-performance" class="section-anchor"></view>
<SupplierPerformance :selectTime="selectTime" />
<view id="supplier-category" class="section-anchor"></view>
<SupplierCategory :selectTime="selectTime" />
<view id="supplier-ranking" class="section-anchor"></view>
<SupplierRanking :selectTime="selectTime" />
<view id="supplier-cooperation" class="section-anchor"></view>
<SupplierCooperation :selectTime="selectTime" />
</view>
<!-- 商户电商生态全景 -->
<view v-else-if="tab.key === 'mallOperation'" class="tab-content">
<view id="member-mall" class="section-anchor"></view>
<MemberMall :selectTime="selectTime" />
<view id="hot-product-list" class="section-anchor"></view>
<HotProductList />
<view id="brand-detail" class="section-anchor"></view>
<BrandDetail />
<view id="supplier-list-box" class="section-anchor"></view>
<SupplierListBox :selectTime="selectTime" />
<view id="mall-order-statistics" class="section-anchor"></view>
<MallOrderStatistics :selectTime="selectTime" />
<view id="this-month-benefits" class="section-anchor"></view>
<ThisMonthBenefits :selectTime="selectTime" />
<view id="analysis-of-member" class="section-anchor"></view>
<AnalysisOfMember />
</view>
</view>
</scroll-view>
</swiper-item>
</swiper>
</view>
</template>
<script>
import OverviewOfServiceArea from './components/OverviewOfServiceArea.vue'
import TradingAlert from './components/TradingAlert.vue'
import TrendOfTrafficFlow from './components/TrendOfTrafficFlow.vue'
import VehiclesEntering from './components/VehiclesEntering.vue'
import VehicleModelStay from './components/VehicleModelStay.vue'
import CustomerAgeGroup from './components/CustomerAgeGroup.vue'
import GenderCustomerGroup from './components/GenderCustomerGroup.vue'
import PreferenceType from './components/PreferenceType.vue'
import CustomerGroup from './components/CustomerGroup.vue'
import CustomerConsumptionPreferences from './components/CustomerConsumptionPreferences.vue'
import ConsumptionConversion from './components/ConsumptionConversion.vue'
import ConsumptionLevel from './components/ConsumptionLevel.vue'
import ConsumptionPeriod from './components/ConsumptionPeriod.vue'
import BrandConsumptionLevel from './components/BrandConsumptionLevel.vue'
import BusinessCase from './components/BusinessCase.vue'
import RegionalRevenue from './components/RegionalRevenue.vue'
import BusinessStructure from './components/BusinessStructure.vue'
import FestivalRevenueSumInfo from './components/FestivalRevenueSumInfo.vue'
import MemberMall from './components/MemberMall.vue'
import HotProductList from './components/HotProductList.vue'
import BrandDetail from './components/BrandDetail.vue'
import SupplierListBox from './components/SupplierListBox.vue'
import MallOrderStatistics from './components/MallOrderStatistics.vue'
import ThisMonthBenefits from './components/ThisMonthBenefits.vue'
import AnalysisOfMember from './components/AnalysisOfMember.vue'
import SupplierOverview from './components/SupplierOverview.vue'
import SupplierPerformance from './components/SupplierPerformance.vue'
import SupplierCategory from './components/SupplierCategory.vue'
import SupplierRanking from './components/SupplierRanking.vue'
import SupplierCooperation from './components/SupplierCooperation.vue'
import moment from 'moment'
export default {
components: {
OverviewOfServiceArea,
TradingAlert,
TrendOfTrafficFlow,
VehiclesEntering,
VehicleModelStay,
CustomerAgeGroup,
GenderCustomerGroup,
PreferenceType,
CustomerGroup,
CustomerConsumptionPreferences,
ConsumptionConversion,
ConsumptionLevel,
ConsumptionPeriod,
BrandConsumptionLevel,
BusinessCase,
RegionalRevenue,
BusinessStructure,
FestivalRevenueSumInfo,
MemberMall,
HotProductList,
BrandDetail,
SupplierListBox,
MallOrderStatistics,
ThisMonthBenefits,
AnalysisOfMember,
SupplierOverview,
SupplierPerformance,
SupplierCategory,
SupplierRanking,
SupplierCooperation
},
data() {
return {
activeTab: 0,
allowScroll: true, // 控制页面滚动
pageStyle: 'overflow-x:hidden', // 页面样式
tabList: [
{ name: '运营中心', key: 'business' },
{ name: '客群画像', key: 'customerProfile' },
{ name: '经营分析', key: 'businessRevenue' },
{ name: '供应链生态', key: 'supplierAnalysis' },
{ name: '电商生态', key: 'mallOperation' },
],
// 各个Tab对应的导航栏数据
navData: {
business: [
{ id: 'overview-of-serviceArea', name: '概况' },// 服务区概况
{ id: 'trading-alert', name: '预警' },// 交易预警
{ id: 'trend-of-trafficFlow', name: '断面' },// 断面流量
{ id: 'vehicles-entering', name: '入区' },// 入区车流
{ id: 'vehicle-model-stay', name: '效益' },// 经营效益
],
customerProfile: [
{ id: 'customer-age-group', name: '年龄' },// 年龄画像
{ id: 'gender-customer-group', name: '性别' },// 性别画像
{ id: 'preference-type', name: '偏好' },// 偏好类型
{ id: 'customer-group', name: '特征' },// 客群特征
{ id: 'customer-consumption-preferences', name: '消费' },// 消费偏好
{ id: 'consumption-conversion', name: '转化率' },// 消费转化率
{ id: 'consumption-level', name: '水平' },// 消费水平
{ id: 'consumption-period', name: '时段' },// 消费时段
{ id: 'brand-consumption-level', name: '品牌' },// 品牌消费
],
businessRevenue: [
{ id: 'business-case', name: '营收' },// 营收特征
{ id: 'regional-revenue', name: '区域' },// 区域营收
{ id: 'business-structure', name: '业态' },// 业态结构
{ id: 'festival-revenue-sum-info', name: '节假日' },// 节假日营收
],
supplierAnalysis: [
{ id: 'supplier-overview', name: '概览' },// 供应商概览
{ id: 'supplier-performance', name: '绩效' },// 供应商绩效
{ id: 'supplier-category', name: '分类' },// 供应商分类
{ id: 'supplier-ranking', name: '排名' },// 供应商排名
{ id: 'supplier-cooperation', name: '合作' },// 合作分析
],
mallOperation: [
{ id: 'member-mall', name: '商城' },// 会员商城
{ id: 'hot-product-list', name: '榜单' },// 商品榜单
{ id: 'brand-detail', name: '类别' },// 商户类别
{ id: 'supplier-list-box', name: '供应商' },// 供应商列表
{ id: 'mall-order-statistics', name: '订单' },// 商城订单统计
{ id: 'this-month-benefits', name: '福利金' },// 福利金额度
{ id: 'analysis-of-member', name: '会员' },// 会员消费
]
},
navColorMap: {
business: '#6F86FF',
customerProfile: '#FF8F6F',
businessRevenue: '#38C9A4',
supplierAnalysis: '#9B7EDE',
mallOperation: '#F1C84C'
},
// 当前活动的导航项
activeNavItem: '',
// 导航栏是否收缩
isNavCollapsed: false,
// 暂时将这一次进入 的数据缓存一下
sessionData: {},
pageScrollTop: 0,
scrollIntoViewMap: {
business: '',
customerProfile: '',
businessRevenue: '',
supplierAnalysis: '',
mallOperation: ''
},
tabScrollPosition: 0,
selectTime: moment().subtract(1, 'M').format('YYYY-MM')
}
},
computed: {
// 获取当前Tab的导航项
currentNavItems() {
const currentTabKey = this.tabList[this.activeTab].key;
return this.navData[currentTabKey] || [];
},
currentTabKey() {
const current = this.tabList[this.activeTab];
return current ? current.key : '';
},
navAccentColor() {
return this.navColorMap[this.currentTabKey] || '#6F86FF';
}
},
onLoad() {
this.fetchTabData(this.activeTab);
this.resetScrollPosition(this.tabList[this.activeTab].key);
// 监听滚动控制事件
uni.$on('disableScroll', this.handleDisableScroll);
uni.$on('enableScroll', this.handleEnableScroll);
},
onShow() {
// 隐藏小程序原生tabbar
uni.hideTabBar();
},
onReady() {
// 初始化活动导航项
const currentTabKey = this.tabList[this.activeTab].key;
const navItems = this.navData[currentTabKey] || [];
if (navItems.length > 0) {
this.activeNavItem = navItems[0].id;
}
},
methods: {
bindDateChange(e) {
console.log('eeeee', e);
this.selectTime = e.detail.value
},
switchTab(index) {
this.activeTab = index;
this.fetchTabData(index);
const targetKey = this.tabList[index].key;
this.resetScrollPosition(targetKey);
this.updateTabScrollPosition(index);
},
handleSwiperChange(e) {
const { current } = e.detail;
if (this.activeTab === current) {
return;
}
this.activeTab = current;
this.fetchTabData(current);
const targetKey = this.tabList[current].key;
this.resetScrollPosition(targetKey);
this.updateTabScrollPosition(current);
},
updateTabScrollPosition(index) {
setTimeout(() => {
const query = uni.createSelectorQuery().in(this)
query.select('.tab-container').boundingClientRect()
query.select(`#tab-${index}`).boundingClientRect()
query.exec((res) => {
if (!res || res.length < 2 || !res[0] || !res[1]) {
const estimatedWidth = 120
this.tabScrollPosition = Math.max(0, (index - 1) * estimatedWidth)
return
}
const container = res[0]
const tab = res[1]
const tabCenter = tab.left + (tab.width / 2)
const containerCenter = container.left + (container.width / 2)
const scrollOffset = tabCenter - containerCenter
const newScrollPosition = Math.round(this.tabScrollPosition + scrollOffset)
this.tabScrollPosition = Math.max(0, newScrollPosition)
})
}, 100)
},
// 滚动到指定组件
scrollToComponent(componentId) {
const currentTabKey = this.tabList[this.activeTab].key;
this.scrollIntoViewMap[currentTabKey] = componentId;
this.activeNavItem = componentId;
},
scrollToTop() {
const tabKey = this.currentTabKey;
if (!tabKey) {
return;
}
this.scrollIntoViewMap[tabKey] = '';
this.$nextTick(() => {
this.scrollIntoViewMap[tabKey] = `top-${tabKey}`;
});
const navItems = this.navData[tabKey] || [];
if (navItems.length > 0) {
this.activeNavItem = navItems[0].id;
}
},
resetScrollPosition(tabKey) {
this.scrollIntoViewMap[tabKey] = `top-${tabKey}`;
},
// 切换导航栏收缩状态
toggleNavCollapse() {
this.isNavCollapsed = !this.isNavCollapsed;
},
handleScroll(e) {
this.pageScrollTop = e.detail.scrollTop
},
// 处理禁用滚动
handleDisableScroll() {
this.allowScroll = false;
this.pageStyle = 'overflow-x:hidden; overflow-y:hidden; position:fixed; width:100%; height:100%;';
},
// 处理启用滚动
handleEnableScroll() {
this.allowScroll = true;
this.pageStyle = 'overflow-x:hidden';
},
// 获取各个tab的数据
async fetchTabData(tabIndex) {
// 切换Tab时重置活动导航项
const currentTabKey = this.tabList[tabIndex].key;
const navItems = this.navData[currentTabKey] || [];
if (navItems.length > 0) {
this.activeNavItem = navItems[0].id;
}
},
formatNavName(name = '') {
const trimmed = name.trim();
return trimmed.length > 6 ? `${trimmed.slice(0, 6)}...` : trimmed;
},
getNavShortName(name = '') {
const trimmed = name.trim();
if (trimmed.length <= 2) {
return trimmed || '--';
}
return trimmed.slice(0, 2);
}
}
}
</script>
<style scoped lang="less">
@primary-color: #667eea;
@secondary-color: #764ba2;
@success-color: #52c41a;
@warning-color: #faad14;
@error-color: #ff4d4f;
@text-primary: #333;
@text-secondary: #666;
@text-light: #999;
@bg-light: #f8f9fb;
@bg-white: #ffffff;
@border-radius: 16rpx;
@shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
@shadow-light: 0 2px 12px rgba(0, 0, 0, 0.06);
.digital-dashboard {
height: 100vh;
background: linear-gradient(180deg, @bg-light 0%, #e9ecf4 100%);
.tab-swiper {
height: 100vh;
}
.tab-scroll-view {
height: 100vh;
}
.tab-container {
background: @bg-white;
box-shadow: @shadow-light;
position: fixed;
top: 0;
z-index: 100;
width: 100%;
.tab-list {
height: 80rpx;
display: flex;
padding: 0 32rpx;
box-sizing: border-box;
position: relative;
width: max-content;
white-space: nowrap;
}
/* 隐藏滚动条 */
::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
.tab-item {
position: relative;
padding: 16rpx 24rpx;
margin-right: 16rpx;
flex-shrink: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
min-width: fit-content;
display: inline-block;
&.active {
.tab-text {
color: @primary-color;
font-weight: 600;
transform: translateY(-1px);
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
background: linear-gradient(90deg, @primary-color, @secondary-color);
border-radius: 3rpx;
animation: slideIn 0.3s ease-out;
}
}
.tab-text {
font-size: 28rpx;
color: @text-secondary;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
position: relative;
&::after {
content: '';
position: absolute;
bottom: -4rpx;
left: 0;
width: 100%;
height: 2rpx;
background: @primary-color;
transform: scaleX(0);
transition: transform 0.3s ease;
}
}
&:active {
transform: scale(0.95);
}
}
}
}
.content-container {
padding: 32rpx;
padding-top: 92rpx;
.section-anchor {
height: 0;
margin-top: -120rpx;
padding-top: 120rpx;
}
.tab-content {
/* 移除切换动画 */
.chart-grid {
.chart-row {
margin-bottom: 24rpx;
.chart-card {
background: @bg-white;
border-radius: @border-radius;
padding: 24rpx;
box-shadow: @shadow;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
box-sizing: border-box;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2rpx;
background: linear-gradient(90deg, @primary-color, @secondary-color);
opacity: 0;
transition: opacity 0.3s ease;
}
&:active {
transform: translateY(-2rpx);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
&::before {
opacity: 1;
}
}
&.full {
width: 100%;
min-height: 400rpx;
}
.chart-header {
margin-bottom: 20rpx;
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: @text-primary;
margin-bottom: 4rpx;
}
.chart-subtitle {
font-size: 22rpx;
color: @text-light;
}
}
.chart-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300rpx;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 12rpx;
position: relative;
overflow: hidden;
&.large {
height: 400rpx;
}
&::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(103, 126, 234, 0.1) 0%, transparent 70%);
animation: rotate 30s infinite linear;
}
.placeholder-text {
font-size: 32rpx;
color: @text-secondary;
margin-bottom: 8rpx;
position: relative;
z-index: 1;
}
.placeholder-desc {
font-size: 24rpx;
color: @text-light;
position: relative;
z-index: 1;
}
}
}
}
}
}
}
.timeBox {
width: 100%;
display: flex;
justify-content: flex-end;
}
.side-navigation {
position: fixed;
right: 12rpx;
top: 50%;
transform: translateY(-50%);
z-index: 1000;
transition: all 0.3s ease;
.nav-rail {
width: 70rpx;
max-height: 70vh;
padding: 12rpx 10rpx;
border-radius: 999rpx;
background: rgba(8, 16, 40, 0.78);
backdrop-filter: blur(18rpx);
border: 1rpx solid rgba(255, 255, 255, 0.12);
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 20rpx 48rpx rgba(6, 6, 34, 0.55);
overflow: hidden;
transform-origin: center;
transition: width 0.7s cubic-bezier(0.33, 1, 0.68, 1), height 0.7s cubic-bezier(0.33, 1, 0.68, 1),
padding 0.7s cubic-bezier(0.33, 1, 0.68, 1), border-radius 0.7s ease, box-shadow 0.7s ease,
transform 0.7s cubic-bezier(0.33, 1, 0.68, 1);
position: relative;
}
.rail-body {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.33, 1, 0.68, 1);
}
.rail-body.hidden {
opacity: 0;
transform: translateY(12rpx) scale(0.9);
pointer-events: none;
}
.rail-header,
.rail-footer {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 10rpx;
}
.rail-title {
color: #fff;
font-size: 20rpx;
line-height: 1.4;
opacity: 0.85;
text-align: center;
}
.rail-toggle {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
border: 1rpx solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
background: rgba(255, 255, 255, 0.08);
}
.toggle-icon {
font-size: 24rpx;
transition: transform 0.3s ease;
&.rotated {
transform: rotate(180deg);
}
}
.rail-track {
width: 100%;
flex: 1;
margin: 16rpx 0;
}
.rail-track::-webkit-scrollbar {
width: 0;
height: 0;
}
.rail-item {
width: 100%;
display: flex;
justify-content: center;
padding: 8rpx 0;
cursor: pointer;
transition: transform 0.2s ease;
&:active {
transform: translateX(-3rpx);
}
&.active .rail-dot::after {
opacity: 1;
}
&.active .rail-dot-text {
color: #fff;
}
}
.rail-dot {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.06);
border: 1rpx solid rgba(255, 255, 255, 0.2);
box-shadow: inset 0 0 12rpx rgba(255, 255, 255, 0.08);
&::before {
content: '';
position: absolute;
inset: 4rpx;
border-radius: 50%;
border: 1rpx solid rgba(255, 255, 255, 0.2);
}
&::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(160deg, rgba(255, 255, 255, 0.18), rgba(0, 0, 0, 0.25));
opacity: 0;
transition: opacity 0.3s ease;
z-index: 0;
}
}
.rail-dot-text {
position: relative;
z-index: 1;
font-size: 20rpx;
letter-spacing: 1rpx;
color: rgba(255, 255, 255, 0.85);
font-weight: 400;
white-space: nowrap;
}
.rail-footer {
padding-top: 8rpx;
}
.rail-back {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
border: 1rpx solid rgba(255, 255, 255, 0.25);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
font-size: 20rpx;
gap: 4rpx;
background: rgba(255, 255, 255, 0.08);
line-height: 1.3;
}
.nav-rail.compact {
width: 86rpx;
height: 86rpx;
padding: 0;
border-radius: 50%;
justify-content: center;
border-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 14rpx 30rpx rgba(6, 6, 34, 0.4);
transform: translateY(0) scale(0.96);
}
.rail-compact {
position: absolute;
inset: 0;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
gap: 4rpx;
cursor: pointer;
opacity: 0;
transform: scale(0.8);
pointer-events: none;
transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.33, 1, 0.68, 1);
}
.rail-compact.visible {
opacity: 1;
transform: scale(1);
pointer-events: auto;
}
.compact-icon {
font-size: 28rpx;
line-height: 1;
}
.compact-text {
font-size: 18rpx;
letter-spacing: 2rpx;
}
}
@keyframes slideIn {
from {
width: 0;
opacity: 0;
}
to {
width: 48rpx;
opacity: 1;
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>