Browse Source

新增邮箱界面,使用rspack替代vite

xwkjjl 5 days ago
parent
commit
2a5c651b7f

+ 3 - 0
.env.production

@@ -10,5 +10,8 @@ APP_DEFAULT_REQUEST_MODE=BOTH
 # 请求超时时间
 APP_REQUEST_TIMEOUT=20000
 
+# 本地API基础URL
+APP_BASE_SERVER_URL=http://127.0.0.1
+
 # 项目名称
 APP_NAME=移动警务

+ 2 - 0
.gitignore

@@ -22,3 +22,5 @@ dist-ssr
 *.njsproj
 *.sln
 *.sw?
+
+.vscode

+ 7 - 1
eslint.config.js

@@ -19,7 +19,13 @@ export default [
     },
     rules: {
       // 添加 prettier 规则
-      'prettier/prettier': 'error',
+      'prettier/prettier': [
+        'error',
+        {
+          singleAttributePerLine: false,
+          htmlWhitespaceSensitivity: 'ignore'
+        }
+      ],
       // 基础规则
       'no-console': ['warn', { allow: ['warn', 'error'] }], // 允许 console.warn 和 console.error
       semi: 'off', // 语句末尾分号

+ 2 - 2
index.html

@@ -4,10 +4,10 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
-    <title>%APP_NAME%</title>
+    <title><%= process.env.APP_NAME %></title>
   </head>
   <body>
     <div id="app"></div>
-    <script type="module" src="/src/main.js"></script>
+    <!-- <script type="module" src="/src/main.js"></script> -->
   </body>
 </html>

File diff suppressed because it is too large
+ 422 - 490
package-lock.json


+ 8 - 7
package.json

@@ -4,33 +4,34 @@
   "version": "0.0.0",
   "type": "module",
   "scripts": {
-    "dev": "vite",
-    "build": "vite build",
+    "dev": "rsbuild",
+    "build": "rsbuild build",
     "lint": "eslint . --ext .js,.vue,.mjs,.cjs ",
-    "preview": "vite preview"
+    "preview": "rsbuild preview"
   },
   "dependencies": {
     "animate.css": "^4.1.1",
     "axios": "^1.8.2",
     "pinia": "^3.0.1",
     "postcss-px-to-viewport-8-plugin": "^1.2.5",
-    "sass": "^1.85.1",
     "vant": "^4.9.17",
     "vue": "^3.5.13",
     "vue-router": "^4.5.0"
   },
   "devDependencies": {
     "@eslint/js": "^9.22.0",
+    "@rsbuild/core": "^1.2.19",
+    "@rsbuild/plugin-sass": "^1.2.2",
+    "@rsbuild/plugin-vue": "^1.0.7",
     "@vant/auto-import-resolver": "^1.3.0",
-    "@vitejs/plugin-vue": "^5.2.1",
     "eslint": "^9.22.0",
     "eslint-config-prettier": "^10.1.1",
     "eslint-plugin-prettier": "^5.2.3",
     "eslint-plugin-vue": "^10.0.0",
     "globals": "^16.0.0",
     "prettier": "3.5.3",
+    "sass": "^1.86.0",
     "unplugin-auto-import": "^19.1.1",
-    "unplugin-vue-components": "^28.4.1",
-    "vite": "^4.5.0"
+    "unplugin-vue-components": "^28.4.1"
   }
 }

+ 88 - 0
rsbuild.config.js

@@ -0,0 +1,88 @@
+import { defineConfig, loadEnv } from '@rsbuild/core'
+import { pluginVue } from '@rsbuild/plugin-vue'
+import { pluginSass } from '@rsbuild/plugin-sass'
+import AutoImport from 'unplugin-auto-import/rspack'
+import Components from 'unplugin-vue-components/rspack'
+import { VantResolver } from '@vant/auto-import-resolver'
+import postcssPluginPxToViewport from 'postcss-px-to-viewport-8-plugin'
+import path from 'path'
+import { fileURLToPath } from 'url'
+import { dirname } from 'path'
+
+const __filename = fileURLToPath(import.meta.url) // 获取当前文件绝对路径
+const __dirname = dirname(__filename) // 获取当前目录路径
+const { publicVars } = loadEnv({ prefixes: ['APP_'] })
+
+export default defineConfig(() => {
+  return {
+    plugins: [
+      pluginVue(),
+      pluginSass({
+        sassLoaderOptions: {
+          additionalData: `@use '@/assets/style/variables.scss' as *;`
+        }
+      })
+    ],
+    source: {
+      define: publicVars,
+      entry: {
+        index: './src/main.js'
+      },
+      alias: {
+        '@': path.resolve(__dirname, './src')
+      }
+    },
+    server: {
+      host: '0.0.0.0',
+      open: true,
+      proxy: {
+        [process.env.APP_BASE_API]: {
+          target: process.env.APP_BASE_SERVER_URL,
+          changeOrigin: true
+        }
+      }
+    },
+    tools: {
+      postcss: {
+        plugins: [
+          postcssPluginPxToViewport({
+            unitToConvert: 'px',
+            viewportWidth: 375,
+            unitPrecision: 6,
+            viewportUnit: 'vw',
+            fontViewportUnit: 'vw',
+            propList: ['*'],
+            selectorBlackList: ['van-'],
+            minPixelValue: 1,
+            mediaQuery: false,
+            replace: true,
+            exclude: [/node_modules/],
+            include: [],
+            landscape: false
+          })
+        ]
+      },
+      htmlPlugin: {
+        template: './index.html'
+      },
+      rspack: {
+        plugins: [
+          AutoImport({
+            resolvers: [VantResolver()]
+          }),
+          Components({
+            resolvers: [VantResolver()]
+          })
+        ]
+      }
+    },
+    output: {
+      cleanDistPath: true,
+      legalComments: 'none',
+      overrideBrowserslist: ['iOS >= 9', 'Android >= 4.4', 'last 2 versions', '> 0.2%', 'not dead']
+    },
+    performance: {
+      removeConsole: true
+    }
+  }
+})

+ 12 - 12
src/App.vue

@@ -1,9 +1,19 @@
 <script setup>
-  import { onBeforeUnmount } from 'vue'
+  import { onBeforeUnmount, onMounted } from 'vue'
   import { resetLocalServiceCheck } from './utils/request'
-  // 在页面关闭前重置本地服务检查
+  import { disableBackButton } from './utils/backButtonHandler'
+
+  let cleanup = null
+
+  onMounted(() => {
+    cleanup = disableBackButton()
+  })
+
   onBeforeUnmount(() => {
     resetLocalServiceCheck()
+    if (cleanup) {
+      cleanup()
+    }
   })
 </script>
 
@@ -28,14 +38,4 @@
   .animate__animated.animate__fadeOutRight {
     --animate-duration: 500ms;
   }
-
-  .top-icon {
-    width: 24px;
-    height: 24px;
-  }
-
-  .content-icon {
-    width: 16px;
-    height: 16px;
-  }
 </style>

