Подготовить HY2XS к production-сборке
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ElConfigProvider } from "element-plus";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
const appStore = useAppStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="appStore.locale" :size="appStore.size">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
@@ -0,0 +1,155 @@
|
||||
import request from "@/utils/request";
|
||||
import { AxiosPromise } from "axios";
|
||||
import {
|
||||
AccountSaveDto,
|
||||
AccountInfo,
|
||||
AccountLoginDto,
|
||||
AccountLoginVo,
|
||||
AccountPageDto,
|
||||
AccountUpdateDto,
|
||||
AccountVo,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
export function getAccountApi(data: IdDto): AxiosPromise<AccountVo> {
|
||||
return request({
|
||||
url: "/account/getAccount",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export function saveAccountApi(data: AccountSaveDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/saveAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前
|
||||
*/
|
||||
export function getAccountInfoApi(): AxiosPromise<AccountInfo> {
|
||||
return request({
|
||||
url: "/account/getAccountInfo",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页
|
||||
* @param data
|
||||
*/
|
||||
export function pageAccountApi(
|
||||
data: AccountPageDto
|
||||
): AxiosPromise<PageVo<AccountVo>> {
|
||||
return request({
|
||||
url: "/account/pageAccount",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export function deleteAccountApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/deleteAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改
|
||||
* @param data
|
||||
*/
|
||||
export function updateAccountApi(data: AccountUpdateDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/updateAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重设流量
|
||||
* @param data
|
||||
*/
|
||||
export function resetTrafficApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/resetTraffic",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param data
|
||||
*/
|
||||
export function loginApi(data: AccountLoginDto): AxiosPromise<AccountLoginVo> {
|
||||
return request({
|
||||
url: "/auth/login",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入
|
||||
*/
|
||||
export function importAccountApi(data: FormData): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/importAccount",
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
export function exportAccountApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/exportAccount",
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除下线状态
|
||||
*/
|
||||
export function releaseKickAccountApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/releaseKickAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否默认密码
|
||||
* @param data
|
||||
*/
|
||||
export function verifyDefaultPassApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/verifyDefaultPass",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
export interface AccountPageDto extends BaseDto {
|
||||
username?: string;
|
||||
deleted?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface AccountUpdateDto extends IdDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
conPass: string;
|
||||
quota: number;
|
||||
expireTime: number;
|
||||
deviceNo: number;
|
||||
deleted: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface AccountSaveDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
conPass: string;
|
||||
quota: number;
|
||||
expireTime: number;
|
||||
deviceNo: number;
|
||||
deleted: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface AccountLoginDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
}
|
||||
|
||||
export interface AccountVo extends IdDto {
|
||||
username: string;
|
||||
quota: number;
|
||||
download: number;
|
||||
upload: number;
|
||||
expireTime: number;
|
||||
kickUtilTime: number;
|
||||
deviceNo: number;
|
||||
role: string;
|
||||
deleted: number;
|
||||
createTime: string;
|
||||
|
||||
online: boolean;
|
||||
device: number;
|
||||
|
||||
loginAt: number;
|
||||
conAt: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface AccountLoginVo {
|
||||
accessToken: string;
|
||||
tokenType: string;
|
||||
}
|
||||
|
||||
export interface AccountInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface AccountForm extends IdDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
conPass: string;
|
||||
quota: number;
|
||||
expireTime: number;
|
||||
deviceNo: number;
|
||||
deleted: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface KickAccountForm {
|
||||
ids: number[];
|
||||
kickUtilTime: number;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
import request from "@/utils/request";
|
||||
import {
|
||||
ConfigDto,
|
||||
ConfigsDto,
|
||||
ConfigUpdateDto,
|
||||
ConfigVo,
|
||||
Hysteria2AcmePathVo,
|
||||
Hysteria2ServerConfig,
|
||||
} from "@/api/config/types";
|
||||
|
||||
export function getHysteria2ConfigApi(): AxiosPromise<Hysteria2ServerConfig> {
|
||||
return request({
|
||||
url: "/config/getHysteria2Config",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function updateHysteria2ConfigApi(
|
||||
data: Hysteria2ServerConfig
|
||||
): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/updateHysteria2Config",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getConfigApi(data: ConfigDto): AxiosPromise<ConfigVo> {
|
||||
return request({
|
||||
url: "/config/getConfig",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function listConfigApi(data: ConfigsDto): AxiosPromise<Array<ConfigVo>> {
|
||||
return request({
|
||||
url: "/config/listConfig",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateConfigsApi(data: ConfigUpdateDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/updateConfigs",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function exportConfigApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/exportConfig",
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
export function importConfigApi(data: FormData): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/importConfig",
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function exportHysteria2ConfigApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/exportHysteria2Config",
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
export function importHysteria2ConfigApi(data: FormData): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/importHysteria2Config",
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function hysteria2AcmePathApi(): AxiosPromise<Hysteria2AcmePathVo> {
|
||||
return request({
|
||||
url: "/config/hysteria2AcmePath",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function restartServerApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/config/restartServer",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadCertFileApi(data: FormData): AxiosPromise<string> {
|
||||
return request({
|
||||
url: "/config/uploadCertFile",
|
||||
method: "post",
|
||||
data,
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
export interface ConfigDto {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface ConfigsDto {
|
||||
keys: Array<string>;
|
||||
}
|
||||
|
||||
export interface ConfigVo {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ConfigsUpdateDto {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ConfigUpdateDto {
|
||||
configUpdateDtos: Array<ConfigsUpdateDto>;
|
||||
}
|
||||
|
||||
export interface Hysteria2ServerConfig {
|
||||
listen: string;
|
||||
tls?: {
|
||||
cert: string;
|
||||
key: string;
|
||||
sniGuard?: string;
|
||||
};
|
||||
acme?: {
|
||||
domains: string[];
|
||||
email: string;
|
||||
ca: string;
|
||||
listenHost: string;
|
||||
dir: string;
|
||||
type?: string;
|
||||
http?: {
|
||||
altPort: number;
|
||||
};
|
||||
tls?: {
|
||||
altPort: number;
|
||||
};
|
||||
dns?: {
|
||||
name: string;
|
||||
config: { [key: string]: string };
|
||||
};
|
||||
disableHTTP: boolean;
|
||||
disableTLSALPN: boolean;
|
||||
altHTTPPort: number;
|
||||
altTLSALPNPort: number;
|
||||
};
|
||||
obfs?: {
|
||||
type: string;
|
||||
salamander: {
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
quic?: {
|
||||
initStreamReceiveWindow?: number;
|
||||
maxStreamReceiveWindow?: number;
|
||||
initConnReceiveWindow?: number;
|
||||
maxConnReceiveWindow?: number;
|
||||
maxIdleTimeout?: string;
|
||||
maxIncomingStreams?: number;
|
||||
disablePathMTUDiscovery?: boolean;
|
||||
};
|
||||
bandwidth?: {
|
||||
up: string;
|
||||
down: string;
|
||||
};
|
||||
ignoreClientBandwidth?: boolean;
|
||||
speedTest?: boolean;
|
||||
disableUDP?: boolean;
|
||||
udpIdleTimeout?: string;
|
||||
resolver?: {
|
||||
type: string;
|
||||
tcp?: {
|
||||
addr: string;
|
||||
timeout: string;
|
||||
};
|
||||
udp?: {
|
||||
addr: string;
|
||||
timeout: string;
|
||||
};
|
||||
tls?: {
|
||||
addr: string;
|
||||
timeout: string;
|
||||
sni: string;
|
||||
insecure: boolean;
|
||||
};
|
||||
https?: {
|
||||
addr: string;
|
||||
timeout: string;
|
||||
sni: string;
|
||||
insecure: boolean;
|
||||
};
|
||||
};
|
||||
sniff?: {
|
||||
enable: boolean;
|
||||
timeout: string;
|
||||
rewriteDomain: boolean;
|
||||
tcpPorts?: string;
|
||||
udpPorts?: string;
|
||||
};
|
||||
acl?: {
|
||||
file?: string;
|
||||
inline?: string[];
|
||||
geoip?: string;
|
||||
geosite?: string;
|
||||
geoUpdateInterval?: string;
|
||||
};
|
||||
outbounds?: Hysteria2ServerConfigOutbound[];
|
||||
trafficStats: {
|
||||
listen: string;
|
||||
};
|
||||
masquerade?: {
|
||||
type: string;
|
||||
file?: {
|
||||
dir: string;
|
||||
};
|
||||
proxy?: {
|
||||
url: string;
|
||||
rewriteHost: boolean;
|
||||
insecure: boolean;
|
||||
};
|
||||
string?: {
|
||||
content: string;
|
||||
headers?: { [key: string]: string };
|
||||
statusCode?: number;
|
||||
};
|
||||
listenHTTP?: string;
|
||||
listenHTTPS?: string;
|
||||
forceHTTPS?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = {
|
||||
listen: ":443",
|
||||
tls: {
|
||||
cert: "",
|
||||
key: "",
|
||||
sniGuard: "",
|
||||
},
|
||||
acme: {
|
||||
domains: [],
|
||||
email: "",
|
||||
ca: "zerossl",
|
||||
listenHost: "0.0.0.0",
|
||||
dir: "my_acme_dir",
|
||||
type: "",
|
||||
http: {
|
||||
altPort: 8888,
|
||||
},
|
||||
tls: {
|
||||
altPort: 44333,
|
||||
},
|
||||
dns: {
|
||||
name: "gomommy",
|
||||
config: {},
|
||||
},
|
||||
disableHTTP: false,
|
||||
disableTLSALPN: false,
|
||||
altHTTPPort: 80,
|
||||
altTLSALPNPort: 443,
|
||||
},
|
||||
obfs: {
|
||||
type: "salamander",
|
||||
salamander: {
|
||||
password: "cry_me_a_r1ver",
|
||||
},
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: 8388608,
|
||||
maxStreamReceiveWindow: 8388608,
|
||||
initConnReceiveWindow: 20971520,
|
||||
maxConnReceiveWindow: 20971520,
|
||||
maxIdleTimeout: "30s",
|
||||
maxIncomingStreams: 1024,
|
||||
disablePathMTUDiscovery: false,
|
||||
},
|
||||
bandwidth: {
|
||||
up: "1 gbps",
|
||||
down: "1 gbps",
|
||||
},
|
||||
ignoreClientBandwidth: false,
|
||||
speedTest: false,
|
||||
disableUDP: false,
|
||||
udpIdleTimeout: "60s",
|
||||
resolver: {
|
||||
type: "",
|
||||
tcp: {
|
||||
addr: "8.8.8.8:53",
|
||||
timeout: "4s",
|
||||
},
|
||||
udp: {
|
||||
addr: "8.8.4.4:53",
|
||||
timeout: "4s",
|
||||
},
|
||||
tls: {
|
||||
addr: "1.1.1.1:853",
|
||||
timeout: "10s",
|
||||
sni: "cloudflare-dns.com",
|
||||
insecure: false,
|
||||
},
|
||||
https: {
|
||||
addr: "1.1.1.1:443",
|
||||
timeout: "10s",
|
||||
sni: "cloudflare-dns.com",
|
||||
insecure: false,
|
||||
},
|
||||
},
|
||||
sniff: {
|
||||
enable: true,
|
||||
timeout: "2s",
|
||||
rewriteDomain: false,
|
||||
tcpPorts: "80,443,8000-9000",
|
||||
udpPorts: "all",
|
||||
},
|
||||
acl: {
|
||||
file: "",
|
||||
inline: [],
|
||||
geoip: "",
|
||||
geosite: "",
|
||||
geoUpdateInterval: "168h",
|
||||
},
|
||||
outbounds: [],
|
||||
trafficStats: {
|
||||
listen: ":9999",
|
||||
},
|
||||
masquerade: {
|
||||
type: "",
|
||||
file: {
|
||||
dir: "",
|
||||
},
|
||||
proxy: {
|
||||
url: "",
|
||||
rewriteHost: true,
|
||||
insecure: false,
|
||||
},
|
||||
string: {
|
||||
content: "hello stupid world",
|
||||
headers: {},
|
||||
statusCode: 200,
|
||||
},
|
||||
listenHTTP: ":80",
|
||||
listenHTTPS: ":443",
|
||||
forceHTTPS: true,
|
||||
},
|
||||
};
|
||||
|
||||
export interface Hysteria2ServerConfigOutbound {
|
||||
name: string;
|
||||
type: string;
|
||||
socks5?: {
|
||||
addr: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
http?: {
|
||||
url: string;
|
||||
insecure: boolean;
|
||||
};
|
||||
direct?: {
|
||||
mode: string;
|
||||
bindIPv4?: string;
|
||||
bindIPv6?: string;
|
||||
bindDevice?: string;
|
||||
fastOpen?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultHysteria2ServerConfigOutbound: Hysteria2ServerConfigOutbound =
|
||||
{
|
||||
name: "",
|
||||
type: "socks5",
|
||||
socks5: {
|
||||
addr: "",
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
},
|
||||
http: {
|
||||
url: "",
|
||||
insecure: false,
|
||||
},
|
||||
direct: {
|
||||
mode: "auto",
|
||||
bindIPv4: undefined,
|
||||
bindIPv6: undefined,
|
||||
bindDevice: undefined,
|
||||
fastOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
export interface Tab {
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export interface Hysteria2AcmePathVo {
|
||||
crtPath: string;
|
||||
keyPath: string;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
import { Hysteria2ServerConfig } from "@/api/config/types";
|
||||
import request from "@/utils/request";
|
||||
import {
|
||||
Hysteria2KickDto,
|
||||
Hysteria2SubscribeVo,
|
||||
Hysteria2SubscribeUrlDto,
|
||||
Hysteria2UrlDto,
|
||||
Hysteria2UrlVo,
|
||||
} from "@/api/hysteria2/types";
|
||||
|
||||
export function hysteria2KickApi(
|
||||
data: Hysteria2KickDto
|
||||
): AxiosPromise<Hysteria2ServerConfig> {
|
||||
return request({
|
||||
url: "/hysteria2/hysteria2Kick",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function hysteria2SubscribeUrlApi(
|
||||
dto: Hysteria2SubscribeUrlDto
|
||||
): AxiosPromise<Hysteria2SubscribeVo> {
|
||||
return request({
|
||||
url: "/hysteria2/hysteria2SubscribeUrl",
|
||||
method: "get",
|
||||
params: dto,
|
||||
});
|
||||
}
|
||||
|
||||
export function hysteria2UrlApi(
|
||||
dto: Hysteria2UrlDto
|
||||
): AxiosPromise<Hysteria2UrlVo> {
|
||||
return request({
|
||||
url: "/hysteria2/hysteria2Url",
|
||||
method: "get",
|
||||
params: dto,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export interface Hysteria2KickDto {
|
||||
ids: number[];
|
||||
kickUtilTime: number;
|
||||
}
|
||||
|
||||
export interface Hysteria2SubscribeUrlDto {
|
||||
accountId: number;
|
||||
protocol: string;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export interface Hysteria2UrlDto {
|
||||
accountId: number;
|
||||
hostname: string;
|
||||
}
|
||||
|
||||
export interface Hysteria2SubscribeVo {
|
||||
url: string;
|
||||
qrCode: string;
|
||||
}
|
||||
|
||||
|
||||
export interface Hysteria2UrlVo {
|
||||
url: string;
|
||||
qrCode: string;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
import request from "@/utils/request";
|
||||
import {
|
||||
LogDto,
|
||||
LogExportDto,
|
||||
LogHysteria2Vo,
|
||||
LogSystemVo,
|
||||
} from "@/api/log/types";
|
||||
|
||||
export function logSystemApi(data: LogDto): AxiosPromise<PageVo<LogSystemVo>> {
|
||||
return request({
|
||||
url: "/log/logSystem",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function logHysteria2Api(
|
||||
data: LogDto
|
||||
): AxiosPromise<PageVo<LogHysteria2Vo>> {
|
||||
return request({
|
||||
url: "/log/logHysteria2",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function exportLogApi(data: LogExportDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/log/exportLog",
|
||||
method: "post",
|
||||
data: data,
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface LogDto {
|
||||
numLine: number;
|
||||
}
|
||||
|
||||
export interface LogExportDto {
|
||||
option: number;
|
||||
}
|
||||
|
||||
export interface LogSystemVo {
|
||||
clientIp: string;
|
||||
latencyTime: string;
|
||||
level: string;
|
||||
msg: string;
|
||||
reqMethod: string;
|
||||
reqUri: string;
|
||||
statusCode: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export interface LogHysteria2Vo {
|
||||
level: string;
|
||||
msg: string;
|
||||
time: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
import request from "@/utils/request";
|
||||
import { Hysteria2MonitorVo, SystemMonitorVo } from "@/api/monitor/types";
|
||||
|
||||
export function monitorSystemApi(): AxiosPromise<SystemMonitorVo> {
|
||||
return request({
|
||||
url: "/monitor/monitorSystem",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function monitorHysteria2Api(): AxiosPromise<Hysteria2MonitorVo> {
|
||||
return request({
|
||||
url: "/monitor/monitorHysteria2",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface SystemMonitorVo {
|
||||
huiVersion: string;
|
||||
cpuPercent: number;
|
||||
diskPercent: number;
|
||||
memPercent: number;
|
||||
}
|
||||
|
||||
export interface Hysteria2MonitorVo {
|
||||
userTotal: number;
|
||||
deviceTotal: number;
|
||||
version: string;
|
||||
running: boolean;
|
||||
}
|
||||
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M19.41 18l8.29-8.29a1 1 0 0 0-1.41-1.41L18 16.59l-8.29-8.3a1 1 0 0 0-1.42 1.42l8.3 8.29l-8.3 8.29A1 1 0 1 0 9.7 27.7l8.3-8.29l8.29 8.29a1 1 0 0 0 1.41-1.41z" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 395 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M26 17H10a1 1 0 0 0 0 2h16a1 1 0 0 0 0-2z" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 279 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M7 12l7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 12l7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 12H7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" ></path><path d="M3 3v18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 647 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 20 20"><path d="M3 5h14V3H3v2zm12 8V7H5v6h10zM3 17h14v-2H3v2z" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g transform="translate(24 0) scale(-1 1)"><g fill="none"><path d="M7 12l7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 12l7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 12H7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path><path d="M3 3v18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 693 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714739812671" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2441" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M798.72 819.2H682.666667v-68.266667h116.053333c80.213333 0 145.066667-64.853333 145.066667-145.066666S878.933333 460.8 798.72 460.8H750.933333v-34.133333c0-131.413333-107.52-238.933333-238.933333-238.933334s-238.933333 107.52-238.933333 238.933334v34.133333h-37.546667c-80.213333 0-145.066667 64.853333-145.066667 145.066667S155.306667 750.933333 235.52 750.933333H341.333333v68.266667h-105.813333C117.76 819.2 20.48 723.626667 20.48 605.866667c0-107.52 80.213333-197.973333 184.32-211.626667C221.866667 240.64 353.28 119.466667 512 119.466667s288.426667 119.466667 305.493333 274.773333c109.226667 10.24 194.56 100.693333 194.56 211.626667-1.706667 117.76-97.28 213.333333-213.333333 213.333333z" fill="#191919" p-id="2442"></path><path d="M482.986667 515.413333h68.266666v307.2h-68.266666z" fill="#00C97C" p-id="2443"></path><path d="M515.413333 901.12l-150.186666-148.48 47.786666-49.493333 102.4 102.4 100.693334-102.4 47.786666 49.493333z" fill="#00C97C" p-id="2444"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714720229787" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8983" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M512 720m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="8984" fill="#000000"></path><path d="M480 416v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8z" p-id="8985" fill="#000000"></path><path d="M955.7 856l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48z m-783.5-27.9L512 239.9l339.8 588.2H172.2z" p-id="8986" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 768 B |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M49.217 41.329l-.136-35.24c-.06-2.715-2.302-4.345-5.022-4.405h-3.65c-2.712-.06-4.866 2.303-4.806 5.016l.152 19.164-24.151-23.79a6.698 6.698 0 0 0-9.499 0 6.76 6.76 0 0 0 0 9.526l23.93 23.713-18.345.074c-2.712-.069-5.228 1.813-5.64 5.02v3.462c.069 2.721 2.31 4.97 5.022 5.03l35.028-.207c.052.005.087.025.133.025l2.457.054a4.626 4.626 0 0 0 3.436-1.38c.88-.874 1.205-2.096 1.169-3.462l-.262-2.465c0-.048.182-.081.182-.136h.002zm52.523 51.212l18.32-.073c2.713.06 5.224-1.609 5.64-4.815v-3.462c-.068-2.722-2.317-4.97-5.021-5.04l-34.58.21c-.053 0-.086-.021-.138-.021l-2.451-.06a4.64 4.64 0 0 0-3.445 1.381c-.885.868-1.201 2.094-1.174 3.46l.27 2.46c.005.06-.177.095-.177.141l.141 34.697c.069 2.713 2.31 4.338 5.022 4.397l3.45.006c2.705.062 4.867-2.31 4.8-5.026l-.153-18.752 24.151 23.946a6.69 6.69 0 0 0 9.494 0 6.747 6.747 0 0 0 0-9.523L101.74 92.54v.001zM48.125 80.662a4.636 4.636 0 0 0-3.437-1.382l-2.457.06c-.05 0-.082.022-.137.022l-35.025-.21c-2.712.07-4.957 2.318-5.022 5.04v3.462c.409 3.206 2.925 4.874 5.633 4.814l18.554.06-24.132 23.928c-2.62 2.626-2.62 6.89 0 9.524a6.694 6.694 0 0 0 9.496 0l24.155-23.79-.155 18.866c-.06 2.722 2.094 5.093 4.801 5.025h3.65c2.72-.069 4.962-1.685 5.022-4.406l.141-34.956c0-.05-.182-.082-.182-.136l.262-2.46c.03-1.366-.286-2.592-1.166-3.46h-.001zM80.08 47.397a4.62 4.62 0 0 0 3.443 1.374l2.45-.054c.055 0 .088-.02.143-.028l35.08.21c2.712-.062 4.953-2.312 5.021-5.033l.009-3.463c-.417-3.211-2.937-5.084-5.64-5.025l-18.615-.073 23.917-23.715c2.63-2.623 2.63-6.879.008-9.513a6.691 6.691 0 0 0-9.494 0L92.251 26.016l.155-19.312c.065-2.713-2.097-5.085-4.802-5.025h-3.45c-2.713.069-4.954 1.693-5.022 4.406l-.139 35.247c0 .054.18.088.18.136l-.267 2.465c-.028 1.366.288 2.588 1.174 3.463v.001z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714739827633" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2765" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M512 989.866667C249.173333 989.866667 34.133333 774.826667 34.133333 512S249.173333 34.133333 512 34.133333s477.866667 215.04 477.866667 477.866667-215.04 477.866667-477.866667 477.866667z m0-887.466667C286.72 102.4 102.4 286.72 102.4 512s184.32 409.6 409.6 409.6 409.6-184.32 409.6-409.6S737.28 102.4 512 102.4z" fill="#191919" p-id="2766"></path><path d="M363.52 725.333333l-44.373333-51.2 158.72-143.36V238.933333h68.266666v322.56z" fill="#00C97C" p-id="2767"></path></svg>
|
||||
|
After Width: | Height: | Size: 807 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
|
||||
|
After Width: | Height: | Size: 944 B |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M38.47 52L52 38.462l-23.648-23.67L43.209 0H.035L0 43.137l14.757-14.865L38.47 52zm74.773 47.726L89.526 76 76 89.536l23.648 23.672L84.795 128h43.174L128 84.863l-14.757 14.863zM89.538 52l23.668-23.648L128 43.207V.038L84.866 0 99.73 14.76 76 38.472 89.538 52zM38.46 76L14.792 99.651 0 84.794v43.173l43.137.033-14.865-14.757L52 89.53 38.46 76z"/></svg>
|
||||
|
After Width: | Height: | Size: 421 B |
@@ -0,0 +1 @@
|
||||
<svg t="1650814907622" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="52318" width="200" height="200"><path d="M958.400956 451.54921c-0.058328-5.760191-2.597151-11.215436-6.965645-14.97097L524.345166 69.511143c-7.498788-6.445806-18.581194-6.445806-26.079982 0L309.582871 231.6755l0-102.017488c0-11.04966-8.901741-19.532869-19.951401-19.532869l-88.034009 0c-11.048637 0-19.928888 8.482185-19.928888 19.532869l0 211.954343L71.176063 436.57824c-4.423753 3.800559-6.967692 9.341762-6.967692 15.173584l0 105.500822c0 7.819083 4.554736 14.921851 11.660574 18.183128 2.670829 1.226944 5.51562 1.824555 8.343015 1.824555 4.699022 0 9.346879-1.654686 13.048177-4.836145l53.29788-45.825698 0 324.100516c0 60.677964 49.364291 110.042255 110.042255 110.042255L764.792447 960.741257c60.677964 0 110.042255-49.364291 110.042255-110.042255L874.834702 527.026228l51.585889 44.335764c5.955642 5.119601 14.356986 6.282077 21.481244 2.965541 7.122211-3.313465 11.645225-10.488889 11.565407-18.342764L958.400956 451.54921zM221.578538 150.034085l48.095391 0 0 115.941616-48.095391 41.336454L221.578538 150.034085zM570.718333 920.725892 436.666244 920.725892 436.666244 700.642404c0-11.031241 8.976442-20.007683 20.007683-20.007683l94.0357 0c11.031241 0 20.007683 8.976442 20.007683 20.007683L570.71731 920.725892zM834.818313 495.895207l0 354.803795c0 38.612413-31.414477 70.02689-70.02689 70.02689l-154.058748 0L610.732675 700.642404c0-33.096792-26.926256-60.023048-60.023048-60.023048l-94.0357 0c-33.096792 0-60.023048 26.926256-60.023048 60.023048l0 220.084511L260.59925 920.726915c-38.612413 0-70.02689-31.414477-70.02689-70.02689L190.57236 495.895207c0-1.172709-0.121773-2.314719-0.315178-3.432169l322.113255-276.958846 322.70268 277.348726C834.921667 493.848595 834.818313 494.858598 834.818313 495.895207zM525.411451 173.947727c-7.502881-6.445806-18.587334-6.446829-26.086122 0.00307L104.223736 513.663896l0-52.726875 407.081439-349.870436 407.176606 349.9523 0.521886 51.205219L525.411451 173.947727z" p-id="52319"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714720422565" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15443" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M235.5 871.691v-740h98v304h385v-304h98v740h-98v-349h-385v349h-98z" p-id="15444" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 440 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714745205151" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9116" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M336 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="9117" fill="#000000"></path><path d="M688 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="9118" fill="#000000"></path><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2-44.3-18.7-84.1-45.6-118.3-79.8-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8c18.7-44.3 45.6-84.1 79.8-118.3 34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2 44.3 18.7 84.1 45.6 118.3 79.8 34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8c-18.7 44.3-45.6 84.1-79.8 118.2z" p-id="9119" fill="#000000"></path><path d="M664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-0.3-4.2-3.9-7.4-8.1-7.4H360c-4.6 0-8.2 3.8-8 8.4 4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6c0.2-4.6-3.4-8.4-8-8.4z" p-id="9120" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg t="1675576810577" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1553" width="200" height="200"><path d="M379.392 460.8l114.688 114.688-42.496 102.4L307.2 532.48l-168.96 168.96-71.68-72.704L234.496 460.8l-45.056-45.056c-27.136-27.136-51.2-66.56-66.56-108.544h112.64c7.68 14.336 16.896 27.136 26.112 35.84l45.568 46.08 45.056-45.056C382.976 312.32 409.6 247.808 409.6 204.8H0V102.4h256V0h102.4v102.4h256v102.4h-102.4c0 70.144-37.888 161.28-87.04 210.944L378.88 460.8z m196.608 409.6L512 1024H409.6l256-614.4h102.4l256 614.4h-102.4l-64-153.6h-281.6z m42.496-102.4h196.608L716.8 532.48 618.496 768z" p-id="1554" data-spm-anchor-id="a313x.7781069.0.i0" class="selected"></path></svg>
|
||||
|
After Width: | Height: | Size: 730 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714720786193" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10390" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M688 312v-48c0-4.4-3.6-8-8-8H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8zM296 400c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H296z" p-id="10391" fill="#000000"></path><path d="M440 852H208V148h560v344c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V108c0-17.7-14.3-32-32-32H168c-17.7 0-32 14.3-32 32v784c0 17.7 14.3 32 32 32h272c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z" p-id="10392" fill="#000000"></path><path d="M885.7 903.5l-93.3-93.3C814.7 780.7 828 743.9 828 704c0-97.2-78.8-176-176-176s-176 78.8-176 176 78.8 176 176 176c35.8 0 69-10.7 96.8-29l94.7 94.7c1.6 1.6 3.6 2.3 5.6 2.3s4.1-0.8 5.6-2.3l31-31c3.1-3.1 3.1-8.1 0-11.2zM652 816c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z" p-id="10393" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714755103595" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8918" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M193 796c0 17.7 14.3 32 32 32h574c17.7 0 32-14.3 32-32V563c0-176.2-142.8-319-319-319S193 386.8 193 563v233z m72-233c0-136.4 110.6-247 247-247s247 110.6 247 247v193H404V585c0-5.5-4.5-10-10-10h-44c-5.5 0-10 4.5-10 10v171h-75V563zM216.9 310.5l39.6-39.6c3.1-3.1 3.1-8.2 0-11.3l-67.9-67.9c-3.1-3.1-8.2-3.1-11.3 0l-39.6 39.6c-3.1 3.1-3.1 8.2 0 11.3l67.9 67.9c3.1 3.1 8.1 3.1 11.3 0zM886.5 231.3l-39.6-39.6c-3.1-3.1-8.2-3.1-11.3 0l-67.9 67.9c-3.1 3.1-3.1 8.2 0 11.3l39.6 39.6c3.1 3.1 8.2 3.1 11.3 0l67.9-67.9c3.1-3.2 3.1-8.2 0-11.3zM832 892H192c-17.7 0-32 14.3-32 32v24c0 4.4 3.6 8 8 8h688c4.4 0 8-3.6 8-8v-24c0-17.7-14.3-32-32-32zM484 180h56c4.4 0 8-3.6 8-8V76c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v96c0 4.4 3.6 8 8 8z" p-id="8919" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802846045" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2750" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M868.593046 403.832442c-30.081109-28.844955-70.037123-44.753273-112.624057-44.753273L265.949606 359.079168c-42.554188 0-82.510202 15.908318-112.469538 44.690852-30.236652 28.782533-46.857191 67.222007-46.857191 108.198258l0 294.079782c0 40.977273 16.619516 79.414701 46.702672 108.136859 29.959336 28.844955 70.069869 44.814672 112.624057 44.814672l490.019383 0c42.585911 0 82.696444-15.969717 112.624057-44.814672 30.082132-28.844955 46.579875-67.222007 46.579875-108.136859L915.172921 511.968278C915.171897 471.053426 898.675178 432.677397 868.593046 403.832442zM841.821309 806.049083c0 22.098297-8.882298 42.772152-25.099654 58.306964-16.154935 15.661701-37.81935 24.203238-60.752666 24.203238L265.949606 888.559285c-22.934339 0-44.567032-8.54256-60.877509-24.264637-16.186657-15.474436-25.067932-36.148291-25.067932-58.246589L180.004165 511.968278c0-22.035876 8.881274-42.772152 25.192775-58.307987 16.186657-15.536858 37.81935-24.139793 60.753689-24.139793l490.019383 0c22.933315 0 44.597731 8.602935 60.752666 24.139793 16.21838 15.535835 25.099654 36.272112 25.099654 58.307987L841.822332 806.049083zM510.974136 135.440715c114.914216 0 208.318536 89.75214 208.318536 200.055338l73.350588 0c0-149.113109-126.366036-270.496667-281.669124-270.496667-155.333788 0-281.699824 121.383558-281.699824 270.496667l73.350588 0C302.623877 225.193879 396.059919 135.440715 510.974136 135.440715zM474.299865 747.244792l73.350588 0L547.650453 629.576859l-73.350588 0L474.299865 747.244792z" p-id="2751"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714739843967" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2926" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M510.293333 972.8L61.44 460.8 235.52 51.2h551.253333l174.08 409.6-450.56 512zM141.653333 448.853333L510.293333 870.4l370.346667-421.546667L740.693333 119.466667h-460.8L141.653333 448.853333z" fill="#191919" p-id="2927"></path><path d="M510.293333 631.466667L332.8 431.786667l49.493333-44.373334 128 141.653334 129.706667-141.653334 49.493333 44.373334z" fill="#00C97C" p-id="2928"></path></svg>
|
||||
|
After Width: | Height: | Size: 725 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 512 512"><path d="M400 148l-21.12-24.57A191.43 191.43 0 0 0 240 64C134 64 48 150 48 256s86 192 192 192a192.09 192.09 0 0 0 181.07-128" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="32"></path><path d="M464 68.45V220a4 4 0 0 1-4 4H308.45a4 4 0 0 1-2.83-6.83L457.17 65.62a4 4 0 0 1 6.83 2.83z" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 561 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714720044650" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8586" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M312.1 591.5c3.1 3.1 8.2 3.1 11.3 0l101.8-101.8 86.1 86.2c3.1 3.1 8.2 3.1 11.3 0l226.3-226.5c3.1-3.1 3.1-8.2 0-11.3l-36.8-36.8c-3.1-3.1-8.2-3.1-11.3 0L517 485.3l-86.1-86.2c-3.1-3.1-8.2-3.1-11.3 0L275.3 543.4c-3.1 3.1-3.1 8.2 0 11.3l36.8 36.8z" p-id="8587" fill="#000000"></path><path d="M904 160H548V96c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H120c-17.7 0-32 14.3-32 32v520c0 17.7 14.3 32 32 32h356.4v32L311.6 884.1c-3.7 2.4-4.7 7.3-2.3 11l30.3 47.2v0.1c2.4 3.7 7.4 4.7 11.1 2.3L512 838.9l161.3 105.8c3.7 2.4 8.7 1.4 11.1-2.3v-0.1l30.3-47.2c2.4-3.7 1.3-8.6-2.3-11L548 776.3V744h356c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 512H160V232h704v440z" p-id="8588" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714719706106" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9222" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56c10.1-8.6 13.8-22.6 9.3-35.2l-0.9-2.6c-18.1-50.5-44.9-96.9-79.7-137.9l-1.8-2.1c-8.6-10.1-22.5-13.9-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85c-2.4-13.1-12.7-23.3-25.8-25.7l-2.7-0.5c-52.1-9.4-106.9-9.4-159 0l-2.7 0.5c-13.1 2.4-23.4 12.6-25.8 25.7l-15.8 85.4c-35.9 13.6-69.2 32.9-99 57.4l-81.9-29.1c-12.5-4.4-26.5-0.7-35.1 9.5l-1.8 2.1c-34.8 41.1-61.6 87.5-79.7 137.9l-0.9 2.6c-4.5 12.5-0.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5c-10.1 8.6-13.8 22.6-9.3 35.2l0.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1c8.6 10.1 22.5 13.9 35.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4c2.4 13.1 12.7 23.3 25.8 25.7l2.7 0.5c26.1 4.7 52.8 7.1 79.5 7.1 26.7 0 53.5-2.4 79.5-7.1l2.7-0.5c13.1-2.4 23.4-12.6 25.8-25.7l15.7-85c36.2-13.6 69.7-32.9 99.7-57.6l81.3 28.9c12.5 4.4 26.5 0.7 35.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l0.9-2.6c4.5-12.3 0.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9c-11.3 26.1-25.6 50.7-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97c-28.1 3.2-56.8 3.2-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9z" p-id="9223" fill="#000000"></path><path d="M512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176z m79.2 255.2C570 602.3 541.9 614 512 614c-29.9 0-58-11.7-79.2-32.8C411.7 560 400 531.9 400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8C612.3 444 624 472.1 624 502c0 29.9-11.7 58-32.8 79.2z" p-id="9224" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714755209531" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9116" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M752 664c-28.5 0-54.8 10-75.4 26.7L469.4 540.8c1.7-9.3 2.6-19 2.6-28.8s-0.9-19.4-2.6-28.8l207.2-149.9C697.2 350 723.5 360 752 360c66.2 0 120-53.8 120-120s-53.8-120-120-120-120 53.8-120 120c0 11.6 1.6 22.7 4.7 33.3L439.9 415.8C410.7 377.1 364.3 352 312 352c-88.4 0-160 71.6-160 160s71.6 160 160 160c52.3 0 98.7-25.1 127.9-63.8l196.8 142.5c-3.1 10.6-4.7 21.8-4.7 33.3 0 66.2 53.8 120 120 120s120-53.8 120-120-53.8-120-120-120z m0-476c28.7 0 52 23.3 52 52s-23.3 52-52 52-52-23.3-52-52 23.3-52 52-52zM312 600c-48.5 0-88-39.5-88-88s39.5-88 88-88 88 39.5 88 88-39.5 88-88 88z m440 236c-28.7 0-52-23.3-52-52s23.3-52 52-52 52 23.3 52 52-23.3 52-52 52z" p-id="9117" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1016 B |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
|
||||
|
After Width: | Height: | Size: 211 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714739816180" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2603" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M798.72 836.266667H682.666667v-68.266667h116.053333c80.213333 0 145.066667-64.853333 145.066667-145.066667S877.226667 477.866667 798.72 477.866667H750.933333v-34.133334c0-131.413333-107.52-238.933333-238.933333-238.933333s-238.933333 107.52-238.933333 238.933333v34.133334h-37.546667c-80.213333 0-145.066667 64.853333-145.066667 145.066666S155.306667 768 235.52 768H341.333333v68.266667h-105.813333C117.76 836.266667 20.48 740.693333 20.48 622.933333c0-107.52 80.213333-197.973333 184.32-211.626666C221.866667 257.706667 353.28 136.533333 512 136.533333s288.426667 119.466667 305.493333 274.773334c109.226667 10.24 194.56 100.693333 194.56 211.626666-1.706667 117.76-97.28 213.333333-213.333333 213.333334z" fill="#191919" p-id="2604"></path><path d="M477.866667 563.2h68.266666v307.2h-68.266666z" fill="#00C97C" p-id="2605"></path><path d="M616.106667 680.96L515.413333 580.266667l-102.4 100.693333-47.786666-47.786667 150.186666-150.186666 148.48 150.186666z" fill="#00C97C" p-id="2606"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714745361102" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9516" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M858.5 763.6c-18.9-44.8-46.1-85-80.6-119.5-34.5-34.5-74.7-61.6-119.5-80.6-0.4-0.2-0.8-0.3-1.2-0.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-0.4 0.2-0.8 0.3-1.2 0.5-44.8 18.9-85 46-119.5 80.6-34.5 34.5-61.6 74.7-80.6 119.5C146.9 807.5 137 854 136 901.8c-0.1 4.5 3.5 8.2 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c0.1 4.4 3.6 7.8 8 7.8h60c4.5 0 8.1-3.7 8-8.2-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z" p-id="9517" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1714745286527" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9317" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12"><path d="M824.2 699.9c-25.4-25.4-54.7-45.7-86.4-60.4C783.1 602.8 812 546.8 812 484c0-110.8-92.4-201.7-203.2-200-109.1 1.7-197 90.6-197 200 0 62.8 29 118.8 74.2 155.5-31.7 14.7-60.9 34.9-86.4 60.4C345 754.6 314 826.8 312 903.8c-0.1 4.5 3.5 8.2 8 8.2h56c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5C493.8 707.7 551.1 684 612 684c60.9 0 118.2 23.7 161.3 66.8C814.5 792 838 846.3 840 904.3c0.1 4.3 3.7 7.7 8 7.7h56c4.5 0 8.1-3.7 8-8.2-2-77-33-149.2-87.8-203.9zM612 612c-34.2 0-66.4-13.3-90.5-37.5-24.5-24.5-37.9-57.1-37.5-91.8 0.3-32.8 13.4-64.5 36.3-88 24-24.6 56.1-38.3 90.4-38.7 33.9-0.3 66.8 12.9 91 36.6 24.8 24.3 38.4 56.8 38.4 91.4 0 34.2-13.3 66.3-37.5 90.5-24.2 24.2-56.4 37.5-90.6 37.5z" p-id="9318" fill="#000000"></path><path d="M361.5 510.4c-0.9-8.7-1.4-17.5-1.4-26.4 0-15.9 1.5-31.4 4.3-46.5 0.7-3.6-1.2-7.3-4.5-8.8-13.6-6.1-26.1-14.5-36.9-25.1-25.8-25.2-39.7-59.3-38.7-95.4 0.9-32.1 13.8-62.6 36.3-85.6 24.7-25.3 57.9-39.1 93.2-38.7 31.9 0.3 62.7 12.6 86 34.4 7.9 7.4 14.7 15.6 20.4 24.4 2 3.1 5.9 4.4 9.3 3.2 17.6-6.1 36.2-10.4 55.3-12.4 5.6-0.6 8.8-6.6 6.3-11.6-32.5-64.3-98.9-108.7-175.7-109.9-110.9-1.7-203.3 89.2-203.3 199.9 0 62.8 28.9 118.8 74.2 155.5-31.8 14.7-61.1 35-86.5 60.4-54.8 54.7-85.8 126.9-87.8 204-0.1 4.5 3.5 8.2 8 8.2h56.1c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5 29.4-29.4 65.4-49.8 104.7-59.7 3.9-1 6.5-4.7 6-8.7z" p-id="9319" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div
|
||||
@click="toggleClick"
|
||||
class="px-[15px] hover:bg-gray-50 cursor-pointer h-[50px] leading-[50px] dark:hover:bg-[var(--el-fill-color-light)]"
|
||||
>
|
||||
<svg
|
||||
:class="{ 'is-active': isActive }"
|
||||
class="hamburger"
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="color: #fff !important"
|
||||
>
|
||||
<path
|
||||
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
isActive: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["toggleClick"]);
|
||||
|
||||
function toggleClick() {
|
||||
emit("toggleClick");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hamburger {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: -4px;
|
||||
|
||||
&.is-active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<draggable class="flex gap-2" :list="tags" item-key="id" animation="200">
|
||||
<template #item="{ element }">
|
||||
<el-tag closable @close="handleClose(element)" size="large">
|
||||
{{ element }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-input
|
||||
v-if="inputVisible"
|
||||
ref="inputRef"
|
||||
v-model="tag"
|
||||
class="w-50"
|
||||
@keyup.enter="handleConfirm"
|
||||
@blur="handleConfirm"
|
||||
/>
|
||||
<el-button v-else @click="showInput">+</el-button>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import draggable from "vuedraggable";
|
||||
import { ElInput } from "element-plus";
|
||||
import { PropType } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
tags: {
|
||||
required: false,
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:tags", value: string[]): void;
|
||||
}>();
|
||||
|
||||
const tags = useVModel(props, "tags", emit);
|
||||
|
||||
const inputRef = ref(ElInput);
|
||||
|
||||
const state = reactive({
|
||||
tag: "",
|
||||
inputVisible: false,
|
||||
});
|
||||
|
||||
const { tag, inputVisible } = toRefs(state);
|
||||
|
||||
const showInput = () => {
|
||||
state.inputVisible = true;
|
||||
nextTick(() => {
|
||||
inputRef.value!.input!.focus();
|
||||
});
|
||||
};
|
||||
const handleConfirm = (): void => {
|
||||
const newTag = state.tag.trim();
|
||||
if (newTag && !tags.value?.includes(newTag)) {
|
||||
tags.value?.push(newTag);
|
||||
state.tag = "";
|
||||
}
|
||||
state.inputVisible = false;
|
||||
};
|
||||
|
||||
const handleClose = (tag: string): void => {
|
||||
const index = tags.value?.indexOf(tag.trim());
|
||||
if (index !== -1) {
|
||||
tags.value?.splice(index, 1);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flex.gap-2 {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { locale } = useI18n();
|
||||
|
||||
function handleLanguageChange(lang: string) {
|
||||
locale.value = lang;
|
||||
appStore.changeLanguage(lang);
|
||||
if (lang == "en") {
|
||||
ElMessage.success("Switch Language Successful!");
|
||||
} else {
|
||||
ElMessage.success("Язык переключён");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleLanguageChange">
|
||||
<div>
|
||||
<svg-icon icon-class="language" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :disabled="appStore.language === 'ru'" command="ru">
|
||||
Русский
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :disabled="appStore.language === 'en'" command="en">
|
||||
English
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<el-tag
|
||||
:key="key"
|
||||
v-for="(value, key) in mapObject"
|
||||
@close="handleClose(key)"
|
||||
@click="handleInfo(key)"
|
||||
size="large"
|
||||
closable
|
||||
>
|
||||
{{ key }}
|
||||
</el-tag>
|
||||
|
||||
<el-button @click="handleAdd">+</el-button>
|
||||
|
||||
<el-dialog
|
||||
:title="dialog.title"
|
||||
v-model="dialog.visible"
|
||||
width="600px"
|
||||
append-to-body
|
||||
@close="closeDialog"
|
||||
>
|
||||
<el-form
|
||||
ref="dataFormRef"
|
||||
:rules="dataFormRules"
|
||||
label-position="top"
|
||||
:model="dataForm"
|
||||
>
|
||||
<el-form-item label="key" prop="key">
|
||||
<el-input v-model="dataForm.key" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="value" prop="value">
|
||||
<el-input v-model="dataForm.value" clearable />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm"
|
||||
>{{ $t("common.confirm") }}
|
||||
</el-button>
|
||||
<el-button @click="closeDialog">{{ $t("common.cancel") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="infoDialog.title"
|
||||
v-model="infoDialog.visible"
|
||||
width="600px"
|
||||
append-to-body
|
||||
@close="infoDialog.visible = false"
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="key" prop="key">
|
||||
<el-tag>{{ dataInfo.key }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="value" prop="value">
|
||||
<el-tag>{{ dataInfo.value }}</el-tag>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="infoDialog.visible = false"
|
||||
>{{ $t("common.cancel") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "mapObject",
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
|
||||
interface Form {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
mapObject: {
|
||||
required: false,
|
||||
type: Object as PropType<{ [key: string]: string }>,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:mapObject", value: { [key: string]: string }): void;
|
||||
}>();
|
||||
|
||||
const mapObject = useVModel(props, "mapObject", emit);
|
||||
|
||||
const dataFormRef = ref(ElForm);
|
||||
|
||||
const dataFormRules = {
|
||||
key: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
value: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
dataForm: {
|
||||
key: "",
|
||||
value: "",
|
||||
} as Form,
|
||||
dialog: {
|
||||
title: "Add",
|
||||
visible: false,
|
||||
} as DialogType,
|
||||
infoDialog: {
|
||||
title: "Info",
|
||||
visible: false,
|
||||
},
|
||||
dataInfo: {
|
||||
key: "",
|
||||
value: "",
|
||||
} as Form,
|
||||
});
|
||||
|
||||
const { dataForm, dialog, infoDialog, dataInfo } = toRefs(state);
|
||||
|
||||
const handleAdd = () => {
|
||||
state.dialog.visible = true;
|
||||
};
|
||||
|
||||
const handleClose = (key: string): void => {
|
||||
delete mapObject.value[key];
|
||||
};
|
||||
|
||||
const handleInfo = (key: string) => {
|
||||
state.dataInfo = {
|
||||
key: key,
|
||||
value: mapObject.value[key] || "",
|
||||
};
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
dataFormRef.value.validate((valid: any) => {
|
||||
if (valid) {
|
||||
if (mapObject.value[state.dataForm.key]) {
|
||||
ElMessage.error("key cannot be repeated");
|
||||
return;
|
||||
}
|
||||
mapObject.value[state.dataForm.key] = state.dataForm.value;
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeDialog = (): void => {
|
||||
state.dialog.visible = false;
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flex.gap-2 {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div :class="'pagination ' + { hidden: hidden }">
|
||||
<el-pagination
|
||||
:background="background"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:layout="layout"
|
||||
:page-sizes="pageSizes"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import { scrollTo } from "@/utils/scroll-to";
|
||||
|
||||
const props = defineProps({
|
||||
total: {
|
||||
required: true,
|
||||
type: Number as PropType<number>,
|
||||
default: 0,
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
pageSizes: {
|
||||
type: Array as PropType<number[]>,
|
||||
default() {
|
||||
return [10, 20, 30, 50];
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: "total, sizes, prev, pager, next, jumper",
|
||||
},
|
||||
background: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
autoScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["pagination"]);
|
||||
|
||||
const currentPage = useVModel(props, "page", emit);
|
||||
|
||||
const pageSize = useVModel(props, "limit", emit);
|
||||
|
||||
function handleSizeChange(val: number) {
|
||||
emit("pagination", { page: currentPage, limit: val });
|
||||
if (props.autoScroll) {
|
||||
scrollTo(0, 800);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCurrentChange(val: number) {
|
||||
currentPage.value = val;
|
||||
emit("pagination", { page: val, limit: props.limit });
|
||||
if (props.autoScroll) {
|
||||
scrollTo(0, 800);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pagination {
|
||||
padding: 12px;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
import { addClass, removeClass } from "@/utils/index";
|
||||
|
||||
const show = ref(false);
|
||||
|
||||
defineProps({
|
||||
buttonTop: {
|
||||
default: 250,
|
||||
type: Number,
|
||||
},
|
||||
});
|
||||
|
||||
watch(show, (value) => {
|
||||
if (value) {
|
||||
addEventClick();
|
||||
}
|
||||
if (value) {
|
||||
addClass(document.body, "showRightPanel");
|
||||
} else {
|
||||
removeClass(document.body, "showRightPanel");
|
||||
}
|
||||
});
|
||||
|
||||
function addEventClick() {
|
||||
window.addEventListener("click", closeSidebar, { passive: true });
|
||||
}
|
||||
|
||||
function closeSidebar(evt: any) {
|
||||
// 主题选择点击不关闭
|
||||
let parent = evt.target.closest(".right-panel-container");
|
||||
if (!parent) {
|
||||
show.value = false;
|
||||
window.removeEventListener("click", closeSidebar);
|
||||
}
|
||||
}
|
||||
|
||||
const rightPanel = ref();
|
||||
|
||||
function insertToBody() {
|
||||
const body = document.querySelector("body") as any;
|
||||
body.insertBefore(rightPanel.value, body.firstChild);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
insertToBody();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
rightPanel.value.remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ show: show }" ref="rightPanel">
|
||||
<div class="right-panel-overlay" />
|
||||
<div class="right-panel-container">
|
||||
<div
|
||||
class="right-panel-btn"
|
||||
:style="{
|
||||
top: buttonTop + 'px',
|
||||
}"
|
||||
@click="show = !show"
|
||||
>
|
||||
<i-ep-close v-show="show" />
|
||||
<i-ep-setting v-show="!show" />
|
||||
</div>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.showRightPanel {
|
||||
position: relative;
|
||||
width: calc(100% - 15px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.right-panel-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.right-panel-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 100vh;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
box-shadow: 0 0 15px 0 rgb(0 0 0 / 5%);
|
||||
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
transform: translate(100%);
|
||||
}
|
||||
|
||||
.show {
|
||||
transition: all 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
|
||||
.right-panel-overlay {
|
||||
z-index: 99;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.right-panel-container {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel-btn {
|
||||
position: absolute;
|
||||
left: -36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: var(--el-color-white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 6px 0 0 6px;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: -10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const sizeOptions = ref([
|
||||
{ label: "默认", value: "default" },
|
||||
{ label: "大型", value: "large" },
|
||||
{ label: "小型", value: "small" },
|
||||
]);
|
||||
|
||||
function handleSizeChange(size: string) {
|
||||
appStore.changeSize(size);
|
||||
ElMessage.success("切换布局大小成功");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleSizeChange">
|
||||
<div>
|
||||
<svg-icon icon-class="size" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item of sizeOptions"
|
||||
:key="item.value"
|
||||
:disabled="appStore.size == item.value"
|
||||
:command="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-icon"
|
||||
:style="'width:' + size + ';height:' + size"
|
||||
>
|
||||
<use :xlink:href="symbolId" :fill="color" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "icon",
|
||||
},
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "1em",
|
||||
},
|
||||
});
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
overflow: hidden;
|
||||
vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
|
||||
outline: none;
|
||||
fill: currentcolor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input-number
|
||||
v-model="capacity"
|
||||
placeholder="Please enter a value"
|
||||
:min="-1"
|
||||
:controls="false"
|
||||
:precision="0"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
/>
|
||||
<el-select
|
||||
v-model="unit"
|
||||
:placeholder="$t('account.unit')"
|
||||
style="width: 100px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in units"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import {
|
||||
calculateBytes,
|
||||
formatStorageCapacity,
|
||||
formatStorageUnit,
|
||||
} from "@/utils/byte";
|
||||
|
||||
const units = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
|
||||
|
||||
const props = defineProps({
|
||||
valueTmp: {
|
||||
type: Number as PropType<number>,
|
||||
required: true,
|
||||
},
|
||||
setValue: {
|
||||
type: Function as PropType<(newValue: number) => void>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
capacity: 0,
|
||||
unit: "GB",
|
||||
});
|
||||
|
||||
const { capacity, unit } = toRefs(state);
|
||||
|
||||
watch(
|
||||
[capacity, unit],
|
||||
([newC, newU]) => {
|
||||
const newValue = calculateBytes(newC, newU);
|
||||
props.setValue(newValue);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.valueTmp,
|
||||
(newValue) => {
|
||||
state.capacity = formatStorageCapacity(newValue);
|
||||
state.unit = formatStorageUnit(newValue);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { App } from "vue";
|
||||
|
||||
import { hasRole } from "./permission";
|
||||
|
||||
// 全局注册 directive
|
||||
export function setupDirective(app: App<Element>) {
|
||||
// 使 v-hasRole 在所有组件中都可用
|
||||
app.directive("hasRole", hasRole);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useAccountStoreHook } from "@/store/modules/account";
|
||||
import { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
/**
|
||||
* 角色权限
|
||||
*/
|
||||
export const hasRole: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
const { value } = binding;
|
||||
|
||||
if (value) {
|
||||
const requiredRoles = value; // DOM绑定需要的角色编码
|
||||
const { roles } = useAccountStoreHook();
|
||||
const hasRole = roles.some((perm) => {
|
||||
return requiredRoles.includes(perm);
|
||||
});
|
||||
|
||||
if (!hasRole) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
} else {
|
||||
throw new Error("need roles! Like v-has-role=\"['admin', 'user']\"");
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
|
||||
const appStore = useAppStore();
|
||||
import enLocale from "./package/en";
|
||||
import ruLocale from "./package/ru";
|
||||
|
||||
const messages = {
|
||||
ru: {
|
||||
...ruLocale,
|
||||
},
|
||||
en: {
|
||||
...enLocale,
|
||||
},
|
||||
};
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: appStore.language,
|
||||
messages: messages,
|
||||
globalInjection: true,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,324 @@
|
||||
export default {
|
||||
// 路由国际化
|
||||
route: {
|
||||
account: "Account",
|
||||
accountList: "Account Manage",
|
||||
hysteria: "Hysteria",
|
||||
hysteriaList: "Hysteria Manage",
|
||||
config: "System",
|
||||
configList: "System Config",
|
||||
monitor: "Monitor",
|
||||
monitorSystem: "System Monitor",
|
||||
log: "Log",
|
||||
logSystem: "System Log",
|
||||
logHysteria: "Hysteria Log",
|
||||
info: "Info",
|
||||
infoAccount: "Account Info",
|
||||
},
|
||||
// 登录页面国际化
|
||||
login: {
|
||||
title: "HY2XS admin",
|
||||
username: "Username",
|
||||
password: "Password",
|
||||
login: "Login",
|
||||
},
|
||||
// 导航栏国际化
|
||||
navbar: {
|
||||
logout: "Logout",
|
||||
},
|
||||
common: {
|
||||
id: "ID",
|
||||
createTime: "Create Time",
|
||||
operate: "Operate",
|
||||
edit: "Edit",
|
||||
delete: "Delete",
|
||||
deleted: "Status",
|
||||
all: "All",
|
||||
enable: "Enable",
|
||||
disable: "Disable",
|
||||
search: "Search",
|
||||
reset: "Reset",
|
||||
add: "Add",
|
||||
confirm: "Confirm",
|
||||
cancel: "Cancel",
|
||||
copySuccess: "Copy successful",
|
||||
subscribe: "Subscribe",
|
||||
subscribeQrCode: "Subscribe QR Code",
|
||||
nodeUrl: "Node URL",
|
||||
nodeQrCode: "Node QR Code",
|
||||
resetTraffic: "Reset traffic",
|
||||
import: "Import",
|
||||
export: "Export",
|
||||
save: "Save",
|
||||
update: "Update",
|
||||
downloadSuccess: "Download successful",
|
||||
wait: "The version is being changed, please wait a moment",
|
||||
enableSuccess: "Hysteria2 start successful",
|
||||
disableSuccess: "Hysteria2 stop successful",
|
||||
success: "Success",
|
||||
refresh: "Refresh",
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
securityRisk: "Security Risks",
|
||||
defaultPassTip: `Please change the default login password as soon as possible, It is recommended to set a strong password to protect your account security. <a href="/#/account/list?focus=change-pass" style="color: #00BFFF">Click here</a> to change`,
|
||||
noHttpsTip: `Your website is not using HTTPS, making data transmission insecure, Please enable HTTPS as soon as possible to protect user information. <a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">Click here</a> to enable`,
|
||||
},
|
||||
info: {
|
||||
expireTime: "y-M-d H:m:s",
|
||||
greeting1: "The cool and fresh air awakens your energy for the day🌅!",
|
||||
greeting2: "Good morning,",
|
||||
greeting3: "Good afternoon,",
|
||||
greeting4: "Good evening,",
|
||||
greeting5:
|
||||
"I want to be a shooting star, cutting through the darkness, just to illuminate your dreams, good night🌛!",
|
||||
},
|
||||
account: {
|
||||
remark: "Remark",
|
||||
username: "Username",
|
||||
pass: "Pass",
|
||||
conPass: "ConPass",
|
||||
quota: "Quota",
|
||||
download: "Download",
|
||||
upload: "Upload",
|
||||
expireTime: "Expire Time",
|
||||
kickUtilTimeLast: "Offline Remaining Time",
|
||||
kickUtilTime: "Offline Time",
|
||||
deviceNo: "Limit Devices",
|
||||
onlineStatus: "Online Status",
|
||||
online: "Online",
|
||||
offline: "Offline",
|
||||
device: "Online Devices",
|
||||
role: "Role",
|
||||
unit: "Unit",
|
||||
loginAt: "Last login time",
|
||||
conAt: "Last connection time",
|
||||
createTime: "Create Time",
|
||||
releaseSuccess: "Release successful",
|
||||
kick: "Kick",
|
||||
kickTip: "Force user to log off",
|
||||
releaseKick: "Release",
|
||||
releaseKickTip: "Remove offline status",
|
||||
},
|
||||
config: {
|
||||
huiWebPort: "HY2XS admin Web Port",
|
||||
huiWebContext: "HY2XS admin Web Context",
|
||||
hysteria2TrafficTime: "Hysteria2 Traffic Time",
|
||||
huiCrtPath: "HY2XS admin CRT File Path",
|
||||
huiKeyPath: "HY2XS admin KEY File Path",
|
||||
uploadCrtFile: "Upload CRT File",
|
||||
uploadKeyFile: "Upload KEY File",
|
||||
restartServer: "Restart Panel",
|
||||
restartTip: "Restarting, please refresh",
|
||||
useHysteria2Cert: "Use Hysteria2 cert",
|
||||
huiHttps: "Open https on the panel",
|
||||
resetTrafficCron: "Reset traffic schedule task",
|
||||
resetTrafficCronTip:
|
||||
"Scheduled task expression, reference: https://pkg.go.dev/github.com/robfig/cron/v3",
|
||||
resetTrafficMonth: "Run once a month, midnight, first of month",
|
||||
resetTrafficWeek: "Run once a week, midnight between Sat/Sun",
|
||||
},
|
||||
monitor: {
|
||||
huiVersion: "HY2XS admin Version",
|
||||
cpuPercent: "CPU Usage",
|
||||
memPercent: "Memory Usage",
|
||||
diskPercent: "Disk Usage",
|
||||
hysteria2UserTotal: "Number of online users",
|
||||
hysteria2DeviceTotal: "Number of online devices",
|
||||
hysteria2Version: "Hysteria2 Version",
|
||||
hysteria2Running: "Hysteria2 Status",
|
||||
hysteria2RunningTrue: "Running",
|
||||
hysteria2RunningFalse: "Stop",
|
||||
},
|
||||
log: {
|
||||
numLine: "Number of lines",
|
||||
},
|
||||
hysteria: {
|
||||
enable: "Enable",
|
||||
disable: "Disable",
|
||||
addConfigItem: "Add Config Item",
|
||||
hysteria2Version: "Hysteria2 Version",
|
||||
hysteria2Running: "Hysteria2 Status",
|
||||
hysteria2ChangeVersion: "Change",
|
||||
addOutbound: "Add Outbound",
|
||||
extension: "Extension",
|
||||
listen: "Listen",
|
||||
tls: "TLS",
|
||||
obfs: "Obfuscation",
|
||||
quic: "QUIC parameters",
|
||||
bandwidth: "Bandwidth",
|
||||
speedTest: "Speed Test",
|
||||
udp: "UDP",
|
||||
resolver: "Resolver",
|
||||
sniff: "Protocol Sniffing",
|
||||
acl: "ACL",
|
||||
outbounds: "Outbounds",
|
||||
http: "Traffic Stats API (HTTP)",
|
||||
masquerade: "Masquerade",
|
||||
config: {
|
||||
enable: "Enable/Disable",
|
||||
remark: "Remark",
|
||||
portHopping:
|
||||
"Port Hopping, Multiple individual ports: 1234,5678,9012; A range of ports: 20000-50000; A combination of both: 1234,5000-6000,7044,8000-9000",
|
||||
clashExtension: "Clash subscription extension",
|
||||
listen:
|
||||
"When the IP address is omitted, the server will listen on all interfaces, both IPv4 and IPv6. To listen on IPv4 only, you can use 0.0.0.0:443. To listen on IPv6 only, you can use [::]:443.",
|
||||
tlsType: "TLS type",
|
||||
tls: {
|
||||
cert: "The path to the Cert file.",
|
||||
key: "The path to the Key file.",
|
||||
sniGuard:
|
||||
'Verify the SNI provided by the client. Accept the connection only when it matches what\'s in the certificate. Terminate the TLS handshake otherwise. Set to strict to enforce this behavior. Set to disable to disable this entirely. The default is dns-san, which enables this feature only when the certificate contains the "Subject Alternative Name" extension with a domain name in it.',
|
||||
},
|
||||
acme: {
|
||||
domains: "Domains",
|
||||
email: "Email",
|
||||
ca: "The CA to use. Can be letsencrypt or zerossl.",
|
||||
listenHost:
|
||||
"The host address (not including the port) to listen on for the ACME challenge. If omitted, the server will listen on all interfaces.",
|
||||
dir: "The directory to store the ACME account key and certificates.",
|
||||
type: "ACME challenge type. Can be http, tls, or dns.",
|
||||
http: {
|
||||
altPort:
|
||||
"Listening port for HTTP challenges. (Note: Changing to a port other than 80 requires port forwarding or HTTP reverse proxy, or the challenge will fail!)",
|
||||
},
|
||||
tls: {
|
||||
altPort:
|
||||
"Listening port for TLS-ALPN challenges. (Note: Changing to a port other than 443 requires port forwarding or TLS reverse proxy, or the challenge will fail!)",
|
||||
},
|
||||
dns: {
|
||||
name: "DNS provider. For details, refer to ACME DNS Configuration.",
|
||||
config: "ACME DNS Configuration",
|
||||
},
|
||||
disableHTTP: "Disable HTTP challenge.",
|
||||
disableTLSALPN: "Disable TLS-ALPN challenge.",
|
||||
altHTTPPort:
|
||||
"Alternate HTTP challenge port. (Note: If you want to use anything other than 80, you must set up port forward/HTTP reverse proxy from 80 to that port, otherwise ACME will not be able to issue the certificate.)",
|
||||
altTLSALPNPort:
|
||||
"Alternate TLS-ALPN challenge port. (Note: If you want to use anything other than 443, you must set up port forward/SNI proxy from 443 to that port, otherwise ACME will not be able to issue the certificate.)",
|
||||
},
|
||||
obfs: {
|
||||
type: "Type",
|
||||
salamander: {
|
||||
password: "Replace with a strong password of your choice.",
|
||||
},
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: "The initial QUIC stream receive window size.",
|
||||
maxStreamReceiveWindow: "The maximum QUIC stream receive window size.",
|
||||
initConnReceiveWindow:
|
||||
"The initial QUIC connection receive window size.",
|
||||
maxConnReceiveWindow:
|
||||
"The maximum QUIC connection receive window size.",
|
||||
maxIdleTimeout:
|
||||
"The maximum idle timeout. How long the server will consider the client still connected without any activity.",
|
||||
maxIncomingStreams:
|
||||
"The maximum number of concurrent incoming streams.",
|
||||
disablePathMTUDiscovery: "Disable QUIC path MTU discovery.",
|
||||
},
|
||||
bandwidth: {
|
||||
up: "Up",
|
||||
down: "Down",
|
||||
},
|
||||
ignoreClientBandwidth:
|
||||
"When enabled, makes the server to disregard any bandwidth hints set by clients",
|
||||
speedTest:
|
||||
"speedTest enables the built-in speed test server. When enabled, clients can test their download and upload speeds with the server. For more information, see the Speed Test documentation.",
|
||||
disableUDP:
|
||||
"disableUDP disables UDP forwarding, only allowing TCP connections.",
|
||||
udpIdleTimeout:
|
||||
"udpIdleTimeout specifies the amount of time the server will keep a local UDP port open for each UDP session that has no activity. This is conceptually similar to the NAT UDP session timeout.",
|
||||
resolver: {
|
||||
type: "Type",
|
||||
tcp: {
|
||||
addr: "The address of the TCP resolver.",
|
||||
timeout: "The timeout for DNS queries.",
|
||||
},
|
||||
udp: {
|
||||
addr: "The address of the UDP resolver.",
|
||||
timeout: "The timeout for DNS queries.",
|
||||
},
|
||||
tls: {
|
||||
addr: "The address of the TLS resolver.",
|
||||
timeout: "The timeout for DNS queries.",
|
||||
sni: "The SNI to use for the TLS resolver.",
|
||||
insecure: "Disable TLS verification for the TLS resolver.",
|
||||
},
|
||||
https: {
|
||||
addr: "The address of the HTTPS resolver.",
|
||||
timeout: "The timeout for DNS queries.",
|
||||
sni: "The SNI to use for the TLS resolver.",
|
||||
insecure: "Disable TLS verification for the TLS resolver.",
|
||||
},
|
||||
},
|
||||
sniff: {
|
||||
enable: "Whether to enable protocol sniffing.",
|
||||
timeout:
|
||||
"Sniffing timeout. If the protocol/domain cannot be determined within this time, the original address will be used to initiate the connection.",
|
||||
rewriteDomain:
|
||||
"Whether to rewrite requests that are already in domain name form. If enabled, requests with the target address already in domain name form will still be sniffed.",
|
||||
tcpPorts:
|
||||
"List of TCP ports. Only TCP requests on these ports will be sniffed.",
|
||||
udpPorts:
|
||||
"List of UDP ports. Only UDP requests on these ports will be sniffed.",
|
||||
},
|
||||
aclType: "ACL type",
|
||||
acl: {
|
||||
file: "The path to the ACL file.",
|
||||
inline: "The list of inline ACL rules.",
|
||||
geoip:
|
||||
"Optional. Uncomment to enable. The path to the GeoIP database file. If this field is omitted, Hysteria will automatically download the latest database to your working directory.",
|
||||
geosite:
|
||||
"Optional. Uncomment to enable. The path to the GeoSite database file. If this field is omitted, Hysteria will automatically download the latest database to your working directory.",
|
||||
geoUpdateInterval:
|
||||
"Optional. The interval at which to refresh the GeoIP/GeoSite databases. 168 hours (1 week) by default. Only applies if the GeoIP/GeoSite databases are automatically downloaded. (Check the note below for more information.)",
|
||||
},
|
||||
outbounds: {
|
||||
name: "The name of the outbound. This is used in ACL rules.",
|
||||
type: "Type",
|
||||
socks5: {
|
||||
addr: "The address of the SOCKS5 proxy.",
|
||||
username:
|
||||
"Optional. The username for the SOCKS5 proxy, if authentication is required.",
|
||||
password:
|
||||
"Optional. The password for the SOCKS5 proxy, if authentication is required.",
|
||||
},
|
||||
http: {
|
||||
url: "The URL of the HTTP/HTTPS proxy. (Can be http:// or https://)",
|
||||
insecure:
|
||||
"Optional. Whether to disable TLS verification. Applies to HTTPS proxies only.",
|
||||
},
|
||||
direct: {
|
||||
mode: "Type",
|
||||
bindIPv4: "The local IPv4 address to bind to.",
|
||||
bindIPv6: "The local IPv6 address to bind to.",
|
||||
bindDevice: "The local network interface to bind to.",
|
||||
fastOpen: "Enable TCP fast open.",
|
||||
},
|
||||
},
|
||||
trafficStats: {
|
||||
listen: "The address to listen on.",
|
||||
},
|
||||
masquerade: {
|
||||
type: "Type",
|
||||
file: {
|
||||
dir: "The directory to serve files from.",
|
||||
},
|
||||
proxy: {
|
||||
url: "The URL of the website to proxy.",
|
||||
rewriteHost:
|
||||
"Whether to rewrite the Host header to match the proxied website. This is required if the target web server uses Host to determine which site to serve.",
|
||||
insecure: "Disable TLS verification for the proxied website.",
|
||||
},
|
||||
string: {
|
||||
content: "The string to return.",
|
||||
headers: "Optional. The headers to return.",
|
||||
statusCode: "Optional. The status code to return. 200 by default.",
|
||||
},
|
||||
listenHTTP: "HTTP (TCP) listen address.",
|
||||
listenHTTPS: "HTTPS (TCP) listen address.",
|
||||
forceHTTPS:
|
||||
"Whether to force HTTPS. If enabled, all HTTP requests will be redirected to HTTPS.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,258 @@
|
||||
export default {
|
||||
route: {
|
||||
account: "Аккаунты",
|
||||
accountList: "Управление аккаунтами",
|
||||
hysteria: "Hysteria",
|
||||
hysteriaList: "Управление Hysteria",
|
||||
config: "Система",
|
||||
configList: "Настройки системы",
|
||||
monitor: "Мониторинг",
|
||||
monitorSystem: "Системный мониторинг",
|
||||
log: "Логи",
|
||||
logSystem: "Системные логи",
|
||||
logHysteria: "Логи Hysteria",
|
||||
info: "Информация",
|
||||
infoAccount: "Профиль",
|
||||
},
|
||||
login: {
|
||||
title: "HY2XS admin",
|
||||
username: "Логин",
|
||||
password: "Пароль",
|
||||
login: "Войти",
|
||||
},
|
||||
navbar: {
|
||||
logout: "Выйти",
|
||||
},
|
||||
common: {
|
||||
id: "ID",
|
||||
createTime: "Создано",
|
||||
operate: "Действия",
|
||||
edit: "Изменить",
|
||||
delete: "Удалить",
|
||||
deleted: "Статус",
|
||||
all: "Все",
|
||||
enable: "Включено",
|
||||
disable: "Отключено",
|
||||
search: "Поиск",
|
||||
reset: "Сброс",
|
||||
add: "Добавить",
|
||||
confirm: "Подтвердить",
|
||||
cancel: "Отмена",
|
||||
copySuccess: "Скопировано",
|
||||
subscribe: "Ссылка подписки",
|
||||
subscribeQrCode: "QR подписки",
|
||||
nodeUrl: "URL узла",
|
||||
nodeQrCode: "QR узла",
|
||||
resetTraffic: "Сбросить трафик",
|
||||
import: "Импорт",
|
||||
export: "Экспорт",
|
||||
save: "Сохранить",
|
||||
update: "Обновить",
|
||||
downloadSuccess: "Загрузка завершена",
|
||||
wait: "Версия меняется, подождите",
|
||||
enableSuccess: "Hysteria2 запущена",
|
||||
disableSuccess: "Hysteria2 остановлена",
|
||||
success: "Готово",
|
||||
refresh: "Обновить",
|
||||
yes: "Да",
|
||||
no: "Нет",
|
||||
securityRisk: "Риски безопасности",
|
||||
defaultPassTip: `Смените пароль по умолчанию как можно скорее. <a href="/#/account/list?focus=change-pass" style="color: #00BFFF">Перейти к смене</a>`,
|
||||
noHttpsTip: `Панель работает без HTTPS. Включите HTTPS для защиты данных. <a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">Открыть настройки</a>`,
|
||||
},
|
||||
info: {
|
||||
expireTime: "г-М-д Ч:м:с",
|
||||
greeting1: "Доброе утро,",
|
||||
greeting2: "Доброе утро,",
|
||||
greeting3: "Добрый день,",
|
||||
greeting4: "Добрый вечер,",
|
||||
greeting5: "Доброй ночи,",
|
||||
},
|
||||
account: {
|
||||
remark: "Комментарий",
|
||||
username: "Логин",
|
||||
pass: "Пароль входа",
|
||||
conPass: "Пароль подключения",
|
||||
quota: "Квота",
|
||||
download: "Скачано",
|
||||
upload: "Отдано",
|
||||
expireTime: "Срок действия",
|
||||
kickUtilTimeLast: "Осталось офлайн",
|
||||
kickUtilTime: "Отключить до",
|
||||
deviceNo: "Лимит устройств",
|
||||
onlineStatus: "Онлайн",
|
||||
online: "Онлайн",
|
||||
offline: "Офлайн",
|
||||
device: "Устройства",
|
||||
role: "Роль",
|
||||
unit: "Ед. изм.",
|
||||
loginAt: "Последний вход",
|
||||
conAt: "Последнее подключение",
|
||||
createTime: "Создано",
|
||||
releaseSuccess: "Ограничение снято",
|
||||
kick: "Отключить",
|
||||
kickTip: "Принудительно отключить пользователя",
|
||||
releaseKick: "Снять",
|
||||
releaseKickTip: "Снять офлайн-статус",
|
||||
},
|
||||
config: {
|
||||
huiWebPort: "Порт HY2XS admin",
|
||||
huiWebContext: "Web-контекст HY2XS admin",
|
||||
hysteria2TrafficTime: "Период учёта трафика Hysteria2",
|
||||
huiCrtPath: "Путь к CRT HY2XS admin",
|
||||
huiKeyPath: "Путь к KEY HY2XS admin",
|
||||
uploadCrtFile: "Загрузить CRT",
|
||||
uploadKeyFile: "Загрузить KEY",
|
||||
restartServer: "Перезапустить панель",
|
||||
restartTip: "Перезапуск, обновите страницу",
|
||||
useHysteria2Cert: "Использовать сертификат Hysteria2",
|
||||
huiHttps: "Включить HTTPS панели",
|
||||
resetTrafficCron: "Расписание сброса трафика",
|
||||
resetTrafficCronTip: "Cron-выражение для планового сброса трафика",
|
||||
resetTrafficMonth: "Раз в месяц, в полночь первого дня",
|
||||
resetTrafficWeek: "Раз в неделю, в полночь между субботой и воскресеньем",
|
||||
},
|
||||
monitor: {
|
||||
huiVersion: "Версия HY2XS admin",
|
||||
cpuPercent: "CPU",
|
||||
memPercent: "Память",
|
||||
diskPercent: "Диск",
|
||||
hysteria2UserTotal: "Пользователей онлайн",
|
||||
hysteria2DeviceTotal: "Устройств онлайн",
|
||||
hysteria2Version: "Версия Hysteria2",
|
||||
hysteria2Running: "Статус Hysteria2",
|
||||
hysteria2RunningTrue: "Работает",
|
||||
hysteria2RunningFalse: "Остановлена",
|
||||
},
|
||||
log: {
|
||||
numLine: "Количество строк",
|
||||
},
|
||||
hysteria: {
|
||||
enable: "Включить",
|
||||
disable: "Отключить",
|
||||
addConfigItem: "Добавить параметр",
|
||||
hysteria2Version: "Версия Hysteria2",
|
||||
hysteria2Running: "Статус Hysteria2",
|
||||
hysteria2ChangeVersion: "Сменить",
|
||||
addOutbound: "Добавить outbound",
|
||||
extension: "Расширение",
|
||||
listen: "Адрес прослушивания",
|
||||
tls: "TLS",
|
||||
obfs: "Маскировка",
|
||||
quic: "Параметры QUIC",
|
||||
bandwidth: "Полоса",
|
||||
speedTest: "Тест скорости",
|
||||
udp: "UDP",
|
||||
resolver: "DNS",
|
||||
sniff: "Sniffing протоколов",
|
||||
acl: "ACL",
|
||||
outbounds: "Outbounds",
|
||||
http: "Traffic Stats API (HTTP)",
|
||||
masquerade: "Masquerade",
|
||||
config: {
|
||||
enable: "Включить/отключить",
|
||||
remark: "Комментарий",
|
||||
portHopping: "Port hopping: отдельные порты, диапазоны или их комбинации",
|
||||
clashExtension: "Расширение подписки Clash",
|
||||
listen: "Адрес и порт прослушивания. Для IPv4 используйте 0.0.0.0:443.",
|
||||
tlsType: "Тип TLS",
|
||||
tls: {
|
||||
cert: "Путь к cert-файлу",
|
||||
key: "Путь к key-файлу",
|
||||
sniGuard: "Проверка SNI клиента перед принятием TLS-соединения.",
|
||||
},
|
||||
acme: {
|
||||
domains: "Домены",
|
||||
email: "Email",
|
||||
ca: "CA: letsencrypt или zerossl",
|
||||
listenHost: "Адрес для ACME challenge",
|
||||
dir: "Каталог ACME аккаунта и сертификатов",
|
||||
type: "Тип ACME challenge: http, tls или dns",
|
||||
http: { altPort: "Альтернативный порт HTTP challenge" },
|
||||
tls: { altPort: "Альтернативный порт TLS-ALPN challenge" },
|
||||
dns: { name: "DNS-провайдер", config: "Конфигурация ACME DNS" },
|
||||
disableHTTP: "Отключить HTTP challenge",
|
||||
disableTLSALPN: "Отключить TLS-ALPN challenge",
|
||||
altHTTPPort: "Альтернативный HTTP-порт",
|
||||
altTLSALPNPort: "Альтернативный TLS-ALPN-порт",
|
||||
},
|
||||
obfs: {
|
||||
type: "Тип",
|
||||
salamander: { password: "Сильный пароль Salamander" },
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: "Начальное окно приёма QUIC stream",
|
||||
maxStreamReceiveWindow: "Максимальное окно приёма QUIC stream",
|
||||
initConnReceiveWindow: "Начальное окно приёма QUIC connection",
|
||||
maxConnReceiveWindow: "Максимальное окно приёма QUIC connection",
|
||||
maxIdleTimeout: "Максимальный idle timeout",
|
||||
maxIncomingStreams: "Максимум входящих stream",
|
||||
disablePathMTUDiscovery: "Отключить QUIC path MTU discovery",
|
||||
},
|
||||
bandwidth: { up: "Вверх", down: "Вниз" },
|
||||
ignoreClientBandwidth: "Игнорировать bandwidth, заявленный клиентом",
|
||||
speedTest: "Встроенный сервер теста скорости",
|
||||
disableUDP: "Отключить UDP forwarding",
|
||||
udpIdleTimeout: "Idle timeout для UDP-сессий",
|
||||
resolver: {
|
||||
type: "Тип",
|
||||
tcp: { addr: "TCP DNS resolver", timeout: "Timeout DNS-запросов" },
|
||||
udp: { addr: "UDP DNS resolver", timeout: "Timeout DNS-запросов" },
|
||||
tls: {
|
||||
addr: "DNS over TLS resolver",
|
||||
timeout: "Timeout DNS-запросов",
|
||||
sni: "SNI для TLS resolver",
|
||||
insecure: "Отключить TLS-проверку",
|
||||
},
|
||||
https: {
|
||||
addr: "DNS over HTTPS resolver",
|
||||
timeout: "Timeout DNS-запросов",
|
||||
sni: "SNI для HTTPS resolver",
|
||||
insecure: "Отключить TLS-проверку",
|
||||
},
|
||||
},
|
||||
sniff: {
|
||||
enable: "Включить sniffing",
|
||||
timeout: "Timeout sniffing",
|
||||
rewriteDomain: "Повторно анализировать доменные запросы",
|
||||
tcpPorts: "TCP-порты для sniffing",
|
||||
udpPorts: "UDP-порты для sniffing",
|
||||
},
|
||||
aclType: "Тип ACL",
|
||||
acl: {
|
||||
file: "Путь к ACL-файлу",
|
||||
inline: "Inline ACL-правила",
|
||||
geoip: "Путь к GeoIP базе",
|
||||
geosite: "Путь к GeoSite базе",
|
||||
geoUpdateInterval: "Интервал обновления GeoIP/GeoSite",
|
||||
},
|
||||
outbounds: {
|
||||
name: "Имя outbound",
|
||||
type: "Тип",
|
||||
socks5: { addr: "Адрес SOCKS5", username: "Логин SOCKS5", password: "Пароль SOCKS5" },
|
||||
http: { url: "URL HTTP/HTTPS proxy", insecure: "Отключить TLS-проверку proxy" },
|
||||
direct: {
|
||||
mode: "Тип",
|
||||
bindIPv4: "Локальный IPv4",
|
||||
bindIPv6: "Локальный IPv6",
|
||||
bindDevice: "Сетевой интерфейс",
|
||||
fastOpen: "TCP fast open",
|
||||
},
|
||||
},
|
||||
trafficStats: { listen: "Адрес прослушивания" },
|
||||
masquerade: {
|
||||
type: "Тип",
|
||||
file: { dir: "Каталог файлов" },
|
||||
proxy: {
|
||||
url: "URL проксируемого сайта",
|
||||
rewriteHost: "Переписывать Host header",
|
||||
insecure: "Отключить TLS-проверку",
|
||||
},
|
||||
string: { content: "Ответ строкой", headers: "HTTP headers", statusCode: "HTTP status code" },
|
||||
listenHTTP: "HTTP listen address",
|
||||
listenHTTPS: "HTTPS listen address",
|
||||
forceHTTPS: "Принудительно использовать HTTPS",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,312 @@
|
||||
export default {
|
||||
// 路由国际化
|
||||
route: {
|
||||
account: "账户",
|
||||
accountList: "账户管理",
|
||||
hysteria: "Hysteria",
|
||||
hysteriaList: "Hysteria 管理",
|
||||
config: "系统",
|
||||
configList: "系统设置",
|
||||
monitor: "监控",
|
||||
monitorSystem: "系统监控",
|
||||
log: "日志",
|
||||
logSystem: "系统日志",
|
||||
logHysteria: "Hysteria 日志",
|
||||
info: "信息",
|
||||
infoAccount: "账户信息",
|
||||
},
|
||||
// 登录页面国际化
|
||||
login: {
|
||||
title: "HY2XS admin",
|
||||
username: "用户名",
|
||||
password: "密码",
|
||||
login: "登 录",
|
||||
},
|
||||
// 导航栏国际化
|
||||
navbar: {
|
||||
logout: "注销",
|
||||
},
|
||||
common: {
|
||||
id: "编号",
|
||||
createTime: "创建时间",
|
||||
operate: "操作",
|
||||
edit: "编辑",
|
||||
delete: "删除",
|
||||
deleted: "状态",
|
||||
all: "全部",
|
||||
enable: "正常",
|
||||
disable: "禁用",
|
||||
search: "搜索",
|
||||
reset: "重设",
|
||||
add: "新增",
|
||||
confirm: "确定",
|
||||
cancel: "取消",
|
||||
copySuccess: "复制成功",
|
||||
subscribe: "订阅链接",
|
||||
subscribeQrCode: "订阅二维码",
|
||||
nodeUrl: "节点 URL",
|
||||
nodeQrCode: "节点二维码",
|
||||
resetTraffic: "重设流量",
|
||||
import: "导入",
|
||||
export: "导出",
|
||||
save: "保存",
|
||||
update: "更新",
|
||||
downloadSuccess: "下载成功",
|
||||
wait: "正在更换版本,请等待一会儿",
|
||||
enableSuccess: "Hysteria2 启动 !!",
|
||||
disableSuccess: "Hysteria2 已关闭",
|
||||
success: "成功",
|
||||
refresh: "刷新",
|
||||
yes: "是",
|
||||
no: "否",
|
||||
securityRisk: `安全隐患`,
|
||||
defaultPassTip: `请尽快修改默认登录密码,建议设置强密码以保护您的账户安全。<a href="/#/account/list?focus=change-pass" style="color: #00BFFF">点击这里</a>修改`,
|
||||
noHttpsTip: `您的网站未启用 HTTPS,数据传输不安全,请尽快开启 HTTPS 以保护用户信息。<a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">点击这里</a>开启`,
|
||||
},
|
||||
info: {
|
||||
expireTime: "年-月-日 时:分:秒",
|
||||
greeting1: "微凉扑面,清新的空气,唤醒一天的活力🌅!",
|
||||
greeting2: "上午好,",
|
||||
greeting3: "下午好,",
|
||||
greeting4: "晚上好,",
|
||||
greeting5: "我愿成为流星,划破黑夜,只为照亮你的梦境,晚安🌛!",
|
||||
},
|
||||
account: {
|
||||
remark: "备注",
|
||||
username: "用户名",
|
||||
pass: "登录密码",
|
||||
conPass: "连接密码",
|
||||
quota: "配额",
|
||||
download: "下载",
|
||||
upload: "上传",
|
||||
expireTime: "过期时间",
|
||||
kickUtilTimeLast: "下线剩余时间",
|
||||
kickUtilTime: "下线截止时间",
|
||||
deviceNo: "限制设备数",
|
||||
onlineStatus: "在线状态",
|
||||
online: "在线",
|
||||
offline: "离线",
|
||||
device: "在线设备数",
|
||||
role: "角色",
|
||||
unit: "单位",
|
||||
loginAt: "最近登录时间",
|
||||
conAt: "最近连接时间",
|
||||
createTime: "注册时间",
|
||||
releaseSuccess: "解除成功",
|
||||
kick: "下线",
|
||||
kickTip: "强制用户下线",
|
||||
releaseKick: "解除",
|
||||
releaseKickTip: "解除下线状态",
|
||||
},
|
||||
config: {
|
||||
huiWebPort: "HY2XS admin Web 端口",
|
||||
huiWebContext: "HY2XS admin Web 上下文",
|
||||
hysteria2TrafficTime: "Hysteria2 流量倍数",
|
||||
huiCrtPath: "HY2XS admin CRT 证书路径",
|
||||
huiKeyPath: "HY2XS admin KEY 证书路径",
|
||||
uploadCrtFile: "上传 CRT 证书",
|
||||
uploadKeyFile: "上传 KEY 证书",
|
||||
restartServer: "重启面板",
|
||||
restartTip: "正在重启,请刷新",
|
||||
useHysteria2Cert: "使用 Hysteria2 的证书",
|
||||
huiHttps: "面板开启 https",
|
||||
resetTrafficCron: "重设流量计划任务",
|
||||
resetTrafficCronTip:
|
||||
"计划任务表达式,参考:https://pkg.go.dev/github.com/robfig/cron/v3",
|
||||
resetTrafficMonth: "每月运行一次,每月第一天午夜",
|
||||
resetTrafficWeek: "每周运行一次,周六/周日午夜",
|
||||
},
|
||||
monitor: {
|
||||
huiVersion: "HY2XS admin 版本",
|
||||
cpuPercent: "CPU 使用率",
|
||||
memPercent: "内存使用率",
|
||||
diskPercent: "磁盘使用率",
|
||||
hysteria2UserTotal: "在线用户数",
|
||||
hysteria2DeviceTotal: "在线设备数",
|
||||
hysteria2Version: "Hysteria2 版本",
|
||||
hysteria2Running: "Hysteria2 状态",
|
||||
hysteria2RunningTrue: "运行",
|
||||
hysteria2RunningFalse: "停止",
|
||||
},
|
||||
log: {
|
||||
numLine: "显示行数",
|
||||
},
|
||||
hysteria: {
|
||||
enable: "开启",
|
||||
disable: "关闭",
|
||||
addConfigItem: "添加配置项",
|
||||
hysteria2Version: "Hysteria2 版本",
|
||||
hysteria2Running: "Hysteria2 状态",
|
||||
hysteria2ChangeVersion: "切换",
|
||||
addOutbound: "添加出站规则",
|
||||
extension: "扩展",
|
||||
listen: "监听地址",
|
||||
tls: "TLS",
|
||||
obfs: "混淆",
|
||||
quic: "QUIC 参数",
|
||||
bandwidth: "带宽",
|
||||
speedTest: "速度测试",
|
||||
udp: "UDP",
|
||||
resolver: "DNS 解析",
|
||||
sniff: "协议嗅探 (Sniff)",
|
||||
acl: "ACL",
|
||||
outbounds: "出站规则",
|
||||
http: "流量统计 API",
|
||||
masquerade: "伪装",
|
||||
config: {
|
||||
enable: "开启/关闭",
|
||||
remark: "别名",
|
||||
portHopping:
|
||||
"端口跳跃,多个单端口:1234,5678,9012;端口范围:20000-50000;两者的组合:1234,5000-6000,7044,8000-9000",
|
||||
clashExtension: "Clash 订阅扩展",
|
||||
listen:
|
||||
"当只有端口没有 IP 地址时,服务器将监听所有可用的 IPv4 和 IPv6 地址。要仅监听 IPv4,可以使用 0.0.0.0:443。要仅监听 IPv6,可以使用 [::]:443。",
|
||||
tlsType: "TLS 类型",
|
||||
tls: {
|
||||
cert: "CERT 路径",
|
||||
key: "KEY 路径",
|
||||
sniGuard:
|
||||
"验证客户端发送的 SNI。 与证书信息匹配时才建立连接, 否则终止 TLS 握手。 设置为 strict 以启用该功能。 设置为 disable 以禁用该功能。 默认为 dns-san, 仅当证书中包含「证书主题背景的备用名称」扩展且该扩展中包含域名时才启用该功能。",
|
||||
},
|
||||
acme: {
|
||||
domains: "域名",
|
||||
email: "邮箱",
|
||||
ca: "要使用的 CA。可以是 letsencrypt 或 zerossl。",
|
||||
listenHost:
|
||||
"用于 ACME 服务器验证的监听地址(不含端口)。默认监听所有可用的地址。",
|
||||
dir: "存储 ACME 账户密钥和证书的目录。",
|
||||
type: "ACME 验证类型。可以是 http, tls 或 dns。",
|
||||
http: {
|
||||
altPort:
|
||||
"用于 HTTP 挑战的监听端口。 (注意: 改为非 80 需要另行配置端口转发或者 HTTP 反向代理,否则证书会签署失败!)",
|
||||
},
|
||||
tls: {
|
||||
altPort:
|
||||
"用于 TLS-ALPN 挑战的监听端口。 (注意: 改为非 443 需要另行配置端口转发或者 SNI Proxy,否则证书会签署失败!)",
|
||||
},
|
||||
dns: {
|
||||
name: "DNS 提供商。详细信息请参考 ACME DNS 配置。",
|
||||
config: "ACME DNS 配置",
|
||||
},
|
||||
disableHTTP: "禁用 HTTP 挑战。",
|
||||
disableTLSALPN: "禁用 TLS-ALPN 挑战。",
|
||||
altHTTPPort:
|
||||
"用于 HTTP 挑战的监听端口。 (注意: 改为非 80 需要另行配置端口转发或者 HTTP 反向代理,否则证书会签署失败!)",
|
||||
altTLSALPNPort:
|
||||
"用于 TLS-ALPN 挑战的监听端口。 (注意: 改为非 443 需要另行配置端口转发或者 SNI Proxy,否则证书会签署失败!)",
|
||||
},
|
||||
obfs: {
|
||||
type: "类型",
|
||||
salamander: {
|
||||
password: "替换为你的混淆密码。",
|
||||
},
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: "初始的 QUIC 流接收窗口大小。",
|
||||
maxStreamReceiveWindow: "最大的 QUIC 流接收窗口大小。",
|
||||
initConnReceiveWindow: "初始的 QUIC 连接接收窗口大小。",
|
||||
maxConnReceiveWindow: "最大的 QUIC 连接接收窗口大小。",
|
||||
maxIdleTimeout:
|
||||
"最长空闲超时时间。服务器会在多长时间没有收到任何客户端数据后关闭连接。",
|
||||
maxIncomingStreams: "最大并发传入流的数量。",
|
||||
disablePathMTUDiscovery: "禁用 MTU 探测。",
|
||||
},
|
||||
bandwidth: {
|
||||
up: "上传",
|
||||
down: "下载",
|
||||
},
|
||||
ignoreClientBandwidth: "忽略客户端带宽设置",
|
||||
speedTest: "speedTest 启用后,服务端将允许客户端进行下载和上传速度测试。",
|
||||
disableUDP: "disableUDP 启用后服务端禁用 UDP 转发,只支持 TCP。",
|
||||
udpIdleTimeout:
|
||||
"udpIdleTimeout 用于指定服务器对于每个 UDP 会话,在没有流量时保持本地 UDP 端口的时间长度。概念上与 NAT 的 UDP 会话超时时间相似。",
|
||||
resolver: {
|
||||
type: "类型",
|
||||
tcp: {
|
||||
addr: "TCP DNS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
},
|
||||
udp: {
|
||||
addr: "UDP DNS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
},
|
||||
tls: {
|
||||
addr: "DNS over TLS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
sni: "DNS over TLS 服务器的 SNI。",
|
||||
insecure: "禁用 TLS 证书验证。",
|
||||
},
|
||||
https: {
|
||||
addr: "DNS over HTTPS 服务器地址。",
|
||||
timeout: "DNS 查询超时时间。",
|
||||
sni: "DNS over TLS 服务器的 SNI。",
|
||||
insecure: "禁用 TLS 证书验证。",
|
||||
},
|
||||
},
|
||||
sniff: {
|
||||
enable: "是否启用协议嗅探。",
|
||||
timeout:
|
||||
"嗅探超时时间。如果超过这个时间仍然无法确定协议/获取域名,将使用原地址发起连接。",
|
||||
rewriteDomain:
|
||||
"是否重写已经是域名的请求。如果启用,对于目标地址已经是域名的请求,仍会进行嗅探。",
|
||||
tcpPorts: "TCP 端口列表。只有这些端口的 TCP 请求会被嗅探。",
|
||||
udpPorts: "UDP 端口列表。只有这些端口的 UDP 请求会被嗅探。",
|
||||
},
|
||||
aclType: "ACL 类型",
|
||||
acl: {
|
||||
file: "ACL 文件的路径。",
|
||||
inline: "内联 ACL 规则的列表。",
|
||||
geoip:
|
||||
"可选。取消注释以启用。GeoIP 数据库文件的路径。如果省略这个字段,Hysteria 会自动下载最新的数据库到工作目录。",
|
||||
geosite:
|
||||
"可选。取消注释以启用。GeoSite 数据库文件的路径。如果省略这个字段,Hysteria 会自动下载最新的数据库到工作目录。",
|
||||
geoUpdateInterval:
|
||||
"可选。GeoIP/GeoSite 数据库刷新的间隔。默认为 168 小时(1 周)。仅在 GeoIP/GeoSite 数据库是自动下载的情况下生效。",
|
||||
},
|
||||
outbounds: {
|
||||
name: "出站规则的名称。在 ACL 中使用。",
|
||||
type: "类型",
|
||||
socks5: {
|
||||
addr: "SOCKS5 代理地址。",
|
||||
username: "可选。SOCKS5 代理用户名。",
|
||||
password: "可选。SOCKS5 代理密码。",
|
||||
},
|
||||
http: {
|
||||
url: "HTTP/HTTPS 代理 URL。(可以是 http:// 或 https:// 开头)",
|
||||
insecure: "可选。禁用 TLS 证书验证。仅适用于 HTTPS 代理。",
|
||||
},
|
||||
direct: {
|
||||
mode: "类型",
|
||||
bindIPv4: "要绑定的本地 IPv4 地址。",
|
||||
bindIPv6: "要绑定的本地 IPv6 地址。",
|
||||
bindDevice: "要绑定的本地网卡。",
|
||||
fastOpen: "启用 TCP 快速打开。",
|
||||
},
|
||||
},
|
||||
trafficStats: {
|
||||
listen: "监听地址。",
|
||||
},
|
||||
masquerade: {
|
||||
type: "类型",
|
||||
file: {
|
||||
dir: "用于提供文件的目录。",
|
||||
},
|
||||
proxy: {
|
||||
url: "要代理的网站的 URL。",
|
||||
rewriteHost:
|
||||
"是否重写 Host 头以匹配被代理的网站。如果目标网站通过 Host 识别请求的网站,这个选项是必须的。",
|
||||
insecure: "禁用对代理网站的 TLS 验证。",
|
||||
},
|
||||
string: {
|
||||
content: "要返回的字符串。",
|
||||
headers: "可选。要返回的 HTTP 头列表。",
|
||||
statusCode: "可选。要返回的 HTTP 状态码。默认为 200。",
|
||||
},
|
||||
listenHTTP: "HTTP (TCP) 监听地址。",
|
||||
listenHTTPS: "HTTPS (TCP) 监听地址。",
|
||||
forceHTTPS:
|
||||
"是否强制使用 HTTPS。如果启用,HTTP 请求将被重定向到 HTTPS。",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="router-fade" mode="out-in">
|
||||
<keep-alive :include="tagsViewStore.cachedViews">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
/* 50= navbar 50 */
|
||||
min-height: calc(100vh - 50px);
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
min-height: 100vh;
|
||||
padding-top: 84px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
import { useAccountStore } from "@/store/modules/account";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const accountStore = useAccountStore();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const { device } = storeToRefs(appStore); // 设备类型:desktop-宽屏设备 || mobile-窄屏设备
|
||||
|
||||
/**
|
||||
* 左侧菜单栏显示/隐藏
|
||||
*/
|
||||
function toggleSideBar() {
|
||||
appStore.toggleSidebar(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* vueUse 全屏
|
||||
*/
|
||||
const { isFullscreen, toggle } = useFullscreen();
|
||||
|
||||
/**
|
||||
* 注销
|
||||
*/
|
||||
function logout() {
|
||||
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
accountStore
|
||||
.logout()
|
||||
.then(() => {
|
||||
tagsViewStore.delAllViews();
|
||||
})
|
||||
.then(() => {
|
||||
router.push(`/login?redirect=${route.fullPath}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="navbar">
|
||||
<!-- 左侧面包屑 -->
|
||||
<div class="flex">
|
||||
<hamburger
|
||||
:is-active="appStore.sidebar.opened"
|
||||
@toggleClick="toggleSideBar"
|
||||
/>
|
||||
<breadcrumb />
|
||||
</div>
|
||||
|
||||
<!-- 右侧导航设置 -->
|
||||
<div class="flex">
|
||||
<!-- 导航栏设置(窄屏隐藏)-->
|
||||
<div class="setting-container" v-if="device !== 'mobile'">
|
||||
<!--全屏 -->
|
||||
<div class="setting-item" @click="toggle">
|
||||
<svg-icon
|
||||
:icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
|
||||
/>
|
||||
</div>
|
||||
<!-- 布局大小 -->
|
||||
<el-tooltip content="布局大小" effect="dark" placement="bottom">
|
||||
<size-select class="setting-item" />
|
||||
</el-tooltip>
|
||||
<!--语言选择-->
|
||||
<lang-select class="setting-item" />
|
||||
</div>
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<el-dropdown trigger="click">
|
||||
<div class="avatar-container">
|
||||
<img src="/src/assets/logo.png" />
|
||||
<i-ep-caret-bottom class="w-3 h-3" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="logout">
|
||||
{{ $t("navbar.logout") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 1px #0003;
|
||||
|
||||
.setting-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.setting-item {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
color: #5a5e66;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgb(249 250 251 / 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
|
||||
import IconEpSunny from "~icons/ep/sunny";
|
||||
import IconEpMoon from "~icons/ep/moon";
|
||||
|
||||
/**
|
||||
* 暗黑模式
|
||||
*/
|
||||
const settingsStore = useSettingsStore();
|
||||
const isDark = useDark();
|
||||
const toggleDark = () => useToggle(isDark);
|
||||
|
||||
/**
|
||||
* 切换布局
|
||||
*/
|
||||
function changeLayout(layout: string) {
|
||||
settingsStore.changeSetting({ key: "layout", value: layout });
|
||||
window.document.body.setAttribute("layout", settingsStore.layout);
|
||||
}
|
||||
|
||||
// 主题颜色
|
||||
const themeColors = ref<string[]>([
|
||||
"#409EFF",
|
||||
"#304156",
|
||||
"#11a983",
|
||||
"#13c2c2",
|
||||
"#6959CD",
|
||||
"#f5222d",
|
||||
]);
|
||||
|
||||
/**
|
||||
* 切换主题颜色
|
||||
*/
|
||||
function changeThemeColor(color: string) {
|
||||
settingsStore.changeSetting({ key: "themeColor", value: color });
|
||||
document.documentElement.style.setProperty(
|
||||
"--el-color-primary",
|
||||
settingsStore.themeColor
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.document.body.setAttribute("layout", settingsStore.layout);
|
||||
document.documentElement.style.setProperty(
|
||||
"--el-color-primary",
|
||||
settingsStore.themeColor
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<h3 class="text-base font-bold">项目配置</h3>
|
||||
<el-divider>主题</el-divider>
|
||||
|
||||
<div class="flex justify-center" @click.stop>
|
||||
<el-switch
|
||||
v-model="isDark"
|
||||
@change="toggleDark"
|
||||
inline-prompt
|
||||
:active-icon="IconEpMoon"
|
||||
:inactive-icon="IconEpSunny"
|
||||
active-color="var(--el-fill-color-dark)"
|
||||
inactive-color="var(--el-color-primary)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-divider>界面设置</el-divider>
|
||||
<div class="py-[8px] flex justify-between">
|
||||
<span class="text-xs">开启 Tags-View</span>
|
||||
<el-switch v-model="settingsStore.tagsView" />
|
||||
</div>
|
||||
|
||||
<div class="py-[8px] flex justify-between">
|
||||
<span class="text-xs">固定 Header</span>
|
||||
<el-switch v-model="settingsStore.fixedHeader" />
|
||||
</div>
|
||||
|
||||
<div class="py-[8px] flex justify-between">
|
||||
<span class="text-xs">侧边栏 Logo</span>
|
||||
<el-switch v-model="settingsStore.sidebarLogo" />
|
||||
</div>
|
||||
|
||||
<el-divider>主题颜色</el-divider>
|
||||
|
||||
<ul class="w-full space-x-2 flex justify-center py-2">
|
||||
<li
|
||||
class="inline-block w-[30px] h-[30px] cursor-pointer"
|
||||
v-for="(color, index) in themeColors"
|
||||
:key="index"
|
||||
:style="{ background: color }"
|
||||
@click="changeThemeColor(color)"
|
||||
></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings-container {
|
||||
padding: 16px;
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
width: 18%;
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&-item.is-active {
|
||||
border: 2px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
&-mix div:nth-child(1) {
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
background: #1b2a47;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
|
||||
&-mix div:nth-child(2) {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 30%;
|
||||
height: 70%;
|
||||
background: #1b2a47;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
|
||||
&-top div:nth-child(1) {
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
background: #1b2a47;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
|
||||
&-left div:nth-child(1) {
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
background: #1b2a47;
|
||||
}
|
||||
|
||||
&-left div:nth-child(2) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 70%;
|
||||
height: 30%;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { isExternal } from "@/utils/index";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
const appStore = useAppStore();
|
||||
|
||||
const sidebar = computed(() => appStore.sidebar);
|
||||
const device = computed(() => appStore.device);
|
||||
|
||||
const props = defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function push() {
|
||||
if (device.value === "mobile" && sidebar.value.opened == true) {
|
||||
appStore.closeSideBar(false);
|
||||
}
|
||||
router.push(props.to).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
|
||||
<slot />
|
||||
</a>
|
||||
<div v-else @click="push">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const logo = ref(new URL(`../../../assets/logo.png`, import.meta.url).href);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-[50px] bg-gray-800 dark:bg-[var(--el-bg-color-overlay)]">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link
|
||||
v-if="collapse"
|
||||
key="collapse"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
to="/"
|
||||
>
|
||||
<img v-if="settingsStore.sidebarLogo" :src="logo" class="w-5 h-5" />
|
||||
<span v-else class="ml-3 text-white text-sm font-bold">HY2XS</span>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
key="expand"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
to="/"
|
||||
>
|
||||
<img v-if="settingsStore.sidebarLogo" :src="logo" class="w-5 h-5" />
|
||||
<span class="ml-3 text-white text-sm font-bold">HY2XS</span>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// https://cn.vuejs.org/guide/built-ins/transition.html#the-transition-component
|
||||
.sidebarLogoFade-enter-active {
|
||||
transition: opacity 2s;
|
||||
}
|
||||
|
||||
.sidebarLogoFade-leave-active,
|
||||
.sidebarLogoFade-enter-from,
|
||||
.sidebarLogoFade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import path from "path-browserify";
|
||||
import { isExternal } from "@/utils/index";
|
||||
import AppLink from "./Link.vue";
|
||||
|
||||
import { translateRouteTitleI18n } from "@/utils/i18n";
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 路由(eg:level_3_1)
|
||||
*/
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* 父层级完整路由路径(eg:/level/level_3/level_3_1)
|
||||
*/
|
||||
basePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const onlyOneChild = ref(); // 临时变量,唯一子路由
|
||||
|
||||
/**
|
||||
* 判断当前路由是否只有一个子路由
|
||||
*
|
||||
* 1:如果只有一个子路由: 返回 true
|
||||
* 2:如果无子路由 :返回 true
|
||||
*
|
||||
* @param children 子路由数组
|
||||
* @param parent 当前路由
|
||||
*/
|
||||
function hasOneShowingChild(children = [], parent: any) {
|
||||
// 需要显示的子路由数组
|
||||
const showingChildren = children.filter((item: any) => {
|
||||
if (item.meta?.hidden) {
|
||||
return false; // 过滤不显示的子路由
|
||||
} else {
|
||||
onlyOneChild.value = item; // 唯一子路由赋值(多个子路由情况 onlyOneChild 变量是用不上的)
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// 1:如果只有一个子路由, 返回 true
|
||||
if (showingChildren.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2:如果无子路由, 复制当前路由信息作为其子路由,满足只拥有一个子路由的条件,所以返回 true
|
||||
if (showingChildren.length === 0) {
|
||||
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析路径
|
||||
*
|
||||
* @param routePath 路由路径
|
||||
*/
|
||||
function resolvePath(routePath: string) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath;
|
||||
}
|
||||
if (isExternal(props.basePath)) {
|
||||
return props.basePath;
|
||||
}
|
||||
// 完整路径 = 父级路径(/level/level_3) + 路由路径
|
||||
const fullPath = path.resolve(props.basePath, routePath); // 相对路径 → 绝对路径
|
||||
return fullPath;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="!item.meta || !item.meta.hidden">
|
||||
<!-- 只包含一个子路由节点的路由,显示其【唯一子路由】 -->
|
||||
<template
|
||||
v-if="
|
||||
hasOneShowingChild(item.children, item) &&
|
||||
(!onlyOneChild.children || onlyOneChild.noShowingChildren)
|
||||
"
|
||||
>
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)">
|
||||
<svg-icon
|
||||
v-if="onlyOneChild.meta && onlyOneChild.meta.icon"
|
||||
:icon-class="onlyOneChild.meta.icon"
|
||||
/>
|
||||
<template #title>
|
||||
{{ translateRouteTitleI18n(onlyOneChild.meta.title) }}
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
|
||||
<!-- 包含多个子路由 -->
|
||||
<el-sub-menu v-else :index="resolvePath(item.path)" teleported>
|
||||
<template #title>
|
||||
<svg-icon
|
||||
v-if="item.meta && item.meta.icon"
|
||||
:icon-class="item.meta.icon"
|
||||
/>
|
||||
<span v-if="item.meta && item.meta.title">{{
|
||||
translateRouteTitleI18n(item.meta.title)
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<sidebar-item
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import SidebarItem from "./SidebarItem.vue";
|
||||
import Logo from "./Logo.vue";
|
||||
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { storeToRefs } from "pinia";
|
||||
import variables from "@/styles/variables.module.scss";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { sidebarLogo } = storeToRefs(settingsStore);
|
||||
const route = useRoute();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'has-logo': sidebarLogo }">
|
||||
<logo v-if="sidebarLogo" :collapse="!appStore.sidebar.opened" />
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:default-active="route.path"
|
||||
:collapse="!appStore.sidebar.opened"
|
||||
:background-color="variables.menuBg"
|
||||
:text-color="variables.menuText"
|
||||
:active-text-color="variables.menuActiveText"
|
||||
:unique-opened="false"
|
||||
:collapse-transition="false"
|
||||
mode="vertical"
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="route in permissionStore.routes"
|
||||
:item="route"
|
||||
:key="route.path"
|
||||
:base-path="route.path"
|
||||
:is-collapse="!appStore.sidebar.opened"
|
||||
/>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore, TagView } from "@/store/modules/tagsView";
|
||||
|
||||
const tagAndTagSpacing = ref(4);
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
const emits = defineEmits(["scroll"]);
|
||||
const emitScroll = () => {
|
||||
emits("scroll");
|
||||
};
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const scrollWrapper = computed(
|
||||
() => proxy?.$refs.scrollContainer.$refs.wrapRef
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
scrollWrapper.value.addEventListener("scroll", emitScroll, true);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
scrollWrapper.value.removeEventListener("scroll", emitScroll);
|
||||
});
|
||||
|
||||
function handleScroll(e: WheelEvent) {
|
||||
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
|
||||
scrollWrapper.value.scrollLeft =
|
||||
scrollWrapper.value.scrollLeft + eventDelta / 4;
|
||||
}
|
||||
|
||||
function moveToTarget(currentTag: TagView) {
|
||||
const $container = proxy.$refs.scrollContainer.$el;
|
||||
const $containerWidth = $container.offsetWidth;
|
||||
const $scrollWrapper = scrollWrapper.value;
|
||||
|
||||
let firstTag = null;
|
||||
let lastTag = null;
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagsViewStore.visitedViews.length > 0) {
|
||||
firstTag = tagsViewStore.visitedViews[0];
|
||||
lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0;
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
|
||||
} else {
|
||||
const tagListDom = document.getElementsByClassName("tags-item");
|
||||
const currentIndex = tagsViewStore.visitedViews.findIndex(
|
||||
(item) => item === currentTag
|
||||
);
|
||||
let prevTag = null;
|
||||
let nextTag = null;
|
||||
for (const k in tagListDom) {
|
||||
if (k !== "length" && Object.hasOwnProperty.call(tagListDom, k)) {
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex - 1].path
|
||||
) {
|
||||
prevTag = tagListDom[k];
|
||||
}
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex + 1].path
|
||||
) {
|
||||
nextTag = tagListDom[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft =
|
||||
(nextTag as any).offsetLeft +
|
||||
(nextTag as any).offsetWidth +
|
||||
tagAndTagSpacing.value;
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft =
|
||||
(prevTag as any).offsetLeft - tagAndTagSpacing.value;
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
moveToTarget,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-scrollbar
|
||||
ref="scrollContainer"
|
||||
class="scroll-container"
|
||||
:vertical="false"
|
||||
@wheel.prevent="handleScroll"
|
||||
>
|
||||
<slot />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,373 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
ComponentInternalInstance,
|
||||
} from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
import path from "path-browserify";
|
||||
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import { translateRouteTitleI18n } from "@/utils/i18n";
|
||||
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
import { useTagsViewStore, TagView } from "@/store/modules/tagsView";
|
||||
import ScrollPane from "./ScrollPane.vue";
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const permissionStore = usePermissionStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const { visitedViews } = storeToRefs(tagsViewStore);
|
||||
|
||||
const selectedTag = ref({});
|
||||
const scrollPaneRef = ref();
|
||||
const left = ref(0);
|
||||
const top = ref(0);
|
||||
const affixTags = ref<TagView[]>([]);
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
addTags();
|
||||
moveToCurrentTag();
|
||||
},
|
||||
{
|
||||
//初始化立即执行
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const tagMenuVisible = ref(false); // 标签操作菜单显示状态
|
||||
watch(tagMenuVisible, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener("click", closeTagMenu);
|
||||
} else {
|
||||
document.body.removeEventListener("click", closeTagMenu);
|
||||
}
|
||||
});
|
||||
|
||||
function filterAffixTags(routes: any[], basePath = "/") {
|
||||
let tags: TagView[] = [];
|
||||
|
||||
routes.forEach((route) => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = path.resolve(basePath, route.path);
|
||||
tags.push({
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name,
|
||||
meta: { ...route.meta },
|
||||
});
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
const childTags = filterAffixTags(route.children, route.path);
|
||||
if (childTags.length >= 1) {
|
||||
tags = tags.concat(childTags);
|
||||
}
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
function initTags() {
|
||||
const tags: TagView[] = filterAffixTags(permissionStore.routes);
|
||||
affixTags.value = tags;
|
||||
for (const tag of tags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
tagsViewStore.addVisitedView(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addTags() {
|
||||
if (route.name) {
|
||||
tagsViewStore.addView(route);
|
||||
}
|
||||
}
|
||||
|
||||
function moveToCurrentTag() {
|
||||
nextTick(() => {
|
||||
for (const r of tagsViewStore.visitedViews) {
|
||||
if (r.path === route.path) {
|
||||
scrollPaneRef.value.moveToTarget(r);
|
||||
// when query is different then update
|
||||
if (r.fullPath !== route.fullPath) {
|
||||
tagsViewStore.updateVisitedView(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isActive(tag: TagView) {
|
||||
return tag.path === route.path;
|
||||
}
|
||||
|
||||
function isAffix(tag: TagView) {
|
||||
return tag.meta && tag.meta.affix;
|
||||
}
|
||||
|
||||
function isFirstView() {
|
||||
try {
|
||||
return (
|
||||
(selectedTag.value as TagView).fullPath ===
|
||||
tagsViewStore.visitedViews[1].fullPath ||
|
||||
(selectedTag.value as TagView).fullPath === "/index"
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isLastView() {
|
||||
try {
|
||||
return (
|
||||
(selectedTag.value as TagView).fullPath ===
|
||||
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSelectedTag(view: TagView) {
|
||||
tagsViewStore.delCachedView(view);
|
||||
const { fullPath } = view;
|
||||
nextTick(() => {
|
||||
router.replace({ path: "/redirect" + fullPath }).catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toLastView(visitedViews: TagView[], view?: any) {
|
||||
const latestView = visitedViews.slice(-1)[0];
|
||||
if (latestView && latestView.fullPath) {
|
||||
router.push(latestView.fullPath);
|
||||
} else {
|
||||
// now the default is to redirect to the home page if there is no tags-view,
|
||||
// you can adjust it according to your needs.
|
||||
if (view.name === "Dashboard") {
|
||||
// to reload home page
|
||||
router.replace({ path: "/redirect" + view.fullPath });
|
||||
} else {
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeSelectedTag(view: TagView) {
|
||||
tagsViewStore.delView(view).then((res: any) => {
|
||||
if (isActive(view)) {
|
||||
toLastView(res.visitedViews, view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeLeftTags() {
|
||||
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeRightTags() {
|
||||
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeOtherTags() {
|
||||
router.push(selectedTag.value);
|
||||
tagsViewStore.delOtherViews(selectedTag.value).then(() => {
|
||||
moveToCurrentTag();
|
||||
});
|
||||
}
|
||||
|
||||
function closeAllTags(view: TagView) {
|
||||
tagsViewStore.delAllViews().then((res: any) => {
|
||||
toLastView(res.visitedViews, view);
|
||||
});
|
||||
}
|
||||
|
||||
function openTagMenu(tag: TagView, e: MouseEvent) {
|
||||
const menuMinWidth = 105;
|
||||
|
||||
// console.log("test", proxy?.$el);
|
||||
|
||||
const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
|
||||
const offsetWidth = proxy?.$el.offsetWidth; // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth; // left boundary
|
||||
const l = e.clientX - offsetLeft + 15; // 15: margin right
|
||||
|
||||
if (l > maxLeft) {
|
||||
left.value = maxLeft;
|
||||
} else {
|
||||
left.value = l;
|
||||
}
|
||||
|
||||
top.value = e.clientY;
|
||||
tagMenuVisible.value = true;
|
||||
selectedTag.value = tag;
|
||||
}
|
||||
|
||||
function closeTagMenu() {
|
||||
tagMenuVisible.value = false;
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
closeTagMenu();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tags-container">
|
||||
<scroll-pane ref="scrollPaneRef" @scroll="handleScroll">
|
||||
<router-link
|
||||
:class="'tags-item ' + (isActive(tag) ? 'active' : '')"
|
||||
v-for="tag in visitedViews"
|
||||
:key="tag.path"
|
||||
:data-path="tag.path"
|
||||
:to="{ path: tag.path, query: tag.query }"
|
||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent="openTagMenu(tag, $event)"
|
||||
>
|
||||
{{ translateRouteTitleI18n(tag.meta?.title) }}
|
||||
<span
|
||||
v-if="!isAffix(tag)"
|
||||
class="tags-item-close"
|
||||
@click.prevent.stop="closeSelectedTag(tag)"
|
||||
>
|
||||
<i-ep-close class="text-[10px]" />
|
||||
</span>
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
|
||||
<!-- tag标签操作菜单 -->
|
||||
<ul
|
||||
v-show="tagMenuVisible"
|
||||
class="tag-menu"
|
||||
:style="{ left: left + 'px', top: top + 'px' }"
|
||||
>
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
<svg-icon icon-class="refresh" />
|
||||
刷新
|
||||
</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||
<svg-icon icon-class="close" />
|
||||
关闭
|
||||
</li>
|
||||
<li @click="closeOtherTags">
|
||||
<svg-icon icon-class="close_other" />
|
||||
关闭其它
|
||||
</li>
|
||||
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||
<svg-icon icon-class="close_left" />
|
||||
关闭左侧
|
||||
</li>
|
||||
<li v-if="!isLastView()" @click="closeRightTags">
|
||||
<svg-icon icon-class="close_right" />
|
||||
关闭右侧
|
||||
</li>
|
||||
<li @click="closeAllTags(selectedTag)">
|
||||
<svg-icon icon-class="close_all" />
|
||||
关闭所有
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-container {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
box-shadow: 0 1px 1px var(--el-box-shadow-light);
|
||||
|
||||
.tags-item {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
margin: 4px 0 0 5px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 5px;
|
||||
content: "";
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&-close {
|
||||
border-radius: 100%;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: rgb(0 0 0 / 16%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-menu {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
font-size: 12px;
|
||||
background: var(--el-bg-color-overlay);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
|
||||
li {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as Navbar } from "./Navbar.vue";
|
||||
export { default as AppMain } from "./AppMain.vue";
|
||||
export { default as Settings } from "./Settings/index.vue";
|
||||
export { default as TagsView } from "./TagsView/index.vue";
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watchEffect } from "vue";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { AppMain, Navbar, Settings, TagsView } from "./components/index";
|
||||
import Sidebar from "./components/Sidebar/index.vue";
|
||||
import RightPanel from "@/components/RightPanel/index.vue";
|
||||
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
/**
|
||||
* 响应式布局容器固定宽度
|
||||
*
|
||||
* 大屏(>=1200px)
|
||||
* 中屏(>=992px)
|
||||
* 小屏(>=768px)
|
||||
*/
|
||||
const WIDTH = 992;
|
||||
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const fixedHeader = computed(() => settingsStore.fixedHeader);
|
||||
const showTagsView = computed(() => settingsStore.tagsView);
|
||||
const showSettings = computed(() => settingsStore.showSettings);
|
||||
|
||||
const classObj = computed(() => ({
|
||||
hideSidebar: !appStore.sidebar.opened,
|
||||
openSidebar: appStore.sidebar.opened,
|
||||
withoutAnimation: appStore.sidebar.withoutAnimation,
|
||||
mobile: appStore.device === "mobile",
|
||||
}));
|
||||
|
||||
watchEffect(() => {
|
||||
if (width.value < WIDTH) {
|
||||
appStore.toggleDevice("mobile");
|
||||
appStore.closeSideBar(true);
|
||||
} else {
|
||||
appStore.toggleDevice("desktop");
|
||||
|
||||
if (width.value >= 1200) {
|
||||
//大屏
|
||||
appStore.openSideBar(true);
|
||||
} else {
|
||||
appStore.closeSideBar(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function handleOutsideClick() {
|
||||
appStore.closeSideBar(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<!-- 手机设备侧边栏打开遮罩层 -->
|
||||
<div
|
||||
v-if="classObj.mobile && classObj.openSidebar"
|
||||
class="drawer-bg"
|
||||
@click="handleOutsideClick"
|
||||
></div>
|
||||
|
||||
<Sidebar class="sidebar-container" />
|
||||
|
||||
<div :class="{ hasTagsView: showTagsView }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<navbar />
|
||||
<tags-view v-if="showTagsView" />
|
||||
</div>
|
||||
|
||||
<!--主页面-->
|
||||
<app-main />
|
||||
|
||||
<!-- 设置面板 -->
|
||||
<RightPanel v-if="showSettings">
|
||||
<settings />
|
||||
</RightPanel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-wrapper {
|
||||
&::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.mobile.openSidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - #{$sideBarWidth});
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "@/router";
|
||||
import { setupStore } from "@/store";
|
||||
import { setupDirective } from "@/directive";
|
||||
|
||||
import "@/permission";
|
||||
|
||||
// 本地SVG图标
|
||||
import "virtual:svg-icons-register";
|
||||
|
||||
// 国际化
|
||||
import i18n from "@/lang/index";
|
||||
|
||||
// 样式
|
||||
import "element-plus/theme-chalk/dark/css-vars.css";
|
||||
import "@/styles/index.scss";
|
||||
import "uno.css";
|
||||
|
||||
const app = createApp(App);
|
||||
// 全局注册 自定义指令(directive)
|
||||
setupDirective(app);
|
||||
// 全局注册 状态管理(store)
|
||||
setupStore(app);
|
||||
|
||||
app.use(router).use(i18n).mount("#app");
|
||||
@@ -0,0 +1,62 @@
|
||||
import router from "@/router";
|
||||
import { useAccountStoreHook } from "@/store/modules/account";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
|
||||
NProgress.configure({ showSpinner: false }); // 进度条
|
||||
|
||||
const permissionStore = usePermissionStoreHook();
|
||||
|
||||
// 白名单路由
|
||||
const whiteList = ["/login", "/register"];
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start();
|
||||
const hasToken = localStorage.getItem("accessToken");
|
||||
if (hasToken) {
|
||||
if (to.path === "/login") {
|
||||
// 如果已登录,跳转首页
|
||||
next({ path: "/" });
|
||||
NProgress.done();
|
||||
} else {
|
||||
const AccountStore = useAccountStoreHook();
|
||||
const hasRoles = AccountStore.roles && AccountStore.roles.length > 0;
|
||||
if (hasRoles) {
|
||||
// 未匹配到任何路由,跳转404
|
||||
if (to.matched.length === 0) {
|
||||
from.name ? next({ name: from.name }) : next("/404");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { roles } = await AccountStore.getAccountInfo();
|
||||
const accessRoutes = permissionStore.generateRoutes(roles);
|
||||
accessRoutes.forEach((route) => {
|
||||
router.addRoute(route);
|
||||
});
|
||||
next({ ...to, replace: true });
|
||||
} catch (error) {
|
||||
// 移除 token 并跳转登录页
|
||||
await AccountStore.resetToken();
|
||||
next(`/login?redirect=${to.path}`);
|
||||
NProgress.done();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 未登录可以访问白名单页面
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
next();
|
||||
} else {
|
||||
next(`/login?redirect=${to.path}`);
|
||||
NProgress.done();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done();
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
RouteLocationNormalized,
|
||||
RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
|
||||
export const Layout = () => import("@/layout/index.vue");
|
||||
|
||||
// 静态路由
|
||||
export const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/redirect",
|
||||
component: Layout,
|
||||
meta: { hidden: true },
|
||||
children: [
|
||||
{
|
||||
path: "/redirect/:path(.*)",
|
||||
component: () => import("@/views/redirect/index.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: "/login",
|
||||
component: () => import("@/views/login/index.vue"),
|
||||
meta: { hidden: true },
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: Layout,
|
||||
redirect: "/info/account",
|
||||
children: [
|
||||
{
|
||||
path: "401",
|
||||
component: () => import("@/views/error-page/401.vue"),
|
||||
meta: { hidden: true },
|
||||
},
|
||||
{
|
||||
path: "404",
|
||||
component: () => import("@/views/error-page/404.vue"),
|
||||
meta: { hidden: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const asyncRoutes: any[] = [
|
||||
{
|
||||
path: "/info",
|
||||
component: "Layout",
|
||||
redirect: "/account",
|
||||
name: "Info",
|
||||
meta: {
|
||||
title: "info",
|
||||
icon: "user",
|
||||
roles: ["user", "admin"],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "account",
|
||||
component: "info/account/index",
|
||||
name: "AccountInfo",
|
||||
meta: {
|
||||
title: "infoAccount",
|
||||
icon: "user",
|
||||
roles: ["user", "admin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/account",
|
||||
component: "Layout",
|
||||
redirect: "/list",
|
||||
name: "Account",
|
||||
meta: { title: "account", icon: "users", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "list",
|
||||
component: "account/list/index",
|
||||
name: "AccountList",
|
||||
meta: {
|
||||
title: "accountList",
|
||||
icon: "users",
|
||||
roles: ["admin"],
|
||||
},
|
||||
props: (route: RouteLocationNormalized) => ({
|
||||
focus: route.query.focus,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/hysteria",
|
||||
component: "Layout",
|
||||
redirect: "/list",
|
||||
name: "Hysteria",
|
||||
meta: { title: "hysteria", icon: "hysteria", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "list",
|
||||
component: "hysteria/list/index",
|
||||
name: "HysteriaList",
|
||||
meta: {
|
||||
title: "hysteriaList",
|
||||
icon: "hysteria",
|
||||
roles: ["admin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/config",
|
||||
component: "Layout",
|
||||
redirect: "/list",
|
||||
name: "Config",
|
||||
meta: { title: "config", icon: "setting", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "list",
|
||||
component: "config/list/index",
|
||||
name: "ConfigList",
|
||||
meta: {
|
||||
title: "configList",
|
||||
icon: "setting",
|
||||
roles: ["admin"],
|
||||
},
|
||||
props: (route: RouteLocationNormalized) => ({
|
||||
focus: route.query.focus,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/monitor",
|
||||
component: "Layout",
|
||||
redirect: "/monitor",
|
||||
name: "Monitor",
|
||||
meta: { title: "monitor", icon: "report", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "system",
|
||||
component: "monitor/system/index",
|
||||
name: "MonitorSystem",
|
||||
meta: {
|
||||
title: "monitorSystem",
|
||||
icon: "report",
|
||||
roles: ["admin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/log",
|
||||
component: "Layout",
|
||||
redirect: "/system",
|
||||
name: "Log",
|
||||
meta: { title: "log", icon: "error", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "system",
|
||||
component: "log/system/index",
|
||||
name: "LogSystem",
|
||||
meta: {
|
||||
title: "logSystem",
|
||||
icon: "log-system",
|
||||
roles: ["admin"],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "hysteria",
|
||||
component: "log/hysteria/index",
|
||||
name: "LogHysteria",
|
||||
meta: {
|
||||
title: "logHysteria",
|
||||
icon: "log-hysteria",
|
||||
roles: ["admin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 创建路由
|
||||
*/
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: constantRoutes as RouteRecordRaw[],
|
||||
// 刷新时,滚动条位置还原
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
});
|
||||
|
||||
/**
|
||||
* 重置路由
|
||||
*/
|
||||
export function resetRouter() {
|
||||
router.replace({ path: "/login" });
|
||||
location.reload();
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,67 @@
|
||||
// 系统设置
|
||||
interface DefaultSettings {
|
||||
/**
|
||||
* 系统title
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* 是否显示设置
|
||||
*/
|
||||
showSettings: boolean;
|
||||
/**
|
||||
* 是否显示多标签导航
|
||||
*/
|
||||
tagsView: boolean;
|
||||
/**
|
||||
*是否固定头部
|
||||
*/
|
||||
fixedHeader: boolean;
|
||||
/**
|
||||
* 是否显示侧边栏Logo
|
||||
*/
|
||||
sidebarLogo: boolean;
|
||||
/**
|
||||
* 导航栏布局
|
||||
*/
|
||||
layout: string;
|
||||
/**
|
||||
* 主题颜色
|
||||
*/
|
||||
themeColor: string;
|
||||
/**
|
||||
* 主题模式
|
||||
*/
|
||||
theme: string;
|
||||
|
||||
/**
|
||||
* 布局大小
|
||||
*/
|
||||
size: string;
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
language: string;
|
||||
}
|
||||
|
||||
const defaultSettings: DefaultSettings = {
|
||||
title: "HY2XS admin",
|
||||
showSettings: true,
|
||||
tagsView: true,
|
||||
fixedHeader: false,
|
||||
sidebarLogo: true,
|
||||
layout: "left",
|
||||
themeColor: "#409EFF",
|
||||
/**
|
||||
* 主题模式
|
||||
*
|
||||
* dark:暗黑模式
|
||||
* light: 明亮模式
|
||||
*/
|
||||
theme: "dark",
|
||||
size: "default", // default |large |small
|
||||
language: "ru", // ru | en
|
||||
};
|
||||
|
||||
export default defaultSettings;
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { App } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
// 全局注册 store
|
||||
export function setupStore(app: App<Element>) {
|
||||
app.use(store);
|
||||
}
|
||||
|
||||
export { store };
|
||||
@@ -0,0 +1,91 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
import { getAccountInfoApi, loginApi } from "@/api/account";
|
||||
import { resetRouter } from "@/router";
|
||||
import { store } from "@/store";
|
||||
|
||||
import { AccountInfo, AccountLoginDto } from "@/api/account/types";
|
||||
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
export const useAccountStore = defineStore("account", () => {
|
||||
// state
|
||||
const token = useStorage("accessToken", "");
|
||||
const id = ref(0);
|
||||
const username = ref("");
|
||||
const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
function login(accountLoginDto: AccountLoginDto) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
loginApi(accountLoginDto)
|
||||
.then((response) => {
|
||||
const { tokenType, accessToken } = response.data;
|
||||
token.value = tokenType + " " + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 查询当前
|
||||
function getAccountInfo() {
|
||||
return new Promise<AccountInfo>((resolve, reject) => {
|
||||
getAccountInfoApi()
|
||||
.then(({ data }) => {
|
||||
if (!data) {
|
||||
return reject("Verification failed, please Login again.");
|
||||
}
|
||||
if (!data.roles || data.roles.length <= 0) {
|
||||
reject("getAccountInfoApi: roles must be a non-null array!");
|
||||
}
|
||||
id.value = data.id;
|
||||
username.value = data.username;
|
||||
roles.value = data.roles;
|
||||
resolve(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 注销
|
||||
function logout() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
resetRouter();
|
||||
resetToken();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// 重置
|
||||
function resetToken() {
|
||||
token.value = "";
|
||||
id.value = 0;
|
||||
username.value = "";
|
||||
roles.value = [];
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
id,
|
||||
username,
|
||||
roles,
|
||||
login,
|
||||
getAccountInfo,
|
||||
logout,
|
||||
resetToken,
|
||||
};
|
||||
});
|
||||
|
||||
// 非setup
|
||||
export function useAccountStoreHook() {
|
||||
return useAccountStore(store);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import defaultSettings from "@/settings";
|
||||
|
||||
import en from "element-plus/es/locale/lang/en";
|
||||
import ru from "element-plus/es/locale/lang/ru";
|
||||
|
||||
// setup
|
||||
export const useAppStore = defineStore("app", () => {
|
||||
// state
|
||||
const device = useStorage("device", "desktop");
|
||||
const size = useStorage<any>("size", defaultSettings.size);
|
||||
const language = useStorage("language", defaultSettings.language);
|
||||
|
||||
const sidebarStatus = useStorage("sidebarStatus", "closed");
|
||||
const sidebar = reactive({
|
||||
opened: sidebarStatus.value !== "closed",
|
||||
withoutAnimation: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据语言标识读取对应的语言包
|
||||
*/
|
||||
const locale = computed(() => {
|
||||
return language?.value == "en" ? en : ru;
|
||||
});
|
||||
|
||||
// actions
|
||||
function toggleSidebar(withoutAnimation: boolean) {
|
||||
sidebar.opened = !sidebar.opened;
|
||||
sidebar.withoutAnimation = withoutAnimation;
|
||||
if (sidebar.opened) {
|
||||
sidebarStatus.value = "opened";
|
||||
} else {
|
||||
sidebarStatus.value = "closed";
|
||||
}
|
||||
}
|
||||
|
||||
function closeSideBar(withoutAnimation: boolean) {
|
||||
sidebar.opened = false;
|
||||
sidebar.withoutAnimation = withoutAnimation;
|
||||
sidebarStatus.value = "closed";
|
||||
}
|
||||
|
||||
function openSideBar(withoutAnimation: boolean) {
|
||||
sidebar.opened = true;
|
||||
sidebar.withoutAnimation = withoutAnimation;
|
||||
sidebarStatus.value = "opened";
|
||||
}
|
||||
|
||||
function toggleDevice(val: string) {
|
||||
device.value = val;
|
||||
}
|
||||
|
||||
function changeSize(val: string) {
|
||||
size.value = val;
|
||||
}
|
||||
/**
|
||||
* 切换语言
|
||||
*
|
||||
* @param val
|
||||
*/
|
||||
function changeLanguage(val: string) {
|
||||
language.value = val;
|
||||
}
|
||||
|
||||
return {
|
||||
device,
|
||||
sidebar,
|
||||
language,
|
||||
locale,
|
||||
size,
|
||||
toggleDevice,
|
||||
changeSize,
|
||||
changeLanguage,
|
||||
toggleSidebar,
|
||||
closeSideBar,
|
||||
openSideBar,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import { defineStore } from "pinia";
|
||||
import { asyncRoutes, constantRoutes } from "@/router";
|
||||
import { store } from "@/store";
|
||||
|
||||
const modules = import.meta.glob("../../views/**/**.vue");
|
||||
const Layout = () => import("@/layout/index.vue");
|
||||
|
||||
/**
|
||||
* Use meta.role to determine if the current user has permission
|
||||
*
|
||||
* @param roles 用户角色集合
|
||||
* @param route 路由
|
||||
* @returns
|
||||
*/
|
||||
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
|
||||
if (route.meta && route.meta.roles) {
|
||||
// 角色【超级管理员】拥有所有权限,忽略校验
|
||||
if (roles.includes("admin")) {
|
||||
return true;
|
||||
}
|
||||
return roles.some((role) => {
|
||||
if (route.meta?.roles !== undefined) {
|
||||
return (route.meta.roles as string[]).includes(role);
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 递归过滤有权限的异步(动态)路由
|
||||
*
|
||||
* @param routes 接口返回的异步(动态)路由
|
||||
* @param roles 用户角色集合
|
||||
* @returns 返回用户有权限的异步(动态)路由
|
||||
*/
|
||||
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
|
||||
const asyncRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
routes.forEach((route) => {
|
||||
const tmpRoute = { ...route }; // ES6扩展运算符复制新对象
|
||||
|
||||
// 判断用户(角色)是否有该路由的访问权限
|
||||
if (hasPermission(roles, tmpRoute)) {
|
||||
if (tmpRoute.component?.toString() == "Layout") {
|
||||
tmpRoute.component = Layout;
|
||||
} else {
|
||||
const component = modules[`../../views/${tmpRoute.component}.vue`];
|
||||
if (component) {
|
||||
tmpRoute.component = component;
|
||||
} else {
|
||||
tmpRoute.component = modules[`../../views/error-page/404.vue`];
|
||||
}
|
||||
}
|
||||
|
||||
if (tmpRoute.children) {
|
||||
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, roles);
|
||||
}
|
||||
|
||||
asyncRoutes.push(tmpRoute);
|
||||
}
|
||||
});
|
||||
|
||||
return asyncRoutes;
|
||||
};
|
||||
|
||||
// setup
|
||||
export const usePermissionStore = defineStore("permission", () => {
|
||||
// state
|
||||
const routes = ref<RouteRecordRaw[]>([]);
|
||||
|
||||
// actions
|
||||
function setRoutes(newRoutes: RouteRecordRaw[]) {
|
||||
routes.value = constantRoutes.concat(newRoutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成动态路由
|
||||
*
|
||||
* @param roles 用户角色集合
|
||||
* @returns
|
||||
*/
|
||||
function generateRoutes(roles: string[]) {
|
||||
// 根据角色获取有访问权限的路由
|
||||
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
|
||||
setRoutes(accessedRoutes);
|
||||
return accessedRoutes;
|
||||
}
|
||||
|
||||
return { routes, setRoutes, generateRoutes };
|
||||
});
|
||||
|
||||
// 非setup
|
||||
export function usePermissionStoreHook() {
|
||||
return usePermissionStore(store);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { defineStore } from "pinia";
|
||||
import defaultSettings from "@/settings";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
export const useSettingsStore = defineStore("setting", () => {
|
||||
// state
|
||||
const tagsView = useStorage<boolean>("tagsView", defaultSettings.tagsView);
|
||||
|
||||
const showSettings = ref<boolean>(defaultSettings.showSettings);
|
||||
const fixedHeader = ref<boolean>(defaultSettings.fixedHeader);
|
||||
const sidebarLogo = ref<boolean>(defaultSettings.sidebarLogo);
|
||||
|
||||
const layout = useStorage<string>("layout", defaultSettings.layout);
|
||||
|
||||
const themeColor = useStorage<string>(
|
||||
"themeColor",
|
||||
defaultSettings.themeColor
|
||||
);
|
||||
|
||||
// actions
|
||||
function changeSetting(param: { key: string; value: any }) {
|
||||
const { key, value } = param;
|
||||
switch (key) {
|
||||
case "showSettings":
|
||||
showSettings.value = value;
|
||||
break;
|
||||
case "fixedHeader":
|
||||
fixedHeader.value = value;
|
||||
break;
|
||||
case "tagsView":
|
||||
tagsView.value = value;
|
||||
break;
|
||||
case "sidevarLogo":
|
||||
sidebarLogo.value = value;
|
||||
break;
|
||||
case "layout":
|
||||
layout.value = value;
|
||||
break;
|
||||
case "themeColor":
|
||||
themeColor.value = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showSettings,
|
||||
tagsView,
|
||||
fixedHeader,
|
||||
sidebarLogo,
|
||||
layout,
|
||||
themeColor,
|
||||
changeSetting,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { RouteLocationNormalized } from "vue-router";
|
||||
|
||||
export interface TagView extends Partial<RouteLocationNormalized> {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// setup
|
||||
export const useTagsViewStore = defineStore("tagsView", () => {
|
||||
// state
|
||||
const visitedViews = ref<TagView[]>([]);
|
||||
const cachedViews = ref<string[]>([]);
|
||||
|
||||
// actions
|
||||
function addVisitedView(view: TagView) {
|
||||
if (visitedViews.value.some((v) => v.path === view.path)) return;
|
||||
if (view.meta && view.meta.affix) {
|
||||
visitedViews.value.unshift(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || "no-name",
|
||||
})
|
||||
);
|
||||
} else {
|
||||
visitedViews.value.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || "no-name",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function addCachedView(view: TagView) {
|
||||
const viewName = view.name as string;
|
||||
if (cachedViews.value.includes(viewName)) return;
|
||||
if (view.meta?.keepAlive) {
|
||||
cachedViews.value.push(viewName);
|
||||
}
|
||||
}
|
||||
|
||||
function delVisitedView(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
for (const [i, v] of visitedViews.value.entries()) {
|
||||
if (v.path === view.path) {
|
||||
visitedViews.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delCachedView(view: TagView) {
|
||||
const viewName = view.name as string;
|
||||
return new Promise((resolve) => {
|
||||
const index = cachedViews.value.indexOf(viewName);
|
||||
index > -1 && cachedViews.value.splice(index, 1);
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherVisitedViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
visitedViews.value = visitedViews.value.filter((v) => {
|
||||
return v.meta?.affix || v.path === view.path;
|
||||
});
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherCachedViews(view: TagView) {
|
||||
const viewName = view.name as string;
|
||||
return new Promise((resolve) => {
|
||||
const index = cachedViews.value.indexOf(viewName);
|
||||
if (index > -1) {
|
||||
cachedViews.value = cachedViews.value.slice(index, index + 1);
|
||||
} else {
|
||||
// if index = -1, there is no cached tags
|
||||
cachedViews.value = [];
|
||||
}
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function updateVisitedView(view: TagView) {
|
||||
for (let v of visitedViews.value) {
|
||||
if (v.path === view.path) {
|
||||
v = Object.assign(v, view);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addView(view: TagView) {
|
||||
addVisitedView(view);
|
||||
addCachedView(view);
|
||||
}
|
||||
|
||||
function delView(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
delVisitedView(view);
|
||||
delCachedView(view);
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
delOtherVisitedViews(view);
|
||||
delOtherCachedViews(view);
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delLeftViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
const currIndex = visitedViews.value.findIndex(
|
||||
(v) => v.path === view.path
|
||||
);
|
||||
if (currIndex === -1) {
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||
// affix:true 固定tag,例如“首页”
|
||||
if (index >= currIndex || (item.meta && item.meta.affix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cacheIndex = cachedViews.value.indexOf(item.name as string);
|
||||
if (cacheIndex > -1) {
|
||||
cachedViews.value.splice(cacheIndex, 1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
function delRightViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
const currIndex = visitedViews.value.findIndex(
|
||||
(v) => v.path === view.path
|
||||
);
|
||||
if (currIndex === -1) {
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||
// affix:true 固定tag,例如“首页”
|
||||
if (index <= currIndex || (item.meta && item.meta.affix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cacheIndex = cachedViews.value.indexOf(item.name as string);
|
||||
if (cacheIndex > -1) {
|
||||
cachedViews.value.splice(cacheIndex, 1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delAllViews() {
|
||||
return new Promise((resolve) => {
|
||||
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
|
||||
visitedViews.value = affixTags;
|
||||
cachedViews.value = [];
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delAllVisitedViews() {
|
||||
return new Promise((resolve) => {
|
||||
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
|
||||
visitedViews.value = affixTags;
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delAllCachedViews() {
|
||||
return new Promise((resolve) => {
|
||||
cachedViews.value = [];
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
visitedViews,
|
||||
cachedViews,
|
||||
addVisitedView,
|
||||
addCachedView,
|
||||
delVisitedView,
|
||||
delCachedView,
|
||||
delOtherVisitedViews,
|
||||
delOtherCachedViews,
|
||||
updateVisitedView,
|
||||
addView,
|
||||
delView,
|
||||
delOtherViews,
|
||||
delLeftViews,
|
||||
delRightViews,
|
||||
delAllViews,
|
||||
delAllVisitedViews,
|
||||
delAllCachedViews,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
html.dark {
|
||||
--menuBg: var(--el-bg-color-overlay);
|
||||
--menuText: #fff;
|
||||
--menuActiveText: var(--el-menu-active-color);
|
||||
--menuHover: rgb(0 0 0 / 20%);
|
||||
--subMenuBg: var(--el-menu-bg-color);
|
||||
--subMenuActiveText: var(--el-menu-active-color);
|
||||
--subMenuHover: rgb(0 0 0 / 20%);
|
||||
|
||||
.navbar {
|
||||
color: var(--el-text-color-regular);
|
||||
background-color: var(--el-bg-color);
|
||||
|
||||
.setting-container .setting-item:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel-btn {
|
||||
background-color: var(--el-color-primary-dark);
|
||||
}
|
||||
|
||||
.svg-icon,
|
||||
svg {
|
||||
fill: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
.el-menu-item.is-active .svg-icon {
|
||||
fill: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
:root {
|
||||
// 这里可以设置你自定义的颜色变量
|
||||
// 这个是element主要按钮:active的颜色,当主题更改后此变量的值也随之更改
|
||||
--el-color-primary-dark: #0d84ff;
|
||||
}
|
||||
|
||||
// 覆盖 element-plus 的样式
|
||||
.el-breadcrumb__inner,
|
||||
.el-breadcrumb__inner a {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.el-upload {
|
||||
input[type="file"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload__input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// dropdown
|
||||
.el-dropdown-menu {
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// to fix el-date-picker css style
|
||||
.el-range-separator {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
// 选中行背景色值
|
||||
.el-table__body tr.current-row td {
|
||||
background-color: #e1f3d8b5 !important;
|
||||
}
|
||||
|
||||
// card 的header统一高度
|
||||
.el-card__header {
|
||||
height: 60px !important;
|
||||
}
|
||||
|
||||
// 表格表头和表体未对齐
|
||||
.el-table__header col[name="gutter"] {
|
||||
display: table-cell !important;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
@import "./sidebar";
|
||||
@import "./reset";
|
||||
@import "./dark";
|
||||
@import "./element-plus";
|
||||
|
||||
.app-container {
|
||||
margin: 20px;
|
||||
|
||||
.search {
|
||||
padding: 18px 0 0 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-color: currentcolor;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
line-height: 1.5;
|
||||
tab-size: 4;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
||||
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
line-height: inherit;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: -0.15em; //因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果
|
||||
}
|
||||
|
||||
ul,
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
a:active,
|
||||
div:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
#app {
|
||||
.main-container {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
margin-left: $sideBarWidth;
|
||||
transition: margin-left 0.28s;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
width: $sideBarWidth !important;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: $menuBg;
|
||||
transition: width 0.28s;
|
||||
|
||||
// reset element-ui css
|
||||
.horizontal-collapse-transition {
|
||||
transition: 0s width ease-in-out, 0s padding-left ease-in-out,
|
||||
0s padding-right ease-in-out;
|
||||
}
|
||||
|
||||
.scrollbar-wrapper {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.el-scrollbar__bar.is-vertical {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.has-logo {
|
||||
.el-scrollbar {
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin-right: 12px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
// menu hover
|
||||
.el-sub-menu__title {
|
||||
&:hover {
|
||||
background-color: $menuHover !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active > .el-sub-menu__title {
|
||||
color: $subMenuActiveText !important;
|
||||
}
|
||||
|
||||
& .nest-menu .el-sub-menu > .el-sub-menu__title,
|
||||
& .el-sub-menu .el-menu-item {
|
||||
min-width: $sideBarWidth !important;
|
||||
background-color: $subMenuBg !important;
|
||||
|
||||
&:hover {
|
||||
background-color: $subMenuHover !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hideSidebar {
|
||||
.sidebar-container {
|
||||
width: 54px !important;
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
margin-left: 54px;
|
||||
}
|
||||
|
||||
.el-sub-menu {
|
||||
overflow: hidden;
|
||||
|
||||
& > .el-sub-menu__title {
|
||||
padding: 0 !important;
|
||||
|
||||
.svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin-left: 19px;
|
||||
}
|
||||
|
||||
.el-sub-menu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu--collapse {
|
||||
.el-sub-menu {
|
||||
& > .el-sub-menu__title {
|
||||
& > span {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu--collapse .el-menu .el-sub-menu {
|
||||
min-width: $sideBarWidth !important;
|
||||
}
|
||||
|
||||
// mobile responsive
|
||||
.mobile {
|
||||
.main-container {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
width: $sideBarWidth !important;
|
||||
transition: transform 0.28s;
|
||||
}
|
||||
|
||||
&.hideSidebar {
|
||||
.sidebar-container {
|
||||
pointer-events: none;
|
||||
transition-duration: 0.3s;
|
||||
transform: translate3d(-$sideBarWidth, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.withoutAnimation {
|
||||
.main-container,
|
||||
.sidebar-container {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// when menu collapsed
|
||||
.el-menu--vertical {
|
||||
& > .el-menu {
|
||||
.svg-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin-right: 12px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.nest-menu .el-sub-menu > .el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
// you can use $subMenuHover
|
||||
background-color: $menuHover !important;
|
||||
}
|
||||
}
|
||||
|
||||
// the scroll bar appears when the subMenu is too long
|
||||
> .el-menu--popup {
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background: #d3dce6;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #99a9bf;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// 导出 variables.module.scss 变量提供给TypeScript使用
|
||||
:export {
|
||||
menuBg: $menuBg;
|
||||
menuText: $menuText;
|
||||
menuActiveText: $menuActiveText;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 全局SCSS变量
|
||||
|
||||
:root {
|
||||
--menuBg: #304156;
|
||||
--menuText: #bfcbd9;
|
||||
--menuActiveText: #409eff;
|
||||
--menuHover: #263445;
|
||||
--subMenuBg: #1f2d3d;
|
||||
--subMenuActiveText: #f4f4f5;
|
||||
--subMenuHover: #001528;
|
||||
}
|
||||
|
||||
$menuBg: var(--menuBg);
|
||||
$menuText: var(--menuText);
|
||||
$menuActiveText: var(--menuActiveText);
|
||||
$menuHover: var(--menuHover);
|
||||
|
||||
$subMenuBg: var(--subMenuBg);
|
||||
$subMenuActiveText: var(--subMenuActiveText);
|
||||
$subMenuHover: var(--subMenuHover);
|
||||
|
||||
$sideBarWidth: 210px;
|
||||
@@ -0,0 +1,532 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElForm: typeof import('element-plus/es')['ElForm']
|
||||
const ElInput: typeof import('element-plus/es')['ElInput']
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
const ElNotification: typeof import('element-plus/es')['ElNotification']
|
||||
const ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveDirective: typeof import('vue')['resolveDirective']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||
const useCached: typeof import('@vueuse/core')['useCached']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||
const useFps: typeof import('@vueuse/core')['useFps']
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useImage: typeof import('@vueuse/core')['useImage']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||
const useNow: typeof import('@vueuse/core')['useNow']
|
||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||
const useToString: typeof import('@vueuse/core')['useToString']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly ElForm: UnwrapRef<typeof import('element-plus/es')['ElForm']>
|
||||
readonly ElInput: UnwrapRef<typeof import('element-plus/es')['ElInput']>
|
||||
readonly ElMessage: UnwrapRef<typeof import('element-plus/es')['ElMessage']>
|
||||
readonly ElMessageBox: UnwrapRef<typeof import('element-plus/es')['ElMessageBox']>
|
||||
readonly ElNotification: UnwrapRef<typeof import('element-plus/es')['ElNotification']>
|
||||
readonly ElSelect: UnwrapRef<typeof import('element-plus/es')['ElSelect']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
|
||||
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
|
||||
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
|
||||
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
|
||||
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
||||
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
|
||||
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
|
||||
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
|
||||
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
|
||||
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
|
||||
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
|
||||
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
||||
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveDirective: UnwrapRef<typeof import('vue')['resolveDirective']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
||||
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
|
||||
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
|
||||
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
|
||||
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
|
||||
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
|
||||
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
|
||||
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
|
||||
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
|
||||
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
|
||||
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
|
||||
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
|
||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
|
||||
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
|
||||
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
|
||||
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
|
||||
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
||||
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
||||
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
||||
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
|
||||
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
|
||||
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
|
||||
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
|
||||
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
|
||||
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
|
||||
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
|
||||
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
|
||||
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
|
||||
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
|
||||
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
|
||||
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
|
||||
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
|
||||
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
|
||||
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
|
||||
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
|
||||
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
|
||||
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
|
||||
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
|
||||
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
|
||||
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
||||
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
||||
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
||||
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
|
||||
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
|
||||
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
|
||||
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
|
||||
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
|
||||
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
|
||||
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
|
||||
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
|
||||
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
||||
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
||||
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
||||
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
||||
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
||||
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
||||
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
|
||||
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
||||
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
||||
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
|
||||
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
|
||||
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
||||
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
||||
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
||||
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
|
||||
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
||||
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
||||
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
||||
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
||||
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
||||
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
||||
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
|
||||
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
|
||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
||||
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
||||
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
||||
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
||||
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
|
||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
||||
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
|
||||
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
|
||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
||||
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
||||
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
||||
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
||||
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
||||
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
||||
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
||||
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
||||
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
||||
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
||||
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
||||
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
|
||||
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
|
||||
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
||||
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
||||
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
||||
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
|
||||
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
|
||||
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
|
||||
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
|
||||
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
|
||||
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
|
||||
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
|
||||
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
|
||||
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
|
||||
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
|
||||
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
|
||||
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
|
||||
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
||||
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
||||
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
||||
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
||||
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
|
||||
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
|
||||
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
Hamburger: typeof import('./../components/Hamburger/index.vue')['default']
|
||||
IEpCaretBottom: typeof import('~icons/ep/caret-bottom')['default']
|
||||
IEpClose: typeof import('~icons/ep/close')['default']
|
||||
IEpDownload: typeof import('~icons/ep/download')['default']
|
||||
IEpRefresh: typeof import('~icons/ep/refresh')['default']
|
||||
IEpRefreshRight: typeof import('~icons/ep/refresh-right')['default']
|
||||
IEpSetting: typeof import('~icons/ep/setting')['default']
|
||||
IEpUpload: typeof import('~icons/ep/upload')['default']
|
||||
ImputMultiple: typeof import('./../components/ImputMultiple/index.vue')['default']
|
||||
LangSelect: typeof import('./../components/LangSelect/index.vue')['default']
|
||||
MapAdd: typeof import('./../components/MapAdd/index.vue')['default']
|
||||
Pagination: typeof import('./../components/Pagination/index.vue')['default']
|
||||
RightPanel: typeof import('./../components/RightPanel/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SizeSelect: typeof import('./../components/SizeSelect/index.vue')['default']
|
||||
SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
|
||||
UnitSelect: typeof import('./../components/UnitSelect/index.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
// 环境变量 TypeScript的智能提示
|
||||
interface ImportMetaEnv {
|
||||
VITE_APP_TITLE: string;
|
||||
VITE_APP_PORT: string;
|
||||
VITE_APP_BASE_API: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
declare global {
|
||||
interface IdDto {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface BaseDto {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
interface PageVo<T> {
|
||||
records: T[];
|
||||
total: number;
|
||||
}
|
||||
}
|
||||
export {};
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 格式化字节大小
|
||||
* @param bytes 字节数
|
||||
* @param decimals 小数位数,默认为 2
|
||||
* @returns 格式化后的字节大小字符串
|
||||
*/
|
||||
export const formatBytes = (bytes: number, decimals = 2): string => {
|
||||
// 检查是否为特殊值
|
||||
if (bytes === -1) {
|
||||
return "Unlimited";
|
||||
}
|
||||
if (bytes === 0) {
|
||||
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,
|
||||
MB: 1024 ** 2,
|
||||
GB: 1024 ** 3,
|
||||
TB: 1024 ** 4,
|
||||
PB: 1024 ** 5,
|
||||
EB: 1024 ** 6,
|
||||
ZB: 1024 ** 7,
|
||||
YB: 1024 ** 8,
|
||||
};
|
||||
|
||||
// 检查传入的单位是否存在于映射关系中
|
||||
if (!Object.prototype.hasOwnProperty.call(unitToBytes, formattedUnit)) {
|
||||
throw new Error("Invalid unit");
|
||||
}
|
||||
|
||||
if (value == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 计算并返回字节数
|
||||
return value * unitToBytes[formattedUnit];
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化存储容量单位
|
||||
* @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 格式化后的存储单位
|
||||
*/
|
||||
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];
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 浅拷贝,忽略 null,支持嵌套对象
|
||||
* @param target
|
||||
* @param source
|
||||
*/
|
||||
export const assignWith = <T>(target: T, source: Partial<T>): void => {
|
||||
if (source === null || typeof source !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in source) {
|
||||
if (source[key] !== null) {
|
||||
if (typeof source[key] === "object") {
|
||||
if (!target[key]) {
|
||||
target[key] = (Array.isArray(source[key]) ? [] : {}) as T[Extract<
|
||||
keyof T,
|
||||
string
|
||||
>];
|
||||
}
|
||||
assignWith(target[key] as any, source[key] as any);
|
||||
} else {
|
||||
target[key] = source[key] as T[Extract<keyof T, string>];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 深拷贝,忽略 null,支持嵌套对象
|
||||
* @param source
|
||||
*/
|
||||
export const deepCopy = <T>(source: Partial<T>): T => {
|
||||
if (source === null || typeof source !== "object") {
|
||||
return source;
|
||||
}
|
||||
|
||||
if (Array.isArray(source)) {
|
||||
const arrCopy = [] as any[];
|
||||
source.forEach((item, index) => {
|
||||
arrCopy[index] = deepCopy(item);
|
||||
});
|
||||
return arrCopy as any;
|
||||
}
|
||||
|
||||
const objCopy = {} as { [key: string]: any };
|
||||
Object.keys(source).forEach((key) => {
|
||||
objCopy[key] = deepCopy((source as { [key: string]: any })[key]);
|
||||
});
|
||||
|
||||
return objCopy as T;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// translate router.meta.title, be used in breadcrumb sidebar tagsview
|
||||
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);
|
||||
return translatedTitle;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Check if an element has a class
|
||||
* @param {HTMLElement} ele
|
||||
* @param {string} cls
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasClass(ele: HTMLElement, cls: string) {
|
||||
return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add class to element
|
||||
* @param {HTMLElement} ele
|
||||
* @param {string} cls
|
||||
*/
|
||||
export function addClass(ele: HTMLElement, cls: string) {
|
||||
if (!hasClass(ele, cls)) ele.className += " " + cls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove class from element
|
||||
* @param {HTMLElement} ele
|
||||
* @param {string} cls
|
||||
*/
|
||||
export function removeClass(ele: HTMLElement, cls: string) {
|
||||
if (hasClass(ele, cls)) {
|
||||
const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)");
|
||||
ele.className = ele.className.replace(reg, " ");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isExternal(path: string) {
|
||||
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
|
||||
return isExternal;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { useAccountStoreHook } from "@/store/modules/account";
|
||||
|
||||
const dynamicBase = (window as any).__dynamic_base__ || "";
|
||||
// 创建 axios 实例
|
||||
const service = axios.create({
|
||||
baseURL: `${dynamicBase}${import.meta.env.VITE_APP_BASE_API}`,
|
||||
timeout: 50000,
|
||||
headers: { "Content-Type": "application/json;charset=utf-8" },
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const accountStore = useAccountStoreHook();
|
||||
if (accountStore.token) {
|
||||
config.headers.Authorization = accountStore.token;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error: any) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
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 || "系统出错");
|
||||
return Promise.reject(new Error(message || "Error"));
|
||||
},
|
||||
(error: any) => {
|
||||
if (error.response.data) {
|
||||
const { code, msg } = error.response.data;
|
||||
// token 过期,重新登录
|
||||
if (code === "A0230") {
|
||||
ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
localStorage.clear();
|
||||
window.location.href = "/";
|
||||
});
|
||||
} else {
|
||||
ElMessage.error(msg || "系统出错");
|
||||
}
|
||||
}
|
||||
return Promise.reject(error.message);
|
||||
}
|
||||
);
|
||||
|
||||
// 导出 axios 实例
|
||||
export default service;
|
||||
@@ -0,0 +1,69 @@
|
||||
const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
|
||||
t /= d / 2;
|
||||
if (t < 1) {
|
||||
return (c / 2) * t * t + b;
|
||||
}
|
||||
t--;
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b;
|
||||
};
|
||||
|
||||
// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
|
||||
const requestAnimFrame = (function () {
|
||||
return (
|
||||
window.requestAnimationFrame ||
|
||||
(window as any).webkitRequestAnimationFrame ||
|
||||
(window as any).mozRequestAnimationFrame ||
|
||||
function (callback) {
|
||||
window.setTimeout(callback, 1000 / 60);
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
||||
/**
|
||||
* Because it's so fucking difficult to detect the scrolling element, just move them all
|
||||
* @param {number} amount
|
||||
*/
|
||||
const move = (amount: number) => {
|
||||
document.documentElement.scrollTop = amount;
|
||||
(document.body.parentNode as HTMLElement).scrollTop = amount;
|
||||
document.body.scrollTop = amount;
|
||||
};
|
||||
|
||||
const position = () => {
|
||||
return (
|
||||
document.documentElement.scrollTop ||
|
||||
(document.body.parentNode as HTMLElement).scrollTop ||
|
||||
document.body.scrollTop
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} to
|
||||
* @param {number} duration
|
||||
* @param {Function} callback
|
||||
*/
|
||||
export const scrollTo = (to: number, duration: number, callback?: any) => {
|
||||
const start = position();
|
||||
const change = to - start;
|
||||
const increment = 20;
|
||||
let currentTime = 0;
|
||||
duration = typeof duration === "undefined" ? 500 : duration;
|
||||
const animateScroll = function () {
|
||||
// increment the time
|
||||
currentTime += increment;
|
||||
// find the value with the quadratic in-out easing function
|
||||
const val = easeInOutQuad(currentTime, start, change, duration);
|
||||
// move the document.body
|
||||
move(val);
|
||||
// do the animation unless its over
|
||||
if (currentTime < duration) {
|
||||
requestAnimFrame(animateScroll);
|
||||
} else {
|
||||
if (callback && typeof callback === "function") {
|
||||
// the animation is done so lets callback
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
animateScroll();
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 将时间戳转换为格式化日期时间字符串(YYYY-MM-DD HH:mm:ss)
|
||||
* @param timestamp 时间戳
|
||||
* @returns 格式化日期时间字符串
|
||||
*/
|
||||
export const timestampToDateTime = (timestamp: number): string => {
|
||||
const date = new Date(timestamp);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
const seconds = date.getSeconds().toString().padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
export const calculateTimeDifference = (timestamp: number): string => {
|
||||
const now = Date.now();
|
||||
const diff = timestamp - now;
|
||||
|
||||
if (diff <= 0) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
const remainingHours = hours % 24;
|
||||
const remainingMinutes = minutes % 60;
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (days > 0) {
|
||||
parts.push(`${days}天`);
|
||||
}
|
||||
if (remainingHours > 0) {
|
||||
parts.push(`${remainingHours}小时`);
|
||||
}
|
||||
if (remainingMinutes > 0) {
|
||||
parts.push(`${remainingMinutes}分钟`);
|
||||
}
|
||||
if (remainingSeconds > 0) {
|
||||
parts.push(`${remainingSeconds}秒`);
|
||||
}
|
||||
|
||||
return parts.join(" ");
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取一小时后的时间戳
|
||||
* @returns 一周后的时间戳
|
||||
*/
|
||||
export const getHourLater = (): number => {
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() + 1);
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取一天后的时间戳
|
||||
* @returns 一周后的时间戳
|
||||
*/
|
||||
export const getDayLater = (): number => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 1);
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取一周后的时间戳
|
||||
* @returns 一周后的时间戳
|
||||
*/
|
||||
export const getWeekLater = (): number => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 7);
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取一个月后的时间戳
|
||||
* @returns 一个月后的时间戳
|
||||
*/
|
||||
export const getMonthLater = (): number => {
|
||||
const date = new Date();
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取一年后的时间戳
|
||||
* @returns 一年后的时间戳
|
||||
*/
|
||||
export const getYearLater = (): number => {
|
||||
const date = new Date();
|
||||
date.setFullYear(date.getFullYear() + 1);
|
||||
return date.getTime();
|
||||
};
|
||||
@@ -0,0 +1,975 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="search">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item :label="$t('account.remark')" prop="remark">
|
||||
<el-input
|
||||
v-model="queryParams.remark"
|
||||
:placeholder="$t('account.remark')"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.username')" prop="username">
|
||||
<el-input
|
||||
v-model="queryParams.username"
|
||||
:placeholder="$t('account.username')"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('common.deleted')" prop="deleted">
|
||||
<el-select
|
||||
v-model="queryParams.deleted"
|
||||
:placeholder="$t('common.all')"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
>
|
||||
<el-option :label="$t('common.enable')" value="0" />
|
||||
<el-option :label="$t('common.disable')" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleQuery"
|
||||
>{{ $t("common.search") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button :icon="Refresh" @click="resetQuery"
|
||||
>{{ $t("common.reset") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button :icon="Plus" @click="handleAdd"
|
||||
>{{ $t("common.add") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
:http-request="handleImport"
|
||||
:show-file-list="false"
|
||||
accept=".json"
|
||||
:limit="1"
|
||||
:before-upload="beforeImport"
|
||||
>
|
||||
<el-button>
|
||||
<template #icon>
|
||||
<i-ep-upload />
|
||||
</template>
|
||||
{{ $t("common.import") }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleExport">
|
||||
<template #icon>
|
||||
<i-ep-download />
|
||||
</template>
|
||||
{{ $t("common.export") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never">
|
||||
<el-table v-loading="loading" :data="records">
|
||||
<el-table-column
|
||||
key="id"
|
||||
:label="$t('common.id')"
|
||||
align="center"
|
||||
prop="id"
|
||||
/>
|
||||
<el-table-column
|
||||
key="remark"
|
||||
:label="$t('account.remark')"
|
||||
align="center"
|
||||
prop="remark"
|
||||
/>
|
||||
<el-table-column
|
||||
key="username"
|
||||
:label="$t('account.username')"
|
||||
align="center"
|
||||
prop="username"
|
||||
/>
|
||||
<el-table-column
|
||||
key="role"
|
||||
:label="$t('account.role')"
|
||||
align="center"
|
||||
prop="role"
|
||||
/>
|
||||
<el-table-column
|
||||
key="quota"
|
||||
:label="$t('account.quota')"
|
||||
align="center"
|
||||
prop="quota"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ formatBytes(scope.row.quota) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="download"
|
||||
:label="$t('account.download')"
|
||||
align="center"
|
||||
prop="download"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ formatBytes(scope.row.download) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="upload"
|
||||
:label="$t('account.upload')"
|
||||
align="center"
|
||||
prop="upload"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ formatBytes(scope.row.upload) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="online"
|
||||
:label="$t('account.onlineStatus')"
|
||||
align="center"
|
||||
prop="online"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.online" type="success"
|
||||
>{{ $t("account.online") }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info">{{ $t("account.offline") }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="device"
|
||||
:label="$t('account.device')"
|
||||
align="center"
|
||||
prop="device"
|
||||
/>
|
||||
<el-table-column
|
||||
key="deviceNo"
|
||||
:label="$t('account.deviceNo')"
|
||||
align="center"
|
||||
prop="deviceNo"
|
||||
/>
|
||||
<el-table-column
|
||||
key="kickUtilTime"
|
||||
:label="$t('account.kickUtilTimeLast')"
|
||||
align="center"
|
||||
prop="kickUtilTime"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ calculateTimeDifference(scope.row.kickUtilTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="expireTime"
|
||||
:label="$t('account.expireTime')"
|
||||
align="center"
|
||||
prop="expireTime"
|
||||
width="160"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ timestampToDateTime(scope.row.expireTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="loginAt"
|
||||
:label="$t('account.loginAt')"
|
||||
align="center"
|
||||
prop="loginAt"
|
||||
width="160"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{
|
||||
scope.row.loginAt ? timestampToDateTime(scope.row.loginAt) : "-"
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="conAt"
|
||||
:label="$t('account.conAt')"
|
||||
align="center"
|
||||
prop="conAt"
|
||||
width="160"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.row.conAt ? timestampToDateTime(scope.row.conAt) : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('common.createTime')"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
width="160"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ timestampToDateTime(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="deleted"
|
||||
:label="$t('common.deleted')"
|
||||
align="center"
|
||||
prop="deleted"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.deleted === 0" type="success"
|
||||
>{{ $t("common.enable") }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="danger">{{ $t("common.disable") }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
:label="$t('common.operate')"
|
||||
align="center"
|
||||
width="300"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleSubscribe(scope.row)"
|
||||
>{{ $t("common.subscribe") }}
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="handleNodeUrl(scope.row)"
|
||||
>{{ $t("common.nodeUrl") }}
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="handleQrCode(scope.row)">
|
||||
{{ $t("common.nodeQrCode") }}
|
||||
</el-button>
|
||||
<el-popconfirm
|
||||
title="Are you sure to reset traffic?"
|
||||
@confirm="resetTraffic(scope.row)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button type="primary" link
|
||||
>{{ $t("common.resetTraffic") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-button type="primary" link @click="handleUpdate(scope.row)"
|
||||
>{{ $t("common.edit") }}
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(scope.row)"
|
||||
>{{ $t("common.delete") }}
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleKick(scope.row)"
|
||||
>{{ $t("account.kick") }}
|
||||
</el-button>
|
||||
<el-popconfirm
|
||||
:title="$t('account.releaseKickTip')"
|
||||
@confirm="confirmReleaseKick(scope.row)"
|
||||
v-if="calculateTimeDifference(scope.row.kickUtilTime) !== '-'"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button type="danger" link
|
||||
>{{ $t("account.releaseKick") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="handleQuery"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
:title="dialog.title"
|
||||
v-model="dialog.visible"
|
||||
width="620px"
|
||||
append-to-body
|
||||
@close="closeDialog"
|
||||
>
|
||||
<el-form
|
||||
ref="dataFormRef"
|
||||
:rules="
|
||||
dialog.title === t('common.add')
|
||||
? dataFormAddRules
|
||||
: dataFormUpdateRules
|
||||
"
|
||||
:model="dataForm"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item :label="$t('account.remark')" prop="remark">
|
||||
<el-input
|
||||
v-model="dataForm.remark"
|
||||
:placeholder="$t('account.remark')"
|
||||
maxlength="50"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.username')" prop="username">
|
||||
<el-input
|
||||
v-model="dataForm.username"
|
||||
:placeholder="$t('account.username')"
|
||||
maxlength="50"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.pass')" prop="pass">
|
||||
<el-input
|
||||
v-model="dataForm.pass"
|
||||
:placeholder="$t('account.pass')"
|
||||
maxlength="50"
|
||||
clearable
|
||||
type="password"
|
||||
show-password
|
||||
ref="dataFormPassRef"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.conPass')" prop="conPass">
|
||||
<el-input
|
||||
v-model="dataForm.conPass"
|
||||
:placeholder="$t('account.conPass')"
|
||||
maxlength="50"
|
||||
clearable
|
||||
type="password"
|
||||
show-password
|
||||
ref="dataFormConPassRef"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.quota')" prop="quota">
|
||||
<unit-select :setValue="setQuota" :valueTmp="quotaTmp" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.deviceNo')" prop="deviceNo">
|
||||
<el-input-number
|
||||
v-model="dataForm.deviceNo"
|
||||
:placeholder="$t('account.deviceNo')"
|
||||
:min="1"
|
||||
:controls="false"
|
||||
:precision="0"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.expireTime')" prop="expireTime">
|
||||
<el-date-picker
|
||||
v-model="dataForm.expireTime"
|
||||
type="datetime"
|
||||
:placeholder="$t('account.expireTime')"
|
||||
value-format="x"
|
||||
:shortcuts="shortcuts"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('common.deleted')" prop="deleted">
|
||||
<el-radio-group v-model="dataForm.deleted">
|
||||
<el-radio :label="0">{{ $t("common.enable") }}</el-radio>
|
||||
<el-radio :label="1">{{ $t("common.disable") }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm"
|
||||
>{{ $t("common.confirm") }}
|
||||
</el-button>
|
||||
<el-button @click="closeDialog">{{ $t("common.cancel") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="dialogKick.title"
|
||||
v-model="dialogKick.visible"
|
||||
width="600px"
|
||||
append-to-body
|
||||
@close="closeDialogKick"
|
||||
>
|
||||
<el-form ref="kickFormRef" :model="kickForm" label-width="100px">
|
||||
<el-form-item :label="$t('account.kickUtilTime')" prop="kickUtilTime">
|
||||
<el-date-picker
|
||||
v-model="kickForm.kickUtilTime"
|
||||
type="datetime"
|
||||
:placeholder="$t('account.kickUtilTime')"
|
||||
value-format="x"
|
||||
:shortcuts="shortcutsKick"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitKickForm"
|
||||
>{{ $t("common.confirm") }}
|
||||
</el-button>
|
||||
<el-button @click="closeDialogKick"
|
||||
>{{ $t("common.cancel") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="qrCodeDialog.title"
|
||||
v-model="qrCodeDialog.visible"
|
||||
width="600px"
|
||||
append-to-body
|
||||
@close="qrCodeDialog.visible = false"
|
||||
>
|
||||
<el-form style="text-align: center">
|
||||
<el-image
|
||||
style="width: 300px; height: 300px"
|
||||
:src="qrCodeSrc"
|
||||
></el-image>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="qrCodeDialog.visible = false"
|
||||
>{{ $t("common.confirm") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "index",
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AccountForm,
|
||||
AccountPageDto,
|
||||
AccountUpdateDto,
|
||||
AccountVo,
|
||||
KickAccountForm,
|
||||
} from "@/api/account/types";
|
||||
import {
|
||||
saveAccountApi,
|
||||
deleteAccountApi,
|
||||
getAccountApi,
|
||||
pageAccountApi,
|
||||
updateAccountApi,
|
||||
exportAccountApi,
|
||||
releaseKickAccountApi,
|
||||
importAccountApi,
|
||||
resetTrafficApi,
|
||||
} from "@/api/account";
|
||||
import { Search, Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import {
|
||||
timestampToDateTime,
|
||||
getMonthLater,
|
||||
getWeekLater,
|
||||
getYearLater,
|
||||
calculateTimeDifference,
|
||||
getHourLater,
|
||||
getDayLater,
|
||||
} from "@/utils/time";
|
||||
import { formatBytes } from "@/utils/byte";
|
||||
|
||||
import {
|
||||
hysteria2KickApi,
|
||||
hysteria2SubscribeUrlApi,
|
||||
hysteria2UrlApi,
|
||||
} from "@/api/hysteria2";
|
||||
import {
|
||||
UploadFile,
|
||||
UploadRawFile,
|
||||
UploadRequestOptions,
|
||||
} from "element-plus/lib/components";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import {
|
||||
Hysteria2SubscribeUrlDto,
|
||||
Hysteria2UrlDto,
|
||||
} from "@/api/hysteria2/types";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const queryFormRef = ref(ElForm); // 查询表单
|
||||
const dataFormRef = ref(ElForm); // 用户表单
|
||||
const kickFormRef = ref(ElForm); // 下线表单
|
||||
const dataFormPassRef = ref(ElInput);
|
||||
const dataFormConPassRef = ref(ElInput);
|
||||
|
||||
const dataFormAddRules = {
|
||||
remark: [
|
||||
{
|
||||
min: 0,
|
||||
max: 32,
|
||||
message: "Remark format is incorrect",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||
message: "Username format is incorrect",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
pass: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||
message: "Pass format is incorrect",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
conPass: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||
message: "ConPass format is incorrect",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
expireTime: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
deviceNo: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
deleted: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dataFormUpdateRules = {
|
||||
remark: [
|
||||
{
|
||||
min: 0,
|
||||
max: 32,
|
||||
message: "Remark format is incorrect",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
username: [
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||
message: "Username format is incorrect",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
pass: [
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||
message: "Pass format is incorrect",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
conPass: [
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||
message: "Pass format is incorrect",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
text: "A week later",
|
||||
value: getWeekLater,
|
||||
},
|
||||
{
|
||||
text: "A month later",
|
||||
value: getMonthLater,
|
||||
},
|
||||
{
|
||||
text: "A year later",
|
||||
value: getYearLater,
|
||||
},
|
||||
];
|
||||
|
||||
const shortcutsKick = [
|
||||
{
|
||||
text: "A hour later",
|
||||
value: getHourLater,
|
||||
},
|
||||
{
|
||||
text: "A day later",
|
||||
value: getDayLater,
|
||||
},
|
||||
{
|
||||
text: "A week later",
|
||||
value: getWeekLater,
|
||||
},
|
||||
{
|
||||
text: "A month later",
|
||||
value: getMonthLater,
|
||||
},
|
||||
];
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
total: 0,
|
||||
records: [] as AccountVo[],
|
||||
dialog: {
|
||||
visible: false,
|
||||
} as DialogType,
|
||||
dialogKick: {
|
||||
visible: false,
|
||||
} as DialogType,
|
||||
dataForm: {
|
||||
quota: 0,
|
||||
expireTime: getMonthLater(),
|
||||
deviceNo: 6,
|
||||
deleted: 0,
|
||||
} as AccountForm,
|
||||
kickForm: {
|
||||
kickUtilTime: getHourLater(),
|
||||
} as KickAccountForm,
|
||||
queryParams: {
|
||||
remark: undefined,
|
||||
username: undefined,
|
||||
deleted: undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
} as AccountPageDto,
|
||||
quotaTmp: 0,
|
||||
fileList: [] as UploadFile[],
|
||||
qrCodeDialog: {
|
||||
title: "QR Code",
|
||||
visible: false,
|
||||
} as DialogType,
|
||||
qrCodeSrc: "",
|
||||
});
|
||||
|
||||
const {
|
||||
loading,
|
||||
total,
|
||||
records,
|
||||
dialog,
|
||||
dialogKick,
|
||||
dataForm,
|
||||
kickForm,
|
||||
queryParams,
|
||||
quotaTmp,
|
||||
fileList,
|
||||
qrCodeDialog,
|
||||
qrCodeSrc,
|
||||
} = toRefs(state);
|
||||
|
||||
const resetDataForm = () => {
|
||||
Object.assign(state.dataForm, {
|
||||
id: undefined,
|
||||
quota: 0,
|
||||
expireTime: getMonthLater(),
|
||||
deleted: 0,
|
||||
});
|
||||
quotaTmp.value = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
const handleQuery = async () => {
|
||||
state.loading = true;
|
||||
try {
|
||||
const { data } = await pageAccountApi(state.queryParams);
|
||||
state.records = data.records;
|
||||
state.total = data.total;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存
|
||||
**/
|
||||
const handleAdd = () => {
|
||||
state.dialog = {
|
||||
title: t("common.add"),
|
||||
visible: true,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 修改
|
||||
**/
|
||||
const handleUpdate = async (row: { [key: string]: any }) => {
|
||||
const id = row.id;
|
||||
const { data } = await getAccountApi({ id: id });
|
||||
Object.assign(state.dataForm, data);
|
||||
quotaTmp.value = data.quota;
|
||||
dialog.value = {
|
||||
title: t("common.update"),
|
||||
visible: true,
|
||||
};
|
||||
};
|
||||
|
||||
const setQuota = (newQuota: number) => {
|
||||
state.dataForm.quota = newQuota;
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单提交
|
||||
*/
|
||||
const submitForm = () => {
|
||||
dataFormRef.value.validate((valid: any) => {
|
||||
if (valid) {
|
||||
const accountId = state.dataForm.id;
|
||||
let accountUpdateDto: AccountUpdateDto = { ...state.dataForm };
|
||||
if (accountId) {
|
||||
updateAccountApi(accountUpdateDto).then(() => {
|
||||
ElMessage.success(t("common.success"));
|
||||
closeDialog();
|
||||
handleQuery();
|
||||
});
|
||||
} else {
|
||||
saveAccountApi(accountUpdateDto).then(() => {
|
||||
ElMessage.success(t("common.success"));
|
||||
closeDialog();
|
||||
handleQuery();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 下线表单提交
|
||||
*/
|
||||
const submitKickForm = () => {
|
||||
kickFormRef.value.validate((valid: any) => {
|
||||
if (valid) {
|
||||
const params = { ...state.kickForm };
|
||||
hysteria2KickApi(params).then(() => {
|
||||
ElMessage.success(t("common.success"));
|
||||
closeDialogKick();
|
||||
handleQuery();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
const handleDelete = (row: { [key: string]: any }) => {
|
||||
const id = row.id;
|
||||
const username = row.username;
|
||||
ElMessageBox.confirm(
|
||||
"Are you sure to delete the data item with the username「" +
|
||||
username +
|
||||
"」?",
|
||||
"Warning",
|
||||
{
|
||||
confirmButtonText: t("common.confirm"),
|
||||
cancelButtonText: t("common.cancel"),
|
||||
type: "warning",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
deleteAccountApi({ id: id }).then(() => {
|
||||
ElMessage.success(t("common.success"));
|
||||
handleQuery();
|
||||
});
|
||||
})
|
||||
.catch(() => ElMessage.info(t("common.cancel")));
|
||||
};
|
||||
|
||||
/**
|
||||
* 强制用户下线
|
||||
* @param row
|
||||
*/
|
||||
const handleKick = (row: { [key: string]: any }) => {
|
||||
state.kickForm.ids = [row.id];
|
||||
dialogKick.value = {
|
||||
title: t("account.kickTip"),
|
||||
visible: true,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 解除下线状态
|
||||
* @param row
|
||||
*/
|
||||
const confirmReleaseKick = (row: { [key: string]: any }) => {
|
||||
releaseKickAccountApi({ id: row.id }).then(() => {
|
||||
ElMessage.success(t("account.releaseSuccess"));
|
||||
handleQuery();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭用户弹窗
|
||||
*/
|
||||
const closeDialog = () => {
|
||||
dialog.value.visible = false;
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
|
||||
if (dialog.value.title == t("common.update")) {
|
||||
resetDataForm();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭下线弹窗
|
||||
*/
|
||||
const closeDialogKick = () => {
|
||||
dialogKick.value.visible = false;
|
||||
kickFormRef.value.resetFields();
|
||||
kickFormRef.value.clearValidate();
|
||||
};
|
||||
|
||||
/**
|
||||
* 导入
|
||||
*/
|
||||
const handleImport = (params: UploadRequestOptions) => {
|
||||
if (state.fileList.length > 0) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", params.file);
|
||||
importAccountApi(formData).then(() => {
|
||||
ElMessage.success(t("common.success"));
|
||||
});
|
||||
state.fileList = [];
|
||||
}
|
||||
};
|
||||
|
||||
const beforeImport = (file: UploadRawFile) => {
|
||||
if (!file.name.endsWith(".json")) {
|
||||
ElMessage.error("file format not supported");
|
||||
return false;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 2) {
|
||||
ElMessage.error("the file is too big, less than 2 MB");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
const handleExport = () => {
|
||||
exportAccountApi().then((res) => {
|
||||
const blob = new Blob([res.data], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
let url = window.URL.createObjectURL(blob);
|
||||
let a = document.createElement("a");
|
||||
document.body.appendChild(a);
|
||||
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"));
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubscribe = async (row: { [key: string]: any }) => {
|
||||
try {
|
||||
const dto: Hysteria2SubscribeUrlDto = {
|
||||
accountId: row.id,
|
||||
protocol: window.location.protocol,
|
||||
host: window.location.host,
|
||||
};
|
||||
const { data } = await hysteria2SubscribeUrlApi(dto);
|
||||
copy(data.url);
|
||||
ElMessage.success(t("common.copySuccess"));
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
const handleNodeUrl = async (row: { [key: string]: any }) => {
|
||||
try {
|
||||
const dto: Hysteria2UrlDto = {
|
||||
accountId: row.id,
|
||||
hostname: window.location.hostname,
|
||||
};
|
||||
const { data } = await hysteria2UrlApi(dto);
|
||||
copy(data.url);
|
||||
ElMessage.success(t("common.copySuccess"));
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
const handleQrCode = async (row: { [key: string]: any }) => {
|
||||
try {
|
||||
const dto: Hysteria2UrlDto = {
|
||||
accountId: row.id,
|
||||
hostname: window.location.hostname,
|
||||
};
|
||||
const { data } = await hysteria2UrlApi(dto);
|
||||
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
||||
state.qrCodeDialog.visible = true;
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
const resetTraffic = async (row: { [key: string]: any }) => {
|
||||
try {
|
||||
await resetTrafficApi({ id: row.id });
|
||||
ElMessage.success(t("common.success"));
|
||||
await handleQuery();
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化用户列表数据
|
||||
handleQuery();
|
||||
if (route.query.focus === "change-pass") {
|
||||
nextTick(() => {
|
||||
handleUpdate({ id: 1 }).then(() => {
|
||||
setTimeout(() => {
|
||||
const inputPass = dataFormPassRef.value.$el.querySelector(
|
||||
".el-input__wrapper input"
|
||||
);
|
||||
if (inputPass) {
|
||||
inputPass.focus();
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,486 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="search">
|
||||
<el-form inline>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm" :icon="Select">
|
||||
{{ $t("common.save") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleRestartServer">
|
||||
<template #icon>
|
||||
<i-ep-refreshRight />
|
||||
</template>
|
||||
{{ $t("config.restartServer") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
:http-request="handleImport"
|
||||
:show-file-list="false"
|
||||
accept=".json"
|
||||
:limit="1"
|
||||
:before-upload="beforeImport"
|
||||
>
|
||||
<el-button>
|
||||
<template #icon>
|
||||
<i-ep-upload />
|
||||
</template>
|
||||
{{ $t("common.import") }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleExport">
|
||||
<template #icon>
|
||||
<i-ep-download />
|
||||
</template>
|
||||
{{ $t("common.export") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never">
|
||||
<el-form
|
||||
ref="dataFormRef"
|
||||
:rules="dataFormRules"
|
||||
:model="dataForm"
|
||||
label-position="top"
|
||||
>
|
||||
<el-form-item :label="$t('config.huiWebPort')" prop="huiWebPort">
|
||||
<el-input
|
||||
v-model="dataForm.huiWebPort"
|
||||
:placeholder="$t('config.huiWebPort')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('config.huiWebContext')" prop="huiWebContext">
|
||||
<el-input
|
||||
v-model="dataForm.huiWebContext"
|
||||
:placeholder="$t('config.huiWebContext')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('config.hysteria2TrafficTime')"
|
||||
prop="hysteria2TrafficTime"
|
||||
>
|
||||
<el-input
|
||||
v-model="dataForm.hysteria2TrafficTime"
|
||||
:placeholder="$t('config.hysteria2TrafficTime')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('config.huiHttps')" prop="huiHttps">
|
||||
<el-select v-model="huiHttps" style="width: 50%" ref="huiHttpsRef">
|
||||
<el-option
|
||||
v-for="item in huiHttpsList"
|
||||
:key="item.key"
|
||||
:label="item.key"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button v-if="huiHttps" @click="setCertPath"
|
||||
>{{ t("config.useHysteria2Cert") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="huiHttps"
|
||||
:label="$t('config.huiCrtPath')"
|
||||
prop="huiCrtPath"
|
||||
>
|
||||
<el-input
|
||||
v-model="dataForm.huiCrtPath"
|
||||
:placeholder="$t('config.huiCrtPath')"
|
||||
style="width: 50%"
|
||||
clearable
|
||||
/>
|
||||
<el-upload
|
||||
style="height: 32px"
|
||||
ref="uploadCrtFile"
|
||||
action=""
|
||||
:file-list="crtFileList"
|
||||
:http-request="uploadCertFile"
|
||||
accept=".crt"
|
||||
:before-upload="
|
||||
() => {
|
||||
crtFileList = [];
|
||||
}
|
||||
"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button>{{ t("config.uploadCrtFile") }}</el-button>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="huiHttps"
|
||||
:label="$t('config.huiKeyPath')"
|
||||
prop="huiKeyPath"
|
||||
>
|
||||
<el-input
|
||||
v-model="dataForm.huiKeyPath"
|
||||
:placeholder="$t('config.huiKeyPath')"
|
||||
style="width: 50%"
|
||||
clearable
|
||||
/>
|
||||
<el-upload
|
||||
style="height: 32px"
|
||||
ref="uploadKeyFile"
|
||||
action=""
|
||||
:file-list="keyFileList"
|
||||
:http-request="uploadCertFile"
|
||||
accept=".key"
|
||||
:before-upload="
|
||||
() => {
|
||||
keyFileList = [];
|
||||
}
|
||||
"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button>{{ t("config.uploadKeyFile") }}</el-button>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-tooltip
|
||||
:content="$t('config.resetTrafficCronTip')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
:label="$t('config.resetTrafficCron')"
|
||||
prop="resetTrafficCron"
|
||||
>
|
||||
<el-select
|
||||
v-model="dataForm.resetTrafficCron"
|
||||
filterable
|
||||
allow-create
|
||||
clearable
|
||||
:placeholder="$t('config.resetTrafficCron')"
|
||||
style="width: 50%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in cronResetTraffic"
|
||||
:key="item.value"
|
||||
:label="item.key"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "index",
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Select } from "@element-plus/icons-vue";
|
||||
import {
|
||||
exportConfigApi,
|
||||
hysteria2AcmePathApi,
|
||||
importConfigApi,
|
||||
listConfigApi,
|
||||
restartServerApi,
|
||||
updateConfigsApi,
|
||||
uploadCertFileApi,
|
||||
} from "@/api/config";
|
||||
import { ConfigsUpdateDto } from "@/api/config/types";
|
||||
import {
|
||||
UploadFile,
|
||||
UploadRawFile,
|
||||
UploadRequestOptions,
|
||||
} from "element-plus/lib/components";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
import { UploadUserFile } from "element-plus";
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const dataFormRef = ref(ElForm);
|
||||
const huiHttpsRef = ref(ElSelect);
|
||||
|
||||
const huiWebPortKey = "H_UI_WEB_PORT";
|
||||
const huiWebContext = "H_UI_WEB_CONTEXT";
|
||||
const hysteria2TrafficTimeKey = "HYSTERIA2_TRAFFIC_TIME";
|
||||
const huiCrtPathKey = "H_UI_CRT_PATH";
|
||||
const huiKeyPathKey = "H_UI_KEY_PATH";
|
||||
const resetTrafficCronKey = "RESET_TRAFFIC_CRON";
|
||||
|
||||
const huiHttpsList = [
|
||||
{ key: t("common.yes"), value: 1 },
|
||||
{ key: t("common.no"), value: 0 },
|
||||
];
|
||||
|
||||
const cronResetTraffic = [
|
||||
{ key: t("config.resetTrafficMonth"), value: "@monthly" },
|
||||
{ key: t("config.resetTrafficWeek"), value: "@weekly" },
|
||||
];
|
||||
|
||||
const dataFormRules = {
|
||||
huiWebPort: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
pattern: /^\d+$/,
|
||||
message: "field must be a integer",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
huiWebContext: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
pattern: /^\/([a-z0-9]+(\/[a-z0-9]+)*)?$/,
|
||||
message: "field must start with / and contain only lowercase letters (a-z) and numbers (0-9)",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
hysteria2TrafficTime: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
pattern: /^\d+(\.\d)?$/,
|
||||
message: "field must be a number with up to one decimal place",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
dataForm: {
|
||||
huiWebPort: "8081",
|
||||
huiWebContext: "/",
|
||||
hysteria2TrafficTime: "1",
|
||||
huiCrtPath: "",
|
||||
huiKeyPath: "",
|
||||
resetTrafficCron: "",
|
||||
},
|
||||
huiHttps: 0,
|
||||
fileList: [] as UploadUserFile[],
|
||||
crtFileList: [] as UploadUserFile[],
|
||||
keyFileList: [] as UploadUserFile[],
|
||||
});
|
||||
|
||||
const { dataForm, huiHttps, fileList, crtFileList, keyFileList } =
|
||||
toRefs(state);
|
||||
|
||||
const submitForm = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
if (state.huiHttps) {
|
||||
if (!state.dataForm.huiCrtPath || !state.dataForm.huiKeyPath) {
|
||||
ElMessage.error("crt and key required");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.huiHttps) {
|
||||
state.dataForm.huiCrtPath = "";
|
||||
state.dataForm.huiKeyPath = "";
|
||||
}
|
||||
|
||||
let configs: ConfigsUpdateDto[] = [
|
||||
{
|
||||
key: huiWebPortKey,
|
||||
value: state.dataForm.huiWebPort,
|
||||
},
|
||||
{
|
||||
key: huiWebContext,
|
||||
value: state.dataForm.huiWebContext,
|
||||
},
|
||||
{
|
||||
key: hysteria2TrafficTimeKey,
|
||||
value: state.dataForm.hysteria2TrafficTime,
|
||||
},
|
||||
{
|
||||
key: huiCrtPathKey,
|
||||
value: state.dataForm.huiCrtPath,
|
||||
},
|
||||
{
|
||||
key: huiKeyPathKey,
|
||||
value: state.dataForm.huiKeyPath,
|
||||
},
|
||||
{
|
||||
key: resetTrafficCronKey,
|
||||
value: state.dataForm.resetTrafficCron,
|
||||
},
|
||||
];
|
||||
|
||||
updateConfigsApi({ configUpdateDtos: configs }).then(() => {
|
||||
ElMessage.success(t("common.success"));
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setConfig = async () => {
|
||||
const { data } = await listConfigApi({
|
||||
keys: [
|
||||
huiCrtPathKey,
|
||||
huiWebContext,
|
||||
huiKeyPathKey,
|
||||
huiWebPortKey,
|
||||
hysteria2TrafficTimeKey,
|
||||
resetTrafficCronKey,
|
||||
],
|
||||
});
|
||||
|
||||
data.forEach((configVo) => {
|
||||
if (configVo.key === huiWebPortKey) {
|
||||
state.dataForm.huiWebPort = configVo.value;
|
||||
} else if (configVo.key === huiWebContext) {
|
||||
state.dataForm.huiWebContext = configVo.value;
|
||||
} else if (configVo.key === hysteria2TrafficTimeKey) {
|
||||
state.dataForm.hysteria2TrafficTime = configVo.value;
|
||||
} else if (configVo.key === huiCrtPathKey) {
|
||||
state.dataForm.huiCrtPath = configVo.value;
|
||||
} else if (configVo.key === huiKeyPathKey) {
|
||||
state.dataForm.huiKeyPath = configVo.value;
|
||||
} else if (configVo.key === resetTrafficCronKey) {
|
||||
state.dataForm.resetTrafficCron = configVo.value;
|
||||
}
|
||||
});
|
||||
|
||||
if (state.dataForm.huiCrtPath != "" && state.dataForm.huiKeyPath != "") {
|
||||
state.huiHttps = 1;
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async (params: UploadRequestOptions) => {
|
||||
if (state.fileList.length > 0) {
|
||||
try {
|
||||
let formData = new FormData();
|
||||
formData.append("file", params.file);
|
||||
await importConfigApi(formData);
|
||||
ElMessage.success(t("common.success"));
|
||||
state.fileList = [];
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
} finally {
|
||||
await setConfig();
|
||||
}
|
||||
}
|
||||
};
|
||||
const beforeImport = (file: UploadRawFile) => {
|
||||
if (!file.name.endsWith(".json")) {
|
||||
ElMessage.error("file format not supported");
|
||||
return false;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 2) {
|
||||
ElMessage.error("the file is too big, less than 2 MB");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
let response = await exportConfigApi();
|
||||
const blob = new Blob([response.data], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
let url = window.URL.createObjectURL(blob);
|
||||
let a = document.createElement("a");
|
||||
document.body.appendChild(a);
|
||||
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"));
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
const setCertPath = async () => {
|
||||
try {
|
||||
const { data } = await hysteria2AcmePathApi();
|
||||
const { crtPath, keyPath } = data;
|
||||
state.dataForm.huiCrtPath = crtPath;
|
||||
state.dataForm.huiKeyPath = keyPath;
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
const uploadCertFile = async (params: UploadRequestOptions) => {
|
||||
try {
|
||||
if (
|
||||
!params.file.name.endsWith(".crt") &&
|
||||
!params.file.name.endsWith(".key")
|
||||
) {
|
||||
ElMessage.error("file format not supported");
|
||||
}
|
||||
if (params.file.size > 1024 * 1024) {
|
||||
ElMessage.error("the file is too big");
|
||||
}
|
||||
let formData = new FormData();
|
||||
formData.append("file", params.file);
|
||||
const { data } = await uploadCertFileApi(formData);
|
||||
if (params.file.name.endsWith(".crt")) {
|
||||
state.dataForm.huiCrtPath = data;
|
||||
} else if (params.file.name.endsWith(".key")) {
|
||||
state.dataForm.huiKeyPath = data;
|
||||
}
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartServer = async () => {
|
||||
try {
|
||||
ElMessageBox.confirm("Are you sure to restart panel?", "Warning", {
|
||||
confirmButtonText: t("common.confirm"),
|
||||
cancelButtonText: t("common.cancel"),
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
restartServerApi();
|
||||
ElMessage.success(t("config.restartTip"));
|
||||
});
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setConfig();
|
||||
if (route.query.focus === "huiHttps") {
|
||||
nextTick(() => {
|
||||
const input = huiHttpsRef.value.$el.querySelector(".el-input__inner");
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-card .el-form {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<!-- setup 无法设置组件名称,组件名称keepAlive必须 -->
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "Page401",
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, toRefs } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const state = reactive({
|
||||
errGif: new URL(`../../assets/401_images/401.gif`, import.meta.url).href,
|
||||
});
|
||||
|
||||
const { errGif } = toRefs(state);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function back() {
|
||||
router.back();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="errPage-container">
|
||||
<el-button class="pan-back-btn" @click="back">Back</el-button>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<h1 class="text-jumbo text-ginormous">Oops!</h1>
|
||||
<h2>You do not have permission to access this page</h2>
|
||||
<ul class="list-unstyled">
|
||||
<li>Or you can go:</li>
|
||||
<li class="link-type">
|
||||
<router-link to="/">Back to Home</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<img
|
||||
:src="errGif"
|
||||
width="313"
|
||||
height="428"
|
||||
alt="Girl has dropped her ice cream."
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.errPage-container {
|
||||
width: 800px;
|
||||
max-width: 100%;
|
||||
margin: 100px auto;
|
||||
|
||||
.pan-back-btn {
|
||||
color: #fff;
|
||||
background: #008489;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.pan-gif {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pan-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.text-jumbo {
|
||||
font-size: 60px;
|
||||
font-weight: 700;
|
||||
color: #484848;
|
||||
}
|
||||
|
||||
.list-unstyled {
|
||||
font-size: 14px;
|
||||
|
||||
li {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #008489;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||