Ryuiso vor 7 Monaten
Ursprung
Commit
d5aeb73826

+ 11 - 0
web/package-lock.json

@@ -10,6 +10,7 @@
       "dependencies": {
         "@element-plus/icons-vue": "^2.3.1",
         "element-plus": "^2.9.8",
+        "emailjs-com": "^3.2.0",
         "pinia": "^3.0.1",
         "vue": "^3.5.13",
         "vue-router": "^4.5.0"
@@ -3157,6 +3158,16 @@
         "vue": "^3.2.0"
       }
     },
+    "node_modules/emailjs-com": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/emailjs-com/-/emailjs-com-3.2.0.tgz",
+      "integrity": "sha512-Prbz3E1usiAwGjMNYRv6EsJ5c373cX7/AGnZQwOfrpNJrygQJ15+E9OOq4pU8yC977Z5xMetRfc3WmDX6RcjAA==",
+      "deprecated": "The SDK name changed to @emailjs/browser",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/entities": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",

+ 1 - 0
web/package.json

@@ -15,6 +15,7 @@
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
     "element-plus": "^2.9.8",
+    "emailjs-com": "^3.2.0",
     "pinia": "^3.0.1",
     "vue": "^3.5.13",
     "vue-router": "^4.5.0"

+ 0 - 0
web/public/downloads/CMSServerV6-WIN-7.34.0.5-20250102.zip


+ 0 - 0
web/public/downloads/CMSV6-WIN-7.34.0.5-20250102.zip


BIN
web/public/downloads/IPVSP远程专家协同系统技术说明书23_3_0.docx


+ 34 - 8
web/src/api/contact.ts

@@ -1,18 +1,44 @@
 
 import type { ContactFormData } from '@/types/contact'
+import emailjs from 'emailjs-com'
+
+// 初始化emailjs - 使用正确的Public Key
+emailjs.init('yArFh9jidEKHVAAEc') // 必须使用EmailJS仪表盘中的Public Key
 
 interface ContactFormResponse {
   success: boolean
+  message?: string
 }
 
 export const submitContactForm = async (data: ContactFormData): Promise<ContactFormResponse> => {
-  // 实际项目中这里调用邮件服务API
-  console.log('Form submitted:', data)
+  try {
+    const templateParams = {
+      to_email: 'inoricyan@gmail.com', // 确保收件人地址不为空
+      reply_to: data.email, // 添加回复地址
+      from_name: data.name,
+      from_email: data.email,
+      company: data.company,
+      phone: data.tel,
+      industry: data.industry,
+      message: data.message
+    }
+
+    const response = await emailjs.send(
+      'service_4hlxrxg', // 替换为实际的服务ID
+      'template_hgveqto', // 替换为实际的模板ID
+      // 'ryuiso@qq.com', // 替换为实际的用户邮箱地址
+      templateParams
+    )
 
-  // 模拟API请求延迟
-  return new Promise((resolve) => {
-    setTimeout(() => {
-      resolve({ success: true })
-    }, 1000)
-  })
+    return {
+      success: response.status === 200,
+      message: '邮件发送成功!我们将尽快与您联系。'
+    }
+  } catch (error) {
+    console.error('邮件发送失败:', error)
+    return {
+      success: false,
+      message: error instanceof Error ? error.message : '邮件发送失败,请稍后再试'
+    }
+  }
 }

Datei-Diff unterdrückt, da er zu groß ist
+ 5 - 0
web/src/assets/icons/android.svg


+ 12 - 0
web/src/assets/icons/document.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="41px" height="48px" viewBox="0 0 41 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>document</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="document" fill="#1296DB" fill-rule="nonzero">
+            <g transform="translate(0.400000, 0.000000)">
+                <polygon id="路径" points="28.8888889 0 28.8888889 12 40 12"></polygon>
+                <path d="M26.6666667,0 L0,0 L0,48 L40,48 L40,14.4 L26.6666667,14.4 L26.6666667,0 Z M28.8888889,24 L24.4444444,40.8 L20,40.8 L17.7777778,32.4 L15.5555556,40.8 L11.1111111,40.8 L6.66666667,24 L11.1111111,24 L13.3333333,32.4 L15.5555556,24 L20,24 L22.2222222,32.4 L24.4444444,24 L28.8888889,24 Z" id="形状"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 9 - 0
web/src/assets/icons/excel.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="41px" height="48px" viewBox="0 0 41 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>excel</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="excel" transform="translate(0.500000, 0.000000)" fill="#1296DB" fill-rule="nonzero">
+            <path d="M39.4659091,12.0375 C39.8068182,12.3589286 40,12.7928571 40,13.2482143 L40,46.2857143 C40,47.2339286 39.1875,48 38.1818182,48 L1.81818182,48 C0.8125,48 0,47.2339286 0,46.2857143 L0,1.71428571 C0,0.766071429 0.8125,0 1.81818182,0 L25.9488636,0 C26.4318182,0 26.8977273,0.182142857 27.2386364,0.503571429 L39.4659091,12.0375 Z M35.8068182,14.0357143 L25.1136364,3.95357143 L25.1136364,14.0357143 L35.8068182,14.0357143 Z M23.5989205,22.1699464 L20.1207386,27.6506786 L16.6071591,22.1675893 C16.4837113,21.9749213 16.2623751,21.8571429 16.0236364,21.8571429 L13.8397159,21.8571429 C13.7108797,21.8571429 13.5846804,21.8915625 13.4757386,21.9564107 C13.1573295,22.1459464 13.0621591,22.5429643 13.2631818,22.8431786 L17.9411932,29.8300714 L13.1997727,36.9436607 C13.1314704,37.0461271 13.0952273,37.164691 13.0952273,37.2857143 C13.0952273,37.6407321 13.4005114,37.9285714 13.7770455,37.9285714 L15.7353409,37.9285714 C15.9716564,37.9285714 16.1911177,37.8131867 16.3153409,37.6236429 L19.8782386,32.1875357 L23.4174432,37.6220893 C23.5414783,37.812553 23.7615321,37.9285714 23.9985795,37.9285714 L26.1284659,37.9285714 C26.259091,37.9285714 26.3869649,37.8931795 26.496875,37.826625 C26.81375,37.6348393 26.9056818,37.237125 26.7022159,36.9384107 L21.9390909,29.9446071 L26.7854545,22.848 C26.8563082,22.7442269 26.8939773,22.623417 26.8939773,22.5 C26.8939773,22.1449821 26.58875,21.8571429 26.2121591,21.8571429 L24.1840341,21.8571429 C23.9442446,21.8571429 23.7220892,21.975932 23.5989773,22.1699464 L23.5989205,22.1699464 Z" id="形状"></path>
+        </g>
+    </g>
+</svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 5 - 0
web/src/assets/icons/linux.svg