BIN
src/assets/icons/btn_plus_black_24.png


BIN
src/assets/icons/icon_draft_black_16.png


BIN
src/assets/icons/icon_draft_gray_16.png


BIN
src/assets/icons/icon_mailbox_black_16.png


BIN
src/assets/icons/icon_mailbox_gray_16.png


BIN
src/assets/icons/icon_send_black_16.png


BIN
src/assets/icons/icon_send_gray_16.png


+ 12 - 0
src/assets/style/variables.scss

@@ -0,0 +1,12 @@
+$topIconWidthAndHeight: 24px;
+$contentIconWidthAndHeight: 16px;
+
+.top-icon {
+  width: $topIconWidthAndHeight;
+  height: $topIconWidthAndHeight;
+}
+
+.content-icon {
+  width: $contentIconWidthAndHeight;
+  height: $contentIconWidthAndHeight;
+}

+ 57 - 21
src/router/route.js

@@ -1,3 +1,43 @@
+// 预警圈路由
+const warningCircleRoutes = [
+  {
+    path: '/WarningCircle',
+    name: 'warningCircle',
+    component: () => import('@/views/WarningCircle.vue'),
+    meta: {
+      showFooter: true
+    }
+  },
+  {
+    path: '/WarningCircle/add',
+    name: 'AddCircle',
+    component: () => import('@/views/WarningCircle/AddCircle.vue'),
+    meta: {
+      showFooter: false
+    }
+  }
+]
+
+// 首页路由
+const homeRoutes = [
+  {
+    path: '/home',
+    name: 'Home',
+    component: () => import('@/views/Home.vue'),
+    meta: {
+      showFooter: true
+    }
+  },
+  {
+    path: '/email',
+    name: 'Email',
+    component: () => import('@/views/Home/Email/Email.vue'),
+    meta: {
+      showFooter: false
+    }
+  }
+]
+
 /**
  * 路由配置
  * @type {import('vue-router').RouteRecordRaw[]}
@@ -8,40 +48,36 @@ const routes = [
     redirect: '/home'
   },
   {
-    path: '/home',
-    name: 'Home',
+    path: '/layout',
+    name: 'Layout',
     redirect: '/warningCircle',
-    component: () => import('@/views/Home.vue'),
+    component: () => import('@/views/LayOut.vue'),
     children: [
-      {
-        path: '/WarningCircle',
-        name: 'warningCircle',
-        component: () => import('@/views/WarningCircle.vue')
-      },
-      {
-        path: '/WarningCircle/add',
-        name: 'AddCircle',
-        component: () => import('@/views/WarningCircle/AddCircle.vue')
-      },
-      {
-        path: '/layout',
-        name: 'layout',
-        component: () => import('@/views/layout.vue')
-      },
+      ...warningCircleRoutes,
+      ...homeRoutes,
       {
         path: '/ai',
         name: 'Ai',
-        component: () => import('@/views/Ai.vue')
+        component: () => import('@/views/Ai.vue'),
+        meta: {
+          showFooter: true
+        }
       },
       {
         path: '/mine',
         name: 'Mine',
-        component: () => import('@/views/Mine.vue')
+        component: () => import('@/views/Mine.vue'),
+        meta: {
+          showFooter: true
+        }
       },
       {
         path: '/message',
         name: 'Message',
-        component: () => import('@/views/Message.vue')
+        component: () => import('@/views/Message.vue'),
+        meta: {
+          showFooter: true
+        }
       }
     ]
   },

+ 0 - 16
src/store/modules/showFooter.js

@@ -1,16 +0,0 @@
-import { defineStore } from 'pinia'
-
-const useShowFooterStore = defineStore('showFooter', {
-  state: () => ({
-    // 控制底部导航栏的显示和隐藏
-    showFooter: true
-  }),
-  actions: {
-    // 设置底部导航栏的显示和隐藏
-    setShowFooter(value) {
-      this.showFooter = value
-    }
-  }
-})
-
-export default useShowFooterStore

+ 22 - 0
src/utils/backButtonHandler.js

@@ -0,0 +1,22 @@
+/**
+ * 禁用系统返回键
+ */
+export const disableBackButton = () => {
+  // 初始状态下就阻止默认的返回行为
+  history.pushState(null, null, location.href)
+
+  // 每当用户点击返回按钮,立即将其推回当前页面
+  window.addEventListener('popstate', function () {
+    history.pushState(null, null, location.href)
+    // 阻止默认行为
+    return false
+  })
+
+  // 返回清理函数
+  return () => {
+    window.removeEventListener('popstate', function () {
+      history.pushState(null, null, location.href)
+      return false
+    })
+  }
+}

+ 6 - 1
src/utils/globalMethods.js

@@ -1,10 +1,11 @@
-import { showSuccessToast, showFailToast, showToast } from 'vant'
+import { showSuccessToast, showFailToast, showToast, showImagePreview } from 'vant'
 
 const toastCommonOptions = {
   duration: 2000,
   className: 'loading-toast',
   iconSize: '36px'
 }
+
 // 注册全局方法
 export default {
   install: app => {
@@ -34,5 +35,9 @@ export default {
         ...options
       })
     }
+
+    app.config.globalProperties.$showImagePreview = (imageUrls, options = {}) => {
+      return showImagePreview(imageUrls, options)
+    }
   }
 }

+ 45 - 2
src/utils/utils.js

@@ -1,7 +1,11 @@
 // 动态获取icon路径
 export const getIconUrl = url => {
-  const r = new URL('../assets/icons/' + url + '.png', import.meta.url)
-  return r.href
+  try {
+    return require(`../assets/icons/${url}.png`)
+  } catch (error) {
+    console.error(`无法加载图标: ${url}`, error)
+    return ''
+  }
 }
 
 // 获取本地存储
@@ -18,3 +22,42 @@ export const setLocalStorage = (key, value) => {
     window.localStorage.setItem(key, value)
   }
 }
+
+// 格式化日期
+export const formatDate = date => {
+  if (!date) return ''
+  const now = new Date()
+  const messageDate = new Date(date)
+
+  // 如果是今天的邮件,只显示时间
+  if (messageDate.toDateString() === now.toDateString()) {
+    return messageDate.toLocaleTimeString('zh-CN', {
+      hour: '2-digit',
+      minute: '2-digit',
+      second: '2-digit'
+    })
+  }
+
+  // 如果是昨天的邮件,显示"昨天"加时间
+  const yesterday = new Date(now)
+  yesterday.setDate(now.getDate() - 1)
+  if (messageDate.toDateString() === yesterday.toDateString()) {
+    return (
+      '昨天 ' +
+      messageDate.toLocaleTimeString('zh-CN', {
+        hour: '2-digit',
+        minute: '2-digit',
+        second: '2-digit'
+      })
+    )
+  }
+
+  // 其他情况显示完整日期 (yyyy-MM-dd格式)
+  return (
+    messageDate.getFullYear() +
+    '-' +
+    String(messageDate.getMonth() + 1).padStart(2, '0') +
+    '-' +
+    String(messageDate.getDate()).padStart(2, '0')
+  )
+}

