Русифицировать README и интерфейс HY2XS admin

This commit is contained in:
2026-04-25 23:51:21 +05:00
parent 84a4e94567
commit 2b4a45ad23
167 changed files with 632 additions and 699 deletions
+31 -39
View File
@@ -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-состояния.
+1
View File
@@ -48,3 +48,4 @@ func Execute() {
os.Exit(1) os.Exit(1)
} }
} }
+1
View File
@@ -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)))
} }
+1
View File
@@ -83,3 +83,4 @@ func initFile() error {
} }
return nil return nil
} }
+1
View File
@@ -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)
} }
+5 -4
View File
@@ -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)
} }
+3 -2
View File
@@ -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)
} }
+3 -2
View File
@@ -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)
} }
+1
View File
@@ -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)
} }
+1
View File
@@ -23,3 +23,4 @@ func MonitorHysteria2(c *gin.Context) {
} }
vo.Success(hysteria2MonitorVo, c) vo.Success(hysteria2MonitorVo, c)
} }
+2 -1
View File
@@ -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
} }
+1
View File
@@ -117,3 +117,4 @@ func ListAccount(query interface{}, args ...interface{}) ([]entity.Account, erro
} }
return accounts, nil return accounts, nil
} }
+1
View File
@@ -64,3 +64,4 @@ func UpsertConfig(configs []entity.Config) error {
} }
return nil return nil
} }
+1
View File
@@ -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))
} }
} }
+8 -8
View File
@@ -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
+2 -2
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
## 生产环境 ## Production environment
VITE_APP_TITLE = 'h-ui' VITE_APP_TITLE = 'h-ui'
VITE_APP_PORT = 80 VITE_APP_PORT = 80
+3 -3
View File
@@ -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
+17 -17
View File
@@ -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,
}; };
+10 -10
View File
@@ -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
View File
@@ -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
```
+1
View File
@@ -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)
} }
+2
View File
@@ -9,3 +9,5 @@ const appStore = useAppStore();
<router-view /> <router-view />
</el-config-provider> </el-config-provider>
</template> </template>
+14 -12
View File
@@ -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",
}); });
} }
+2
View File
@@ -77,3 +77,5 @@ export interface KickAccountForm {
ids: number[]; ids: number[];
kickUtilTime: number; kickUtilTime: number;
} }
+2
View File
@@ -110,3 +110,5 @@ export function uploadCertFileApi(data: FormData): AxiosPromise<string> {
headers: { "Content-Type": "multipart/form-data" }, headers: { "Content-Type": "multipart/form-data" },
}); });
} }
+2
View File
@@ -300,3 +300,5 @@ export interface Hysteria2AcmePathVo {
crtPath: string; crtPath: string;
keyPath: string; keyPath: string;
} }
+2
View File
@@ -38,3 +38,5 @@ export function hysteria2UrlApi(
params: dto, params: dto,
}); });
} }
+2
View File
@@ -24,3 +24,5 @@ export interface Hysteria2UrlVo {
url: string; url: string;
qrCode: string; qrCode: string;
} }
+2
View File
@@ -33,3 +33,5 @@ export function exportLogApi(data: LogExportDto): AxiosPromise {
responseType: "blob", responseType: "blob",
}); });
} }
+2
View File
@@ -22,3 +22,5 @@ export interface LogHysteria2Vo {
msg: string; msg: string;
time: string; time: string;
} }
+2
View File
@@ -15,3 +15,5 @@ export function monitorHysteria2Api(): AxiosPromise<Hysteria2MonitorVo> {
method: "get", method: "get",
}); });
} }
+2
View File
@@ -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>
+4 -2
View File
@@ -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 = {
} }
}, },
}; };
+2
View File
@@ -22,3 +22,5 @@ const i18n = createI18n({
}); });
export default i18n; export default i18n;
+5 -3
View File
@@ -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 {
}, },
}, },
}; };
+2
View File
@@ -256,3 +256,5 @@ export default {
}, },
}, },
}; };
-312
View File
@@ -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>
+18 -16
View File
@@ -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";
+10 -8
View File
@@ -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>
+7 -5
View File
@@ -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");
+8 -6
View File
@@ -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();
}); });
+6 -4
View File
@@ -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;
+16 -14
View File
@@ -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 -1
View File
@@ -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 };
+8 -6
View File
@@ -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);
} }
+4 -2
View File
@@ -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,
}; };
}); });
+15 -13
View File
@@ -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,
}; };
}); });
+4 -2
View File
@@ -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,
}; };
}); });
+2
View File
@@ -31,3 +31,5 @@ html.dark {
} }
} }
} }
+8 -6
View File
@@ -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;
} }
+2
View File
@@ -15,3 +15,5 @@
box-shadow: var(--el-box-shadow-light); box-shadow: var(--el-box-shadow-light);
} }
} }
+4 -2
View File
@@ -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;
} }
+2
View File
@@ -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;
} }
+3 -1
View File
@@ -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;
+3 -1
View File
@@ -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;
} }
+2
View File
@@ -16,3 +16,5 @@ declare global {
} }
} }
export {}; export {};
+26 -24
View File
@@ -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];
}; };
+4 -2
View File
@@ -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;
}; };
+3 -1
View File
@@ -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;
} }
+2
View File
@@ -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;
} }
+12 -10
View File
@@ -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;
+2
View File
@@ -67,3 +67,5 @@ export const scrollTo = (to: number, duration: number, callback?: any) => {
}; };
animateScroll(); animateScroll();
}; };
+19 -17
View File
@@ -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();
}; };
+20 -18
View File
@@ -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>
+3 -1
View File
@@ -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>
+3 -1
View File
@@ -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>
+3 -1
View File
@@ -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>
+13 -11
View File
@@ -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>
+1 -1
View File
@@ -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": [
+15 -15
View File
@@ -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]",
}), }),
], ],
+1
View File
@@ -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