This commit is contained in:
77
web/auto-imports.d.ts
vendored
Normal file
77
web/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const computed: typeof import('vue').computed
|
||||
const createApp: typeof import('vue').createApp
|
||||
const customRef: typeof import('vue').customRef
|
||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||
const defineComponent: typeof import('vue').defineComponent
|
||||
const effectScope: typeof import('vue').effectScope
|
||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||
const h: typeof import('vue').h
|
||||
const inject: typeof import('vue').inject
|
||||
const isProxy: typeof import('vue').isProxy
|
||||
const isReactive: typeof import('vue').isReactive
|
||||
const isReadonly: typeof import('vue').isReadonly
|
||||
const isRef: typeof import('vue').isRef
|
||||
const isShallow: typeof import('vue').isShallow
|
||||
const markRaw: typeof import('vue').markRaw
|
||||
const nextTick: typeof import('vue').nextTick
|
||||
const onActivated: typeof import('vue').onActivated
|
||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||
const onDeactivated: typeof import('vue').onDeactivated
|
||||
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||
const onMounted: typeof import('vue').onMounted
|
||||
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||
const onUnmounted: typeof import('vue').onUnmounted
|
||||
const onUpdated: typeof import('vue').onUpdated
|
||||
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||
const provide: typeof import('vue').provide
|
||||
const reactive: typeof import('vue').reactive
|
||||
const readonly: typeof import('vue').readonly
|
||||
const ref: typeof import('vue').ref
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const shallowReactive: typeof import('vue').shallowReactive
|
||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||
const shallowRef: typeof import('vue').shallowRef
|
||||
const toRaw: typeof import('vue').toRaw
|
||||
const toRef: typeof import('vue').toRef
|
||||
const toRefs: typeof import('vue').toRefs
|
||||
const toValue: typeof import('vue').toValue
|
||||
const triggerRef: typeof import('vue').triggerRef
|
||||
const unref: typeof import('vue').unref
|
||||
const useAttrs: typeof import('vue').useAttrs
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useDialog: typeof import('naive-ui').useDialog
|
||||
const useId: typeof import('vue').useId
|
||||
const useLoadingBar: typeof import('naive-ui').useLoadingBar
|
||||
const useMessage: typeof import('naive-ui').useMessage
|
||||
const useModel: typeof import('vue').useModel
|
||||
const useNotification: typeof import('naive-ui').useNotification
|
||||
const useSlots: typeof import('vue').useSlots
|
||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||
const watch: typeof import('vue').watch
|
||||
const watchEffect: typeof import('vue').watchEffect
|
||||
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
18
web/components.d.ts
vendored
Normal file
18
web/components.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
619
web/package-lock.json
generated
619
web/package-lock.json
generated
@@ -8,7 +8,9 @@
|
||||
"name": "webui",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"axios": "^1.13.2",
|
||||
"naive-ui": "^2.43.2",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
@@ -17,6 +19,8 @@
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.2.4",
|
||||
"vue-tsc": "^3.1.4"
|
||||
}
|
||||
@@ -67,6 +71,30 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@css-render/plugin-bem": {
|
||||
"version": "0.15.14",
|
||||
"resolved": "https://registry.npmjs.org/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz",
|
||||
"integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"css-render": "~0.15.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@css-render/vue3-ssr": {
|
||||
"version": "0.15.14",
|
||||
"resolved": "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz",
|
||||
"integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/hash": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||
@@ -509,12 +537,61 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@juggle/resize-observer": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.53",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
||||
@@ -837,6 +914,27 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/katex": {
|
||||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
|
||||
"integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
|
||||
@@ -848,6 +946,12 @@
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vicons/ionicons5": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vicons/ionicons5/-/ionicons5-0.13.0.tgz",
|
||||
"integrity": "sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz",
|
||||
@@ -1035,6 +1139,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
|
||||
@@ -1042,6 +1159,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -1072,6 +1195,22 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -1084,12 +1223,74 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
|
||||
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-render": {
|
||||
"version": "0.15.14",
|
||||
"resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.14.tgz",
|
||||
"integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emotion/hash": "~0.8.0",
|
||||
"csstype": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/css-render/node_modules/csstype": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
|
||||
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns-tz": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
|
||||
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"date-fns": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -1212,12 +1413,38 @@
|
||||
"@esbuild/win32-x64": "0.27.2"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
|
||||
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/evtd": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz",
|
||||
"integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -1384,6 +1611,52 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/local-pkg": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
||||
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mlly": "^1.7.4",
|
||||
"pkg-types": "^2.3.0",
|
||||
"quansync": "^0.2.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.22",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
|
||||
"integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -1423,6 +1696,45 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
|
||||
"integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0",
|
||||
"pathe": "^2.0.3",
|
||||
"pkg-types": "^1.3.1",
|
||||
"ufo": "^1.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly/node_modules/confbox": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mlly/node_modules/pkg-types": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.8",
|
||||
"mlly": "^1.7.4",
|
||||
"pathe": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
@@ -1430,6 +1742,36 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/naive-ui": {
|
||||
"version": "2.43.2",
|
||||
"resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.43.2.tgz",
|
||||
"integrity": "sha512-YlLMnGrwGTOc+zMj90sG3ubaH5/7czsgLgGcjTLA981IUaz8r6t4WIujNt8r9PNr+dqv6XNEr0vxkARgPPjfBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@css-render/plugin-bem": "^0.15.14",
|
||||
"@css-render/vue3-ssr": "^0.15.14",
|
||||
"@types/katex": "^0.16.2",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"async-validator": "^4.2.5",
|
||||
"css-render": "^0.15.14",
|
||||
"csstype": "^3.1.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"evtd": "^0.2.4",
|
||||
"highlight.js": "^11.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"seemly": "^0.3.10",
|
||||
"treemate": "^0.3.11",
|
||||
"vdirs": "^0.1.8",
|
||||
"vooks": "^0.2.12",
|
||||
"vueuc": "^0.4.65"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -1455,6 +1797,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1467,7 +1816,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -1475,6 +1823,18 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.2",
|
||||
"exsolve": "^1.0.7",
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -1509,6 +1869,37 @@
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
|
||||
@@ -1551,6 +1942,19 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/scule": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
|
||||
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/seemly": {
|
||||
"version": "0.3.10",
|
||||
"resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.10.tgz",
|
||||
"integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1560,6 +1964,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
||||
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^9.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@@ -1577,6 +1994,12 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/treemate": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/treemate/-/treemate-0.3.11.tgz",
|
||||
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -1592,6 +2015,13 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
|
||||
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
@@ -1599,6 +2029,156 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unimport": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.6.0.tgz",
|
||||
"integrity": "sha512-8rqAmtJV8o60x46kBAJKtHpJDJWkA2xcBqWKPI14MgUb05o1pnpnCnXSxedUXyeq7p8fR5g3pTo2BaswZ9lD9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"estree-walker": "^3.0.3",
|
||||
"local-pkg": "^1.1.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"mlly": "^1.8.0",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"pkg-types": "^2.3.0",
|
||||
"scule": "^1.3.0",
|
||||
"strip-literal": "^3.1.0",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"unplugin": "^2.3.11",
|
||||
"unplugin-utils": "^0.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unimport/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin": {
|
||||
"version": "2.3.11",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"acorn": "^8.15.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"webpack-virtual-modules": "^0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin-auto-import": {
|
||||
"version": "20.3.0",
|
||||
"resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-20.3.0.tgz",
|
||||
"integrity": "sha512-RcSEQiVv7g0mLMMXibYVKk8mpteKxvyffGuDKqZZiFr7Oq3PB1HwgHdK5O7H4AzbhzHoVKG0NnMnsk/1HIVYzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"local-pkg": "^1.1.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"picomatch": "^4.0.3",
|
||||
"unimport": "^5.5.0",
|
||||
"unplugin": "^2.3.11",
|
||||
"unplugin-utils": "^0.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nuxt/kit": "^4.0.0",
|
||||
"@vueuse/core": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nuxt/kit": {
|
||||
"optional": true
|
||||
},
|
||||
"@vueuse/core": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin-utils": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
|
||||
"integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin-vue-components": {
|
||||
"version": "30.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-30.0.0.tgz",
|
||||
"integrity": "sha512-4qVE/lwCgmdPTp6h0qsRN2u642tt4boBQtcpn4wQcWZAsr8TQwq+SPT3NDu/6kBFxzo/sSEK4ioXhOOBrXc3iw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
"debug": "^4.4.3",
|
||||
"local-pkg": "^1.1.2",
|
||||
"magic-string": "^0.30.19",
|
||||
"mlly": "^1.8.0",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"unplugin": "^2.3.10",
|
||||
"unplugin-utils": "^0.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/parser": "^7.15.8",
|
||||
"@nuxt/kit": "^3.2.2 || ^4.0.0",
|
||||
"vue": "2 || 3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/parser": {
|
||||
"optional": true
|
||||
},
|
||||
"@nuxt/kit": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vdirs": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
|
||||
"integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"evtd": "^0.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||
@@ -1675,6 +2255,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vooks": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/vooks/-/vooks-0.2.12.tgz",
|
||||
"integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"evtd": "^0.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||
@@ -1735,6 +2327,31 @@
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vueuc": {
|
||||
"version": "0.4.65",
|
||||
"resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.65.tgz",
|
||||
"integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@css-render/vue3-ssr": "^0.15.10",
|
||||
"@juggle/resize-observer": "^3.3.1",
|
||||
"css-render": "^0.15.10",
|
||||
"evtd": "^0.2.4",
|
||||
"seemly": "^0.3.6",
|
||||
"vdirs": "^0.1.4",
|
||||
"vooks": "^0.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-virtual-modules": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"axios": "^1.13.2",
|
||||
"naive-ui": "^2.43.2",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
@@ -18,6 +20,8 @@
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.2.4",
|
||||
"vue-tsc": "^3.1.4"
|
||||
}
|
||||
|
||||
113
web/src/App.vue
113
web/src/App.vue
@@ -1,12 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { getServerStatus } from './api'
|
||||
import { ref, onMounted, computed, h } from 'vue'
|
||||
import { RouterView, useRouter, useRoute } from 'vue-router'
|
||||
import { NLayout, NLayoutHeader, NLayoutContent, NMenu, NButton, NSpace, NTag, NIcon, NConfigProvider, NMessageProvider, NDialogProvider } from 'naive-ui'
|
||||
import { HomeOutline, ExtensionPuzzleOutline, LogOutOutline } from '@vicons/ionicons5'
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
import { getServerStatus, removeToken } from './api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const serverInfo = ref({ bind_addr: '', bind_port: 0 })
|
||||
const clientCount = ref(0)
|
||||
|
||||
const isLoginPage = computed(() => route.path === '/login')
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
{
|
||||
label: '客户端',
|
||||
key: '/',
|
||||
icon: () => h(NIcon, null, { default: () => h(HomeOutline) })
|
||||
},
|
||||
{
|
||||
label: '插件',
|
||||
key: '/plugins',
|
||||
icon: () => h(NIcon, null, { default: () => h(ExtensionPuzzleOutline) })
|
||||
}
|
||||
]
|
||||
|
||||
const activeKey = computed(() => {
|
||||
if (route.path.startsWith('/client/')) return '/'
|
||||
return route.path
|
||||
})
|
||||
|
||||
const handleMenuUpdate = (key: string) => {
|
||||
router.push(key)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (isLoginPage.value) return
|
||||
try {
|
||||
const { data } = await getServerStatus()
|
||||
serverInfo.value = data.server
|
||||
@@ -15,41 +45,50 @@ onMounted(async () => {
|
||||
console.error('Failed to get server status', e)
|
||||
}
|
||||
})
|
||||
|
||||
const logout = () => {
|
||||
removeToken()
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app">
|
||||
<header class="header">
|
||||
<h1>GoTunnel 控制台</h1>
|
||||
<div class="server-info">
|
||||
<span>{{ serverInfo.bind_addr }}:{{ serverInfo.bind_port }}</span>
|
||||
<span class="badge">{{ clientCount }} 客户端</span>
|
||||
</div>
|
||||
</header>
|
||||
<main class="main">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
<n-config-provider>
|
||||
<n-dialog-provider>
|
||||
<n-message-provider>
|
||||
<n-layout v-if="!isLoginPage" style="min-height: 100vh;">
|
||||
<n-layout-header bordered style="height: 64px; padding: 0 24px; display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center; gap: 32px;">
|
||||
<div style="font-size: 20px; font-weight: 600; color: #18a058; cursor: pointer;" @click="router.push('/')">
|
||||
GoTunnel
|
||||
</div>
|
||||
<n-menu
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
:value="activeKey"
|
||||
@update:value="handleMenuUpdate"
|
||||
/>
|
||||
</div>
|
||||
<n-space align="center" :size="16">
|
||||
<n-tag type="info" round>
|
||||
{{ serverInfo.bind_addr }}:{{ serverInfo.bind_port }}
|
||||
</n-tag>
|
||||
<n-tag type="success" round>
|
||||
{{ clientCount }} 客户端
|
||||
</n-tag>
|
||||
<n-button quaternary circle @click="logout">
|
||||
<template #icon>
|
||||
<n-icon><LogOutOutline /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-layout-header>
|
||||
<n-layout-content content-style="padding: 24px; max-width: 1200px; margin: 0 auto; width: 100%;">
|
||||
<RouterView />
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
<RouterView v-else />
|
||||
</n-message-provider>
|
||||
</n-dialog-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app { min-height: 100vh; background: #f5f7fa; }
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header h1 { font-size: 20px; color: #2c3e50; }
|
||||
.server-info { display: flex; align-items: center; gap: 12px; color: #666; }
|
||||
.badge {
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.main { padding: 24px; max-width: 1200px; margin: 0 auto; }
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,44 @@
|
||||
import axios from 'axios'
|
||||
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus } from '../types'
|
||||
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo } from '../types'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
// Token 管理
|
||||
const TOKEN_KEY = 'gotunnel_token'
|
||||
|
||||
export const getToken = () => localStorage.getItem(TOKEN_KEY)
|
||||
export const setToken = (token: string) => localStorage.setItem(TOKEN_KEY, token)
|
||||
export const removeToken = () => localStorage.removeItem(TOKEN_KEY)
|
||||
|
||||
// 请求拦截器:添加 token
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// 响应拦截器:处理 401
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
removeToken()
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 认证 API
|
||||
export const login = (username: string, password: string) =>
|
||||
api.post<{ token: string }>('/auth/login', { username, password })
|
||||
export const checkAuth = () => api.get('/auth/check')
|
||||
|
||||
export const getServerStatus = () => api.get<ServerStatus>('/status')
|
||||
export const getClients = () => api.get<ClientStatus[]>('/clients')
|
||||
export const getClient = (id: string) => api.get<ClientDetail>(`/client/${id}`)
|
||||
@@ -14,4 +47,15 @@ export const updateClient = (id: string, client: ClientConfig) => api.put(`/clie
|
||||
export const deleteClient = (id: string) => api.delete(`/client/${id}`)
|
||||
export const reloadConfig = () => api.post('/config/reload')
|
||||
|
||||
// 客户端控制
|
||||
export const pushConfigToClient = (id: string) => api.post(`/client/${id}/push`)
|
||||
export const disconnectClient = (id: string) => api.post(`/client/${id}/disconnect`)
|
||||
export const installPluginsToClient = (id: string, plugins: string[]) =>
|
||||
api.post(`/client/${id}/install-plugins`, { plugins })
|
||||
|
||||
// 插件管理
|
||||
export const getPlugins = () => api.get<PluginInfo[]>('/plugins')
|
||||
export const enablePlugin = (name: string) => api.post(`/plugin/${name}/enable`)
|
||||
export const disablePlugin = (name: string) => api.post(`/plugin/${name}/disable`)
|
||||
|
||||
export default api
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import naive from 'naive-ui'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.use(naive)
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { getToken } from '../api'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('../views/LoginView.vue'),
|
||||
meta: { public: true },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
@@ -13,7 +20,24 @@ const router = createRouter({
|
||||
name: 'client',
|
||||
component: () => import('../views/ClientView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/plugins',
|
||||
name: 'plugins',
|
||||
component: () => import('../views/PluginsView.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = getToken()
|
||||
if (!to.meta.public && !token) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && token) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,79 +1,15 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,20 @@ export interface ProxyRule {
|
||||
local_ip: string
|
||||
local_port: number
|
||||
remote_port: number
|
||||
type?: string
|
||||
}
|
||||
|
||||
// 客户端配置
|
||||
export interface ClientConfig {
|
||||
id: string
|
||||
nickname?: string
|
||||
rules: ProxyRule[]
|
||||
}
|
||||
|
||||
// 客户端状态
|
||||
export interface ClientStatus {
|
||||
id: string
|
||||
nickname?: string
|
||||
online: boolean
|
||||
last_ping?: string
|
||||
rule_count: number
|
||||
@@ -23,6 +26,7 @@ export interface ClientStatus {
|
||||
// 客户端详情
|
||||
export interface ClientDetail {
|
||||
id: string
|
||||
nickname?: string
|
||||
rules: ProxyRule[]
|
||||
online: boolean
|
||||
last_ping?: string
|
||||
@@ -36,3 +40,23 @@ export interface ServerStatus {
|
||||
}
|
||||
client_count: number
|
||||
}
|
||||
|
||||
// 插件类型
|
||||
export const PluginType = {
|
||||
Proxy: 'proxy',
|
||||
App: 'app',
|
||||
Service: 'service',
|
||||
Tool: 'tool'
|
||||
} as const
|
||||
|
||||
export type PluginTypeValue = typeof PluginType[keyof typeof PluginType]
|
||||
|
||||
// 插件信息
|
||||
export interface PluginInfo {
|
||||
name: string
|
||||
version: string
|
||||
type: string
|
||||
description: string
|
||||
source: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
@@ -1,24 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getClient, updateClient, deleteClient } from '../api'
|
||||
import type { ProxyRule } from '../types'
|
||||
import {
|
||||
NCard, NButton, NSpace, NTag, NTable, NEmpty,
|
||||
NFormItem, NInput, NInputNumber, NSelect, NModal, NCheckbox,
|
||||
NIcon, useMessage, useDialog
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
ArrowBackOutline, CreateOutline, TrashOutline,
|
||||
PushOutline, PowerOutline, AddOutline, SaveOutline, CloseOutline,
|
||||
DownloadOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import { getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, getPlugins, installPluginsToClient } from '../api'
|
||||
import type { ProxyRule, PluginInfo } from '../types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const clientId = route.params.id as string
|
||||
|
||||
const online = ref(false)
|
||||
const lastPing = ref('')
|
||||
const nickname = ref('')
|
||||
const rules = ref<ProxyRule[]>([])
|
||||
const editing = ref(false)
|
||||
const editNickname = ref('')
|
||||
const editRules = ref<ProxyRule[]>([])
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'HTTPS', value: 'https' },
|
||||
{ label: 'SOCKS5', value: 'socks5' }
|
||||
]
|
||||
|
||||
// 插件安装相关
|
||||
const showInstallModal = ref(false)
|
||||
const availablePlugins = ref<PluginInfo[]>([])
|
||||
const selectedPlugins = ref<string[]>([])
|
||||
|
||||
const loadPlugins = async () => {
|
||||
try {
|
||||
const { data } = await getPlugins()
|
||||
availablePlugins.value = (data || []).filter(p => p.enabled)
|
||||
} catch (e) {
|
||||
console.error('Failed to load plugins', e)
|
||||
}
|
||||
}
|
||||
|
||||
const openInstallModal = async () => {
|
||||
await loadPlugins()
|
||||
selectedPlugins.value = []
|
||||
showInstallModal.value = true
|
||||
}
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = { proxy: '协议', app: '应用', service: '服务', tool: '工具' }
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const loadClient = async () => {
|
||||
try {
|
||||
const { data } = await getClient(clientId)
|
||||
online.value = data.online
|
||||
lastPing.value = data.last_ping || ''
|
||||
nickname.value = data.nickname || ''
|
||||
rules.value = data.rules || []
|
||||
} catch (e) {
|
||||
console.error('Failed to load client', e)
|
||||
@@ -28,7 +76,11 @@ const loadClient = async () => {
|
||||
onMounted(loadClient)
|
||||
|
||||
const startEdit = () => {
|
||||
editRules.value = JSON.parse(JSON.stringify(rules.value))
|
||||
editNickname.value = nickname.value
|
||||
editRules.value = rules.value.map(rule => ({
|
||||
...rule,
|
||||
type: rule.type || 'tcp'
|
||||
}))
|
||||
editing.value = true
|
||||
}
|
||||
|
||||
@@ -38,7 +90,7 @@ const cancelEdit = () => {
|
||||
|
||||
const addRule = () => {
|
||||
editRules.value.push({
|
||||
name: '', local_ip: '127.0.0.1', local_port: 80, remote_port: 8080
|
||||
name: '', local_ip: '127.0.0.1', local_port: 80, remote_port: 8080, type: 'tcp'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,156 +100,228 @@ const removeRule = (index: number) => {
|
||||
|
||||
const saveEdit = async () => {
|
||||
try {
|
||||
await updateClient(clientId, { id: clientId, rules: editRules.value })
|
||||
await updateClient(clientId, { id: clientId, nickname: editNickname.value, rules: editRules.value })
|
||||
editing.value = false
|
||||
message.success('保存成功')
|
||||
loadClient()
|
||||
} catch (e) {
|
||||
alert('保存失败')
|
||||
message.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!confirm('确定删除此客户端?')) return
|
||||
const confirmDelete = () => {
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: '确定要删除此客户端吗?',
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await deleteClient(clientId)
|
||||
message.success('删除成功')
|
||||
router.push('/')
|
||||
} catch (e) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const pushConfig = async () => {
|
||||
try {
|
||||
await deleteClient(clientId)
|
||||
router.push('/')
|
||||
} catch (e) {
|
||||
alert('删除失败')
|
||||
await pushConfigToClient(clientId)
|
||||
message.success('配置已推送')
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data || '推送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
dialog.warning({
|
||||
title: '确认断开',
|
||||
content: '确定要断开此客户端连接吗?',
|
||||
positiveText: '断开',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await disconnectClient(clientId)
|
||||
online.value = false
|
||||
message.success('已断开连接')
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data || '断开失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const installPlugins = async () => {
|
||||
if (selectedPlugins.value.length === 0) {
|
||||
message.warning('请选择要安装的插件')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await installPluginsToClient(clientId, selectedPlugins.value)
|
||||
message.success(`已推送 ${selectedPlugins.value.length} 个插件到客户端`)
|
||||
showInstallModal.value = false
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data || '安装失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="client-view">
|
||||
<div class="header">
|
||||
<button class="btn" @click="router.push('/')">← 返回</button>
|
||||
<h2>{{ clientId }}</h2>
|
||||
<span :class="['status-badge', online ? 'online' : 'offline']">
|
||||
{{ online ? '在线' : '离线' }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 头部信息卡片 -->
|
||||
<n-card style="margin-bottom: 16px;">
|
||||
<n-space justify="space-between" align="center" wrap>
|
||||
<n-space align="center">
|
||||
<n-button quaternary @click="router.push('/')">
|
||||
<template #icon><n-icon><ArrowBackOutline /></n-icon></template>
|
||||
返回
|
||||
</n-button>
|
||||
<h2 style="margin: 0;">{{ nickname || clientId }}</h2>
|
||||
<span v-if="nickname" style="color: #999; font-size: 12px;">{{ clientId }}</span>
|
||||
<n-tag :type="online ? 'success' : 'default'">
|
||||
{{ online ? '在线' : '离线' }}
|
||||
</n-tag>
|
||||
<span v-if="lastPing" style="color: #666; font-size: 14px;">
|
||||
最后心跳: {{ lastPing }}
|
||||
</span>
|
||||
</n-space>
|
||||
<n-space>
|
||||
<template v-if="online">
|
||||
<n-button type="info" @click="pushConfig">
|
||||
<template #icon><n-icon><PushOutline /></n-icon></template>
|
||||
推送配置
|
||||
</n-button>
|
||||
<n-button type="success" @click="openInstallModal">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
安装插件
|
||||
</n-button>
|
||||
<n-button type="warning" @click="disconnect">
|
||||
<template #icon><n-icon><PowerOutline /></n-icon></template>
|
||||
断开连接
|
||||
</n-button>
|
||||
</template>
|
||||
<template v-if="!editing">
|
||||
<n-button type="primary" @click="startEdit">
|
||||
<template #icon><n-icon><CreateOutline /></n-icon></template>
|
||||
编辑规则
|
||||
</n-button>
|
||||
<n-button type="error" @click="confirmDelete">
|
||||
<template #icon><n-icon><TrashOutline /></n-icon></template>
|
||||
删除
|
||||
</n-button>
|
||||
</template>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<div v-if="lastPing" class="ping-info">最后心跳: {{ lastPing }}</div>
|
||||
|
||||
<div class="rules-section">
|
||||
<div class="section-header">
|
||||
<h3>代理规则</h3>
|
||||
<div v-if="!editing">
|
||||
<button class="btn primary" @click="startEdit">编辑</button>
|
||||
<button class="btn danger" @click="confirmDelete">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 规则卡片 -->
|
||||
<n-card title="代理规则">
|
||||
<template #header-extra v-if="editing">
|
||||
<n-space>
|
||||
<n-button @click="cancelEdit">
|
||||
<template #icon><n-icon><CloseOutline /></n-icon></template>
|
||||
取消
|
||||
</n-button>
|
||||
<n-button type="primary" @click="saveEdit">
|
||||
<template #icon><n-icon><SaveOutline /></n-icon></template>
|
||||
保存
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<!-- 查看模式 -->
|
||||
<table v-if="!editing" class="rules-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>本地地址</th>
|
||||
<th>远程端口</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="rule in rules" :key="rule.name">
|
||||
<td>{{ rule.name }}</td>
|
||||
<td>{{ rule.local_ip }}:{{ rule.local_port }}</td>
|
||||
<td>{{ rule.remote_port }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<template v-if="!editing">
|
||||
<n-empty v-if="rules.length === 0" description="暂无代理规则" />
|
||||
<n-table v-else :bordered="false" :single-line="false">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>本地地址</th>
|
||||
<th>远程端口</th>
|
||||
<th>类型</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="rule in rules" :key="rule.name">
|
||||
<td>{{ rule.name || '未命名' }}</td>
|
||||
<td>{{ rule.local_ip }}:{{ rule.local_port }}</td>
|
||||
<td>{{ rule.remote_port }}</td>
|
||||
<td><n-tag size="small">{{ rule.type || 'tcp' }}</n-tag></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<!-- 编辑模式 -->
|
||||
<div v-if="editing" class="edit-form">
|
||||
<div v-for="(rule, i) in editRules" :key="i" class="rule-row">
|
||||
<input v-model="rule.name" placeholder="名称" />
|
||||
<input v-model="rule.local_ip" placeholder="本地IP" />
|
||||
<input v-model.number="rule.local_port" type="number" placeholder="本地端口" />
|
||||
<input v-model.number="rule.remote_port" type="number" placeholder="远程端口" />
|
||||
<button class="btn-icon" @click="removeRule(i)">×</button>
|
||||
</div>
|
||||
<button class="btn secondary" @click="addRule">+ 添加规则</button>
|
||||
<div class="edit-actions">
|
||||
<button class="btn" @click="cancelEdit">取消</button>
|
||||
<button class="btn primary" @click="saveEdit">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<n-space vertical :size="12">
|
||||
<n-form-item label="昵称" :show-feedback="false">
|
||||
<n-input v-model:value="editNickname" placeholder="给客户端起个名字(可选)" style="max-width: 300px;" />
|
||||
</n-form-item>
|
||||
<n-card v-for="(rule, i) in editRules" :key="i" size="small">
|
||||
<n-space align="center">
|
||||
<n-form-item label="名称" :show-feedback="false">
|
||||
<n-input v-model:value="rule.name" placeholder="规则名称" />
|
||||
</n-form-item>
|
||||
<n-form-item label="类型" :show-feedback="false">
|
||||
<n-select v-model:value="rule.type" :options="typeOptions" style="width: 100px;" />
|
||||
</n-form-item>
|
||||
<n-form-item label="本地IP" :show-feedback="false">
|
||||
<n-input v-model:value="rule.local_ip" placeholder="127.0.0.1" />
|
||||
</n-form-item>
|
||||
<n-form-item label="本地端口" :show-feedback="false">
|
||||
<n-input-number v-model:value="rule.local_port" :show-button="false" />
|
||||
</n-form-item>
|
||||
<n-form-item label="远程端口" :show-feedback="false">
|
||||
<n-input-number v-model:value="rule.remote_port" :show-button="false" />
|
||||
</n-form-item>
|
||||
<n-button v-if="editRules.length > 1" quaternary type="error" @click="removeRule(i)">
|
||||
<template #icon><n-icon><TrashOutline /></n-icon></template>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<n-button dashed block @click="addRule">
|
||||
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||||
添加规则
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
|
||||
<!-- 安装插件模态框 -->
|
||||
<n-modal v-model:show="showInstallModal" preset="card" title="安装插件到客户端" style="width: 500px;">
|
||||
<n-empty v-if="availablePlugins.length === 0" description="暂无可用插件" />
|
||||
<n-space v-else vertical :size="12">
|
||||
<n-card v-for="plugin in availablePlugins" :key="plugin.name" size="small">
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-space vertical :size="4">
|
||||
<n-space align="center">
|
||||
<span style="font-weight: 500;">{{ plugin.name }}</span>
|
||||
<n-tag size="small">{{ getTypeLabel(plugin.type) }}</n-tag>
|
||||
</n-space>
|
||||
<span style="color: #666; font-size: 12px;">{{ plugin.description }}</span>
|
||||
</n-space>
|
||||
<n-checkbox
|
||||
:checked="selectedPlugins.includes(plugin.name)"
|
||||
@update:checked="(v: boolean) => {
|
||||
if (v) selectedPlugins.push(plugin.name)
|
||||
else selectedPlugins = selectedPlugins.filter(n => n !== plugin.name)
|
||||
}"
|
||||
/>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showInstallModal = false">取消</n-button>
|
||||
<n-button type="primary" @click="installPlugins" :disabled="selectedPlugins.length === 0">
|
||||
安装 ({{ selectedPlugins.length }})
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.header h2 { margin: 0; }
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.status-badge.online { background: #d4edda; color: #155724; }
|
||||
.status-badge.offline { background: #f8d7da; color: #721c24; }
|
||||
.ping-info { color: #666; margin-bottom: 20px; }
|
||||
.rules-section {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section-header h3 { margin: 0; }
|
||||
.section-header .btn { margin-left: 8px; }
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.btn.primary { background: #3498db; color: #fff; }
|
||||
.btn.secondary { background: #95a5a6; color: #fff; }
|
||||
.btn.danger { background: #e74c3c; color: #fff; }
|
||||
.rules-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.rules-table th, .rules-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.rules-table th { font-weight: 600; }
|
||||
.rule-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.rule-row input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.btn-icon {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
width: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getClients, addClient } from '../api'
|
||||
import type { ClientStatus, ProxyRule } from '../types'
|
||||
import { NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi, NEmpty } from 'naive-ui'
|
||||
import { getClients } from '../api'
|
||||
import type { ClientStatus } from '../types'
|
||||
|
||||
const router = useRouter()
|
||||
const clients = ref<ClientStatus[]>([])
|
||||
const showModal = ref(false)
|
||||
const newClientId = ref('')
|
||||
const newRules = ref<ProxyRule[]>([])
|
||||
|
||||
const loadClients = async () => {
|
||||
try {
|
||||
@@ -19,33 +17,16 @@ const loadClients = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const onlineClients = computed(() => {
|
||||
return clients.value.filter(client => client.online).length
|
||||
})
|
||||
|
||||
const totalRules = computed(() => {
|
||||
return clients.value.reduce((sum, client) => sum + client.rule_count, 0)
|
||||
})
|
||||
|
||||
onMounted(loadClients)
|
||||
|
||||
const openAddModal = () => {
|
||||
newClientId.value = ''
|
||||
newRules.value = [{ name: '', local_ip: '127.0.0.1', local_port: 80, remote_port: 8080 }]
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const addRule = () => {
|
||||
newRules.value.push({ name: '', local_ip: '127.0.0.1', local_port: 80, remote_port: 8080 })
|
||||
}
|
||||
|
||||
const removeRule = (index: number) => {
|
||||
newRules.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const saveClient = async () => {
|
||||
if (!newClientId.value) return
|
||||
try {
|
||||
await addClient({ id: newClientId.value, rules: newRules.value })
|
||||
showModal.value = false
|
||||
loadClients()
|
||||
} catch (e) {
|
||||
alert('添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
const viewClient = (id: string) => {
|
||||
router.push(`/client/${id}`)
|
||||
}
|
||||
@@ -53,139 +34,49 @@ const viewClient = (id: string) => {
|
||||
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="toolbar">
|
||||
<h2>客户端列表</h2>
|
||||
<button class="btn primary" @click="openAddModal">添加客户端</button>
|
||||
<div style="margin-bottom: 24px;">
|
||||
<h2 style="margin: 0 0 8px 0;">客户端管理</h2>
|
||||
<p style="margin: 0; color: #666;">查看已连接的隧道客户端</p>
|
||||
</div>
|
||||
|
||||
<div class="client-grid">
|
||||
<div v-for="client in clients" :key="client.id" class="client-card" @click="viewClient(client.id)">
|
||||
<div class="card-header">
|
||||
<span class="client-id">{{ client.id }}</span>
|
||||
<span :class="['status', client.online ? 'online' : 'offline']"></span>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<span>{{ client.rule_count }} 条规则</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<n-grid :cols="3" :x-gap="16" :y-gap="16" style="margin-bottom: 24px;">
|
||||
<n-gi>
|
||||
<n-card>
|
||||
<n-statistic label="总客户端" :value="clients.length" />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card>
|
||||
<n-statistic label="在线客户端" :value="onlineClients" />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card>
|
||||
<n-statistic label="总规则数" :value="totalRules" />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<div v-if="clients.length === 0" class="empty">暂无客户端配置</div>
|
||||
<n-empty v-if="clients.length === 0" description="暂无客户端连接" />
|
||||
|
||||
<!-- 添加客户端模态框 -->
|
||||
<div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
|
||||
<div class="modal">
|
||||
<h3>添加客户端</h3>
|
||||
<div class="form-group">
|
||||
<label>客户端 ID</label>
|
||||
<input v-model="newClientId" placeholder="例如: client-a" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>代理规则</label>
|
||||
<div v-for="(rule, i) in newRules" :key="i" class="rule-row">
|
||||
<input v-model="rule.name" placeholder="名称" />
|
||||
<input v-model="rule.local_ip" placeholder="本地IP" />
|
||||
<input v-model.number="rule.local_port" type="number" placeholder="本地端口" />
|
||||
<input v-model.number="rule.remote_port" type="number" placeholder="远程端口" />
|
||||
<button class="btn-icon" @click="removeRule(i)">×</button>
|
||||
</div>
|
||||
<button class="btn secondary" @click="addRule">+ 添加规则</button>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn" @click="showModal = false">取消</button>
|
||||
<button class="btn primary" @click="saveClient">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<n-grid v-else :cols="3" :x-gap="16" :y-gap="16" responsive="screen" cols-s="1" cols-m="2">
|
||||
<n-gi v-for="client in clients" :key="client.id">
|
||||
<n-card hoverable style="cursor: pointer;" @click="viewClient(client.id)">
|
||||
<n-space justify="space-between" align="center">
|
||||
<div>
|
||||
<h3 style="margin: 0 0 4px 0;">{{ client.nickname || client.id }}</h3>
|
||||
<p v-if="client.nickname" style="margin: 0 0 8px 0; color: #999; font-size: 12px;">{{ client.id }}</p>
|
||||
<n-space>
|
||||
<n-tag :type="client.online ? 'success' : 'default'" size="small">
|
||||
{{ client.online ? '在线' : '离线' }}
|
||||
</n-tag>
|
||||
<n-tag type="info" size="small">{{ client.rule_count }} 条规则</n-tag>
|
||||
</n-space>
|
||||
</div>
|
||||
<n-button size="small" @click.stop="viewClient(client.id)">查看详情</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.toolbar h2 { font-size: 18px; color: #2c3e50; }
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.btn.primary { background: #3498db; color: #fff; }
|
||||
.btn.secondary { background: #95a5a6; color: #fff; }
|
||||
.client-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.client-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.client-card:hover { transform: translateY(-2px); }
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.client-id { font-weight: 600; }
|
||||
.status { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.status.online { background: #27ae60; }
|
||||
.status.offline { background: #95a5a6; }
|
||||
.card-info { font-size: 14px; color: #666; }
|
||||
.empty { text-align: center; color: #999; padding: 40px; }
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
width: 500px;
|
||||
max-width: 90%;
|
||||
}
|
||||
.modal h3 { margin-bottom: 16px; }
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-group label { display: block; margin-bottom: 8px; font-weight: 500; }
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.rule-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.rule-row input { flex: 1; width: auto; }
|
||||
.btn-icon {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
width: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
122
web/src/views/LoginView.vue
Normal file
122
web/src/views/LoginView.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NCard, NForm, NFormItem, NInput, NButton, NAlert } from 'naive-ui'
|
||||
import { login, setToken } from '../api'
|
||||
|
||||
const router = useRouter()
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!username.value || !password.value) {
|
||||
error.value = '请输入用户名和密码'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const { data } = await login(username.value, password.value)
|
||||
setToken(data.token)
|
||||
router.push('/')
|
||||
} catch (e: any) {
|
||||
error.value = e.response?.data?.error || '登录失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<n-card class="login-card" :bordered="false">
|
||||
<template #header>
|
||||
<div class="login-header">
|
||||
<h1 class="logo">GoTunnel</h1>
|
||||
<p class="subtitle">安全的内网穿透工具</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-form @submit.prevent="handleLogin">
|
||||
<n-form-item label="用户名">
|
||||
<n-input
|
||||
v-model:value="username"
|
||||
placeholder="请输入用户名"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="密码">
|
||||
<n-input
|
||||
v-model:value="password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
:disabled="loading"
|
||||
show-password-on="click"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-alert v-if="error" type="error" :show-icon="true" style="margin-bottom: 16px;">
|
||||
{{ error }}
|
||||
</n-alert>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="loading"
|
||||
attr-type="submit"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</n-button>
|
||||
</n-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="login-footer">欢迎使用 GoTunnel</div>
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #18a058;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
137
web/src/views/PluginsView.vue
Normal file
137
web/src/views/PluginsView.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi,
|
||||
NEmpty, NSpin, NIcon, NSwitch, useMessage
|
||||
} from 'naive-ui'
|
||||
import { ArrowBackOutline, ExtensionPuzzleOutline } from '@vicons/ionicons5'
|
||||
import { getPlugins, enablePlugin, disablePlugin } from '../api'
|
||||
import type { PluginInfo } from '../types'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const plugins = ref<PluginInfo[]>([])
|
||||
const loading = ref(true)
|
||||
|
||||
const loadPlugins = async () => {
|
||||
try {
|
||||
const { data } = await getPlugins()
|
||||
plugins.value = data || []
|
||||
} catch (e) {
|
||||
console.error('Failed to load plugins', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const proxyPlugins = computed(() =>
|
||||
plugins.value.filter(p => p.type === 'proxy')
|
||||
)
|
||||
|
||||
const appPlugins = computed(() =>
|
||||
plugins.value.filter(p => p.type === 'app')
|
||||
)
|
||||
|
||||
const togglePlugin = async (plugin: PluginInfo) => {
|
||||
try {
|
||||
if (plugin.enabled) {
|
||||
await disablePlugin(plugin.name)
|
||||
message.success(`已禁用 ${plugin.name}`)
|
||||
} else {
|
||||
await enablePlugin(plugin.name)
|
||||
message.success(`已启用 ${plugin.name}`)
|
||||
}
|
||||
plugin.enabled = !plugin.enabled
|
||||
} catch (e) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
proxy: '协议',
|
||||
app: '应用',
|
||||
service: '服务',
|
||||
tool: '工具'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors: Record<string, 'info' | 'success' | 'warning' | 'error' | 'default'> = {
|
||||
proxy: 'info',
|
||||
app: 'success',
|
||||
service: 'warning',
|
||||
tool: 'default'
|
||||
}
|
||||
return colors[type] || 'default'
|
||||
}
|
||||
|
||||
onMounted(loadPlugins)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plugins-view">
|
||||
<n-space justify="space-between" align="center" style="margin-bottom: 24px;">
|
||||
<div>
|
||||
<h2 style="margin: 0 0 8px 0;">插件管理</h2>
|
||||
<p style="margin: 0; color: #666;">查看和管理已注册的插件</p>
|
||||
</div>
|
||||
<n-button quaternary @click="router.push('/')">
|
||||
<template #icon><n-icon><ArrowBackOutline /></n-icon></template>
|
||||
返回首页
|
||||
</n-button>
|
||||
</n-space>
|
||||
|
||||
<n-spin :show="loading">
|
||||
<n-grid :cols="3" :x-gap="16" :y-gap="16" style="margin-bottom: 24px;">
|
||||
<n-gi>
|
||||
<n-card>
|
||||
<n-statistic label="总插件数" :value="plugins.length" />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card>
|
||||
<n-statistic label="协议插件" :value="proxyPlugins.length" />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card>
|
||||
<n-statistic label="应用插件" :value="appPlugins.length" />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-empty v-if="!loading && plugins.length === 0" description="暂无插件" />
|
||||
|
||||
<n-grid v-else :cols="3" :x-gap="16" :y-gap="16" responsive="screen" cols-s="1" cols-m="2">
|
||||
<n-gi v-for="plugin in plugins" :key="plugin.name">
|
||||
<n-card hoverable>
|
||||
<template #header>
|
||||
<n-space align="center">
|
||||
<n-icon size="24" color="#18a058"><ExtensionPuzzleOutline /></n-icon>
|
||||
<span>{{ plugin.name }}</span>
|
||||
</n-space>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-switch :value="plugin.enabled" @update:value="togglePlugin(plugin)" />
|
||||
</template>
|
||||
<n-space vertical :size="8">
|
||||
<n-space>
|
||||
<n-tag size="small">v{{ plugin.version }}</n-tag>
|
||||
<n-tag size="small" :type="getTypeColor(plugin.type)">
|
||||
{{ getTypeLabel(plugin.type) }}
|
||||
</n-tag>
|
||||
<n-tag size="small" :type="plugin.source === 'builtin' ? 'default' : 'warning'">
|
||||
{{ plugin.source === 'builtin' ? '内置' : 'WASM' }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<p style="margin: 0; color: #666;">{{ plugin.description }}</p>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-spin>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,33 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
{
|
||||
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar']
|
||||
}
|
||||
]
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver()]
|
||||
})
|
||||
],
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vue-vendor': ['vue', 'vue-router'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user