+ 7 - 138
src/views/Home.vue

@@ -1,147 +1,16 @@
 <template>
-  <div class="home">
-    <div class="content">
-      <router-view v-slot="{ Component }">
-        <transition
-          appear
-          enter-active-class="animate__animated animate__fadeInRight"
-          leact-active-class="animate__animated animate__fadeOutRight"
-        >
-          <component :is="Component"></component>
-        </transition>
-      </router-view>
-    </div>
-    <div v-show="footStore.showFooter" class="footer">
-      <div
-        v-for="(item, index) in footerItem"
-        :key="index"
-        :class="[
-          'footer-item',
-          {
-            active: route.path === item.routerPath,
-            'ai-button': item.title == 'AI助手'
-          }
-        ]"
-        @click="goPage(item.routerPath)"
-      >
-        <img :src="route.path === item.routerPath ? item.iconActive : item.icon" alt="" />
-        <span>{{ item.title }}</span>
-      </div>
-    </div>
+  <div>
+    <van-button type="primary" @click="routerClick('/email')">邮箱</van-button>
   </div>
 </template>
 
 <script setup>
-  import { getIconUrl } from '@/utils/utils'
-  import { useRoute, useRouter } from 'vue-router'
-  import useShowFooterStore from '@/store/modules/showFooter'
-  const footStore = useShowFooterStore()
+  import { useRouter } from 'vue-router'
   const router = useRouter()
-  const route = useRoute()
-  const footerItem = [
-    {
-      icon: getIconUrl('label_home_gray'),
-      iconActive: getIconUrl('label_home_color'),
-      title: '首页',
-      routerPath: '/layout'
-    },
-    {
-      icon: getIconUrl('label_message_gray'),
-      iconActive: getIconUrl('label_message_color'),
-      title: '消息',
-      routerPath: '/message'
-    },
-    {
-      icon: getIconUrl('label_ai_color'),
-      iconActive: getIconUrl('label_ai_color'),
-      title: 'AI助手',
-      routerPath: '/ai'
-    },
-    {
-      icon: getIconUrl('label_circle_gray'),
-      iconActive: getIconUrl('label_circle_color'),
-      title: '警事圈',
-      routerPath: '/warningCircle'
-    },
-    {
-      icon: getIconUrl('label_my_gray'),
-      iconActive: getIconUrl('label_my_color'),
-      title: '我的',
-      routerPath: '/mine'
-    }
-  ]
-
-  const goPage = path => {
-    router.push({
-      path,
-      replace: true
-    })
+  const routerClick = path => {
+    if (!path) return
+    router.push(path)
   }
 </script>
 
-<style lang="scss" scoped>
-  .home {
-    width: 100%;
-    height: 100vh;
-    .content {
-      width: 100%;
-      height: 100%;
-    }
-    .footer {
-      position: fixed;
-      bottom: 0;
-      z-index: 999;
-      height: calc(70px + env(safe-area-inset-bottom));
-      padding-bottom: env(safe-area-inset-bottom);
-      background: url('@/assets/icons/bj_label.png') no-repeat;
-      background-size: 100% 100%;
-      width: 100%;
-      display: grid;
-      grid-template-columns: repeat(5, 1fr);
-      filter: drop-shadow(0 5px 5px rgba(0, 0, 0, 0.5));
-
-      .footer-item {
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-        justify-content: center;
-        box-sizing: border-box;
-        padding-top: 15px;
-
-        img {
-          width: 24px;
-          height: 24px;
-          transition: all 0.3s;
-          /* 解决高分辨率屏幕下图标模糊问题 */
-          image-rendering: pixelated;
-          transform: translateZ(0);
-          backface-visibility: hidden;
-        }
-
-        span {
-          font-size: 12px;
-          color: #999;
-          margin-top: 4px;
-          transition: all 0.3s;
-        }
-
-        &.active {
-          span {
-            color: #2979ff;
-          }
-        }
-
-        &.ai-button {
-          img {
-            width: 55px;
-            height: 44px;
-            margin-top: -10px;
-          }
-          span {
-            margin-top: -5px;
-          }
-        }
-      }
-    }
-  }
-</style>
+<style lang="scss" scoped></style>

+ 310 - 0
src/views/Home/Email/Email.vue

@@ -0,0 +1,310 @@
+<template>
+  <div class="email-container">
+    <transition
+      enter-active-class="animate__animated animate__fadeInRight"
+      leave-active-class="animate__animated animate__fadeOutRight"
+      :duration="300"
+    >
+      <email-detail v-if="showDetail" @to-detail="handleToDetail" />
+    </transition>
+    <div v-show="!showDetail" style="height: 100%">
+      <!-- 顶部导航栏 -->
+      <van-nav-bar
+        left-arrow
+        @click-left="onClickLeft"
+        @click-right="onClickRight"
+        :title="'yukai@gat.jl'"
+        :border="false"
+      >
+        <template #right>
+          <img
+            :src="getIconUrl('btn_search_black_24')"
+            alt=""
+            class="top-icon"
+            style="margin-right: 5px"
+          />
+          <img :src="getIconUrl('btn_plus_black_24')" alt="" class="top-icon" />
+        </template>
+      </van-nav-bar>
+
+      <!-- 顶部切换区域 -->
+      <van-row :class="['top-title', `active-${activeIndex}`]">
+        <van-col
+          :class="{ active: activeIndex == index }"
+          span="8"
+          v-for="(item, index) in menu"
+          :key="index"
+          @click="activeIndex = index"
+        >
+          <img
+            class="content-icon"
+            :src="getIconUrl(activeIndex == index ? item.activeIcon : item.icon)"
+            alt=""
+          />
+          <span>{{ item.text }}</span>
+        </van-col>
+      </van-row>
+
+      <!-- 内容区域 -->
+      <van-pull-refresh
+        class="email-content"
+        v-model="refreshLoading"
+        @refresh="onRefresh"
+        :disabled="!isAtTop"
+      >
+        <van-list
+          class="email-list"
+          v-model:loading="listLoading"
+          :finished="finished"
+          finished-text="没有更多了"
+          @scroll="onScroll"
+        >
+          <div
+            :class="['item', { read: index == 2 }]"
+            v-for="(item, index) in 6"
+            :key="index"
+            @click="handleEmailClick"
+            :data="item"
+          >
+            <div class="left">
+              <van-badge dot :offset="[5, 10]">
+                <img src="@/assets/icons/icon_my_head_64@2x.png" alt="" />
+              </van-badge>
+            </div>
+            <div class="right">
+              <div class="top">
+                <span class="name">吉林省人才部</span>
+                <span class="time">{{ formatDate(new Date()) }}</span>
+              </div>
+              <div class="title">吉林省优化人才管理政策</div>
+              <div class="content">
+                为深入实施人才强省战略,贯彻落实省委人才领导小组办公室《关于以人才工作驱动产业高质量...222
+              </div>
+            </div>
+          </div>
+        </van-list>
+      </van-pull-refresh>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { getIconUrl, formatDate } from '@/utils/utils.js'
+  import EmailDetail from './EmailDetail.vue'
+  import { ref } from 'vue'
+  import 'animate.css'
+  import { useRouter } from 'vue-router'
+  const router = useRouter()
+  const onClickLeft = () => {
+    router.push('/home')
+  }
+  const onClickRight = () => {}
+  const activeIndex = ref(0)
+  const menu = [
+    { text: '收件箱', icon: 'icon_draft_gray_16', activeIcon: 'icon_draft_black_16' },
+    { text: '已发送', icon: 'icon_send_gray_16', activeIcon: 'icon_send_black_16' },
+    { text: '草稿箱', icon: 'icon_mailbox_gray_16', activeIcon: 'icon_mailbox_black_16' }
+  ]
+
+  const showDetail = ref(false)
+  const refreshLoading = ref(false)
+  const onRefresh = () => {
+    setTimeout(() => {
+      refreshLoading.value = false
+    }, 1000)
+  }
+
+  const listLoading = ref(false)
+  const finished = ref(true)
+
+  const isAtTop = ref(true)
+
+  const onScroll = ({ target: { scrollTop } }) => {
+    isAtTop.value = scrollTop < 10
+  }
+
+  const handleEmailClick = () => {
+    showDetail.value = true
+  }
+
+  const handleToDetail = value => {
+    showDetail.value = value
+  }
+</script>
+
+<style lang="scss" scoped>
+  .email-container {
+    width: 100%;
+    height: 100vh;
+    position: relative;
+    overflow: hidden;
+
+    .top-title {
+      width: 100%;
+      background-color: #fff;
+      position: relative;
+
+      // 添加一个公共的指示线元素
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 0;
+        left: calc(100% / 6); // 初始位置在第一个选项中间
+        transform: translateX(-50%);
+        width: 20px;
+        height: 5px;
+        background: url('@/assets/icons/nav_line.png') no-repeat;
+        background-size: 100% 100%;
+        border-radius: 3px;
+        bottom: 3px;
+        transition: left 0.3s ease; // 只对left属性添加过渡效果
+      }
+
+      // 根据激活的选项调整指示线位置
+      &.active-0::after {
+        left: calc(100% / 6);
+      }
+
+      &.active-1::after {
+        left: calc(100% / 2);
+      }
+
+      &.active-2::after {
+        left: calc(5 * 100% / 6);
+      }
+
+      .van-col {
+        height: 40px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        &.active {
+          span {
+            color: #000;
+          }
+        }
+
+        span {
+          color: #7b7b7b;
+          margin-left: 5px;
+        }
+      }
+    }
+
+    .email-content {
+      width: 100%;
+      height: calc(100% - 80px);
+      background-color: rgba(248, 248, 248, 1);
+      box-sizing: border-box;
+      padding: 3px 16px 13px 16px;
+      .email-list {
+        width: 100%;
+        height: 100%;
+        overflow-y: auto;
+        overflow-x: hidden;
+        scrollbar-width: none; /* Firefox */
+        -ms-overflow-style: none; /* IE and Edge */
+        &::-webkit-scrollbar {
+          display: none; /* Chrome, Safari and Opera */
+        }
+        .item {
+          display: flex;
+          padding: 16px;
+          width: 100%;
+          border-radius: 12px;
+          background-color: #fff;
+          margin-top: 12px;
+          &:last-child {
+            margin-bottom: 12px;
+          }
+
+          .left {
+            img {
+              width: 40px;
+              height: 40px;
+              border-radius: 50%;
+              background: #e8f2fe;
+            }
+            :deep(.van-badge--dot) {
+              width: 6px;
+              height: 6px;
+            }
+          }
+
+          .right {
+            flex: 1;
+            margin-left: 12px;
+
+            .top {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+              margin-bottom: 8px;
+
+              .name {
+                font-size: 16px;
+                font-weight: 500;
+                color: #333;
+              }
+
+              .time {
+                font-size: 12px;
+                color: #999;
+              }
+            }
+
+            .title {
+              font-size: 14px;
+              color: #333;
+              margin-bottom: 4px;
+            }
+
+            .content {
+              font-size: 12px;
+              /* 设置字体大小为12像素 */
+              color: #999;
+              /* 设置字体颜色为浅灰色 */
+              overflow: hidden;
+              /* 溢出内容隐藏 */
+              text-overflow: ellipsis;
+              /* 文本溢出时显示省略号 */
+              display: -webkit-box;
+              /* 将容器设置为弹性盒子 */
+              -webkit-line-clamp: 2;
+              /* 限制文本显示为2行 */
+              -webkit-box-orient: vertical;
+              /* 设置文本垂直排列 */
+            }
+          }
+
+          &.read {
+            .right {
+              .name,
+              .title {
+                color: #666;
+                font-weight: normal;
+              }
+
+              .content {
+                color: #aaa;
+              }
+            }
+
+            // 已读状态下隐藏红点
+            .left :deep(.van-badge--dot) {
+              display: none;
+            }
+            backdrop-filter: blur(5px);
+          }
+        }
+      }
+    }
+  }
+
+  /* 导航栏图标颜色覆盖 */
+  :deep(.van-nav-bar .van-icon) {
+    color: #333;
+    font-size: 24px;
+  }
+</style>

