This commit is contained in:
yiqiuyang
2026-07-30 16:54:55 +08:00
parent 2b9a322335
commit 661ee7d7a3
6 changed files with 380 additions and 11 deletions

View File

@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^13.8.0",
"axios": "^1.12.2",
@ -21,6 +22,7 @@
"quill": "^2.0.3",
"terser": "^5.43.1",
"v-viewer": "^3.0.11",
"vant": "^4.10.0",
"vue": "^3.5.18",
"vue-router": "^4.5.1"
},

View File

@ -1,10 +1,18 @@
<script setup>
import {storeToRefs} from 'pinia'
import {useNavStore} from '@/store/nav'
import {isMobileDevice} from '@/utils/rem'
import {onMounted, ref} from 'vue'
import Header from './views/layout/pc/header/index.vue'
import Footer from './views/layout/pc/footer/index.vue'
import {onMounted, ref, watch} from 'vue'
import MobileLayout from './views/layout/mobile/index.vue'
import {visit} from '@/api/index'
import axios from 'axios'
// 移动端判断
const navStore = useNavStore()
const {isMobile} = storeToRefs(navStore)
const referrer = ref(document.referrer || '')
const ipUrl = ref('')
@ -12,6 +20,14 @@ const CACHE_KEY = 'ip'
const TIME_KEY = 'ipFetchTime'
const CACHE_MIN = 60 * 60 * 1000 // 1h
// 初始化移动端状态
navStore.setIsMobile(isMobileDevice())
// 监听窗口 resize
window.addEventListener('resize', () => {
navStore.setIsMobile(isMobileDevice())
})
async function getUrl() {
const now = Date.now()
const cachedIp = sessionStorage.getItem(CACHE_KEY)
@ -40,7 +56,6 @@ function postVisit() {
referrer: referrer.value,
real_ip: ipUrl.value,
}
visit(params).then((res) => {})
}
@ -50,7 +65,8 @@ onMounted(async () => {
</script>
<template>
<div id="app">
<!-- PC端布局 -->
<div v-if="!isMobile" id="app">
<Header id="header" />
<router-view id="main" v-slot="{Component, title}">
<keep-alive include="New">
@ -59,6 +75,9 @@ onMounted(async () => {
</router-view>
<Footer id="footer" />
</div>
<!-- 移动端布局 -->
<MobileLayout v-else />
</template>
<style lang="scss" scoped>

View File

@ -105,12 +105,21 @@ const router = createRouter({
routes,
})
// router.beforeEach(async (to, from, next) => {
// const {useNavStore} = await import('@/store/nav.js')
// const navStore = useNavStore()
// navStore.setLoad(true)
// next()
// })
// 移动端/PC端断点
const MOBILE_BREAKPOINT = 1000
// 判断是否为移动端
function checkIsMobile() {
return window.innerWidth < MOBILE_BREAKPOINT
}
router.beforeEach(async (to, from, next) => {
const {useNavStore} = await import('@/store/nav.js')
const navStore = useNavStore()
// 更新移动端状态
navStore.setIsMobile(checkIsMobile())
next()
})
router.afterEach((to, from) => {
// 只有路径变化时才滚动到顶部

View File

@ -6,6 +6,7 @@ export const useNavStore = defineStore(
() => {
const navIndex = ref('/')
const showLoad = ref(true)
const isMobile = ref(window.innerWidth < 1000)
const changeNavIndex = (id) => {
if (!id) {
@ -20,11 +21,17 @@ export const useNavStore = defineStore(
showLoad.value = status
}
const setIsMobile = (val) => {
isMobile.value = val
}
return {
navIndex,
showLoad,
isMobile,
changeNavIndex,
setLoad,
setIsMobile,
}
},
{

View File

@ -1,13 +1,28 @@
// rem等比适配配置
// 基准大小
const baseSize = 16
// 设计稿宽度
const designWidth = 1920
// 设计稿宽度PC端
const pcDesignWidth = 1920
// 设计稿宽度(移动端)
const mobileDesignWidth = 375
// 移动端/PC端断点
const mobileBreakpoint = 1000
// 最大缩放比例
const maxScale = 2
// 检测是否为移动端
export function isMobileDevice() {
return window.innerWidth < mobileBreakpoint
}
// 获取当前设计稿宽度
function getDesignWidth() {
return isMobileDevice() ? mobileDesignWidth : pcDesignWidth
}
// 设置 rem 函数
function setRem() {
const designWidth = getDesignWidth()
// 当前页面宽度相对于设计稿宽度的缩放比例
const scale = document.documentElement.clientWidth / designWidth
// 设置页面根节点字体大小最高放大比例为maxScale
@ -18,6 +33,7 @@ function setRem() {
export function fontSize(res) {
const clientWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
if (!clientWidth) return res
const designWidth = getDesignWidth()
const scale = clientWidth / designWidth
return res * Math.min(scale, maxScale)
}

View File

@ -0,0 +1,316 @@
<script setup>
import {ref, onMounted, watch} from 'vue'
import {useRouter} from 'vue-router'
import {useNavStore} from '@/store/nav'
import {storeToRefs} from 'pinia'
import {Menu, ArrowDown, ArrowRight} from '@element-plus/icons-vue'
const navStore = useNavStore()
const {navIndex} = storeToRefs(navStore)
const router = useRouter()
const sidebarVisible = ref(false)
const navList = ref([])
const activeIndex = ref('/')
const expandedSubMenus = ref([])
const toggleSidebar = () => {
sidebarVisible.value = !sidebarVisible.value
}
const closeSidebar = () => {
sidebarVisible.value = false
}
const toggleSubMenu = (url) => {
const idx = expandedSubMenus.value.indexOf(url)
if (idx > -1) {
expandedSubMenus.value.splice(idx, 1)
} else {
expandedSubMenus.value.push(url)
}
}
const handleNavClick = (nav) => {
if (nav.hasChildren || nav.children?.length > 0) {
toggleSubMenu(nav.url)
return
}
if (nav.open) {
window.open(nav.url)
sidebarVisible.value = false
return
}
navStore.changeNavIndex(nav.url)
activeIndex.value = nav.url
router.push(nav.url)
sidebarVisible.value = false
}
watch(
navIndex,
(newVal) => {
activeIndex.value = newVal
},
{immediate: true}
)
onMounted(() => {
navList.value = window.nav?.header || []
})
const toMIIF = () => {
window.open('https://beian.miit.gov.cn')
}
</script>
<template>
<div class="mobile-layout">
<!-- ==================== 顶部 Header ==================== -->
<div class="mobile-header">
<el-icon class="header-menu-icon" :size="22" @click="toggleSidebar">
<Menu />
</el-icon>
<div class="header-logo" @click="router.push('/')"></div>
</div>
<!-- ==================== 侧边栏 ==================== -->
<transition name="sidebar-slide">
<div v-show="sidebarVisible" class="sidebar">
<div class="sidebar-nav">
<template v-for="nav in navList" :key="nav.id">
<!-- 有子菜单 -->
<div v-if="nav.hasChildren || nav.children?.length > 0" class="nav-group">
<div
class="nav-item nav-item--parent"
:class="{'nav-item--expanded': expandedSubMenus.includes(nav.url)}"
@click="handleNavClick(nav)"
>
<span class="nav-label">{{ nav.label }}</span>
<el-icon class="nav-arrow" :size="12">
<ArrowDown v-if="expandedSubMenus.includes(nav.url)" />
<ArrowRight v-else />
</el-icon>
</div>
<div v-show="expandedSubMenus.includes(nav.url)" class="nav-sub">
<div
v-for="child in nav.children"
:key="child.id"
class="nav-item nav-item--child"
:class="{'nav-item--active': activeIndex === child.url}"
@click="handleNavClick(child)"
>
{{ child.label }}
</div>
</div>
</div>
<!-- 无子菜单 -->
<div
v-else
class="nav-item"
:class="{'nav-item--active': activeIndex === nav.url}"
@click="handleNavClick(nav)"
>
<span class="nav-label">{{ nav.label }}</span>
</div>
</template>
</div>
</div>
</transition>
<!-- ==================== Content 内容区域 ==================== -->
<div class="mobile-content">
<router-view v-slot="{Component}">
<keep-alive include="New">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
<!-- ==================== Footer 底部 ==================== -->
<div class="mobile-footer">
<div class="footer-info">
<span>Copyright © 2023.Rangu Technology Co.,Ltd.</span>
<span>燃谷科技南京有限公司</span>
</div>
<div class="footer-beian" @click="toMIIF">苏ICP备2023030548号-1</div>
</div>
</div>
</template>
<style lang="scss" scoped>
// ==================== 布局容器 ====================
.mobile-layout {
width: 100%;
max-width: 100vw;
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: $bg_color;
overflow-x: hidden;
}
// ==================== Header ====================
.mobile-header {
position: fixed;
top: 0;
left: 0;
width: 100%; /* no */
max-width: 100vw;
height: 44px; /* no */
background-color: $bg_color;
z-index: 110;
display: flex;
align-items: center;
padding: 0 10px; /* no */
box-sizing: border-box;
.header-menu-icon {
color: $white;
cursor: pointer;
flex-shrink: 0;
}
.header-logo {
background-image: url('/static/images/header/logo.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
width: 100px;
height: 32px; /* no */
cursor: pointer;
flex-shrink: 0;
}
}
// ==================== 侧边栏遮罩层 ====================
.sidebar-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 100;
}
.mask-fade-enter-active,
.mask-fade-leave-active {
transition: opacity 0.3s ease;
}
.mask-fade-enter-from,
.mask-fade-leave-to {
opacity: 0;
}
// ==================== 侧边栏(浮动层) ====================
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 160px; /* no */
height: 100%;
background-color: $bg_color;
z-index: 105;
overflow-y: auto;
padding-top: 56px; /* no */
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.3); /* no */
}
.sidebar-slide-enter-active,
.sidebar-slide-leave-active {
transition: transform 0.3s ease;
}
.sidebar-slide-enter-from,
.sidebar-slide-leave-to {
transform: translateX(-100%);
}
.sidebar-nav {
padding: 0;
}
.nav-group {
border-bottom: 1px solid rgba(255, 255, 255, 0.08); /* no */
}
.nav-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px; /* no */
cursor: pointer;
color: #999999;
font-size: 14px;
transition: all 0.2s ease;
user-select: none;
&:active {
background-color: rgba(255, 255, 255, 0.05);
}
&--active {
color: $white;
}
&--parent {
font-weight: 500;
}
&--expanded {
color: $white;
}
&--child {
padding-left: 32px; /* no */
font-size: 13px;
}
.nav-label {
flex: 1;
}
.nav-arrow {
color: #999999;
flex-shrink: 0;
}
}
.nav-sub {
overflow: hidden;
}
// ==================== Content 内容区域 ====================
.mobile-content {
margin-top: 44px; /* no */
flex: 1;
width: 100%;
max-width: 100vw;
overflow-x: hidden;
}
// ==================== Footer 底部 ====================
.mobile-footer {
width: 100%;
max-width: 100vw;
padding: 20px 16px; /* no */
box-sizing: border-box;
text-align: center;
background-color: $bg_color;
.footer-info {
display: flex;
flex-direction: column;
gap: 4px;
color: #999999;
font-size: 12px;
margin-bottom: 8px;
}
.footer-beian {
color: #0056b3;
font-size: 11px;
cursor: pointer;
}
}
</style>