Русифицировать 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
+2
View File
@@ -9,3 +9,5 @@ const appStore = useAppStore();
<router-view />
</el-config-provider>
</template>
+14 -12
View File
@@ -11,7 +11,7 @@ import {
} from "./types";
/**
* 查询
* Поиск
*/
export function getAccountApi(data: IdDto): AxiosPromise<AccountVo> {
return request({
@@ -22,7 +22,7 @@ export function getAccountApi(data: IdDto): AxiosPromise<AccountVo> {
}
/**
* 保存
* Сохранение
*
* @param data
*/
@@ -35,7 +35,7 @@ export function saveAccountApi(data: AccountSaveDto): AxiosPromise {
}
/**
* 查询当前
* Запрос текущего пользователя
*/
export function getAccountInfoApi(): AxiosPromise<AccountInfo> {
return request({
@@ -45,7 +45,7 @@ export function getAccountInfoApi(): AxiosPromise<AccountInfo> {
}
/**
* 分页
* Пагинация
* @param data
*/
export function pageAccountApi(
@@ -59,7 +59,7 @@ export function pageAccountApi(
}
/**
* 删除
* Удаление
*
* @param data
*/
@@ -72,7 +72,7 @@ export function deleteAccountApi(data: IdDto): AxiosPromise {
}
/**
* 修改
* Изменение
* @param data
*/
export function updateAccountApi(data: AccountUpdateDto): AxiosPromise {
@@ -84,7 +84,7 @@ export function updateAccountApi(data: AccountUpdateDto): AxiosPromise {
}
/**
* 重设流量
* Сброс трафика
* @param data
*/
export function resetTrafficApi(data: IdDto): AxiosPromise {
@@ -96,7 +96,7 @@ export function resetTrafficApi(data: IdDto): AxiosPromise {
}
/**
* 登录
* Вход
* @param data
*/
export function loginApi(data: AccountLoginDto): AxiosPromise<AccountLoginVo> {
@@ -108,7 +108,7 @@ export function loginApi(data: AccountLoginDto): AxiosPromise<AccountLoginVo> {
}
/**
* 导入
* Импорт
*/
export function importAccountApi(data: FormData): AxiosPromise {
return request({
@@ -122,7 +122,7 @@ export function importAccountApi(data: FormData): AxiosPromise {
}
/**
* 导出
* Экспорт
*/
export function exportAccountApi(): AxiosPromise {
return request({
@@ -133,7 +133,7 @@ export function exportAccountApi(): AxiosPromise {
}
/**
* 解除下线状态
* Снятие статуса отключения
*/
export function releaseKickAccountApi(data: IdDto): AxiosPromise {
return request({
@@ -144,7 +144,7 @@ export function releaseKickAccountApi(data: IdDto): AxiosPromise {
}
/**
* 是否默认密码
* Проверка пароля по умолчанию
* @param data
*/
export function verifyDefaultPassApi(): AxiosPromise {
@@ -153,3 +153,5 @@ export function verifyDefaultPassApi(): AxiosPromise {
method: "get",
});
}
+2
View File
@@ -77,3 +77,5 @@ export interface KickAccountForm {
ids: number[];
kickUtilTime: number;
}
+2
View File
@@ -110,3 +110,5 @@ export function uploadCertFileApi(data: FormData): AxiosPromise<string> {
headers: { "Content-Type": "multipart/form-data" },
});
}
+2
View File
@@ -300,3 +300,5 @@ export interface Hysteria2AcmePathVo {
crtPath: string;
keyPath: string;
}
+2
View File
@@ -38,3 +38,5 @@ export function hysteria2UrlApi(
params: dto,
});
}
+2
View File
@@ -24,3 +24,5 @@ export interface Hysteria2UrlVo {
url: string;
qrCode: string;
}
+2
View File
@@ -33,3 +33,5 @@ export function exportLogApi(data: LogExportDto): AxiosPromise {
responseType: "blob",
});
}
+2
View File
@@ -22,3 +22,5 @@ export interface LogHysteria2Vo {
msg: string;
time: string;
}
+2
View File
@@ -15,3 +15,5 @@ export function monitorHysteria2Api(): AxiosPromise<Hysteria2MonitorVo> {
method: "get",
});
}
+2
View File
@@ -11,3 +11,5 @@ export interface Hysteria2MonitorVo {
version: string;
running: boolean;
}
@@ -44,3 +44,5 @@ function toggleClick() {
}
}
</style>
@@ -75,3 +75,5 @@ const handleClose = (tag: string): void => {
flex-wrap: wrap;
}
</style>
@@ -34,3 +34,5 @@ function handleLanguageChange(lang: string) {
</template>
</el-dropdown>
</template>
@@ -178,3 +178,5 @@ const closeDialog = (): void => {
flex-wrap: wrap;
}
</style>
@@ -86,3 +86,5 @@ function handleCurrentChange(val: number) {
}
}
</style>
@@ -28,7 +28,7 @@ function addEventClick() {
}
function closeSidebar(evt: any) {
// 主题选择点击不关闭
// Клик по выбору темы не закрывает панель
let parent = evt.target.closest(".right-panel-container");
if (!parent) {
show.value = false;
@@ -134,3 +134,5 @@ onBeforeUnmount(() => {
}
}
</style>
@@ -4,14 +4,14 @@ import { useAppStore } from "@/store/modules/app";
const appStore = useAppStore();
const sizeOptions = ref([
{ label: "默认", value: "default" },
{ label: "大型", value: "large" },
{ label: "小型", value: "small" },
{ label: "Обычный", value: "default" },
{ label: "Крупный", value: "large" },
{ label: "Компактный", value: "small" },
]);
function handleSizeChange(size: string) {
appStore.changeSize(size);
ElMessage.success("切换布局大小成功");
ElMessage.success("Размер интерфейса изменён");
}
</script>
@@ -34,3 +34,5 @@ function handleSizeChange(size: string) {
</template>
</el-dropdown>
</template>
@@ -36,8 +36,10 @@ const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
width: 1em;
height: 1em;
overflow: hidden;
vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
vertical-align: -0.15em; /* Так как размер icon равен размеру шрифта, добавляем смещение для визуального выравнивания */
outline: none;
fill: currentcolor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
fill: currentcolor; /* currentColor наследует текущий цвет элемента или родителя */
}
</style>
@@ -72,3 +72,5 @@ watch(
</script>
<style lang="scss" scoped></style>
+4 -2
View File
@@ -2,8 +2,10 @@ import type { App } from "vue";
import { hasRole } from "./permission";
// 全局注册 directive
// Глобальная регистрация directive
export function setupDirective(app: App<Element>) {
// 使 v-hasRole 在所有组件中都可用
// Делает v-hasRole доступным во всех компонентах
app.directive("hasRole", hasRole);
}
@@ -2,14 +2,14 @@ import { useAccountStoreHook } from "@/store/modules/account";
import { Directive, DirectiveBinding } from "vue";
/**
* 角色权限
* Ролевые права
*/
export const hasRole: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
if (value) {
const requiredRoles = value; // DOM绑定需要的角色编码
const requiredRoles = value; // Коды ролей, требуемые DOM-привязкой
const { roles } = useAccountStoreHook();
const hasRole = roles.some((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;
+5 -3
View File
@@ -1,5 +1,5 @@
export default {
// 路由国际化
// МаршрутЛокализация
route: {
account: "Account",
accountList: "Account Manage",
@@ -15,14 +15,14 @@ export default {
info: "Info",
infoAccount: "Account Info",
},
// 登录页面国际化
// Локализация страницы входа
login: {
title: "HY2XS admin",
username: "Username",
password: "Password",
login: "Login",
},
// 导航栏国际化
// Локализация навигационной панели
navbar: {
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>
+18 -16
View File
@@ -12,27 +12,27 @@ const accountStore = useAccountStore();
const route = useRoute();
const router = useRouter();
const { device } = storeToRefs(appStore); // 设备类型:desktop-宽屏设备 || mobile-窄屏设备
const { device } = storeToRefs(appStore); // Тип устройства: desktop — широкий экран, mobile — узкий экран
/**
* 左侧菜单栏显示/隐藏
* Показать или скрыть левое меню.
*/
function toggleSideBar() {
appStore.toggleSidebar(true);
}
/**
* vueUse 全屏
* Полноэкранный режим через VueUse.
*/
const { isFullscreen, toggle } = useFullscreen();
/**
* 注销
* Выход из системы.
*/
function logout() {
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
ElMessageBox.confirm("Выйти из системы?", "Подтверждение", {
confirmButtonText: "Выйти",
cancelButtonText: "Отмена",
type: "warning",
}).then(() => {
accountStore
@@ -48,9 +48,9 @@ function logout() {
</script>
<template>
<!-- 顶部导航栏 -->
<!-- Верхняя навигационная панель -->
<div class="navbar">
<!-- 左侧面包屑 -->
<!-- Хлебные крошки слева -->
<div class="flex">
<hamburger
:is-active="appStore.sidebar.opened"
@@ -59,25 +59,25 @@ function logout() {
<breadcrumb />
</div>
<!-- 右侧导航设置 -->
<!-- Настройки навигации справа -->
<div class="flex">
<!-- 导航栏设置(窄屏隐藏)-->
<!-- Настройки навигации, скрытые на узком экране -->
<div class="setting-container" v-if="device !== 'mobile'">
<!--全屏 -->
<!-- Полноэкранный режим -->
<div class="setting-item" @click="toggle">
<svg-icon
:icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
/>
</div>
<!-- 布局大小 -->
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<!-- Размер интерфейса -->
<el-tooltip content="Размер интерфейса" effect="dark" placement="bottom">
<size-select class="setting-item" />
</el-tooltip>
<!--语言选择-->
<!-- Выбор языка -->
<lang-select class="setting-item" />
</div>
<!-- 用户头像 -->
<!-- Аватар пользователя -->
<el-dropdown trigger="click">
<div class="avatar-container">
<img src="/src/assets/logo.png" />
@@ -138,3 +138,5 @@ function logout() {
}
}
</style>
@@ -5,21 +5,21 @@ import IconEpSunny from "~icons/ep/sunny";
import IconEpMoon from "~icons/ep/moon";
/**
* 暗黑模式
* Тёмная тема.
*/
const settingsStore = useSettingsStore();
const isDark = useDark();
const toggleDark = () => useToggle(isDark);
/**
* 切换布局
* Переключение layout.
*/
function changeLayout(layout: string) {
settingsStore.changeSetting({ key: "layout", value: layout });
window.document.body.setAttribute("layout", settingsStore.layout);
}
// 主题颜色
// Цвета темы
const themeColors = ref<string[]>([
"#409EFF",
"#304156",
@@ -30,7 +30,7 @@ const themeColors = ref<string[]>([
]);
/**
* 切换主题颜色
* Переключение цвета темы.
*/
function changeThemeColor(color: string) {
settingsStore.changeSetting({ key: "themeColor", value: color });
@@ -51,8 +51,8 @@ onMounted(() => {
<template>
<div class="settings-container">
<h3 class="text-base font-bold">项目配置</h3>
<el-divider>主题</el-divider>
<h3 class="text-base font-bold">Настройки проекта</h3>
<el-divider>Тема</el-divider>
<div class="flex justify-center" @click.stop>
<el-switch
@@ -66,23 +66,23 @@ onMounted(() => {
/>
</div>
<el-divider>界面设置</el-divider>
<el-divider>Настройки интерфейса</el-divider>
<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" />
</div>
<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" />
</div>
<div class="py-[8px] flex justify-between">
<span class="text-xs">侧边栏 Logo</span>
<span class="text-xs">Логотип в боковом меню</span>
<el-switch v-model="settingsStore.sidebarLogo" />
</div>
<el-divider>主题颜色</el-divider>
<el-divider>Цвет темы</el-divider>
<ul class="w-full space-x-2 flex justify-center py-2">
<li
@@ -163,3 +163,5 @@ onMounted(() => {
}
}
</style>
@@ -35,3 +35,5 @@ function push() {
<slot />
</div>
</template>
@@ -51,3 +51,5 @@ const logo = ref(new URL(`../../../assets/logo.png`, import.meta.url).href);
opacity: 0;
}
</style>
@@ -8,7 +8,7 @@ import SvgIcon from "@/components/SvgIcon/index.vue";
const props = defineProps({
/**
* 路由(eg:level_3_1)
* Route, например level_3_1
*/
item: {
type: Object,
@@ -16,7 +16,7 @@ const props = defineProps({
},
/**
* 父层级完整路由路径(eg:/level/level_3/level_3_1)
* Полный path родительского уровня, например /level/level_3/level_3_1
*/
basePath: {
type: String,
@@ -24,34 +24,34 @@ const props = defineProps({
},
});
const onlyOneChild = ref(); // 临时变量,唯一子路由
const onlyOneChild = ref(); // Временная переменная для единственного дочернего route
/**
* 判断当前路由是否只有一个子路由
* Проверяет, есть ли у текущего route только один дочерний route
*
* 1:如果只有一个子路由: 返回 true
* 2:如果无子路由 :返回 true
* 1: если дочерний route один, возвращает true
* 2: если дочерних routes нет, возвращает true
*
* @param children 子路由数组
* @param parent 当前路由
* @param children Массив дочерних routes
* @param parent Текущий route
*/
function hasOneShowingChild(children = [], parent: any) {
// 需要显示的子路由数组
// Массив дочерних routes для отображения
const showingChildren = children.filter((item: any) => {
if (item.meta?.hidden) {
return false; // 过滤不显示的子路由
return false; // Фильтрация скрытого дочернего route
} else {
onlyOneChild.value = item; // 唯一子路由赋值(多个子路由情况 onlyOneChild 变量是用不上的)
onlyOneChild.value = item; // Запись единственного дочернего route, при нескольких routes переменная onlyOneChild не используется
return true;
}
});
// 1:如果只有一个子路由, 返回 true
// 1: если дочерний route один, возвращает true
if (showingChildren.length === 1) {
return true;
}
// 2:如果无子路由, 复制当前路由信息作为其子路由,满足只拥有一个子路由的条件,所以返回 true
// 2: если дочерних routes нет, копирует текущий route как дочерний и возвращает true
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
@@ -60,9 +60,9 @@ function hasOneShowingChild(children = [], parent: any) {
}
/**
* 解析路径
* Разбор path
*
* @param routePath 路由路径
* @param routePath Route path
*/
function resolvePath(routePath: string) {
if (isExternal(routePath)) {
@@ -71,14 +71,14 @@ function resolvePath(routePath: string) {
if (isExternal(props.basePath)) {
return props.basePath;
}
// 完整路径 = 父级路径(/level/level_3) + 路由路径
const fullPath = path.resolve(props.basePath, routePath); // 相对路径 → 绝对路径
// Полный path = parent path (/level/level_3) + route path
const fullPath = path.resolve(props.basePath, routePath); // Relative path → absolute path
return fullPath;
}
</script>
<template>
<div v-if="!item.meta || !item.meta.hidden">
<!-- 只包含一个子路由节点的路由显示其唯一子路由 -->
<!-- Route с одним дочерним route показывает этот единственный дочерний route -->
<template
v-if="
hasOneShowingChild(item.children, item) &&
@@ -98,7 +98,7 @@ function resolvePath(routePath: string) {
</app-link>
</template>
<!-- 包含多个子路由 -->
<!-- Содержит несколько дочерних routes -->
<el-sub-menu v-else :index="resolvePath(item.path)" teleported>
<template #title>
<svg-icon
@@ -119,3 +119,5 @@ function resolvePath(routePath: string) {
</el-sub-menu>
</div>
</template>
@@ -43,3 +43,5 @@ const route = useRoute();
</el-scrollbar>
</div>
</template>
@@ -119,3 +119,5 @@ defineExpose({
}
}
</style>
@@ -41,12 +41,12 @@ watch(
moveToCurrentTag();
},
{
//初始化立即执行
// Выполнить сразу при инициализации
immediate: true,
}
);
const tagMenuVisible = ref(false); // 标签操作菜单显示状态
const tagMenuVisible = ref(false); // Состояние отображения меню действий с вкладкой
watch(tagMenuVisible, (value) => {
if (value) {
document.body.addEventListener("click", closeTagMenu);
@@ -264,7 +264,7 @@ onMounted(() => {
</router-link>
</scroll-pane>
<!-- tag标签操作菜单 -->
<!-- Меню действий с вкладкой -->
<ul
v-show="tagMenuVisible"
class="tag-menu"
@@ -272,27 +272,27 @@ onMounted(() => {
>
<li @click="refreshSelectedTag(selectedTag)">
<svg-icon icon-class="refresh" />
刷新
Обновить
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<svg-icon icon-class="close" />
关闭
Закрыть
</li>
<li @click="closeOtherTags">
<svg-icon icon-class="close_other" />
关闭其它
Закрыть остальные
</li>
<li v-if="!isFirstView()" @click="closeLeftTags">
<svg-icon icon-class="close_left" />
关闭左侧
Закрыть слева
</li>
<li v-if="!isLastView()" @click="closeRightTags">
<svg-icon icon-class="close_right" />
关闭右侧
Закрыть справа
</li>
<li @click="closeAllTags(selectedTag)">
<svg-icon icon-class="close_all" />
关闭所有
Закрыть все
</li>
</ul>
</div>
@@ -371,3 +371,5 @@ onMounted(() => {
}
}
</style>
@@ -2,3 +2,5 @@ export { default as Navbar } from "./Navbar.vue";
export { default as AppMain } from "./AppMain.vue";
export { default as Settings } from "./Settings/index.vue";
export { default as TagsView } from "./TagsView/index.vue";
+10 -8
View File
@@ -11,11 +11,11 @@ import { useSettingsStore } from "@/store/modules/settings";
const { width } = useWindowSize();
/**
* 响应式布局容器固定宽度
* Фиксированная ширина контейнера адаптивного layout
*
* 大屏>=1200px
* 中屏>=992px
* 小屏>=768px
* Большой экран>=1200px
* Средний экран>=992px
* Малый экран>=768px
*/
const WIDTH = 992;
@@ -41,7 +41,7 @@ watchEffect(() => {
appStore.toggleDevice("desktop");
if (width.value >= 1200) {
//大屏
//Большой экран
appStore.openSideBar(true);
} else {
appStore.closeSideBar(true);
@@ -56,7 +56,7 @@ function handleOutsideClick() {
<template>
<div :class="classObj" class="app-wrapper">
<!-- 手机设备侧边栏打开遮罩层 -->
<!-- Overlay открытого бокового меню на мобильных устройствах -->
<div
v-if="classObj.mobile && classObj.openSidebar"
class="drawer-bg"
@@ -71,10 +71,10 @@ function handleOutsideClick() {
<tags-view v-if="showTagsView" />
</div>
<!--主页面-->
<!--Основная страница-->
<app-main />
<!-- 设置面板 -->
<!-- Панель настроек -->
<RightPanel v-if="showSettings">
<settings />
</RightPanel>
@@ -127,3 +127,5 @@ function handleOutsideClick() {
opacity: 0.3;
}
</style>
+7 -5
View File
@@ -6,21 +6,23 @@ import { setupDirective } from "@/directive";
import "@/permission";
// 本地SVG图标
// Локальные SVG-иконки
import "virtual:svg-icons-register";
// 国际化
// Локализация
import i18n from "@/lang/index";
// 样式
// Стили
import "element-plus/theme-chalk/dark/css-vars.css";
import "@/styles/index.scss";
import "uno.css";
const app = createApp(App);
// 全局注册 自定义指令(directive)
// Глобальная регистрация пользовательских директив
setupDirective(app);
// 全局注册 状态管理(store)
// Глобальная регистрация store
setupStore(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/nprogress.css";
NProgress.configure({ showSpinner: false }); // 进度条
NProgress.configure({ showSpinner: false }); // Индикатор загрузки
const permissionStore = usePermissionStoreHook();
// 白名单路由
// Разрешённые маршруты
const whiteList = ["/login", "/register"];
router.beforeEach(async (to, from, next) => {
@@ -17,14 +17,14 @@ router.beforeEach(async (to, from, next) => {
const hasToken = localStorage.getItem("accessToken");
if (hasToken) {
if (to.path === "/login") {
// 如果已登录,跳转首页
// Если пользователь уже вошёл, перейти на главную страницу
next({ path: "/" });
NProgress.done();
} else {
const AccountStore = useAccountStoreHook();
const hasRoles = AccountStore.roles && AccountStore.roles.length > 0;
if (hasRoles) {
// 未匹配到任何路由,跳转404
// Если маршрут не найден, перейти на 404
if (to.matched.length === 0) {
from.name ? next({ name: from.name }) : next("/404");
} else {
@@ -39,7 +39,7 @@ router.beforeEach(async (to, from, next) => {
});
next({ ...to, replace: true });
} catch (error) {
// 移除 token 并跳转登录页
// Удалить token и перейти на страницу входа
await AccountStore.resetToken();
next(`/login?redirect=${to.path}`);
NProgress.done();
@@ -47,7 +47,7 @@ router.beforeEach(async (to, from, next) => {
}
}
} else {
// 未登录可以访问白名单页面
// Без входа доступны только разрешённые страницы
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
@@ -60,3 +60,5 @@ router.beforeEach(async (to, from, next) => {
router.afterEach(() => {
NProgress.done();
});
+6 -4
View File
@@ -7,7 +7,7 @@ import {
export const Layout = () => import("@/layout/index.vue");
// 静态路由
// Статические маршруты
export const constantRoutes: RouteRecordRaw[] = [
{
path: "/redirect",
@@ -183,17 +183,17 @@ export const asyncRoutes: any[] = [
];
/**
* 创建路由
* Создание маршрутизатора
*/
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes as RouteRecordRaw[],
// 刷新时,滚动条位置还原
// Восстановление позиции прокрутки при обновлении
scrollBehavior: () => ({ left: 0, top: 0 }),
});
/**
* 重置路由
* Сброс маршрутов
*/
export function resetRouter() {
router.replace({ path: "/login" });
@@ -201,3 +201,5 @@ export function resetRouter() {
}
export default router;
+16 -14
View File
@@ -1,46 +1,46 @@
// 系统设置
// Системные настройки
interface DefaultSettings {
/**
* 系统title
* Заголовок системы
*/
title: string;
/**
* 是否显示设置
* Показывать настройки
*/
showSettings: boolean;
/**
* 是否显示多标签导航
* Показывать навигацию по вкладкам
*/
tagsView: boolean;
/**
*是否固定头部
*Фиксировать header
*/
fixedHeader: boolean;
/**
* 是否显示侧边栏Logo
* Показывать логотип в боковом меню
*/
sidebarLogo: boolean;
/**
* 导航栏布局
* Layout навигации
*/
layout: string;
/**
* 主题颜色
* Цвет темы
*/
themeColor: string;
/**
* 主题模式
* Тема интерфейса
*/
theme: string;
/**
* 布局大小
* Размер интерфейса
*/
size: string;
/**
* 语言
* Язык
*/
language: string;
}
@@ -54,10 +54,10 @@ const defaultSettings: DefaultSettings = {
layout: "left",
themeColor: "#409EFF",
/**
* 主题模式
* Тема интерфейса
*
* dark:暗黑模式
* light: 明亮模式
* dark:Тёмная тема
* light: Светлая тема
*/
theme: "dark",
size: "default", // default |large |small
@@ -65,3 +65,5 @@ const defaultSettings: DefaultSettings = {
};
export default defaultSettings;
+3 -1
View File
@@ -3,9 +3,11 @@ import { createPinia } from "pinia";
const store = createPinia();
// 全局注册 store
// Глобальная регистрация store
export function setupStore(app: App<Element>) {
app.use(store);
}
export { store };
+8 -6
View File
@@ -13,10 +13,10 @@ export const useAccountStore = defineStore("account", () => {
const token = useStorage("accessToken", "");
const id = ref(0);
const username = ref("");
const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
const roles = ref<Array<string>>([]); // Коды ролей пользователя для проверки доступа к маршрутам
/**
* 登录
* Вход
*
* @returns
*/
@@ -34,7 +34,7 @@ export const useAccountStore = defineStore("account", () => {
});
}
// 查询当前
// Запрос текущего пользователя
function getAccountInfo() {
return new Promise<AccountInfo>((resolve, reject) => {
getAccountInfoApi()
@@ -56,7 +56,7 @@ export const useAccountStore = defineStore("account", () => {
});
}
// 注销
// Выход
function logout() {
return new Promise<void>((resolve, reject) => {
resetRouter();
@@ -65,7 +65,7 @@ export const useAccountStore = defineStore("account", () => {
});
}
// 重置
// Сброс
function resetToken() {
token.value = "";
id.value = 0;
@@ -85,7 +85,9 @@ export const useAccountStore = defineStore("account", () => {
};
});
// setup
// Вне setup
export function useAccountStoreHook() {
return useAccountStore(store);
}
+4 -2
View File
@@ -19,7 +19,7 @@ export const useAppStore = defineStore("app", () => {
});
/**
* 根据语言标识读取对应的语言包
* Загрузка пакета локализации по коду языка
*/
const locale = computed(() => {
return language?.value == "en" ? en : ru;
@@ -56,7 +56,7 @@ export const useAppStore = defineStore("app", () => {
size.value = val;
}
/**
* 切换语言
* Переключение языка
*
* @param val
*/
@@ -78,3 +78,5 @@ export const useAppStore = defineStore("app", () => {
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
*
* @param roles 用户角色集合
* @param route 路由
* @param roles Набор ролей пользователя
* @param route Маршрут
* @returns
*/
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
if (route.meta && route.meta.roles) {
// 角色【超级管理员】拥有所有权限,忽略校验
// Роль admin имеет все права, проверка пропускается
if (roles.includes("admin")) {
return true;
}
@@ -29,19 +29,19 @@ const hasPermission = (roles: string[], route: RouteRecordRaw) => {
};
/**
* 递归过滤有权限的异步(动态)路由
* Рекурсивная фильтрация доступных async/dynamic routes
*
* @param routes 接口返回的异步(动态)路由
* @param roles 用户角色集合
* @returns 返回用户有权限的异步(动态)路由
* @param routes Async/dynamic routes, возвращённые API
* @param roles Набор ролей пользователя
* @returns Возвращает async/dynamic routes, доступные пользователю
*/
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
const asyncRoutes: RouteRecordRaw[] = [];
routes.forEach((route) => {
const tmpRoute = { ...route }; // ES6扩展运算符复制新对象
const tmpRoute = { ...route }; // Копирование объекта через spread operator ES6
// 判断用户(角色)是否有该路由的访问权限
// Проверка доступа пользователя/роли к маршруту
if (hasPermission(roles, tmpRoute)) {
if (tmpRoute.component?.toString() == "Layout") {
tmpRoute.component = Layout;
@@ -76,13 +76,13 @@ export const usePermissionStore = defineStore("permission", () => {
}
/**
* 生成动态路由
* Генерация dynamic routes
*
* @param roles 用户角色集合
* @param roles Набор ролей пользователя
* @returns
*/
function generateRoutes(roles: string[]) {
// 根据角色获取有访问权限的路由
// Получение доступных routes по ролям
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
setRoutes(accessedRoutes);
return accessedRoutes;
@@ -91,7 +91,9 @@ export const usePermissionStore = defineStore("permission", () => {
return { routes, setRoutes, generateRoutes };
});
// setup
// Вне setup
export function usePermissionStoreHook() {
return usePermissionStore(store);
}
@@ -54,3 +54,5 @@ export const useSettingsStore = defineStore("setting", () => {
changeSetting,
};
});
+4 -2
View File
@@ -127,7 +127,7 @@ export const useTagsViewStore = defineStore("tagsView", () => {
return;
}
visitedViews.value = visitedViews.value.filter((item, index) => {
// affix:true 固定tag,例如“首页”
// affix:true Фиксированная вкладка, например главная
if (index >= currIndex || (item.meta && item.meta.affix)) {
return true;
}
@@ -152,7 +152,7 @@ export const useTagsViewStore = defineStore("tagsView", () => {
return;
}
visitedViews.value = visitedViews.value.filter((item, index) => {
// affix:true 固定tag,例如“首页”
// affix:true Фиксированная вкладка, например главная
if (index <= currIndex || (item.meta && item.meta.affix)) {
return true;
}
@@ -216,3 +216,5 @@ export const useTagsViewStore = defineStore("tagsView", () => {
delAllCachedViews,
};
});
+2
View File
@@ -31,3 +31,5 @@ html.dark {
}
}
}
+8 -6
View File
@@ -1,10 +1,10 @@
:root {
// 这里可以设置你自定义的颜色变量
// 这个是element主要按钮:active的颜色,当主题更改后此变量的值也随之更改
// Здесь можно задать пользовательские переменные цвета
// Цвет active-состояния основной кнопки Element Plus, меняется вместе с темой
--el-color-primary-dark: #0d84ff;
}
// 覆盖 element-plus 的样式
// Переопределение стилей Element Plus
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
@@ -32,17 +32,19 @@
box-sizing: content-box;
}
// 选中行背景色值
// Цвет фона выбранной строки
.el-table__body tr.current-row td {
background-color: #e1f3d8b5 !important;
}
// card 的header统一高度
// Единая высота header у card
.el-card__header {
height: 60px !important;
}
// 表格表头和表体未对齐
// Исправление несовпадения header и body таблицы
.el-table__header col[name="gutter"] {
display: table-cell !important;
}
+2
View File
@@ -15,3 +15,5 @@
box-shadow: var(--el-box-shadow-light);
}
}
+4 -2
View File
@@ -26,7 +26,7 @@ body {
height: 100%;
margin: 0;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
"Microsoft YaHei", "Arial", Arial, sans-serif;
line-height: inherit;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
@@ -44,7 +44,7 @@ svg {
}
svg {
vertical-align: -0.15em; //因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果
vertical-align: -0.15em; //Так как размер icon равен размеру шрифта, добавляем смещение для визуального выравнивания
}
ul,
@@ -73,3 +73,5 @@ a:active,
div:focus {
outline: none;
}
+2
View File
@@ -203,3 +203,5 @@
}
}
}
@@ -1,6 +1,8 @@
// 导出 variables.module.scss 变量提供给TypeScript使用
// Экспорт variables.module.scss для использования в TypeScript
:export {
menuBg: $menuBg;
menuText: $menuText;
menuActiveText: $menuActiveText;
}
+3 -1
View File
@@ -1,4 +1,4 @@
// 全局SCSS变量
// Глобальные SCSS-переменные
:root {
--menuBg: #304156;
@@ -20,3 +20,5 @@ $subMenuActiveText: var(--subMenuActiveText);
$subMenuHover: var(--subMenuHover);
$sideBarWidth: 210px;
+3 -1
View File
@@ -7,7 +7,7 @@ declare module "*.vue" {
export default component;
}
// 环境变量 TypeScript的智能提示
// TypeScript-подсказки для переменных окружения
interface ImportMetaEnv {
VITE_APP_TITLE: string;
VITE_APP_PORT: string;
@@ -17,3 +17,5 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+2
View File
@@ -16,3 +16,5 @@ declare global {
}
}
export {};
+26 -24
View File
@@ -1,11 +1,11 @@
/**
* 格式化字节大小
* @param bytes 字节数
* @param decimals 小数位数,默认为 2
* @returns 格式化后的字节大小字符串
* Форматирование размера в байтах
* @param bytes Количество байт
* @param decimals Количество знаков после запятой, по умолчанию 2
* @returns Отформатированная строка размера
*/
export const formatBytes = (bytes: number, decimals = 2): string => {
// 检查是否为特殊值
// Проверка специального значения
if (bytes === -1) {
return "Unlimited";
}
@@ -13,21 +13,21 @@ export const formatBytes = (bytes: number, decimals = 2): string => {
return "0 Bytes";
}
// 计算单位和大小
// Расчёт единицы и размера
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
// 返回格式化后的字符串
// Возврат отформатированной строки
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
};
export const calculateBytes = (value = 0, unit = "Bytes"): number => {
// 将单位转换为大写,并去除空格
// Приведение единицы к верхнему регистру и удаление пробелов
const formattedUnit = unit.toUpperCase().trim();
// 定义存储单位和对应的字节数的映射关系
// Маппинг единиц хранения на количество байт
const unitToBytes: Record<string, number> = {
BYTES: 1,
KB: 1024 ** 1,
@@ -40,7 +40,7 @@ export const calculateBytes = (value = 0, unit = "Bytes"): number => {
YB: 1024 ** 8,
};
// 检查传入的单位是否存在于映射关系中
// Проверка наличия единицы в маппинге
if (!Object.prototype.hasOwnProperty.call(unitToBytes, formattedUnit)) {
throw new Error("Invalid unit");
}
@@ -49,47 +49,49 @@ export const calculateBytes = (value = 0, unit = "Bytes"): number => {
return -1;
}
// 计算并返回字节数
// Расчёт и возврат количества байт
return value * unitToBytes[formattedUnit];
};
/**
* 格式化存储容量单位
* @param bytes 存储容量(字节数)
* @param decimals 小数位数,默认为 2
* @returns 格式化后的存储容量值
* Форматирование единицы хранения
* @param bytes Объём хранения в байтах
* @param decimals Количество знаков после запятой, по умолчанию 2
* @returns Отформатированное значение объёма хранения
*/
export const formatStorageCapacity = (bytes: number, decimals = 2): number => {
// 检查输入是否有效
// Проверка корректности входных данных
if (!bytes || bytes <= 0) {
return bytes;
}
// 计算存储单位
// Расчёт единицы хранения
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const i = Math.floor(Math.log(bytes) / Math.log(k));
// 格式化存储容量值并返回
// Форматирование и возврат объёма хранения
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
};
/**
* 格式化存储容量单位
* @param bytes 存储容量(字节数)
* @returns 格式化后的存储单位
* Форматирование единицы хранения
* @param bytes Объём хранения в байтах
* @returns Отформатированная единица хранения
*/
export const formatStorageUnit = (bytes: number): string => {
// 检查输入是否有效
// Проверка корректности входных данных
if (!bytes || bytes <= 0) {
return "Bytes";
}
// 计算存储单位
// Расчёт единицы хранения
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
// 返回格式化后的存储单位
// Возврат отформатированной единицы хранения
return sizes[i];
};
+4 -2
View File
@@ -1,5 +1,5 @@
/**
* 浅拷贝,忽略 null,支持嵌套对象
* Поверхностное копирование с игнорированием null и поддержкой вложенных объектов
* @param target
* @param source
*/
@@ -26,7 +26,7 @@ export const assignWith = <T>(target: T, source: Partial<T>): void => {
};
/**
* 深拷贝,忽略 null,支持嵌套对象
* Глубокое копирование с игнорированием null и поддержкой вложенных объектов
* @param source
*/
export const deepCopy = <T>(source: Partial<T>): T => {
@@ -49,3 +49,5 @@ export const deepCopy = <T>(source: Partial<T>): T => {
return objCopy as T;
};
+3 -1
View File
@@ -2,7 +2,7 @@
import i18n from "@/lang/index";
export function translateRouteTitleI18n(title: any) {
// 判断是否存在国际化配置,如果没有原生返回
// Проверяет наличие локализованного заголовка, иначе возвращает исходное значение
const hasKey = i18n.global.te("route." + title);
if (hasKey) {
const translatedTitle = i18n.global.t("route." + title);
@@ -10,3 +10,5 @@ export function translateRouteTitleI18n(title: any) {
}
return title;
}
+2
View File
@@ -37,3 +37,5 @@ export function isExternal(path: string) {
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
return isExternal;
}
+12 -10
View File
@@ -2,14 +2,14 @@ import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
import { useAccountStoreHook } from "@/store/modules/account";
const dynamicBase = (window as any).__dynamic_base__ || "";
// 创建 axios 实例
// Создание axios instance
const service = axios.create({
baseURL: `${dynamicBase}${import.meta.env.VITE_APP_BASE_API}`,
timeout: 50000,
headers: { "Content-Type": "application/json;charset=utf-8" },
});
// 请求拦截器
// Request interceptor
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accountStore = useAccountStoreHook();
@@ -23,40 +23,42 @@ service.interceptors.request.use(
}
);
// 响应拦截器
// Response interceptor
service.interceptors.response.use(
(response: AxiosResponse) => {
const { code, message } = response.data;
if (code === 20000) {
return response.data;
}
// 响应数据为二进制流处理(文件导出)
// Обработка бинарного ответа при экспорте файлов
if (response.data instanceof ArrayBuffer || response.data instanceof Blob) {
return response;
}
ElMessage.error(message || "系统出错");
ElMessage.error(message || "Системная ошибка");
return Promise.reject(new Error(message || "Error"));
},
(error: any) => {
if (error.response.data) {
const { code, msg } = error.response.data;
// token 过期,重新登录
// Token истёк, нужен повторный вход
if (code === "A0230") {
ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
confirmButtonText: "确定",
ElMessageBox.confirm("Текущая сессия истекла, войдите снова", "Подтверждение", {
confirmButtonText: "ОК",
type: "warning",
}).then(() => {
localStorage.clear();
window.location.href = "/";
});
} else {
ElMessage.error(msg || "系统出错");
ElMessage.error(msg || "Системная ошибка");
}
}
return Promise.reject(error.message);
}
);
// 导出 axios 实例
// Export axios instance
export default service;
+2
View File
@@ -67,3 +67,5 @@ export const scrollTo = (to: number, duration: number, callback?: any) => {
};
animateScroll();
};
+19 -17
View File
@@ -1,7 +1,7 @@
/**
* 将时间戳转换为格式化日期时间字符串(YYYY-MM-DD HH:mm:ss
* @param timestamp 时间戳
* @returns 格式化日期时间字符串
* Преобразует timestamp в строку даты и времени формата YYYY-MM-DD HH:mm:ss.
* @param timestamp Timestamp.
* @returns Отформатированная строка даты и времени.
*/
export const timestampToDateTime = (timestamp: number): string => {
const date = new Date(timestamp);
@@ -35,24 +35,24 @@ export const calculateTimeDifference = (timestamp: number): string => {
const parts: string[] = [];
if (days > 0) {
parts.push(`${days}`);
parts.push(`${days} дн.`);
}
if (remainingHours > 0) {
parts.push(`${remainingHours}小时`);
parts.push(`${remainingHours} ч.`);
}
if (remainingMinutes > 0) {
parts.push(`${remainingMinutes}分钟`);
parts.push(`${remainingMinutes} мин.`);
}
if (remainingSeconds > 0) {
parts.push(`${remainingSeconds}`);
parts.push(`${remainingSeconds} с.`);
}
return parts.join(" ");
};
/**
* 获取一小时后的时间戳
* @returns 一周后的时间戳
* Возвращает timestamp через один час.
* @returns Timestamp через один час.
*/
export const getHourLater = (): number => {
const date = new Date();
@@ -61,8 +61,8 @@ export const getHourLater = (): number => {
};
/**
* 获取一天后的时间戳
* @returns 一周后的时间戳
* Возвращает timestamp через один день.
* @returns Timestamp через один день.
*/
export const getDayLater = (): number => {
const date = new Date();
@@ -71,8 +71,8 @@ export const getDayLater = (): number => {
};
/**
* 获取一周后的时间戳
* @returns 一周后的时间戳
* Возвращает timestamp через одну неделю.
* @returns Timestamp через одну неделю.
*/
export const getWeekLater = (): number => {
const date = new Date();
@@ -81,8 +81,8 @@ export const getWeekLater = (): number => {
};
/**
* 获取一个月后的时间戳
* @returns 一个月后的时间戳
* Возвращает timestamp через один месяц.
* @returns Timestamp через один месяц.
*/
export const getMonthLater = (): number => {
const date = new Date();
@@ -91,11 +91,13 @@ export const getMonthLater = (): number => {
};
/**
* 获取一年后的时间戳
* @returns 一年后的时间戳
* Возвращает timestamp через один год.
* @returns Timestamp через один год.
*/
export const getYearLater = (): number => {
const date = new Date();
date.setFullYear(date.getFullYear() + 1);
return date.getTime();
};
+20 -18
View File
@@ -493,9 +493,9 @@ import { useRoute } from "vue-router";
const { t } = useI18n();
const route = useRoute();
const queryFormRef = ref(ElForm); // 查询表单
const dataFormRef = ref(ElForm); // 用户表单
const kickFormRef = ref(ElForm); // 下线表单
const queryFormRef = ref(ElForm); // Форма поиска
const dataFormRef = ref(ElForm); // Форма пользователя
const kickFormRef = ref(ElForm); // Форма отключения пользователя
const dataFormPassRef = ref(ElInput);
const dataFormConPassRef = ref(ElInput);
@@ -694,7 +694,7 @@ const resetDataForm = () => {
};
/**
* 查询
* Поиск
*/
const handleQuery = async () => {
state.loading = true;
@@ -708,7 +708,7 @@ const handleQuery = async () => {
};
/**
* 重置
* Сброс
*/
const resetQuery = () => {
queryFormRef.value.resetFields();
@@ -716,7 +716,7 @@ const resetQuery = () => {
};
/**
* 保存
* Сохранение
**/
const handleAdd = () => {
state.dialog = {
@@ -726,7 +726,7 @@ const handleAdd = () => {
};
/**
* 修改
* Изменение
**/
const handleUpdate = async (row: { [key: string]: any }) => {
const id = row.id;
@@ -744,7 +744,7 @@ const setQuota = (newQuota: number) => {
};
/**
* 表单提交
* Отправка формы
*/
const submitForm = () => {
dataFormRef.value.validate((valid: any) => {
@@ -769,7 +769,7 @@ const submitForm = () => {
};
/**
* 下线表单提交
* Отправка формы отключения пользователя
*/
const submitKickForm = () => {
kickFormRef.value.validate((valid: any) => {
@@ -785,7 +785,7 @@ const submitKickForm = () => {
};
/**
* 删除
* Удаление
*/
const handleDelete = (row: { [key: string]: any }) => {
const id = row.id;
@@ -811,7 +811,7 @@ const handleDelete = (row: { [key: string]: any }) => {
};
/**
* 强制用户下线
* Принудительное отключение пользователя
* @param row
*/
const handleKick = (row: { [key: string]: any }) => {
@@ -823,7 +823,7 @@ const handleKick = (row: { [key: string]: any }) => {
};
/**
* 解除下线状态
* Снятие статуса отключения
* @param row
*/
const confirmReleaseKick = (row: { [key: string]: any }) => {
@@ -834,7 +834,7 @@ const confirmReleaseKick = (row: { [key: string]: any }) => {
};
/**
* 关闭用户弹窗
* Закрытие окна пользователя
*/
const closeDialog = () => {
dialog.value.visible = false;
@@ -847,7 +847,7 @@ const closeDialog = () => {
};
/**
* 关闭下线弹窗
* Закрытие окна отключения
*/
const closeDialogKick = () => {
dialogKick.value.visible = false;
@@ -856,7 +856,7 @@ const closeDialogKick = () => {
};
/**
* 导入
* Импорт
*/
const handleImport = (params: UploadRequestOptions) => {
if (state.fileList.length > 0) {
@@ -881,7 +881,7 @@ const beforeImport = (file: UploadRawFile) => {
};
/**
* 导出
* Экспорт
*/
const handleExport = () => {
exportAccountApi().then((res) => {
@@ -894,7 +894,7 @@ const handleExport = () => {
a.href = url;
let dis = res.headers["content-disposition"];
a.download = dis.split("attachment; filename=")[1];
// 模拟点击下载
// Имитация клика для скачивания
a.click();
window.URL.revokeObjectURL(url);
ElMessage.success(t("common.success"));
@@ -955,7 +955,7 @@ const resetTraffic = async (row: { [key: string]: any }) => {
};
onMounted(() => {
// 初始化用户列表数据
// Инициализация списка пользователей
handleQuery();
if (route.query.focus === "change-pass") {
nextTick(() => {
@@ -973,3 +973,5 @@ onMounted(() => {
}
});
</script>
@@ -406,7 +406,7 @@ const handleExport = async () => {
a.href = url;
let dis = response.headers["content-disposition"];
a.download = dis.split("attachment; filename=")[1];
// 模拟点击下载
// Имитация клика для скачивания
a.click();
window.URL.revokeObjectURL(url);
ElMessage.success(t("common.success"));
@@ -484,3 +484,5 @@ onMounted(() => {
margin: 0 auto;
}
</style>
+3 -1
View File
@@ -1,4 +1,4 @@
<!-- setup 无法设置组件名称组件名称keepAlive必须 -->
<!-- setup не задаёт имя компонента, а имя нужно для keepAlive -->
<script lang="ts">
export default {
name: "Page401",
@@ -95,3 +95,5 @@ function back() {
}
}
</style>
+3 -1
View File
@@ -1,4 +1,4 @@
<!-- setup 无法设置组件名称组件名称keepAlive必须 -->
<!-- setup не задаёт имя компонента, а имя нужно для keepAlive -->
<script lang="ts">
export default {
name: "Page404",
@@ -269,3 +269,5 @@ function message() {
}
}
</style>
@@ -377,3 +377,5 @@ const closeDialog = (): void => {
flex-wrap: wrap;
}
</style>
@@ -1624,3 +1624,5 @@ onMounted(() => {
margin: 0 auto;
}
</style>
@@ -337,3 +337,5 @@ onMounted(() => {
}
}
</style>
@@ -94,7 +94,7 @@ const handleExport = async () => {
a.href = url;
let dis = response.headers["content-disposition"];
a.download = dis.split("attachment; filename=")[1];
// 模拟点击下载
// Имитация клика для скачивания
a.click();
window.URL.revokeObjectURL(url);
ElMessage.success(t("common.success"));
@@ -109,3 +109,5 @@ onMounted(() => {
</script>
<style lang="scss" scoped></style>
+3 -1
View File
@@ -94,7 +94,7 @@ const handleExport = async () => {
a.href = url;
let dis = response.headers["content-disposition"];
a.download = dis.split("attachment; filename=")[1];
// 模拟点击下载
// Имитация клика для скачивания
a.click();
window.URL.revokeObjectURL(url);
ElMessage.success(t("common.success"));
@@ -109,3 +109,5 @@ onMounted(() => {
</script>
<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 SvgIcon from "@/components/SvgIcon/index.vue";
// 状态管理依赖
// Зависимость store
import { useAccountStore } from "@/store/modules/account";
// API依赖
// Зависимость API
import { LocationQuery, LocationQueryValue, useRoute } from "vue-router";
import { AccountLoginDto } from "@/api/account/types";
@@ -87,25 +87,25 @@ const accountStore = useAccountStore();
const route = useRoute();
/**
* 按钮loading
* Состояние загрузки кнопки
*/
const loading = ref(false);
/**
* 是否大写锁定
* Включён ли Caps Lock
*/
const isCapslock = ref(false);
/**
* 密码是否可见
* Видимость пароля
*/
const passVisible = ref(false);
/**
* 登录表单引用
* Ссылка на форму входа
*/
const loginFormRef = ref(ElForm);
/**
* 登录表单
* Форма входа
*/
const loginForm = ref<AccountLoginDto>({
username: "",
@@ -140,7 +140,7 @@ const loginRules = {
};
/**
* 检查输入大小写状态
* Проверка состояния регистра ввода
*/
const checkCapslock = (e: any) => {
const { key } = e;
@@ -148,7 +148,7 @@ const checkCapslock = (e: any) => {
};
/**
* 登录
* Вход
*/
const handleLogin = () => {
loginFormRef.value.validate((valid: boolean) => {
@@ -208,7 +208,7 @@ const handleLogin = () => {
.el-input {
background: transparent;
// 子组件 scoped 无效,使用 :deep
// scoped не действует на дочерний компонент, используется :deep
:deep(.el-input__wrapper) {
padding: 0;
background: transparent;
@@ -226,7 +226,7 @@ const handleLogin = () => {
-webkit-text-fill-color: #fff !important;
}
// 设置输入框自动填充的延迟属性
// Настройка задержки автозаполнения поля ввода
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus,
@@ -238,3 +238,5 @@ const handleLogin = () => {
}
}
</style>
@@ -220,3 +220,5 @@ onMounted(() => {
}
}
</style>
@@ -13,3 +13,5 @@ const { path } = params;
router.replace({ path: "/" + path, query });
</script>