+ 431 - 0
src/views/Home/Email/EmailDetail.vue

@@ -0,0 +1,431 @@
+<template>
+  <div class="email-detail">
+    <!-- 顶部导航栏 -->
+    <van-nav-bar
+      class="email-detail-bar"
+      left-arrow
+      @click-left="goBack"
+      :title="email.subject || '无主题'"
+      :border="false"
+    >
+      <template #right>
+        <van-icon name="delete-o" size="18" @click="handleDelete" />
+      </template>
+    </van-nav-bar>
+
+    <!-- 邮件信息头部 -->
+    <div class="email-header">
+      <div class="subject">{{ email.subject || '无主题' }}</div>
+
+      <div class="sender-info">
+        <van-image round width="40" height="40" :src="senderAvatar" fit="cover" />
+        <div class="sender-details">
+          <div class="sender-name">{{ email.sender || 'unknown@example.com' }}</div>
+          <div class="sender-to">发送至:{{ email.to || 'me' }}</div>
+        </div>
+        <div class="email-time">{{ formatDate(email.time) }}</div>
+      </div>
+    </div>
+
+    <!-- 邮件正文 -->
+    <div class="email-content">
+      <div v-html="email.content"></div>
+    </div>
+
+    <!-- 附件区域 -->
+    <div v-if="email.attachments && email.attachments.length > 0" class="attachments-section">
+      <div class="section-title">附件 ({{ email.attachments.length }})</div>
+      <div class="attachments-list">
+        <div
+          v-for="(attachment, index) in email.attachments"
+          :key="index"
+          class="attachment-item"
+          @click="previewAttachment(attachment)"
+        >
+          <div class="attachment-icon">
+            <van-icon :name="getFileIcon(attachment.type)" size="24" />
+          </div>
+          <div class="attachment-info">
+            <div class="attachment-name">{{ attachment.name }}</div>
+            <div class="attachment-size">{{ formatFileSize(attachment.size) }}</div>
+          </div>
+          <van-icon name="down" class="download-icon" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref } from 'vue'
+  import { getCurrentInstance } from 'vue'
+  import { formatDate } from '@/utils/utils'
+  const emit = defineEmits(['toDetail'])
+  const { proxy } = getCurrentInstance()
+  defineProps({
+    data: {
+      type: Object,
+      default: () => {}
+    }
+  })
+
+  // 模拟邮件数据
+  const email = ref({
+    id: '123456',
+    subject: '项目进度报告 - 2023年第二季度',
+    sender: 'project-manager@company.com',
+    senderName: '项目经理',
+    to: 'yukai@gat.jl',
+    time: '2025-03-16 12:23:45',
+    content: `<p>尊敬的于凯:</p>
+              <p>附件是本季度的项目进度报告,请查收。</p>
+              <p>报告中包含了所有项目的当前状态、完成百分比以及遇到的问题。</p>
+              <p>如有任何疑问,请随时与我联系。</p>
+              <p>此致,</p>
+              <p>项目管理部</p>`,
+    starred: false,
+    attachments: [
+      { name: '2023Q2项目进度报告.pdf', type: 'pdf', size: 2456000 },
+      { name: '财务数据.xlsx', type: 'excel', size: 1200000 },
+      { name: '会议记录.docx', type: 'word', size: 350000 },
+      { name: '演示文稿.pptx', type: 'ppt', size: 4500000 },
+      { name: '项目照片.jpg', type: 'image', size: 1800000 },
+      { name: '演示视频.mp4', type: 'video', size: 15000000 }
+    ]
+  })
+
+  const senderAvatar = ref('https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg')
+
+  // 返回上一页
+  const goBack = () => {
+    emit('toDetail', false)
+  }
+
+  // 格式化文件大小
+  const formatFileSize = bytes => {
+    if (bytes === 0) return '0 B'
+    const k = 1024
+    const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
+    const i = Math.floor(Math.log(bytes) / Math.log(k))
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+  }
+
+  // 根据文件类型获取图标
+  const getFileIcon = type => {
+    const iconMap = {
+      pdf: 'description',
+      word: 'description',
+      excel: 'description',
+      ppt: 'description',
+      image: 'photo-o',
+      video: 'video-o',
+      audio: 'music-o',
+      zip: 'description',
+      text: 'description'
+    }
+    return iconMap[type] || 'description'
+  }
+
+  // 处理删除
+  const handleDelete = () => {
+    proxy.$showToast('删除邮件')
+    emit('toDetail', false)
+  }
+
+  const getBinaryData = id => {
+    return id
+  }
+
+  // 预览附件
+  const previewAttachment = attachment => {
+    // 创建一个Blob对象来处理二进制数据
+    // 注意:这里假设你已经有了二进制数据,实际使用时需要从API获取
+    const binaryData = getBinaryData(attachment.id) // 这个函数需要你实现,用于获取二进制数据
+
+    // 根据文件类型选择不同的预览方式
+    switch (attachment.type) {
+      case 'image':
+        previewImage(attachment, binaryData)
+        break
+      case 'pdf':
+        previewPDF(attachment, binaryData)
+        break
+      case 'word':
+      case 'excel':
+      case 'ppt':
+        previewOffice(attachment, binaryData)
+        break
+      case 'text':
+        previewText(attachment, binaryData)
+        break
+      case 'video':
+        previewVideo(attachment, binaryData)
+        break
+      case 'audio':
+        previewAudio(attachment, binaryData)
+        break
+      default:
+        // 对于不支持预览的文件类型,提供下载选项
+        downloadFile(attachment, binaryData)
+        break
+    }
+  }
+
+  // 预览图片
+  const previewImage = (attachment, binaryData) => {
+    const blob = new Blob([binaryData], { type: 'image/jpeg' }) // 根据实际类型调整
+    const url = URL.createObjectURL(blob)
+
+    // 使用vant的ImagePreview组件
+    proxy.$showImagePreview([url])
+  }
+
+  // 预览PDF
+  const previewPDF = (attachment, binaryData) => {
+    const blob = new Blob([binaryData], { type: 'application/pdf' })
+    const url = URL.createObjectURL(blob)
+
+    // 打开新窗口或使用iframe嵌入PDF查看器
+    window.open(url, '_blank')
+  }
+
+  // 预览Office文档
+  const previewOffice = (attachment, binaryData) => {
+    // 可以使用Microsoft Office Online或Google Docs等在线服务
+    // 这里简化为下载或使用第三方库
+    const mimeTypes = {
+      word: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+      excel: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+      ppt: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
+    }
+
+    const blob = new Blob([binaryData], { type: mimeTypes[attachment.type] })
+    const url = URL.createObjectURL(blob)
+
+    // 可以使用第三方库如mammoth.js(Word)、SheetJS(Excel)等
+    // 或者使用在线预览服务
+    proxy.$showToast(`使用在线服务预览: ${attachment.name}`)
+    window.open(
+      `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`,
+      '_blank'
+    )
+  }
+
+  // 预览文本文件
+  const previewText = (attachment, binaryData) => {
+    const blob = new Blob([binaryData], { type: 'text/plain' })
+    const reader = new FileReader()
+
+    reader.onload = e => {
+      const content = e.target.result
+      // 显示文本内容,可以使用弹窗或模态框
+      proxy.$dialog.alert({
+        title: attachment.name,
+        message: content
+      })
+    }
+
+    reader.readAsText(blob)
+  }
+
+  // 预览视频
+  const previewVideo = (attachment, binaryData) => {
+    const blob = new Blob([binaryData], { type: 'video/mp4' }) // 根据实际类型调整
+    const url = URL.createObjectURL(blob)
+
+    // 使用视频播放器组件或打开新窗口
+    proxy.$dialog.alert({
+      title: attachment.name,
+      message: `<video controls style="width:100%"><source src="${url}" type="video/mp4"></video>`,
+      allowHtml: true
+    })
+  }
+
+  // 预览音频
+  const previewAudio = (attachment, binaryData) => {
+    const blob = new Blob([binaryData], { type: 'audio/mpeg' }) // 根据实际类型调整
+    const url = URL.createObjectURL(blob)
+
+    // 使用音频播放器组件或打开新窗口
+    proxy.$dialog.alert({
+      title: attachment.name,
+      message: `<audio controls style="width:100%"><source src="${url}" type="audio/mpeg"></audio>`,
+      allowHtml: true
+    })
+  }
+
+  // 下载文件
+  const downloadFile = (attachment, binaryData) => {
+    const blob = new Blob([binaryData])
+    const url = URL.createObjectURL(blob)
+
+    const a = document.createElement('a')
+    a.href = url
+    a.download = attachment.name
+    document.body.appendChild(a)
+    a.click()
+    document.body.removeChild(a)
+
+    // 清理URL对象
+    setTimeout(() => URL.revokeObjectURL(url), 100)
+  }
+</script>
+
+<style lang="scss" scoped>
+  .email-detail {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    overflow-y: auto;
+    overflow-x: hidden;
+    background-color: #f7f8fa;
+    scrollbar-width: none; /* Firefox */
+    -ms-overflow-style: none; /* IE and Edge */
+    &::-webkit-scrollbar {
+      display: none; /* Chrome, Safari and Opera */
+    }
+    .email-detail-bar {
+      position: fixed;
+      top: 0;
+      width: 100%;
+      z-index: 100;
+    }
+  }
+
+  .email-header {
+    padding: 16px;
+    background-color: #fff;
+    margin-bottom: 8px;
+
+    .subject {
+      font-size: 18px;
+      font-weight: bold;
+      margin-bottom: 16px;
+      color: #323233;
+    }
+
+    .sender-info {
+      display: flex;
+      align-items: center;
+
+      .sender-details {
+        flex: 1;
+        margin-left: 12px;
+
+        .sender-name {
+          font-size: 15px;
+          font-weight: 500;
+          color: #323233;
+        }
+
+        .sender-to {
+          font-size: 13px;
+          color: #969799;
+          margin-top: 2px;
+        }
+      }
+
+      .email-time {
+        font-size: 13px;
+        color: #969799;
+      }
+    }
+  }
+
+  .email-content {
+    padding: 16px;
+    background-color: #fff;
+    flex: 1;
+    line-height: 1.6;
+    color: #323233;
+    font-size: 15px;
+  }
+
+  .attachments-section {
+    margin-top: 8px;
+    background-color: #fff;
+    padding: 16px;
+
+    .section-title {
+      font-size: 15px;
+      font-weight: 500;
+      margin-bottom: 12px;
+      color: #323233;
+    }
+
+    .attachments-list {
+      .attachment-item {
+        display: flex;
+        align-items: center;
+        padding: 12px;
+        background-color: #f7f8fa;
+        border-radius: 8px;
+        margin-bottom: 8px;
+
+        .attachment-icon {
+          width: 40px;
+          height: 40px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          background-color: #e8f3ff;
+          border-radius: 8px;
+          margin-right: 12px;
+        }
+
+        .attachment-info {
+          flex: 1;
+
+          .attachment-name {
+            font-size: 14px;
+            color: #323233;
+            margin-bottom: 4px;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            max-width: 200px;
+          }
+
+          .attachment-size {
+            font-size: 12px;
+            color: #969799;
+          }
+        }
+
+        .download-icon {
+          color: #1989fa;
+        }
+      }
+    }
+  }
+
+  .action-bar {
+    display: flex;
+    justify-content: space-around;
+    padding: 12px 0;
+    background-color: #fff;
+    border-top: 1px solid #ebedf0;
+
+    .action-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      padding: 8px 12px;
+
+      span {
+        font-size: 12px;
+        margin-top: 4px;
+        color: #646566;
+      }
+
+      .van-icon {
+        color: #323233;
+      }
+
+      .starred {
+        color: #ff9800;
+      }
+    }
+  }
+</style>

