Browse Source

✨ feat: 增加对electron应用的适配

Pchen. 10 months ago
parent
commit
dd67d87e4a

+ 1 - 0
logo.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="1754786218152" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28063" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M512.558545 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#1296db" p-id="28064"></path><path d="M194.932364 765.393455l170.356363-98.490182 59.578182-91.973818a60.136727 60.136727 0 0 0 30.161455 33.698909l26.251636 12.846545-59.764364 93.090909A37.236364 37.236364 0 0 1 409.6 726.109091l-177.989818 102.772364a36.305455 36.305455 0 0 1-36.305455-62.743273z m526.522181-389.492364a84.526545 84.526545 0 1 1 84.526546-84.526546 84.526545 84.526545 0 0 1-84.526546 84.526546z m-40.401454 81.733818A60.695273 60.695273 0 0 0 701.160727 409.6l39.098182 26.251636 73.169455-80.058181a36.305455 36.305455 0 0 1 53.434181 48.779636l-94.208 103.144727a36.119273 36.119273 0 0 1-46.917818 5.585455l-61.067636-40.96z m0 0" fill="#ffffff" p-id="28065"></path><path d="M606.021818 655.36L465.454545 587.031273a36.305455 36.305455 0 0 1-8.192-59.578182l124.369455-110.592-74.472727-49.152-108.544 31.092364a36.305455 36.305455 0 0 1-19.921455-69.632l124.555637-35.560728a36.119273 36.119273 0 0 1 29.975272 4.654546l127.348364 84.340363a36.305455 36.305455 0 0 1 4.096 57.157819l-119.156364 105.937454 127.720728 62.184727a36.305455 36.305455 0 0 1 14.894545 51.572364l-96.628364 156.392727a36.119273 36.119273 0 1 1-61.626181-37.236363z m0 0" fill="#ffffff" p-id="28066"></path></svg>

+ 0 - 1
public/logo.svg

@@ -1 +0,0 @@
-<?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="1747188290992" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9213" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M550.4 1024c-8.7 0-17.2-2.3-24.8-6.5-24.7-13.7-33.6-44.9-19.9-69.6l106.6-191.8-145.1-130-103.5 117.3c-9.3 15.2-25.9 24.5-43.7 24.6H89.6c-28.3 0-51.2-22.9-51.2-51.2 0-28.3 22.9-51.2 51.2-51.2h201.6l132.3-209-1.2-0.7 110-194.9-102.6-30.6-126.8 95.9c-17.7 11.8-41.5 7-53.3-10.7-11.8-17.7-7-41.5 10.7-53.3l136.5-102.4c6.3-4.2 13.7-6.4 21.3-6.3l163.5 20.1h0.3c7 0 13.8 1.9 19.7 5.5l122.2 69.5c2.9 1.8 5.4 3.9 7.7 6.4 3.3 3.3 6 7.1 7.9 11.4l67.1 168.7h140.8c21.2 0 38.4 17.2 38.4 38.4S968.5 512 947.3 512H781.1c-15.4 0-29.3-9.2-35.3-23.3L681 362.1 576.1 547.8l133.5 153.9c20.2 15.4 25.9 43.3 13.5 65.5l-128 230.4c-9 16.2-26.1 26.4-44.7 26.4z m204.8-819.2c-56.6 0-102.4-45.8-102.4-102.4S698.6 0 755.2 0s102.4 45.8 102.4 102.4-45.8 102.4-102.4 102.4z" fill="#FFBA57" p-id="9214"></path><path d="M423.5 456.6l-132.3 209 72.5 77.7 186.1-210.9-126.3-75.8z" fill="#2D4C5C" p-id="9215"></path><path d="M723.7 248.7l-122.1-69.5c-5.9-3.6-12.8-5.5-19.7-5.5h-0.3L422.4 455.9 576 548.1l160.9-285.4c-3.1-5.7-7.6-10.5-13.2-14z" fill="#00ACC1" p-id="9216"></path><path d="M576 548.2l-153.6-92.3 25.9-45.9-38.7 68.8c-24.7 41.3-12.4 94.7 27.9 120.9l174.7 156.5 97.4-54.3L576 548v0.2z" fill="#546E7A" p-id="9217"></path></svg>