Datei-Diff unterdrückt, da er zu groß ist
+ 5 - 0
web/src/assets/icons/sdk.svg


+ 1 - 0
web/src/assets/icons/windows.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745973561262" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7857" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M523.8 191.4v288.9h382V128.1zM523.8 833.6l382 62.2v-352h-382zM120.1 480.2H443V201.9l-322.9 53.5zM120.1 770.6L443 823.2V543.8H120.1z" p-id="7858" fill="#1296db"></path></svg>

BIN
web/src/assets/img/architecture_4800.png


BIN
web/src/assets/img/earth_2400.jpg


BIN
web/src/assets/img/topology_2400.png


+ 19 - 7
web/src/assets/styles/README.md

@@ -14,12 +14,24 @@ styles/
 
 ## 颜色系统
 
-### 主要颜色
-- `$primary-color: #0070D5` - 主题色,用于重要按钮、链接等
-- `$secondary-color: #0066ff` - 次要主题色
-- `$success-color: #31D158` - 成功状态
-- `$warning-color: #faad14` - 警告状态
-- `$error-color: #f5222d` - 错误状态
+### 颜色体系 (Element UI风格)
+
+#### 主题色
+- `$color-primary: #0070D5` - 主色
+- `$color-primary-light-1` 到 `$color-primary-light-9` - 主色渐变色
+- `$color-success: #31D158` - 成功色
+- `$color-warning: #faad14` - 警告色
+- `$color-danger: #f5222d` - 危险色
+- `$color-info: #909399` - 信息色
+
+#### 兼容变量
+保留了旧变量名以兼容现有代码:
+```scss
+$primary-color: $color-primary;
+$success-color: $color-success;
+$warning-color: $color-warning;
+$error-color: $color-danger;
+```
 
 ### 文本颜色
 - `$text-color: #000000` - 主要文本色
@@ -238,4 +250,4 @@ $breakpoint-xl: 1200px    // 超大屏幕
 3. **如何处理特定组件的样式?**
    - 使用 scoped 样式
    - 遵循 BEM 命名规范
-   - 复用现有的变量和 mixins
+   - 复用现有的变量和 mixins

+ 26 - 0
web/src/assets/styles/mixins.scss

@@ -7,6 +7,17 @@
   justify-content: center;
 }
 
+@mixin flex-column-center {
+  @include flex-center;
+  flex-direction: column;
+}
+
+@mixin flex-center {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
 @mixin flex-between {
   display: flex;
   align-items: center;
@@ -72,6 +83,21 @@
   }
 }
 
+// Layout mixins
+@mixin section-layout {
+  position: relative;
+
+  &.centered {
+    text-align: center;
+  }
+}
+
+@mixin content-container {
+  max-width: v.$container-width;
+  margin: 0 auto;
+  padding: 0 v.$spacing-md;
+}
+
 // Common patterns
 @mixin card {
   background: v.$background-primary;

+ 1 - 1
web/src/assets/styles/variables.scss

@@ -40,7 +40,7 @@ $spacing-xxl: 40px;
 $spacing-xxxl: 60px;
 
 // Layout
-$header-height: 64px;
+$header-height: 60px;
 :root {
   --header-height: #{$header-height};
 }

+ 71 - 35
web/src/components/ContactForm/index.vue

@@ -44,39 +44,83 @@ const validateForm = () => {
   if (!formData.value.company) errors.value.company = '请输入公司信息'
   if (!formData.value.industry) errors.value.industry = '请选择行业'
   if (!formData.value.email) errors.value.email = '请输入邮箱'
-  if (!formData.value.terms) errors.value.terms = '请接受条款'
 
   return Object.keys(errors.value).length === 0
 }
 
 const submitForm = async () => {
-  if (!validateForm()) return
+  if (!validateForm()) {
+    console.warn('表单验证未通过:', errors.value)
+    return
+  }
+
+  // 调试日志:表单数据
+  console.group('表单提交调试信息')
+  console.log('表单数据:', JSON.stringify(formData.value, null, 2))
+  console.log('EmailJS配置:', {
+    serviceID: 'service_4hlxrxg',
+    templateID: 'template_ad5q0zo',
+    userID: 'ryuiso@qq.com'
+  })
 
   isSubmitting.value = true
 
   try {
     const result = await submitContactForm(formData.value)
+    console.log('API完整响应:', result)
 
     if (result.success) {
-      alert('提交成功!我们将尽快与您联系。')
+      alert(result.message || '提交成功!我们将尽快与您联系。')
       // 重置表单
       formData.value = {
         name: '',
         tel: '',
         company: '',
         industry: '',
-        city: '上海市 上海市',
+        city: '',
         email: '',
         message: '',
         subscribe: false,
         terms: false
       }
+    } else {
+      console.error('表单提交失败 - 详细错误:', {
+        message: result.message,
+        stack: new Error().stack
+      })
+      alert(result.message || '提交失败,请检查控制台日志')
     }
   } catch (error) {
-    alert('提交失败,请稍后再试')
-    console.error(error)
+    console.group('提交异常详情')
+    console.error('错误对象:', error)
+    console.log('错误类型:', typeof error)
+    console.log('是否是Error实例:', error instanceof Error)
+
+    // 增强错误信息捕获
+    if (error instanceof Error) {
+      console.log('错误名称:', error.name)
+      console.log('错误消息:', error.message)
+      console.log('错误堆栈:', error.stack)
+
+      // 检查是否是网络错误
+      if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
+        console.warn('可能是网络连接问题')
+      }
+
+      // 检查是否是EmailJS配置错误
+      if (error.message.includes('EmailJS')) {
+        console.warn('请检查EmailJS配置是否正确')
+      }
+    }
+
+    console.groupEnd()
+
+    alert(`提交失败: ${error instanceof Error ?
+      `${error.message} (请查看控制台获取详细信息)` :
+      '未知错误,请查看控制台日志'}`)
   } finally {
     isSubmitting.value = false
+    console.groupEnd()
   }
 }
 </script>
