Подготовить HY2XS к production-сборке
@@ -0,0 +1,14 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
# 表示所有文件适用
|
||||
[*]
|
||||
charset = utf-8 # 设置文件字符集为 utf-8
|
||||
end_of_line = lf # 控制换行类型(lf | cr | crlf)
|
||||
indent_style = tab # 缩进风格(tab | space)
|
||||
insert_final_newline = true # 始终在文件末尾插入一个新行
|
||||
|
||||
# 表示仅 md 文件适用以下规则
|
||||
[*.md]
|
||||
max_line_length = off # 关闭最大行长度限制
|
||||
trim_trailing_whitespace = false # 关闭末尾空格修剪
|
||||
@@ -0,0 +1,8 @@
|
||||
## 开发环境
|
||||
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV='development'
|
||||
|
||||
VITE_APP_TITLE = 'h-ui'
|
||||
VITE_APP_PORT = 3000
|
||||
VITE_APP_BASE_API = '/hui'
|
||||
@@ -0,0 +1,5 @@
|
||||
## 生产环境
|
||||
|
||||
VITE_APP_TITLE = 'h-ui'
|
||||
VITE_APP_PORT = 80
|
||||
VITE_APP_BASE_API = '/hui'
|
||||
@@ -0,0 +1,13 @@
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
.vscode
|
||||
.idea
|
||||
*.sh
|
||||
*.md
|
||||
|
||||
src/assets
|
||||
|
||||
.eslintrc.cjs
|
||||
.prettierrc.cjs
|
||||
.stylelintrc.cjs
|
||||
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"globals": {
|
||||
"EffectScope": true,
|
||||
"ElForm": true,
|
||||
"ElMessage": true,
|
||||
"ElMessageBox": true,
|
||||
"ElTree": true,
|
||||
"asyncComputed": true,
|
||||
"autoResetRef": true,
|
||||
"computed": true,
|
||||
"computedAsync": true,
|
||||
"computedEager": true,
|
||||
"computedInject": true,
|
||||
"computedWithControl": true,
|
||||
"controlledComputed": true,
|
||||
"controlledRef": true,
|
||||
"createApp": true,
|
||||
"createEventHook": true,
|
||||
"createGlobalState": true,
|
||||
"createInjectionState": true,
|
||||
"createReactiveFn": true,
|
||||
"createSharedComposable": true,
|
||||
"createUnrefFn": true,
|
||||
"customRef": true,
|
||||
"debouncedRef": true,
|
||||
"debouncedWatch": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"eagerComputed": true,
|
||||
"effectScope": true,
|
||||
"extendRef": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"ignorableWatch": true,
|
||||
"inject": true,
|
||||
"isDefined": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"makeDestructurable": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onClickOutside": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onKeyStroke": true,
|
||||
"onLongPress": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onStartTyping": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"pausableWatch": true,
|
||||
"provide": true,
|
||||
"reactify": true,
|
||||
"reactifyObject": true,
|
||||
"reactive": true,
|
||||
"reactiveComputed": true,
|
||||
"reactiveOmit": true,
|
||||
"reactivePick": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"refAutoReset": true,
|
||||
"refDebounced": true,
|
||||
"refDefault": true,
|
||||
"refThrottled": true,
|
||||
"refWithControl": true,
|
||||
"resolveComponent": true,
|
||||
"resolveDirective": true,
|
||||
"resolveRef": true,
|
||||
"resolveUnref": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"syncRef": true,
|
||||
"syncRefs": true,
|
||||
"templateRef": true,
|
||||
"throttledRef": true,
|
||||
"throttledWatch": true,
|
||||
"toRaw": true,
|
||||
"toReactive": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"triggerRef": true,
|
||||
"tryOnBeforeMount": true,
|
||||
"tryOnBeforeUnmount": true,
|
||||
"tryOnMounted": true,
|
||||
"tryOnScopeDispose": true,
|
||||
"tryOnUnmounted": true,
|
||||
"unref": true,
|
||||
"unrefElement": true,
|
||||
"until": true,
|
||||
"useActiveElement": true,
|
||||
"useArrayEvery": true,
|
||||
"useArrayFilter": true,
|
||||
"useArrayFind": true,
|
||||
"useArrayFindIndex": true,
|
||||
"useArrayFindLast": true,
|
||||
"useArrayJoin": true,
|
||||
"useArrayMap": true,
|
||||
"useArrayReduce": true,
|
||||
"useArraySome": true,
|
||||
"useArrayUnique": true,
|
||||
"useAsyncQueue": true,
|
||||
"useAsyncState": true,
|
||||
"useAttrs": true,
|
||||
"useBase64": true,
|
||||
"useBattery": true,
|
||||
"useBluetooth": true,
|
||||
"useBreakpoints": true,
|
||||
"useBroadcastChannel": true,
|
||||
"useBrowserLocation": true,
|
||||
"useCached": true,
|
||||
"useClipboard": true,
|
||||
"useCloned": true,
|
||||
"useColorMode": true,
|
||||
"useConfirmDialog": true,
|
||||
"useCounter": true,
|
||||
"useCssModule": true,
|
||||
"useCssVar": true,
|
||||
"useCssVars": true,
|
||||
"useCurrentElement": true,
|
||||
"useCycleList": true,
|
||||
"useDark": true,
|
||||
"useDateFormat": true,
|
||||
"useDebounce": true,
|
||||
"useDebounceFn": true,
|
||||
"useDebouncedRefHistory": true,
|
||||
"useDeviceMotion": true,
|
||||
"useDeviceOrientation": true,
|
||||
"useDevicePixelRatio": true,
|
||||
"useDevicesList": true,
|
||||
"useDisplayMedia": true,
|
||||
"useDocumentVisibility": true,
|
||||
"useDraggable": true,
|
||||
"useDropZone": true,
|
||||
"useElementBounding": true,
|
||||
"useElementByPoint": true,
|
||||
"useElementHover": true,
|
||||
"useElementSize": true,
|
||||
"useElementVisibility": true,
|
||||
"useEventBus": true,
|
||||
"useEventListener": true,
|
||||
"useEventSource": true,
|
||||
"useEyeDropper": true,
|
||||
"useFavicon": true,
|
||||
"useFetch": true,
|
||||
"useFileDialog": true,
|
||||
"useFileSystemAccess": true,
|
||||
"useFocus": true,
|
||||
"useFocusWithin": true,
|
||||
"useFps": true,
|
||||
"useFullscreen": true,
|
||||
"useGamepad": true,
|
||||
"useGeolocation": true,
|
||||
"useIdle": true,
|
||||
"useImage": true,
|
||||
"useInfiniteScroll": true,
|
||||
"useIntersectionObserver": true,
|
||||
"useInterval": true,
|
||||
"useIntervalFn": true,
|
||||
"useKeyModifier": true,
|
||||
"useLastChanged": true,
|
||||
"useLocalStorage": true,
|
||||
"useMagicKeys": true,
|
||||
"useManualRefHistory": true,
|
||||
"useMediaControls": true,
|
||||
"useMediaQuery": true,
|
||||
"useMemoize": true,
|
||||
"useMemory": true,
|
||||
"useMounted": true,
|
||||
"useMouse": true,
|
||||
"useMouseInElement": true,
|
||||
"useMousePressed": true,
|
||||
"useMutationObserver": true,
|
||||
"useNavigatorLanguage": true,
|
||||
"useNetwork": true,
|
||||
"useNow": true,
|
||||
"useObjectUrl": true,
|
||||
"useOffsetPagination": true,
|
||||
"useOnline": true,
|
||||
"usePageLeave": true,
|
||||
"useParallax": true,
|
||||
"usePermission": true,
|
||||
"usePointer": true,
|
||||
"usePointerLock": true,
|
||||
"usePointerSwipe": true,
|
||||
"usePreferredColorScheme": true,
|
||||
"usePreferredContrast": true,
|
||||
"usePreferredDark": true,
|
||||
"usePreferredLanguages": true,
|
||||
"usePreferredReducedMotion": true,
|
||||
"usePrevious": true,
|
||||
"useRafFn": true,
|
||||
"useRefHistory": true,
|
||||
"useResizeObserver": true,
|
||||
"useScreenOrientation": true,
|
||||
"useScreenSafeArea": true,
|
||||
"useScriptTag": true,
|
||||
"useScroll": true,
|
||||
"useScrollLock": true,
|
||||
"useSessionStorage": true,
|
||||
"useShare": true,
|
||||
"useSlots": true,
|
||||
"useSorted": true,
|
||||
"useSpeechRecognition": true,
|
||||
"useSpeechSynthesis": true,
|
||||
"useStepper": true,
|
||||
"useStorage": true,
|
||||
"useStorageAsync": true,
|
||||
"useStyleTag": true,
|
||||
"useSupported": true,
|
||||
"useSwipe": true,
|
||||
"useTemplateRefsList": true,
|
||||
"useTextDirection": true,
|
||||
"useTextSelection": true,
|
||||
"useTextareaAutosize": true,
|
||||
"useThrottle": true,
|
||||
"useThrottleFn": true,
|
||||
"useThrottledRefHistory": true,
|
||||
"useTimeAgo": true,
|
||||
"useTimeout": true,
|
||||
"useTimeoutFn": true,
|
||||
"useTimeoutPoll": true,
|
||||
"useTimestamp": true,
|
||||
"useTitle": true,
|
||||
"useToNumber": true,
|
||||
"useToString": true,
|
||||
"useToggle": true,
|
||||
"useTransition": true,
|
||||
"useUrlSearchParams": true,
|
||||
"useUserMedia": true,
|
||||
"useVModel": true,
|
||||
"useVModels": true,
|
||||
"useVibrate": true,
|
||||
"useVirtualList": true,
|
||||
"useWakeLock": true,
|
||||
"useWebNotification": true,
|
||||
"useWebSocket": true,
|
||||
"useWebWorker": true,
|
||||
"useWebWorkerFn": true,
|
||||
"useWindowFocus": true,
|
||||
"useWindowScroll": true,
|
||||
"useWindowSize": true,
|
||||
"watch": true,
|
||||
"watchArray": true,
|
||||
"watchAtMost": true,
|
||||
"watchDebounced": true,
|
||||
"watchEffect": true,
|
||||
"watchIgnorable": true,
|
||||
"watchOnce": true,
|
||||
"watchPausable": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true,
|
||||
"watchThrottled": true,
|
||||
"watchTriggerable": true,
|
||||
"watchWithFilter": true,
|
||||
"whenever": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "vue-eslint-parser", // https://eslint.vuejs.org/user-guide/#bundle-configurations
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-essential",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"./.eslintrc-auto-import.json",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
parser: "@typescript-eslint/parser",
|
||||
},
|
||||
plugins: ["vue", "@typescript-eslint"],
|
||||
rules: {
|
||||
"vue/multi-word-component-names": "off", // 关闭组件名必须多字: https://eslint.vuejs.org/rules/multi-word-component-names.html
|
||||
"@typescript-eslint/no-empty-function": "off", // 关闭空方法检查
|
||||
"@typescript-eslint/no-explicit-any": "off", // 关闭any类型的警告
|
||||
"vue/no-v-model-argument": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
}, // https://eslint.org/docs/latest/use/configure/language-options#specifying-globals
|
||||
globals: {
|
||||
DialogType: "readonly",
|
||||
OptionType: "readonly",
|
||||
},
|
||||
};
|
||||
цц
|
||||
@@ -0,0 +1,14 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.local
|
||||
@@ -0,0 +1,9 @@
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
.vscode
|
||||
.idea
|
||||
*.sh
|
||||
*.md
|
||||
|
||||
src/assets
|
||||
@@ -0,0 +1,36 @@
|
||||
module.exports = {
|
||||
// (x)=>{},单个参数箭头函数是否显示小括号。(always:始终显示;avoid:省略括号。默认:always)
|
||||
arrowParens: "always",
|
||||
// 开始标签的右尖括号是否跟随在最后一行属性末尾,默认false
|
||||
bracketSameLine: false,
|
||||
// 对象字面量的括号之间打印空格 (true - Example: { foo: bar } ; false - Example: {foo:bar})
|
||||
bracketSpacing: true,
|
||||
// 是否格式化一些文件中被嵌入的代码片段的风格(auto|off;默认auto)
|
||||
embeddedLanguageFormatting: "auto",
|
||||
// 指定 HTML 文件的空格敏感度 (css|strict|ignore;默认css)
|
||||
htmlWhitespaceSensitivity: "css",
|
||||
// 当文件已经被 Prettier 格式化之后,是否会在文件顶部插入一个特殊的 @format 标记,默认false
|
||||
insertPragma: false,
|
||||
// 在 JSX 中使用单引号替代双引号,默认false
|
||||
jsxSingleQuote: false,
|
||||
// 每行最多字符数量,超出换行(默认80)
|
||||
printWidth: 80,
|
||||
// 超出打印宽度 (always | never | preserve )
|
||||
proseWrap: "preserve",
|
||||
// 对象属性是否使用引号(as-needed | consistent | preserve;默认as-needed:对象的属性需要加引号才添加;)
|
||||
quoteProps: "as-needed",
|
||||
// 是否只格式化在文件顶部包含特定注释(@prettier| @format)的文件,默认false
|
||||
requirePragma: false,
|
||||
// 结尾添加分号
|
||||
semi: true,
|
||||
// 使用单引号 (true:单引号;false:双引号)
|
||||
singleQuote: false,
|
||||
// 缩进空格数,默认2个空格
|
||||
tabWidth: 2,
|
||||
// 元素末尾是否加逗号,默认es5: ES5中的 objects, arrays 等会添加逗号,TypeScript 中的 type 后不加逗号
|
||||
trailingComma: "es5",
|
||||
// 指定缩进方式,空格或tab,默认false,即使用空格
|
||||
useTabs: false,
|
||||
// vue 文件中是否缩进 <style> 和 <script> 标签,默认 false
|
||||
vueIndentScriptAndStyle: false,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
.vscode
|
||||
.idea
|
||||
*.sh
|
||||
*.md
|
||||
|
||||
src/assets
|
||||
@@ -0,0 +1,43 @@
|
||||
module.exports = {
|
||||
// 继承推荐规范配置
|
||||
extends: [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended-scss",
|
||||
"stylelint-config-recommended-vue/scss",
|
||||
"stylelint-config-html/vue",
|
||||
"stylelint-config-recess-order",
|
||||
],
|
||||
// 指定不同文件对应的解析器
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.{vue,html}"],
|
||||
customSyntax: "postcss-html",
|
||||
},
|
||||
{
|
||||
files: ["**/*.{css,scss}"],
|
||||
customSyntax: "postcss-scss",
|
||||
},
|
||||
],
|
||||
// 自定义规则
|
||||
rules: {
|
||||
"import-notation": "string", // 指定导入CSS文件的方式("string"|"url")
|
||||
"selector-class-pattern": null, // 选择器类名命名规则
|
||||
"custom-property-pattern": null, // 自定义属性命名规则
|
||||
"keyframes-name-pattern": null, // 动画帧节点样式命名规则
|
||||
"no-descending-specificity": null, // 允许无降序特异性
|
||||
// 允许 global 、export 、deep伪类
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true,
|
||||
{
|
||||
ignorePseudoClasses: ["global", "export", "deep"],
|
||||
},
|
||||
],
|
||||
// 允许未知属性
|
||||
"property-no-unknown": [
|
||||
true,
|
||||
{
|
||||
ignoreProperties: ["menuBg", "menuText", "menuActiveText"],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
# Frontend
|
||||
|
||||
HY2XS admin frontend
|
||||
@@ -0,0 +1,75 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
func InitFrontend(router *gin.Engine, relativePath string) {
|
||||
router.GET(relativePath, func(c *gin.Context) {
|
||||
indexHTML, err := staticFiles.ReadFile("dist/index.html")
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Internal Server Error")
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "text/html", []byte(replaceRelativePaths(string(indexHTML), relativePath)))
|
||||
})
|
||||
|
||||
router.GET(path.Join(relativePath, "favicon.ico"), func(c *gin.Context) {
|
||||
indexHTML, err := staticFiles.ReadFile("dist/favicon.ico")
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Internal Server Error")
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "image/x-icon", indexHTML)
|
||||
})
|
||||
|
||||
router.StaticFS(path.Join(relativePath, "assets"), http.FS(getStaticFS()))
|
||||
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
filePath := c.Request.URL.Path
|
||||
if relativePath != "/" && !strings.HasPrefix(filePath, relativePath) {
|
||||
c.String(http.StatusNotFound, "404")
|
||||
return
|
||||
}
|
||||
fileContent, err := getFileContent(filePath)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "404")
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, http.DetectContentType(fileContent), fileContent)
|
||||
})
|
||||
}
|
||||
|
||||
func getStaticFS() fs.FS {
|
||||
staticFs, _ := fs.Sub(staticFiles, "dist/assets")
|
||||
return staticFs
|
||||
}
|
||||
|
||||
func getFileContent(filePath string) ([]byte, error) {
|
||||
fileContent, err := staticFiles.ReadFile(fmt.Sprintf("dist%s", filePath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileContent, nil
|
||||
}
|
||||
|
||||
func replaceRelativePaths(htmlContent string, basePath string) string {
|
||||
if basePath == "/" {
|
||||
basePath = ""
|
||||
}
|
||||
htmlContent = strings.ReplaceAll(htmlContent, "/__dynamic_base__/", basePath+"/")
|
||||
injection := fmt.Sprintf(`
|
||||
<script>
|
||||
window.__dynamic_base__ = "%s";
|
||||
</script>`, basePath)
|
||||
return strings.Replace(htmlContent, "</head>", injection+"</head>", 1)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="HY2XS admin" />
|
||||
<meta name="keywords" content="HY2XS,HY2XS admin,Hysteria2" />
|
||||
<title>HY2XS admin</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "hy2xs-admin",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "HY2XS admin",
|
||||
"author": "HY2XS",
|
||||
"scripts": {
|
||||
"dev": "vite serve --mode development",
|
||||
"build:prod": "vite build --mode production && vue-tsc --noEmit",
|
||||
"lint:eslint": "eslint --fix --ext .ts,.js,.vue ./src ",
|
||||
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
|
||||
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^1.0.0",
|
||||
"@vueuse/core": "^9.1.1",
|
||||
"axios": "^1.3.4",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"echarts": "^5.2.2",
|
||||
"element-plus": "^2.3.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"pinia": "^2.0.33",
|
||||
"screenfull": "^6.0.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-i18n": "9",
|
||||
"vue-router": "^4.1.6",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/ep": "^1.1.8",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/path-browserify": "^1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"fast-glob": "^3.2.11",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"prettier": "^2.8.7",
|
||||
"sass": "^1.58.3",
|
||||
"stylelint": "^15.5.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recess-order": "^4.0.0",
|
||||
"stylelint-config-recommended-scss": "^9.0.1",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "^32.0.0",
|
||||
"stylelint-config-standard-scss": "^8.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"unocss": "^0.50.1",
|
||||
"unplugin-auto-import": "^0.13.0",
|
||||
"unplugin-icons": "^0.15.1",
|
||||
"unplugin-vue-components": "^0.23.0",
|
||||
"vite": "^4.3.1",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-tsc": "^0.35.0",
|
||||
"vite-plugin-dynamic-base": "1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 361 KiB |
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ElConfigProvider } from "element-plus";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
const appStore = useAppStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="appStore.locale" :size="appStore.size">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
@@ -0,0 +1,155 @@
|
||||
import request from "@/utils/request";
|
||||
import { AxiosPromise } from "axios";
|
||||
import {
|
||||
AccountSaveDto,
|
||||
AccountInfo,
|
||||
AccountLoginDto,
|
||||
AccountLoginVo,
|
||||
AccountPageDto,
|
||||
AccountUpdateDto,
|
||||
AccountVo,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
export function getAccountApi(data: IdDto): AxiosPromise<AccountVo> {
|
||||
return request({
|
||||
url: "/account/getAccount",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export function saveAccountApi(data: AccountSaveDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/saveAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前
|
||||
*/
|
||||
export function getAccountInfoApi(): AxiosPromise<AccountInfo> {
|
||||
return request({
|
||||
url: "/account/getAccountInfo",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页
|
||||
* @param data
|
||||
*/
|
||||
export function pageAccountApi(
|
||||
data: AccountPageDto
|
||||
): AxiosPromise<PageVo<AccountVo>> {
|
||||
return request({
|
||||
url: "/account/pageAccount",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export function deleteAccountApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/deleteAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改
|
||||
* @param data
|
||||
*/
|
||||
export function updateAccountApi(data: AccountUpdateDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/updateAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重设流量
|
||||
* @param data
|
||||
*/
|
||||
export function resetTrafficApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/resetTraffic",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param data
|
||||
*/
|
||||
export function loginApi(data: AccountLoginDto): AxiosPromise<AccountLoginVo> {
|
||||
return request({
|
||||
url: "/auth/login",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入
|
||||
*/
|
||||
export function importAccountApi(data: FormData): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/importAccount",
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
export function exportAccountApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/exportAccount",
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除下线状态
|
||||
*/
|
||||
export function releaseKickAccountApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/releaseKickAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否默认密码
|
||||
* @param data
|
||||
*/
|
||||
export function verifyDefaultPassApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/verifyDefaultPass",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
export interface AccountPageDto extends BaseDto {
|
||||
username?: string;
|
||||
deleted?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface AccountUpdateDto extends IdDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
conPass: string;
|
||||
quota: number;
|
||||
expireTime: number;
|
||||
deviceNo: number;
|
||||
deleted: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface AccountSaveDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
conPass: string;
|
||||
quota: number;
|
||||
expireTime: number;
|
||||
deviceNo: number;
|
||||
deleted: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface AccountLoginDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
}
|
||||
|
||||
export interface AccountVo extends IdDto {
|
||||
username: string;
|
||||
quota: number;
|
||||
download: number;
|
||||
upload: number;
|
||||
expireTime: number;
|
||||
kickUtilTime: number;
|
||||
deviceNo: number;
|
||||
role: string;
|
||||
deleted: number;
|
||||
createTime: string;
|
||||
|
||||
online: boolean;
|
||||
device: number;
|
||||
|
||||
loginAt: number;
|
||||
conAt: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface AccountLoginVo {
|
||||
accessToken: string;
|
||||
tokenType: string;
|
||||
}
|
||||
|
||||
export interface AccountInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface AccountForm extends IdDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
conPass: string;
|
||||
quota: number;
|
||||
expireTime: number;
|
||||
deviceNo: number;
|
||||
deleted: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface KickAccountForm {
|
||||
ids: number[];
|
||||
kickUtilTime: number;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
import request from "@/utils/request";
|
||||
import {
|
||||
ConfigDto,
|
||||
ConfigsDto,
|
||||
ConfigUpdateDto,
|
||||
ConfigVo,
|
||||
Hysteria2AcmePathVo,
|
||||
Hysteria2ServerConfig,
|
||||
} from "@/api/config/types";
|
||||
|
||||
export function getHysteria2ConfigApi(): AxiosPromise<Hysteria2ServerConfig> {
|
||||
return request({
|
||||
url: "/config/getHysteria2Config",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function updateHysteria2ConfigApi(
|
||||
data: Hysteria2ServerConfig
|
||||
): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/updateHysteria2Config",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getConfigApi(data: ConfigDto): AxiosPromise<ConfigVo> {
|
||||
return request({
|
||||
url: "/config/getConfig",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function listConfigApi(data: ConfigsDto): AxiosPromise<Array<ConfigVo>> {
|
||||
return request({
|
||||
url: "/config/listConfig",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateConfigsApi(data: ConfigUpdateDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/updateConfigs",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function exportConfigApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/exportConfig",
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
export function importConfigApi(data: FormData): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/importConfig",
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function exportHysteria2ConfigApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/exportHysteria2Config",
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
export function importHysteria2ConfigApi(data: FormData): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/importHysteria2Config",
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function hysteria2AcmePathApi(): AxiosPromise<Hysteria2AcmePathVo> {
|
||||
return request({
|
||||
url: "/config/hysteria2AcmePath",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function restartServerApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/restartServer",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadCertFileApi(data: FormData): AxiosPromise<string> {
|
||||
return request({
|
||||
url: "/config/uploadCertFile",
|
||||
method: "post",
|
||||
data,
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
export interface ConfigDto {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface ConfigsDto {
|
||||
keys: Array<string>;
|
||||
}
|
||||
|
||||
export interface ConfigVo {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ConfigsUpdateDto {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ConfigUpdateDto {
|
||||
configUpdateDtos: Array<ConfigsUpdateDto>;
|
||||
}
|
||||
|
||||
export interface Hysteria2ServerConfig {
|
||||
listen: string;
|
||||
tls?: {
|
||||
cert: string;
|
||||
key: string;
|
||||
sniGuard?: string;
|
||||
};
|
||||
acme?: {
|
||||
domains: string[];
|
||||
email: string;
|
||||
ca: string;
|
||||
listenHost: string;
|
||||
dir: string;
|
||||
type?: string;
|
||||
http?: {
|
||||
altPort: number;
|
||||
};
|
||||
tls?: {
|
||||
altPort: number;
|
||||
};
|
||||
dns?: {
|
||||
name: string;
|
||||
config: { [key: string]: string };
|
||||
};
|
||||
disableHTTP: boolean;
|
||||
disableTLSALPN: boolean;
|
||||
altHTTPPort: number;
|
||||
altTLSALPNPort: number;
|
||||
};
|
||||
obfs?: {
|
||||
type: string;
|
||||
salamander: {
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
quic?: {
|
||||
initStreamReceiveWindow?: number;
|
||||
maxStreamReceiveWindow?: number;
|
||||
initConnReceiveWindow?: number;
|
||||
maxConnReceiveWindow?: number;
|
||||
maxIdleTimeout?: string;
|
||||
maxIncomingStreams?: number;
|
||||
disablePathMTUDiscovery?: boolean;
|
||||
};
|
||||
bandwidth?: {
|
||||
up: string;
|
||||
down: string;
|
||||
};
|
||||
ignoreClientBandwidth?: boolean;
|
||||
speedTest?: boolean;
|
||||
disableUDP?: boolean;
|
||||
udpIdleTimeout?: string;
|
||||
resolver?: {
|
||||
type: string;
|
||||
tcp?: {
|
||||
addr: string;
|
||||
timeout: string;
|
||||
};
|
||||
udp?: {
|
||||
addr: string;
|
||||
timeout: string;
|
||||
};
|
||||
tls?: {
|
||||
addr: string;
|
||||
timeout: string;
|
||||
sni: string;
|
||||
insecure: boolean;
|
||||
};
|
||||
https?: {
|
||||
addr: string;
|
||||
timeout: string;
|
||||
sni: string;
|
||||
insecure: boolean;
|
||||
};
|
||||
};
|
||||
sniff?: {
|
||||
enable: boolean;
|
||||
timeout: string;
|
||||
rewriteDomain: boolean;
|
||||
tcpPorts?: string;
|
||||
udpPorts?: string;
|
||||
};
|
||||
acl?: {
|
||||
file?: string;
|
||||
inline?: string[];
|
||||
geoip?: string;
|
||||
geosite?: string;
|
||||
geoUpdateInterval?: string;
|
||||
};
|
||||
outbounds?: Hysteria2ServerConfigOutbound[];
|
||||
trafficStats: {
|
||||
listen: string;
|
||||
};
|
||||
masquerade?: {
|
||||
type: string;
|
||||
file?: {
|
||||
dir: string;
|
||||
};
|
||||
proxy?: {
|
||||
url: string;
|
||||
rewriteHost: boolean;
|
||||
insecure: boolean;
|
||||
};
|
||||
string?: {
|
||||
content: string;
|
||||
headers?: { [key: string]: string };
|
||||
statusCode?: number;
|
||||
};
|
||||
listenHTTP?: string;
|
||||
listenHTTPS?: string;
|
||||
forceHTTPS?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = {
|
||||
listen: ":443",
|
||||
tls: {
|
||||
cert: "",
|
||||
key: "",
|
||||
sniGuard: "",
|
||||
},
|
||||
acme: {
|
||||
domains: [],
|
||||
email: "",
|
||||
ca: "zerossl",
|
||||
listenHost: "0.0.0.0",
|
||||
dir: "my_acme_dir",
|
||||
type: "",
|
||||
http: {
|
||||
altPort: 8888,
|
||||
},
|
||||
tls: {
|
||||
altPort: 44333,
|
||||
},
|
||||
dns: {
|
||||
name: "gomommy",
|
||||
config: {},
|
||||
},
|
||||
disableHTTP: false,
|
||||
disableTLSALPN: false,
|
||||
altHTTPPort: 80,
|
||||
altTLSALPNPort: 443,
|
||||
},
|
||||
obfs: {
|
||||
type: "salamander",
|
||||
salamander: {
|
||||
password: "cry_me_a_r1ver",
|
||||
},
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: 8388608,
|
||||
maxStreamReceiveWindow: 8388608,
|
||||
initConnReceiveWindow: 20971520,
|
||||
maxConnReceiveWindow: 20971520,
|
||||
maxIdleTimeout: "30s",
|
||||
maxIncomingStreams: 1024,
|
||||
disablePathMTUDiscovery: false,
|
||||
},
|
||||
bandwidth: {
|
||||
up: "1 gbps",
|
||||
down: "1 gbps",
|
||||
},
|
||||
ignoreClientBandwidth: false,
|
||||
speedTest: false,
|
||||
disableUDP: false,
|
||||
udpIdleTimeout: "60s",
|
||||
resolver: {
|
||||
type: "",
|
||||
tcp: {
|
||||
addr: "8.8.8.8:53",
|
||||
timeout: "4s",
|
||||
},
|
||||
udp: {
|
||||
addr: "8.8.4.4:53",
|
||||
timeout: "4s",
|
||||
},
|
||||
tls: {
|
||||
addr: "1.1.1.1:853",
|
||||
timeout: "10s",
|
||||
sni: "cloudflare-dns.com",
|
||||
insecure: false,
|
||||
},
|
||||
https: {
|
||||
addr: "1.1.1.1:443",
|
||||
timeout: "10s",
|
||||
sni: "cloudflare-dns.com",
|
||||
insecure: false,
|
||||
},
|
||||
},
|
||||
sniff: {
|
||||
enable: true,
|
||||
timeout: "2s",
|
||||
rewriteDomain: false,
|
||||
tcpPorts: "80,443,8000-9000",
|
||||
udpPorts: "all",
|
||||
},
|
||||
acl: {
|
||||
file: "",
|
||||
inline: [],
|
||||
geoip: "",
|
||||
geosite: "",
|
||||
geoUpdateInterval: "168h",
|
||||
},
|
||||
outbounds: [],
|
||||
trafficStats: {
|
||||
listen: ":9999",
|
||||
},
|
||||
masquerade: {
|
||||
type: "",
|
||||
file: {
|
||||
dir: "",
|
||||
},
|
||||
proxy: {
|
||||
url: "",
|
||||
rewriteHost: true,
|
||||
insecure: false,
|
||||
},
|
||||
string: {
|
||||
content: "hello stupid world",
|
||||
headers: {},
|
||||
statusCode: 200,
|
||||
},
|
||||
listenHTTP: ":80",
|
||||
listenHTTPS: ":443",
|
||||
forceHTTPS: true,
|
||||
},
|
||||
};
|
||||
|
||||
export interface Hysteria2ServerConfigOutbound {
|
||||
name: string;
|
||||
type: string;
|
||||
socks5?: {
|
||||
addr: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
http?: {
|
||||
url: string;
|
||||
insecure: boolean;
|
||||
};
|
||||
direct?: {
|
||||
mode: string;
|
||||
bindIPv4?: string;
|
||||
bindIPv6?: string;
|
||||
bindDevice?: string;
|
||||
fastOpen?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultHysteria2ServerConfigOutbound: Hysteria2ServerConfigOutbound =
|
||||
{
|
||||
name: "",
|
||||
type: "socks5",
|
||||
socks5: {
|
||||
addr: "",
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
},
|
||||
http: {
|
||||
url: "",
|
||||
insecure: false,
|
||||
},
|
||||
direct: {
|
||||
mode: "auto",
|
||||
bindIPv4: undefined,
|
||||
bindIPv6: undefined,
|
||||
bindDevice: undefined,
|
||||
fastOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
export interface Tab {
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export interface Hysteria2AcmePathVo {
|
||||
crtPath: string;
|
||||
keyPath: string;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
import { Hysteria2ServerConfig } from "@/api/config/types";
|
||||
import request from "@/utils/request";
|
||||
import {
|
||||
Hysteria2KickDto,
|
||||
Hysteria2SubscribeVo,
|
||||
Hysteria2SubscribeUrlDto,
|
||||
Hysteria2UrlDto,
|
||||
Hysteria2UrlVo,
|
||||
} from "@/api/hysteria2/types";
|
||||
|
||||
export function hysteria2KickApi(
|
||||
data: Hysteria2KickDto
|
||||
): AxiosPromise<Hysteria2ServerConfig> {
|
||||
return request({
|
||||
url: "/hysteria2/hysteria2Kick",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function hysteria2SubscribeUrlApi(
|
||||
dto: Hysteria2SubscribeUrlDto
|
||||
): AxiosPromise<Hysteria2SubscribeVo> {
|
||||
return request({
|
||||
url: "/hysteria2/hysteria2SubscribeUrl",
|
||||
method: "get",
|
||||
params: dto,
|
||||
});
|
||||
}
|
||||
|
||||
export function hysteria2UrlApi(
|
||||
dto: Hysteria2UrlDto
|
||||
): AxiosPromise<Hysteria2UrlVo> {
|
||||
return request({
|
||||
url: "/hysteria2/hysteria2Url",
|
||||
method: "get",
|
||||
params: dto,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export interface Hysteria2KickDto {
|
||||
ids: number[];
|
||||
kickUtilTime: number;
|
||||
}
|
||||
|
||||
export interface Hysteria2SubscribeUrlDto {
|
||||
accountId: number;
|
||||
protocol: string;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export interface Hysteria2UrlDto {
|
||||
accountId: number;
|
||||
hostname: string;
|
||||
}
|
||||
|
||||
export interface Hysteria2SubscribeVo {
|
||||
url: string;
|
||||
qrCode: string;
|
||||
}
|
||||
|
||||
|
||||
export interface Hysteria2UrlVo {
|
||||
url: string;
|
||||
qrCode: string;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
import request from "@/utils/request";
|
||||
import {
|
||||
LogDto,
|
||||
LogExportDto,
|
||||
LogHysteria2Vo,
|
||||
LogSystemVo,
|
||||
} from "@/api/log/types";
|
||||
|
||||
export function logSystemApi(data: LogDto): AxiosPromise<PageVo<LogSystemVo>> {
|
||||
return request({
|
||||
url: "/log/logSystem",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function logHysteria2Api(
|
||||
data: LogDto
|
||||
): AxiosPromise<PageVo<LogHysteria2Vo>> {
|
||||
return request({
|
||||
url: "/log/logHysteria2",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function exportLogApi(data: LogExportDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/log/exportLog",
|
||||
method: "post",
|
||||
data: data,
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface LogDto {
|
||||
numLine: number;
|
||||
}
|
||||
|
||||
export interface LogExportDto {
|
||||
option: number;
|
||||
}
|
||||
|
||||
export interface LogSystemVo {
|
||||
clientIp: string;
|
||||
latencyTime: string;
|
||||
level: string;
|
||||
msg: string;
|
||||
reqMethod: string;
|
||||
reqUri: string;
|
||||
statusCode: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export interface LogHysteria2Vo {
|
||||
level: string;
|
||||
msg: string;
|
||||
time: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
import request from "@/utils/request";
|
||||
import { Hysteria2MonitorVo, SystemMonitorVo } from "@/api/monitor/types";
|
||||
|
||||
export function monitorSystemApi(): AxiosPromise<SystemMonitorVo> {
|
||||
return request({
|
||||
url: "/monitor/monitorSystem",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function monitorHysteria2Api(): AxiosPromise<Hysteria2MonitorVo> {
|
||||
return request({
|
||||
url: "/monitor/monitorHysteria2",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface SystemMonitorVo {
|
||||
huiVersion: string;
|
||||
cpuPercent: number;
|
||||
diskPercent: number;
|
||||
memPercent: number;
|
||||
}
|
||||
|
||||
export interface Hysteria2MonitorVo {
|
||||
userTotal: number;
|
||||
deviceTotal: number;
|
||||
version: string;
|
||||
running: boolean;
|
||||
}
|
||||
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M19.41 18l8.29-8.29a1 1 0 0 0-1.41-1.41L18 16.59l-8.29-8.3a1 1 0 0 0-1.42 1.42l8.3 8.29l-8.3 8.29A1 1 0 1 0 9.7 27.7l8.3-8.29l8.29 8.29a1 1 0 0 0 1.41-1.41z" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 395 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M26 17H10a1 1 0 0 0 0 2h16a1 1 0 0 0 0-2z" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 279 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M7 12l7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 12l7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 12H7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" ></path><path d="M3 3v18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 647 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 20 20"><path d="M3 5h14V3H3v2zm12 8V7H5v6h10zM3 17h14v-2H3v2z" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g transform="translate(24 0) scale(-1 1)"><g fill="none"><path d="M7 12l7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 12l7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 12H7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path><path d="M3 3v18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 693 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714739812671" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2441" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M798.72 819.2H682.666667v-68.266667h116.053333c80.213333 0 145.066667-64.853333 145.066667-145.066666S878.933333 460.8 798.72 460.8H750.933333v-34.133333c0-131.413333-107.52-238.933333-238.933333-238.933334s-238.933333 107.52-238.933333 238.933334v34.133333h-37.546667c-80.213333 0-145.066667 64.853333-145.066667 145.066667S155.306667 750.933333 235.52 750.933333H341.333333v68.266667h-105.813333C117.76 819.2 20.48 723.626667 20.48 605.866667c0-107.52 80.213333-197.973333 184.32-211.626667C221.866667 240.64 353.28 119.466667 512 119.466667s288.426667 119.466667 305.493333 274.773333c109.226667 10.24 194.56 100.693333 194.56 211.626667-1.706667 117.76-97.28 213.333333-213.333333 213.333333z" fill="#191919" p-id="2442"></path><path d="M482.986667 515.413333h68.266666v307.2h-68.266666z" fill="#00C97C" p-id="2443"></path><path d="M515.413333 901.12l-150.186666-148.48 47.786666-49.493333 102.4 102.4 100.693334-102.4 47.786666 49.493333z" fill="#00C97C" p-id="2444"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714720229787" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8983" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M512 720m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="8984" fill="#000000"></path><path d="M480 416v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8z" p-id="8985" fill="#000000"></path><path d="M955.7 856l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48z m-783.5-27.9L512 239.9l339.8 588.2H172.2z" p-id="8986" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 768 B |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M49.217 41.329l-.136-35.24c-.06-2.715-2.302-4.345-5.022-4.405h-3.65c-2.712-.06-4.866 2.303-4.806 5.016l.152 19.164-24.151-23.79a6.698 6.698 0 0 0-9.499 0 6.76 6.76 0 0 0 0 9.526l23.93 23.713-18.345.074c-2.712-.069-5.228 1.813-5.64 5.02v3.462c.069 2.721 2.31 4.97 5.022 5.03l35.028-.207c.052.005.087.025.133.025l2.457.054a4.626 4.626 0 0 0 3.436-1.38c.88-.874 1.205-2.096 1.169-3.462l-.262-2.465c0-.048.182-.081.182-.136h.002zm52.523 51.212l18.32-.073c2.713.06 5.224-1.609 5.64-4.815v-3.462c-.068-2.722-2.317-4.97-5.021-5.04l-34.58.21c-.053 0-.086-.021-.138-.021l-2.451-.06a4.64 4.64 0 0 0-3.445 1.381c-.885.868-1.201 2.094-1.174 3.46l.27 2.46c.005.06-.177.095-.177.141l.141 34.697c.069 2.713 2.31 4.338 5.022 4.397l3.45.006c2.705.062 4.867-2.31 4.8-5.026l-.153-18.752 24.151 23.946a6.69 6.69 0 0 0 9.494 0 6.747 6.747 0 0 0 0-9.523L101.74 92.54v.001zM48.125 80.662a4.636 4.636 0 0 0-3.437-1.382l-2.457.06c-.05 0-.082.022-.137.022l-35.025-.21c-2.712.07-4.957 2.318-5.022 5.04v3.462c.409 3.206 2.925 4.874 5.633 4.814l18.554.06-24.132 23.928c-2.62 2.626-2.62 6.89 0 9.524a6.694 6.694 0 0 0 9.496 0l24.155-23.79-.155 18.866c-.06 2.722 2.094 5.093 4.801 5.025h3.65c2.72-.069 4.962-1.685 5.022-4.406l.141-34.956c0-.05-.182-.082-.182-.136l.262-2.46c.03-1.366-.286-2.592-1.166-3.46h-.001zM80.08 47.397a4.62 4.62 0 0 0 3.443 1.374l2.45-.054c.055 0 .088-.02.143-.028l35.08.21c2.712-.062 4.953-2.312 5.021-5.033l.009-3.463c-.417-3.211-2.937-5.084-5.64-5.025l-18.615-.073 23.917-23.715c2.63-2.623 2.63-6.879.008-9.513a6.691 6.691 0 0 0-9.494 0L92.251 26.016l.155-19.312c.065-2.713-2.097-5.085-4.802-5.025h-3.45c-2.713.069-4.954 1.693-5.022 4.406l-.139 35.247c0 .054.18.088.18.136l-.267 2.465c-.028 1.366.288 2.588 1.174 3.463v.001z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714739827633" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2765" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M512 989.866667C249.173333 989.866667 34.133333 774.826667 34.133333 512S249.173333 34.133333 512 34.133333s477.866667 215.04 477.866667 477.866667-215.04 477.866667-477.866667 477.866667z m0-887.466667C286.72 102.4 102.4 286.72 102.4 512s184.32 409.6 409.6 409.6 409.6-184.32 409.6-409.6S737.28 102.4 512 102.4z" fill="#191919" p-id="2766"></path><path d="M363.52 725.333333l-44.373333-51.2 158.72-143.36V238.933333h68.266666v322.56z" fill="#00C97C" p-id="2767"></path></svg>
|
||||
|
After Width: | Height: | Size: 807 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
|
||||
|
After Width: | Height: | Size: 944 B |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M38.47 52L52 38.462l-23.648-23.67L43.209 0H.035L0 43.137l14.757-14.865L38.47 52zm74.773 47.726L89.526 76 76 89.536l23.648 23.672L84.795 128h43.174L128 84.863l-14.757 14.863zM89.538 52l23.668-23.648L128 43.207V.038L84.866 0 99.73 14.76 76 38.472 89.538 52zM38.46 76L14.792 99.651 0 84.794v43.173l43.137.033-14.865-14.757L52 89.53 38.46 76z"/></svg>
|
||||
|
After Width: | Height: | Size: 421 B |
@@ -0,0 +1 @@
|
||||
<svg t="1650814907622" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="52318" width="200" height="200"><path d="M958.400956 451.54921c-0.058328-5.760191-2.597151-11.215436-6.965645-14.97097L524.345166 69.511143c-7.498788-6.445806-18.581194-6.445806-26.079982 0L309.582871 231.6755l0-102.017488c0-11.04966-8.901741-19.532869-19.951401-19.532869l-88.034009 0c-11.048637 0-19.928888 8.482185-19.928888 19.532869l0 211.954343L71.176063 436.57824c-4.423753 3.800559-6.967692 9.341762-6.967692 15.173584l0 105.500822c0 7.819083 4.554736 14.921851 11.660574 18.183128 2.670829 1.226944 5.51562 1.824555 8.343015 1.824555 4.699022 0 9.346879-1.654686 13.048177-4.836145l53.29788-45.825698 0 324.100516c0 60.677964 49.364291 110.042255 110.042255 110.042255L764.792447 960.741257c60.677964 0 110.042255-49.364291 110.042255-110.042255L874.834702 527.026228l51.585889 44.335764c5.955642 5.119601 14.356986 6.282077 21.481244 2.965541 7.122211-3.313465 11.645225-10.488889 11.565407-18.342764L958.400956 451.54921zM221.578538 150.034085l48.095391 0 0 115.941616-48.095391 41.336454L221.578538 150.034085zM570.718333 920.725892 436.666244 920.725892 436.666244 700.642404c0-11.031241 8.976442-20.007683 20.007683-20.007683l94.0357 0c11.031241 0 20.007683 8.976442 20.007683 20.007683L570.71731 920.725892zM834.818313 495.895207l0 354.803795c0 38.612413-31.414477 70.02689-70.02689 70.02689l-154.058748 0L610.732675 700.642404c0-33.096792-26.926256-60.023048-60.023048-60.023048l-94.0357 0c-33.096792 0-60.023048 26.926256-60.023048 60.023048l0 220.084511L260.59925 920.726915c-38.612413 0-70.02689-31.414477-70.02689-70.02689L190.57236 495.895207c0-1.172709-0.121773-2.314719-0.315178-3.432169l322.113255-276.958846 322.70268 277.348726C834.921667 493.848595 834.818313 494.858598 834.818313 495.895207zM525.411451 173.947727c-7.502881-6.445806-18.587334-6.446829-26.086122 0.00307L104.223736 513.663896l0-52.726875 407.081439-349.870436 407.176606 349.9523 0.521886 51.205219L525.411451 173.947727z" p-id="52319"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714720422565" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15443" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M235.5 871.691v-740h98v304h385v-304h98v740h-98v-349h-385v349h-98z" p-id="15444" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 440 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714745205151" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9116" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M336 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="9117" fill="#000000"></path><path d="M688 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="9118" fill="#000000"></path><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2-44.3-18.7-84.1-45.6-118.3-79.8-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8c18.7-44.3 45.6-84.1 79.8-118.3 34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2 44.3 18.7 84.1 45.6 118.3 79.8 34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8c-18.7 44.3-45.6 84.1-79.8 118.2z" p-id="9119" fill="#000000"></path><path d="M664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-0.3-4.2-3.9-7.4-8.1-7.4H360c-4.6 0-8.2 3.8-8 8.4 4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6c0.2-4.6-3.4-8.4-8-8.4z" p-id="9120" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg t="1675576810577" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1553" width="200" height="200"><path d="M379.392 460.8l114.688 114.688-42.496 102.4L307.2 532.48l-168.96 168.96-71.68-72.704L234.496 460.8l-45.056-45.056c-27.136-27.136-51.2-66.56-66.56-108.544h112.64c7.68 14.336 16.896 27.136 26.112 35.84l45.568 46.08 45.056-45.056C382.976 312.32 409.6 247.808 409.6 204.8H0V102.4h256V0h102.4v102.4h256v102.4h-102.4c0 70.144-37.888 161.28-87.04 210.944L378.88 460.8z m196.608 409.6L512 1024H409.6l256-614.4h102.4l256 614.4h-102.4l-64-153.6h-281.6z m42.496-102.4h196.608L716.8 532.48 618.496 768z" p-id="1554" data-spm-anchor-id="a313x.7781069.0.i0" class="selected"></path></svg>
|
||||
|
After Width: | Height: | Size: 730 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714720786193" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10390" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M688 312v-48c0-4.4-3.6-8-8-8H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8zM296 400c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H296z" p-id="10391" fill="#000000"></path><path d="M440 852H208V148h560v344c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V108c0-17.7-14.3-32-32-32H168c-17.7 0-32 14.3-32 32v784c0 17.7 14.3 32 32 32h272c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z" p-id="10392" fill="#000000"></path><path d="M885.7 903.5l-93.3-93.3C814.7 780.7 828 743.9 828 704c0-97.2-78.8-176-176-176s-176 78.8-176 176 78.8 176 176 176c35.8 0 69-10.7 96.8-29l94.7 94.7c1.6 1.6 3.6 2.3 5.6 2.3s4.1-0.8 5.6-2.3l31-31c3.1-3.1 3.1-8.1 0-11.2zM652 816c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z" p-id="10393" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714755103595" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8918" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M193 796c0 17.7 14.3 32 32 32h574c17.7 0 32-14.3 32-32V563c0-176.2-142.8-319-319-319S193 386.8 193 563v233z m72-233c0-136.4 110.6-247 247-247s247 110.6 247 247v193H404V585c0-5.5-4.5-10-10-10h-44c-5.5 0-10 4.5-10 10v171h-75V563zM216.9 310.5l39.6-39.6c3.1-3.1 3.1-8.2 0-11.3l-67.9-67.9c-3.1-3.1-8.2-3.1-11.3 0l-39.6 39.6c-3.1 3.1-3.1 8.2 0 11.3l67.9 67.9c3.1 3.1 8.1 3.1 11.3 0zM886.5 231.3l-39.6-39.6c-3.1-3.1-8.2-3.1-11.3 0l-67.9 67.9c-3.1 3.1-3.1 8.2 0 11.3l39.6 39.6c3.1 3.1 8.2 3.1 11.3 0l67.9-67.9c3.1-3.2 3.1-8.2 0-11.3zM832 892H192c-17.7 0-32 14.3-32 32v24c0 4.4 3.6 8 8 8h688c4.4 0 8-3.6 8-8v-24c0-17.7-14.3-32-32-32zM484 180h56c4.4 0 8-3.6 8-8V76c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v96c0 4.4 3.6 8 8 8z" p-id="8919" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802846045" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2750" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M868.593046 403.832442c-30.081109-28.844955-70.037123-44.753273-112.624057-44.753273L265.949606 359.079168c-42.554188 0-82.510202 15.908318-112.469538 44.690852-30.236652 28.782533-46.857191 67.222007-46.857191 108.198258l0 294.079782c0 40.977273 16.619516 79.414701 46.702672 108.136859 29.959336 28.844955 70.069869 44.814672 112.624057 44.814672l490.019383 0c42.585911 0 82.696444-15.969717 112.624057-44.814672 30.082132-28.844955 46.579875-67.222007 46.579875-108.136859L915.172921 511.968278C915.171897 471.053426 898.675178 432.677397 868.593046 403.832442zM841.821309 806.049083c0 22.098297-8.882298 42.772152-25.099654 58.306964-16.154935 15.661701-37.81935 24.203238-60.752666 24.203238L265.949606 888.559285c-22.934339 0-44.567032-8.54256-60.877509-24.264637-16.186657-15.474436-25.067932-36.148291-25.067932-58.246589L180.004165 511.968278c0-22.035876 8.881274-42.772152 25.192775-58.307987 16.186657-15.536858 37.81935-24.139793 60.753689-24.139793l490.019383 0c22.933315 0 44.597731 8.602935 60.752666 24.139793 16.21838 15.535835 25.099654 36.272112 25.099654 58.307987L841.822332 806.049083zM510.974136 135.440715c114.914216 0 208.318536 89.75214 208.318536 200.055338l73.350588 0c0-149.113109-126.366036-270.496667-281.669124-270.496667-155.333788 0-281.699824 121.383558-281.699824 270.496667l73.350588 0C302.623877 225.193879 396.059919 135.440715 510.974136 135.440715zM474.299865 747.244792l73.350588 0L547.650453 629.576859l-73.350588 0L474.299865 747.244792z" p-id="2751"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714739843967" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2926" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M510.293333 972.8L61.44 460.8 235.52 51.2h551.253333l174.08 409.6-450.56 512zM141.653333 448.853333L510.293333 870.4l370.346667-421.546667L740.693333 119.466667h-460.8L141.653333 448.853333z" fill="#191919" p-id="2927"></path><path d="M510.293333 631.466667L332.8 431.786667l49.493333-44.373334 128 141.653334 129.706667-141.653334 49.493333 44.373334z" fill="#00C97C" p-id="2928"></path></svg>
|
||||
|
After Width: | Height: | Size: 725 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 512 512"><path d="M400 148l-21.12-24.57A191.43 191.43 0 0 0 240 64C134 64 48 150 48 256s86 192 192 192a192.09 192.09 0 0 0 181.07-128" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="32"></path><path d="M464 68.45V220a4 4 0 0 1-4 4H308.45a4 4 0 0 1-2.83-6.83L457.17 65.62a4 4 0 0 1 6.83 2.83z" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 561 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714720044650" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8586" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M312.1 591.5c3.1 3.1 8.2 3.1 11.3 0l101.8-101.8 86.1 86.2c3.1 3.1 8.2 3.1 11.3 0l226.3-226.5c3.1-3.1 3.1-8.2 0-11.3l-36.8-36.8c-3.1-3.1-8.2-3.1-11.3 0L517 485.3l-86.1-86.2c-3.1-3.1-8.2-3.1-11.3 0L275.3 543.4c-3.1 3.1-3.1 8.2 0 11.3l36.8 36.8z" p-id="8587" fill="#000000"></path><path d="M904 160H548V96c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H120c-17.7 0-32 14.3-32 32v520c0 17.7 14.3 32 32 32h356.4v32L311.6 884.1c-3.7 2.4-4.7 7.3-2.3 11l30.3 47.2v0.1c2.4 3.7 7.4 4.7 11.1 2.3L512 838.9l161.3 105.8c3.7 2.4 8.7 1.4 11.1-2.3v-0.1l30.3-47.2c2.4-3.7 1.3-8.6-2.3-11L548 776.3V744h356c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 512H160V232h704v440z" p-id="8588" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714719706106" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9222" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56c10.1-8.6 13.8-22.6 9.3-35.2l-0.9-2.6c-18.1-50.5-44.9-96.9-79.7-137.9l-1.8-2.1c-8.6-10.1-22.5-13.9-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85c-2.4-13.1-12.7-23.3-25.8-25.7l-2.7-0.5c-52.1-9.4-106.9-9.4-159 0l-2.7 0.5c-13.1 2.4-23.4 12.6-25.8 25.7l-15.8 85.4c-35.9 13.6-69.2 32.9-99 57.4l-81.9-29.1c-12.5-4.4-26.5-0.7-35.1 9.5l-1.8 2.1c-34.8 41.1-61.6 87.5-79.7 137.9l-0.9 2.6c-4.5 12.5-0.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5c-10.1 8.6-13.8 22.6-9.3 35.2l0.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1c8.6 10.1 22.5 13.9 35.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4c2.4 13.1 12.7 23.3 25.8 25.7l2.7 0.5c26.1 4.7 52.8 7.1 79.5 7.1 26.7 0 53.5-2.4 79.5-7.1l2.7-0.5c13.1-2.4 23.4-12.6 25.8-25.7l15.7-85c36.2-13.6 69.7-32.9 99.7-57.6l81.3 28.9c12.5 4.4 26.5 0.7 35.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l0.9-2.6c4.5-12.3 0.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9c-11.3 26.1-25.6 50.7-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97c-28.1 3.2-56.8 3.2-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9z" p-id="9223" fill="#000000"></path><path d="M512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176z m79.2 255.2C570 602.3 541.9 614 512 614c-29.9 0-58-11.7-79.2-32.8C411.7 560 400 531.9 400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8C612.3 444 624 472.1 624 502c0 29.9-11.7 58-32.8 79.2z" p-id="9224" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714755209531" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9116" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M752 664c-28.5 0-54.8 10-75.4 26.7L469.4 540.8c1.7-9.3 2.6-19 2.6-28.8s-0.9-19.4-2.6-28.8l207.2-149.9C697.2 350 723.5 360 752 360c66.2 0 120-53.8 120-120s-53.8-120-120-120-120 53.8-120 120c0 11.6 1.6 22.7 4.7 33.3L439.9 415.8C410.7 377.1 364.3 352 312 352c-88.4 0-160 71.6-160 160s71.6 160 160 160c52.3 0 98.7-25.1 127.9-63.8l196.8 142.5c-3.1 10.6-4.7 21.8-4.7 33.3 0 66.2 53.8 120 120 120s120-53.8 120-120-53.8-120-120-120z m0-476c28.7 0 52 23.3 52 52s-23.3 52-52 52-52-23.3-52-52 23.3-52 52-52zM312 600c-48.5 0-88-39.5-88-88s39.5-88 88-88 88 39.5 88 88-39.5 88-88 88z m440 236c-28.7 0-52-23.3-52-52s23.3-52 52-52 52 23.3 52 52-23.3 52-52 52z" p-id="9117" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1016 B |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
|
||||
|
After Width: | Height: | Size: 211 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714739816180" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2603" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M798.72 836.266667H682.666667v-68.266667h116.053333c80.213333 0 145.066667-64.853333 145.066667-145.066667S877.226667 477.866667 798.72 477.866667H750.933333v-34.133334c0-131.413333-107.52-238.933333-238.933333-238.933333s-238.933333 107.52-238.933333 238.933333v34.133334h-37.546667c-80.213333 0-145.066667 64.853333-145.066667 145.066666S155.306667 768 235.52 768H341.333333v68.266667h-105.813333C117.76 836.266667 20.48 740.693333 20.48 622.933333c0-107.52 80.213333-197.973333 184.32-211.626666C221.866667 257.706667 353.28 136.533333 512 136.533333s288.426667 119.466667 305.493333 274.773334c109.226667 10.24 194.56 100.693333 194.56 211.626666-1.706667 117.76-97.28 213.333333-213.333333 213.333334z" fill="#191919" p-id="2604"></path><path d="M477.866667 563.2h68.266666v307.2h-68.266666z" fill="#00C97C" p-id="2605"></path><path d="M616.106667 680.96L515.413333 580.266667l-102.4 100.693333-47.786666-47.786667 150.186666-150.186666 148.48 150.186666z" fill="#00C97C" p-id="2606"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714745361102" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9516" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M858.5 763.6c-18.9-44.8-46.1-85-80.6-119.5-34.5-34.5-74.7-61.6-119.5-80.6-0.4-0.2-0.8-0.3-1.2-0.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-0.4 0.2-0.8 0.3-1.2 0.5-44.8 18.9-85 46-119.5 80.6-34.5 34.5-61.6 74.7-80.6 119.5C146.9 807.5 137 854 136 901.8c-0.1 4.5 3.5 8.2 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c0.1 4.4 3.6 7.8 8 7.8h60c4.5 0 8.1-3.7 8-8.2-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z" p-id="9517" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714745286527" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9317" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M824.2 699.9c-25.4-25.4-54.7-45.7-86.4-60.4C783.1 602.8 812 546.8 812 484c0-110.8-92.4-201.7-203.2-200-109.1 1.7-197 90.6-197 200 0 62.8 29 118.8 74.2 155.5-31.7 14.7-60.9 34.9-86.4 60.4C345 754.6 314 826.8 312 903.8c-0.1 4.5 3.5 8.2 8 8.2h56c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5C493.8 707.7 551.1 684 612 684c60.9 0 118.2 23.7 161.3 66.8C814.5 792 838 846.3 840 904.3c0.1 4.3 3.7 7.7 8 7.7h56c4.5 0 8.1-3.7 8-8.2-2-77-33-149.2-87.8-203.9zM612 612c-34.2 0-66.4-13.3-90.5-37.5-24.5-24.5-37.9-57.1-37.5-91.8 0.3-32.8 13.4-64.5 36.3-88 24-24.6 56.1-38.3 90.4-38.7 33.9-0.3 66.8 12.9 91 36.6 24.8 24.3 38.4 56.8 38.4 91.4 0 34.2-13.3 66.3-37.5 90.5-24.2 24.2-56.4 37.5-90.6 37.5z" p-id="9318" fill="#000000"></path><path d="M361.5 510.4c-0.9-8.7-1.4-17.5-1.4-26.4 0-15.9 1.5-31.4 4.3-46.5 0.7-3.6-1.2-7.3-4.5-8.8-13.6-6.1-26.1-14.5-36.9-25.1-25.8-25.2-39.7-59.3-38.7-95.4 0.9-32.1 13.8-62.6 36.3-85.6 24.7-25.3 57.9-39.1 93.2-38.7 31.9 0.3 62.7 12.6 86 34.4 7.9 7.4 14.7 15.6 20.4 24.4 2 3.1 5.9 4.4 9.3 3.2 17.6-6.1 36.2-10.4 55.3-12.4 5.6-0.6 8.8-6.6 6.3-11.6-32.5-64.3-98.9-108.7-175.7-109.9-110.9-1.7-203.3 89.2-203.3 199.9 0 62.8 28.9 118.8 74.2 155.5-31.8 14.7-61.1 35-86.5 60.4-54.8 54.7-85.8 126.9-87.8 204-0.1 4.5 3.5 8.2 8 8.2h56.1c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5 29.4-29.4 65.4-49.8 104.7-59.7 3.9-1 6.5-4.7 6-8.7z" p-id="9319" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div
|
||||
@click="toggleClick"
|
||||
class="px-[15px] hover:bg-gray-50 cursor-pointer h-[50px] leading-[50px] dark:hover:bg-[var(--el-fill-color-light)]"
|
||||
>
|
||||
<svg
|
||||
:class="{ 'is-active': isActive }"
|
||||
class="hamburger"
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="color: #fff !important"
|
||||
>
|
||||
<path
|
||||
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
isActive: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["toggleClick"]);
|
||||
|
||||
function toggleClick() {
|
||||
emit("toggleClick");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hamburger {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: -4px;
|
||||
|
||||
&.is-active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<draggable class="flex gap-2" :list="tags" item-key="id" animation="200">
|
||||
<template #item="{ element }">
|
||||
<el-tag closable @close="handleClose(element)" size="large">
|
||||
{{ element }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-input
|
||||
v-if="inputVisible"
|
||||
ref="inputRef"
|
||||
v-model="tag"
|
||||
class="w-50"
|
||||
@keyup.enter="handleConfirm"
|
||||
@blur="handleConfirm"
|
||||
/>
|
||||
<el-button v-else @click="showInput">+</el-button>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import draggable from "vuedraggable";
|
||||
import { ElInput } from "element-plus";
|
||||
import { PropType } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
tags: {
|
||||
required: false,
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:tags", value: string[]): void;
|
||||
}>();
|
||||
|
||||
const tags = useVModel(props, "tags", emit);
|
||||
|
||||
const inputRef = ref(ElInput);
|
||||
|
||||
const state = reactive({
|
||||
tag: "",
|
||||
inputVisible: false,
|
||||
});
|
||||
|
||||
const { tag, inputVisible } = toRefs(state);
|
||||
|
||||
const showInput = () => {
|
||||
state.inputVisible = true;
|
||||
nextTick(() => {
|
||||
inputRef.value!.input!.focus();
|
||||
});
|
||||
};
|
||||
const handleConfirm = (): void => {
|
||||
const newTag = state.tag.trim();
|
||||
if (newTag && !tags.value?.includes(newTag)) {
|
||||
tags.value?.push(newTag);
|
||||
state.tag = "";
|
||||
}
|
||||
state.inputVisible = false;
|
||||
};
|
||||
|
||||
const handleClose = (tag: string): void => {
|
||||
const index = tags.value?.indexOf(tag.trim());
|
||||
if (index !== -1) {
|
||||
tags.value?.splice(index, 1);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flex.gap-2 {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { locale } = useI18n();
|
||||
|
||||
function handleLanguageChange(lang: string) {
|
||||
locale.value = lang;
|
||||
appStore.changeLanguage(lang);
|
||||
if (lang == "en") {
|
||||
ElMessage.success("Switch Language Successful!");
|
||||
} else {
|
||||
ElMessage.success("Язык переключён");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleLanguageChange">
|
||||
<div>
|
||||
<svg-icon icon-class="language" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :disabled="appStore.language === 'ru'" command="ru">
|
||||
Русский
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :disabled="appStore.language === 'en'" command="en">
|
||||
English
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<el-tag
|
||||
:key="key"
|
||||
v-for="(value, key) in mapObject"
|
||||
@close="handleClose(key)"
|
||||
@click="handleInfo(key)"
|
||||
size="large"
|
||||
closable
|
||||
>
|
||||
{{ key }}
|
||||
</el-tag>
|
||||
|
||||
<el-button @click="handleAdd">+</el-button>
|
||||
|
||||
<el-dialog
|
||||
:title="dialog.title"
|
||||
v-model="dialog.visible"
|
||||
width="600px"
|
||||
append-to-body
|
||||
@close="closeDialog"
|
||||
>
|
||||
<el-form
|
||||
ref="dataFormRef"
|
||||
:rules="dataFormRules"
|
||||
label-position="top"
|
||||
:model="dataForm"
|
||||
>
|
||||
<el-form-item label="key" prop="key">
|
||||
<el-input v-model="dataForm.key" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="value" prop="value">
|
||||
<el-input v-model="dataForm.value" clearable />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm"
|
||||
>{{ $t("common.confirm") }}
|
||||
</el-button>
|
||||
<el-button @click="closeDialog">{{ $t("common.cancel") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="infoDialog.title"
|
||||
v-model="infoDialog.visible"
|
||||
width="600px"
|
||||
append-to-body
|
||||
@close="infoDialog.visible = false"
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="key" prop="key">
|
||||
<el-tag>{{ dataInfo.key }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="value" prop="value">
|
||||
<el-tag>{{ dataInfo.value }}</el-tag>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="infoDialog.visible = false"
|
||||
>{{ $t("common.cancel") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "mapObject",
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
|
||||
interface Form {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
mapObject: {
|
||||
required: false,
|
||||
type: Object as PropType<{ [key: string]: string }>,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:mapObject", value: { [key: string]: string }): void;
|
||||
}>();
|
||||
|
||||
const mapObject = useVModel(props, "mapObject", emit);
|
||||
|
||||
const dataFormRef = ref(ElForm);
|
||||
|
||||
const dataFormRules = {
|
||||
key: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
value: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
dataForm: {
|
||||
key: "",
|
||||
value: "",
|
||||
} as Form,
|
||||
dialog: {
|
||||
title: "Add",
|
||||
visible: false,
|
||||
} as DialogType,
|
||||
infoDialog: {
|
||||
title: "Info",
|
||||
visible: false,
|
||||
},
|
||||
dataInfo: {
|
||||
key: "",
|
||||
value: "",
|
||||
} as Form,
|
||||
});
|
||||
|
||||
const { dataForm, dialog, infoDialog, dataInfo } = toRefs(state);
|
||||
|
||||
const handleAdd = () => {
|
||||
state.dialog.visible = true;
|
||||
};
|
||||
|
||||
const handleClose = (key: string): void => {
|
||||
delete mapObject.value[key];
|
||||
};
|
||||
|
||||
const handleInfo = (key: string) => {
|
||||
state.dataInfo = {
|
||||
key: key,
|
||||
value: mapObject.value[key] || "",
|
||||
};
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
dataFormRef.value.validate((valid: any) => {
|
||||
if (valid) {
|
||||
if (mapObject.value[state.dataForm.key]) {
|
||||
ElMessage.error("key cannot be repeated");
|
||||
return;
|
||||
}
|
||||
mapObject.value[state.dataForm.key] = state.dataForm.value;
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeDialog = (): void => {
|
||||
state.dialog.visible = false;
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flex.gap-2 {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div :class="'pagination ' + { hidden: hidden }">
|
||||
<el-pagination
|
||||
:background="background"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:layout="layout"
|
||||
:page-sizes="pageSizes"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import { scrollTo } from "@/utils/scroll-to";
|
||||
|
||||
const props = defineProps({
|
||||
total: {
|
||||
required: true,
|
||||
type: Number as PropType<number>,
|
||||
default: 0,
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
pageSizes: {
|
||||
type: Array as PropType<number[]>,
|
||||
default() {
|
||||
return [10, 20, 30, 50];
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: "total, sizes, prev, pager, next, jumper",
|
||||
},
|
||||
background: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
autoScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["pagination"]);
|
||||
|
||||
const currentPage = useVModel(props, "page", emit);
|
||||
|
||||
const pageSize = useVModel(props, "limit", emit);
|
||||
|
||||
function handleSizeChange(val: number) {
|
||||
emit("pagination", { page: currentPage, limit: val });
|
||||
if (props.autoScroll) {
|
||||
scrollTo(0, 800);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCurrentChange(val: number) {
|
||||
currentPage.value = val;
|
||||
emit("pagination", { page: val, limit: props.limit });
|
||||
if (props.autoScroll) {
|
||||
scrollTo(0, 800);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pagination {
|
||||
padding: 12px;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
import { addClass, removeClass } from "@/utils/index";
|
||||
|
||||
const show = ref(false);
|
||||
|
||||
defineProps({
|
||||
buttonTop: {
|
||||
default: 250,
|
||||
type: Number,
|
||||
},
|
||||
});
|
||||
|
||||
watch(show, (value) => {
|
||||
if (value) {
|
||||
addEventClick();
|
||||
}
|
||||
if (value) {
|
||||
addClass(document.body, "showRightPanel");
|
||||
} else {
|
||||
removeClass(document.body, "showRightPanel");
|
||||
}
|
||||
});
|
||||
|
||||
function addEventClick() {
|
||||
window.addEventListener("click", closeSidebar, { passive: true });
|
||||
}
|
||||
|
||||
function closeSidebar(evt: any) {
|
||||
// 主题选择点击不关闭
|
||||
let parent = evt.target.closest(".right-panel-container");
|
||||
if (!parent) {
|
||||
show.value = false;
|
||||
window.removeEventListener("click", closeSidebar);
|
||||
}
|
||||
}
|
||||
|
||||
const rightPanel = ref();
|
||||
|
||||
function insertToBody() {
|
||||
const body = document.querySelector("body") as any;
|
||||
body.insertBefore(rightPanel.value, body.firstChild);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
insertToBody();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
rightPanel.value.remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ show: show }" ref="rightPanel">
|
||||
<div class="right-panel-overlay" />
|
||||
<div class="right-panel-container">
|
||||
<div
|
||||
class="right-panel-btn"
|
||||
:style="{
|
||||
top: buttonTop + 'px',
|
||||
}"
|
||||
@click="show = !show"
|
||||
>
|
||||
<i-ep-close v-show="show" />
|
||||
<i-ep-setting v-show="!show" />
|
||||
</div>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.showRightPanel {
|
||||
position: relative;
|
||||
width: calc(100% - 15px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.right-panel-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.right-panel-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 100vh;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
box-shadow: 0 0 15px 0 rgb(0 0 0 / 5%);
|
||||
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
transform: translate(100%);
|
||||
}
|
||||
|
||||
.show {
|
||||
transition: all 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
|
||||
.right-panel-overlay {
|
||||
z-index: 99;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.right-panel-container {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel-btn {
|
||||
position: absolute;
|
||||
left: -36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: var(--el-color-white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 6px 0 0 6px;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: -10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const sizeOptions = ref([
|
||||
{ label: "默认", value: "default" },
|
||||
{ label: "大型", value: "large" },
|
||||
{ label: "小型", value: "small" },
|
||||
]);
|
||||
|
||||
function handleSizeChange(size: string) {
|
||||
appStore.changeSize(size);
|
||||
ElMessage.success("切换布局大小成功");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleSizeChange">
|
||||
<div>
|
||||
<svg-icon icon-class="size" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item of sizeOptions"
|
||||
:key="item.value"
|
||||
:disabled="appStore.size == item.value"
|
||||
:command="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-icon"
|
||||
:style="'width:' + size + ';height:' + size"
|
||||
>
|
||||
<use :xlink:href="symbolId" :fill="color" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "icon",
|
||||
},
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "1em",
|
||||
},
|
||||
});
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
overflow: hidden;
|
||||
vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
|
||||
outline: none;
|
||||
fill: currentcolor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input-number
|
||||
v-model="capacity"
|
||||
placeholder="Please enter a value"
|
||||
:min="-1"
|
||||
:controls="false"
|
||||
:precision="0"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
/>
|
||||
<el-select
|
||||
v-model="unit"
|
||||
:placeholder="$t('account.unit')"
|
||||
style="width: 100px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in units"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import {
|
||||
calculateBytes,
|
||||
formatStorageCapacity,
|
||||
formatStorageUnit,
|
||||
} from "@/utils/byte";
|
||||
|
||||
const units = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
|
||||
|
||||
const props = defineProps({
|
||||
valueTmp: {
|
||||
type: Number as PropType<number>,
|
||||
required: true,
|
||||
},
|
||||
setValue: {
|
||||
type: Function as PropType<(newValue: number) => void>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
capacity: 0,
|
||||
unit: "GB",
|
||||
});
|
||||
|
||||
const { capacity, unit } = toRefs(state);
|
||||
|
||||
watch(
|
||||
[capacity, unit],
|
||||
([newC, newU]) => {
|
||||
const newValue = calculateBytes(newC, newU);
|
||||
props.setValue(newValue);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.valueTmp,
|
||||
(newValue) => {
|
||||
state.capacity = formatStorageCapacity(newValue);
|
||||
state.unit = formatStorageUnit(newValue);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { App } from "vue";
|
||||
|
||||
import { hasRole } from "./permission";
|
||||
|
||||
// 全局注册 directive
|
||||
export function setupDirective(app: App<Element>) {
|
||||
// 使 v-hasRole 在所有组件中都可用
|
||||
app.directive("hasRole", hasRole);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useAccountStoreHook } from "@/store/modules/account";
|
||||
import { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
/**
|
||||
* 角色权限
|
||||
*/
|
||||
export const hasRole: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
const { value } = binding;
|
||||
|
||||
if (value) {
|
||||
const requiredRoles = value; // DOM绑定需要的角色编码
|
||||
const { roles } = useAccountStoreHook();
|
||||
const hasRole = roles.some((perm) => {
|
||||
return requiredRoles.includes(perm);
|
||||
});
|
||||
|
||||
if (!hasRole) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
} else {
|
||||
throw new Error("need roles! Like v-has-role=\"['admin', 'user']\"");
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
|
||||
const appStore = useAppStore();
|
||||
import enLocale from "./package/en";
|
||||
import ruLocale from "./package/ru";
|
||||
|
||||
const messages = {
|
||||
ru: {
|
||||
...ruLocale,
|
||||
},
|
||||
en: {
|
||||
...enLocale,
|
||||
},
|
||||
};
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: appStore.language,
|
||||
messages: messages,
|
||||
globalInjection: true,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,324 @@
|
||||
export default {
|
||||
// 路由国际化
|
||||
route: {
|
||||
account: "Account",
|
||||
accountList: "Account Manage",
|
||||
hysteria: "Hysteria",
|
||||
hysteriaList: "Hysteria Manage",
|
||||
config: "System",
|
||||
configList: "System Config",
|
||||
monitor: "Monitor",
|
||||
monitorSystem: "System Monitor",
|
||||
log: "Log",
|
||||
logSystem: "System Log",
|
||||
logHysteria: "Hysteria Log",
|
||||
info: "Info",
|
||||
infoAccount: "Account Info",
|
||||
},
|
||||
// 登录页面国际化
|
||||
login: {
|
||||
title: "HY2XS admin",
|
||||
username: "Username",
|
||||
password: "Password",
|
||||
login: "Login",
|
||||
},
|
||||
// 导航栏国际化
|
||||
navbar: {
|
||||
logout: "Logout",
|
||||
},
|
||||
common: {
|
||||
id: "ID",
|
||||
createTime: "Create Time",
|
||||
operate: "Operate",
|
||||
edit: "Edit",
|
||||
delete: "Delete",
|
||||
deleted: "Status",
|
||||
all: "All",
|
||||
enable: "Enable",
|
||||
disable: "Disable",
|
||||
search: "Search",
|
||||
reset: "Reset",
|
||||
add: "Add",
|
||||
confirm: "Confirm",
|
||||
cancel: "Cancel",
|
||||
copySuccess: "Copy successful",
|
||||
subscribe: "Subscribe",
|
||||
subscribeQrCode: "Subscribe QR Code",
|
||||
nodeUrl: "Node URL",
|
||||
nodeQrCode: "Node QR Code",
|
||||
resetTraffic: "Reset traffic",
|
||||
import: "Import",
|
||||
export: "Export",
|
||||
save: "Save",
|
||||
update: "Update",
|
||||
downloadSuccess: "Download successful",
|
||||
wait: "The version is being changed, please wait a moment",
|
||||
enableSuccess: "Hysteria2 start successful",
|
||||
disableSuccess: "Hysteria2 stop successful",
|
||||
success: "Success",
|
||||
refresh: "Refresh",
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
securityRisk: "Security Risks",
|
||||
defaultPassTip: `Please change the default login password as soon as possible, It is recommended to set a strong password to protect your account security. <a href="/#/account/list?focus=change-pass" style="color: #00BFFF">Click here</a> to change`,
|
||||
noHttpsTip: `Your website is not using HTTPS, making data transmission insecure, Please enable HTTPS as soon as possible to protect user information. <a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">Click here</a> to enable`,
|
||||
},
|
||||
info: {
|
||||
expireTime: "y-M-d H:m:s",
|
||||
greeting1: "The cool and fresh air awakens your energy for the day🌅!",
|
||||
greeting2: "Good morning,",
|
||||
greeting3: "Good afternoon,",
|
||||
greeting4: "Good evening,",
|
||||
greeting5:
|
||||
"I want to be a shooting star, cutting through the darkness, just to illuminate your dreams, good night🌛!",
|
||||
},
|
||||
account: {
|
||||
remark: "Remark",
|
||||
username: "Username",
|
||||
pass: "Pass",
|
||||
conPass: "ConPass",
|
||||
quota: "Quota",
|
||||
download: "Download",
|
||||
upload: "Upload",
|
||||
expireTime: "Expire Time",
|
||||
kickUtilTimeLast: "Offline Remaining Time",
|
||||
kickUtilTime: "Offline Time",
|
||||
deviceNo: "Limit Devices",
|
||||
onlineStatus: "Online Status",
|
||||
online: "Online",
|
||||
offline: "Offline",
|
||||
device: "Online Devices",
|
||||
role: "Role",
|
||||
unit: "Unit",
|
||||
loginAt: "Last login time",
|
||||
conAt: "Last connection time",
|
||||
createTime: "Create Time",
|
||||
releaseSuccess: "Release successful",
|
||||
kick: "Kick",
|
||||
kickTip: "Force user to log off",
|
||||
releaseKick: "Release",
|
||||
releaseKickTip: "Remove offline status",
|
||||
},
|
||||
config: {
|
||||
huiWebPort: "HY2XS admin Web Port",
|
||||
huiWebContext: "HY2XS admin Web Context",
|
||||
hysteria2TrafficTime: "Hysteria2 Traffic Time",
|
||||
huiCrtPath: "HY2XS admin CRT File Path",
|
||||
huiKeyPath: "HY2XS admin KEY File Path",
|
||||
uploadCrtFile: "Upload CRT File",
|
||||
uploadKeyFile: "Upload KEY File",
|
||||
restartServer: "Restart Panel",
|
||||
restartTip: "Restarting, please refresh",
|
||||
useHysteria2Cert: "Use Hysteria2 cert",
|
||||
huiHttps: "Open https on the panel",
|
||||
resetTrafficCron: "Reset traffic schedule task",
|
||||
resetTrafficCronTip:
|
||||
"Scheduled task expression, reference: https://pkg.go.dev/github.com/robfig/cron/v3",
|
||||
resetTrafficMonth: "Run once a month, midnight, first of month",
|
||||
resetTrafficWeek: "Run once a week, midnight between Sat/Sun",
|
||||
},
|
||||
monitor: {
|
||||
huiVersion: "HY2XS admin Version",
|
||||
cpuPercent: "CPU Usage",
|
||||
memPercent: "Memory Usage",
|
||||
diskPercent: "Disk Usage",
|
||||
hysteria2UserTotal: "Number of online users",
|
||||
hysteria2DeviceTotal: "Number of online devices",
|
||||
hysteria2Version: "Hysteria2 Version",
|
||||
hysteria2Running: "Hysteria2 Status",
|
||||
hysteria2RunningTrue: "Running",
|
||||
hysteria2RunningFalse: "Stop",
|
||||
},
|
||||
log: {
|
||||
numLine: "Number of lines",
|
||||
},
|
||||
hysteria: {
|
||||
enable: "Enable",
|
||||
disable: "Disable",
|
||||
addConfigItem: "Add Config Item",
|
||||
hysteria2Version: "Hysteria2 Version",
|
||||
hysteria2Running: "Hysteria2 Status",
|
||||
hysteria2ChangeVersion: "Change",
|
||||
addOutbound: "Add Outbound",
|
||||
extension: "Extension",
|
||||
listen: "Listen",
|
||||
tls: "TLS",
|
||||
obfs: "Obfuscation",
|
||||
quic: "QUIC parameters",
|
||||
bandwidth: "Bandwidth",
|
||||
speedTest: "Speed Test",
|
||||
udp: "UDP",
|
||||
resolver: "Resolver",
|
||||
sniff: "Protocol Sniffing",
|
||||
acl: "ACL",
|
||||
outbounds: "Outbounds",
|
||||
http: "Traffic Stats API (HTTP)",
|
||||
masquerade: "Masquerade",
|
||||
config: {
|
||||
enable: "Enable/Disable",
|
||||
remark: "Remark",
|
||||
portHopping:
|
||||
"Port Hopping, Multiple individual ports: 1234,5678,9012; A range of ports: 20000-50000; A combination of both: 1234,5000-6000,7044,8000-9000",
|
||||
clashExtension: "Clash subscription extension",
|
||||
listen:
|
||||
"When the IP address is omitted, the server will listen on all interfaces, both IPv4 and IPv6. To listen on IPv4 only, you can use 0.0.0.0:443. To listen on IPv6 only, you can use [::]:443.",
|
||||
tlsType: "TLS type",
|
||||
tls: {
|
||||
cert: "The path to the Cert file.",
|
||||
key: "The path to the Key file.",
|
||||
sniGuard:
|
||||
'Verify the SNI provided by the client. Accept the connection only when it matches what\'s in the certificate. Terminate the TLS handshake otherwise. Set to strict to enforce this behavior. Set to disable to disable this entirely. The default is dns-san, which enables this feature only when the certificate contains the "Subject Alternative Name" extension with a domain name in it.',
|
||||
},
|
||||
acme: {
|
||||
domains: "Domains",
|
||||
email: "Email",
|
||||
ca: "The CA to use. Can be letsencrypt or zerossl.",
|
||||
listenHost:
|
||||
"The host address (not including the port) to listen on for the ACME challenge. If omitted, the server will listen on all interfaces.",
|
||||
dir: "The directory to store the ACME account key and certificates.",
|
||||
type: "ACME challenge type. Can be http, tls, or dns.",
|
||||
http: {
|
||||
altPort:
|
||||
"Listening port for HTTP challenges. (Note: Changing to a port other than 80 requires port forwarding or HTTP reverse proxy, or the challenge will fail!)",
|
||||
},
|
||||
tls: {
|
||||
altPort:
|
||||
"Listening port for TLS-ALPN challenges. (Note: Changing to a port other than 443 requires port forwarding or TLS reverse proxy, or the challenge will fail!)",
|
||||
},
|
||||
dns: {
|
||||
name: "DNS provider. For details, refer to ACME DNS Configuration.",
|
||||
config: "ACME DNS Configuration",
|
||||
},
|
||||
disableHTTP: "Disable HTTP challenge.",
|
||||
disableTLSALPN: "Disable TLS-ALPN challenge.",
|
||||
altHTTPPort:
|
||||
"Alternate HTTP challenge port. (Note: If you want to use anything other than 80, you must set up port forward/HTTP reverse proxy from 80 to that port, otherwise ACME will not be able to issue the certificate.)",
|
||||
altTLSALPNPort:
|
||||
"Alternate TLS-ALPN challenge port. (Note: If you want to use anything other than 443, you must set up port forward/SNI proxy from 443 to that port, otherwise ACME will not be able to issue the certificate.)",
|
||||
},
|
||||
obfs: {
|
||||
type: "Type",
|
||||
salamander: {
|
||||
password: "Replace with a strong password of your choice.",
|
||||
},
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: "The initial QUIC stream receive window size.",
|
||||
maxStreamReceiveWindow: "The maximum QUIC stream receive window size.",
|
||||
initConnReceiveWindow:
|
||||
"The initial QUIC connection receive window size.",
|
||||
maxConnReceiveWindow:
|
||||
"The maximum QUIC connection receive window size.",
|
||||
maxIdleTimeout:
|
||||
"The maximum idle timeout. How long the server will consider the client still connected without any activity.",
|
||||
maxIncomingStreams:
|
||||
"The maximum number of concurrent incoming streams.",
|
||||
disablePathMTUDiscovery: "Disable QUIC path MTU discovery.",
|
||||
},
|
||||
bandwidth: {
|
||||
up: "Up",
|
||||
down: "Down",
|
||||
},
|
||||
ignoreClientBandwidth:
|
||||
"When enabled, makes the server to disregard any bandwidth hints set by clients",
|
||||
speedTest:
|
||||
"speedTest enables the built-in speed test server. When enabled, clients can test their download and upload speeds with the server. For more information, see the Speed Test documentation.",
|
||||
disableUDP:
|
||||
"disableUDP disables UDP forwarding, only allowing TCP connections.",
|
||||
udpIdleTimeout:
|
||||
"udpIdleTimeout specifies the amount of time the server will keep a local UDP port open for each UDP session that has no activity. This is conceptually similar to the NAT UDP session timeout.",
|
||||
resolver: {
|
||||
type: "Type",
|
||||
tcp: {
|
||||
addr: "The address of the TCP resolver.",
|
||||
timeout: "The timeout for DNS queries.",
|
||||
},
|
||||
udp: {
|
||||
addr: "The address of the UDP resolver.",
|
||||
timeout: "The timeout for DNS queries.",
|
||||
},
|
||||
tls: {
|
||||
addr: "The address of the TLS resolver.",
|
||||
timeout: "The timeout for DNS queries.",
|
||||
sni: "The SNI to use for the TLS resolver.",
|
||||
insecure: "Disable TLS verification for the TLS resolver.",
|
||||
},
|
||||
https: {
|
||||
addr: "The address of the HTTPS resolver.",
|
||||
timeout: "The timeout for DNS queries.",
|
||||
sni: "The SNI to use for the TLS resolver.",
|
||||
insecure: "Disable TLS verification for the TLS resolver.",
|
||||
},
|
||||
},
|
||||
sniff: {
|
||||
enable: "Whether to enable protocol sniffing.",
|
||||
timeout:
|
||||
"Sniffing timeout. If the protocol/domain cannot be determined within this time, the original address will be used to initiate the connection.",
|
||||
rewriteDomain:
|
||||
"Whether to rewrite requests that are already in domain name form. If enabled, requests with the target address already in domain name form will still be sniffed.",
|
||||
tcpPorts:
|
||||
"List of TCP ports. Only TCP requests on these ports will be sniffed.",
|
||||
udpPorts:
|
||||
"List of UDP ports. Only UDP requests on these ports will be sniffed.",
|
||||
},
|
||||
aclType: "ACL type",
|
||||
acl: {
|
||||
file: "The path to the ACL file.",
|
||||
inline: "The list of inline ACL rules.",
|
||||
geoip:
|
||||
"Optional. Uncomment to enable. The path to the GeoIP database file. If this field is omitted, Hysteria will automatically download the latest database to your working directory.",
|
||||
geosite:
|
||||
"Optional. Uncomment to enable. The path to the GeoSite database file. If this field is omitted, Hysteria will automatically download the latest database to your working directory.",
|
||||
geoUpdateInterval:
|
||||
"Optional. The interval at which to refresh the GeoIP/GeoSite databases. 168 hours (1 week) by default. Only applies if the GeoIP/GeoSite databases are automatically downloaded. (Check the note below for more information.)",
|
||||
},
|
||||
outbounds: {
|
||||
name: "The name of the outbound. This is used in ACL rules.",
|
||||
type: "Type",
|
||||
socks5: {
|
||||
addr: "The address of the SOCKS5 proxy.",
|
||||
username:
|
||||
"Optional. The username for the SOCKS5 proxy, if authentication is required.",
|
||||
password:
|
||||
"Optional. The password for the SOCKS5 proxy, if authentication is required.",
|
||||
},
|
||||
http: {
|
||||
url: "The URL of the HTTP/HTTPS proxy. (Can be http:// or https://)",
|
||||
insecure:
|
||||
"Optional. Whether to disable TLS verification. Applies to HTTPS proxies only.",
|
||||
},
|
||||
direct: {
|
||||
mode: "Type",
|
||||
bindIPv4: "The local IPv4 address to bind to.",
|
||||
bindIPv6: "The local IPv6 address to bind to.",
|
||||
bindDevice: "The local network interface to bind to.",
|
||||
fastOpen: "Enable TCP fast open.",
|
||||
},
|
||||
},
|
||||
trafficStats: {
|
||||
listen: "The address to listen on.",
|
||||
},
|
||||
masquerade: {
|
||||
type: "Type",
|
||||
file: {
|
||||
dir: "The directory to serve files from.",
|
||||
},
|
||||
proxy: {
|
||||
url: "The URL of the website to proxy.",
|
||||
rewriteHost:
|
||||
"Whether to rewrite the Host header to match the proxied website. This is required if the target web server uses Host to determine which site to serve.",
|
||||
insecure: "Disable TLS verification for the proxied website.",
|
||||
},
|
||||
string: {
|
||||
content: "The string to return.",
|
||||
headers: "Optional. The headers to return.",
|
||||
statusCode: "Optional. The status code to return. 200 by default.",
|
||||
},
|
||||
listenHTTP: "HTTP (TCP) listen address.",
|
||||
listenHTTPS: "HTTPS (TCP) listen address.",
|
||||
forceHTTPS:
|
||||
"Whether to force HTTPS. If enabled, all HTTP requests will be redirected to HTTPS.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,258 @@
|
||||
export default {
|
||||
route: {
|
||||
account: "Аккаунты",
|
||||
accountList: "Управление аккаунтами",
|
||||
hysteria: "Hysteria",
|
||||
hysteriaList: "Управление Hysteria",
|
||||
config: "Система",
|
||||
configList: "Настройки системы",
|
||||
monitor: "Мониторинг",
|
||||
monitorSystem: "Системный мониторинг",
|
||||
log: "Логи",
|
||||
logSystem: "Системные логи",
|
||||
logHysteria: "Логи Hysteria",
|
||||
info: "Информация",
|
||||
infoAccount: "Профиль",
|
||||
},
|
||||
login: {
|
||||
title: "HY2XS admin",
|
||||
username: "Логин",
|
||||
password: "Пароль",
|
||||
login: "Войти",
|
||||
},
|
||||
navbar: {
|
||||
logout: "Выйти",
|
||||
},
|
||||
common: {
|
||||
id: "ID",
|
||||
createTime: "Создано",
|
||||
operate: "Действия",
|
||||
edit: "Изменить",
|
||||
delete: "Удалить",
|
||||
deleted: "Статус",
|
||||
all: "Все",
|
||||
enable: "Включено",
|
||||
disable: "Отключено",
|
||||
search: "Поиск",
|
||||
reset: "Сброс",
|
||||
add: "Добавить",
|
||||
confirm: "Подтвердить",
|
||||
cancel: "Отмена",
|
||||
copySuccess: "Скопировано",
|
||||
subscribe: "Ссылка подписки",
|
||||
subscribeQrCode: "QR подписки",
|
||||
nodeUrl: "URL узла",
|
||||
nodeQrCode: "QR узла",
|
||||
resetTraffic: "Сбросить трафик",
|
||||
import: "Импорт",
|
||||
export: "Экспорт",
|
||||
save: "Сохранить",
|
||||
update: "Обновить",
|
||||
downloadSuccess: "Загрузка завершена",
|
||||
wait: "Версия меняется, подождите",
|
||||
enableSuccess: "Hysteria2 запущена",
|
||||
disableSuccess: "Hysteria2 остановлена",
|
||||
success: "Готово",
|
||||
refresh: "Обновить",
|
||||
yes: "Да",
|
||||
no: "Нет",
|
||||
securityRisk: "Риски безопасности",
|
||||
defaultPassTip: `Смените пароль по умолчанию как можно скорее. <a href="/#/account/list?focus=change-pass" style="color: #00BFFF">Перейти к смене</a>`,
|
||||
noHttpsTip: `Панель работает без HTTPS. Включите HTTPS для защиты данных. <a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">Открыть настройки</a>`,
|
||||
},
|
||||
info: {
|
||||
expireTime: "г-М-д Ч:м:с",
|
||||
greeting1: "Доброе утро,",
|
||||
greeting2: "Доброе утро,",
|
||||
greeting3: "Добрый день,",
|
||||
greeting4: "Добрый вечер,",
|
||||
greeting5: "Доброй ночи,",
|
||||
},
|
||||
account: {
|
||||
remark: "Комментарий",
|
||||
username: "Логин",
|
||||
pass: "Пароль входа",
|
||||
conPass: "Пароль подключения",
|
||||
quota: "Квота",
|
||||
download: "Скачано",
|
||||
upload: "Отдано",
|
||||
expireTime: "Срок действия",
|
||||
kickUtilTimeLast: "Осталось офлайн",
|
||||
kickUtilTime: "Отключить до",
|
||||
deviceNo: "Лимит устройств",
|
||||
onlineStatus: "Онлайн",
|
||||
online: "Онлайн",
|
||||
offline: "Офлайн",
|
||||
device: "Устройства",
|
||||
role: "Роль",
|
||||
unit: "Ед. изм.",
|
||||
loginAt: "Последний вход",
|
||||
conAt: "Последнее подключение",
|
||||
createTime: "Создано",
|
||||
releaseSuccess: "Ограничение снято",
|
||||
kick: "Отключить",
|
||||
kickTip: "Принудительно отключить пользователя",
|
||||
releaseKick: "Снять",
|
||||
releaseKickTip: "Снять офлайн-статус",
|
||||
},
|
||||
config: {
|
||||
huiWebPort: "Порт HY2XS admin",
|
||||
huiWebContext: "Web-контекст HY2XS admin",
|
||||
hysteria2TrafficTime: "Период учёта трафика Hysteria2",
|
||||
huiCrtPath: "Путь к CRT HY2XS admin",
|
||||
huiKeyPath: "Путь к KEY HY2XS admin",
|
||||
uploadCrtFile: "Загрузить CRT",
|
||||
uploadKeyFile: "Загрузить KEY",
|
||||
restartServer: "Перезапустить панель",
|
||||
restartTip: "Перезапуск, обновите страницу",
|
||||
useHysteria2Cert: "Использовать сертификат Hysteria2",
|
||||
huiHttps: "Включить HTTPS панели",
|
||||
resetTrafficCron: "Расписание сброса трафика",
|
||||
resetTrafficCronTip: "Cron-выражение для планового сброса трафика",
|
||||
resetTrafficMonth: "Раз в месяц, в полночь первого дня",
|
||||
resetTrafficWeek: "Раз в неделю, в полночь между субботой и воскресеньем",
|
||||
},
|
||||
monitor: {
|
||||
huiVersion: "Версия HY2XS admin",
|
||||
cpuPercent: "CPU",
|
||||
memPercent: "Память",
|
||||
diskPercent: "Диск",
|
||||
hysteria2UserTotal: "Пользователей онлайн",
|
||||
hysteria2DeviceTotal: "Устройств онлайн",
|
||||
hysteria2Version: "Версия Hysteria2",
|
||||
hysteria2Running: "Статус Hysteria2",
|
||||
hysteria2RunningTrue: "Работает",
|
||||
hysteria2RunningFalse: "Остановлена",
|
||||
},
|
||||
log: {
|
||||
numLine: "Количество строк",
|
||||
},
|
||||
hysteria: {
|
||||
enable: "Включить",
|
||||
disable: "Отключить",
|
||||
addConfigItem: "Добавить параметр",
|
||||
hysteria2Version: "Версия Hysteria2",
|
||||
hysteria2Running: "Статус Hysteria2",
|
||||
hysteria2ChangeVersion: "Сменить",
|
||||
addOutbound: "Добавить outbound",
|
||||
extension: "Расширение",
|
||||
listen: "Адрес прослушивания",
|
||||
tls: "TLS",
|
||||
obfs: "Маскировка",
|
||||
quic: "Параметры QUIC",
|
||||
bandwidth: "Полоса",
|
||||
speedTest: "Тест скорости",
|
||||
udp: "UDP",
|
||||
resolver: "DNS",
|
||||
sniff: "Sniffing протоколов",
|
||||
acl: "ACL",
|
||||
outbounds: "Outbounds",
|
||||
http: "Traffic Stats API (HTTP)",
|
||||
masquerade: "Masquerade",
|
||||
config: {
|
||||
enable: "Включить/отключить",
|
||||
remark: "Комментарий",
|
||||
portHopping: "Port hopping: отдельные порты, диапазоны или их комбинации",
|
||||
clashExtension: "Расширение подписки Clash",
|
||||
listen: "Адрес и порт прослушивания. Для IPv4 используйте 0.0.0.0:443.",
|
||||
tlsType: "Тип TLS",
|
||||
tls: {
|
||||
cert: "Путь к cert-файлу",
|
||||
key: "Путь к key-файлу",
|
||||
sniGuard: "Проверка SNI клиента перед принятием TLS-соединения.",
|
||||
},
|
||||
acme: {
|
||||
domains: "Домены",
|
||||
email: "Email",
|
||||
ca: "CA: letsencrypt или zerossl",
|
||||
listenHost: "Адрес для ACME challenge",
|
||||
dir: "Каталог ACME аккаунта и сертификатов",
|
||||
type: "Тип ACME challenge: http, tls или dns",
|
||||
http: { altPort: "Альтернативный порт HTTP challenge" },
|
||||
tls: { altPort: "Альтернативный порт TLS-ALPN challenge" },
|
||||
dns: { name: "DNS-провайдер", config: "Конфигурация ACME DNS" },
|
||||
disableHTTP: "Отключить HTTP challenge",
|
||||
disableTLSALPN: "Отключить TLS-ALPN challenge",
|
||||
altHTTPPort: "Альтернативный HTTP-порт",
|
||||
altTLSALPNPort: "Альтернативный TLS-ALPN-порт",
|
||||
},
|
||||
obfs: {
|
||||
type: "Тип",
|
||||
salamander: { password: "Сильный пароль Salamander" },
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: "Начальное окно приёма QUIC stream",
|
||||
maxStreamReceiveWindow: "Максимальное окно приёма QUIC stream",
|
||||
initConnReceiveWindow: "Начальное окно приёма QUIC connection",
|
||||
maxConnReceiveWindow: "Максимальное окно приёма QUIC connection",
|
||||
maxIdleTimeout: "Максимальный idle timeout",
|
||||
maxIncomingStreams: "Максимум входящих stream",
|
||||
disablePathMTUDiscovery: "Отключить QUIC path MTU discovery",
|
||||
},
|
||||
bandwidth: { up: "Вверх", down: "Вниз" },
|
||||
ignoreClientBandwidth: "Игнорировать bandwidth, заявленный клиентом",
|
||||
speedTest: "Встроенный сервер теста скорости",
|
||||
disableUDP: "Отключить UDP forwarding",
|
||||
udpIdleTimeout: "Idle timeout для UDP-сессий",
|
||||
resolver: {
|
||||
type: "Тип",
|
||||
tcp: { addr: "TCP DNS resolver", timeout: "Timeout DNS-запросов" },
|
||||
udp: { addr: "UDP DNS resolver", timeout: "Timeout DNS-запросов" },
|
||||
tls: {
|
||||
addr: "DNS over TLS resolver",
|
||||
timeout: "Timeout DNS-запросов",
|
||||
sni: "SNI для TLS resolver",
|
||||
insecure: "Отключить TLS-проверку",
|
||||
},
|
||||
https: {
|
||||
addr: "DNS over HTTPS resolver",
|
||||
timeout: "Timeout DNS-запросов",
|
||||
sni: "SNI для HTTPS resolver",
|
||||
insecure: "Отключить TLS-проверку",
|
||||
},
|
||||
},
|
||||
sniff: {
|
||||
enable: "Включить sniffing",
|
||||
timeout: "Timeout sniffing",
|
||||
rewriteDomain: "Повторно анализировать доменные запросы",
|
||||
tcpPorts: "TCP-порты для sniffing",
|
||||
udpPorts: "UDP-порты для sniffing",
|
||||
},
|
||||
aclType: "Тип ACL",
|
||||
acl: {
|
||||
file: "Путь к ACL-файлу",
|
||||
inline: "Inline ACL-правила",
|
||||
geoip: "Путь к GeoIP базе",
|
||||
geosite: "Путь к GeoSite базе",
|
||||
geoUpdateInterval: "Интервал обновления GeoIP/GeoSite",
|
||||
},
|
||||
outbounds: {
|
||||
name: "Имя outbound",
|
||||
type: "Тип",
|
||||
socks5: { addr: "Адрес SOCKS5", username: "Логин SOCKS5", password: "Пароль SOCKS5" },
|
||||
http: { url: "URL HTTP/HTTPS proxy", insecure: "Отключить TLS-проверку proxy" },
|
||||
direct: {
|
||||
mode: "Тип",
|
||||
bindIPv4: "Локальный IPv4",
|
||||
bindIPv6: "Локальный IPv6",
|
||||
bindDevice: "Сетевой интерфейс",
|
||||
fastOpen: "TCP fast open",
|
||||
},
|
||||
},
|
||||
trafficStats: { listen: "Адрес прослушивания" },
|
||||
masquerade: {
|
||||
type: "Тип",
|
||||
file: { dir: "Каталог файлов" },
|
||||
proxy: {
|
||||
url: "URL проксируемого сайта",
|
||||
rewriteHost: "Переписывать Host header",
|
||||
insecure: "Отключить TLS-проверку",
|
||||
},
|
||||
string: { content: "Ответ строкой", headers: "HTTP headers", statusCode: "HTTP status code" },
|
||||
listenHTTP: "HTTP listen address",
|
||||
listenHTTPS: "HTTPS listen address",
|
||||
forceHTTPS: "Принудительно использовать HTTPS",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,312 @@
|
||||
export default {
|
||||
// 路由国际化
|
||||
route: {
|
||||
account: "账户",
|
||||
accountList: "账户管理",
|
||||
hysteria: "Hysteria",
|
||||
hysteriaList: "Hysteria 管理",
|
||||
config: "系统",
|
||||
configList: "系统设置",
|
||||
monitor: "监控",
|
||||
monitorSystem: "系统监控",
|
||||
log: "日志",
|
||||
logSystem: "系统日志",
|
||||
logHysteria: "Hysteria 日志",
|
||||
info: "信息",
|
||||
infoAccount: "账户信息",
|
||||
},
|
||||
// 登录页面国际化
|
||||
login: {
|
||||
title: "HY2XS admin",
|
||||
username: "用户名",
|
||||
password: "密码",
|
||||
login: "登 录",
|
||||
},
|
||||
// 导航栏国际化
|
||||
navbar: {
|
||||
logout: "注销",
|
||||
},
|
||||
common: {
|
||||
id: "编号",
|
||||
createTime: "创建时间",
|
||||
operate: "操作",
|
||||
edit: "编辑",
|
||||
delete: "删除",
|
||||
deleted: "状态",
|
||||
all: "全部",
|
||||
enable: "正常",
|
||||
disable: "禁用",
|
||||
search: "搜索",
|
||||
reset: "重设",
|
||||
add: "新增",
|
||||
confirm: "确定",
|
||||
cancel: "取消",
|
||||
copySuccess: "复制成功",
|
||||
subscribe: "订阅链接",
|
||||
subscribeQrCode: "订阅二维码",
|
||||
nodeUrl: "节点 URL",
|
||||
nodeQrCode: "节点二维码",
|
||||
resetTraffic: "重设流量",
|
||||
import: "导入",
|
||||
export: "导出",
|
||||
save: "保存",
|
||||
update: "更新",
|
||||
downloadSuccess: "下载成功",
|
||||
wait: "正在更换版本,请等待一会儿",
|
||||
enableSuccess: "Hysteria2 启动 !!",
|
||||
disableSuccess: "Hysteria2 已关闭",
|
||||
success: "成功",
|
||||
refresh: "刷新",
|
||||
yes: "是",
|
||||
no: "否",
|
||||
securityRisk: `安全隐患`,
|
||||
defaultPassTip: `请尽快修改默认登录密码,建议设置强密码以保护您的账户安全。<a href="/#/account/list?focus=change-pass" style="color: #00BFFF">点击这里</a>修改`,
|
||||
noHttpsTip: `您的网站未启用 HTTPS,数据传输不安全,请尽快开启 HTTPS 以保护用户信息。<a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">点击这里</a>开启`,
|
||||
},
|
||||
info: {
|
||||
expireTime: "年-月-日 时:分:秒",
|
||||
greeting1: "微凉扑面,清新的空气,唤醒一天的活力🌅!",
|
||||
greeting2: "上午好,",
|
||||
greeting3: "下午好,",
|
||||
greeting4: "晚上好,",
|
||||
greeting5: "我愿成为流星,划破黑夜,只为照亮你的梦境,晚安🌛!",
|
||||
},
|
||||
account: {
|
||||
remark: "备注",
|
||||
username: "用户名",
|
||||
pass: "登录密码",
|
||||
conPass: "连接密码",
|
||||
quota: "配额",
|
||||
download: "下载",
|
||||
upload: "上传",
|
||||
expireTime: "过期时间",
|
||||
kickUtilTimeLast: "下线剩余时间",
|
||||
kickUtilTime: "下线截止时间",
|
||||
deviceNo: "限制设备数",
|
||||
onlineStatus: "在线状态",
|
||||
online: "在线",
|
||||
offline: "离线",
|
||||
device: "在线设备数",
|
||||
role: "角色",
|
||||
unit: "单位",
|
||||
loginAt: "最近登录时间",
|
||||
conAt: "最近连接时间",
|
||||
createTime: "注册时间",
|
||||
releaseSuccess: "解除成功",
|
||||
kick: "下线",
|
||||
kickTip: "强制用户下线",
|
||||
releaseKick: "解除",
|
||||
releaseKickTip: "解除下线状态",
|
||||
},
|
||||
config: {
|
||||
huiWebPort: "HY2XS admin Web 端口",
|
||||
huiWebContext: "HY2XS admin Web 上下文",
|
||||
hysteria2TrafficTime: "Hysteria2 流量倍数",
|
||||
huiCrtPath: "HY2XS admin CRT 证书路径",
|
||||
huiKeyPath: "HY2XS admin KEY 证书路径",
|
||||
uploadCrtFile: "上传 CRT 证书",
|
||||
uploadKeyFile: "上传 KEY 证书",
|
||||
restartServer: "重启面板",
|
||||
restartTip: "正在重启,请刷新",
|
||||
useHysteria2Cert: "使用 Hysteria2 的证书",
|
||||
huiHttps: "面板开启 https",
|
||||
resetTrafficCron: "重设流量计划任务",
|
||||
resetTrafficCronTip:
|
||||
"计划任务表达式,参考:https://pkg.go.dev/github.com/robfig/cron/v3",
|
||||
resetTrafficMonth: "每月运行一次,每月第一天午夜",
|
||||
resetTrafficWeek: "每周运行一次,周六/周日午夜",
|
||||
},
|
||||
monitor: {
|
||||
huiVersion: "HY2XS admin 版本",
|
||||
cpuPercent: "CPU 使用率",
|
||||
memPercent: "内存使用率",
|
||||
diskPercent: "磁盘使用率",
|
||||
hysteria2UserTotal: "在线用户数",
|
||||
hysteria2DeviceTotal: "在线设备数",
|
||||
hysteria2Version: "Hysteria2 版本",
|
||||
hysteria2Running: "Hysteria2 状态",
|
||||
hysteria2RunningTrue: "运行",
|
||||
hysteria2RunningFalse: "停止",
|
||||
},
|
||||
log: {
|
||||
numLine: "显示行数",
|
||||
},
|
||||
hysteria: {
|
||||
enable: "开启",
|
||||
disable: "关闭",
|
||||
addConfigItem: "添加配置项",
|
||||
hysteria2Version: "Hysteria2 版本",
|
||||
hysteria2Running: "Hysteria2 状态",
|
||||
hysteria2ChangeVersion: "切换",
|
||||
addOutbound: "添加出站规则",
|
||||
extension: "扩展",
|
||||
listen: "监听地址",
|
||||
tls: "TLS",
|
||||
obfs: "混淆",
|
||||
quic: "QUIC 参数",
|
||||
bandwidth: "带宽",
|
||||
speedTest: "速度测试",
|
||||
udp: "UDP",
|
||||
resolver: "DNS 解析",
|
||||
sniff: "协议嗅探 (Sniff)",
|
||||
acl: "ACL",
|
||||
outbounds: "出站规则",
|
||||
http: "流量统计 API",
|
||||
masquerade: "伪装",
|
||||
config: {
|
||||
enable: "开启/关闭",
|
||||
remark: "别名",
|
||||
portHopping:
|
||||
"端口跳跃,多个单端口:1234,5678,9012;端口范围:20000-50000;两者的组合:1234,5000-6000,7044,8000-9000",
|
||||
clashExtension: "Clash 订阅扩展",
|
||||
listen:
|
||||
"当只有端口没有 IP 地址时,服务器将监听所有可用的 IPv4 和 IPv6 地址。要仅监听 IPv4,可以使用 0.0.0.0:443。要仅监听 IPv6,可以使用 [::]:443。",
|
||||
tlsType: "TLS 类型",
|
||||
tls: {
|
||||
cert: "CERT 路径",
|
||||
key: "KEY 路径",
|
||||
sniGuard:
|
||||
"验证客户端发送的 SNI。 与证书信息匹配时才建立连接, 否则终止 TLS 握手。 设置为 strict 以启用该功能。 设置为 disable 以禁用该功能。 默认为 dns-san, 仅当证书中包含「证书主题背景的备用名称」扩展且该扩展中包含域名时才启用该功能。",
|
||||
},
|
||||
acme: {
|
||||
domains: "域名",
|
||||
email: "邮箱",
|
||||
ca: "要使用的 CA。可以是 letsencrypt 或 zerossl。",
|
||||
listenHost:
|
||||
"用于 ACME 服务器验证的监听地址(不含端口)。默认监听所有可用的地址。",
|
||||
dir: "存储 ACME 账户密钥和证书的目录。",
|
||||
type: "ACME 验证类型。可以是 http, tls 或 dns。",
|
||||
http: {
|
||||
altPort:
|
||||
"用于 HTTP 挑战的监听端口。 (注意: 改为非 80 需要另行配置端口转发或者 HTTP 反向代理,否则证书会签署失败!)",
|
||||
},
|
||||
tls: {
|
||||
altPort:
|
||||
"用于 TLS-ALPN 挑战的监听端口。 (注意: 改为非 443 需要另行配置端口转发或者 SNI Proxy,否则证书会签署失败!)",
|
||||
},
|
||||
dns: {
|
||||
name: "DNS 提供商。详细信息请参考 ACME DNS 配置。",
|
||||
config: "ACME DNS 配置",
|
||||
},
|
||||
disableHTTP: "禁用 HTTP 挑战。",
|
||||
disableTLSALPN: "禁用 TLS-ALPN 挑战。",
|
||||
altHTTPPort:
|
||||
"用于 HTTP 挑战的监听端口。 (注意: 改为非 80 需要另行配置端口转发或者 HTTP 反向代理,否则证书会签署失败!)",
|
||||
altTLSALPNPort:
|
||||
"用于 TLS-ALPN 挑战的监听端口。 (注意: 改为非 443 需要另行配置端口转发或者 SNI Proxy,否则证书会签署失败!)",
|
||||
},
|
||||
obfs: {
|
||||
type: "类型",
|
||||
salamander: {
|
||||
password: "替换为你的混淆密码。",
|
||||
},
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: "初始的 QUIC 流接收窗口大小。",
|
||||
maxStreamReceiveWindow: "最大的 QUIC 流接收窗口大小。",
|
||||
initConnReceiveWindow: "初始的 QUIC 连接接收窗口大小。",
|
||||
maxConnReceiveWindow: "最大的 QUIC 连接接收窗口大小。",
|
||||
maxIdleTimeout:
|
||||
"最长空闲超时时间。服务器会在多长时间没有收到任何客户端数据后关闭连接。",
|
||||
maxIncomingStreams: "最大并发传入流的数量。",
|
||||
disablePathMTUDiscovery: "禁用 MTU 探测。",
|
||||
},
|
||||
bandwidth: {
|
||||
up: "上传",
|
||||
down: "下载",
|
||||
},
|
||||
ignoreClientBandwidth: "忽略客户端带宽设置",
|
||||
speedTest: "speedTest 启用后,服务端将允许客户端进行下载和上传速度测试。",
|
||||
disableUDP: "disableUDP 启用后服务端禁用 UDP 转发,只支持 TCP。",
|
||||
udpIdleTimeout:
|
||||
"udpIdleTimeout 用于指定服务器对于每个 UDP 会话,在没有流量时保持本地 UDP 端口的时间长度。概念上与 NAT 的 UDP 会话超时时间相似。",
|
||||
resolver: {
|
||||
type: "类型",
|
||||
tcp: {
|
||||
addr: "TCP DNS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
},
|
||||
udp: {
|
||||
addr: "UDP DNS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
},
|
||||
tls: {
|
||||
addr: "DNS over TLS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
sni: "DNS over TLS 服务器的 SNI。",
|
||||
insecure: "禁用 TLS 证书验证。",
|
||||
},
|
||||
https: {
|
||||
addr: "DNS over HTTPS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
sni: "DNS over TLS 服务器的 SNI。",
|
||||
insecure: "禁用 TLS 证书验证。",
|
||||
},
|
||||
},
|
||||
sniff: {
|
||||
enable: "是否启用协议嗅探。",
|
||||
timeout:
|
||||
"嗅探超时时间。如果超过这个时间仍然无法确定协议/获取域名,将使用原地址发起连接。",
|
||||
rewriteDomain:
|
||||
"是否重写已经是域名的请求。如果启用,对于目标地址已经是域名的请求,仍会进行嗅探。",
|
||||
tcpPorts: "TCP 端口列表。只有这些端口的 TCP 请求会被嗅探。",
|
||||
udpPorts: "UDP 端口列表。只有这些端口的 UDP 请求会被嗅探。",
|
||||
},
|
||||
aclType: "ACL 类型",
|
||||
acl: {
|
||||
file: "ACL 文件的路径。",
|
||||
inline: "内联 ACL 规则的列表。",
|
||||
geoip:
|
||||
"可选。取消注释以启用。GeoIP 数据库文件的路径。如果省略这个字段,Hysteria 会自动下载最新的数据库到工作目录。",
|
||||
geosite:
|
||||
"可选。取消注释以启用。GeoSite 数据库文件的路径。如果省略这个字段,Hysteria 会自动下载最新的数据库到工作目录。",
|
||||
geoUpdateInterval:
|
||||
"可选。GeoIP/GeoSite 数据库刷新的间隔。默认为 168 小时(1 周)。仅在 GeoIP/GeoSite 数据库是自动下载的情况下生效。",
|
||||
},
|
||||
outbounds: {
|
||||
name: "出站规则的名称。在 ACL 中使用。",
|
||||
type: "类型",
|
||||
socks5: {
|
||||
addr: "SOCKS5 代理地址。",
|
||||
username: "可选。SOCKS5 代理用户名。",
|
||||
password: "可选。SOCKS5 代理密码。",
|
||||
},
|
||||
http: {
|
||||
url: "HTTP/HTTPS 代理 URL。(可以是 http:// 或 https:// 开头)",
|
||||
insecure: "可选。禁用 TLS 证书验证。仅适用于 HTTPS 代理。",
|
||||
},
|
||||
direct: {
|
||||
mode: "类型",
|
||||
bindIPv4: "要绑定的本地 IPv4 地址。",
|
||||
bindIPv6: "要绑定的本地 IPv6 地址。",
|
||||
bindDevice: "要绑定的本地网卡。",
|
||||
fastOpen: "启用 TCP 快速打开。",
|
||||
},
|
||||
},
|
||||
trafficStats: {
|
||||
listen: "监听地址。",
|
||||
},
|
||||
masquerade: {
|
||||
type: "类型",
|
||||
file: {
|
||||
dir: "用于提供文件的目录。",
|
||||
},
|
||||
proxy: {
|
||||
url: "要代理的网站的 URL。",
|
||||
rewriteHost:
|
||||
"是否重写 Host 头以匹配被代理的网站。如果目标网站通过 Host 识别请求的网站,这个选项是必须的。",
|
||||
insecure: "禁用对代理网站的 TLS 验证。",
|
||||
},
|
||||
string: {
|
||||
content: "要返回的字符串。",
|
||||
headers: "可选。要返回的 HTTP 头列表。",
|
||||
statusCode: "可选。要返回的 HTTP 状态码。默认为 200。",
|
||||
},
|
||||
listenHTTP: "HTTP (TCP) 监听地址。",
|
||||
listenHTTPS: "HTTPS (TCP) 监听地址。",
|
||||
forceHTTPS:
|
||||
"是否强制使用 HTTPS。如果启用,HTTP 请求将被重定向到 HTTPS。",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="router-fade" mode="out-in">
|
||||
<keep-alive :include="tagsViewStore.cachedViews">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
/* 50= navbar 50 */
|
||||
min-height: calc(100vh - 50px);
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
min-height: 100vh;
|
||||
padding-top: 84px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
import { useAccountStore } from "@/store/modules/account";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const accountStore = useAccountStore();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const { device } = storeToRefs(appStore); // 设备类型:desktop-宽屏设备 || mobile-窄屏设备
|
||||
|
||||
/**
|
||||
* 左侧菜单栏显示/隐藏
|
||||
*/
|
||||
function toggleSideBar() {
|
||||
appStore.toggleSidebar(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* vueUse 全屏
|
||||
*/
|
||||
const { isFullscreen, toggle } = useFullscreen();
|
||||
|
||||
/**
|
||||
* 注销
|
||||
*/
|
||||
function logout() {
|
||||
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
accountStore
|
||||
.logout()
|
||||
.then(() => {
|
||||
tagsViewStore.delAllViews();
|
||||
})
|
||||
.then(() => {
|
||||
router.push(`/login?redirect=${route.fullPath}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="navbar">
|
||||
<!-- 左侧面包屑 -->
|
||||
<div class="flex">
|
||||
<hamburger
|
||||
:is-active="appStore.sidebar.opened"
|
||||
@toggleClick="toggleSideBar"
|
||||
/>
|
||||
<breadcrumb />
|
||||
</div>
|
||||
|
||||
<!-- 右侧导航设置 -->
|
||||
<div class="flex">
|
||||
<!-- 导航栏设置(窄屏隐藏)-->
|
||||
<div class="setting-container" v-if="device !== 'mobile'">
|
||||
<!--全屏 -->
|
||||
<div class="setting-item" @click="toggle">
|
||||
<svg-icon
|
||||
:icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
|
||||
/>
|
||||
</div>
|
||||
<!-- 布局大小 -->
|
||||
<el-tooltip content="布局大小" effect="dark" placement="bottom">
|
||||
<size-select class="setting-item" />
|
||||
</el-tooltip>
|
||||
<!--语言选择-->
|
||||
<lang-select class="setting-item" />
|
||||
</div>
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<el-dropdown trigger="click">
|
||||
<div class="avatar-container">
|
||||
<img src="/src/assets/logo.png" />
|
||||
<i-ep-caret-bottom class="w-3 h-3" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="logout">
|
||||
{{ $t("navbar.logout") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 1px #0003;
|
||||
|
||||
.setting-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.setting-item {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
color: #5a5e66;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgb(249 250 251 / 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
|
||||
import IconEpSunny from "~icons/ep/sunny";
|
||||
import IconEpMoon from "~icons/ep/moon";
|
||||
|
||||
/**
|
||||
* 暗黑模式
|
||||
*/
|
||||
const settingsStore = useSettingsStore();
|
||||
const isDark = useDark();
|
||||
const toggleDark = () => useToggle(isDark);
|
||||
|
||||
/**
|
||||
* 切换布局
|
||||
*/
|
||||
function changeLayout(layout: string) {
|
||||
settingsStore.changeSetting({ key: "layout", value: layout });
|
||||
window.document.body.setAttribute("layout", settingsStore.layout);
|
||||
}
|
||||
|
||||
// 主题颜色
|
||||
const themeColors = ref<string[]>([
|
||||
"#409EFF",
|
||||
"#304156",
|
||||
"#11a983",
|
||||
"#13c2c2",
|
||||
"#6959CD",
|
||||
"#f5222d",
|
||||
]);
|
||||
|
||||
/**
|
||||
* 切换主题颜色
|
||||
*/
|
||||
function changeThemeColor(color: string) {
|
||||
settingsStore.changeSetting({ key: "themeColor", value: color });
|
||||
document.documentElement.style.setProperty(
|
||||
"--el-color-primary",
|
||||
settingsStore.themeColor
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.document.body.setAttribute("layout", settingsStore.layout);
|
||||
document.documentElement.style.setProperty(
|
||||
"--el-color-primary",
|
||||
settingsStore.themeColor
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<h3 class="text-base font-bold">项目配置</h3>
|
||||
<el-divider>主题</el-divider>
|
||||
|
||||
<div class="flex justify-center" @click.stop>
|
||||
<el-switch
|
||||
v-model="isDark"
|
||||
@change="toggleDark"
|
||||
inline-prompt
|
||||
:active-icon="IconEpMoon"
|
||||
:inactive-icon="IconEpSunny"
|
||||
active-color="var(--el-fill-color-dark)"
|
||||
inactive-color="var(--el-color-primary)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-divider>界面设置</el-divider>
|
||||
<div class="py-[8px] flex justify-between">
|
||||
<span class="text-xs">开启 Tags-View</span>
|
||||
<el-switch v-model="settingsStore.tagsView" />
|
||||
</div>
|
||||
|
||||
<div class="py-[8px] flex justify-between">
|
||||
<span class="text-xs">固定 Header</span>
|
||||
<el-switch v-model="settingsStore.fixedHeader" />
|
||||
</div>
|
||||
|
||||
<div class="py-[8px] flex justify-between">
|
||||
<span class="text-xs">侧边栏 Logo</span>
|
||||
<el-switch v-model="settingsStore.sidebarLogo" />
|
||||
</div>
|
||||
|
||||
<el-divider>主题颜色</el-divider>
|
||||
|
||||
<ul class="w-full space-x-2 flex justify-center py-2">
|
||||
<li
|
||||
class="inline-block w-[30px] h-[30px] cursor-pointer"
|
||||
v-for="(color, index) in themeColors"
|
||||
:key="index"
|
||||
:style="{ background: color }"
|
||||
@click="changeThemeColor(color)"
|
||||
></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings-container {
|
||||
padding: 16px;
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
width: 18%;
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&-item.is-active {
|
||||
border: 2px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
&-mix div:nth-child(1) {
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
background: #1b2a47;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
|
||||
&-mix div:nth-child(2) {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 30%;
|
||||
height: 70%;
|
||||
background: #1b2a47;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
|
||||
&-top div:nth-child(1) {
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
background: #1b2a47;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
|
||||
&-left div:nth-child(1) {
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
background: #1b2a47;
|
||||
}
|
||||
|
||||
&-left div:nth-child(2) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 70%;
|
||||
height: 30%;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { isExternal } from "@/utils/index";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
const appStore = useAppStore();
|
||||
|
||||
const sidebar = computed(() => appStore.sidebar);
|
||||
const device = computed(() => appStore.device);
|
||||
|
||||
const props = defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function push() {
|
||||
if (device.value === "mobile" && sidebar.value.opened == true) {
|
||||
appStore.closeSideBar(false);
|
||||
}
|
||||
router.push(props.to).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
|
||||
<slot />
|
||||
</a>
|
||||
<div v-else @click="push">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const logo = ref(new URL(`../../../assets/logo.png`, import.meta.url).href);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-[50px] bg-gray-800 dark:bg-[var(--el-bg-color-overlay)]">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link
|
||||
v-if="collapse"
|
||||
key="collapse"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
to="/"
|
||||
>
|
||||
<img v-if="settingsStore.sidebarLogo" :src="logo" class="w-5 h-5" />
|
||||
<span v-else class="ml-3 text-white text-sm font-bold">HY2XS</span>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
key="expand"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
to="/"
|
||||
>
|
||||
<img v-if="settingsStore.sidebarLogo" :src="logo" class="w-5 h-5" />
|
||||
<span class="ml-3 text-white text-sm font-bold">HY2XS</span>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// https://cn.vuejs.org/guide/built-ins/transition.html#the-transition-component
|
||||
.sidebarLogoFade-enter-active {
|
||||
transition: opacity 2s;
|
||||
}
|
||||
|
||||
.sidebarLogoFade-leave-active,
|
||||
.sidebarLogoFade-enter-from,
|
||||
.sidebarLogoFade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import path from "path-browserify";
|
||||
import { isExternal } from "@/utils/index";
|
||||
import AppLink from "./Link.vue";
|
||||
|
||||
import { translateRouteTitleI18n } from "@/utils/i18n";
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 路由(eg:level_3_1)
|
||||
*/
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* 父层级完整路由路径(eg:/level/level_3/level_3_1)
|
||||
*/
|
||||
basePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const onlyOneChild = ref(); // 临时变量,唯一子路由
|
||||
|
||||
/**
|
||||
* 判断当前路由是否只有一个子路由
|
||||
*
|
||||
* 1:如果只有一个子路由: 返回 true
|
||||
* 2:如果无子路由 :返回 true
|
||||
*
|
||||
* @param children 子路由数组
|
||||
* @param parent 当前路由
|
||||
*/
|
||||
function hasOneShowingChild(children = [], parent: any) {
|
||||
// 需要显示的子路由数组
|
||||
const showingChildren = children.filter((item: any) => {
|
||||
if (item.meta?.hidden) {
|
||||
return false; // 过滤不显示的子路由
|
||||
} else {
|
||||
onlyOneChild.value = item; // 唯一子路由赋值(多个子路由情况 onlyOneChild 变量是用不上的)
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// 1:如果只有一个子路由, 返回 true
|
||||
if (showingChildren.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2:如果无子路由, 复制当前路由信息作为其子路由,满足只拥有一个子路由的条件,所以返回 true
|
||||
if (showingChildren.length === 0) {
|
||||
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析路径
|
||||
*
|
||||
* @param routePath 路由路径
|
||||
*/
|
||||
function resolvePath(routePath: string) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath;
|
||||
}
|
||||
if (isExternal(props.basePath)) {
|
||||
return props.basePath;
|
||||
}
|
||||
// 完整路径 = 父级路径(/level/level_3) + 路由路径
|
||||
const fullPath = path.resolve(props.basePath, routePath); // 相对路径 → 绝对路径
|
||||
return fullPath;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="!item.meta || !item.meta.hidden">
|
||||
<!-- 只包含一个子路由节点的路由,显示其【唯一子路由】 -->
|
||||
<template
|
||||
v-if="
|
||||
hasOneShowingChild(item.children, item) &&
|
||||
(!onlyOneChild.children || onlyOneChild.noShowingChildren)
|
||||
"
|
||||
>
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)">
|
||||
<svg-icon
|
||||
v-if="onlyOneChild.meta && onlyOneChild.meta.icon"
|
||||
:icon-class="onlyOneChild.meta.icon"
|
||||
/>
|
||||
<template #title>
|
||||
{{ translateRouteTitleI18n(onlyOneChild.meta.title) }}
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
|
||||
<!-- 包含多个子路由 -->
|
||||
<el-sub-menu v-else :index="resolvePath(item.path)" teleported>
|
||||
<template #title>
|
||||
<svg-icon
|
||||
v-if="item.meta && item.meta.icon"
|
||||
:icon-class="item.meta.icon"
|
||||
/>
|
||||
<span v-if="item.meta && item.meta.title">{{
|
||||
translateRouteTitleI18n(item.meta.title)
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<sidebar-item
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import SidebarItem from "./SidebarItem.vue";
|
||||
import Logo from "./Logo.vue";
|
||||
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { storeToRefs } from "pinia";
|
||||
import variables from "@/styles/variables.module.scss";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { sidebarLogo } = storeToRefs(settingsStore);
|
||||
const route = useRoute();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'has-logo': sidebarLogo }">
|
||||
<logo v-if="sidebarLogo" :collapse="!appStore.sidebar.opened" />
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:default-active="route.path"
|
||||
:collapse="!appStore.sidebar.opened"
|
||||
:background-color="variables.menuBg"
|
||||
:text-color="variables.menuText"
|
||||
:active-text-color="variables.menuActiveText"
|
||||
:unique-opened="false"
|
||||
:collapse-transition="false"
|
||||
mode="vertical"
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="route in permissionStore.routes"
|
||||
:item="route"
|
||||
:key="route.path"
|
||||
:base-path="route.path"
|
||||
:is-collapse="!appStore.sidebar.opened"
|
||||
/>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore, TagView } from "@/store/modules/tagsView";
|
||||
|
||||
const tagAndTagSpacing = ref(4);
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
const emits = defineEmits(["scroll"]);
|
||||
const emitScroll = () => {
|
||||
emits("scroll");
|
||||
};
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const scrollWrapper = computed(
|
||||
() => proxy?.$refs.scrollContainer.$refs.wrapRef
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
scrollWrapper.value.addEventListener("scroll", emitScroll, true);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
scrollWrapper.value.removeEventListener("scroll", emitScroll);
|
||||
});
|
||||
|
||||
function handleScroll(e: WheelEvent) {
|
||||
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
|
||||
scrollWrapper.value.scrollLeft =
|
||||
scrollWrapper.value.scrollLeft + eventDelta / 4;
|
||||
}
|
||||
|
||||
function moveToTarget(currentTag: TagView) {
|
||||
const $container = proxy.$refs.scrollContainer.$el;
|
||||
const $containerWidth = $container.offsetWidth;
|
||||
const $scrollWrapper = scrollWrapper.value;
|
||||
|
||||
let firstTag = null;
|
||||
let lastTag = null;
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagsViewStore.visitedViews.length > 0) {
|
||||
firstTag = tagsViewStore.visitedViews[0];
|
||||
lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0;
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
|
||||
} else {
|
||||
const tagListDom = document.getElementsByClassName("tags-item");
|
||||
const currentIndex = tagsViewStore.visitedViews.findIndex(
|
||||
(item) => item === currentTag
|
||||
);
|
||||
let prevTag = null;
|
||||
let nextTag = null;
|
||||
for (const k in tagListDom) {
|
||||
if (k !== "length" && Object.hasOwnProperty.call(tagListDom, k)) {
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex - 1].path
|
||||
) {
|
||||
prevTag = tagListDom[k];
|
||||
}
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex + 1].path
|
||||
) {
|
||||
nextTag = tagListDom[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft =
|
||||
(nextTag as any).offsetLeft +
|
||||
(nextTag as any).offsetWidth +
|
||||
tagAndTagSpacing.value;
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft =
|
||||
(prevTag as any).offsetLeft - tagAndTagSpacing.value;
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
moveToTarget,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-scrollbar
|
||||
ref="scrollContainer"
|
||||
class="scroll-container"
|
||||
:vertical="false"
|
||||
@wheel.prevent="handleScroll"
|
||||
>
|
||||
<slot />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,373 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
ComponentInternalInstance,
|
||||
} from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
import path from "path-browserify";
|
||||
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import { translateRouteTitleI18n } from "@/utils/i18n";
|
||||
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
import { useTagsViewStore, TagView } from "@/store/modules/tagsView";
|
||||
import ScrollPane from "./ScrollPane.vue";
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const permissionStore = usePermissionStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const { visitedViews } = storeToRefs(tagsViewStore);
|
||||
|
||||
const selectedTag = ref({});
|
||||
const scrollPaneRef = ref();
|
||||
const left = ref(0);
|
||||
const top = ref(0);
|
||||
const affixTags = ref<TagView[]>([]);
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
addTags();
|
||||
moveToCurrentTag();
|
||||
},
|
||||
{
|
||||
//初始化立即执行
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const tagMenuVisible = ref(false); // 标签操作菜单显示状态
|
||||
watch(tagMenuVisible, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener("click", closeTagMenu);
|
||||
} else {
|
||||
document.body.removeEventListener("click", closeTagMenu);
|
||||
}
|
||||
});
|
||||
|
||||
function filterAffixTags(routes: any[], basePath = "/") {
|
||||
let tags: TagView[] = [];
|
||||
|
||||
routes.forEach((route) => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = path.resolve(basePath, route.path);
|
||||
tags.push({
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name,
|
||||
meta: { ...route.meta },
|
||||
});
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
const childTags = filterAffixTags(route.children, route.path);
|
||||
if (childTags.length >= 1) {
|
||||
tags = tags.concat(childTags);
|
||||
}
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
function initTags() {
|
||||
const tags: TagView[] = filterAffixTags(permissionStore.routes);
|
||||
affixTags.value = tags;
|
||||
for (const tag of tags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
tagsViewStore.addVisitedView(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addTags() {
|
||||
if (route.name) {
|
||||
tagsViewStore.addView(route);
|
||||
}
|
||||
}
|
||||
|
||||
function moveToCurrentTag() {
|
||||
nextTick(() => {
|
||||
for (const r of tagsViewStore.visitedViews) {
|
||||
if (r.path === route.path) {
|
||||
scrollPaneRef.value.moveToTarget(r);
|
||||
// when query is different then update
|
||||
if (r.fullPath !== route.fullPath) {
|
||||
tagsViewStore.updateVisitedView(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isActive(tag: TagView) {
|
||||
return tag.path === route.path;
|
||||
}
|
||||
|
||||
function isAffix(tag: TagView) {
|
||||
return tag.meta && tag.meta.affix;
|
||||
}
|
||||
|
||||
function isFirstView() {
|
||||
try {
|
||||
return (
|
||||
(selectedTag.value as TagView).fullPath ===
|
||||
tagsViewStore.visitedViews[1].fullPath ||
|
||||
(selectedTag.value as TagView).fullPath === "/index"
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isLastView() {
|
||||
try {
|
||||
return (
|
||||
(selectedTag.value as TagView).fullPath ===
|
||||
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSelectedTag(view: TagView) {
|
||||
tagsViewStore.delCachedView(view);
|
||||
const { fullPath } = view;
|
||||
nextTick(() => {
|
||||
router.replace({ path: "/redirect" + fullPath }).catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toLastView(visitedViews: TagView[], view?: any) {
|
||||
const latestView = visitedViews.slice(-1)[0];
|
||||
if (latestView && latestView.fullPath) {
|
||||
router.push(latestView.fullPath);
|
||||
} else {
|
||||
// now the default is to redirect to the home page if there is no tags-view,
|
||||
// you can adjust it according to your needs.
|
||||
if (view.name === "Dashboard") {
|
||||
// to reload home page
|
||||
router.replace({ path: "/redirect" + view.fullPath });
|
||||
} else {
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeSelectedTag(view: TagView) {
|
||||
tagsViewStore.delView(view).then((res: any) => {
|
||||
if (isActive(view)) {
|
||||
toLastView(res.visitedViews, view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeLeftTags() {
|
||||
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeRightTags() {
|
||||
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeOtherTags() {
|
||||
router.push(selectedTag.value);
|
||||
tagsViewStore.delOtherViews(selectedTag.value).then(() => {
|
||||
moveToCurrentTag();
|
||||
});
|
||||
}
|
||||
|
||||
function closeAllTags(view: TagView) {
|
||||
tagsViewStore.delAllViews().then((res: any) => {
|
||||
toLastView(res.visitedViews, view);
|
||||
});
|
||||
}
|
||||
|
||||
function openTagMenu(tag: TagView, e: MouseEvent) {
|
||||
const menuMinWidth = 105;
|
||||
|
||||
// console.log("test", proxy?.$el);
|
||||
|
||||
const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
|
||||
const offsetWidth = proxy?.$el.offsetWidth; // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth; // left boundary
|
||||
const l = e.clientX - offsetLeft + 15; // 15: margin right
|
||||
|
||||
if (l > maxLeft) {
|
||||
left.value = maxLeft;
|
||||
} else {
|
||||
left.value = l;
|
||||
}
|
||||
|
||||
top.value = e.clientY;
|
||||
tagMenuVisible.value = true;
|
||||
selectedTag.value = tag;
|
||||
}
|
||||
|
||||
function closeTagMenu() {
|
||||
tagMenuVisible.value = false;
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
closeTagMenu();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tags-container">
|
||||
<scroll-pane ref="scrollPaneRef" @scroll="handleScroll">
|
||||
<router-link
|
||||
:class="'tags-item ' + (isActive(tag) ? 'active' : '')"
|
||||
v-for="tag in visitedViews"
|
||||
:key="tag.path"
|
||||
:data-path="tag.path"
|
||||
:to="{ path: tag.path, query: tag.query }"
|
||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent="openTagMenu(tag, $event)"
|
||||
>
|
||||
{{ translateRouteTitleI18n(tag.meta?.title) }}
|
||||
<span
|
||||
v-if="!isAffix(tag)"
|
||||
class="tags-item-close"
|
||||
@click.prevent.stop="closeSelectedTag(tag)"
|
||||
>
|
||||
<i-ep-close class="text-[10px]" />
|
||||
</span>
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
|
||||
<!-- tag标签操作菜单 -->
|
||||
<ul
|
||||
v-show="tagMenuVisible"
|
||||
class="tag-menu"
|
||||
:style="{ left: left + 'px', top: top + 'px' }"
|
||||
>
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
<svg-icon icon-class="refresh" />
|
||||
刷新
|
||||
</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||
<svg-icon icon-class="close" />
|
||||
关闭
|
||||
</li>
|
||||
<li @click="closeOtherTags">
|
||||
<svg-icon icon-class="close_other" />
|
||||
关闭其它
|
||||
</li>
|
||||
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||
<svg-icon icon-class="close_left" />
|
||||
关闭左侧
|
||||
</li>
|
||||
<li v-if="!isLastView()" @click="closeRightTags">
|
||||
<svg-icon icon-class="close_right" />
|
||||
关闭右侧
|
||||
</li>
|
||||
<li @click="closeAllTags(selectedTag)">
|
||||
<svg-icon icon-class="close_all" />
|
||||
关闭所有
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-container {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
box-shadow: 0 1px 1px var(--el-box-shadow-light);
|
||||
|
||||
.tags-item {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
margin: 4px 0 0 5px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 5px;
|
||||
content: "";
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&-close {
|
||||
border-radius: 100%;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: rgb(0 0 0 / 16%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-menu {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
font-size: 12px;
|
||||
background: var(--el-bg-color-overlay);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
|
||||
li {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as Navbar } from "./Navbar.vue";
|
||||
export { default as AppMain } from "./AppMain.vue";
|
||||
export { default as Settings } from "./Settings/index.vue";
|
||||
export { default as TagsView } from "./TagsView/index.vue";
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watchEffect } from "vue";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { AppMain, Navbar, Settings, TagsView } from "./components/index";
|
||||
import Sidebar from "./components/Sidebar/index.vue";
|
||||
import RightPanel from "@/components/RightPanel/index.vue";
|
||||
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
/**
|
||||
* 响应式布局容器固定宽度
|
||||
*
|
||||
* 大屏(>=1200px)
|
||||
* 中屏(>=992px)
|
||||
* 小屏(>=768px)
|
||||
*/
|
||||
const WIDTH = 992;
|
||||
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const fixedHeader = computed(() => settingsStore.fixedHeader);
|
||||
const showTagsView = computed(() => settingsStore.tagsView);
|
||||
const showSettings = computed(() => settingsStore.showSettings);
|
||||
|
||||
const classObj = computed(() => ({
|
||||
hideSidebar: !appStore.sidebar.opened,
|
||||
openSidebar: appStore.sidebar.opened,
|
||||
withoutAnimation: appStore.sidebar.withoutAnimation,
|
||||
mobile: appStore.device === "mobile",
|
||||
}));
|
||||
|
||||
watchEffect(() => {
|
||||
if (width.value < WIDTH) {
|
||||
appStore.toggleDevice("mobile");
|
||||
appStore.closeSideBar(true);
|
||||
} else {
|
||||
appStore.toggleDevice("desktop");
|
||||
|
||||
if (width.value >= 1200) {
|
||||
//大屏
|
||||
appStore.openSideBar(true);
|
||||
} else {
|
||||
appStore.closeSideBar(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function handleOutsideClick() {
|
||||
appStore.closeSideBar(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<!-- 手机设备侧边栏打开遮罩层 -->
|
||||
<div
|
||||
v-if="classObj.mobile && classObj.openSidebar"
|
||||
class="drawer-bg"
|
||||
@click="handleOutsideClick"
|
||||
></div>
|
||||
|
||||
<Sidebar class="sidebar-container" />
|
||||
|
||||
<div :class="{ hasTagsView: showTagsView }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<navbar />
|
||||
<tags-view v-if="showTagsView" />
|
||||
</div>
|
||||
|
||||
<!--主页面-->
|
||||
<app-main />
|
||||
|
||||
<!-- 设置面板 -->
|
||||
<RightPanel v-if="showSettings">
|
||||
<settings />
|
||||
</RightPanel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-wrapper {
|
||||
&::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.mobile.openSidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - #{$sideBarWidth});
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "@/router";
|
||||
import { setupStore } from "@/store";
|
||||
import { setupDirective } from "@/directive";
|
||||
|
||||
import "@/permission";
|
||||
|
||||
// 本地SVG图标
|
||||
import "virtual:svg-icons-register";
|
||||
|
||||
// 国际化
|
||||
import i18n from "@/lang/index";
|
||||
|
||||
// 样式
|
||||
import "element-plus/theme-chalk/dark/css-vars.css";
|
||||
import "@/styles/index.scss";
|
||||
import "uno.css";
|
||||
|
||||
const app = createApp(App);
|
||||
// 全局注册 自定义指令(directive)
|
||||
setupDirective(app);
|
||||
// 全局注册 状态管理(store)
|
||||
setupStore(app);
|
||||
|
||||
app.use(router).use(i18n).mount("#app");
|
||||
@@ -0,0 +1,62 @@
|
||||
import router from "@/router";
|
||||
import { useAccountStoreHook } from "@/store/modules/account";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
|
||||
NProgress.configure({ showSpinner: false }); // 进度条
|
||||
|
||||
const permissionStore = usePermissionStoreHook();
|
||||
|
||||
// 白名单路由
|
||||
const whiteList = ["/login", "/register"];
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start();
|
||||
const hasToken = localStorage.getItem("accessToken");
|
||||
if (hasToken) {
|
||||
if (to.path === "/login") {
|
||||
// 如果已登录,跳转首页
|
||||
next({ path: "/" });
|
||||
NProgress.done();
|
||||
} else {
|
||||
const AccountStore = useAccountStoreHook();
|
||||
const hasRoles = AccountStore.roles && AccountStore.roles.length > 0;
|
||||
if (hasRoles) {
|
||||
// 未匹配到任何路由,跳转404
|
||||
if (to.matched.length === 0) {
|
||||
from.name ? next({ name: from.name }) : next("/404");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { roles } = await AccountStore.getAccountInfo();
|
||||
const accessRoutes = permissionStore.generateRoutes(roles);
|
||||
accessRoutes.forEach((route) => {
|
||||
router.addRoute(route);
|
||||
});
|
||||
next({ ...to, replace: true });
|
||||
} catch (error) {
|
||||
// 移除 token 并跳转登录页
|
||||
await AccountStore.resetToken();
|
||||
next(`/login?redirect=${to.path}`);
|
||||
NProgress.done();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 未登录可以访问白名单页面
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
next();
|
||||
} else {
|
||||
next(`/login?redirect=${to.path}`);
|
||||
NProgress.done();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done();
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
RouteLocationNormalized,
|
||||
RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
|
||||
export const Layout = () => import("@/layout/index.vue");
|
||||
|
||||
// 静态路由
|
||||
export const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/redirect",
|
||||
component: Layout,
|
||||
meta: { hidden: true },
|
||||
children: [
|
||||
{
|
||||
path: "/redirect/:path(.*)",
|
||||
component: () => import("@/views/redirect/index.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: "/login",
|
||||
component: () => import("@/views/login/index.vue"),
|
||||
meta: { hidden: true },
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: Layout,
|
||||
redirect: "/info/account",
|
||||
children: [
|
||||
{
|
||||
path: "401",
|
||||
component: () => import("@/views/error-page/401.vue"),
|
||||
meta: { hidden: true },
|
||||
},
|
||||
{
|
||||
path: "404",
|
||||
component: () => import("@/views/error-page/404.vue"),
|
||||
meta: { hidden: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const asyncRoutes: any[] = [
|
||||
{
|
||||
path: "/info",
|
||||
component: "Layout",
|
||||
redirect: "/account",
|
||||
name: "Info",
|
||||
meta: {
|
||||
title: "info",
|
||||
icon: "user",
|
||||
roles: ["user", "admin"],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "account",
|
||||
component: "info/account/index",
|
||||
name: "AccountInfo",
|
||||
meta: {
|
||||
title: "infoAccount",
|
||||
icon: "user",
|
||||
roles: ["user", "admin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/account",
|
||||
component: "Layout",
|
||||
redirect: "/list",
|
||||
name: "Account",
|
||||
meta: { title: "account", icon: "users", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "list",
|
||||
component: "account/list/index",
|
||||
name: "AccountList",
|
||||
meta: {
|
||||
title: "accountList",
|
||||
icon: "users",
|
||||
roles: ["admin"],
|
||||
},
|
||||
props: (route: RouteLocationNormalized) => ({
|
||||
focus: route.query.focus,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/hysteria",
|
||||
component: "Layout",
|
||||
redirect: "/list",
|
||||
name: "Hysteria",
|
||||
meta: { title: "hysteria", icon: "hysteria", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "list",
|
||||
component: "hysteria/list/index",
|
||||
name: "HysteriaList",
|
||||
meta: {
|
||||
title: "hysteriaList",
|
||||
icon: "hysteria",
|
||||
roles: ["admin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/config",
|
||||
component: "Layout",
|
||||
redirect: "/list",
|
||||
name: "Config",
|
||||
meta: { title: "config", icon: "setting", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "list",
|
||||
component: "config/list/index",
|
||||
name: "ConfigList",
|
||||
meta: {
|
||||
title: "configList",
|
||||
icon: "setting",
|
||||
roles: ["admin"],
|
||||
},
|
||||
props: (route: RouteLocationNormalized) => ({
|
||||
focus: route.query.focus,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/monitor",
|
||||
component: "Layout",
|
||||
redirect: "/monitor",
|
||||
name: "Monitor",
|
||||
meta: { title: "monitor", icon: "report", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "system",
|
||||
component: "monitor/system/index",
|
||||
name: "MonitorSystem",
|
||||
meta: {
|
||||
title: "monitorSystem",
|
||||
icon: "report",
|
||||
roles: ["admin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/log",
|
||||
component: "Layout",
|
||||
redirect: "/system",
|
||||
name: "Log",
|
||||
meta: { title: "log", icon: "error", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "system",
|
||||
component: "log/system/index",
|
||||
name: "LogSystem",
|
||||
meta: {
|
||||
title: "logSystem",
|
||||
icon: "log-system",
|
||||
roles: ["admin"],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "hysteria",
|
||||
component: "log/hysteria/index",
|
||||
name: "LogHysteria",
|
||||
meta: {
|
||||
title: "logHysteria",
|
||||
icon: "log-hysteria",
|
||||
roles: ["admin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 创建路由
|
||||
*/
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: constantRoutes as RouteRecordRaw[],
|
||||
// 刷新时,滚动条位置还原
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
});
|
||||
|
||||
/**
|
||||
* 重置路由
|
||||
*/
|
||||
export function resetRouter() {
|
||||
router.replace({ path: "/login" });
|
||||
location.reload();
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,67 @@
|
||||
// 系统设置
|
||||
interface DefaultSettings {
|
||||
/**
|
||||
* 系统title
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* 是否显示设置
|
||||
*/
|
||||
showSettings: boolean;
|
||||
/**
|
||||
* 是否显示多标签导航
|
||||
*/
|
||||
tagsView: boolean;
|
||||
/**
|
||||
*是否固定头部
|
||||
*/
|
||||
fixedHeader: boolean;
|
||||
/**
|
||||
* 是否显示侧边栏Logo
|
||||
*/
|
||||
sidebarLogo: boolean;
|
||||
/**
|
||||
* 导航栏布局
|
||||
*/
|
||||
layout: string;
|
||||
/**
|
||||
* 主题颜色
|
||||
*/
|
||||
themeColor: string;
|
||||
/**
|
||||
* 主题模式
|
||||
*/
|
||||
theme: string;
|
||||
|
||||
/**
|
||||
* 布局大小
|
||||
*/
|
||||
size: string;
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
language: string;
|
||||
}
|
||||
|
||||
const defaultSettings: DefaultSettings = {
|
||||
title: "HY2XS admin",
|
||||
showSettings: true,
|
||||
tagsView: true,
|
||||
fixedHeader: false,
|
||||
sidebarLogo: true,
|
||||
layout: "left",
|
||||
themeColor: "#409EFF",
|
||||
/**
|
||||
* 主题模式
|
||||
*
|
||||
* dark:暗黑模式
|
||||
* light: 明亮模式
|
||||
*/
|
||||
theme: "dark",
|
||||
size: "default", // default |large |small
|
||||
language: "ru", // ru | en
|
||||
};
|
||||
|
||||
export default defaultSettings;
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { App } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
// 全局注册 store
|
||||
export function setupStore(app: App<Element>) {
|
||||
app.use(store);
|
||||
}
|
||||
|
||||
export { store };
|
||||
@@ -0,0 +1,91 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
import { getAccountInfoApi, loginApi } from "@/api/account";
|
||||
import { resetRouter } from "@/router";
|
||||
import { store } from "@/store";
|
||||
|
||||
import { AccountInfo, AccountLoginDto } from "@/api/account/types";
|
||||
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
export const useAccountStore = defineStore("account", () => {
|
||||
// state
|
||||
const token = useStorage("accessToken", "");
|
||||
const id = ref(0);
|
||||
const username = ref("");
|
||||
const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
function login(accountLoginDto: AccountLoginDto) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
loginApi(accountLoginDto)
|
||||
.then((response) => {
|
||||
const { tokenType, accessToken } = response.data;
|
||||
token.value = tokenType + " " + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 查询当前
|
||||
function getAccountInfo() {
|
||||
return new Promise<AccountInfo>((resolve, reject) => {
|
||||
getAccountInfoApi()
|
||||
.then(({ data }) => {
|
||||
if (!data) {
|
||||
return reject("Verification failed, please Login again.");
|
||||
}
|
||||
if (!data.roles || data.roles.length <= 0) {
|
||||
reject("getAccountInfoApi: roles must be a non-null array!");
|
||||
}
|
||||
id.value = data.id;
|
||||
username.value = data.username;
|
||||
roles.value = data.roles;
|
||||
resolve(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 注销
|
||||
function logout() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
resetRouter();
|
||||
resetToken();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// 重置
|
||||
function resetToken() {
|
||||
token.value = "";
|
||||
id.value = 0;
|
||||
username.value = "";
|
||||
roles.value = [];
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
id,
|
||||
username,
|
||||
roles,
|
||||
login,
|
||||
getAccountInfo,
|
||||
logout,
|
||||
resetToken,
|
||||
};
|
||||
});
|
||||
|
||||
// 非setup
|
||||
export function useAccountStoreHook() {
|
||||
return useAccountStore(store);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import defaultSettings from "@/settings";
|
||||
|
||||
import en from "element-plus/es/locale/lang/en";
|
||||
import ru from "element-plus/es/locale/lang/ru";
|
||||
|
||||
// setup
|
||||
export const useAppStore = defineStore("app", () => {
|
||||
// state
|
||||
const device = useStorage("device", "desktop");
|
||||
const size = useStorage<any>("size", defaultSettings.size);
|
||||
const language = useStorage("language", defaultSettings.language);
|
||||
|
||||
const sidebarStatus = useStorage("sidebarStatus", "closed");
|
||||
const sidebar = reactive({
|
||||
opened: sidebarStatus.value !== "closed",
|
||||
withoutAnimation: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据语言标识读取对应的语言包
|
||||
*/
|
||||
const locale = computed(() => {
|
||||
return language?.value == "en" ? en : ru;
|
||||
});
|
||||
|
||||
// actions
|
||||
function toggleSidebar(withoutAnimation: boolean) {
|
||||
sidebar.opened = !sidebar.opened;
|
||||
sidebar.withoutAnimation = withoutAnimation;
|
||||
if (sidebar.opened) {
|
||||
sidebarStatus.value = "opened";
|
||||
} else {
|
||||
sidebarStatus.value = "closed";
|
||||
}
|
||||
}
|
||||
|
||||
function closeSideBar(withoutAnimation: boolean) {
|
||||
sidebar.opened = false;
|
||||
sidebar.withoutAnimation = withoutAnimation;
|
||||
sidebarStatus.value = "closed";
|
||||
}
|
||||
|
||||
function openSideBar(withoutAnimation: boolean) {
|
||||
sidebar.opened = true;
|
||||
sidebar.withoutAnimation = withoutAnimation;
|
||||
sidebarStatus.value = "opened";
|
||||
}
|
||||
|
||||
function toggleDevice(val: string) {
|
||||
device.value = val;
|
||||
}
|
||||
|
||||
function changeSize(val: string) {
|
||||
size.value = val;
|
||||
}
|
||||
/**
|
||||
* 切换语言
|
||||
*
|
||||
* @param val
|
||||
*/
|
||||
function changeLanguage(val: string) {
|
||||
language.value = val;
|
||||
}
|
||||
|
||||
return {
|
||||
device,
|
||||
sidebar,
|
||||
language,
|
||||
locale,
|
||||
size,
|
||||
toggleDevice,
|
||||
changeSize,
|
||||
changeLanguage,
|
||||
toggleSidebar,
|
||||
closeSideBar,
|
||||
openSideBar,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import { defineStore } from "pinia";
|
||||
import { asyncRoutes, constantRoutes } from "@/router";
|
||||
import { store } from "@/store";
|
||||
|
||||
const modules = import.meta.glob("../../views/**/**.vue");
|
||||
const Layout = () => import("@/layout/index.vue");
|
||||
|
||||
/**
|
||||
* Use meta.role to determine if the current user has permission
|
||||
*
|
||||
* @param roles 用户角色集合
|
||||
* @param route 路由
|
||||
* @returns
|
||||
*/
|
||||
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
|
||||
if (route.meta && route.meta.roles) {
|
||||
// 角色【超级管理员】拥有所有权限,忽略校验
|
||||
if (roles.includes("admin")) {
|
||||
return true;
|
||||
}
|
||||
return roles.some((role) => {
|
||||
if (route.meta?.roles !== undefined) {
|
||||
return (route.meta.roles as string[]).includes(role);
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 递归过滤有权限的异步(动态)路由
|
||||
*
|
||||
* @param routes 接口返回的异步(动态)路由
|
||||
* @param roles 用户角色集合
|
||||
* @returns 返回用户有权限的异步(动态)路由
|
||||
*/
|
||||
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
|
||||
const asyncRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
routes.forEach((route) => {
|
||||
const tmpRoute = { ...route }; // ES6扩展运算符复制新对象
|
||||
|
||||
// 判断用户(角色)是否有该路由的访问权限
|
||||
if (hasPermission(roles, tmpRoute)) {
|
||||
if (tmpRoute.component?.toString() == "Layout") {
|
||||
tmpRoute.component = Layout;
|
||||
} else {
|
||||
const component = modules[`../../views/${tmpRoute.component}.vue`];
|
||||
if (component) {
|
||||
tmpRoute.component = component;
|
||||
} else {
|
||||
tmpRoute.component = modules[`../../views/error-page/404.vue`];
|
||||
}
|
||||
}
|
||||
|
||||
if (tmpRoute.children) {
|
||||
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, roles);
|
||||
}
|
||||
|
||||
asyncRoutes.push(tmpRoute);
|
||||
}
|
||||
});
|
||||
|
||||
return asyncRoutes;
|
||||
};
|
||||
|
||||
// setup
|
||||
export const usePermissionStore = defineStore("permission", () => {
|
||||
// state
|
||||
const routes = ref<RouteRecordRaw[]>([]);
|
||||
|
||||
// actions
|
||||
function setRoutes(newRoutes: RouteRecordRaw[]) {
|
||||
routes.value = constantRoutes.concat(newRoutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成动态路由
|
||||
*
|
||||
* @param roles 用户角色集合
|
||||
* @returns
|
||||
*/
|
||||
function generateRoutes(roles: string[]) {
|
||||
// 根据角色获取有访问权限的路由
|
||||
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
|
||||
setRoutes(accessedRoutes);
|
||||
return accessedRoutes;
|
||||
}
|
||||
|
||||
return { routes, setRoutes, generateRoutes };
|
||||
});
|
||||
|
||||
// 非setup
|
||||
export function usePermissionStoreHook() {
|
||||
return usePermissionStore(store);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { defineStore } from "pinia";
|
||||
import defaultSettings from "@/settings";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
export const useSettingsStore = defineStore("setting", () => {
|
||||
// state
|
||||
const tagsView = useStorage<boolean>("tagsView", defaultSettings.tagsView);
|
||||
|
||||
const showSettings = ref<boolean>(defaultSettings.showSettings);
|
||||
const fixedHeader = ref<boolean>(defaultSettings.fixedHeader);
|
||||
const sidebarLogo = ref<boolean>(defaultSettings.sidebarLogo);
|
||||
|
||||
const layout = useStorage<string>("layout", defaultSettings.layout);
|
||||
|
||||
const themeColor = useStorage<string>(
|
||||
"themeColor",
|
||||
defaultSettings.themeColor
|
||||
);
|
||||
|
||||
// actions
|
||||
function changeSetting(param: { key: string; value: any }) {
|
||||
const { key, value } = param;
|
||||
switch (key) {
|
||||
case "showSettings":
|
||||
showSettings.value = value;
|
||||
break;
|
||||
case "fixedHeader":
|
||||
fixedHeader.value = value;
|
||||
break;
|
||||
case "tagsView":
|
||||
tagsView.value = value;
|
||||
break;
|
||||
case "sidevarLogo":
|
||||
sidebarLogo.value = value;
|
||||
break;
|
||||
case "layout":
|
||||
layout.value = value;
|
||||
break;
|
||||
case "themeColor":
|
||||
themeColor.value = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showSettings,
|
||||
tagsView,
|
||||
fixedHeader,
|
||||
sidebarLogo,
|
||||
layout,
|
||||
themeColor,
|
||||
changeSetting,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { RouteLocationNormalized } from "vue-router";
|
||||
|
||||
export interface TagView extends Partial<RouteLocationNormalized> {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// setup
|
||||
export const useTagsViewStore = defineStore("tagsView", () => {
|
||||
// state
|
||||
const visitedViews = ref<TagView[]>([]);
|
||||
const cachedViews = ref<string[]>([]);
|
||||
|
||||
// actions
|
||||
function addVisitedView(view: TagView) {
|
||||
if (visitedViews.value.some((v) => v.path === view.path)) return;
|
||||
if (view.meta && view.meta.affix) {
|
||||
visitedViews.value.unshift(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || "no-name",
|
||||
})
|
||||
);
|
||||
} else {
|
||||
visitedViews.value.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || "no-name",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function addCachedView(view: TagView) {
|
||||
const viewName = view.name as string;
|
||||
if (cachedViews.value.includes(viewName)) return;
|
||||
if (view.meta?.keepAlive) {
|
||||
cachedViews.value.push(viewName);
|
||||
}
|
||||
}
|
||||
|
||||
function delVisitedView(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
for (const [i, v] of visitedViews.value.entries()) {
|
||||
if (v.path === view.path) {
|
||||
visitedViews.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delCachedView(view: TagView) {
|
||||
const viewName = view.name as string;
|
||||
return new Promise((resolve) => {
|
||||
const index = cachedViews.value.indexOf(viewName);
|
||||
index > -1 && cachedViews.value.splice(index, 1);
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherVisitedViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
visitedViews.value = visitedViews.value.filter((v) => {
|
||||
return v.meta?.affix || v.path === view.path;
|
||||
});
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherCachedViews(view: TagView) {
|
||||
const viewName = view.name as string;
|
||||
return new Promise((resolve) => {
|
||||
const index = cachedViews.value.indexOf(viewName);
|
||||
if (index > -1) {
|
||||
cachedViews.value = cachedViews.value.slice(index, index + 1);
|
||||
} else {
|
||||
// if index = -1, there is no cached tags
|
||||
cachedViews.value = [];
|
||||
}
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function updateVisitedView(view: TagView) {
|
||||
for (let v of visitedViews.value) {
|
||||
if (v.path === view.path) {
|
||||
v = Object.assign(v, view);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addView(view: TagView) {
|
||||
addVisitedView(view);
|
||||
addCachedView(view);
|
||||
}
|
||||
|
||||
function delView(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
delVisitedView(view);
|
||||
delCachedView(view);
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
delOtherVisitedViews(view);
|
||||
delOtherCachedViews(view);
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delLeftViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
const currIndex = visitedViews.value.findIndex(
|
||||
(v) => v.path === view.path
|
||||
);
|
||||
if (currIndex === -1) {
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||
// affix:true 固定tag,例如“首页”
|
||||
if (index >= currIndex || (item.meta && item.meta.affix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cacheIndex = cachedViews.value.indexOf(item.name as string);
|
||||
if (cacheIndex > -1) {
|
||||
cachedViews.value.splice(cacheIndex, 1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
function delRightViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
const currIndex = visitedViews.value.findIndex(
|
||||
(v) => v.path === view.path
|
||||
);
|
||||
if (currIndex === -1) {
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||
// affix:true 固定tag,例如“首页”
|
||||
if (index <= currIndex || (item.meta && item.meta.affix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cacheIndex = cachedViews.value.indexOf(item.name as string);
|
||||
if (cacheIndex > -1) {
|
||||
cachedViews.value.splice(cacheIndex, 1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delAllViews() {
|
||||
return new Promise((resolve) => {
|
||||
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
|
||||
visitedViews.value = affixTags;
|
||||
cachedViews.value = [];
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delAllVisitedViews() {
|
||||
return new Promise((resolve) => {
|
||||
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
|
||||
visitedViews.value = affixTags;
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delAllCachedViews() {
|
||||
return new Promise((resolve) => {
|
||||
cachedViews.value = [];
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
visitedViews,
|
||||
cachedViews,
|
||||
addVisitedView,
|
||||
addCachedView,
|
||||
delVisitedView,
|
||||
delCachedView,
|
||||
delOtherVisitedViews,
|
||||
delOtherCachedViews,
|
||||
updateVisitedView,
|
||||
addView,
|
||||
delView,
|
||||
delOtherViews,
|
||||
delLeftViews,
|
||||
delRightViews,
|
||||
delAllViews,
|
||||
delAllVisitedViews,
|
||||
delAllCachedViews,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
html.dark {
|
||||
--menuBg: var(--el-bg-color-overlay);
|
||||
--menuText: #fff;
|
||||
--menuActiveText: var(--el-menu-active-color);
|
||||
--menuHover: rgb(0 0 0 / 20%);
|
||||
--subMenuBg: var(--el-menu-bg-color);
|
||||
--subMenuActiveText: var(--el-menu-active-color);
|
||||
--subMenuHover: rgb(0 0 0 / 20%);
|
||||
|
||||
.navbar {
|
||||
color: var(--el-text-color-regular);
|
||||
background-color: var(--el-bg-color);
|
||||
|
||||
.setting-container .setting-item:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel-btn {
|
||||
background-color: var(--el-color-primary-dark);
|
||||
}
|
||||
|
||||
.svg-icon,
|
||||
svg {
|
||||
fill: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
.el-menu-item.is-active .svg-icon {
|
||||
fill: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
:root {
|
||||
// 这里可以设置你自定义的颜色变量
|
||||
// 这个是element主要按钮:active的颜色,当主题更改后此变量的值也随之更改
|
||||
--el-color-primary-dark: #0d84ff;
|
||||
}
|
||||
|
||||
// 覆盖 element-plus 的样式
|
||||
.el-breadcrumb__inner,
|
||||
.el-breadcrumb__inner a {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.el-upload {
|
||||
input[type="file"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload__input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// dropdown
|
||||
.el-dropdown-menu {
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// to fix el-date-picker css style
|
||||
.el-range-separator {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
// 选中行背景色值
|
||||
.el-table__body tr.current-row td {
|
||||
background-color: #e1f3d8b5 !important;
|
||||
}
|
||||
|
||||
// card 的header统一高度
|
||||
.el-card__header {
|
||||
height: 60px !important;
|
||||
}
|
||||
|
||||
// 表格表头和表体未对齐
|
||||
.el-table__header col[name="gutter"] {
|
||||
display: table-cell !important;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
@import "./sidebar";
|
||||
@import "./reset";
|
||||
@import "./dark";
|
||||
@import "./element-plus";
|
||||
|
||||
.app-container {
|
||||
margin: 20px;
|
||||
|
||||
.search {
|
||||
padding: 18px 0 0 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-color: currentcolor;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
line-height: 1.5;
|
||||
tab-size: 4;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
||||
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
line-height: inherit;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: -0.15em; //因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果
|
||||
}
|
||||
|
||||
ul,
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
a:active,
|
||||
div:focus {
|
||||
outline: none;
|
||||
}
|
||||