+ 2 - 0
src/App.vue

@@ -62,4 +62,6 @@ onMounted(() => {
   top: 50%;
   top: 50%;
   transform: translateY(-50%);
   transform: translateY(-50%);
 }
 }
+
+
 </style>
 </style>

+ 1 - 1
src/components/Footer/index.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
     <div class="footer">
     <div class="footer">
-        <span>© {{ new Date().getFullYear() }} CTBU_RunForge</span>
+        <span>© {{ new Date().getFullYear() }} RunForge - 自助乐跑平台</span>
     </div>
     </div>
 </template>
 </template>
 
 

+ 11 - 1
src/components/Menu/index.vue

@@ -29,9 +29,11 @@ import { onMounted, ref } from 'vue'
 import { routes } from '@/router'
 import { routes } from '@/router'
 import { useUserStore } from '@/store/modules/user'
 import { useUserStore } from '@/store/modules/user'
 import { useRouteListener } from '@/utils/route-listener'
 import { useRouteListener } from '@/utils/route-listener'
+import { isElectron } from '../../utils/electron'
 
 
 const userStore = useUserStore()
 const userStore = useUserStore()
 const user = ref({})
 const user = ref({})
+const electron  =ref(false)
 const menuData = ref([])
 const menuData = ref([])
 
 
 const { selectedKey } = useRouteListener()
 const { selectedKey } = useRouteListener()
@@ -42,9 +44,16 @@ const hasPermission = (route) => {
     return route.meta.permission.some((perm) => user.value.roles.includes(perm))
     return route.meta.permission.some((perm) => user.value.roles.includes(perm))
 }
 }
 
 