+ 85 - 121
src/views/WarningCircle.vue

@@ -1,98 +1,89 @@
 <template>
-  <div>
-    <div v-show="!inAddScene" class="container">
-      <sticky-bar>
-        <template #left>
-          <div class="title-bar" :class="{ active: scene == 0 }" @click="changeScene(0)">
-            <van-badge dot>警事圈</van-badge>
+  <div class="circle-container">
+    <sticky-bar>
+      <template #left>
+        <div class="title-bar" :class="{ active: scene == 0 }" @click="changeScene(0)">
+          <van-badge dot>警事圈</van-badge>
+        </div>
+        <div
+          class="title-bar"
+          style="margin-left: 20px"
+          :class="{ active: scene == 1 }"
+          @click="changeScene(1)"
+        >
+          热榜
+        </div>
+      </template>
+      <template #right>
+        <img
+          :src="getIconUrl('btn_search_black_24')"
+          alt=""
+          class="top-icon"
+          style="margin-right: 15px"
+        />
+        <img :src="getIconUrl('btn_phone_black_24')" alt="" class="top-icon" @click="openAdd" />
+      </template>
+    </sticky-bar>
+
+    <van-pull-refresh
+      v-model="refreshLoading"
+      :head-height="80"
+      class="content"
+      @refresh="onRefresh"
+    >
+      <!-- 下拉提示 -->
+      <template #pulling="props">
+        <div class="custom-refresh">
+          <div class="refresh-progress-bar">
+            <div
+              class="progress"
+              :style="{ width: `${Math.min((props.distance / 80) * 100, 100)}%` }"
+            ></div>
           </div>
-          <div
-            class="title-bar"
-            style="margin-left: 20px"
-            :class="{ active: scene == 1 }"
-            @click="changeScene(1)"
-          >
-            热榜
+          <div class="refresh-text">
+            <van-icon
+              name="arrow-down"
+              :style="{ transform: `rotate(${Math.min((props.distance / 80) * 180, 180)}deg)` }"
+            />
+            <span>{{ props.distance > 70 ? '松开刷新' : '下拉刷新' }}</span>
           </div>
-        </template>
-        <template #right>
-          <img
-            :src="getIconUrl('btn_search_black_24')"
-            alt=""
-            class="top-icon"
-            style="margin-right: 15px"
-          />
-          <img :src="getIconUrl('btn_phone_black_24')" alt="" class="top-icon" @click="openAdd" />
-        </template>
-      </sticky-bar>
-
-      <van-pull-refresh
-        v-model="refreshLoading"
-        :head-height="80"
-        class="content"
-        @refresh="onRefresh"
-      >
-        <!-- 下拉提示 -->
-        <template #pulling="props">
-          <div class="custom-refresh">
-            <div class="refresh-progress-bar">
-              <div
-                class="progress"
-                :style="{ width: `${Math.min((props.distance / 80) * 100, 100)}%` }"
-              ></div>
-            </div>
-            <div class="refresh-text">
-              <van-icon
-                name="arrow-down"
-                :style="{ transform: `rotate(${Math.min((props.distance / 80) * 180, 180)}deg)` }"
-              />
-              <span>{{ props.distance > 70 ? '松开刷新' : '下拉刷新' }}</span>
-            </div>
+        </div>
+      </template>
+
+      <!-- 释放提示 -->
+      <template #loosing>
+        <div class="custom-refresh">
+          <div class="refresh-progress-bar">
+            <div class="progress complete"></div>
           </div>
-        </template>
-
-        <!-- 释放提示 -->
-        <template #loosing>
-          <div class="custom-refresh">
-            <div class="refresh-progress-bar">
-              <div class="progress complete"></div>
-            </div>
-            <div class="refresh-text">
-              <van-icon name="arrow-up" />
-              <span>松开刷新</span>
-            </div>
+          <div class="refresh-text">
+            <van-icon name="arrow-up" />
+            <span>松开刷新</span>
           </div>
