2
0

fix:移动和pc自适应菜单

This commit is contained in:
zhongzm
2024-12-24 18:46:45 +08:00
parent f3ca00d88a
commit 70979b17e1
7 changed files with 431 additions and 168 deletions

View File

@@ -103,7 +103,7 @@ setupMixMenuContext();
:sider-visible="siderVisible"
:sider-width="siderWidth"
:sider-collapsed-width="siderCollapsedWidth"
:footer-visible="themeStore.footer.visible"
:footer-visible="appStore.isMobile && themeStore.footer.visible"
:footer-height="themeStore.footer.height"
:fixed-footer="themeStore.footer.fixed"
:right-footer="themeStore.footer.right"
@@ -120,7 +120,7 @@ setupMixMenuContext();
<GlobalContent />
<ThemeDrawer />
<template #footer>
<GlobalFooter />
<GlobalFooter v-if="appStore.isMobile" /> <!-- 修改这里 -->
</template>
</AdminLayout>
</template>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
import {ProfileOutlined,UserOutlined,HomeOutlined} from "@ant-design/icons-vue";
import {useRouterPush} from "@/hooks/common";
const { routerPushByKey } = useRouterPush();
defineOptions({
name: 'GlobalFooter'
});
@@ -6,10 +10,34 @@ defineOptions({
<template>
<DarkModeContainer class="h-full flex-center">
<a href="#" target="_blank" rel="noopener noreferrer">
Copyright © 2024 WANFi
</a>
<div class="flex-item">
<ButtonIcon class="text-icon-large" @click="routerPushByKey('home')">
<HomeOutlined />
</ButtonIcon>
</div>
<div class="flex-item">
<ButtonIcon>
<ProfileOutlined class="text-icon-large" @click="routerPushByKey('billing_billservice')"/>
</ButtonIcon>
</div>
<div class="flex-item">
<ButtonIcon class="text-icon-large" @click="routerPushByKey('user-info/usercard')">
<UserOutlined />
</ButtonIcon>
</div>
</DarkModeContainer>
</template>
<style scoped></style>
<style scoped>
.flex-center {
display: flex;
align-items: center;
width: 100%;
}
.flex-item {
flex-basis: 33.33%; /* 每个子元素占据三分之一的宽度 */
display: flex;
justify-content: center; /* 在各自的空间内居中 */
}
</style>

View File

@@ -52,10 +52,16 @@ const headerMenus = computed(() => {
<DarkModeContainer class="h-full flex-y-center shadow-header">
<GlobalLogo v-if="showLogo" class="h-full" :style="{ width: themeStore.sider.width + 'px' }" />
<HorizontalMenu v-if="showMenu" mode="horizontal" :menus="headerMenus" class="px-12px" />
<div v-else class="h-full flex-y-center flex-1-hidden">
<!-- 只在非移动端显示菜单按钮 -->
<div v-if="!appStore.isMobile" class="h-full flex-y-center flex-1-hidden">
<MenuToggler v-if="showMenuToggler" :collapsed="appStore.siderCollapse" @click="appStore.toggleSiderCollapse" />
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
<GlobalBreadcrumb class="ml-12px" />
</div>
<!-- 在移动端时使用这个来保持布局 -->
<div v-else class="flex-1"></div>
<div class="h-full flex-y-center justify-end">
<LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" />
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />

View File

@@ -10,7 +10,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
tab: {visible: false, cache: true, height: 44, mode: "chrome"},
fixedHeaderAndTab: true,
sider: {inverted: false, width: 220, collapsedWidth: 64, mixWidth: 90, mixCollapsedWidth: 64, mixChildMenuWidth: 200},
footer: {visible: true, fixed: false, height: 36, right: true}
footer: {visible: true, fixed: false, height: 50, right: true}
};
/**

View File

@@ -2,18 +2,18 @@
import HeaderBanner from './modules/header-banner.vue';
import CardData from './modules/card-data.vue';
import LineChart from './modules/line-chart.vue';
import PieChart from './modules/pie-chart.vue';
import ProjectNews from './modules/project-news.vue';
import CreativityBanner from './modules/creativity-banner.vue';
// import PieChart from './modules/pie-chart.vue';
// import ProjectNews from './modules/project-news.vue';
// import CreativityBanner from './modules/creativity-banner.vue';
</script>
<template>
<ASpace direction="vertical" :size="16">
<HeaderBanner />
<LineChart />
<CardData />
<!-- <ARow :gutter="[16, 16]">-->
<!-- <ACol :span="24" :lg="14">-->
<!-- <LineChart />-->
<!-- </ACol>-->
<!-- <ACol :span="24" :lg="10">-->
<!-- <PieChart />-->

View File

@@ -1,151 +1,365 @@
<script setup lang="ts">
import { watch } from 'vue';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useEcharts } from '@/hooks/common/echarts';
import { useI18n } from 'vue-i18n';
import { ref, onMounted } from 'vue';
import { fetchPackageList, submitPackageOrder } from '@/service/api/auth';
import { message } from 'ant-design-vue';
defineOptions({
name: 'LineChart'
const { t } = useI18n();
interface PackageOption {
id: string;
packageName: string;
price: number;
clientNum: number;
traffic: number;
trafficDisplay: string;
isRecommended?: boolean;
promotion?: string;
periodNum: number;
periodType: number;
validityPeriod: string;
}
// 添加有效期类型枚举
const PERIOD_TYPE = {
HOUR: 0,
DAY: 1,
MONTH: 2,
YEAR: 3
} as const;
// 添加有效期单位映射
const PERIOD_UNIT = {
[PERIOD_TYPE.HOUR]: '小时',
[PERIOD_TYPE.DAY]: '天',
[PERIOD_TYPE.MONTH]: '月',
[PERIOD_TYPE.YEAR]: '年'
} as const;
// 格式化有效期显示
const formatValidityPeriod = (num: number, type: number): string => {
const unit = PERIOD_UNIT[type as keyof typeof PERIOD_UNIT] || '未知';
return `${num}${unit}`;
};
// 流量单位转换函数
const formatTraffic = (trafficKB: number): string => {
const KB_TO_MB = 1024;
const KB_TO_GB = 1024 * 1024;
const KB_TO_TB = 1024 * 1024 * 1024;
if (trafficKB >= KB_TO_TB) {
// KB -> TB (除以 1024^3)
return `${(trafficKB / KB_TO_TB).toFixed(2)}TB`;
}
if (trafficKB >= KB_TO_GB) {
// KB -> GB (除以 1024^2)
return `${(trafficKB / KB_TO_GB).toFixed(2)}GB`;
}
if (trafficKB >= KB_TO_MB) {
// KB -> MB (除以 1024)
return `${(trafficKB / KB_TO_MB).toFixed(2)}MB`;
}
// 小于1MB的情况保持KB单位
return `${trafficKB.toFixed(2)}KB`;
};
const packageOptions = ref<PackageOption[]>([]);
const selectedPackage = ref<PackageOption>({
id: '1',
packageName: '',
price: 0,
clientNum: 0,
traffic: 0,
trafficDisplay: '0GB',
isRecommended: false,
promotion: '',
periodNum: 0,
periodType: PERIOD_TYPE.MONTH,
validityPeriod: '0月'
});
const appStore = useAppStore();
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
legend: {
data: [$t('page.home.downloadCount'), $t('page.home.registerCount')]
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [] as string[]
},
yAxis: {
type: 'value'
},
series: [
{
color: '#8e9dff',
name: $t('page.home.downloadCount'),
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#8e9dff'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [] as number[]
},
{
color: '#26deca',
name: $t('page.home.registerCount'),
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#26deca'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: []
}
]
const fetchPackages = async () => {
try {
const response = await fetchPackageList();
if (response.data && Array.isArray(response.data)) {
packageOptions.value = response.data.map(pkg => ({
id: pkg.id,
packageName: pkg.packageName,
price: parseFloat(pkg.price),
clientNum: Number(pkg.clientNum),
traffic: Number(pkg.traffic),
trafficDisplay: formatTraffic(Number(pkg.traffic)),
isRecommended: pkg.isRecommended || false,
promotion: pkg.promotion || '',
periodNum: Number(pkg.periodNum),
periodType: Number(pkg.periodType),
validityPeriod: formatValidityPeriod(Number(pkg.periodNum), Number(pkg.periodType))
}));
async function mockData() {
await new Promise(resolve => {
setTimeout(resolve, 1000);
if (packageOptions.value.length > 0) {
selectedPackage.value = packageOptions.value[0];
}
}
} catch (error) {
console.error('Failed to fetch packages:', error);
}
};
const selectPackage = (option: PackageOption) => {
selectedPackage.value = option;
};
// 添加办理套餐的方法
const handleSubmitOrder = async () => {
try {
await submitPackageOrder(selectedPackage.value.id);
message.success('套餐办理成功!');
} catch (error) {
message.error('套餐办理失败,请重试!');
console.error('Failed to submit order:', error);
}
};
onMounted(async () => {
await fetchPackages();
});
updateOptions(opts => {
opts.xAxis.data = ['06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00', '24:00'];
opts.series[0].data = [4623, 6145, 6268, 6411, 1890, 4251, 2978, 3880, 3606, 4311];
opts.series[1].data = [2208, 2016, 2916, 4512, 8281, 2008, 1963, 2367, 2956, 678];
return opts;
});
}
function updateLocale() {
updateOptions((opts, factory) => {
const originOpts = factory();
opts.legend.data = originOpts.legend.data;
opts.series[0].name = originOpts.series[0].name;
opts.series[1].name = originOpts.series[1].name;
return opts;
});
}
async function init() {
mockData();
}
watch(
() => appStore.locale,
() => {
updateLocale();
}
);
// init
init();
</script>
<template>
<ACard :bordered="false" class="card-wrapper">
<div ref="domRef" class="h-360px overflow-hidden"></div>
</ACard>
<div class="package-container">
<!-- 顶部价格展示 -->
<div class="price-header">
<div class="price">
<span class="currency">¥</span>
<span class="amount">{{ selectedPackage.price }}</span>
<span class="period">/</span>
</div>
<div class="subtitle">{{ selectedPackage.packageName }}</div>
</div>
<!-- 套餐选项 -->
<div class="package-options">
<h3 class="section-title">{{ t('page.setmeal.changablelevel') }}</h3>
<div class="options-grid">
<div
v-for="option in packageOptions"
:key="option.id"
:class="[
'option-card',
{
recommended: option.isRecommended,
selected: selectedPackage.id === option.id
}
]"
@click="selectPackage(option)"
>
<div v-if="option.isRecommended" class="recommended-tag">
{{ t('page.setmeal.highlyrecommended') }}
</div>
<div class="package-name">{{ option.packageName }}</div>
<div class="price">¥{{ option.price }}</div>
<div class="traffic">{{ option.trafficDisplay }}</div>
<div class="device-count">{{ option.clientNum }}台设备</div>
</div>
</div>
</div>
<!-- 套餐详情 -->
<div class="package-details">
<h3 class="section-title">{{ t('page.setmeal.mealdetail') }}</h3>
<div class="details-list">
<div class="detail-item">
<div class="label">套餐名称</div>
<div class="value">{{ selectedPackage.packageName }}</div>
</div>
<div class="detail-item">
<div class="label">{{ t('page.setmeal.GeneralPurposeTraffic') }}</div>
<div class="value">{{ selectedPackage.trafficDisplay }}当月有效</div>
</div>
<div class="detail-item">
<div class="label">设备数量</div>
<div class="value">最多{{ selectedPackage.clientNum }}台设备同时在线</div>
</div>
<div class="detail-item">
<div class="label">有效期限</div>
<div class="value">{{ selectedPackage.validityPeriod }}</div>
</div>
<div class="bottom-bar">
<button
class="btn-primary"
@click="handleSubmitOrder"
:disabled="!selectedPackage.id"
>
{{ t('page.setmeal.Applynow') }}
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>
<style scoped>
.package-container {
min-height: 100vh;
padding: 16px 16px 80px;
}
.price-header {
background: #fff1f0;
padding: 20px;
border-radius: 12px;
margin-bottom: 16px;
}
.price {
color: #ff4d4f;
margin-bottom: 8px;
}
.currency {
font-size: 20px;
}
.amount {
font-size: 32px;
font-weight: bold;
}
.period {
font-size: 16px;
}
.subtitle {
font-size: 14px;
color: #666;
}
.section-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
color: #333;
position: relative;
padding-left: 12px;
}
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background: #1890ff;
border-radius: 2px;
}
.options-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.option-card {
background: white;
padding: 16px;
border-radius: 8px;
text-align: center;
position: relative;
border: 1px solid #e8e8e8;
cursor: pointer;
transition: all 0.3s;
}
.option-card:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.option-card.selected {
border-color: #ff4d4f;
background: #fff1f0;
}
.recommended-tag {
position: absolute;
top: 0;
left: 0;
background: #ff4d4f;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 0 0 8px 0;
}
.package-details {
background: white;
padding: 16px;
border-radius: 12px;
}
.detail-item {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.detail-item:last-child {
border-bottom: none;
}
.label {
width: 80px;
color: #666;
}
.value {
flex: 1;
color: #333;
}
.value.highlight {
color: #ff4d4f;
}
.bottom-bar {
padding: 12px 16px;
display: flex;
justify-content: center;
align-items: center;
height: 60px;
}
.btn-primary {
background: #ff4d4f;
color: white;
border: none;
padding: 12px 32px;
border-radius: 24px;
font-size: 16px;
width: 90%;
max-width: 400px;
}
.package-name {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
.traffic {
font-size: 14px;
color: #666;
margin-top: 8px;
}
.device-count {
font-size: 14px;
color: #666;
margin-top: 4px;
}
</style>

View File

@@ -2,7 +2,7 @@
import { useAuthStore } from '@/store/modules/auth';
import { useRouter } from 'vue-router';
import { computed } from 'vue';
import { UserOutlined, LockOutlined, SafetyCertificateOutlined, MobileOutlined, RightOutlined} from '@ant-design/icons-vue';
import { UserOutlined, LockOutlined, SafetyCertificateOutlined, MobileOutlined, RightOutlined, LinkOutlined, ApiOutlined, CalendarOutlined} from '@ant-design/icons-vue';
import { useRouterPush } from '@/hooks/common/router';
import {useI18n} from "vue-i18n";
@@ -42,7 +42,22 @@ const menuItems = [
icon: MobileOutlined,
title: t('page.usercard.deviceconsole'),
path: 'device'
}
},
{
icon: LinkOutlined,
title: t('page.usercard.access'),
path: 'access'
},
{
icon: ApiOutlined,
title: t('page.usercard.records'),
path: 'records'
},
{
icon: CalendarOutlined,
title: t('page.usercard.cdrlrecords'),
path: 'cdrlrecords'
},
];
const handleMenuClick = (path: string) => {