mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-10 18:05:48 +02:00
feat: workspace
This commit is contained in:
parent
82da0ef05e
commit
df95386088
13 changed files with 833 additions and 669 deletions
7
app/env.d.ts
vendored
7
app/env.d.ts
vendored
|
@ -1,3 +1,10 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
// Extend Window interface
|
||||
interface Window {
|
||||
inWorkspace?: boolean
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
import type React from 'react'
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
color: #fff;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<title>Nginx UI</title>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
1257
app/pnpm-lock.yaml
generated
1257
app/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -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
app/src/global.d.ts
vendored
Normal file
4
app/src/global.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
// This file is used to extend global interfaces
|
||||
declare interface Window {
|
||||
inWorkspace?: boolean
|
||||
}
|
|
@ -33,7 +33,7 @@ function getClientWidth() {
|
|||
}
|
||||
|
||||
function collapse() {
|
||||
return getClientWidth() < 1280
|
||||
return getClientWidth() < 1080
|
||||
}
|
||||
|
||||
const { server_name } = storeToRefs(useSettingsStore())
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
|
@ -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
app/src/types.d.ts
vendored
4
app/src/types.d.ts
vendored
|
@ -1 +1,5 @@
|
|||
export type CheckedType = boolean | string | number
|
||||
|
||||
interface Window {
|
||||
inWorkspace?: boolean
|
||||
}
|
||||
|
|
141
app/src/views/workspace/WorkSpace.vue
Normal file
141
app/src/views/workspace/WorkSpace.vue
Normal file
|
@ -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>
|
|
@ -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()
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue