Просмотр исходного кода

feat(mobile): 添加移动端基础页面、组件和响应式适配

- 新增移动端产品页面(DNS、SDK、Web、Boost)和占位页面(docs、solutions)
- 重构移动端布局,使用新的 Header 和 Footer 组件替换旧组件
- 添加 postcss-pxtorem 实现移动端 REM 适配,配置根字体大小响应式
- 更新全局重定向中间件,优化移动端检测逻辑
- 配置 Nuxt 以支持移动端独立的样式转换和视口设置
reaper 1 месяц назад
Родитель
Сommit
b9981305cd

+ 6 - 0
app/assets/scss/main.scss

@@ -11,6 +11,12 @@ html {
   background-color: #030014;
   font-family: 'Source Han Sans CN', sans-serif;
 }
+
+@media (max-width: 768px) {
+  html {
+    font-size: clamp(32px, 10vw, 46px);
+  }
+}
 html,
 body,
 ul,

+ 0 - 144
app/components/HeaderH5.vue

@@ -1,144 +0,0 @@
-<template>
-  <header class="h5-header">
-    <div class="h5-header-container">
-      <a href="/" class="brand">
-        <NuxtImg src="/logo.png" alt="DDAC logo" class="brand-logo" width="24" height="24" />
-        <h1 class="brand-title">DDAC</h1>
-      </a>
-      <button class="menu-button" @click="toggleMenu">
-        <span class="menu-icon"></span>
-      </button>
-    </div>
-    <nav v-show="isMenuOpen" class="h5-nav">
-      <div class="nav-item">首页</div>
-      <div class="nav-item">产品</div>
-      <div class="nav-item">行业解决方案</div>
-      <div class="nav-item">文档中心</div>
-      <div class="nav-item">了解我们</div>
-      <div class="nav-actions">
-        <div class="nav-action-item">登录</div>
-        <div class="nav-action-item register">注册</div>
-      </div>
-    </nav>
-  </header>
-</template>
-
-<script setup>
-const isMenuOpen = ref(false)
-
-const toggleMenu = () => {
-  isMenuOpen.value = !isMenuOpen.value
-}
-</script>
-
-<style lang="scss" scoped>
-.h5-header {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  z-index: 1000;
-  background: #030014;
-  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
-}
-
-.h5-header-container {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 12px 16px;
-}
-
-.brand {
-  display: flex;
-  align-items: center;
-  text-decoration: none;
-
-  .brand-logo {
-    margin-right: 8px;
-  }
-
-  .brand-title {
-    color: #FFF;
-    font-size: 20px;
-    font-weight: 700;
-    line-height: 24px;
-    margin: 0;
-  }
-}
-
-.menu-button {
-  width: 32px;
-  height: 32px;
-  background: none;
-  border: none;
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0;
-}
-
-.menu-icon {
-  display: block;
-  width: 24px;
-  height: 2px;
-  background-color: #fff;
-  position: relative;
-
-  &::before,
-  &::after {
-    content: '';
-    position: absolute;
-    width: 24px;
-    height: 2px;
-    background-color: #fff;
-    left: 0;
-  }
-
-  &::before {
-    top: -8px;
-  }
-
-  &::after {
-    top: 8px;
-  }
-}
-
-.mobile-nav {
-  background: #030014;
-  border-top: 1px solid rgba(255, 255, 255, 0.1);
-  padding: 16px;
-}
-
-.nav-item {
-  padding: 12px 0;
-  color: #FFF;
-  font-size: 16px;
-  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
-
-  &:last-of-type {
-    border-bottom: none;
-  }
-}
-
-.nav-actions {
-  display: flex;
-  gap: 12px;
-  margin-top: 16px;
-}
-
-.nav-action-item {
-  flex: 1;
-  padding: 12px;
-  text-align: center;
-  color: #FFF;
-  font-size: 14px;
-  border-radius: 8px;
-  background: rgba(255, 255, 255, 0.05);
-
-  &.register {
-    background: linear-gradient(91deg, #A39DFF 1.24%, #7D46FF 122.93%);
-  }
-}
-</style>

+ 0 - 0
app/components/FooterH5.vue → app/components/mobile/Footer.vue


+ 236 - 0
app/components/mobile/Header.vue

@@ -0,0 +1,236 @@
+<template>
+  <header class="mb-header">
+    <div class="mb-header-container">
+      <a href="/" class="brand">
+        <NuxtImg src="/logo.png" alt="DDAC logo" class="brand-logo" width="14" height="14" />
+        <h1 class="brand-title">DDAC</h1>
+      </a>
+      <div class="mb-header-right">
+        <Icon name="lineicons:magnifier" size="20" />
+        <Icon name="material-symbols:account-circle-full" size="20" />
+        <Icon @click="toggleMenu" name="material-symbols:menu-rounded" size="20" />
+      </div>
+    </div>
+    <Transition name="nav-expand">
+      <nav v-show="isMenuOpen" class="mobile-nav">
+        <div class="nav-item">
+          <NuxtLink to="/mobile" @click="closeMenu">首页</NuxtLink>
+        </div>
+        <div class="nav-item-wrapper">
+          <div class="nav-item" @click="toggleProducts">
+            <span>产品</span>
+            <Icon name="line-md:chevron-down" class="nav-item-icon" :class="{ 'icon-rotated': isProductsOpen }" />
+          </div>
+          <Transition name="submenu-expand">
+            <div v-show="isProductsOpen" class="dropdown-menu">
+              <NuxtLink to="/mobile/products/sdk" class="dropdown-item" @click="closeMenu">
+                <img class="item-img" src="~/assets/svg/header/icon1.svg" alt="SDK 安全加固">
+                <p>SDK 安全加固</p>
+              </NuxtLink>
+              <NuxtLink to="/mobile/products/web" class="dropdown-item" @click="closeMenu">
+                <img class="item-img" src="~/assets/svg/header/icon2.svg" alt="Web 安全加速">
+                <p>Web 安全加速</p>
+              </NuxtLink>
+              <NuxtLink to="/mobile/products/dns" class="dropdown-item" @click="closeMenu">
+                <img class="item-img" src="~/assets/svg/header/icon3.svg" alt="DNS 全球解析">
+                <p>DNS 全球解析</p>
+              </NuxtLink>
+              <NuxtLink to="/mobile/products/boost" class="dropdown-item" @click="closeMenu">
+                <img class="item-img" src="~/assets/svg/header/icon4.svg" alt="高防服务器">
+                <p>高防服务器</p>
+              </NuxtLink>
+            </div>
+          </Transition>
+        </div>
+        <div class="nav-item">
+          <NuxtLink to="/mobile/solutions" @click="closeMenu">行业解决方案</NuxtLink>
+        </div>
+        <div class="nav-item">
+          <NuxtLink to="/mobile/docs" @click="closeMenu">文档中心</NuxtLink>
+        </div>
+        <div class="nav-item">
+          <NuxtLink to="/mobile/about" @click="closeMenu">了解我们</NuxtLink>
+        </div>
+      </nav>
+    </Transition>
+  </header>
+</template>
+
+<script setup>
+const isMenuOpen = ref(false)
+const isProductsOpen = ref(false)
+
+const toggleMenu = () => {
+  isMenuOpen.value = !isMenuOpen.value
+  // 关闭菜单时也关闭产品子菜单
+  if (!isMenuOpen.value) {
+    isProductsOpen.value = false
+  }
+}
+
+const closeMenu = () => {
+  isMenuOpen.value = false
+  isProductsOpen.value = false
+}
+
+const toggleProducts = () => {
+  isProductsOpen.value = !isProductsOpen.value
+}
+</script>
+
+<style lang="scss" scoped>
+.mb-header {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 1000;
+  background: #030014;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.mb-header-container {
+  height: 44px;
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  box-sizing: border-box;
+  padding: 0 16px;
+}
+
+.brand {
+  display: flex;
+  align-items: center;
+  text-decoration: none;
+
+  .brand-logo {
+    margin-right: 8px;
+  }
+
+  .brand-title {
+    color: #FFF;
+    font-size: 14px;
+    font-weight: 700;
+  }
+}
+
+.mb-header-right {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  color: #fff;
+}
+
+// Nav 展开动画
+.nav-expand-enter-active,
+.nav-expand-leave-active {
+  overflow: hidden;
+  transition: max-height 0.3s ease-out;
+}
+
+.nav-expand-enter-from,
+.nav-expand-leave-to {
+  max-height: 0 !important;
+}
+
+.nav-expand-enter-to,
+.nav-expand-leave-from {
+  max-height: 500px !important;
+}
+
+// 子菜单展开动画
+.submenu-expand-enter-active,
+.submenu-expand-leave-active {
+  overflow: hidden;
+  transition: max-height 0.25s ease-out;
+}
+
+.submenu-expand-enter-from,
+.submenu-expand-leave-to {
+  max-height: 0 !important;
+}
+
+.submenu-expand-enter-to,
+.submenu-expand-leave-from {
+  max-height: 300px !important;
+}
+
+.mobile-nav {
+  background: #030014;
+  border-top: 1px solid rgba(255, 255, 255, 0.1);
+  max-height: 500px;
+}
+
+.nav-item {
+  padding: 10px;
+  color: #FFF;
+  font-size: 16px;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+  cursor: pointer;
+
+  &:last-of-type {
+    border-bottom: none;
+  }
+
+  a {
+    color: inherit;
+    text-decoration: none;
+    display: block;
+  }
+}
+
+.nav-item-wrapper {
+  &:last-of-type {
+    border-bottom: none;
+  }
+}
+
+.nav-item-icon {
+  transition: transform 0.3s ease;
+  float: right;
+}
+
+.icon-rotated {
+  transform: rotate(180deg);
+}
+
+.dropdown-menu {
+  background: rgba(63, 63, 63, 0.60);
+  max-height: 300px;
+  overflow: hidden;
+  padding: 0;
+}
+
+.dropdown-item {
+  padding: 12px;
+  display: flex;
+  align-items: center;
+  color: #FFF;
+  text-decoration: none;
+  font-size: 14px;
+  transition: background 0.2s;
+  gap: 12px;
+  white-space: nowrap;
+
+  &:hover {
+    background: rgba(255, 255, 255, 0.1);
+  }
+
+  .item-img {
+    width: 20px;
+    height: 20px;
+    flex-shrink: 0;
+  }
+
+  p {
+    color: #FFF;
+    font-size: 14px;
+    font-weight: 500;
+    margin: 0;
+    white-space: nowrap;
+  }
+}
+</style>

+ 25 - 2
app/layouts/mobile.vue

@@ -1,15 +1,37 @@
 <template>
   <div class="mobile-layout">
-    <HeaderH5 />
+    <Header />
     <main class="mobile-layout-main">
       <slot />
     </main>
-    <FooterH5 />
+    <Footer />
   </div>
 </template>
 
 <script setup>
+import Header from '~/components/mobile/Header.vue'
+import Footer from '~/components/mobile/Footer.vue'
 
+// const { width } = useWindowSize()
+
+// const updateRootFontSize = (value) => {
+//   const baseWidth = 375
+//   const baseFont = 37.5
+//   const minFont = 32
+//   const maxFont = 46
+//   const nextFont = (value / baseWidth) * baseFont
+//   const finalFont = Math.min(Math.max(nextFont, minFont), maxFont)
+
+//   if (typeof document !== 'undefined') {
+//     document.documentElement.style.fontSize = `${finalFont}px`
+//   }
+// }
+
+// watchEffect(() => {
+//   if (width.value > 0) {
+//     updateRootFontSize(width.value)
+//   }
+// })
 </script>
 
 <style scoped lang="scss">
@@ -19,6 +41,7 @@
   background-color: #030014;
   display: flex;
   flex-direction: column;
+  box-sizing: border-box;
 }
 
 .mobile-layout-main {

+ 11 - 12
app/middleware/redirect.global.ts

@@ -1,26 +1,25 @@
 export default defineNuxtRouteMiddleware((to, from) => {
   // 仅在根路径 '/' 进行判断,避免循环重定向或其他页面被误伤
-  if (to.path === '/') {
+  if (to.path === '/' || to.path === '/web' || to.path === '/mobile') {
     // 获取 User-Agent
     // 注意:在 SSR 环境下,我们需要从请求头获取;在客户端导航时,使用 navigator
-    let userAgent = '';
-    
+    let userAgent = ''
+
     if (process.server) {
-      const headers = useRequestHeaders(['user-agent']);
-      userAgent = headers['user-agent'] || '';
+      const headers = useRequestHeaders(['user-agent'])
+      userAgent = headers['user-agent'] || ''
     } else {
-      userAgent = navigator.userAgent;
+      userAgent = navigator.userAgent
     }
 
     // 定义移动端正则匹配
-    const isMobile = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(userAgent);
+    const isMobile = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(userAgent)
 
     // 执行重定向
     // 302: 临时重定向 (建议先用 302 测试,稳定后可改为 301)
-    if (isMobile) {
-      return navigateTo('/mobile', { redirectCode: 301 });
-    } else {
-      return navigateTo('/web', { redirectCode: 301 });
+    const target = isMobile ? '/mobile' : '/web'
+    if (to.path !== target) {
+      return navigateTo(target, { redirectCode: 302 })
     }
   }
-});
+})

+ 5 - 0
app/pages/mobile/docs.vue

@@ -0,0 +1,5 @@
+<template>
+</template>
+<script setup>
+
+</script>

+ 26 - 0
app/pages/mobile/products/boost.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="mobile-product">
+    <h1>高防服务器</h1>
+  </div>
+</template>
+
+<script setup>
+useHead(() => ({
+  title: '高防服务器 - 移动端',
+  meta: [
+    { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }
+  ]
+}))
+</script>
+
+<style scoped lang="scss">
+.mobile-product {
+  color: #fff;
+  text-align: center;
+  padding-top: 60px;
+
+  h1 {
+    font-size: 24px;
+  }
+}
+</style>

+ 26 - 0
app/pages/mobile/products/dns.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="mobile-product">
+    <h1>DNS 全球解析</h1>
+  </div>
+</template>
+
+<script setup>
+useHead(() => ({
+  title: 'DNS全球解析 - 移动端',
+  meta: [
+    { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }
+  ]
+}))
+</script>
+
+<style scoped lang="scss">
+.mobile-product {
+  color: #fff;
+  text-align: center;
+  padding-top: 60px;
+
+  h1 {
+    font-size: 24px;
+  }
+}
+</style>

+ 26 - 0
app/pages/mobile/products/sdk.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="mobile-product">
+    <h1>SDK 安全加固</h1>
+  </div>
+</template>
+
+<script setup>
+useHead(() => ({
+  title: 'SDK安全加固 - 移动端',
+  meta: [
+    { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }
+  ]
+}))
+</script>
+
+<style scoped lang="scss">
+.mobile-product {
+  color: #fff;
+  text-align: center;
+  padding-top: 60px;
+
+  h1 {
+    font-size: 24px;
+  }
+}
+</style>

+ 26 - 0
app/pages/mobile/products/web.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="mobile-product">
+    <h1>Web 安全加速</h1>
+  </div>
+</template>
+
+<script setup>
+useHead(() => ({
+  title: 'Web安全加速 - 移动端',
+  meta: [
+    { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }
+  ]
+}))
+</script>
+
+<style scoped lang="scss">
+.mobile-product {
+  color: #fff;
+  text-align: center;
+  padding-top: 60px;
+
+  h1 {
+    font-size: 24px;
+  }
+}
+</style>

+ 4 - 0
app/pages/mobile/solutions.vue

@@ -0,0 +1,4 @@
+<template></template>
+<script setup>
+
+</script>

+ 28 - 1
nuxt.config.ts

@@ -5,6 +5,33 @@ export default defineNuxtConfig({
   // devtools: { enabled: true },
   css: ['~/assets/scss/main.scss'], // 全局样式文件
   plugins: ['~/plugins/index.js'],
+  app: {
+    head: {
+      meta: [
+        { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }
+      ]
+    }
+  },
+  postcss: {
+    plugins: {
+      'postcss-pxtorem': {
+        rootValue: 37.5,
+        unitPrecision: 4,
+        propList: ['*'],
+        selectorBlackList: [],
+        replace: true,
+        mediaQuery: false,
+        minRemValue: 0,
+        exclude: e => {
+          if (e.includes('/app/pages/mobile/') || e.includes('/app/components/mobile/') || e.includes('/app/layouts/mobile.vue')) {
+            return false
+          } else {
+            return true
+          }
+        }
+      }
+    }
+  },
   icon: {
     localApiEndpoint: '/nuxt-icon'
   },
@@ -24,7 +51,7 @@ export default defineNuxtConfig({
   routeRules: {
     '/web/**': { appLayout: 'web' },
     '/mobile/**': { appLayout: 'mobile' }
-  },
+  }
   // vite: {
   //   css: {
   //     preprocessorOptions: {

+ 2 - 0
package.json

@@ -23,6 +23,8 @@
   },
   "devDependencies": {
     "@types/node": "^25.0.7",
+    "postcss": "^8.5.6",
+    "postcss-pxtorem": "^6.1.0",
     "sass": "^1.97.2"
   }
 }

+ 15 - 0
pnpm-lock.yaml

@@ -39,6 +39,12 @@ importers:
       '@types/node':
         specifier: ^25.0.7
         version: 25.0.7
+      postcss:
+        specifier: ^8.5.6
+        version: 8.5.6
+      postcss-pxtorem:
+        specifier: ^6.1.0
+        version: 6.1.0(postcss@8.5.6)
       sass:
         specifier: ^1.97.2
         version: 1.97.2
@@ -2879,6 +2885,11 @@ packages:
     peerDependencies:
       postcss: ^8.4.32
 
+  postcss-pxtorem@6.1.0:
+    resolution: {integrity: sha512-ROODSNci9ADal3zUcPHOF/K83TiCgNSPXQFSbwyPHNV8ioHIE4SaC+FPOufd8jsr5jV2uIz29v1Uqy1c4ov42g==}
+    peerDependencies:
+      postcss: ^8.0.0
+
   postcss-reduce-initial@7.0.5:
     resolution: {integrity: sha512-RHagHLidG8hTZcnr4FpyMB2jtgd/OcyAazjMhoy5qmWJOx1uxKh4ntk0Pb46ajKM0rkf32lRH4C8c9qQiPR6IA==}
     engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
@@ -6704,6 +6715,10 @@ snapshots:
       postcss: 8.5.6
       postcss-value-parser: 4.2.0
 
+  postcss-pxtorem@6.1.0(postcss@8.5.6):
+    dependencies:
+      postcss: 8.5.6
+
   postcss-reduce-initial@7.0.5(postcss@8.5.6):
     dependencies:
       browserslist: 4.28.1