Browse Source

feat: workspace

Jacky 2 weeks ago
parent
commit
df95386088

+ 7 - 0
app/env.d.ts

@@ -1,3 +1,10 @@
+/// <reference types="vite/client" />
+
+// Extend Window interface
+interface Window {
+  inWorkspace?: boolean
+}
+
 declare module '*.svg' {
   import type React from 'react'
 

+ 1 - 1
app/index.html

@@ -14,7 +14,7 @@
       color: #fff;
     }
     #app {
-      height: 100%;
+      height: 100vh;
     }
 	</style>
 	<title>Nginx UI</title>

+ 7 - 7
app/package.json

@@ -2,14 +2,14 @@
   "name": "nginx-ui-app-next",
   "type": "module",
   "version": "2.0.0-rc.5",
-  "packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677",
+  "packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6",
   "scripts": {
     "dev": "vite --host",
     "typecheck": "vue-tsc --noEmit",
     "lint": "eslint .",
     "lint:fix": "eslint --fix .",
     "build": "vite build",
-    "preview": "vite preview --host",
+    "preview": "vite preview",
     "gettext:extract": "vue-gettext-extract"
   },
   "dependencies": {
@@ -25,7 +25,6 @@
     "@xterm/addon-attach": "^0.11.0",
     "@xterm/addon-fit": "^0.10.0",
     "@xterm/xterm": "^5.5.0",
-    "ace-builds": "^1.40.0",
     "ant-design-vue": "^4.2.6",
     "apexcharts": "^4.5.0",
     "axios": "^1.8.4",
@@ -40,10 +39,10 @@
     "pinia-plugin-persistedstate": "^4.2.0",
     "reconnecting-websocket": "^4.4.0",
     "sortablejs": "^1.15.6",
+    "splitpanes": "^4.0.3",
     "sse.js": "^2.6.0",
     "universal-cookie": "^8.0.1",
     "unocss": "^66.0.0",
-    "uuid": "^11.1.0",
     "vite-plugin-build-id": "0.5.0",
     "vue": "^3.5.13",
     "vue-dompurify-html": "^5.2.0",
@@ -69,16 +68,17 @@
     "@vitejs/plugin-vue-jsx": "^4.1.2",
     "@vue/compiler-sfc": "^3.5.13",
     "@vue/tsconfig": "^0.7.0",
+    "ace-builds": "^1.40.0",
     "autoprefixer": "^10.4.21",
-    "eslint": "9.24.0",
+    "eslint": "9.23.0",
     "eslint-plugin-sonarjs": "^3.0.2",
     "less": "^4.3.0",
     "postcss": "^8.5.3",
-    "typescript": "5.8.3",
+    "typescript": "5.8.2",
     "unplugin-auto-import": "^19.1.2",
     "unplugin-vue-components": "^28.5.0",
     "unplugin-vue-define-options": "^1.5.5",
-    "vite": "^6.3.0",
+    "vite": "^6.3.2",
     "vite-svg-loader": "^5.1.0",
     "vue-tsc": "^2.2.8"
   }

File diff suppressed because it is too large
+ 240 - 252
app/pnpm-lock.yaml


+ 9 - 0
app/src/App.vue

@@ -9,6 +9,7 @@ import zh_TW from 'ant-design-vue/es/locale/zh_TW'
 // This starter template is using Vue 3 <script setup> SFCs
 // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
 import { computed, provide } from 'vue'
+import router from './routes'
 
 const route = useRoute()
 
@@ -52,6 +53,14 @@ const settings = useSettingsStore()
 const is_theme_dark = computed(() => settings.theme === 'dark')
 
 loadTranslations(route)
+
+watch(route, () => {
+  settings.route_path = route.path
+})
+
+onMounted(() => {
+  router.push(settings.route_path)
+})
 </script>
 
 <template>

+ 4 - 0
app/src/global.d.ts

@@ -0,0 +1,4 @@
+// This file is used to extend global interfaces
+declare interface Window {
+  inWorkspace?: boolean
+} 

+ 1 - 1
app/src/layouts/BaseLayout.vue

@@ -33,7 +33,7 @@ function getClientWidth() {
 }
 
 function collapse() {
-  return getClientWidth() < 1280
+  return getClientWidth() < 1080
 }
 
 const { server_name } = storeToRefs(useSettingsStore())