-        </template>
-
-        <!-- 加载提示 -->
-        <template #loading>
-          <div class="custom-refresh">
-            <div class="refresh-loading">
-              <div class="loading-circle"></div>
-            </div>
-            <div class="refresh-text">
-              <span>正在刷新...</span>
-            </div>
+        </div>
+      </template>
+
+      <!-- 加载提示 -->
+      <template #loading>
+        <div class="custom-refresh">
+          <div class="refresh-loading">
+            <div class="loading-circle"></div>
           </div>
-        </template>
-
-        <van-list v-model:loading="loading" :finished="finished" finished-text="我是有底线的~">
-          <PoliceAffairsItem
-            v-for="item in 10"
-            :key="item"
-            @comment="handleComment"
-          ></PoliceAffairsItem>
-        </van-list>
-        <van-back-top right="2vw" bottom="10vh" />
-      </van-pull-refresh>
-    </div>
-
-    <Transition
-      enter-active-class="animate__animated animate__fadeInRight"
-      leact-active-class="animate__animated animate__fadeOutRight"
-    >
-      <AddCircleVue v-if="inAddScene" @push-jsq="pushJsq"></AddCircleVue>
-    </Transition>
+          <div class="refresh-text">
+            <span>正在刷新...</span>
+          </div>
+        </div>
+      </template>
+
+      <van-list v-model:loading="loading" :finished="finished" finished-text="我是有底线的~">
+        <PoliceAffairsItem
+          v-for="item in 10"
+          :key="item"
+          @comment="handleComment"
+        ></PoliceAffairsItem>
+      </van-list>
+      <van-back-top right="2vw" bottom="10vh" />
+    </van-pull-refresh>
 
     <!-- 添加共享的评论弹出层 -->
     <van-popup
@@ -108,7 +99,7 @@
           placeholder="说点什么..."
           class="comment-input"
         />
-        <van-button type="primary" size="small" @click="submitComment"> 发送 </van-button>
+        <van-button type="primary" size="small" @click="submitComment">发送</van-button>
       </div>
     </van-popup>
   </div>
@@ -117,16 +108,13 @@
 <script setup>
   import { getIconUrl } from '@/utils/utils'
   import PoliceAffairsItem from '@/views/WarningCircle/PoliceAffairsItem.vue'
-  import AddCircleVue from './WarningCircle/AddCircle.vue'
-  import useShowFooterStore from '@/store/modules/showFooter'
   import { nextTick, getCurrentInstance } from 'vue'
-
+  import { useRouter } from 'vue-router'
+  const router = useRouter()
   const { proxy } = getCurrentInstance()
   // 初始化底部导航栏状态管理
-  const showFooterStore = useShowFooterStore()
   import { ref } from 'vue'
   const scene = ref(0)
-  const inAddScene = ref(false)
 
   // 评论相关状态
   const showCommentPopup = ref(false)
@@ -167,14 +155,8 @@
   const loading = ref(false)
   const finished = ref(true)
 
-  const pushJsq = () => {
-    inAddScene.value = false
-    showFooterStore.setShowFooter(true)
-  }
-
   const openAdd = () => {
-    inAddScene.value = true
-    showFooterStore.setShowFooter(false)
+    router.push('/warningCircle/add')
   }
 
   const refreshLoading = ref(false)
@@ -187,7 +169,7 @@
 </script>
 
 <style lang="scss" scoped>
-  .container {
+  .circle-container {
     width: 100%;
     height: 100vh;
     background: linear-gradient(to left bottom, #cfeafe, #ffffff);
@@ -345,22 +327,4 @@
       }
     }
   }
-
-  .right-in-enter-active {
-    animation: right-in 0.3s;
-  }
-  .right-in-leave-active {
-    animation: right-in 0.3s reverse;
-  }
-
-  @keyframes right-in {
-    from {
-      transform: translateX(100%);
-      opacity: 0;
-    }
-    to {
-      transform: translateX(0);
-      opacity: 1;
-    }
-  }
 </style>

+ 5 - 49
src/views/WarningCircle/AddCircle.vue

@@ -91,10 +91,13 @@
 </template>
 
 <script setup>
-  import { ref, onMounted, onBeforeUnmount } from 'vue'
+  import { ref } from 'vue'
   import { showConfirmDialog } from 'vant'
   import { getCurrentInstance } from 'vue'
   const { proxy } = getCurrentInstance()
+  import { useRouter } from 'vue-router'
+  const router = useRouter()
+
   // 文本内容响应式变量
   const thoughtText = ref('')
   // 权限选择器显示状态
@@ -102,8 +105,6 @@
   // 当前选择的权限值
   const fieldValue = ref('全部')
 
-  const emit = defineEmits(['pushJsq'])
-
   const fileList = ref([]) //upload绑定的数组
   const videoUrls = ref([]) // 存储多个视频的URL
   const showVideoPlay = ref(false)
@@ -176,55 +177,10 @@
       width: '300px',
       className: 'confirm-dialog'
     }).then(() => {
-      emit('pushJsq')
+      router.push('/warningCircle')
     })
   }
 
-  // 处理浏览器返回事件
-  const handlePopState = event => {
-    // 阻止默认的返回行为
-    event.preventDefault()
-    // 显示确认对话框
-    showConfirmDialog({
-      message: '退出此次编辑?',
-      confirmButtonText: '退出',
-      confirmButtonColor: '#ff0000',
-      width: '300px',
-      className: 'confirm-dialog'
-    })
-      .then(() => {
-        // 用户确认退出
-        emit('pushJsq')
-      })
-      .catch(() => {
-        // 用户取消退出,将历史记录推回
-        // 先移除当前的popstate监听器,防止触发新的事件
-        window.removeEventListener('popstate', handlePopState)
-
-        // 添加新的历史记录
-        window.history.pushState(null, '', window.location.href)
-
-        // 重新添加监听器
-        setTimeout(() => {
-          window.addEventListener('popstate', handlePopState)
-        }, 0)
-      })
-  }
-
-  // 组件挂载时
-  onMounted(() => {
-    // 添加历史记录,用于捕获返回事件
-    window.history.pushState(null, '', window.location.href)
-    // 监听返回事件
-    window.addEventListener('popstate', handlePopState)
-  })
-
-  // 组件卸载前
-  onBeforeUnmount(() => {
-    // 移除事件监听
-    window.removeEventListener('popstate', handlePopState)
-  })
-
   /**
    * 导航栏右侧按钮点击事件
    */

+ 1 - 2
src/views/WarningCircle/PoliceAffairsItem.vue

@@ -75,14 +75,13 @@
 
 <script setup>
   import { getIconUrl } from '@/utils/utils'
