1
This commit is contained in:
@ -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"
|
||||
},
|
||||
|
||||
25
src/home.vue
25
src/home.vue
@ -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>
|
||||
|
||||
@ -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) => {
|
||||
// 只有路径变化时才滚动到顶部
|
||||
|
||||
@ -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,
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
316
src/views/layout/mobile/index.vue
Normal file
316
src/views/layout/mobile/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user