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

View File

@@ -1,4 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import {ProfileOutlined,UserOutlined,HomeOutlined} from "@ant-design/icons-vue";
import {useRouterPush} from "@/hooks/common";
const { routerPushByKey } = useRouterPush();
defineOptions({ defineOptions({
name: 'GlobalFooter' name: 'GlobalFooter'
}); });
@@ -6,10 +10,34 @@ defineOptions({
<template> <template>
<DarkModeContainer class="h-full flex-center"> <DarkModeContainer class="h-full flex-center">
<a href="#" target="_blank" rel="noopener noreferrer"> <div class="flex-item">
Copyright © 2024 WANFi <ButtonIcon class="text-icon-large" @click="routerPushByKey('home')">
</a> <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> </DarkModeContainer>
</template> </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"> <DarkModeContainer class="h-full flex-y-center shadow-header">
<GlobalLogo v-if="showLogo" class="h-full" :style="{ width: themeStore.sider.width + 'px' }" /> <GlobalLogo v-if="showLogo" class="h-full" :style="{ width: themeStore.sider.width + 'px' }" />
<HorizontalMenu v-if="showMenu" mode="horizontal" :menus="headerMenus" class="px-12px" /> <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" /> <MenuToggler v-if="showMenuToggler" :collapsed="appStore.siderCollapse" @click="appStore.toggleSiderCollapse" />
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" /> <GlobalBreadcrumb class="ml-12px" />
</div> </div>
<!-- 在移动端时使用这个来保持布局 -->
<div v-else class="flex-1"></div>
<div class="h-full flex-y-center justify-end"> <div class="h-full flex-y-center justify-end">
<LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" /> <LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" />
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" /> <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"}, tab: {visible: false, cache: true, height: 44, mode: "chrome"},
fixedHeaderAndTab: true, fixedHeaderAndTab: true,
sider: {inverted: false, width: 220, collapsedWidth: 64, mixWidth: 90, mixCollapsedWidth: 64, mixChildMenuWidth: 200}, 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 HeaderBanner from './modules/header-banner.vue';
import CardData from './modules/card-data.vue'; import CardData from './modules/card-data.vue';
import LineChart from './modules/line-chart.vue'; import LineChart from './modules/line-chart.vue';
import PieChart from './modules/pie-chart.vue'; // import PieChart from './modules/pie-chart.vue';
import ProjectNews from './modules/project-news.vue'; // import ProjectNews from './modules/project-news.vue';
import CreativityBanner from './modules/creativity-banner.vue'; // import CreativityBanner from './modules/creativity-banner.vue';
</script> </script>
<template> <template>
<ASpace direction="vertical" :size="16"> <ASpace direction="vertical" :size="16">
<HeaderBanner /> <HeaderBanner />
<LineChart />
<CardData /> <CardData />
<!-- <ARow :gutter="[16, 16]">--> <!-- <ARow :gutter="[16, 16]">-->
<!-- <ACol :span="24" :lg="14">--> <!-- <ACol :span="24" :lg="14">-->
<!-- <LineChart />-->
<!-- </ACol>--> <!-- </ACol>-->
<!-- <ACol :span="24" :lg="10">--> <!-- <ACol :span="24" :lg="10">-->
<!-- <PieChart />--> <!-- <PieChart />-->

View File

@@ -1,151 +1,365 @@
<script setup lang="ts"> <script setup lang="ts">
import { watch } from 'vue'; import { useI18n } from 'vue-i18n';
import { $t } from '@/locales'; import { ref, onMounted } from 'vue';
import { useAppStore } from '@/store/modules/app'; import { fetchPackageList, submitPackageOrder } from '@/service/api/auth';
import { useEcharts } from '@/hooks/common/echarts'; import { message } from 'ant-design-vue';
defineOptions({ const { t } = useI18n();
name: 'LineChart'
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 fetchPackages = async () => {
try {
const { domRef, updateOptions } = useEcharts(() => ({ const response = await fetchPackageList();
tooltip: { if (response.data && Array.isArray(response.data)) {
trigger: 'axis', packageOptions.value = response.data.map(pkg => ({
axisPointer: { id: pkg.id,
type: 'cross', packageName: pkg.packageName,
label: { price: parseFloat(pkg.price),
backgroundColor: '#6a7985' clientNum: Number(pkg.clientNum),
} traffic: Number(pkg.traffic),
} trafficDisplay: formatTraffic(Number(pkg.traffic)),
}, isRecommended: pkg.isRecommended || false,
legend: { promotion: pkg.promotion || '',
data: [$t('page.home.downloadCount'), $t('page.home.registerCount')] periodNum: Number(pkg.periodNum),
}, periodType: Number(pkg.periodType),
grid: { validityPeriod: formatValidityPeriod(Number(pkg.periodNum), Number(pkg.periodType))
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: []
}
]
})); }));
async function mockData() { if (packageOptions.value.length > 0) {
await new Promise(resolve => { selectedPackage.value = packageOptions.value[0];
setTimeout(resolve, 1000); }
}
} 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> </script>
<template> <template>
<ACard :bordered="false" class="card-wrapper"> <div class="package-container">
<div ref="domRef" class="h-360px overflow-hidden"></div> <!-- 顶部价格展示 -->
</ACard> <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> </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 { useAuthStore } from '@/store/modules/auth';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { computed } from 'vue'; 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 { useRouterPush } from '@/hooks/common/router';
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
@@ -42,7 +42,22 @@ const menuItems = [
icon: MobileOutlined, icon: MobileOutlined,
title: t('page.usercard.deviceconsole'), title: t('page.usercard.deviceconsole'),
path: 'device' 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) => { const handleMenuClick = (path: string) => {