Русифицировать README и интерфейс HY2XS admin
This commit is contained in:
@@ -1,76 +1,68 @@
|
|||||||
# HY2XS
|
# HY2XS
|
||||||
|
|
||||||
HY2XS is a two-layer install package for a clean Debian 12 target:
|
HY2XS — двухслойный install package для развёртывания на чистом Debian 12 amd64.
|
||||||
|
|
||||||
- local builder layer: creates one transferable install archive
|
Состав baseline:
|
||||||
- install-only Bun/TypeScript orchestrator: runs on the target as a compiled artifact
|
|
||||||
- bundled HY2XS admin fork: shipped inside the package
|
|
||||||
- vanilla Hysteria2: downloaded from the official upstream during install
|
|
||||||
|
|
||||||
The baseline deliberately excludes target-side builds, update, rollback, uninstall, Telegram bot delivery, billing, and external access/profile delivery.
|
- production builder на Debian 12 amd64 создаёт один переносимый архив;
|
||||||
|
- install-only оркестратор на Bun + TypeScript поставляется на target как готовый compiled artifact;
|
||||||
|
- HY2XS admin поставляется в составе пакета как bundled fork;
|
||||||
|
- vanilla Hysteria2 скачивается с official upstream во время установки.
|
||||||
|
|
||||||
## Repository Layout
|
В baseline намеренно не входят target-side build, update, rollback, uninstall, Telegram bot delivery, billing и внешний контур выдачи access/profile.
|
||||||
|
|
||||||
- `builder/` - local shell-first packaging pipeline
|
## Структура репозитория
|
||||||
- `orchestrator/` - Bun + TypeScript install-only orchestrator sources
|
|
||||||
- `package/` - package skeleton: thin entrypoint, templates, systemd units, package docs
|
|
||||||
- `apps/` - current HY2XS admin fork source
|
|
||||||
- `docs/` - baseline architecture and acceptance docs
|
|
||||||
- `hy2xs_implementation_plan-no_git/` - implementation roadmap and task breakdown
|
|
||||||
- `dist/` - final install archives generated by the builder
|
|
||||||
|
|
||||||
## Build
|
- [`tools/build`](tools/build) — production builder и packaging pipeline.
|
||||||
|
- [`orchestrator`](orchestrator) — исходники install-only оркестратора на Bun + TypeScript.
|
||||||
|
- [`package`](package) — skeleton итогового install package: entrypoint, templates, systemd units, package docs.
|
||||||
|
- [`apps`](apps) — исходники HY2XS admin fork.
|
||||||
|
- [`docs`](docs) — архитектурная документация и acceptance.
|
||||||
|
- [`hy2xs_implementation_plan-no_git`](hy2xs_implementation_plan-no_git) — roadmap и task breakdown.
|
||||||
|
- [`dist`](dist) — итоговые install archives, создаются builder'ом и не хранятся в git.
|
||||||
|
|
||||||
On the local build machine:
|
## Сборка
|
||||||
|
|
||||||
|
Production-сборка выполняется только на Debian 12 amd64:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
PACKAGE_VERSION=0.1.0 ./builder/build.sh
|
PACKAGE_VERSION=0.1.0 ./tools/build/build.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
If tools are not in `PATH`, pass explicit binaries:
|
Builder сам проверяет ОС, архитектуру, структуру проекта, lock-файлы, версии Go/Bun/Node.js/pnpm и при необходимости скачивает локальный toolchain.
|
||||||
|
|
||||||
```sh
|
Ожидаемый результат:
|
||||||
GO_BIN=/path/to/go PNPM_BIN=/path/to/pnpm BUN_BIN=/path/to/bun PACKAGE_VERSION=0.1.0 ./builder/build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
On the current Windows workstation the known tool paths are:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:PATH = "E:\Git\cmd;E:\go\bin;" + $env:PATH
|
|
||||||
E:\go\bin\go.exe test ./...
|
|
||||||
pnpm.cmd install --frozen-lockfile
|
|
||||||
```
|
|
||||||
|
|
||||||
The expected result is:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
dist/hy2xs-install-0.1.0.tar.gz
|
dist/hy2xs-install-0.1.0.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
The target server must not run `bun install`, `pnpm install`, TypeScript transpilation, frontend builds, or Go builds.
|
На target server не должно быть `bun install`, `pnpm install`, TypeScript transpilation, frontend build или Go build.
|
||||||
|
|
||||||
## Publication
|
Подробности сборки: [`tools/build/README.md`](tools/build/README.md).
|
||||||
|
|
||||||
Target repository:
|
## Публикация
|
||||||
|
|
||||||
|
Целевой репозиторий:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
https://git.ext.flamy.studio/flamy_dev/HY2XS_flamy.git
|
https://git.ext.flamy.studio/flamy_dev/HY2XS_flamy.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Publish only after the full baseline acceptance is complete, including the Bun/TypeScript orchestrator artifact build and final package smoke checks.
|
Публиковать изменения нужно только после прохождения baseline acceptance, сборки оркестратора и smoke checks итогового пакета.
|
||||||
|
|
||||||
## Install
|
## Установка
|
||||||
|
|
||||||
On a clean Debian 12 target, unpack the archive and run as root:
|
На чистом Debian 12 target нужно распаковать архив и запустить от root:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./install.sh --non-interactive --domain example.com
|
./install.sh --non-interactive --domain example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Useful baseline flags:
|
Полезные baseline-флаги:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./install.sh --non-interactive --domain example.com --port 443 --ssh-port 22 --ui-port 8080
|
./install.sh --non-interactive --domain example.com --port 443 --ssh-port 22 --ui-port 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
The install flow writes `/etc/hysteria/post-install.env` with the resulting package, Hysteria, port, and path state.
|
После установки создаётся `/etc/hysteria/post-install.env` с фактическими параметрами deploy-состояния.
|
||||||
|
|||||||
@@ -48,3 +48,4 @@ func Execute() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,3 +49,4 @@ func runReset(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Println(fmt.Sprintf("HY2XS admin Login Password: %s", password))
|
fmt.Println(fmt.Sprintf("HY2XS admin Login Password: %s", password))
|
||||||
fmt.Println(fmt.Sprintf("HY2XS admin Connection Password: %s", fmt.Sprintf("%s.%s", username, password)))
|
fmt.Println(fmt.Sprintf("HY2XS admin Connection Password: %s", fmt.Sprintf("%s.%s", username, password)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,3 +83,4 @@ func initFile() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ func init() {
|
|||||||
func runVersion(cmd *cobra.Command, args []string) {
|
func runVersion(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println("HY2XS admin version", constant.Version)
|
fmt.Println("HY2XS admin version", constant.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ func GetAccountInfo(c *gin.Context) {
|
|||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 更新最近登录时间
|
// Обновление времени последнего входа
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
if err = service.UpdateAccount(entity.Account{
|
if err = service.UpdateAccount(entity.Account{
|
||||||
BaseEntity: entity.BaseEntity{Id: &accountInfoVo.Id},
|
BaseEntity: entity.BaseEntity{Id: &accountInfoVo.Id},
|
||||||
@@ -255,12 +255,12 @@ func ImportAccount(c *gin.Context) {
|
|||||||
vo.Fail(constant.SysError, c)
|
vo.Fail(constant.SysError, c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 文件大小 2MB
|
// Размер файла 2 MB
|
||||||
if header.Size > 1024*1024*2 {
|
if header.Size > 1024*1024*2 {
|
||||||
vo.Fail("the file is too big", c)
|
vo.Fail("the file is too big", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 文件后缀.json
|
// Расширение файла .json
|
||||||
if !strings.HasSuffix(header.Filename, ".json") {
|
if !strings.HasSuffix(header.Filename, ".json") {
|
||||||
vo.Fail("file format not supported", c)
|
vo.Fail("file format not supported", c)
|
||||||
return
|
return
|
||||||
@@ -297,7 +297,7 @@ func ExportAccount(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载
|
// Скачивание
|
||||||
if !util.Exists(filePath) {
|
if !util.Exists(filePath) {
|
||||||
vo.Fail("file not exist", c)
|
vo.Fail("file not exist", c)
|
||||||
return
|
return
|
||||||
@@ -333,3 +333,4 @@ func VerifyDefaultPass(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
vo.Success(account.Pass != nil && *account.Pass == "02f382b76ca1ab7aa06ab03345c7712fd5b971fb0c0f2aef98bac9cd", c)
|
vo.Success(account.Pass != nil && *account.Pass == "02f382b76ca1ab7aa06ab03345c7712fd5b971fb0c0f2aef98bac9cd", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ func ExportHysteria2Config(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认值
|
// Значения по умолчанию
|
||||||
config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.JwtSecret})
|
config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.JwtSecret})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
@@ -324,7 +324,7 @@ func ImportHysteria2Config(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认值
|
// Значения по умолчанию
|
||||||
config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.JwtSecret})
|
config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.JwtSecret})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
@@ -501,3 +501,4 @@ func UploadCertFile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
vo.Success(certPath, c)
|
vo.Success(certPath, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func Hysteria2Auth(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新最近连接时间
|
// Обновление времени последнего подключения
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
if err = service.UpdateAccount(entity.Account{
|
if err = service.UpdateAccount(entity.Account{
|
||||||
BaseEntity: entity.BaseEntity{Id: &id},
|
BaseEntity: entity.BaseEntity{Id: &id},
|
||||||
@@ -69,7 +69,7 @@ func Hysteria2Url(c *gin.Context) {
|
|||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 生成二维码
|
// Генерация QR-кода
|
||||||
qrCode, err := qrcode.Encode(url, qrcode.Medium, 300)
|
qrCode, err := qrcode.Encode(url, qrcode.Medium, 300)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
@@ -150,3 +150,4 @@ func Hysteria2Subscribe(c *gin.Context) {
|
|||||||
|
|
||||||
c.String(200, configStr)
|
c.String(200, configStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,3 +114,4 @@ func ExportLog(c *gin.Context) {
|
|||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName))
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName))
|
||||||
c.File(filePath)
|
c.File(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ func MonitorHysteria2(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
vo.Success(hysteria2MonitorVo, c)
|
vo.Success(hysteria2MonitorVo, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func init() {
|
|||||||
|
|
||||||
func validateStr(f validator.FieldLevel) bool {
|
func validateStr(f validator.FieldLevel) bool {
|
||||||
field := f.Field().String()
|
field := f.Field().String()
|
||||||
// 字符串必须6-32位是字母或者数字或部分特殊字符的组合
|
// Строка должна быть длиной 6-32 символа и состоять из букв, цифр или разрешённых спецсимволов
|
||||||
reg := "^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$"
|
reg := "^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$"
|
||||||
compile := regexp.MustCompile(reg)
|
compile := regexp.MustCompile(reg)
|
||||||
return field == "" || compile.MatchString(field)
|
return field == "" || compile.MatchString(field)
|
||||||
@@ -39,3 +39,4 @@ func validateField[T interface{}](c *gin.Context, field T) (T, error) {
|
|||||||
}
|
}
|
||||||
return field, nil
|
return field, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,3 +117,4 @@ func ListAccount(query interface{}, args ...interface{}) ([]entity.Account, erro
|
|||||||
}
|
}
|
||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,3 +64,4 @@ func UpsertConfig(configs []entity.Config) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,3 +117,4 @@ func Paginate(pageNum *int64, pageSize *int64) func(db *gorm.DB) *gorm.DB {
|
|||||||
return db.Offset(int((num - 1) * size)).Limit(int(size))
|
return db.Offset(int((num - 1) * size)).Limit(int(size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# http://editorconfig.org
|
# http://editorconfig.org
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
# 表示所有文件适用
|
# Applies to all files
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8 # 设置文件字符集为 utf-8
|
charset = utf-8 # File charset
|
||||||
end_of_line = lf # 控制换行类型(lf | cr | crlf)
|
end_of_line = lf # Line ending type: lf | cr | crlf
|
||||||
indent_style = tab # 缩进风格(tab | space)
|
indent_style = tab # Indentation style: tab | space
|
||||||
insert_final_newline = true # 始终在文件末尾插入一个新行
|
insert_final_newline = true # Always insert a final newline
|
||||||
|
|
||||||
# 表示仅 md 文件适用以下规则
|
# Applies only to md files
|
||||||
[*.md]
|
[*.md]
|
||||||
max_line_length = off # 关闭最大行长度限制
|
max_line_length = off # Disable max line length limit
|
||||||
trim_trailing_whitespace = false # 关闭末尾空格修剪
|
trim_trailing_whitespace = false # Disable trailing whitespace trimming
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## 开发环境
|
## Development environment
|
||||||
|
|
||||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
# Variables must use the VITE_ prefix to be exposed externally
|
||||||
NODE_ENV='development'
|
NODE_ENV='development'
|
||||||
|
|
||||||
VITE_APP_TITLE = 'h-ui'
|
VITE_APP_TITLE = 'h-ui'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## 生产环境
|
## Production environment
|
||||||
|
|
||||||
VITE_APP_TITLE = 'h-ui'
|
VITE_APP_TITLE = 'h-ui'
|
||||||
VITE_APP_PORT = 80
|
VITE_APP_PORT = 80
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
plugins: ["vue", "@typescript-eslint"],
|
plugins: ["vue", "@typescript-eslint"],
|
||||||
rules: {
|
rules: {
|
||||||
"vue/multi-word-component-names": "off", // 关闭组件名必须多字: https://eslint.vuejs.org/rules/multi-word-component-names.html
|
"vue/multi-word-component-names": "off", // Disable multi-word component name requirement: https://eslint.vuejs.org/rules/multi-word-component-names.html
|
||||||
"@typescript-eslint/no-empty-function": "off", // 关闭空方法检查
|
"@typescript-eslint/no-empty-function": "off", // Disable empty function checks
|
||||||
"@typescript-eslint/no-explicit-any": "off", // 关闭any类型的警告
|
"@typescript-eslint/no-explicit-any": "off", // Disable warnings for any
|
||||||
"vue/no-v-model-argument": "off",
|
"vue/no-v-model-argument": "off",
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
}, // https://eslint.org/docs/latest/use/configure/language-options#specifying-globals
|
}, // https://eslint.org/docs/latest/use/configure/language-options#specifying-globals
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
// (x)=>{},单个参数箭头函数是否显示小括号。(always:始终显示;avoid:省略括号。默认:always)
|
// Parentheses around a single arrow function parameter. Default: always.
|
||||||
arrowParens: "always",
|
arrowParens: "always",
|
||||||
// 开始标签的右尖括号是否跟随在最后一行属性末尾,默认false
|
// Put the closing bracket of a multi-line element at the end of the last line of attributes.
|
||||||
bracketSameLine: false,
|
bracketSameLine: false,
|
||||||
// 对象字面量的括号之间打印空格 (true - Example: { foo: bar } ; false - Example: {foo:bar})
|
// Print spaces between brackets in object literals.
|
||||||
bracketSpacing: true,
|
bracketSpacing: true,
|
||||||
// 是否格式化一些文件中被嵌入的代码片段的风格(auto|off;默认auto)
|
// Format embedded code snippets.
|
||||||
embeddedLanguageFormatting: "auto",
|
embeddedLanguageFormatting: "auto",
|
||||||
// 指定 HTML 文件的空格敏感度 (css|strict|ignore;默认css)
|
// HTML whitespace sensitivity.
|
||||||
htmlWhitespaceSensitivity: "css",
|
htmlWhitespaceSensitivity: "css",
|
||||||
// 当文件已经被 Prettier 格式化之后,是否会在文件顶部插入一个特殊的 @format 标记,默认false
|
// Insert a special @format marker at the top of formatted files.
|
||||||
insertPragma: false,
|
insertPragma: false,
|
||||||
// 在 JSX 中使用单引号替代双引号,默认false
|
// Use single quotes in JSX.
|
||||||
jsxSingleQuote: false,
|
jsxSingleQuote: false,
|
||||||
// 每行最多字符数量,超出换行(默认80)
|
// Maximum line length.
|
||||||
printWidth: 80,
|
printWidth: 80,
|
||||||
// 超出打印宽度 (always | never | preserve )
|
// Markdown prose wrapping.
|
||||||
proseWrap: "preserve",
|
proseWrap: "preserve",
|
||||||
// 对象属性是否使用引号(as-needed | consistent | preserve;默认as-needed:对象的属性需要加引号才添加;)
|
// Object property quote policy.
|
||||||
quoteProps: "as-needed",
|
quoteProps: "as-needed",
|
||||||
// 是否只格式化在文件顶部包含特定注释(@prettier| @format)的文件,默认false
|
// Format only files with @prettier or @format pragma.
|
||||||
requirePragma: false,
|
requirePragma: false,
|
||||||
// 结尾添加分号
|
// Add semicolons.
|
||||||
semi: true,
|
semi: true,
|
||||||
// 使用单引号 (true:单引号;false:双引号)
|
// Use single quotes.
|
||||||
singleQuote: false,
|
singleQuote: false,
|
||||||
// 缩进空格数,默认2个空格
|
// Indentation width.
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
// 元素末尾是否加逗号,默认es5: ES5中的 objects, arrays 等会添加逗号,TypeScript 中的 type 后不加逗号
|
// Trailing commas.
|
||||||
trailingComma: "es5",
|
trailingComma: "es5",
|
||||||
// 指定缩进方式,空格或tab,默认false,即使用空格
|
// Use tabs for indentation.
|
||||||
useTabs: false,
|
useTabs: false,
|
||||||
// vue 文件中是否缩进 <style> 和 <script> 标签,默认 false
|
// Indent <style> and <script> tags in Vue files.
|
||||||
vueIndentScriptAndStyle: false,
|
vueIndentScriptAndStyle: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
// 继承推荐规范配置
|
// Extend recommended configs
|
||||||
extends: [
|
extends: [
|
||||||
"stylelint-config-standard",
|
"stylelint-config-standard",
|
||||||
"stylelint-config-recommended-scss",
|
"stylelint-config-recommended-scss",
|
||||||
@@ -7,7 +7,7 @@ module.exports = {
|
|||||||
"stylelint-config-html/vue",
|
"stylelint-config-html/vue",
|
||||||
"stylelint-config-recess-order",
|
"stylelint-config-recess-order",
|
||||||
],
|
],
|
||||||
// 指定不同文件对应的解析器
|
// Parsers for different file types
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ["**/*.{vue,html}"],
|
files: ["**/*.{vue,html}"],
|
||||||
@@ -18,21 +18,21 @@ module.exports = {
|
|||||||
customSyntax: "postcss-scss",
|
customSyntax: "postcss-scss",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// 自定义规则
|
// Custom rules
|
||||||
rules: {
|
rules: {
|
||||||
"import-notation": "string", // 指定导入CSS文件的方式("string"|"url")
|
"import-notation": "string", // CSS import notation: "string" | "url"
|
||||||
"selector-class-pattern": null, // 选择器类名命名规则
|
"selector-class-pattern": null, // Selector class naming rule
|
||||||
"custom-property-pattern": null, // 自定义属性命名规则
|
"custom-property-pattern": null, // Custom property naming rule
|
||||||
"keyframes-name-pattern": null, // 动画帧节点样式命名规则
|
"keyframes-name-pattern": null, // Keyframe naming rule
|
||||||
"no-descending-specificity": null, // 允许无降序特异性
|
"no-descending-specificity": null, // Allow non-descending specificity
|
||||||
// 允许 global 、export 、deep伪类
|
// Allow global, export and deep pseudo classes
|
||||||
"selector-pseudo-class-no-unknown": [
|
"selector-pseudo-class-no-unknown": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
ignorePseudoClasses: ["global", "export", "deep"],
|
ignorePseudoClasses: ["global", "export", "deep"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// 允许未知属性
|
// Allow unknown properties
|
||||||
"property-no-unknown": [
|
"property-no-unknown": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
|
|||||||
+22
-2
@@ -1,3 +1,23 @@
|
|||||||
# Frontend
|
# HY2XS admin frontend
|
||||||
|
|
||||||
HY2XS admin frontend
|
Frontend-часть панели HY2XS admin.
|
||||||
|
|
||||||
|
## Языки интерфейса
|
||||||
|
|
||||||
|
Поддерживаемые языки:
|
||||||
|
|
||||||
|
- русский;
|
||||||
|
- английский.
|
||||||
|
|
||||||
|
Язык по умолчанию — русский.
|
||||||
|
|
||||||
|
## Production-сборка
|
||||||
|
|
||||||
|
Frontend собирается production builder'ом из [`tools/build`](../../tools/build). На target server frontend не собирается.
|
||||||
|
|
||||||
|
Локальная проверка:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm run build:prod
|
||||||
|
```
|
||||||
|
|||||||
@@ -73,3 +73,4 @@ func replaceRelativePaths(htmlContent string, basePath string) string {
|
|||||||
</script>`, basePath)
|
</script>`, basePath)
|
||||||
return strings.Replace(htmlContent, "</head>", injection+"</head>", 1)
|
return strings.Replace(htmlContent, "</head>", injection+"</head>", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ const appStore = useAppStore();
|
|||||||
<router-view />
|
<router-view />
|
||||||
</el-config-provider>
|
</el-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询
|
* Поиск
|
||||||
*/
|
*/
|
||||||
export function getAccountApi(data: IdDto): AxiosPromise<AccountVo> {
|
export function getAccountApi(data: IdDto): AxiosPromise<AccountVo> {
|
||||||
return request({
|
return request({
|
||||||
@@ -22,7 +22,7 @@ export function getAccountApi(data: IdDto): AxiosPromise<AccountVo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存
|
* Сохранение
|
||||||
*
|
*
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
@@ -35,7 +35,7 @@ export function saveAccountApi(data: AccountSaveDto): AxiosPromise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询当前
|
* Запрос текущего пользователя
|
||||||
*/
|
*/
|
||||||
export function getAccountInfoApi(): AxiosPromise<AccountInfo> {
|
export function getAccountInfoApi(): AxiosPromise<AccountInfo> {
|
||||||
return request({
|
return request({
|
||||||
@@ -45,7 +45,7 @@ export function getAccountInfoApi(): AxiosPromise<AccountInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页
|
* Пагинация
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
export function pageAccountApi(
|
export function pageAccountApi(
|
||||||
@@ -59,7 +59,7 @@ export function pageAccountApi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除
|
* Удаление
|
||||||
*
|
*
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
@@ -72,7 +72,7 @@ export function deleteAccountApi(data: IdDto): AxiosPromise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改
|
* Изменение
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
export function updateAccountApi(data: AccountUpdateDto): AxiosPromise {
|
export function updateAccountApi(data: AccountUpdateDto): AxiosPromise {
|
||||||
@@ -84,7 +84,7 @@ export function updateAccountApi(data: AccountUpdateDto): AxiosPromise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重设流量
|
* Сброс трафика
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
export function resetTrafficApi(data: IdDto): AxiosPromise {
|
export function resetTrafficApi(data: IdDto): AxiosPromise {
|
||||||
@@ -96,7 +96,7 @@ export function resetTrafficApi(data: IdDto): AxiosPromise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录
|
* Вход
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
export function loginApi(data: AccountLoginDto): AxiosPromise<AccountLoginVo> {
|
export function loginApi(data: AccountLoginDto): AxiosPromise<AccountLoginVo> {
|
||||||
@@ -108,7 +108,7 @@ export function loginApi(data: AccountLoginDto): AxiosPromise<AccountLoginVo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入
|
* Импорт
|
||||||
*/
|
*/
|
||||||
export function importAccountApi(data: FormData): AxiosPromise {
|
export function importAccountApi(data: FormData): AxiosPromise {
|
||||||
return request({
|
return request({
|
||||||
@@ -122,7 +122,7 @@ export function importAccountApi(data: FormData): AxiosPromise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出
|
* Экспорт
|
||||||
*/
|
*/
|
||||||
export function exportAccountApi(): AxiosPromise {
|
export function exportAccountApi(): AxiosPromise {
|
||||||
return request({
|
return request({
|
||||||
@@ -133,7 +133,7 @@ export function exportAccountApi(): AxiosPromise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解除下线状态
|
* Снятие статуса отключения
|
||||||
*/
|
*/
|
||||||
export function releaseKickAccountApi(data: IdDto): AxiosPromise {
|
export function releaseKickAccountApi(data: IdDto): AxiosPromise {
|
||||||
return request({
|
return request({
|
||||||
@@ -144,7 +144,7 @@ export function releaseKickAccountApi(data: IdDto): AxiosPromise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否默认密码
|
* Проверка пароля по умолчанию
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
export function verifyDefaultPassApi(): AxiosPromise {
|
export function verifyDefaultPassApi(): AxiosPromise {
|
||||||
@@ -153,3 +153,5 @@ export function verifyDefaultPassApi(): AxiosPromise {
|
|||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -77,3 +77,5 @@ export interface KickAccountForm {
|
|||||||
ids: number[];
|
ids: number[];
|
||||||
kickUtilTime: number;
|
kickUtilTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -110,3 +110,5 @@ export function uploadCertFileApi(data: FormData): AxiosPromise<string> {
|
|||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -300,3 +300,5 @@ export interface Hysteria2AcmePathVo {
|
|||||||
crtPath: string;
|
crtPath: string;
|
||||||
keyPath: string;
|
keyPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,3 +38,5 @@ export function hysteria2UrlApi(
|
|||||||
params: dto,
|
params: dto,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,3 +24,5 @@ export interface Hysteria2UrlVo {
|
|||||||
url: string;
|
url: string;
|
||||||
qrCode: string;
|
qrCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,3 +33,5 @@ export function exportLogApi(data: LogExportDto): AxiosPromise {
|
|||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,3 +22,5 @@ export interface LogHysteria2Vo {
|
|||||||
msg: string;
|
msg: string;
|
||||||
time: string;
|
time: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,5 @@ export function monitorHysteria2Api(): AxiosPromise<Hysteria2MonitorVo> {
|
|||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export interface Hysteria2MonitorVo {
|
|||||||
version: string;
|
version: string;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,3 +44,5 @@ function toggleClick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -75,3 +75,5 @@ const handleClose = (tag: string): void => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,3 +34,5 @@ function handleLanguageChange(lang: string) {
|
|||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -178,3 +178,5 @@ const closeDialog = (): void => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,3 +86,5 @@ function handleCurrentChange(val: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function addEventClick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeSidebar(evt: any) {
|
function closeSidebar(evt: any) {
|
||||||
// 主题选择点击不关闭
|
// Клик по выбору темы не закрывает панель
|
||||||
let parent = evt.target.closest(".right-panel-container");
|
let parent = evt.target.closest(".right-panel-container");
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
show.value = false;
|
show.value = false;
|
||||||
@@ -134,3 +134,5 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { useAppStore } from "@/store/modules/app";
|
|||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const sizeOptions = ref([
|
const sizeOptions = ref([
|
||||||
{ label: "默认", value: "default" },
|
{ label: "Обычный", value: "default" },
|
||||||
{ label: "大型", value: "large" },
|
{ label: "Крупный", value: "large" },
|
||||||
{ label: "小型", value: "small" },
|
{ label: "Компактный", value: "small" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function handleSizeChange(size: string) {
|
function handleSizeChange(size: string) {
|
||||||
appStore.changeSize(size);
|
appStore.changeSize(size);
|
||||||
ElMessage.success("切换布局大小成功");
|
ElMessage.success("Размер интерфейса изменён");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -34,3 +34,5 @@ function handleSizeChange(size: string) {
|
|||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
|
|||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
|
vertical-align: -0.15em; /* Так как размер icon равен размеру шрифта, добавляем смещение для визуального выравнивания */
|
||||||
outline: none;
|
outline: none;
|
||||||
fill: currentcolor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
|
fill: currentcolor; /* currentColor наследует текущий цвет элемента или родителя */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,3 +72,5 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import type { App } from "vue";
|
|||||||
|
|
||||||
import { hasRole } from "./permission";
|
import { hasRole } from "./permission";
|
||||||
|
|
||||||
// 全局注册 directive
|
// Глобальная регистрация directive
|
||||||
export function setupDirective(app: App<Element>) {
|
export function setupDirective(app: App<Element>) {
|
||||||
// 使 v-hasRole 在所有组件中都可用
|
// Делает v-hasRole доступным во всех компонентах
|
||||||
app.directive("hasRole", hasRole);
|
app.directive("hasRole", hasRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { useAccountStoreHook } from "@/store/modules/account";
|
|||||||
import { Directive, DirectiveBinding } from "vue";
|
import { Directive, DirectiveBinding } from "vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 角色权限
|
* Ролевые права
|
||||||
*/
|
*/
|
||||||
export const hasRole: Directive = {
|
export const hasRole: Directive = {
|
||||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
const { value } = binding;
|
const { value } = binding;
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
const requiredRoles = value; // DOM绑定需要的角色编码
|
const requiredRoles = value; // Коды ролей, требуемые DOM-привязкой
|
||||||
const { roles } = useAccountStoreHook();
|
const { roles } = useAccountStoreHook();
|
||||||
const hasRole = roles.some((perm) => {
|
const hasRole = roles.some((perm) => {
|
||||||
return requiredRoles.includes(perm);
|
return requiredRoles.includes(perm);
|
||||||
@@ -23,3 +23,5 @@ export const hasRole: Directive = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,3 +22,5 @@ const i18n = createI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
// 路由国际化
|
// МаршрутЛокализация
|
||||||
route: {
|
route: {
|
||||||
account: "Account",
|
account: "Account",
|
||||||
accountList: "Account Manage",
|
accountList: "Account Manage",
|
||||||
@@ -15,14 +15,14 @@ export default {
|
|||||||
info: "Info",
|
info: "Info",
|
||||||
infoAccount: "Account Info",
|
infoAccount: "Account Info",
|
||||||
},
|
},
|
||||||
// 登录页面国际化
|
// Локализация страницы входа
|
||||||
login: {
|
login: {
|
||||||
title: "HY2XS admin",
|
title: "HY2XS admin",
|
||||||
username: "Username",
|
username: "Username",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
login: "Login",
|
login: "Login",
|
||||||
},
|
},
|
||||||
// 导航栏国际化
|
// Локализация навигационной панели
|
||||||
navbar: {
|
navbar: {
|
||||||
logout: "Logout",
|
logout: "Logout",
|
||||||
},
|
},
|
||||||
@@ -322,3 +322,5 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -256,3 +256,5 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,312 +0,0 @@
|
|||||||
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。",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -43,3 +43,5 @@ const tagsViewStore = useTagsViewStore();
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,27 +12,27 @@ const accountStore = useAccountStore();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { device } = storeToRefs(appStore); // 设备类型:desktop-宽屏设备 || mobile-窄屏设备
|
const { device } = storeToRefs(appStore); // Тип устройства: desktop — широкий экран, mobile — узкий экран
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 左侧菜单栏显示/隐藏
|
* Показать или скрыть левое меню.
|
||||||
*/
|
*/
|
||||||
function toggleSideBar() {
|
function toggleSideBar() {
|
||||||
appStore.toggleSidebar(true);
|
appStore.toggleSidebar(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vueUse 全屏
|
* Полноэкранный режим через VueUse.
|
||||||
*/
|
*/
|
||||||
const { isFullscreen, toggle } = useFullscreen();
|
const { isFullscreen, toggle } = useFullscreen();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注销
|
* Выход из системы.
|
||||||
*/
|
*/
|
||||||
function logout() {
|
function logout() {
|
||||||
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
|
ElMessageBox.confirm("Выйти из системы?", "Подтверждение", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "Выйти",
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: "Отмена",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
accountStore
|
accountStore
|
||||||
@@ -48,9 +48,9 @@ function logout() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 顶部导航栏 -->
|
<!-- Верхняя навигационная панель -->
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<!-- 左侧面包屑 -->
|
<!-- Хлебные крошки слева -->
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<hamburger
|
<hamburger
|
||||||
:is-active="appStore.sidebar.opened"
|
:is-active="appStore.sidebar.opened"
|
||||||
@@ -59,25 +59,25 @@ function logout() {
|
|||||||
<breadcrumb />
|
<breadcrumb />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧导航设置 -->
|
<!-- Настройки навигации справа -->
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<!-- 导航栏设置(窄屏隐藏)-->
|
<!-- Настройки навигации, скрытые на узком экране -->
|
||||||
<div class="setting-container" v-if="device !== 'mobile'">
|
<div class="setting-container" v-if="device !== 'mobile'">
|
||||||
<!--全屏 -->
|
<!-- Полноэкранный режим -->
|
||||||
<div class="setting-item" @click="toggle">
|
<div class="setting-item" @click="toggle">
|
||||||
<svg-icon
|
<svg-icon
|
||||||
:icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
|
:icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 布局大小 -->
|
<!-- Размер интерфейса -->
|
||||||
<el-tooltip content="布局大小" effect="dark" placement="bottom">
|
<el-tooltip content="Размер интерфейса" effect="dark" placement="bottom">
|
||||||
<size-select class="setting-item" />
|
<size-select class="setting-item" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<!--语言选择-->
|
<!-- Выбор языка -->
|
||||||
<lang-select class="setting-item" />
|
<lang-select class="setting-item" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 用户头像 -->
|
<!-- Аватар пользователя -->
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<img src="/src/assets/logo.png" />
|
<img src="/src/assets/logo.png" />
|
||||||
@@ -138,3 +138,5 @@ function logout() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ import IconEpSunny from "~icons/ep/sunny";
|
|||||||
import IconEpMoon from "~icons/ep/moon";
|
import IconEpMoon from "~icons/ep/moon";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 暗黑模式
|
* Тёмная тема.
|
||||||
*/
|
*/
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const isDark = useDark();
|
const isDark = useDark();
|
||||||
const toggleDark = () => useToggle(isDark);
|
const toggleDark = () => useToggle(isDark);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换布局
|
* Переключение layout.
|
||||||
*/
|
*/
|
||||||
function changeLayout(layout: string) {
|
function changeLayout(layout: string) {
|
||||||
settingsStore.changeSetting({ key: "layout", value: layout });
|
settingsStore.changeSetting({ key: "layout", value: layout });
|
||||||
window.document.body.setAttribute("layout", settingsStore.layout);
|
window.document.body.setAttribute("layout", settingsStore.layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主题颜色
|
// Цвета темы
|
||||||
const themeColors = ref<string[]>([
|
const themeColors = ref<string[]>([
|
||||||
"#409EFF",
|
"#409EFF",
|
||||||
"#304156",
|
"#304156",
|
||||||
@@ -30,7 +30,7 @@ const themeColors = ref<string[]>([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换主题颜色
|
* Переключение цвета темы.
|
||||||
*/
|
*/
|
||||||
function changeThemeColor(color: string) {
|
function changeThemeColor(color: string) {
|
||||||
settingsStore.changeSetting({ key: "themeColor", value: color });
|
settingsStore.changeSetting({ key: "themeColor", value: color });
|
||||||
@@ -51,8 +51,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
<h3 class="text-base font-bold">项目配置</h3>
|
<h3 class="text-base font-bold">Настройки проекта</h3>
|
||||||
<el-divider>主题</el-divider>
|
<el-divider>Тема</el-divider>
|
||||||
|
|
||||||
<div class="flex justify-center" @click.stop>
|
<div class="flex justify-center" @click.stop>
|
||||||
<el-switch
|
<el-switch
|
||||||
@@ -66,23 +66,23 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-divider>界面设置</el-divider>
|
<el-divider>Настройки интерфейса</el-divider>
|
||||||
<div class="py-[8px] flex justify-between">
|
<div class="py-[8px] flex justify-between">
|
||||||
<span class="text-xs">开启 Tags-View</span>
|
<span class="text-xs">Включить вкладки</span>
|
||||||
<el-switch v-model="settingsStore.tagsView" />
|
<el-switch v-model="settingsStore.tagsView" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-[8px] flex justify-between">
|
<div class="py-[8px] flex justify-between">
|
||||||
<span class="text-xs">固定 Header</span>
|
<span class="text-xs">Зафиксировать header</span>
|
||||||
<el-switch v-model="settingsStore.fixedHeader" />
|
<el-switch v-model="settingsStore.fixedHeader" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-[8px] flex justify-between">
|
<div class="py-[8px] flex justify-between">
|
||||||
<span class="text-xs">侧边栏 Logo</span>
|
<span class="text-xs">Логотип в боковом меню</span>
|
||||||
<el-switch v-model="settingsStore.sidebarLogo" />
|
<el-switch v-model="settingsStore.sidebarLogo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-divider>主题颜色</el-divider>
|
<el-divider>Цвет темы</el-divider>
|
||||||
|
|
||||||
<ul class="w-full space-x-2 flex justify-center py-2">
|
<ul class="w-full space-x-2 flex justify-center py-2">
|
||||||
<li
|
<li
|
||||||
@@ -163,3 +163,5 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,3 +35,5 @@ function push() {
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,5 @@ const logo = ref(new URL(`../../../assets/logo.png`, import.meta.url).href);
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import SvgIcon from "@/components/SvgIcon/index.vue";
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/**
|
/**
|
||||||
* 路由(eg:level_3_1)
|
* Route, например level_3_1
|
||||||
*/
|
*/
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -16,7 +16,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 父层级完整路由路径(eg:/level/level_3/level_3_1)
|
* Полный path родительского уровня, например /level/level_3/level_3_1
|
||||||
*/
|
*/
|
||||||
basePath: {
|
basePath: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -24,34 +24,34 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onlyOneChild = ref(); // 临时变量,唯一子路由
|
const onlyOneChild = ref(); // Временная переменная для единственного дочернего route
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断当前路由是否只有一个子路由
|
* Проверяет, есть ли у текущего route только один дочерний route
|
||||||
*
|
*
|
||||||
* 1:如果只有一个子路由: 返回 true
|
* 1: если дочерний route один, возвращает true
|
||||||
* 2:如果无子路由 :返回 true
|
* 2: если дочерних routes нет, возвращает true
|
||||||
*
|
*
|
||||||
* @param children 子路由数组
|
* @param children Массив дочерних routes
|
||||||
* @param parent 当前路由
|
* @param parent Текущий route
|
||||||
*/
|
*/
|
||||||
function hasOneShowingChild(children = [], parent: any) {
|
function hasOneShowingChild(children = [], parent: any) {
|
||||||
// 需要显示的子路由数组
|
// Массив дочерних routes для отображения
|
||||||
const showingChildren = children.filter((item: any) => {
|
const showingChildren = children.filter((item: any) => {
|
||||||
if (item.meta?.hidden) {
|
if (item.meta?.hidden) {
|
||||||
return false; // 过滤不显示的子路由
|
return false; // Фильтрация скрытого дочернего route
|
||||||
} else {
|
} else {
|
||||||
onlyOneChild.value = item; // 唯一子路由赋值(多个子路由情况 onlyOneChild 变量是用不上的)
|
onlyOneChild.value = item; // Запись единственного дочернего route, при нескольких routes переменная onlyOneChild не используется
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1:如果只有一个子路由, 返回 true
|
// 1: если дочерний route один, возвращает true
|
||||||
if (showingChildren.length === 1) {
|
if (showingChildren.length === 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2:如果无子路由, 复制当前路由信息作为其子路由,满足只拥有一个子路由的条件,所以返回 true
|
// 2: если дочерних routes нет, копирует текущий route как дочерний и возвращает true
|
||||||
if (showingChildren.length === 0) {
|
if (showingChildren.length === 0) {
|
||||||
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
|
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
|
||||||
return true;
|
return true;
|
||||||
@@ -60,9 +60,9 @@ function hasOneShowingChild(children = [], parent: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析路径
|
* Разбор path
|
||||||
*
|
*
|
||||||
* @param routePath 路由路径
|
* @param routePath Route path
|
||||||
*/
|
*/
|
||||||
function resolvePath(routePath: string) {
|
function resolvePath(routePath: string) {
|
||||||
if (isExternal(routePath)) {
|
if (isExternal(routePath)) {
|
||||||
@@ -71,14 +71,14 @@ function resolvePath(routePath: string) {
|
|||||||
if (isExternal(props.basePath)) {
|
if (isExternal(props.basePath)) {
|
||||||
return props.basePath;
|
return props.basePath;
|
||||||
}
|
}
|
||||||
// 完整路径 = 父级路径(/level/level_3) + 路由路径
|
// Полный path = parent path (/level/level_3) + route path
|
||||||
const fullPath = path.resolve(props.basePath, routePath); // 相对路径 → 绝对路径
|
const fullPath = path.resolve(props.basePath, routePath); // Relative path → absolute path
|
||||||
return fullPath;
|
return fullPath;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!item.meta || !item.meta.hidden">
|
<div v-if="!item.meta || !item.meta.hidden">
|
||||||
<!-- 只包含一个子路由节点的路由,显示其【唯一子路由】 -->
|
<!-- Route с одним дочерним route показывает этот единственный дочерний route -->
|
||||||
<template
|
<template
|
||||||
v-if="
|
v-if="
|
||||||
hasOneShowingChild(item.children, item) &&
|
hasOneShowingChild(item.children, item) &&
|
||||||
@@ -98,7 +98,7 @@ function resolvePath(routePath: string) {
|
|||||||
</app-link>
|
</app-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 包含多个子路由 -->
|
<!-- Содержит несколько дочерних routes -->
|
||||||
<el-sub-menu v-else :index="resolvePath(item.path)" teleported>
|
<el-sub-menu v-else :index="resolvePath(item.path)" teleported>
|
||||||
<template #title>
|
<template #title>
|
||||||
<svg-icon
|
<svg-icon
|
||||||
@@ -119,3 +119,5 @@ function resolvePath(routePath: string) {
|
|||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,3 +43,5 @@ const route = useRoute();
|
|||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -119,3 +119,5 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ watch(
|
|||||||
moveToCurrentTag();
|
moveToCurrentTag();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
//初始化立即执行
|
// Выполнить сразу при инициализации
|
||||||
immediate: true,
|
immediate: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const tagMenuVisible = ref(false); // 标签操作菜单显示状态
|
const tagMenuVisible = ref(false); // Состояние отображения меню действий с вкладкой
|
||||||
watch(tagMenuVisible, (value) => {
|
watch(tagMenuVisible, (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
document.body.addEventListener("click", closeTagMenu);
|
document.body.addEventListener("click", closeTagMenu);
|
||||||
@@ -264,7 +264,7 @@ onMounted(() => {
|
|||||||
</router-link>
|
</router-link>
|
||||||
</scroll-pane>
|
</scroll-pane>
|
||||||
|
|
||||||
<!-- tag标签操作菜单 -->
|
<!-- Меню действий с вкладкой -->
|
||||||
<ul
|
<ul
|
||||||
v-show="tagMenuVisible"
|
v-show="tagMenuVisible"
|
||||||
class="tag-menu"
|
class="tag-menu"
|
||||||
@@ -272,27 +272,27 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<li @click="refreshSelectedTag(selectedTag)">
|
<li @click="refreshSelectedTag(selectedTag)">
|
||||||
<svg-icon icon-class="refresh" />
|
<svg-icon icon-class="refresh" />
|
||||||
刷新
|
Обновить
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||||
<svg-icon icon-class="close" />
|
<svg-icon icon-class="close" />
|
||||||
关闭
|
Закрыть
|
||||||
</li>
|
</li>
|
||||||
<li @click="closeOtherTags">
|
<li @click="closeOtherTags">
|
||||||
<svg-icon icon-class="close_other" />
|
<svg-icon icon-class="close_other" />
|
||||||
关闭其它
|
Закрыть остальные
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!isFirstView()" @click="closeLeftTags">
|
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||||
<svg-icon icon-class="close_left" />
|
<svg-icon icon-class="close_left" />
|
||||||
关闭左侧
|
Закрыть слева
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!isLastView()" @click="closeRightTags">
|
<li v-if="!isLastView()" @click="closeRightTags">
|
||||||
<svg-icon icon-class="close_right" />
|
<svg-icon icon-class="close_right" />
|
||||||
关闭右侧
|
Закрыть справа
|
||||||
</li>
|
</li>
|
||||||
<li @click="closeAllTags(selectedTag)">
|
<li @click="closeAllTags(selectedTag)">
|
||||||
<svg-icon icon-class="close_all" />
|
<svg-icon icon-class="close_all" />
|
||||||
关闭所有
|
Закрыть все
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -371,3 +371,5 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,5 @@ export { default as Navbar } from "./Navbar.vue";
|
|||||||
export { default as AppMain } from "./AppMain.vue";
|
export { default as AppMain } from "./AppMain.vue";
|
||||||
export { default as Settings } from "./Settings/index.vue";
|
export { default as Settings } from "./Settings/index.vue";
|
||||||
export { default as TagsView } from "./TagsView/index.vue";
|
export { default as TagsView } from "./TagsView/index.vue";
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import { useSettingsStore } from "@/store/modules/settings";
|
|||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 响应式布局容器固定宽度
|
* Фиксированная ширина контейнера адаптивного layout
|
||||||
*
|
*
|
||||||
* 大屏(>=1200px)
|
* Большой экран(>=1200px)
|
||||||
* 中屏(>=992px)
|
* Средний экран(>=992px)
|
||||||
* 小屏(>=768px)
|
* Малый экран(>=768px)
|
||||||
*/
|
*/
|
||||||
const WIDTH = 992;
|
const WIDTH = 992;
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ watchEffect(() => {
|
|||||||
appStore.toggleDevice("desktop");
|
appStore.toggleDevice("desktop");
|
||||||
|
|
||||||
if (width.value >= 1200) {
|
if (width.value >= 1200) {
|
||||||
//大屏
|
//Большой экран
|
||||||
appStore.openSideBar(true);
|
appStore.openSideBar(true);
|
||||||
} else {
|
} else {
|
||||||
appStore.closeSideBar(true);
|
appStore.closeSideBar(true);
|
||||||
@@ -56,7 +56,7 @@ function handleOutsideClick() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="classObj" class="app-wrapper">
|
<div :class="classObj" class="app-wrapper">
|
||||||
<!-- 手机设备侧边栏打开遮罩层 -->
|
<!-- Overlay открытого бокового меню на мобильных устройствах -->
|
||||||
<div
|
<div
|
||||||
v-if="classObj.mobile && classObj.openSidebar"
|
v-if="classObj.mobile && classObj.openSidebar"
|
||||||
class="drawer-bg"
|
class="drawer-bg"
|
||||||
@@ -71,10 +71,10 @@ function handleOutsideClick() {
|
|||||||
<tags-view v-if="showTagsView" />
|
<tags-view v-if="showTagsView" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--主页面-->
|
<!--Основная страница-->
|
||||||
<app-main />
|
<app-main />
|
||||||
|
|
||||||
<!-- 设置面板 -->
|
<!-- Панель настроек -->
|
||||||
<RightPanel v-if="showSettings">
|
<RightPanel v-if="showSettings">
|
||||||
<settings />
|
<settings />
|
||||||
</RightPanel>
|
</RightPanel>
|
||||||
@@ -127,3 +127,5 @@ function handleOutsideClick() {
|
|||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,21 +6,23 @@ import { setupDirective } from "@/directive";
|
|||||||
|
|
||||||
import "@/permission";
|
import "@/permission";
|
||||||
|
|
||||||
// 本地SVG图标
|
// Локальные SVG-иконки
|
||||||
import "virtual:svg-icons-register";
|
import "virtual:svg-icons-register";
|
||||||
|
|
||||||
// 国际化
|
// Локализация
|
||||||
import i18n from "@/lang/index";
|
import i18n from "@/lang/index";
|
||||||
|
|
||||||
// 样式
|
// Стили
|
||||||
import "element-plus/theme-chalk/dark/css-vars.css";
|
import "element-plus/theme-chalk/dark/css-vars.css";
|
||||||
import "@/styles/index.scss";
|
import "@/styles/index.scss";
|
||||||
import "uno.css";
|
import "uno.css";
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
// 全局注册 自定义指令(directive)
|
// Глобальная регистрация пользовательских директив
|
||||||
setupDirective(app);
|
setupDirective(app);
|
||||||
// 全局注册 状态管理(store)
|
// Глобальная регистрация store
|
||||||
setupStore(app);
|
setupStore(app);
|
||||||
|
|
||||||
app.use(router).use(i18n).mount("#app");
|
app.use(router).use(i18n).mount("#app");
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { usePermissionStoreHook } from "@/store/modules/permission";
|
|||||||
import NProgress from "nprogress";
|
import NProgress from "nprogress";
|
||||||
import "nprogress/nprogress.css";
|
import "nprogress/nprogress.css";
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false }); // 进度条
|
NProgress.configure({ showSpinner: false }); // Индикатор загрузки
|
||||||
|
|
||||||
const permissionStore = usePermissionStoreHook();
|
const permissionStore = usePermissionStoreHook();
|
||||||
|
|
||||||
// 白名单路由
|
// Разрешённые маршруты
|
||||||
const whiteList = ["/login", "/register"];
|
const whiteList = ["/login", "/register"];
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
@@ -17,14 +17,14 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
const hasToken = localStorage.getItem("accessToken");
|
const hasToken = localStorage.getItem("accessToken");
|
||||||
if (hasToken) {
|
if (hasToken) {
|
||||||
if (to.path === "/login") {
|
if (to.path === "/login") {
|
||||||
// 如果已登录,跳转首页
|
// Если пользователь уже вошёл, перейти на главную страницу
|
||||||
next({ path: "/" });
|
next({ path: "/" });
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
} else {
|
} else {
|
||||||
const AccountStore = useAccountStoreHook();
|
const AccountStore = useAccountStoreHook();
|
||||||
const hasRoles = AccountStore.roles && AccountStore.roles.length > 0;
|
const hasRoles = AccountStore.roles && AccountStore.roles.length > 0;
|
||||||
if (hasRoles) {
|
if (hasRoles) {
|
||||||
// 未匹配到任何路由,跳转404
|
// Если маршрут не найден, перейти на 404
|
||||||
if (to.matched.length === 0) {
|
if (to.matched.length === 0) {
|
||||||
from.name ? next({ name: from.name }) : next("/404");
|
from.name ? next({ name: from.name }) : next("/404");
|
||||||
} else {
|
} else {
|
||||||
@@ -39,7 +39,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
});
|
});
|
||||||
next({ ...to, replace: true });
|
next({ ...to, replace: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 移除 token 并跳转登录页
|
// Удалить token и перейти на страницу входа
|
||||||
await AccountStore.resetToken();
|
await AccountStore.resetToken();
|
||||||
next(`/login?redirect=${to.path}`);
|
next(`/login?redirect=${to.path}`);
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
@@ -47,7 +47,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 未登录可以访问白名单页面
|
// Без входа доступны только разрешённые страницы
|
||||||
if (whiteList.indexOf(to.path) !== -1) {
|
if (whiteList.indexOf(to.path) !== -1) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
@@ -60,3 +60,5 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
router.afterEach(() => {
|
router.afterEach(() => {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
|
|
||||||
export const Layout = () => import("@/layout/index.vue");
|
export const Layout = () => import("@/layout/index.vue");
|
||||||
|
|
||||||
// 静态路由
|
// Статические маршруты
|
||||||
export const constantRoutes: RouteRecordRaw[] = [
|
export const constantRoutes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: "/redirect",
|
path: "/redirect",
|
||||||
@@ -183,17 +183,17 @@ export const asyncRoutes: any[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建路由
|
* Создание маршрутизатора
|
||||||
*/
|
*/
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes: constantRoutes as RouteRecordRaw[],
|
routes: constantRoutes as RouteRecordRaw[],
|
||||||
// 刷新时,滚动条位置还原
|
// Восстановление позиции прокрутки при обновлении
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置路由
|
* Сброс маршрутов
|
||||||
*/
|
*/
|
||||||
export function resetRouter() {
|
export function resetRouter() {
|
||||||
router.replace({ path: "/login" });
|
router.replace({ path: "/login" });
|
||||||
@@ -201,3 +201,5 @@ export function resetRouter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
// 系统设置
|
// Системные настройки
|
||||||
interface DefaultSettings {
|
interface DefaultSettings {
|
||||||
/**
|
/**
|
||||||
* 系统title
|
* Заголовок системы
|
||||||
*/
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否显示设置
|
* Показывать настройки
|
||||||
*/
|
*/
|
||||||
showSettings: boolean;
|
showSettings: boolean;
|
||||||
/**
|
/**
|
||||||
* 是否显示多标签导航
|
* Показывать навигацию по вкладкам
|
||||||
*/
|
*/
|
||||||
tagsView: boolean;
|
tagsView: boolean;
|
||||||
/**
|
/**
|
||||||
*是否固定头部
|
*Фиксировать header
|
||||||
*/
|
*/
|
||||||
fixedHeader: boolean;
|
fixedHeader: boolean;
|
||||||
/**
|
/**
|
||||||
* 是否显示侧边栏Logo
|
* Показывать логотип в боковом меню
|
||||||
*/
|
*/
|
||||||
sidebarLogo: boolean;
|
sidebarLogo: boolean;
|
||||||
/**
|
/**
|
||||||
* 导航栏布局
|
* Layout навигации
|
||||||
*/
|
*/
|
||||||
layout: string;
|
layout: string;
|
||||||
/**
|
/**
|
||||||
* 主题颜色
|
* Цвет темы
|
||||||
*/
|
*/
|
||||||
themeColor: string;
|
themeColor: string;
|
||||||
/**
|
/**
|
||||||
* 主题模式
|
* Тема интерфейса
|
||||||
*/
|
*/
|
||||||
theme: string;
|
theme: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 布局大小
|
* Размер интерфейса
|
||||||
*/
|
*/
|
||||||
size: string;
|
size: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言
|
* Язык
|
||||||
*/
|
*/
|
||||||
language: string;
|
language: string;
|
||||||
}
|
}
|
||||||
@@ -54,10 +54,10 @@ const defaultSettings: DefaultSettings = {
|
|||||||
layout: "left",
|
layout: "left",
|
||||||
themeColor: "#409EFF",
|
themeColor: "#409EFF",
|
||||||
/**
|
/**
|
||||||
* 主题模式
|
* Тема интерфейса
|
||||||
*
|
*
|
||||||
* dark:暗黑模式
|
* dark:Тёмная тема
|
||||||
* light: 明亮模式
|
* light: Светлая тема
|
||||||
*/
|
*/
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
size: "default", // default |large |small
|
size: "default", // default |large |small
|
||||||
@@ -65,3 +65,5 @@ const defaultSettings: DefaultSettings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default defaultSettings;
|
export default defaultSettings;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { createPinia } from "pinia";
|
|||||||
|
|
||||||
const store = createPinia();
|
const store = createPinia();
|
||||||
|
|
||||||
// 全局注册 store
|
// Глобальная регистрация store
|
||||||
export function setupStore(app: App<Element>) {
|
export function setupStore(app: App<Element>) {
|
||||||
app.use(store);
|
app.use(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { store };
|
export { store };
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ export const useAccountStore = defineStore("account", () => {
|
|||||||
const token = useStorage("accessToken", "");
|
const token = useStorage("accessToken", "");
|
||||||
const id = ref(0);
|
const id = ref(0);
|
||||||
const username = ref("");
|
const username = ref("");
|
||||||
const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
|
const roles = ref<Array<string>>([]); // Коды ролей пользователя для проверки доступа к маршрутам
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录
|
* Вход
|
||||||
*
|
*
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@@ -34,7 +34,7 @@ export const useAccountStore = defineStore("account", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询当前
|
// Запрос текущего пользователя
|
||||||
function getAccountInfo() {
|
function getAccountInfo() {
|
||||||
return new Promise<AccountInfo>((resolve, reject) => {
|
return new Promise<AccountInfo>((resolve, reject) => {
|
||||||
getAccountInfoApi()
|
getAccountInfoApi()
|
||||||
@@ -56,7 +56,7 @@ export const useAccountStore = defineStore("account", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注销
|
// Выход
|
||||||
function logout() {
|
function logout() {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
resetRouter();
|
resetRouter();
|
||||||
@@ -65,7 +65,7 @@ export const useAccountStore = defineStore("account", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置
|
// Сброс
|
||||||
function resetToken() {
|
function resetToken() {
|
||||||
token.value = "";
|
token.value = "";
|
||||||
id.value = 0;
|
id.value = 0;
|
||||||
@@ -85,7 +85,9 @@ export const useAccountStore = defineStore("account", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 非setup
|
// Вне setup
|
||||||
export function useAccountStoreHook() {
|
export function useAccountStoreHook() {
|
||||||
return useAccountStore(store);
|
return useAccountStore(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据语言标识读取对应的语言包
|
* Загрузка пакета локализации по коду языка
|
||||||
*/
|
*/
|
||||||
const locale = computed(() => {
|
const locale = computed(() => {
|
||||||
return language?.value == "en" ? en : ru;
|
return language?.value == "en" ? en : ru;
|
||||||
@@ -56,7 +56,7 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
size.value = val;
|
size.value = val;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 切换语言
|
* Переключение языка
|
||||||
*
|
*
|
||||||
* @param val
|
* @param val
|
||||||
*/
|
*/
|
||||||
@@ -78,3 +78,5 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
openSideBar,
|
openSideBar,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ const Layout = () => import("@/layout/index.vue");
|
|||||||
/**
|
/**
|
||||||
* Use meta.role to determine if the current user has permission
|
* Use meta.role to determine if the current user has permission
|
||||||
*
|
*
|
||||||
* @param roles 用户角色集合
|
* @param roles Набор ролей пользователя
|
||||||
* @param route 路由
|
* @param route Маршрут
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
|
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
|
||||||
if (route.meta && route.meta.roles) {
|
if (route.meta && route.meta.roles) {
|
||||||
// 角色【超级管理员】拥有所有权限,忽略校验
|
// Роль admin имеет все права, проверка пропускается
|
||||||
if (roles.includes("admin")) {
|
if (roles.includes("admin")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -29,19 +29,19 @@ const hasPermission = (roles: string[], route: RouteRecordRaw) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 递归过滤有权限的异步(动态)路由
|
* Рекурсивная фильтрация доступных async/dynamic routes
|
||||||
*
|
*
|
||||||
* @param routes 接口返回的异步(动态)路由
|
* @param routes Async/dynamic routes, возвращённые API
|
||||||
* @param roles 用户角色集合
|
* @param roles Набор ролей пользователя
|
||||||
* @returns 返回用户有权限的异步(动态)路由
|
* @returns Возвращает async/dynamic routes, доступные пользователю
|
||||||
*/
|
*/
|
||||||
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
|
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
|
||||||
const asyncRoutes: RouteRecordRaw[] = [];
|
const asyncRoutes: RouteRecordRaw[] = [];
|
||||||
|
|
||||||
routes.forEach((route) => {
|
routes.forEach((route) => {
|
||||||
const tmpRoute = { ...route }; // ES6扩展运算符复制新对象
|
const tmpRoute = { ...route }; // Копирование объекта через spread operator ES6
|
||||||
|
|
||||||
// 判断用户(角色)是否有该路由的访问权限
|
// Проверка доступа пользователя/роли к маршруту
|
||||||
if (hasPermission(roles, tmpRoute)) {
|
if (hasPermission(roles, tmpRoute)) {
|
||||||
if (tmpRoute.component?.toString() == "Layout") {
|
if (tmpRoute.component?.toString() == "Layout") {
|
||||||
tmpRoute.component = Layout;
|
tmpRoute.component = Layout;
|
||||||
@@ -76,13 +76,13 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成动态路由
|
* Генерация dynamic routes
|
||||||
*
|
*
|
||||||
* @param roles 用户角色集合
|
* @param roles Набор ролей пользователя
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function generateRoutes(roles: string[]) {
|
function generateRoutes(roles: string[]) {
|
||||||
// 根据角色获取有访问权限的路由
|
// Получение доступных routes по ролям
|
||||||
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
|
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
|
||||||
setRoutes(accessedRoutes);
|
setRoutes(accessedRoutes);
|
||||||
return accessedRoutes;
|
return accessedRoutes;
|
||||||
@@ -91,7 +91,9 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
return { routes, setRoutes, generateRoutes };
|
return { routes, setRoutes, generateRoutes };
|
||||||
});
|
});
|
||||||
|
|
||||||
// 非setup
|
// Вне setup
|
||||||
export function usePermissionStoreHook() {
|
export function usePermissionStoreHook() {
|
||||||
return usePermissionStore(store);
|
return usePermissionStore(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -54,3 +54,5 @@ export const useSettingsStore = defineStore("setting", () => {
|
|||||||
changeSetting,
|
changeSetting,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visitedViews.value = visitedViews.value.filter((item, index) => {
|
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||||
// affix:true 固定tag,例如“首页”
|
// affix:true Фиксированная вкладка, например главная
|
||||||
if (index >= currIndex || (item.meta && item.meta.affix)) {
|
if (index >= currIndex || (item.meta && item.meta.affix)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visitedViews.value = visitedViews.value.filter((item, index) => {
|
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||||
// affix:true 固定tag,例如“首页”
|
// affix:true Фиксированная вкладка, например главная
|
||||||
if (index <= currIndex || (item.meta && item.meta.affix)) {
|
if (index <= currIndex || (item.meta && item.meta.affix)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -216,3 +216,5 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
delAllCachedViews,
|
delAllCachedViews,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,3 +31,5 @@ html.dark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
:root {
|
:root {
|
||||||
// 这里可以设置你自定义的颜色变量
|
// Здесь можно задать пользовательские переменные цвета
|
||||||
// 这个是element主要按钮:active的颜色,当主题更改后此变量的值也随之更改
|
// Цвет active-состояния основной кнопки Element Plus, меняется вместе с темой
|
||||||
--el-color-primary-dark: #0d84ff;
|
--el-color-primary-dark: #0d84ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 覆盖 element-plus 的样式
|
// Переопределение стилей Element Plus
|
||||||
.el-breadcrumb__inner,
|
.el-breadcrumb__inner,
|
||||||
.el-breadcrumb__inner a {
|
.el-breadcrumb__inner a {
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
@@ -32,17 +32,19 @@
|
|||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选中行背景色值
|
// Цвет фона выбранной строки
|
||||||
.el-table__body tr.current-row td {
|
.el-table__body tr.current-row td {
|
||||||
background-color: #e1f3d8b5 !important;
|
background-color: #e1f3d8b5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// card 的header统一高度
|
// Единая высота header у card
|
||||||
.el-card__header {
|
.el-card__header {
|
||||||
height: 60px !important;
|
height: 60px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表格表头和表体未对齐
|
// Исправление несовпадения header и body таблицы
|
||||||
.el-table__header col[name="gutter"] {
|
.el-table__header col[name="gutter"] {
|
||||||
display: table-cell !important;
|
display: table-cell !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,5 @@
|
|||||||
box-shadow: var(--el-box-shadow-light);
|
box-shadow: var(--el-box-shadow-light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
||||||
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
"Microsoft YaHei", "Arial", Arial, sans-serif;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -44,7 +44,7 @@ svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
vertical-align: -0.15em; //因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果
|
vertical-align: -0.15em; //Так как размер icon равен размеру шрифта, добавляем смещение для визуального выравнивания
|
||||||
}
|
}
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
@@ -73,3 +73,5 @@ a:active,
|
|||||||
div:focus {
|
div:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -203,3 +203,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// 导出 variables.module.scss 变量提供给TypeScript使用
|
// Экспорт variables.module.scss для использования в TypeScript
|
||||||
:export {
|
:export {
|
||||||
menuBg: $menuBg;
|
menuBg: $menuBg;
|
||||||
menuText: $menuText;
|
menuText: $menuText;
|
||||||
menuActiveText: $menuActiveText;
|
menuActiveText: $menuActiveText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// 全局SCSS变量
|
// Глобальные SCSS-переменные
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--menuBg: #304156;
|
--menuBg: #304156;
|
||||||
@@ -20,3 +20,5 @@ $subMenuActiveText: var(--subMenuActiveText);
|
|||||||
$subMenuHover: var(--subMenuHover);
|
$subMenuHover: var(--subMenuHover);
|
||||||
|
|
||||||
$sideBarWidth: 210px;
|
$sideBarWidth: 210px;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Vendored
+3
-1
@@ -7,7 +7,7 @@ declare module "*.vue" {
|
|||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 环境变量 TypeScript的智能提示
|
// TypeScript-подсказки для переменных окружения
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
VITE_APP_TITLE: string;
|
VITE_APP_TITLE: string;
|
||||||
VITE_APP_PORT: string;
|
VITE_APP_PORT: string;
|
||||||
@@ -17,3 +17,5 @@ interface ImportMetaEnv {
|
|||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
@@ -16,3 +16,5 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* 格式化字节大小
|
* Форматирование размера в байтах
|
||||||
* @param bytes 字节数
|
* @param bytes Количество байт
|
||||||
* @param decimals 小数位数,默认为 2
|
* @param decimals Количество знаков после запятой, по умолчанию 2
|
||||||
* @returns 格式化后的字节大小字符串
|
* @returns Отформатированная строка размера
|
||||||
*/
|
*/
|
||||||
export const formatBytes = (bytes: number, decimals = 2): string => {
|
export const formatBytes = (bytes: number, decimals = 2): string => {
|
||||||
// 检查是否为特殊值
|
// Проверка специального значения
|
||||||
if (bytes === -1) {
|
if (bytes === -1) {
|
||||||
return "Unlimited";
|
return "Unlimited";
|
||||||
}
|
}
|
||||||
@@ -13,21 +13,21 @@ export const formatBytes = (bytes: number, decimals = 2): string => {
|
|||||||
return "0 Bytes";
|
return "0 Bytes";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算单位和大小
|
// Расчёт единицы и размера
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const dm = decimals < 0 ? 0 : decimals;
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
// 返回格式化后的字符串
|
// Возврат отформатированной строки
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculateBytes = (value = 0, unit = "Bytes"): number => {
|
export const calculateBytes = (value = 0, unit = "Bytes"): number => {
|
||||||
// 将单位转换为大写,并去除空格
|
// Приведение единицы к верхнему регистру и удаление пробелов
|
||||||
const formattedUnit = unit.toUpperCase().trim();
|
const formattedUnit = unit.toUpperCase().trim();
|
||||||
|
|
||||||
// 定义存储单位和对应的字节数的映射关系
|
// Маппинг единиц хранения на количество байт
|
||||||
const unitToBytes: Record<string, number> = {
|
const unitToBytes: Record<string, number> = {
|
||||||
BYTES: 1,
|
BYTES: 1,
|
||||||
KB: 1024 ** 1,
|
KB: 1024 ** 1,
|
||||||
@@ -40,7 +40,7 @@ export const calculateBytes = (value = 0, unit = "Bytes"): number => {
|
|||||||
YB: 1024 ** 8,
|
YB: 1024 ** 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查传入的单位是否存在于映射关系中
|
// Проверка наличия единицы в маппинге
|
||||||
if (!Object.prototype.hasOwnProperty.call(unitToBytes, formattedUnit)) {
|
if (!Object.prototype.hasOwnProperty.call(unitToBytes, formattedUnit)) {
|
||||||
throw new Error("Invalid unit");
|
throw new Error("Invalid unit");
|
||||||
}
|
}
|
||||||
@@ -49,47 +49,49 @@ export const calculateBytes = (value = 0, unit = "Bytes"): number => {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算并返回字节数
|
// Расчёт и возврат количества байт
|
||||||
return value * unitToBytes[formattedUnit];
|
return value * unitToBytes[formattedUnit];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化存储容量单位
|
* Форматирование единицы хранения
|
||||||
* @param bytes 存储容量(字节数)
|
* @param bytes Объём хранения в байтах
|
||||||
* @param decimals 小数位数,默认为 2
|
* @param decimals Количество знаков после запятой, по умолчанию 2
|
||||||
* @returns 格式化后的存储容量值
|
* @returns Отформатированное значение объёма хранения
|
||||||
*/
|
*/
|
||||||
export const formatStorageCapacity = (bytes: number, decimals = 2): number => {
|
export const formatStorageCapacity = (bytes: number, decimals = 2): number => {
|
||||||
// 检查输入是否有效
|
// Проверка корректности входных данных
|
||||||
if (!bytes || bytes <= 0) {
|
if (!bytes || bytes <= 0) {
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算存储单位
|
// Расчёт единицы хранения
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const dm = decimals < 0 ? 0 : decimals;
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
// 格式化存储容量值并返回
|
// Форматирование и возврат объёма хранения
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化存储容量单位
|
* Форматирование единицы хранения
|
||||||
* @param bytes 存储容量(字节数)
|
* @param bytes Объём хранения в байтах
|
||||||
* @returns 格式化后的存储单位
|
* @returns Отформатированная единица хранения
|
||||||
*/
|
*/
|
||||||
export const formatStorageUnit = (bytes: number): string => {
|
export const formatStorageUnit = (bytes: number): string => {
|
||||||
// 检查输入是否有效
|
// Проверка корректности входных данных
|
||||||
if (!bytes || bytes <= 0) {
|
if (!bytes || bytes <= 0) {
|
||||||
return "Bytes";
|
return "Bytes";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算存储单位
|
// Расчёт единицы хранения
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
// 返回格式化后的存储单位
|
// Возврат отформатированной единицы хранения
|
||||||
return sizes[i];
|
return sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 浅拷贝,忽略 null,支持嵌套对象
|
* Поверхностное копирование с игнорированием null и поддержкой вложенных объектов
|
||||||
* @param target
|
* @param target
|
||||||
* @param source
|
* @param source
|
||||||
*/
|
*/
|
||||||
@@ -26,7 +26,7 @@ export const assignWith = <T>(target: T, source: Partial<T>): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 深拷贝,忽略 null,支持嵌套对象
|
* Глубокое копирование с игнорированием null и поддержкой вложенных объектов
|
||||||
* @param source
|
* @param source
|
||||||
*/
|
*/
|
||||||
export const deepCopy = <T>(source: Partial<T>): T => {
|
export const deepCopy = <T>(source: Partial<T>): T => {
|
||||||
@@ -49,3 +49,5 @@ export const deepCopy = <T>(source: Partial<T>): T => {
|
|||||||
|
|
||||||
return objCopy as T;
|
return objCopy as T;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import i18n from "@/lang/index";
|
import i18n from "@/lang/index";
|
||||||
|
|
||||||
export function translateRouteTitleI18n(title: any) {
|
export function translateRouteTitleI18n(title: any) {
|
||||||
// 判断是否存在国际化配置,如果没有原生返回
|
// Проверяет наличие локализованного заголовка, иначе возвращает исходное значение
|
||||||
const hasKey = i18n.global.te("route." + title);
|
const hasKey = i18n.global.te("route." + title);
|
||||||
if (hasKey) {
|
if (hasKey) {
|
||||||
const translatedTitle = i18n.global.t("route." + title);
|
const translatedTitle = i18n.global.t("route." + title);
|
||||||
@@ -10,3 +10,5 @@ export function translateRouteTitleI18n(title: any) {
|
|||||||
}
|
}
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,3 +37,5 @@ export function isExternal(path: string) {
|
|||||||
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
|
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
|
||||||
return isExternal;
|
return isExternal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
|
|||||||
import { useAccountStoreHook } from "@/store/modules/account";
|
import { useAccountStoreHook } from "@/store/modules/account";
|
||||||
|
|
||||||
const dynamicBase = (window as any).__dynamic_base__ || "";
|
const dynamicBase = (window as any).__dynamic_base__ || "";
|
||||||
// 创建 axios 实例
|
// Создание axios instance
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: `${dynamicBase}${import.meta.env.VITE_APP_BASE_API}`,
|
baseURL: `${dynamicBase}${import.meta.env.VITE_APP_BASE_API}`,
|
||||||
timeout: 50000,
|
timeout: 50000,
|
||||||
headers: { "Content-Type": "application/json;charset=utf-8" },
|
headers: { "Content-Type": "application/json;charset=utf-8" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// 请求拦截器
|
// Request interceptor
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
const accountStore = useAccountStoreHook();
|
const accountStore = useAccountStoreHook();
|
||||||
@@ -23,40 +23,42 @@ service.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 响应拦截器
|
// Response interceptor
|
||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
(response: AxiosResponse) => {
|
(response: AxiosResponse) => {
|
||||||
const { code, message } = response.data;
|
const { code, message } = response.data;
|
||||||
if (code === 20000) {
|
if (code === 20000) {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
// 响应数据为二进制流处理(文件导出)
|
// Обработка бинарного ответа при экспорте файлов
|
||||||
if (response.data instanceof ArrayBuffer || response.data instanceof Blob) {
|
if (response.data instanceof ArrayBuffer || response.data instanceof Blob) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.error(message || "系统出错");
|
ElMessage.error(message || "Системная ошибка");
|
||||||
return Promise.reject(new Error(message || "Error"));
|
return Promise.reject(new Error(message || "Error"));
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
if (error.response.data) {
|
if (error.response.data) {
|
||||||
const { code, msg } = error.response.data;
|
const { code, msg } = error.response.data;
|
||||||
// token 过期,重新登录
|
// Token истёк, нужен повторный вход
|
||||||
if (code === "A0230") {
|
if (code === "A0230") {
|
||||||
ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
|
ElMessageBox.confirm("Текущая сессия истекла, войдите снова", "Подтверждение", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "ОК",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(msg || "系统出错");
|
ElMessage.error(msg || "Системная ошибка");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject(error.message);
|
return Promise.reject(error.message);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 导出 axios 实例
|
// Export axios instance
|
||||||
export default service;
|
export default service;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,3 +67,5 @@ export const scrollTo = (to: number, duration: number, callback?: any) => {
|
|||||||
};
|
};
|
||||||
animateScroll();
|
animateScroll();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* 将时间戳转换为格式化日期时间字符串(YYYY-MM-DD HH:mm:ss)
|
* Преобразует timestamp в строку даты и времени формата YYYY-MM-DD HH:mm:ss.
|
||||||
* @param timestamp 时间戳
|
* @param timestamp Timestamp.
|
||||||
* @returns 格式化日期时间字符串
|
* @returns Отформатированная строка даты и времени.
|
||||||
*/
|
*/
|
||||||
export const timestampToDateTime = (timestamp: number): string => {
|
export const timestampToDateTime = (timestamp: number): string => {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
@@ -35,24 +35,24 @@ export const calculateTimeDifference = (timestamp: number): string => {
|
|||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
parts.push(`${days}天`);
|
parts.push(`${days} дн.`);
|
||||||
}
|
}
|
||||||
if (remainingHours > 0) {
|
if (remainingHours > 0) {
|
||||||
parts.push(`${remainingHours}小时`);
|
parts.push(`${remainingHours} ч.`);
|
||||||
}
|
}
|
||||||
if (remainingMinutes > 0) {
|
if (remainingMinutes > 0) {
|
||||||
parts.push(`${remainingMinutes}分钟`);
|
parts.push(`${remainingMinutes} мин.`);
|
||||||
}
|
}
|
||||||
if (remainingSeconds > 0) {
|
if (remainingSeconds > 0) {
|
||||||
parts.push(`${remainingSeconds}秒`);
|
parts.push(`${remainingSeconds} с.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.join(" ");
|
return parts.join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一小时后的时间戳
|
* Возвращает timestamp через один час.
|
||||||
* @returns 一周后的时间戳
|
* @returns Timestamp через один час.
|
||||||
*/
|
*/
|
||||||
export const getHourLater = (): number => {
|
export const getHourLater = (): number => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
@@ -61,8 +61,8 @@ export const getHourLater = (): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一天后的时间戳
|
* Возвращает timestamp через один день.
|
||||||
* @returns 一周后的时间戳
|
* @returns Timestamp через один день.
|
||||||
*/
|
*/
|
||||||
export const getDayLater = (): number => {
|
export const getDayLater = (): number => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
@@ -71,8 +71,8 @@ export const getDayLater = (): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一周后的时间戳
|
* Возвращает timestamp через одну неделю.
|
||||||
* @returns 一周后的时间戳
|
* @returns Timestamp через одну неделю.
|
||||||
*/
|
*/
|
||||||
export const getWeekLater = (): number => {
|
export const getWeekLater = (): number => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
@@ -81,8 +81,8 @@ export const getWeekLater = (): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一个月后的时间戳
|
* Возвращает timestamp через один месяц.
|
||||||
* @returns 一个月后的时间戳
|
* @returns Timestamp через один месяц.
|
||||||
*/
|
*/
|
||||||
export const getMonthLater = (): number => {
|
export const getMonthLater = (): number => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
@@ -91,11 +91,13 @@ export const getMonthLater = (): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一年后的时间戳
|
* Возвращает timestamp через один год.
|
||||||
* @returns 一年后的时间戳
|
* @returns Timestamp через один год.
|
||||||
*/
|
*/
|
||||||
export const getYearLater = (): number => {
|
export const getYearLater = (): number => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
date.setFullYear(date.getFullYear() + 1);
|
date.setFullYear(date.getFullYear() + 1);
|
||||||
return date.getTime();
|
return date.getTime();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -493,9 +493,9 @@ import { useRoute } from "vue-router";
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const queryFormRef = ref(ElForm); // 查询表单
|
const queryFormRef = ref(ElForm); // Форма поиска
|
||||||
const dataFormRef = ref(ElForm); // 用户表单
|
const dataFormRef = ref(ElForm); // Форма пользователя
|
||||||
const kickFormRef = ref(ElForm); // 下线表单
|
const kickFormRef = ref(ElForm); // Форма отключения пользователя
|
||||||
const dataFormPassRef = ref(ElInput);
|
const dataFormPassRef = ref(ElInput);
|
||||||
const dataFormConPassRef = ref(ElInput);
|
const dataFormConPassRef = ref(ElInput);
|
||||||
|
|
||||||
@@ -694,7 +694,7 @@ const resetDataForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询
|
* Поиск
|
||||||
*/
|
*/
|
||||||
const handleQuery = async () => {
|
const handleQuery = async () => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
@@ -708,7 +708,7 @@ const handleQuery = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置
|
* Сброс
|
||||||
*/
|
*/
|
||||||
const resetQuery = () => {
|
const resetQuery = () => {
|
||||||
queryFormRef.value.resetFields();
|
queryFormRef.value.resetFields();
|
||||||
@@ -716,7 +716,7 @@ const resetQuery = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存
|
* Сохранение
|
||||||
**/
|
**/
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
state.dialog = {
|
state.dialog = {
|
||||||
@@ -726,7 +726,7 @@ const handleAdd = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改
|
* Изменение
|
||||||
**/
|
**/
|
||||||
const handleUpdate = async (row: { [key: string]: any }) => {
|
const handleUpdate = async (row: { [key: string]: any }) => {
|
||||||
const id = row.id;
|
const id = row.id;
|
||||||
@@ -744,7 +744,7 @@ const setQuota = (newQuota: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单提交
|
* Отправка формы
|
||||||
*/
|
*/
|
||||||
const submitForm = () => {
|
const submitForm = () => {
|
||||||
dataFormRef.value.validate((valid: any) => {
|
dataFormRef.value.validate((valid: any) => {
|
||||||
@@ -769,7 +769,7 @@ const submitForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下线表单提交
|
* Отправка формы отключения пользователя
|
||||||
*/
|
*/
|
||||||
const submitKickForm = () => {
|
const submitKickForm = () => {
|
||||||
kickFormRef.value.validate((valid: any) => {
|
kickFormRef.value.validate((valid: any) => {
|
||||||
@@ -785,7 +785,7 @@ const submitKickForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除
|
* Удаление
|
||||||
*/
|
*/
|
||||||
const handleDelete = (row: { [key: string]: any }) => {
|
const handleDelete = (row: { [key: string]: any }) => {
|
||||||
const id = row.id;
|
const id = row.id;
|
||||||
@@ -811,7 +811,7 @@ const handleDelete = (row: { [key: string]: any }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 强制用户下线
|
* Принудительное отключение пользователя
|
||||||
* @param row
|
* @param row
|
||||||
*/
|
*/
|
||||||
const handleKick = (row: { [key: string]: any }) => {
|
const handleKick = (row: { [key: string]: any }) => {
|
||||||
@@ -823,7 +823,7 @@ const handleKick = (row: { [key: string]: any }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解除下线状态
|
* Снятие статуса отключения
|
||||||
* @param row
|
* @param row
|
||||||
*/
|
*/
|
||||||
const confirmReleaseKick = (row: { [key: string]: any }) => {
|
const confirmReleaseKick = (row: { [key: string]: any }) => {
|
||||||
@@ -834,7 +834,7 @@ const confirmReleaseKick = (row: { [key: string]: any }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭用户弹窗
|
* Закрытие окна пользователя
|
||||||
*/
|
*/
|
||||||
const closeDialog = () => {
|
const closeDialog = () => {
|
||||||
dialog.value.visible = false;
|
dialog.value.visible = false;
|
||||||
@@ -847,7 +847,7 @@ const closeDialog = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭下线弹窗
|
* Закрытие окна отключения
|
||||||
*/
|
*/
|
||||||
const closeDialogKick = () => {
|
const closeDialogKick = () => {
|
||||||
dialogKick.value.visible = false;
|
dialogKick.value.visible = false;
|
||||||
@@ -856,7 +856,7 @@ const closeDialogKick = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入
|
* Импорт
|
||||||
*/
|
*/
|
||||||
const handleImport = (params: UploadRequestOptions) => {
|
const handleImport = (params: UploadRequestOptions) => {
|
||||||
if (state.fileList.length > 0) {
|
if (state.fileList.length > 0) {
|
||||||
@@ -881,7 +881,7 @@ const beforeImport = (file: UploadRawFile) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出
|
* Экспорт
|
||||||
*/
|
*/
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
exportAccountApi().then((res) => {
|
exportAccountApi().then((res) => {
|
||||||
@@ -894,7 +894,7 @@ const handleExport = () => {
|
|||||||
a.href = url;
|
a.href = url;
|
||||||
let dis = res.headers["content-disposition"];
|
let dis = res.headers["content-disposition"];
|
||||||
a.download = dis.split("attachment; filename=")[1];
|
a.download = dis.split("attachment; filename=")[1];
|
||||||
// 模拟点击下载
|
// Имитация клика для скачивания
|
||||||
a.click();
|
a.click();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
ElMessage.success(t("common.success"));
|
ElMessage.success(t("common.success"));
|
||||||
@@ -955,7 +955,7 @@ const resetTraffic = async (row: { [key: string]: any }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化用户列表数据
|
// Инициализация списка пользователей
|
||||||
handleQuery();
|
handleQuery();
|
||||||
if (route.query.focus === "change-pass") {
|
if (route.query.focus === "change-pass") {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@@ -973,3 +973,5 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ const handleExport = async () => {
|
|||||||
a.href = url;
|
a.href = url;
|
||||||
let dis = response.headers["content-disposition"];
|
let dis = response.headers["content-disposition"];
|
||||||
a.download = dis.split("attachment; filename=")[1];
|
a.download = dis.split("attachment; filename=")[1];
|
||||||
// 模拟点击下载
|
// Имитация клика для скачивания
|
||||||
a.click();
|
a.click();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
ElMessage.success(t("common.success"));
|
ElMessage.success(t("common.success"));
|
||||||
@@ -484,3 +484,5 @@ onMounted(() => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- setup 无法设置组件名称,组件名称keepAlive必须 -->
|
<!-- setup не задаёт имя компонента, а имя нужно для keepAlive -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default {
|
export default {
|
||||||
name: "Page401",
|
name: "Page401",
|
||||||
@@ -95,3 +95,5 @@ function back() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- setup 无法设置组件名称,组件名称keepAlive必须 -->
|
<!-- setup не задаёт имя компонента, а имя нужно для keepAlive -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default {
|
export default {
|
||||||
name: "Page404",
|
name: "Page404",
|
||||||
@@ -269,3 +269,5 @@ function message() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -377,3 +377,5 @@ const closeDialog = (): void => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1624,3 +1624,5 @@ onMounted(() => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -337,3 +337,5 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const handleExport = async () => {
|
|||||||
a.href = url;
|
a.href = url;
|
||||||
let dis = response.headers["content-disposition"];
|
let dis = response.headers["content-disposition"];
|
||||||
a.download = dis.split("attachment; filename=")[1];
|
a.download = dis.split("attachment; filename=")[1];
|
||||||
// 模拟点击下载
|
// Имитация клика для скачивания
|
||||||
a.click();
|
a.click();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
ElMessage.success(t("common.success"));
|
ElMessage.success(t("common.success"));
|
||||||
@@ -109,3 +109,5 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const handleExport = async () => {
|
|||||||
a.href = url;
|
a.href = url;
|
||||||
let dis = response.headers["content-disposition"];
|
let dis = response.headers["content-disposition"];
|
||||||
a.download = dis.split("attachment; filename=")[1];
|
a.download = dis.split("attachment; filename=")[1];
|
||||||
// 模拟点击下载
|
// Имитация клика для скачивания
|
||||||
a.click();
|
a.click();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
ElMessage.success(t("common.success"));
|
ElMessage.success(t("common.success"));
|
||||||
@@ -109,3 +109,5 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -76,10 +76,10 @@ import router from "@/router";
|
|||||||
import LangSelect from "@/components/LangSelect/index.vue";
|
import LangSelect from "@/components/LangSelect/index.vue";
|
||||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||||
|
|
||||||
// 状态管理依赖
|
// Зависимость store
|
||||||
import { useAccountStore } from "@/store/modules/account";
|
import { useAccountStore } from "@/store/modules/account";
|
||||||
|
|
||||||
// API依赖
|
// Зависимость API
|
||||||
import { LocationQuery, LocationQueryValue, useRoute } from "vue-router";
|
import { LocationQuery, LocationQueryValue, useRoute } from "vue-router";
|
||||||
import { AccountLoginDto } from "@/api/account/types";
|
import { AccountLoginDto } from "@/api/account/types";
|
||||||
|
|
||||||
@@ -87,25 +87,25 @@ const accountStore = useAccountStore();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按钮loading
|
* Состояние загрузки кнопки
|
||||||
*/
|
*/
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
/**
|
/**
|
||||||
* 是否大写锁定
|
* Включён ли Caps Lock
|
||||||
*/
|
*/
|
||||||
const isCapslock = ref(false);
|
const isCapslock = ref(false);
|
||||||
/**
|
/**
|
||||||
* 密码是否可见
|
* Видимость пароля
|
||||||
*/
|
*/
|
||||||
const passVisible = ref(false);
|
const passVisible = ref(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录表单引用
|
* Ссылка на форму входа
|
||||||
*/
|
*/
|
||||||
const loginFormRef = ref(ElForm);
|
const loginFormRef = ref(ElForm);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录表单
|
* Форма входа
|
||||||
*/
|
*/
|
||||||
const loginForm = ref<AccountLoginDto>({
|
const loginForm = ref<AccountLoginDto>({
|
||||||
username: "",
|
username: "",
|
||||||
@@ -140,7 +140,7 @@ const loginRules = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查输入大小写状态
|
* Проверка состояния регистра ввода
|
||||||
*/
|
*/
|
||||||
const checkCapslock = (e: any) => {
|
const checkCapslock = (e: any) => {
|
||||||
const { key } = e;
|
const { key } = e;
|
||||||
@@ -148,7 +148,7 @@ const checkCapslock = (e: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录
|
* Вход
|
||||||
*/
|
*/
|
||||||
const handleLogin = () => {
|
const handleLogin = () => {
|
||||||
loginFormRef.value.validate((valid: boolean) => {
|
loginFormRef.value.validate((valid: boolean) => {
|
||||||
@@ -208,7 +208,7 @@ const handleLogin = () => {
|
|||||||
.el-input {
|
.el-input {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
// 子组件 scoped 无效,使用 :deep
|
// scoped не действует на дочерний компонент, используется :deep
|
||||||
:deep(.el-input__wrapper) {
|
:deep(.el-input__wrapper) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -226,7 +226,7 @@ const handleLogin = () => {
|
|||||||
-webkit-text-fill-color: #fff !important;
|
-webkit-text-fill-color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置输入框自动填充的延迟属性
|
// Настройка задержки автозаполнения поля ввода
|
||||||
&:-webkit-autofill,
|
&:-webkit-autofill,
|
||||||
&:-webkit-autofill:hover,
|
&:-webkit-autofill:hover,
|
||||||
&:-webkit-autofill:focus,
|
&:-webkit-autofill:focus,
|
||||||
@@ -238,3 +238,5 @@ const handleLogin = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -220,3 +220,5 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ const { path } = params;
|
|||||||
|
|
||||||
router.replace({ path: "/" + path, query });
|
router.replace({ path: "/" + path, query });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"types": ["vite/client", "element-plus/global", "unplugin-icons/types/vue"],
|
"types": ["vite/client", "element-plus/global", "unplugin-icons/types/vue"],
|
||||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||||
"allowSyntheticDefaultImports": true /* 允许默认导入 */,
|
"allowSyntheticDefaultImports": true /* Allow default imports. */,
|
||||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */
|
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
// CSS 预处理器
|
// CSS preprocessor
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
//define global scss variable
|
//define global scss variable
|
||||||
scss: {
|
scss: {
|
||||||
@@ -47,9 +47,9 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: Number(env.VITE_APP_PORT),
|
port: Number(env.VITE_APP_PORT),
|
||||||
open: true, // 运行是否自动打开浏览器
|
open: true, // Automatically open browser on start
|
||||||
proxy: {
|
proxy: {
|
||||||
// 反向代理解决跨域
|
// Reverse proxy for CORS in development
|
||||||
[env.VITE_APP_BASE_API]: {
|
[env.VITE_APP_BASE_API]: {
|
||||||
target: "http://127.0.0.1:8081",
|
target: "http://127.0.0.1:8081",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
@@ -63,7 +63,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||||||
/* options */
|
/* options */
|
||||||
}),
|
}),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
|
// Auto import Vue helpers: ref, reactive, toRef, etc.
|
||||||
imports: ["vue", "@vueuse/core"],
|
imports: ["vue", "@vueuse/core"],
|
||||||
eslintrc: {
|
eslintrc: {
|
||||||
enabled: false, // Default `false`
|
enabled: false, // Default `false`
|
||||||
@@ -71,36 +71,36 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||||||
globalsPropValue: true, // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
|
globalsPropValue: true, // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
|
||||||
},
|
},
|
||||||
resolvers: [
|
resolvers: [
|
||||||
// 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
|
// Auto import Element Plus helpers with styles: ElMessage, ElMessageBox, etc.
|
||||||
ElementPlusResolver(),
|
ElementPlusResolver(),
|
||||||
// 自动导入图标组件
|
// Auto import icon components
|
||||||
IconsResolver({}),
|
IconsResolver({}),
|
||||||
],
|
],
|
||||||
vueTemplate: true, // 是否在 vue 模板中自动导入
|
vueTemplate: true, // Auto import in Vue templates
|
||||||
dts: path.resolve(pathSrc, "types", "auto-imports.d.ts"), // 自动导入组件类型声明文件位置,默认根目录; false 关闭自动生成
|
dts: path.resolve(pathSrc, "types", "auto-imports.d.ts"), // Type declarations for auto imports; false disables generation
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Components({
|
Components({
|
||||||
resolvers: [
|
resolvers: [
|
||||||
// 自动注册图标组件
|
// Auto register icon components
|
||||||
IconsResolver({
|
IconsResolver({
|
||||||
enabledCollections: ["ep"], //@iconify-json/ep 是 Element Plus 的图标库
|
enabledCollections: ["ep"], // @iconify-json/ep is the Element Plus icon collection
|
||||||
}),
|
}),
|
||||||
// 自动导入 Element Plus 组件
|
// Auto import Element Plus components
|
||||||
ElementPlusResolver(),
|
ElementPlusResolver(),
|
||||||
],
|
],
|
||||||
dts: path.resolve(pathSrc, "types", "components.d.ts"), // 自动导入组件类型声明文件位置,默认根目录; false 关闭自动生成
|
dts: path.resolve(pathSrc, "types", "components.d.ts"), // Type declarations for auto components; false disables generation
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Icons({
|
Icons({
|
||||||
// 自动安装图标库
|
// Auto install icon collections
|
||||||
autoInstall: true,
|
autoInstall: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createSvgIconsPlugin({
|
createSvgIconsPlugin({
|
||||||
// 指定需要缓存的图标文件夹
|
// Icon folders to cache
|
||||||
iconDirs: [path.resolve(pathSrc, "assets/icons")],
|
iconDirs: [path.resolve(pathSrc, "assets/icons")],
|
||||||
// 指定symbolId格式
|
// symbolId format
|
||||||
symbolId: "icon-[dir]-[name]",
|
symbolId: "icon-[dir]-[name]",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ import "hy2xs-admin/cmd"
|
|||||||
func main() {
|
func main() {
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user