Русифицировать README и интерфейс HY2XS admin
This commit is contained in:
@@ -9,3 +9,5 @@ const appStore = useAppStore();
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -77,3 +77,5 @@ export interface KickAccountForm {
|
||||
ids: number[];
|
||||
kickUtilTime: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -110,3 +110,5 @@ export function uploadCertFileApi(data: FormData): AxiosPromise<string> {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -300,3 +300,5 @@ export interface Hysteria2AcmePathVo {
|
||||
crtPath: string;
|
||||
keyPath: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -38,3 +38,5 @@ export function hysteria2UrlApi(
|
||||
params: dto,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,3 +24,5 @@ export interface Hysteria2UrlVo {
|
||||
url: string;
|
||||
qrCode: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,3 +33,5 @@ export function exportLogApi(data: LogExportDto): AxiosPromise {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,5 @@ export interface LogHysteria2Vo {
|
||||
msg: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,3 +15,5 @@ export function monitorHysteria2Api(): AxiosPromise<Hysteria2MonitorVo> {
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,5 @@ const i18n = createI18n({
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -256,3 +256,5 @@ export default {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
export default {
|
||||
// 路由国际化
|
||||
route: {
|
||||
account: "账户",
|
||||
accountList: "账户管理",
|
||||
hysteria: "Hysteria",
|
||||
hysteriaList: "Hysteria 管理",
|
||||
config: "系统",
|
||||
configList: "系统设置",
|
||||
monitor: "监控",
|
||||
monitorSystem: "系统监控",
|
||||
log: "日志",
|
||||
logSystem: "系统日志",
|
||||
logHysteria: "Hysteria 日志",
|
||||
info: "信息",
|
||||
infoAccount: "账户信息",
|
||||
},
|
||||
// 登录页面国际化
|
||||
login: {
|
||||
title: "HY2XS admin",
|
||||
username: "用户名",
|
||||
password: "密码",
|
||||
login: "登 录",
|
||||
},
|
||||
// 导航栏国际化
|
||||
navbar: {
|
||||
logout: "注销",
|
||||
},
|
||||
common: {
|
||||
id: "编号",
|
||||
createTime: "创建时间",
|
||||
operate: "操作",
|
||||
edit: "编辑",
|
||||
delete: "删除",
|
||||
deleted: "状态",
|
||||
all: "全部",
|
||||
enable: "正常",
|
||||
disable: "禁用",
|
||||
search: "搜索",
|
||||
reset: "重设",
|
||||
add: "新增",
|
||||
confirm: "确定",
|
||||
cancel: "取消",
|
||||
copySuccess: "复制成功",
|
||||
subscribe: "订阅链接",
|
||||
subscribeQrCode: "订阅二维码",
|
||||
nodeUrl: "节点 URL",
|
||||
nodeQrCode: "节点二维码",
|
||||
resetTraffic: "重设流量",
|
||||
import: "导入",
|
||||
export: "导出",
|
||||
save: "保存",
|
||||
update: "更新",
|
||||
downloadSuccess: "下载成功",
|
||||
wait: "正在更换版本,请等待一会儿",
|
||||
enableSuccess: "Hysteria2 启动 !!",
|
||||
disableSuccess: "Hysteria2 已关闭",
|
||||
success: "成功",
|
||||
refresh: "刷新",
|
||||
yes: "是",
|
||||
no: "否",
|
||||
securityRisk: `安全隐患`,
|
||||
defaultPassTip: `请尽快修改默认登录密码,建议设置强密码以保护您的账户安全。<a href="/#/account/list?focus=change-pass" style="color: #00BFFF">点击这里</a>修改`,
|
||||
noHttpsTip: `您的网站未启用 HTTPS,数据传输不安全,请尽快开启 HTTPS 以保护用户信息。<a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">点击这里</a>开启`,
|
||||
},
|
||||
info: {
|
||||
expireTime: "年-月-日 时:分:秒",
|
||||
greeting1: "微凉扑面,清新的空气,唤醒一天的活力🌅!",
|
||||
greeting2: "上午好,",
|
||||
greeting3: "下午好,",
|
||||
greeting4: "晚上好,",
|
||||
greeting5: "我愿成为流星,划破黑夜,只为照亮你的梦境,晚安🌛!",
|
||||
},
|
||||
account: {
|
||||
remark: "备注",
|
||||
username: "用户名",
|
||||
pass: "登录密码",
|
||||
conPass: "连接密码",
|
||||
quota: "配额",
|
||||
download: "下载",
|
||||
upload: "上传",
|
||||
expireTime: "过期时间",
|
||||
kickUtilTimeLast: "下线剩余时间",
|
||||
kickUtilTime: "下线截止时间",
|
||||
deviceNo: "限制设备数",
|
||||
onlineStatus: "在线状态",
|
||||
online: "在线",
|
||||
offline: "离线",
|
||||
device: "在线设备数",
|
||||
role: "角色",
|
||||
unit: "单位",
|
||||
loginAt: "最近登录时间",
|
||||
conAt: "最近连接时间",
|
||||
createTime: "注册时间",
|
||||
releaseSuccess: "解除成功",
|
||||
kick: "下线",
|
||||
kickTip: "强制用户下线",
|
||||
releaseKick: "解除",
|
||||
releaseKickTip: "解除下线状态",
|
||||
},
|
||||
config: {
|
||||
huiWebPort: "HY2XS admin Web 端口",
|
||||
huiWebContext: "HY2XS admin Web 上下文",
|
||||
hysteria2TrafficTime: "Hysteria2 流量倍数",
|
||||
huiCrtPath: "HY2XS admin CRT 证书路径",
|
||||
huiKeyPath: "HY2XS admin KEY 证书路径",
|
||||
uploadCrtFile: "上传 CRT 证书",
|
||||
uploadKeyFile: "上传 KEY 证书",
|
||||
restartServer: "重启面板",
|
||||
restartTip: "正在重启,请刷新",
|
||||
useHysteria2Cert: "使用 Hysteria2 的证书",
|
||||
huiHttps: "面板开启 https",
|
||||
resetTrafficCron: "重设流量计划任务",
|
||||
resetTrafficCronTip:
|
||||
"计划任务表达式,参考:https://pkg.go.dev/github.com/robfig/cron/v3",
|
||||
resetTrafficMonth: "每月运行一次,每月第一天午夜",
|
||||
resetTrafficWeek: "每周运行一次,周六/周日午夜",
|
||||
},
|
||||
monitor: {
|
||||
huiVersion: "HY2XS admin 版本",
|
||||
cpuPercent: "CPU 使用率",
|
||||
memPercent: "内存使用率",
|
||||
diskPercent: "磁盘使用率",
|
||||
hysteria2UserTotal: "在线用户数",
|
||||
hysteria2DeviceTotal: "在线设备数",
|
||||
hysteria2Version: "Hysteria2 版本",
|
||||
hysteria2Running: "Hysteria2 状态",
|
||||
hysteria2RunningTrue: "运行",
|
||||
hysteria2RunningFalse: "停止",
|
||||
},
|
||||
log: {
|
||||
numLine: "显示行数",
|
||||
},
|
||||
hysteria: {
|
||||
enable: "开启",
|
||||
disable: "关闭",
|
||||
addConfigItem: "添加配置项",
|
||||
hysteria2Version: "Hysteria2 版本",
|
||||
hysteria2Running: "Hysteria2 状态",
|
||||
hysteria2ChangeVersion: "切换",
|
||||
addOutbound: "添加出站规则",
|
||||
extension: "扩展",
|
||||
listen: "监听地址",
|
||||
tls: "TLS",
|
||||
obfs: "混淆",
|
||||
quic: "QUIC 参数",
|
||||
bandwidth: "带宽",
|
||||
speedTest: "速度测试",
|
||||
udp: "UDP",
|
||||
resolver: "DNS 解析",
|
||||
sniff: "协议嗅探 (Sniff)",
|
||||
acl: "ACL",
|
||||
outbounds: "出站规则",
|
||||
http: "流量统计 API",
|
||||
masquerade: "伪装",
|
||||
config: {
|
||||
enable: "开启/关闭",
|
||||
remark: "别名",
|
||||
portHopping:
|
||||
"端口跳跃,多个单端口:1234,5678,9012;端口范围:20000-50000;两者的组合:1234,5000-6000,7044,8000-9000",
|
||||
clashExtension: "Clash 订阅扩展",
|
||||
listen:
|
||||
"当只有端口没有 IP 地址时,服务器将监听所有可用的 IPv4 和 IPv6 地址。要仅监听 IPv4,可以使用 0.0.0.0:443。要仅监听 IPv6,可以使用 [::]:443。",
|
||||
tlsType: "TLS 类型",
|
||||
tls: {
|
||||
cert: "CERT 路径",
|
||||
key: "KEY 路径",
|
||||
sniGuard:
|
||||
"验证客户端发送的 SNI。 与证书信息匹配时才建立连接, 否则终止 TLS 握手。 设置为 strict 以启用该功能。 设置为 disable 以禁用该功能。 默认为 dns-san, 仅当证书中包含「证书主题背景的备用名称」扩展且该扩展中包含域名时才启用该功能。",
|
||||
},
|
||||
acme: {
|
||||
domains: "域名",
|
||||
email: "邮箱",
|
||||
ca: "要使用的 CA。可以是 letsencrypt 或 zerossl。",
|
||||
listenHost:
|
||||
"用于 ACME 服务器验证的监听地址(不含端口)。默认监听所有可用的地址。",
|
||||
dir: "存储 ACME 账户密钥和证书的目录。",
|
||||
type: "ACME 验证类型。可以是 http, tls 或 dns。",
|
||||
http: {
|
||||
altPort:
|
||||
"用于 HTTP 挑战的监听端口。 (注意: 改为非 80 需要另行配置端口转发或者 HTTP 反向代理,否则证书会签署失败!)",
|
||||
},
|
||||
tls: {
|
||||
altPort:
|
||||
"用于 TLS-ALPN 挑战的监听端口。 (注意: 改为非 443 需要另行配置端口转发或者 SNI Proxy,否则证书会签署失败!)",
|
||||
},
|
||||
dns: {
|
||||
name: "DNS 提供商。详细信息请参考 ACME DNS 配置。",
|
||||
config: "ACME DNS 配置",
|
||||
},
|
||||
disableHTTP: "禁用 HTTP 挑战。",
|
||||
disableTLSALPN: "禁用 TLS-ALPN 挑战。",
|
||||
altHTTPPort:
|
||||
"用于 HTTP 挑战的监听端口。 (注意: 改为非 80 需要另行配置端口转发或者 HTTP 反向代理,否则证书会签署失败!)",
|
||||
altTLSALPNPort:
|
||||
"用于 TLS-ALPN 挑战的监听端口。 (注意: 改为非 443 需要另行配置端口转发或者 SNI Proxy,否则证书会签署失败!)",
|
||||
},
|
||||
obfs: {
|
||||
type: "类型",
|
||||
salamander: {
|
||||
password: "替换为你的混淆密码。",
|
||||
},
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: "初始的 QUIC 流接收窗口大小。",
|
||||
maxStreamReceiveWindow: "最大的 QUIC 流接收窗口大小。",
|
||||
initConnReceiveWindow: "初始的 QUIC 连接接收窗口大小。",
|
||||
maxConnReceiveWindow: "最大的 QUIC 连接接收窗口大小。",
|
||||
maxIdleTimeout:
|
||||
"最长空闲超时时间。服务器会在多长时间没有收到任何客户端数据后关闭连接。",
|
||||
maxIncomingStreams: "最大并发传入流的数量。",
|
||||
disablePathMTUDiscovery: "禁用 MTU 探测。",
|
||||
},
|
||||
bandwidth: {
|
||||
up: "上传",
|
||||
down: "下载",
|
||||
},
|
||||
ignoreClientBandwidth: "忽略客户端带宽设置",
|
||||
speedTest: "speedTest 启用后,服务端将允许客户端进行下载和上传速度测试。",
|
||||
disableUDP: "disableUDP 启用后服务端禁用 UDP 转发,只支持 TCP。",
|
||||
udpIdleTimeout:
|
||||
"udpIdleTimeout 用于指定服务器对于每个 UDP 会话,在没有流量时保持本地 UDP 端口的时间长度。概念上与 NAT 的 UDP 会话超时时间相似。",
|
||||
resolver: {
|
||||
type: "类型",
|
||||
tcp: {
|
||||
addr: "TCP DNS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
},
|
||||
udp: {
|
||||
addr: "UDP DNS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
},
|
||||
tls: {
|
||||
addr: "DNS over TLS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
sni: "DNS over TLS 服务器的 SNI。",
|
||||
insecure: "禁用 TLS 证书验证。",
|
||||
},
|
||||
https: {
|
||||
addr: "DNS over HTTPS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
sni: "DNS over TLS 服务器的 SNI。",
|
||||
insecure: "禁用 TLS 证书验证。",
|
||||
},
|
||||
},
|
||||
sniff: {
|
||||
enable: "是否启用协议嗅探。",
|
||||
timeout:
|
||||
"嗅探超时时间。如果超过这个时间仍然无法确定协议/获取域名,将使用原地址发起连接。",
|
||||
rewriteDomain:
|
||||
"是否重写已经是域名的请求。如果启用,对于目标地址已经是域名的请求,仍会进行嗅探。",
|
||||
tcpPorts: "TCP 端口列表。只有这些端口的 TCP 请求会被嗅探。",
|
||||
udpPorts: "UDP 端口列表。只有这些端口的 UDP 请求会被嗅探。",
|
||||
},
|
||||
aclType: "ACL 类型",
|
||||
acl: {
|
||||
file: "ACL 文件的路径。",
|
||||
inline: "内联 ACL 规则的列表。",
|
||||
geoip:
|
||||
"可选。取消注释以启用。GeoIP 数据库文件的路径。如果省略这个字段,Hysteria 会自动下载最新的数据库到工作目录。",
|
||||
geosite:
|
||||
"可选。取消注释以启用。GeoSite 数据库文件的路径。如果省略这个字段,Hysteria 会自动下载最新的数据库到工作目录。",
|
||||
geoUpdateInterval:
|
||||
"可选。GeoIP/GeoSite 数据库刷新的间隔。默认为 168 小时(1 周)。仅在 GeoIP/GeoSite 数据库是自动下载的情况下生效。",
|
||||
},
|
||||
outbounds: {
|
||||
name: "出站规则的名称。在 ACL 中使用。",
|
||||
type: "类型",
|
||||
socks5: {
|
||||
addr: "SOCKS5 代理地址。",
|
||||
username: "可选。SOCKS5 代理用户名。",
|
||||
password: "可选。SOCKS5 代理密码。",
|
||||
},
|
||||
http: {
|
||||
url: "HTTP/HTTPS 代理 URL。(可以是 http:// 或 https:// 开头)",
|
||||
insecure: "可选。禁用 TLS 证书验证。仅适用于 HTTPS 代理。",
|
||||
},
|
||||
direct: {
|
||||
mode: "类型",
|
||||
bindIPv4: "要绑定的本地 IPv4 地址。",
|
||||
bindIPv6: "要绑定的本地 IPv6 地址。",
|
||||
bindDevice: "要绑定的本地网卡。",
|
||||
fastOpen: "启用 TCP 快速打开。",
|
||||
},
|
||||
},
|
||||
trafficStats: {
|
||||
listen: "监听地址。",
|
||||
},
|
||||
masquerade: {
|
||||
type: "类型",
|
||||
file: {
|
||||
dir: "用于提供文件的目录。",
|
||||
},
|
||||
proxy: {
|
||||
url: "要代理的网站的 URL。",
|
||||
rewriteHost:
|
||||
"是否重写 Host 头以匹配被代理的网站。如果目标网站通过 Host 识别请求的网站,这个选项是必须的。",
|
||||
insecure: "禁用对代理网站的 TLS 验证。",
|
||||
},
|
||||
string: {
|
||||
content: "要返回的字符串。",
|
||||
headers: "可选。要返回的 HTTP 头列表。",
|
||||
statusCode: "可选。要返回的 HTTP 状态码。默认为 200。",
|
||||
},
|
||||
listenHTTP: "HTTP (TCP) 监听地址。",
|
||||
listenHTTPS: "HTTPS (TCP) 监听地址。",
|
||||
forceHTTPS:
|
||||
"是否强制使用 HTTPS。如果启用,HTTP 请求将被重定向到 HTTPS。",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -43,3 +43,5 @@ const tagsViewStore = useTagsViewStore();
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -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,9 +3,11 @@ import { createPinia } from "pinia";
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
// 全局注册 store
|
||||
// Глобальная регистрация store
|
||||
export function setupStore(app: App<Element>) {
|
||||
app.use(store);
|
||||
}
|
||||
|
||||
export { store };
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -31,3 +31,5 @@ html.dark {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,3 +15,5 @@
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -203,3 +203,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 导出 variables.module.scss 变量提供给TypeScript使用
|
||||
// Экспорт variables.module.scss для использования в TypeScript
|
||||
:export {
|
||||
menuBg: $menuBg;
|
||||
menuText: $menuText;
|
||||
menuActiveText: $menuActiveText;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 全局SCSS变量
|
||||
// Глобальные SCSS-переменные
|
||||
|
||||
:root {
|
||||
--menuBg: #304156;
|
||||
@@ -20,3 +20,5 @@ $subMenuActiveText: var(--subMenuActiveText);
|
||||
$subMenuHover: var(--subMenuHover);
|
||||
|
||||
$sideBarWidth: 210px;
|
||||
|
||||
|
||||
|
||||
Vendored
+3
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Vendored
+2
@@ -16,3 +16,5 @@ declare global {
|
||||
}
|
||||
}
|
||||
export {};
|
||||
|
||||
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -37,3 +37,5 @@ export function isExternal(path: string) {
|
||||
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
|
||||
return isExternal;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -67,3 +67,5 @@ export const scrollTo = (to: number, duration: number, callback?: any) => {
|
||||
};
|
||||
animateScroll();
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- setup 无法设置组件名称,组件名称keepAlive必须 -->
|
||||
<!-- setup не задаёт имя компонента, а имя нужно для keepAlive -->
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "Page401",
|
||||
@@ -95,3 +95,5 @@ function back() {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user