+ 22 - 3
app/src/layouts/HeaderLayout.vue

@@ -5,7 +5,7 @@ import NginxControl from '@/components/NginxControl/NginxControl.vue'
 import Notification from '@/components/Notification/Notification.vue'
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
-import { HomeOutlined, LogoutOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue'
+import { DesktopOutlined, HomeOutlined, LogoutOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue'
 import { message } from 'ant-design-vue'
 import { useRouter } from 'vue-router'
 
@@ -24,6 +24,10 @@ function logout() {
 }
 
 const headerRef = useTemplateRef('headerRef') as Readonly<ShallowRef<HTMLDivElement>>
+
+const isWorkspace = computed(() => {
+  return !!window.inWorkspace
+})
 </script>
 
 <template>
@@ -31,12 +35,19 @@ const headerRef = useTemplateRef('headerRef') as Readonly<ShallowRef<HTMLDivElem
     <div class="tool">
       <MenuUnfoldOutlined @click="emit('clickUnFold')" />
     </div>
+    <div v-if="!isWorkspace" class="workspace-entry">
+      <RouterLink to="/workspace">
+        <ATooltip :title="$gettext('Workspace')">
+          <DesktopOutlined />
+        </ATooltip>
+      </RouterLink>
+    </div>
 
     <ASpace
       class="user-wrapper"
       :size="24"
     >
-      <SetLanguage class="set_lang" />
+      <SetLanguage v-if="!isWorkspace" class="set_lang" />
 
       <SwitchAppearance />
 
@@ -48,7 +59,7 @@ const headerRef = useTemplateRef('headerRef') as Readonly<ShallowRef<HTMLDivElem
         <HomeOutlined />
       </a>
 
-      <a @click="logout">
+      <a v-if="!isWorkspace" @click="logout">
         <LogoutOutlined />
       </a>
     </ASpace>
@@ -86,6 +97,14 @@ const headerRef = useTemplateRef('headerRef') as Readonly<ShallowRef<HTMLDivElem
   }
 }
 
+.workspace-entry {
+  position: absolute;
+  left: 20px;
+  @media (max-width: 600px) {
+    display: none;
+  }
+}
+
 .user-wrapper {
   position: absolute;
   right: 28px;

+ 12 - 1
app/src/pinia/moudule/settings.ts

@@ -10,6 +10,7 @@ export const useSettingsStore = defineStore('settings', {
       name: 'Local',
     },
     server_name: '',
+    route_path: '',
   }),
   getters: {
     is_remote(): boolean {
@@ -32,5 +33,15 @@ export const useSettingsStore = defineStore('settings', {
       this.environment.name = 'Local'
     },
   },
-  persist: true,
+  persist: [
+    {
+      key: `LOCAL_${window.name || 'main'}`,
+      storage: localStorage,
+      pick: ['environment', 'server_name', 'route_path'],
+    },
+    {
+      storage: localStorage,
+      pick: ['language', 'theme', 'preference_theme'],
+    },
+  ],
 })

+ 8 - 0
app/src/routes/index.ts

@@ -48,6 +48,14 @@ export const routes: RouteRecordRaw[] = [
     },
     children: mainLayoutChildren,
   },
+  {
+    path: '/workspace',
+    name: 'Workspace',
+    component: () => import('@/views/workspace/WorkSpace.vue'),
+    meta: {
+      name: () => $gettext('Workspace'),
+    },
+  },
   ...authRoutes,
   ...errorRoutes,
 ]

+ 4 - 0
app/src/types.d.ts

@@ -1 +1,5 @@
 export type CheckedType = boolean | string | number
+
+interface Window {
+  inWorkspace?: boolean
+}

+ 141 - 0
app/src/views/workspace/WorkSpace.vue