-  import { showImagePreview } from 'vant'
   import { getCurrentInstance } from 'vue'
   const { proxy } = getCurrentInstance()
   const props = defineProps({
     data: { type: Object, default: () => {} }
   })
   const showImage = data => {
-    showImagePreview({
+    proxy.$showImagePreview({
       images: [data]
     })
   }

+ 145 - 3
src/views/layout.vue

@@ -1,7 +1,149 @@
 <template>
-  <div>主页</div>
+  <div class="home">
+    <div class="content">
+      <router-view v-slot="{ Component }">
+        <transition
+          appear
+          enter-active-class="animate__animated animate__fadeInRight"
+          leact-active-class="animate__animated animate__fadeOutRight"
+        >
+          <component :is="Component"></component>
+        </transition>
+      </router-view>
+    </div>
+    <div v-show="showFooter" class="footer">
+      <div
+        v-for="(item, index) in footerItem"
+        :key="index"
+        :class="[
+          'footer-item',
+          {
+            active: route.path === item.routerPath,
+            'ai-button': item.title == 'AI助手'
+          }
+        ]"
+        @click="goPage(item.routerPath)"
+      >
+        <img :src="route.path === item.routerPath ? item.iconActive : item.icon" alt="" />
+        <span>{{ item.title }}</span>
+      </div>
+    </div>
+  </div>
 </template>
 
-<script setup></script>
+<script setup>
+  import { getIconUrl } from '@/utils/utils'
+  import { useRoute, useRouter } from 'vue-router'
+  import { computed } from 'vue'
+  const router = useRouter()
+  const route = useRoute()
+  const footerItem = [
+    {
+      icon: getIconUrl('label_home_gray'),
+      iconActive: getIconUrl('label_home_color'),
+      title: '首页',
+      routerPath: '/home'
+    },
+    {
+      icon: getIconUrl('label_message_gray'),
+      iconActive: getIconUrl('label_message_color'),
+      title: '消息',
+      routerPath: '/message'
+    },
+    {
+      icon: getIconUrl('label_ai_color'),
+      iconActive: getIconUrl('label_ai_color'),
+      title: 'AI助手',
+      routerPath: '/ai'
+    },
+    {
+      icon: getIconUrl('label_circle_gray'),
+      iconActive: getIconUrl('label_circle_color'),
+      title: '警事圈',
+      routerPath: '/warningCircle'
+    },
+    {
+      icon: getIconUrl('label_my_gray'),
+      iconActive: getIconUrl('label_my_color'),
+      title: '我的',
+      routerPath: '/mine'
+    }
+  ]
 
-<style lang="scss" scoped></style>
+  const goPage = path => {
+    router.push({
+      path
+    })
+  }
+
+  const showFooter = computed(() => {
+    return route.meta.showFooter || route.query.keepFooter === 'true'
+  })
+</script>
+
+<style lang="scss" scoped>
+  .home {
+    width: 100%;
+    height: 100vh;
+    .content {
+      width: 100%;
+      height: 100%;
+    }
+    .footer {
+      position: fixed;
+      bottom: 0;
+      z-index: 999;
+      height: calc(70px + env(safe-area-inset-bottom));
+      padding-bottom: env(safe-area-inset-bottom);
+      background: url('@/assets/icons/bj_label.png') no-repeat;
+      background-size: 100% 100%;
+      width: 100%;
+      display: grid;
+      grid-template-columns: repeat(5, 1fr);
+      filter: drop-shadow(0 5px 5px rgba(0, 0, 0, 0.5));
+
+      .footer-item {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        box-sizing: border-box;
+        padding-top: 15px;
+
+        img {
+          width: 24px;
+          height: 24px;
+          transition: all 0.3s;
+          /* 解决高分辨率屏幕下图标模糊问题 */
+          image-rendering: pixelated;
+          transform: translateZ(0);
+          backface-visibility: hidden;
+        }
+
+        span {
+          font-size: 12px;
+          color: #999;
+          margin-top: 4px;
+          transition: all 0.3s;
+        }
+
+        &.active {
+          span {
+            color: #2979ff;
+          }
+        }
+
+        &.ai-button {
+          img {
+            width: 55px;
+            height: 44px;
+            margin-top: -10px;
+          }
+          span {
+            margin-top: -5px;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 0 - 90
vite.config.js

@@ -1,90 +0,0 @@
-// 导入 Vite 的配置函数
-import { defineConfig, loadEnv } from 'vite'
-// 导入 Vue 插件
-import vue from '@vitejs/plugin-vue'
-// 导入自动导入插件,用于自动导入 API
-import AutoImport from 'unplugin-auto-import/vite'
-// 导入组件自动注册插件
-import Components from 'unplugin-vue-components/vite'
-// 导入 Vant 组件库的解析器
-import { VantResolver } from '@vant/auto-import-resolver'
-import path from 'node:path'
-import postcssPxToViewport from 'postcss-px-to-viewport-8-plugin'
-
-// 导出 Vite 配置
-export default defineConfig(({ mode }) => {
-  const env = loadEnv(mode, process.cwd())
-  return {
-    // 配置环境变量前缀
-    envPrefix: 'APP_',
-    // 配置插件
-    plugins: [
-      // 启用 Vue 插件
-      vue(),
-      // 配置自动导入插件
-      AutoImport({
-        // 使用 Vant 解析器
-        resolvers: [VantResolver()]
-      }),
-      // 配置组件自动注册插件
-      Components({
-        // 使用 Vant 解析器
-        resolvers: [VantResolver()]
-      })
-    ],
-    css: {
-      postcss: {
-        plugins: [
-          postcssPxToViewport({
-            // 要转化的单位
-            unitToConvert: 'px',
-            // UI设计稿的大小
-            viewportWidth: 375,
-            // 转换后的精度
-            unitPrecision: 6,
-            // 转换后的单位
-            viewportUnit: 'vw',
-            // 字体转换后的单位
-            fontViewportUnit: 'vw',
-            // 能转换的属性,*表示所有属性,!border表示border不转
-            propList: ['*'],
-            // 指定不转换为视窗单位的类名,
-            selectorBlackList: ['van-'],
-            // 最小转换的值,小于等于1不转
-            minPixelValue: 1,
-            // 是否在媒体查询的css代码中也进行转换,默认false
-            mediaQuery: false,
-            // 是否转换后直接更换属性值
-            replace: true,
-            // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
-            exclude: [/node_modules/],
-            // 包含那些文件或者特定文件
-            include: [],
-            // 是否处理横屏情况
-            landscape: false
-          })
-        ]
-      },
-      preprocessorOptions: {
-        scss: {
-          silenceDeprecations: ['legacy-js-api'],
-          additionalData: `@use "@/assets/style/variables.scss";`
-        }
-      }
-    },
-    resolve: {
-      alias: {
-        '@': path.resolve(__dirname, './src')
-      }
-    },
-    server: {
-      host: '0.0.0.0',
-      proxy: {
-        [env.APP_BASE_API]: {
-          target: env.APP_BASE_SERVER_URL,
-          changeOrigin: true
-        }
-      }
-    }
-  }
-})

Some files were not shown because too many files changed in this diff