+const checkEnv = (route) => {
+  if (!route.meta) return true
+  if (route.meta.onlyWeb) return !electron.value
+  if (route.meta.onlyElectron) return !!electron.value
+  return true
+}
+
 const generateMenu = (routes, parentPath = '') => {
 const generateMenu = (routes, parentPath = '') => {
     return routes
     return routes
-        .filter((route) => hasPermission(route) && !(route.meta && route.meta.hideInMenu))
+        .filter((route) => hasPermission(route) && !(route.meta && route.meta.hideInMenu) && checkEnv(route))
         .map((route) => {
         .map((route) => {
             const fullPath = parentPath + route.path; // 确保子路由路径完整
             const fullPath = parentPath + route.path; // 确保子路由路径完整
             const menu = {
             const menu = {
@@ -64,6 +73,7 @@ const generateMenu = (routes, parentPath = '') => {
 
 
 onMounted(async () => {
 onMounted(async () => {
     user.value = await userStore.getInfo()
     user.value = await userStore.getInfo()
+    electron.value = isElectron()
     menuData.value = generateMenu(routes)
     menuData.value = generateMenu(routes)
 })
 })
 </script>
 </script>

+ 50 - 11
src/components/Navbar/index.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
   <div class="navbar">
   <div class="navbar">
     <div class="left-side">
     <div class="left-side">
-      <a-space style="cursor: pointer;" @click="$router.push('/')">
+      <a-space style="cursor: pointer; -webkit-app-region: no-drag;" @click="$router.push('/')">
         <img alt="RunForge" src="/logo.svg" height="35">
         <img alt="RunForge" src="/logo.svg" height="35">
         <a-typography-title :style="{ margin: 0, fontSize: '18px' }" :heading="5">
         <a-typography-title :style="{ margin: 0, fontSize: '18px' }" :heading="5">
           RunForge
           RunForge
@@ -10,7 +10,7 @@
     </div>
     </div>
 
 
     <ul class="right-side">
     <ul class="right-side">
-      <li>
+      <li v-if="!isElectron()">
         <a-tooltip content="首页">
         <a-tooltip content="首页">
           <a-button class="nav-btn" type="outline" :shape="'circle'" @click="$router.push('/')">
           <a-button class="nav-btn" type="outline" :shape="'circle'" @click="$router.push('/')">
             <template #icon>
             <template #icon>
@@ -20,10 +20,9 @@
         </a-tooltip>
         </a-tooltip>
       </li>
       </li>
 
 
-
-      <li>
+      <li v-if="avatar">
         <a-dropdown trigger="hover">
         <a-dropdown trigger="hover">
-          <a-avatar :size="32" :style="{ marginRight: '8px', cursor: 'pointer' }">
+          <a-avatar class="nav-btn" :size="32" :style="{ marginRight: '8px', cursor: 'pointer' }">
             <img alt="avatar" :src="avatar" />
             <img alt="avatar" :src="avatar" />
           </a-avatar>
           </a-avatar>
           <template #content>
           <template #content>
@@ -46,23 +45,41 @@
           </template>
           </template>
         </a-dropdown>
         </a-dropdown>
       </li>
       </li>
+
+      <div v-if="isElectron()">
+        <a-button class="actions" type="text" @click="minimizeWindow">
+          <template #icon>
+            <icon-minus />
+          </template>
+        </a-button>
+
+        <a-button class="actions" type="text" @click="maximizeWindow">
+          <template #icon>
+            <icon-fullscreen />
+          </template>
+        </a-button>
+
+        <a-button class="actions close" type="text" @click="closeWindow">
+          <template #icon>
+            <icon-close />
+          </template>
+        </a-button>
+      </div>
     </ul>
     </ul>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { computed, ref } from 'vue'
+import { computed } from 'vue'
 import { Message, Modal } from '@arco-design/web-vue'
 import { Message, Modal } from '@arco-design/web-vue'
 import { useUserStore } from '@/store'
 import { useUserStore } from '@/store'
+import { isElectron } from '@/utils/electron'
 
 
 const userStore = useUserStore();
 const userStore = useUserStore();
 
 
 const avatar = computed(() => {
 const avatar = computed(() => {
   return userStore.avatar;
   return userStore.avatar;
-});
-
-const refBtn = ref()
-const triggerBtn = ref()
+})
 
 
 const handleLogout = () => {
 const handleLogout = () => {
   Modal.confirm({
   Modal.confirm({
@@ -77,12 +94,23 @@ const handleLogout = () => {
     onCancel: () => {
     onCancel: () => {
 
 
     }
     }
-  });
+  })
+}
+
+function closeWindow() {
+  window.electron.closeWindow()  // 调用关闭窗口的方法
+}
+function minimizeWindow() {
+  window.electron.minimizeWindow()  // 调用最小化窗口的方法
+}
+function maximizeWindow() {
+  window.electron.maximizeWindow()  // 调用最大化窗口的方法
 }
 }
 </script>
 </script>
 
 
 <style scoped lang="less">
 <style scoped lang="less">
 .navbar {
 .navbar {
+  -webkit-app-region: drag;
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
   height: 100%;
   height: 100%;
@@ -122,6 +150,7 @@ const handleLogout = () => {
   }
   }
 
 
   .nav-btn {
   .nav-btn {
+    -webkit-app-region: no-drag;
     border-color: rgb(var(--gray-2));
     border-color: rgb(var(--gray-2));
     color: rgb(var(--gray-8));
     color: rgb(var(--gray-8));
     font-size: 16px;
     font-size: 16px;
@@ -137,4 +166,14 @@ const handleLogout = () => {
     margin-left: 14px;
     margin-left: 14px;
   }
   }
 }
 }
+
+.actions {
+  color: rgb(var(--gray-8));
+  -webkit-app-region: no-drag;
+}
+
+.close:hover {
+  background-color: rgb(var(--red-5));
+  color: white;
+}
 </style>
 </style>

+ 20 - 0
src/electron.css

@@ -0,0 +1,20 @@
+
+body::-webkit-scrollbar {
+  width: 0;
+}
+
+body::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+body::-webkit-scrollbar-thumb {
+  background-color: rgba(0, 0, 0, 0.5);
+  /* 滚动条滑块背景 */
+  border-radius: 10px;
+  /* 滚动条滑块圆角 */
+}
+
+body::-webkit-scrollbar-thumb:hover {
+  background-color: rgba(0, 0, 0, 0.3);
+  /* 滚动条滑块悬停时的背景 */
+} 

+ 56 - 0
src/layout/html-view.vue

@@ -0,0 +1,56 @@
+<template>
+  <a-layout class="layout">
+    <a-layout-header class="layout-navbar">
+      <Navbar />
+    </a-layout-header>
+    <a-layout class="layout-content">
+        <router-view class="page"/>
+        <Footer />
+    </a-layout>
+  </a-layout>
+</template>
+
+<script setup>
+import PageLayout from './page-layout.vue'
+import Navbar from '@/components/Navbar/index.vue'
+import Footer from '@/components/Footer/index.vue'
+</script>
+
+<style scoped lang="less">
+@nav-size-height: 60px;
+
+.layout {
+  width: 100%;
+  height: 100%;
+}
+
+.layout-navbar {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  width: 100%;
+  height: @nav-size-height;
+}
+
+.layout-content {
+  position: absolute;
+  top: @nav-size-height;
+  bottom: 0;
+  width: 100%;
+  height: calc(100% - @nav-size-height);
+  overflow-y: auto;
+  background-color: var(--color-fill-2);
+}
+
+.page {
+  height: 100%;
+  margin-bottom: 30px;
+}
+
+router-view {
+  display: block;
+  height: 100%;
+}
+
+</style>

+ 6 - 0
src/main.js

@@ -1,6 +1,12 @@
 import { createApp } from 'vue'
 import { createApp } from 'vue'
 import './style.css'
 import './style.css'
 
 
+import { isElectron } from '@/utils/electron'
+
+if (isElectron()) {
+  import('./electron.css')
+}
+
 import ArcoVueIcon from '@arco-design/web-vue/es/icon'
 import ArcoVueIcon from '@arco-design/web-vue/es/icon'
 
 
 import App from './App.vue'
 import App from './App.vue'

+ 6 - 3
src/pages/Login/Login.vue

@@ -1,6 +1,7 @@
 <template>
 <template>
     <div class="root">
     <div class="root">
-        <Header />
+        <Header v-if="!isElectron()"/>
+        <Navbar v-else style="position: relative; z-index: 999;"/>
         <Container />
         <Container />
         <CanvasBackend />
         <CanvasBackend />
         <Footer />
         <Footer />
@@ -10,8 +11,10 @@
 <script setup>
 <script setup>
 import CanvasBackend from '@/components/CanvasBackend/index.vue'
 import CanvasBackend from '@/components/CanvasBackend/index.vue'
 import Header from '@/components/Header/index.vue'
 import Header from '@/components/Header/index.vue'
+import Navbar from '@/components/Navbar/index.vue'
 import Container from './components/container.vue'
 import Container from './components/container.vue'
 import Footer from '@/components/Footer/index.vue'
 import Footer from '@/components/Footer/index.vue'
+import { isElectron } from '@/utils/electron'
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
@@ -27,12 +30,12 @@ import Footer from '@/components/Footer/index.vue'
     height: 300px;
     height: 300px;
     z-index: 2;
     z-index: 2;
     transform: translate(-50%, -50%);
     transform: translate(-50%, -50%);
-    pointer-events: auto; /* center 可以接收鼠标事件 */
+    pointer-events: auto;
+    /* center 可以接收鼠标事件 */
 }
 }
 
 
 .button {
 .button {
     position: absolute;
     position: absolute;
     z-index: 101;
     z-index: 101;
 }
 }
-
 </style>
 </style>

+ 10 - 2
src/pages/Login/uniLogin/uniLogin.vue

@@ -40,6 +40,7 @@ import { useRoute, useRouter } from 'vue-router'
 import { ref, onMounted } from 'vue'
 import { ref, onMounted } from 'vue'
 import { Notification } from '@arco-design/web-vue'
 import { Notification } from '@arco-design/web-vue'
 import { useUserStore } from '@/store/modules/user'
 import { useUserStore } from '@/store/modules/user'
+import { isElectron } from '@/utils/electron'
 
 
 const userStore = useUserStore()
 const userStore = useUserStore()
 
 
@@ -56,8 +57,12 @@ const GetLoginUrl = async (type) => {
         const res = await getLoginUrl({ type })
         const res = await getLoginUrl({ type })
         if (!res || res.code != 0)
         if (!res || res.code != 0)
             return requestFailed('获取登录链接失败!' + res?.msg ?? '')
             return requestFailed('获取登录链接失败!' + res?.msg ?? '')
-        window.location.href = res.data
+        if (isElectron())
+            window.electron.openNewWindow(res.data)
+        else
+            window.location.href = res.data
     } catch (error) {
     } catch (error) {
+        console.log(error)
         requestFailed('获取登录链接失败!')
         requestFailed('获取登录链接失败!')
     } finally {
     } finally {
         changeLoading(false)
         changeLoading(false)
@@ -90,7 +95,10 @@ const loginSuccess = (res) => {
         content: '欢迎回来!',
         content: '欢迎回来!',
         duration: 2000
         duration: 2000
     })
     })
-    router.push(from || '/')
+    if (isElectron())
+        window.electron.openOldWindow(from || '/')
+    else
+        router.push(from || '/')
 }
 }
 
 
 const requestFailed = (msg) => {
 const requestFailed = (msg) => {

+ 12 - 0
src/pages/htmlView/index.vue

@@ -0,0 +1,12 @@
+<template>
+  <div style="width: 100%; height: 100%">
+    <iframe :src="url" style="width: 100%; height: 100%; border: none;"></iframe>
+  </div>
+</template>
+
+<script setup>
+import { useRoute } from 'vue-router'
+const route = useRoute()
+const url = route.query.url
+console.log(url)
+</script>

+ 5 - 5
src/pages/lepao/lepaoRecords/index.vue

@@ -72,7 +72,7 @@
             slotName: 'name-filter',
             slotName: 'name-filter',
             icon: () => h(IconSearch)
             icon: () => h(IconSearch)
           }"></a-table-column>
           }"></a-table-column>
-          <a-table-column title="账号名称" :filterable="{
+          <a-table-column title="账号名称" :width="140" :filterable="{
             filter: (value, record) => (record.name ?? '').includes(value),
             filter: (value, record) => (record.name ?? '').includes(value),
             slotName: 'name-filter',
             slotName: 'name-filter',
             icon: () => h(IconSearch)
             icon: () => h(IconSearch)
@@ -95,22 +95,22 @@
             filter: (value, record) => (record.result.pass_tit ?? '').includes(value),
             filter: (value, record) => (record.result.pass_tit ?? '').includes(value),
             slotName: 'name-filter',
             slotName: 'name-filter',
             icon: () => h(IconSearch)
             icon: () => h(IconSearch)
-          }">
+          }" :width="220">
             <template #cell="{ record }">
             <template #cell="{ record }">
               {{ record.result.pass_tit }}
               {{ record.result.pass_tit }}
             </template>
             </template>
           </a-table-column>
           </a-table-column>
-          <a-table-column title="乐跑距离" :width="120" ellipsis tooltip>
+          <a-table-column title="乐跑距离" :width="110" ellipsis tooltip>
             <template #cell="{ record }">
             <template #cell="{ record }">
               {{ record.result.distance }} Km
               {{ record.result.distance }} Km
             </template>
             </template>
           </a-table-column>
           </a-table-column>
-          <a-table-column title="跑步时长" :width="120" ellipsis tooltip>
+          <a-table-column title="跑步时长" :width="110" ellipsis tooltip>
             <template #cell="{ record }">
             <template #cell="{ record }">
               {{ formatSecondsToMinSec(record.result.time) }}
               {{ formatSecondsToMinSec(record.result.time) }}
             </template>
             </template>
           </a-table-column>
           </a-table-column>
-          <a-table-column title="平均配速" :width="120" ellipsis tooltip>
+          <a-table-column title="平均配速" :width="110" ellipsis tooltip>
             <template #cell="{ record }">
             <template #cell="{ record }">
               {{ calculatePace(record.result.time, record.result.distance) }}
               {{ calculatePace(record.result.time, record.result.distance) }}
             </template>
             </template>

+ 32 - 4
src/router/index.js

@@ -1,7 +1,9 @@
 import * as VueRouter from 'vue-router'
 import * as VueRouter from 'vue-router'
 import { useUserStore } from '@/store'
 import { useUserStore } from '@/store'
+import { isElectron } from '../utils/electron'
 const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue')
 const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue')
-import { Message } from '@arco-design/web-vue'
+const HTML_VIEW = () => import('@/layout/html-view.vue')
+// import { Message } from '@arco-design/web-vue'
 
 
 const routes = [
 const routes = [
     {
     {
@@ -9,7 +11,8 @@ const routes = [
         name: "main",
         name: "main",
         component: () => import('../pages/Main/Main.vue'),
         component: () => import('../pages/Main/Main.vue'),
         meta: {
         meta: {
-            hideInMenu: true
+            hideInMenu: true,
+            onlyWeb: true
         }
         }
     },
     },
     {
     {
@@ -21,6 +24,25 @@ const routes = [
             hideInMenu: true
             hideInMenu: true
         }
         }
     },
     },
+    {
+        path: "/htmlView",
+        name: "htmlView",
+        component: HTML_VIEW,
+        meta: {
+            title: '第三方页面',
+            hideInMenu: true
+        },
+        children: [
+            {
+                path: 'view',
+                name: 'htmlView.view',
+                component: () => import('../pages/htmlView/index.vue'),
+                meta: {
+                    title: '第三方页面'
+                }
+            }
+        ]
+    },
     {
     {
         path: "/store",
         path: "/store",
         name: 'store',
         name: 'store',
@@ -127,7 +149,7 @@ const routes = [
                 name: 'service.orderList',
                 name: 'service.orderList',
                 component: () => import('../pages/service/orderList.vue'),
                 component: () => import('../pages/service/orderList.vue'),
                 meta: {
                 meta: {
-                    title: '工单列表'
+                    title: '我的工单'
                 }
                 }
             },
             },
             {
             {
@@ -277,7 +299,7 @@ const router = VueRouter.createRouter({
     routes: routes
     routes: routes
 })
 })
 
 
-const allow = ['/', '/login']
+const allow = ['/', '/login', '/htmlView/view']
 
 
 router.beforeEach(async (to, from, next) => {
 router.beforeEach(async (to, from, next) => {
     if (!allow.includes(to.path)) {
     if (!allow.includes(to.path)) {
@@ -290,6 +312,12 @@ router.beforeEach(async (to, from, next) => {
         }
         }
     }
     }
 
 
+    if (to.meta && (to.meta.onlyWeb || to.meta.onlyElectron)) {
+        const electronEnv = isElectron()
+        if (to.meta.onlyWeb && electronEnv) return next('/lepao/accountList')
+        if (to.meta.onlyElectron && !electronEnv) return next('/lepao/accountList')
+    }
+
     if (!to.meta.title) {
     if (!to.meta.title) {
         document.title = 'RunForge - 让跑步的意义由技术重新定义'
         document.title = 'RunForge - 让跑步的意义由技术重新定义'
     } else {
     } else {

+ 3 - 0
src/utils/electron.js

@@ -0,0 +1,3 @@
+export function isElectron() {
+  return !!(window && window.electron);
+}