@@ -0,0 +1,141 @@
+<script lang="ts" setup>
+import { CloseOutlined } from '@ant-design/icons-vue'
+import { Pane, Splitpanes } from 'splitpanes'
+import { useRouter } from 'vue-router'
+import 'splitpanes/dist/splitpanes.css'
+
+const router = useRouter()
+
+const src = computed(() => {
+  return location.pathname
+})
+
+const paneSize = ref(localStorage.paneSize ?? 50) // Read from persistent localStorage.
+function storePaneSize({ prevPane }) {
+  localStorage.paneSize = prevPane.size // Store in persistent localStorage.
+}
+
+function closeSplitView() {
+  router.push('/')
+}
+
+const leftFrame = useTemplateRef('leftFrame')
+const rightFrame = useTemplateRef('rightFrame')
+
+function handleLoad(iframeRef: HTMLIFrameElement | null) {
+  if (!iframeRef) {
+    return
+  }
+
+  iframeRef.addEventListener('load', () => {
+    if (iframeRef.contentWindow) {
+      iframeRef.contentWindow.inWorkspace = true
+    }
+  })
+}
+
+onMounted(() => {
+  handleLoad(leftFrame.value)
+  handleLoad(rightFrame.value)
+})
+</script>
+
+<template>
+  <div class="h-100vh macos-window">
+    <div class="macos-titlebar flex items-center p-2 relative">
+      <div class="traffic-lights flex ml-2">
+        <div class="traffic-light close" @click="closeSplitView">
+          <CloseOutlined class="traffic-icon" />
+        </div>
+      </div>
+      <div class="window-title absolute left-0 right-0 text-center">
+        {{ $gettext('Workspace') }}
+      </div>
+    </div>
+
+    <Splitpanes class="default-theme split-container" @resized="storePaneSize">
+      <Pane :size="paneSize" :min-size="20">
+        <iframe ref="leftFrame" name="split-view-left" :src class="w-full h-full iframe-no-border" />
+      </Pane>
+      <Pane :size="100 - paneSize">
+        <iframe ref="rightFrame" name="split-view-right" :src class="w-full h-full iframe-no-border" />
+      </Pane>
+    </Splitpanes>
+  </div>
+</template>
+
+<style scoped>
+.macos-window {
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+.macos-titlebar {
+  background: linear-gradient(to bottom, #f9f9f9, #ececec);
+  height: 32px;
+  border-bottom: 1px solid #e1e1e1;
+  -webkit-app-region: drag;
+  user-select: none;
+}
+
+.dark .macos-titlebar {
+  background: linear-gradient(to bottom, #323232, #282828);
+  border-bottom: 1px solid #3a3a3a;
+}
+
+.split-container {
+  height: calc(100vh - 32px);
+}
+
+.traffic-lights {
+  -webkit-app-region: no-drag;
+  z-index: 10;
+}
+
+.traffic-light {
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  margin-right: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+}
+
+.traffic-light.close {
+  background-color: #ff5f57;
+  border: 1px solid #e0443e;
+}
+
+.traffic-icon {
+  opacity: 0;
+  font-size: 9px;
+  color: rgba(0, 0, 0, 0.5);
+}
+
+.traffic-light:hover .traffic-icon {
+  opacity: 1;
+}
+
+.window-title {
+  font-size: 13px;
+  font-weight: 500;
+  color: #333;
+  pointer-events: none;
+}
+
+.dark .window-title {
+  color: #e0e0e0;
+}
+
+:deep(.splitpanes__splitter) {
+  background-color: #ececec !important;
+}
+
+.iframe-no-border {
+  border: none;
+  outline: none;
+}
+</style>

+ 0 - 16
app/vite.config.ts

@@ -1,4 +1,3 @@
-import { Agent } from 'node:http'
 import { fileURLToPath, URL } from 'node:url'
 import vue from '@vitejs/plugin-vue'
 import vueJsx from '@vitejs/plugin-vue-jsx'
@@ -83,21 +82,6 @@ export default defineConfig(({ mode }) => {
           secure: false,
           ws: true,
           timeout: 60000,
-          agent: new Agent({
-            keepAlive: false,
-          }),
-          onProxyReq(proxyReq, req) {
-            proxyReq.setHeader('Connection', 'keep-alive')
-            if (req.headers.accept === 'text/event-stream') {
-              proxyReq.setHeader('Cache-Control', 'no-cache')
-              proxyReq.setHeader('Content-Type', 'text/event-stream')
-            }
-          },
-          onProxyReqWs(proxyReq, req, socket) {
-            socket.on('close', () => {
-              proxyReq.destroy()
-            })
-          },
         },
       },
     },

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