@@ -186,15 +230,6 @@ const submitForm = async () => {
             >
             <label for="subscribe">订阅产品资讯</label>
           </div>
-          <!--<div class="checkbox-item">-->
-          <!--  <input-->
-          <!--    type="checkbox"-->
-          <!--    id="terms"-->
-          <!--    v-model="formData.terms"-->
-          <!--    required-->
-          <!--  >-->
-          <!--  <label for="terms">我已阅读并同意<a href="#">用户协议</a></label>-->
-          <!--</div>-->
         </div>
 
         <div class="form-group">
@@ -209,9 +244,11 @@ const submitForm = async () => {
 </template>
 
 <style scoped lang="scss">
+@use '@/assets/styles/variables' as v;
+@use "sass:color";
 .contact-section {
-  padding: 64px 0;
-  background-color: #f9fafb;
+  padding: v.$spacing-xl 0;
+  background-color: v.$background-primary;
 
   .text-box {
     text-align: center;
@@ -223,13 +260,13 @@ const submitForm = async () => {
     .title {
       font-size: 32px;
       margin-bottom: 16px;
-      color: #1f2937;
+      color: v.$text-primary;
       font-weight: 600;
     }
 
     .desc {
       font-size: 16px;
-      color: #6b7280;
+      color: v.$text-secondary;
       line-height: 1.6;
     }
   }
@@ -259,37 +296,36 @@ const submitForm = async () => {
 
         &.has-error {
           input, select, textarea {
-            border-color: #ef4444;
+            border-color: v.$error-color;
           }
         }
 
         input, select, textarea {
           width: 100%;
           padding: 12px 16px;
-          border: 1px solid #dcdfe6;
+          border: 1px solid v.$border-color;
           border-radius: 4px;
           font-size: 14px;
           transition: border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
 
           &:focus {
             outline: none;
-            border-color: #409eff;
-            box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+            border-color: v.$primary-color;
+            box-shadow: 0 0 0 2px rgba(v.$primary-color, 0.2);
           }
         }
 
         input[type="email"] {
           &:hover {
-            border-color: #c0c4cc;
+            border-color: color.adjust(v.$border-color, $lightness: -10%);
           }
           &:focus {
-            border-color: #409eff;
+            border-color: v.$primary-color;
           }
         }
 
         select {
           appearance: none;
-          background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
           background-repeat: no-repeat;
           background-position: right 12px center;
           background-size: 16px;
@@ -304,7 +340,7 @@ const submitForm = async () => {
           position: absolute;
           left: 16px;
           top: 12px;
-          color: #9ca3af;
+          color: v.$text-light;
           font-size: 14px;
           transition: all 0.2s;
           pointer-events: none;
@@ -321,7 +357,7 @@ const submitForm = async () => {
           top: -8px;
           left: 12px;
           font-size: 12px;
-          color: #2563eb;
+            color: v.$primary-color;
           background: white;
         }
       }
@@ -341,11 +377,11 @@ const submitForm = async () => {
 
           label {
             position: static;
-            color: #4b5563;
+            color: v.$text-secondary;
             font-size: 14px;
 
             a {
-              color: #2563eb;
+              color: v.$primary-color;
               text-decoration: none;
 
               &:hover {
@@ -355,13 +391,13 @@ const submitForm = async () => {
           }
 
           &.has-error label {
-            color: #ef4444;
+            color: v.$error-color;
           }
         }
       }
 
       .error-message {
-        color: #ef4444;
+        color: v.$error-color;
         font-size: 12px;
         margin-top: 4px;
       }
@@ -369,7 +405,7 @@ const submitForm = async () => {
 
     .submit-btn {
       width: 100%;
-      background: #2563eb;
+            background: v.$primary-color;
       color: white;
       padding: 12px;
       border: none;
@@ -380,11 +416,11 @@ const submitForm = async () => {
       transition: background-color 0.2s;
 
       &:hover {
-        background: #1d4ed8;
+        background: color.adjust(v.$primary-color, $lightness: -10%);
       }
 
       &:disabled {
-        background: #93c5fd;
+        background: v.$background-gray;
         cursor: not-allowed;
       }
     }

+ 0 - 174
web/src/components/DocLayout/index.vue

@@ -1,174 +0,0 @@
-
-<script setup lang="ts">
-import { ref, computed } from 'vue'
-
-defineProps<{
-  menus: Array<{
-    title: string
-    path?: string
-    children?: Array<{
-      title: string
-      path: string
-    }>
-  }>
-}>()
-
-const currentPath = ref('')
-const sidebarOpen = ref(true)
-
-// 响应式处理
-const isMobile = ref(false)
-const checkIsMobile = () => {
-  isMobile.value = window.innerWidth < 768
-  if (isMobile.value) sidebarOpen.value = false
-}
-
-// 初始化检查
-if (typeof window !== 'undefined') {
-  checkIsMobile()
-  window.addEventListener('resize', checkIsMobile)
-}
-</script>
-
-<template>
-  <div class="doc-container">
-    <!-- 移动端菜单按钮 -->
-    <button
-      class="mobile-menu-button"
-      @click="sidebarOpen = !sidebarOpen"
-      v-if="isMobile"
-    >
-      <svg viewBox="0 0 24 24" width="24" height="24">
-        <path fill="currentColor" d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
-      </svg>
-    </button>
-
-    <!-- 侧边栏导航 -->
-    <aside
-      class="sidebar"
-      :class="{ 'sidebar-open': sidebarOpen }"
-    >
-      <nav>
-        <div v-for="group in menus" :key="group.title">
-          <h3 class="menu-group-title">{{ group.title }}</h3>
-          <ul v-if="group.children">
-            <li
-              v-for="item in group.children"
-              :key="item.path"
-              class="menu-item"
-            >
-              <RouterLink
-                :to="item.path"
-                class="menu-link"
-                :class="{ active: currentPath === item.path }"
-                @click="currentPath = item.path"
-              >
-                {{ item.title }}
-              </RouterLink>
-            </li>
-          </ul>
-        </div>
-      </nav>
-    </aside>
-
-    <!-- 主内容区 -->
-    <main class="content">
-      <div class="content-container">
-        <slot />
-      </div>
-    </main>
-  </div>
-</template>
-
-<style scoped>
-.doc-container {
-  display: flex;
-  min-height: calc(100vh - var(--header-height));
-  position: relative;
-}
-
-.sidebar {
-  width: 280px;
-  border-right: 1px solid var(--c-divider);
-  background-color: var(--c-bg);
-  position: fixed;
-  top: var(--header-height);
-  bottom: 0;
-  left: 0;
-  z-index: 10;
-  transition: transform 0.2s ease;
-  overflow-y: auto;
-  padding: 24px 0;
-}
-
-.content {
-  flex: 1;
-  padding: 32px 48px;
-  margin-left: 280px;
-}
-
-.menu-group-title {
-  padding: 8px 24px;
-  font-size: 14px;
-  font-weight: 600;
-  color: var(--c-text-2);
-  text-transform: uppercase;
-  letter-spacing: 0.5px;
-}
-
-.menu-item {
-  margin: 4px 0;
-}
-
-.menu-link {
-  display: block;
-  padding: 8px 24px;
-  color: var(--c-text-1);
-  font-size: 14px;
-  transition: color 0.2s, background-color 0.2s;
-}
-
-.menu-link:hover {
-  color: var(--c-brand);
-  background-color: var(--c-bg-light);
-}
-
-.menu-link.active {
-  color: var(--c-brand);
-  border-right: 2px solid var(--c-brand);
-  background-color: var(--c-bg-light);
-}
-
-/* 移动端样式 */
-@media (max-width: 768px) {
-  .sidebar {
-    transform: translateX(-100%);
-  }
-
-  .sidebar.sidebar-open {
-    transform: translateX(0);
-  }
-
-  .content {
-    margin-left: 0;
-    padding: 24px;
-  }
-
-  .mobile-menu-button {
-    position: fixed;
-    bottom: 20px;
-    right: 20px;
-    z-index: 20;
-    background: var(--c-brand);
-    color: white;
-    border: none;
-    border-radius: 50%;
-    width: 50px;
-    height: 50px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
-  }
-}
-</style>

+ 0 - 111
web/src/components/DocNavigation/index.vue

@@ -1,111 +0,0 @@
-
-<script setup lang="ts">
-import { ref } from 'vue'
-
-defineProps<{
-  menus: Array<{
-    title: string
-    path?: string
-    children?: Array<{
-      title: string
-      path: string
-      children?: Array<{
-        title: string
-        path: string
-      }>
-    }>
-  }>
-}>()
-
-const activePath = ref('')
-</script>
-
-<template>
-  <nav class="doc-navigation">
-    <ul class="first-level">
-      <li v-for="item in menus" :key="item.title">
-        <RouterLink
-          v-if="item.path"
-          :to="item.path"
-          class="nav-link"
-          :class="{ active: activePath === item.path }"
-        >
-          {{ item.title }}
-        </RouterLink>
-        <div v-else class="nav-title">{{ item.title }}</div>
-
-        <ul v-if="item.children" class="second-level">
-          <li v-for="child in item.children" :key="child.title">
-            <RouterLink
-              v-if="child.path"
-              :to="child.path"
-              class="nav-link"
-              :class="{ active: activePath === child.path }"
-            >
-              {{ child.title }}
-            </RouterLink>
-            <div v-else class="nav-title">{{ child.title }}</div>
-
-            <ul v-if="child.children" class="third-level">
-              <li v-for="sub in child.children" :key="sub.title">
-                <RouterLink
-                  :to="sub.path"
-                  class="nav-link"
-                  :class="{ active: activePath === sub.path }"
-                >
-                  {{ sub.title }}
-                </RouterLink>
-              </li>
-            </ul>
-          </li>
-        </ul>
-      </li>
-    </ul>
-  </nav>
-</template>
-
-<style scoped>
-.doc-navigation {
-  padding: 16px 0;
-}
-
-.first-level > li,
-.second-level > li,
-.third-level > li {
-  margin: 4px 0;
-}
-
-.nav-title {
-  font-weight: 600;
-  color: var(--c-text-1);
-  padding: 8px 16px;
-  margin-top: 12px;
-}
-
-.nav-link {
-  display: block;
-  padding: 8px 16px;
-  color: var(--c-text-2);
-  border-radius: 4px;
-  transition: all 0.2s;
-}
-
-.nav-link:hover {
-  color: var(--c-brand);
-  background: var(--c-bg-light);
-}
-
-.nav-link.active {
-  color: var(--c-brand);
-  background: var(--c-bg-light);
-  font-weight: 500;
-}
-
-.second-level {
-  padding-left: 8px;
-}
-
-.third-level {
-  padding-left: 16px;
-}
-</style>

+ 0 - 42
web/src/components/SceneCard/index.vue

@@ -1,42 +0,0 @@
-<script setup lang="ts">
-import BaseCard from '@/components/BaseCard/index.vue'
-
-defineProps({
-  title: {
-    type: String,
-    required: true
-  },
-  image: {
-    type: String,
-    default: ''
-  },
-  description: {
-    type: String,
-    default: ''
-  }
-})
-</script>
-
-<template>
-  <BaseCard
-    :title="title"
-    :image="image"
-    :description="description"
-    class="scene-card"
-  />
-</template>
-
-<style lang="scss" scoped>
-.scene-card {
-  :deep(.card-content) {
-    h3 {
-      font-size: 20px;
-      margin-bottom: 8px;
-    }
-    p {
-      font-size: 14px;
-      line-height: 1.4;
-    }
-  }
-}
-</style>

+ 0 - 128
web/src/components/Section/index.vue

@@ -1,128 +0,0 @@
-<script setup lang="ts">
-defineProps({
-  title: {
-    type: String,
-    default: ''
-  },
-  subtitle: {
-    type: String,
-    default: ''
-  },
-  dark: {
-    type: Boolean,
-    default: false
-  },
-  bgImage: {
-    type: String,
-    default: ''
-  },
-  bgColor: {
-    type: String,
-    default: ''
-  },
-  heroSection: {
-    type: Boolean,
-    default: false
-  },
-  centered: {
-    type: Boolean,
-    default: false
-  }
-})
-</script>
-
-<template>
-  <section
-    class="section"
-    :class="{
-      'dark': dark,
-      'hero-section': heroSection,
-      'centered': centered
-    }"
-    :style="{
-      backgroundImage: bgImage ? `url('${bgImage}')` : 'none',
-      backgroundColor: bgColor || ''
-    }"
-  >
-    <div v-if="title || subtitle" class="section-header">
-      <h2 v-if="title" class="headline">{{ title }}</h2>
-      <p v-if="subtitle" class="description">{{ subtitle }}</p>
-    </div>
-    <div class="section-content">
-      <slot></slot>
-    </div>
-  </section>
-</template>
-
-<style lang="scss" scoped>
-@use '@/assets/styles/variables' as v;
-@use '@/assets/styles/mixins' as m;
-
-.section {
-  padding: v.$spacing-xxxl 0;
-  position: relative;
-
-  &.dark {
-    color: v.$text-color-light;
-  }
-
-  &.hero-section {
-    background-size: cover;
-    background-position: center;
-    min-height: 400px;
-    @include m.flex-column;
-    justify-content: center;
-  }
-
-  &.centered {
-    text-align: center;
-
-    .section-content {
-      @include m.flex-column;
-      align-items: center;
-    }
-  }
-
-  .section-header {
-    @include m.flex-column;
-    align-items: center;
-    justify-content: center;
-    text-align: center;
-    max-width: v.$content-max-width;
-    margin: 0 auto v.$spacing-xl;
-    min-height: 100px; // 确保有足够的空间进行垂直居中
-
-    .headline {
-      font-size: v.$font-size-xxl;
-      margin-bottom: v.$spacing-md;
-      width: 100%;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-    }
-
-    .description {
-      font-size: v.$font-size-md;
-      line-height: 1.6;
-      width: 100%;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-    }
-  }
-
-  .section-content {
-    max-width: v.$container-width;
-    margin: 0 auto;
-    padding: 0 v.$spacing-md;
-    @include m.flex-column;
-    align-items: center;
-    justify-content: center;
-
-    // 如果section-content内部有需要占满宽度的元素,可以使用以下样式
-    :deep(> *) {
-      width: 100%;
-    }
-  }
-}
-</style>

+ 66 - 20
web/src/layout/Header/index.vue

@@ -33,10 +33,6 @@ defineProps({
     type: String,
     default: '',
   },
-  heroSection: {
-    type: Boolean,
-    default: false,
-  },
   centered: {
     type: Boolean,
     default: false,
@@ -45,7 +41,7 @@ defineProps({
 </script>
 
 <template>
-  <header class="new-header" :class="{ 'new-header-doc': isDocView }">
+  <header class="header" :class="{ 'header-doc': isDocView }">
     <div class="header-container">
       <!-- Logo 区域 -->
       <div class="logo-section">
@@ -88,7 +84,9 @@ defineProps({
       <!-- 右侧操作区 -->
       <div class="action-section">
         <RouterLink to="/contact" class="action-link">联系我们</RouterLink>
-        <a href="#" class="action-button">预览平台</a>
+        <a href="https://ipvsp.cn" class="action-button" target="_blank"
+                   rel="noopener noreferrer">预览平台
+        </a>
       </div>
     </div>
   </header>
@@ -99,8 +97,9 @@ defineProps({
 @use '@/assets/styles/mixins' as m;
 @use "sass:color";
 
-.new-header {
-  position: relative;
+.header {
+  position: sticky;
+  top: 0;
   width: 100%;
   background-color: v.$background-light;
   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
@@ -111,7 +110,7 @@ defineProps({
   @include m.container;
   @include m.flex-between;
   padding: 0 v.$spacing-lg;
-  height: 64px;
+  height: v.$header-height;
 }
 
 // Logo 区域样式
@@ -159,14 +158,36 @@ defineProps({
     padding: 0 v.$spacing-lg;
   }
 
-  .menu-link {
-    color: v.$text-primary;
-    text-decoration: none;
-    font-size: v.$font-size-sm;
-    transition: color v.$transition-fast;
+    .menu-link {
+      color: v.$text-primary;
+      text-decoration: none;
+      font-size: v.$font-size-sm;
+      transition: all v.$transition-fast; /* 修改为all以包含font-weight */
+      position: relative;
+      display: inline-block;
+
+      &:hover {
+        color: v.$primary-color;
+        font-weight: v.$font-weight-bold; /* 添加加粗效果 */
+
+        &::after {
+          width: 100%;
+        }
+      }
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: -20px; /* 调整到header底部 */
+      left: -16px;
+      width: 0;
+      height: 2px;
+      background-color: v.$primary-color;
+      transition: width 0.1s;
+    }
 
-    &:hover {
-      color: v.$primary-color;
+    &:hover::after {
+      width: calc(100% + 32px);
     }
   }
 }
@@ -182,9 +203,26 @@ defineProps({
     text-decoration: none;
     font-size: v.$font-size-sm;
     transition: color v.$transition-fast;
+    position: relative;
+    display: inline-block;
 
     &:hover {
       color: v.$primary-color;
+
+      &::after {
+        width: 100%;
+      }
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: -20px; /* 调整到header底部 */
+      left: 0;
+      width: 0;
+      height: 2px;
+      background-color: v.$primary-color;
+      transition: width 0.1s;
     }
   }
 
@@ -203,7 +241,7 @@ defineProps({
   }
 }
 
-.new-header-doc {
+.header-doc {
   .header-container {
     max-width: none;
     padding: 0 v.$spacing-xl;
@@ -256,7 +294,7 @@ defineProps({
     }
   }
 
-  .new-header-doc {
+  .header-doc {
     .header-container {
       padding: 0 v.$spacing-lg;
     }
@@ -279,10 +317,10 @@ defineProps({
 
   .nav-menu {
     position: fixed;
-    top: 64px;
+    top: 60px;
     left: 0;
     width: 100%;
-    height: calc(100vh - 64px);
+    height: calc(100vh - 60px);
     background: v.$background-light;
     flex-direction: column;
     padding: 2rem;
@@ -309,6 +347,14 @@ defineProps({
       font-size: 1rem;
       padding: 0.5rem 0;
       display: block;
+      transition: transform 0.2s ease;
+
+      &:hover {
+        transform: scale(1.05);
+        &::after {
+          display: none; /* 移除移动端下划线 */
+        }
+      }
     }
   }
 

+ 3 - 0
web/src/main.ts

@@ -3,6 +3,8 @@ import './assets/styles/global.scss'
 
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
 
 import App from './App.vue'
 import router from './router'
@@ -11,5 +13,6 @@ const app = createApp(App)
 
 app.use(createPinia())
 app.use(router)
+app.use(ElementPlus)
 
 app.mount('#app')

+ 7 - 7
web/src/router/index.ts

@@ -195,6 +195,13 @@ const routes: NavItem[] = [
     children: enterpriseRoutes,
     redirect: { path: '/enterprise/public-safety' },
   },
+  {
+    path: '/download',
+    name: 'download',
+    component: DownloadView,
+    title: '下载中心',
+    showInNav: true,
+  },
   {
     path: '/doc',
     name: 'doc',
@@ -205,13 +212,6 @@ const routes: NavItem[] = [
       hideFooter: true,
     }
   },
-  {
-    path: '/download',
-    name: 'download',
-    component: DownloadView,
-    title: '下载中心',
-    showInNav: true,
-  },
   {
     path: '/contact',
     name: 'contact',

+ 260 - 110
web/src/views/Download/index.vue

@@ -1,41 +1,144 @@
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { ref, onMounted, computed } from 'vue'
+import { Download, Search } from '@element-plus/icons-vue'
+import WindowsIcon from '/src/assets/icons/windows.svg'
+import LinuxIcon from '/src/assets/icons/linux.svg'
+import AndroidIcon from '/src/assets/icons/android.svg'
+import SdkIcon from '/src/assets/icons/sdk.svg'
+import DocumentIcon from '/src/assets/icons/document.svg'
+import ExcelIcon from '/src/assets/icons/excel.svg'
 
 // 下载分类
 const categories = ref([
-  { id: 1, name: 'Windows' },
-  { id: 50, name: '信创/Linux' },
-  { id: 56, name: 'CMSV7客户端' },
-  { id: 60, name: 'APP' },
-  { id: 28, name: '对接SDK' },
-  { id: 37, name: '说明书' },
-  { id: 43, name: '功能列表' },
-  { id: 3, name: '协议标准' }
+  { id: 0, name: '全部' },
+  { id: 1, name: 'Windows', icon: WindowsIcon },
+  { id: 2, name: '信创/Linux', icon: LinuxIcon },
+  { id: 3, name: '安卓APP', icon: AndroidIcon },
+  { id: 4, name: '对接SDK', icon: SdkIcon },
+  { id: 5, name: '说明书', icon: DocumentIcon },
+  { id: 6, name: '功能列表', icon: ExcelIcon },
 ])
 
+// 下载项类型
+interface DownloadItem {
+  id: number
+  title: string
+  description: string
+  version: string
+  file: string
+  category: number | number[] // 类别可以是单个数字或数组,用于支持多个分类
+  size?: string // 可选属性,动态添加
+}
+
 // 下载项数据
-const downloads = ref([
+const downloads = ref<DownloadItem[]>([
   {
     id: 1,
-    title: 'CMSserverV6服务器',
-    description: '服务器安装包,集公务车、渣土车、第三方、网约车、主动安全、校车六位一体的版本。',
-    version: '7.34.0.5_20250102',
-    size: '957.65 MB',
-    file: '/downloads/CMSServerV6-WIN-7.34.0.5-20250102.zip'
+    title: 'IPVSP-IPU-2.0',
+    description: 'IP视频服务平台安装包,支持多种视频协议和格式。',
+    version: '2.0.0',
+    file: '/downloads/IPVSP-IPU-2.0.apk',
+    category: 3 // 安卓APP
   },
   {
     id: 2,
-    title: 'CMSV6客户端(64位)',
-    description: '64位电脑客户端,车载平台和执法仪平台通用,适用于64位的Windows操作系统。',
-    version: '7.34.0.5_20250102',
-    size: '108.19 MB',
-    file: '/downloads/CMSV6-WIN-7.34.0.5-20250102.zip'
+    title: 'IPVSP安装包操作说明',
+    description: '详细说明IPVSP-IPU-2.0的安装和使用方法。',
+    version: '1.0.0',
+    file: '/downloads/IPVSP安装包操作说明.docx',
+    category: 5 // 说明书
+  },
+  {
+    id: 3,
+    title: 'IPVSP远程专家协通系统技术说明书',
+    description: '详细列出IPVSP-IPU-2.0的所有功能和特性。',
+    version: '23.3.0',
+    file: '/downloads/IPVSP远程专家协同系统技术说明书23_3_0.docx',
+    category: [ 5, 7 ]
+  },
+  {
+    id: 4,
+    title: 'AnLinkSetup',
+    description: 'AnLinkSetup安装包,用于安卓设备与PC的连接,支持WiFi和USB连接,多点触控,共享剪切板等。',
+    version: '1.0.0',
+    file: '/downloads/AnLinkSetup.zip',
+    category: 1 // Windows
   }
 ])
 
+
+// 获取文件大小(MB)
+const getFileSize = async (filePath: string) => {
+  try {
+    const response = await fetch(filePath)
+    const contentLength = response.headers.get('content-length')
+    if (contentLength) {
+      const sizeMB = (parseInt(contentLength) / (1024 * 1024)).toFixed(2)
+      return `${sizeMB} MB`
+    }
+    return '未知大小'
+  } catch (error) {
+    console.error('获取文件大小失败:', error)
+    return '未知大小'
+  }
+}
+
+// 初始化下载项数据
+const initDownloads = async () => {
+  for (const item of downloads.value) {
+    const size = await getFileSize(item.file)
+    // 使用类型断言确保类型安全
+    ;(item as DownloadItem).size = size
+  }
+}
+
+onMounted(() => {
+  initDownloads()
+})
+
+// 搜索关键词
+const searchKeyword = ref('')
 // 当前选中分类
-const activeCategory = ref(-1)
+const activeCategory = ref(0) // 默认选中"全部"
+
+// 处理搜索
+const handleSearch = () => {
+  // 搜索时保持当前分类状态
+}
+
+// 计算筛选后的下载项
+const filteredDownloads = computed<DownloadItem[]>(() => {
+  let result = downloads.value
+
+  // 分类筛选
+  if (activeCategory.value !== 0) {
+    result = result.filter(item =>
+      Array.isArray(item.category)
+        ? item.category.includes(activeCategory.value)
+        : item.category === activeCategory.value
+    )
+  }
+
+  // 模糊搜索
+  if (searchKeyword.value.trim()) {
+    const keyword = searchKeyword.value.toLowerCase()
+    result = result.filter(item =>
+      item.title.toLowerCase().includes(keyword) ||
+      item.description.toLowerCase().includes(keyword)
+    )
+  }
+
+  return result
+})
+
+// 获取分类图标
+const getCategoryIcon = (category: number | number[]) => {
+  if (Array.isArray(category)) {
+    return categories.value.find(c => c.id === category[0])?.icon;
+  }
+  return categories.value.find(c => c.id === category)?.icon;
+}
 
 // 处理下载
 const handleDownload = (filePath: string) => {
@@ -52,82 +155,114 @@ const handleDownload = (filePath: string) => {
   <div class="download-center">
     <!-- 搜索区域 -->
     <div class="search-box">
-      <input
-        type="text"
-        placeholder="请输入名称或内容"
-        class="search-input"
-      >
-      <button class="search-button">搜索</button>
+      <el-input
+        v-model="searchKeyword"
+        :prefix-icon="Search"
+        placeholder="请输入名称或描述内容搜索"
+        clearable
+        @keyup.enter="handleSearch"
+      />
     </div>
 
     <!-- 分类筛选 -->
     <div class="category-filter">
-      <div
+      <el-button
         v-for="category in categories"
         :key="category.id"
-        class="category-item"
-        :class="{ active: activeCategory === category.id }"
+        :type="activeCategory === category.id ? 'primary' : ''"
         @click="activeCategory = category.id"
+        size="small"
       >
+        <template v-if="category.icon">
+          <img :src="category.icon" class="category-icon" alt=""/>
+        </template>
         {{ category.name }}
-      </div>
+      </el-button>
     </div>
 
     <!-- 下载列表 -->
     <div class="download-list">
-      <div
-        v-for="item in downloads"
+      <el-card
+        v-for="item in filteredDownloads"
         :key="item.id"
-        class="download-item"
+        class="card"
+        shadow="hover"
       >
-        <div class="download-info">
-          <h3>{{ item.title }}</h3>
-          <p>{{ item.description }}</p>
-          <div class="download-meta">
-            <span>版本: {{ item.version }}</span>
-            <span>大小: {{ item.size }}</span>
+        <div class="card-bg-icon">
+          <template v-if="getCategoryIcon(item.category)">
+            <img
+              :src="getCategoryIcon(item.category)"
+              class="bg-icon-img"
+              alt=""
+            />
+          </template>
+          <template v-else>
+            <svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+              <path d="M50 0 L100 50 L50 100 L0 50 Z" fill="rgba(255,255,255,0.1)"/>
+            </svg>
+          </template>
+        </div>
+        <div class="card-content">
+          <div class="card-header">
+            <h3>{{ item.title }}</h3>
+
+          </div>
+          <p class="card-description">{{ item.description }}</p>
+          <div class="card-footer">
+            <div class="download-meta">
+              <span>版本: {{ item.version }}</span>
+              <span v-if="item.size">大小: {{ item.size }}</span>
+              <span v-else>正在获取文件大小...</span>
+              <el-button
+                type="primary"
+                :icon="Download"
+                @click="handleDownload(item.file)"
+                size="small"
+                style="margin-left: auto;"
+              />
+            </div>
           </div>
         </div>
-        <button
-          class="download-button"
-          @click="handleDownload(item.file)"
-        >
-          下载
-        </button>
-      </div>
+      </el-card>
     </div>
   </div>
 </template>
 
-<style scoped>
+<style lang="scss" scoped>
 .download-center {
   max-width: 1200px;
   margin: 0 auto;
   padding: 2rem;
 }
 
-.search-box {
-  display: flex;
-  margin-bottom: 2rem;
-  gap: 1rem;
+.category-icon {
+  width: 16px;
+  height: 16px;
+  margin-right: 6px;
+  vertical-align: middle;
+
 }
 
-.search-input {
-  flex: 1;
-  padding: 0.75rem 1rem;
-  border: 1px solid #ddd;
-  border-radius: 4px;
-  font-size: 1rem;
+.bg-icon-img {
+  width: 100%;
+  height: 100%;
+  opacity: 0.5;
+  object-fit: contain;
+  transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
 }
 
-.search-button {
-  padding: 0 2rem;
-  background: #3270FF;
-  color: white;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
-  font-size: 1rem;
+.card:hover .bg-icon-img {
+  transform: scale(1.2) rotate(5deg);
+  opacity: 0.7;
+}
+
+.search-box {
+  margin-bottom: 2rem;
+
+  :deep(.el-input__wrapper) {
+    height: 48px;
+    padding: 0 16px;
+  }
 }
 
 .category-filter {
@@ -137,57 +272,72 @@ const handleDownload = (filePath: string) => {
   margin-bottom: 2rem;
 }
 
-.category-item {
-  padding: 0.5rem 1rem;
-  background: #f5f5f5;
-  border-radius: 4px;
-  cursor: pointer;
-}
-
-.category-item.active {
-  background: #3270FF;
-  color: white;
-}
-
 .download-list {
   display: grid;
   gap: 1.5rem;
-}
 
-.download-item {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 1.5rem;
-  background: white;
-  border-radius: 8px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
-}
+  .card {
+    margin-bottom: 1rem;
+    position: relative;
+    background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(247,250,255,0.9) 100%);
+    overflow: hidden;
 
-.download-info h3 {
-  margin: 0 0 0.5rem;
-  font-size: 1.25rem;
-}
+    :deep(.el-card__body) {
+      padding: 1.5rem;
+      position: relative;
+      z-index: 1;
+    }
 
-.download-info p {
-  margin: 0 0 1rem;
-  color: #666;
-}
+    .card-bg-icon {
+      position: absolute;
+      top: 0;
+      right: 0;
+      opacity: 0.3;
+      z-index: 0;
+    }
 
-.download-meta {
-  display: flex;
-  gap: 1rem;
-  color: #888;
-  font-size: 0.875rem;
-}
+    .card-content {
+      display: flex;
+      flex-direction: column;
+      gap: 1rem;
+    }
+
+    .card-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
 
-.download-button {
-  padding: 0.5rem 1.5rem;
-  background: #3270FF;
-  color: white;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
-  font-size: 1rem;
+      h3 {
+        margin: 0;
+        font-size: 1.25rem;
+        color: var(--el-text-color-primary);
+      }
+    }
+
+    .card-description {
+      margin: 0;
+      color: var(--el-text-color-secondary);
+      font-size: 0.95rem;
+      line-height: 1.5;
+    }
+
+    .download-meta {
+      display: flex;
+      align-items: center;
+      gap: 1rem;
+      font-size: 1rem;
+      width: 100%;
+
+      > :last-child {
+        margin-left: auto;
+      }
+
+      span {
+        color: var(--el-text-color-regular);
+      }
+    }
+
+  }
 }
+
 </style>

+ 115 - 72
web/src/views/Home/index.vue

@@ -1,96 +1,139 @@
 <script setup lang="ts">
-import { ref } from 'vue'
-import Section from '@/components/Section/index.vue'
-import ProductCard from '@/components/ProductCard/index.vue'
-
-const supportedProducts = ref([
-  { name: 'DJI Dock 3', image: '//devcn.djicdn.com/img/74c76dc.png' },
-  { name: 'Matrice 4D/4TD', image: '//devcn.djicdn.com/img/df96c61.png' },
-  { name: 'DJI Dock 2', image: '//devcn.djicdn.com/img/85b4ea2.png' },
-  { name: 'Matrice 3D/3TD', image: '//devcn.djicdn.com/img/e5bc389.png' },
-  { name: 'DJI Dock', image: '//devcn.djicdn.com/img/ae989b7.png' },
-  { name: 'Matrice 4E/4T', image: '//devcn.djicdn.com/img/6d906ff.png' },
-  { name: 'Matrice 300 RTK', image: '//devcn.djicdn.com/img/fb310a2.png' },
-  { name: 'Matrice 30 Series', image: '//devcn.djicdn.com/img/d1a58e8.png' },
-  { name: 'DJI Mavic 3 Enterprise Series', image: '//devcn.djicdn.com/img/0d698cc.png' },
-  { name: 'Matrice 350 RTK', image: '//devcn.djicdn.com/img/e75bd33.jpg' },
-])
+import { ref, defineAsyncComponent } from 'vue'
+import homeBgImage from '@/assets/img/earth_2400.jpg'
+
+const ContactForm = defineAsyncComponent(() => import('@/components/ContactForm/index.vue'))
+
 </script>
 
 <template>
-  <div class="api-overview-page">
+  <div class="overview-page">
     <!-- 头部区域 -->
-    <Section
-      heroSection
-      dark
-      centered
-      bgImage="//devcn.djicdn.com/img/6997ab1.png"
-      title="低门槛接入本地化部署的流媒体平台"
-      subtitle="无需重复开发APP,即可让您的设备快速接入安全自主的流媒体平台"
-    />
+    <section
+      class="section section--hero dark centered"
+      :style="{ backgroundImage: `url('${homeBgImage}')` }"
+    >
+      <div class="section-header">
+        <h2 class="headline">新一代流媒体服务提供商</h2>
+        <br>
+        <h3>低门槛接入本地化部署的流媒体平台,无需重复开发APP,即可让您的设备快速接入安全自主的流媒体平台</h3>
+      </div>
+      <div class="section-content"></div>
+    </section>
 
     <!-- 核心理念 -->
-    <Section
-      title="核心理念"
-      subtitle="基于业界标准的MQTT、HTTPS、Websocket协议,对直播业务功能集充分抽象,隔离了复杂的硬件底层业务逻辑,让开发者聚焦在场景业务实现上。同时,可以连接到任意网络,只要您的设备可以访问三方云平台即可工作"
-    >
-      <img src="//devcn.djicdn.com/img/046ccbe.png" class="section-img" alt="Core Concept" />
-    </Section>
+    <section class="section">
+      <div class="section-header">
+        <h2 class="headline">核心理念</h2>
+        <p class="description">基于业界标准的WebSocket、RTSP、RTMP、HTTP_FLV
+          协议,对直播业务功能集充分抽象,隔离了复杂的硬件底层业务逻辑,让开发者聚焦在场景业务实现上。同时,基于OIS模块,提供了GB2818等协议的支持,可以连接到任意网络,只要您的设备可以访问三方云平台即可工作</p>
+      </div>
+      <div class="section-content">
+        <img src="@/assets/img/topology_2400.png" class="section-img" alt="System Topology" />
+      </div>
+    </section>
 
     <!-- 方案架构 -->
-    <Section title="方案架构">
-      <img
-        src="//devcn.djicdn.com/img/baca5bf.jpg"
-        class="section-img"
-        alt="Solution Architecture"
-      />
-    </Section>
-
-    <!-- 支持的产品 -->
-    <Section title="支持的产品">
-      <div class="supported-products">
-        <ProductCard
-          v-for="product in supportedProducts"
-          :key="product.name"
-          :title="product.name"
-          :image="product.image"
-          class="product-item"
-        />
+    <section class="section">
+      <div class="section-header">
+        <h2 class="headline">方案架构</h2>
+      </div>
+      <div class="section-content">
+        <img src="@/assets/img/architecture_4800.png" class="section-img" alt="Solution Architecture" />
+      </div>
+    </section>
+
+    <!-- 联系我们 -->
+    <section class="section">
+
+      <div class="section-content">
+        <ContactForm />
       </div>
-    </Section>
+    </section>
   </div>
 </template>
 
 <style lang="scss" scoped>
-.api-overview-page {
-  .section-img {
-    max-width: 100%;
-    height: auto;
-  }
+@use '@/assets/styles/variables' as v;
+@use '@/assets/styles/mixins' as m;
 
-  .supported-products {
-    display: grid;
-    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
-    gap: 20px;
+.overview-page {
+  .section {
+    @include m.section-layout;
+    margin-bottom: v.$spacing-xl;
 
-    .product-item {
-      :deep(.product-card) {
-        flex-direction: column;
-        text-align: center;
+    &:last-child {
+      margin-bottom: v.$spacing-xxxl;
+    }
 
-        img {
-          width: 100%;
-          height: auto;
-          margin-bottom: 10px;
-        }
+    &.dark {
+      color: v.$text-color-light !important;
+
+      .headline,
+      .description {
+        color: inherit;
+      }
+    }
 
-        .product-info {
-          h4 {
-            font-size: 0.9rem;
-          }
+    &.section--hero {
+      background-size: 120%;
+      background-position: center;
+      min-height: 400px;
+      @include m.flex-column-center;
+      animation: bgScale 20s infinite alternate ease-in-out;
+    }
+
+    @keyframes bgScale {
+      0% {
+        background-size: 120%;
+      }
+      100% {
+        background-size: 100%;
+      }
+    }
+
+    .section-header {
+      @include m.flex-column-center;
+      max-width: v.$content-max-width;
+      margin: 0 auto 0;
+      padding-bottom: v.$spacing-md ;
+
+      .headline {
+        font-size: v.$font-size-xxl;
+        margin: 0 auto 0;
+        //padding-bottom: v.$spacing-md;
+        width: 100%;
+        @include m.flex-center;
+      }
+
+      .description {
+        font-size: v.$font-size-md;
+        color: v.$text-color-secondary;
+        line-height: 1.6;
+        width: 100%;
+        @include m.flex-center;
+
+        padding: 0 40px;
+        @include m.mobile {
+          padding: 0 16px;
         }
       }
     }
+
+    .section-content {
+      @include m.content-container;
+      @include m.flex-column-center;
+
+      :deep(> *) {
+        width: 100%;
+      }
+    }
+  }
+
+  .section-img {
+    max-width: 100%;
+    height: auto;
   }
+
 }
 </style>

+ 18 - 0
web/vite.config.ts

@@ -6,6 +6,24 @@ import vueDevTools from 'vite-plugin-vue-devtools'
 
 // https://vite.dev/config/
 export default defineConfig({
+  build: {
+    chunkSizeWarningLimit: 1000,
+    rollupOptions: {
+      output: {
+        manualChunks: (id) => {
+          if (id.includes('node_modules')) {
+            if (id.includes('element-plus')) {
+              return 'element-plus'
+            }
+            if (id.includes('vue')) {
+              return 'vue-libs'
+            }
+            return 'vendor'
+          }
+        }
+      }
+    }
+  },
   plugins: [
     vue(),
     vueDevTools(),

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.