feat(v1): Gecko-обфускация, latest-stable Hysteria на сборке и forward-compatible admin
Сквозная миграция HY2XS на современную Hysteria (2.12.2) и переход на v1. Build: - версия Hysteria резолвится на этапе сборки из HyNetworks/hysteria и замораживается в metadata пакета (version + immutable url + sha256); - compatibility gate: реальный бинарник должен принять канонический конфиг HY2XS для gecko и salamander до создания пакета; - сборка прогоняет тесты оркестратора и админки. Конфигурационный контракт: - HY2XS_CONFIG_SCHEMA_VERSION=2, чужая схема отклоняется fail-fast; - obfs стал настоящим union gecko|salamander, gecko — default; - obfs-блок рендерится оркестратором целиком, два подтипа одновременно структурно невозможны; - современный baseline: congestion bbr/standard, disableLossCompensation=false, disableStatelessReset=false, полный quic-блок. Исправления: - share URI для gecko: генератор был завязан на Obfs.Salamander.Password и выдавал нерабочую ссылку при любой другой обфускации; - SNI брался только из ACME-блока и уходил пустым при HY2XS_TLS_MODE=file; - экспорт конфига выносил trafficStats.secret, access_token и obfs-пароль; - экспорт терял неизвестные upstream-поля при round-trip через типизированную модель; - renderRuntimeEnv печатал тип обфускации литералом, расходясь с конфигом; - namedotcom удалён из ACME-реестра (нет в Hysteria с 2.11.0). Тесты: - 95 тестов оркестратора: env, рендер, семантика профиля, резолвер, rollover; - тесты URI и экспорта в Go; - tools/test/e2e-hysteria.sh с реальным клиентом Hysteria. UX: - подсказки и примеры в форме создания пира. Прочее: CHANGELOG.md, .gitattributes (LF для target-side файлов), документация на русском.
This commit is contained in:
@@ -26,6 +26,10 @@ export interface Hysteria2ServerConfig {
|
||||
cert: string;
|
||||
key: string;
|
||||
sniGuard?: string;
|
||||
clientCA?: string;
|
||||
};
|
||||
ech?: {
|
||||
keyPath?: string;
|
||||
};
|
||||
acme?: {
|
||||
domains: string[];
|
||||
@@ -51,9 +55,14 @@ export interface Hysteria2ServerConfig {
|
||||
};
|
||||
obfs?: {
|
||||
type: string;
|
||||
salamander: {
|
||||
salamander?: {
|
||||
password: string;
|
||||
};
|
||||
gecko?: {
|
||||
password: string;
|
||||
minPacketSize?: number;
|
||||
maxPacketSize?: number;
|
||||
};
|
||||
};
|
||||
quic?: {
|
||||
initStreamReceiveWindow?: number;
|
||||
@@ -63,10 +72,16 @@ export interface Hysteria2ServerConfig {
|
||||
maxIdleTimeout?: string;
|
||||
maxIncomingStreams?: number;
|
||||
disablePathMTUDiscovery?: boolean;
|
||||
disableStatelessReset?: boolean;
|
||||
};
|
||||
bandwidth?: {
|
||||
up: string;
|
||||
down: string;
|
||||
disableLossCompensation?: boolean;
|
||||
};
|
||||
congestion?: {
|
||||
type?: string;
|
||||
bbrProfile?: string;
|
||||
};
|
||||
ignoreClientBandwidth?: boolean;
|
||||
speedTest?: boolean;
|
||||
@@ -122,6 +137,7 @@ export interface Hysteria2ServerConfig {
|
||||
url: string;
|
||||
rewriteHost: boolean;
|
||||
insecure: boolean;
|
||||
xForwarded?: boolean;
|
||||
};
|
||||
string?: {
|
||||
content: string;
|
||||
@@ -132,6 +148,26 @@ export interface Hysteria2ServerConfig {
|
||||
listenHTTPS?: string;
|
||||
forceHTTPS?: boolean;
|
||||
};
|
||||
mimic?: {
|
||||
enabled?: boolean;
|
||||
interface?: string;
|
||||
xdpMode?: string;
|
||||
path?: string;
|
||||
extraArgs?: string[];
|
||||
};
|
||||
realm?: {
|
||||
stunServers?: string[];
|
||||
stunTimeout?: string;
|
||||
punchTimeout?: string;
|
||||
heartbeatInterval?: string;
|
||||
insecure?: boolean;
|
||||
ipMode?: string;
|
||||
portMapping?: {
|
||||
enabled?: boolean;
|
||||
timeout?: string;
|
||||
lifetime?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = {
|
||||
@@ -144,9 +180,9 @@ export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = {
|
||||
acme: {
|
||||
domains: [],
|
||||
email: "",
|
||||
ca: "zerossl",
|
||||
ca: "letsencrypt",
|
||||
listenHost: "0.0.0.0",
|
||||
dir: "my_acme_dir",
|
||||
dir: "/var/lib/hysteria/acme",
|
||||
type: "",
|
||||
http: {
|
||||
altPort: 8888,
|
||||
@@ -155,7 +191,7 @@ export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = {
|
||||
altPort: 44333,
|
||||
},
|
||||
dns: {
|
||||
name: "gomommy",
|
||||
name: "cloudflare",
|
||||
config: {},
|
||||
},
|
||||
disableHTTP: false,
|
||||
@@ -164,9 +200,11 @@ export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = {
|
||||
altTLSALPNPort: 443,
|
||||
},
|
||||
obfs: {
|
||||
type: "salamander",
|
||||
salamander: {
|
||||
password: "cry_me_a_r1ver",
|
||||
type: "gecko",
|
||||
gecko: {
|
||||
password: "",
|
||||
minPacketSize: 512,
|
||||
maxPacketSize: 1200,
|
||||
},
|
||||
},
|
||||
quic: {
|
||||
@@ -177,10 +215,16 @@ export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = {
|
||||
maxIdleTimeout: "30s",
|
||||
maxIncomingStreams: 1024,
|
||||
disablePathMTUDiscovery: false,
|
||||
disableStatelessReset: false,
|
||||
},
|
||||
bandwidth: {
|
||||
up: "1 gbps",
|
||||
down: "1 gbps",
|
||||
up: "50 mbps",
|
||||
down: "50 mbps",
|
||||
disableLossCompensation: false,
|
||||
},
|
||||
congestion: {
|
||||
type: "bbr",
|
||||
bbrProfile: "standard",
|
||||
},
|
||||
ignoreClientBandwidth: false,
|
||||
speedTest: false,
|
||||
@@ -236,6 +280,7 @@ export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = {
|
||||
url: "",
|
||||
rewriteHost: true,
|
||||
insecure: false,
|
||||
xForwarded: false,
|
||||
},
|
||||
string: {
|
||||
content: "hello stupid world",
|
||||
|
||||
@@ -128,6 +128,17 @@ export default {
|
||||
name: "Peer",
|
||||
remark: "Remark",
|
||||
secret: "Secret",
|
||||
form: {
|
||||
namePlaceholder: "e.g. ivan-laptop",
|
||||
nameHint:
|
||||
"Short peer identifier. Use latin letters, digits and hyphens — the name becomes part of the auto-generated secret.",
|
||||
remarkPlaceholder: "e.g. Ivan's laptop, sales team",
|
||||
remarkHint: "Optional operator note. It is never shown to the client.",
|
||||
secretPlaceholder: "leave empty to generate automatically",
|
||||
secretHint:
|
||||
"Client connection password. Leave empty to generate one automatically. If set manually: 6 to 128 characters.",
|
||||
quotaHint: "Traffic limit in bytes. Use -1 for unlimited.",
|
||||
},
|
||||
maxDevices: "Max devices",
|
||||
disabled: "Disabled",
|
||||
status: "Status",
|
||||
@@ -234,6 +245,7 @@ export default {
|
||||
obfs: "Obfuscation",
|
||||
quic: "QUIC parameters",
|
||||
bandwidth: "Bandwidth",
|
||||
congestion: "Congestion control",
|
||||
speedTest: "Speed Test",
|
||||
udp: "UDP",
|
||||
resolver: "Resolver",
|
||||
@@ -282,10 +294,17 @@ export default {
|
||||
"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",
|
||||
type: "Obfuscation type: gecko (HY2XS production default) or salamander (compatibility fallback).",
|
||||
salamander: {
|
||||
password: "Replace with a strong password of your choice.",
|
||||
},
|
||||
gecko: {
|
||||
password: "Replace with a strong password of your choice.",
|
||||
minPacketSize:
|
||||
"Minimum QUIC handshake fragment size. HY2XS baseline: 512.",
|
||||
maxPacketSize:
|
||||
"Maximum QUIC handshake fragment size. HY2XS baseline: 1200, upstream limit: 2048.",
|
||||
},
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: "The initial QUIC stream receive window size.",
|
||||
@@ -299,10 +318,18 @@ export default {
|
||||
maxIncomingStreams:
|
||||
"The maximum number of concurrent incoming streams.",
|
||||
disablePathMTUDiscovery: "Disable QUIC path MTU discovery.",
|
||||
disableStatelessReset:
|
||||
"Disable QUIC stateless reset. Kept off in the HY2XS baseline: stateless reset lets a client with a stale connection reconnect immediately after a server restart or device sleep.",
|
||||
},
|
||||
bandwidth: {
|
||||
up: "Up",
|
||||
down: "Down",
|
||||
disableLossCompensation:
|
||||
"Disable loss compensation. Kept off in the HY2XS baseline, so compensation stays active.",
|
||||
},
|
||||
congestion: {
|
||||
type: "Fallback congestion controller: bbr or reno. Used when Brutal bandwidth is not negotiated by both sides.",
|
||||
bbrProfile: "BBR profile: standard, conservative or aggressive.",
|
||||
},
|
||||
ignoreClientBandwidth:
|
||||
"When enabled, makes the server to disregard any bandwidth hints set by clients",
|
||||
|
||||
@@ -124,6 +124,17 @@ export default {
|
||||
name: "Пир",
|
||||
remark: "Комментарий",
|
||||
secret: "Секрет",
|
||||
form: {
|
||||
namePlaceholder: "например, ivan-laptop",
|
||||
nameHint:
|
||||
"Короткий идентификатор пира. Используйте латиницу, цифры и дефис — имя попадает в автогенерируемый секрет.",
|
||||
remarkPlaceholder: "например, Ноутбук Ивана, отдел продаж",
|
||||
remarkHint: "Необязательная пометка для оператора. Клиент её не видит.",
|
||||
secretPlaceholder: "оставьте пустым — сгенерируем автоматически",
|
||||
secretHint:
|
||||
"Пароль подключения клиента. Если оставить поле пустым, секрет будет сгенерирован автоматически. При ручном вводе: от 6 до 128 символов.",
|
||||
quotaHint: "Лимит трафика в байтах. Укажите -1 для безлимита.",
|
||||
},
|
||||
maxDevices: "Лимит устройств",
|
||||
disabled: "Отключён",
|
||||
status: "Статус",
|
||||
@@ -230,6 +241,7 @@ export default {
|
||||
obfs: "Маскировка",
|
||||
quic: "Параметры QUIC",
|
||||
bandwidth: "Полоса",
|
||||
congestion: "Congestion control",
|
||||
speedTest: "Тест скорости",
|
||||
udp: "UDP",
|
||||
resolver: "DNS",
|
||||
@@ -264,8 +276,15 @@ export default {
|
||||
altTLSALPNPort: "Альтернативный TLS-ALPN-порт",
|
||||
},
|
||||
obfs: {
|
||||
type: "Тип",
|
||||
type: "Тип обфускации: gecko (production default HY2XS) или salamander (совместимость)",
|
||||
salamander: { password: "Сильный пароль Salamander" },
|
||||
gecko: {
|
||||
password: "Сильный пароль Gecko",
|
||||
minPacketSize:
|
||||
"Минимальный размер фрагмента QUIC handshake. Baseline HY2XS: 512",
|
||||
maxPacketSize:
|
||||
"Максимальный размер фрагмента QUIC handshake. Baseline HY2XS: 1200, верхний предел upstream: 2048",
|
||||
},
|
||||
},
|
||||
quic: {
|
||||
initStreamReceiveWindow: "Начальное окно приёма QUIC stream",
|
||||
@@ -275,8 +294,19 @@ export default {
|
||||
maxIdleTimeout: "Максимальный idle timeout",
|
||||
maxIncomingStreams: "Максимум входящих stream",
|
||||
disablePathMTUDiscovery: "Отключить QUIC path MTU discovery",
|
||||
disableStatelessReset:
|
||||
"Отключить QUIC stateless reset. В baseline HY2XS выключено: stateless reset ускоряет переподключение клиента после перезапуска сервера или сна устройства",
|
||||
},
|
||||
bandwidth: {
|
||||
up: "Вверх",
|
||||
down: "Вниз",
|
||||
disableLossCompensation:
|
||||
"Отключить компенсацию потерь. В baseline HY2XS выключено, то есть компенсация работает",
|
||||
},
|
||||
congestion: {
|
||||
type: "Fallback congestion controller: bbr или reno. Применяется, когда Brutal bandwidth не согласован сторонами",
|
||||
bbrProfile: "Профиль BBR: standard, conservative или aggressive",
|
||||
},
|
||||
bandwidth: { up: "Вверх", down: "Вниз" },
|
||||
ignoreClientBandwidth: "Игнорировать bandwidth, заявленный клиентом",
|
||||
speedTest: "Встроенный сервер теста скорости",
|
||||
disableUDP: "Отключить UDP forwarding",
|
||||
|
||||
@@ -361,21 +361,69 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="dataForm.obfs.type === 'salamander'"
|
||||
:content="$t('hysteria.config.obfs.salamander.password')"
|
||||
placement="bottom"
|
||||
<template
|
||||
v-if="
|
||||
dataForm.obfs.type === 'salamander' && dataForm.obfs.salamander
|
||||
"
|
||||
>
|
||||
<el-form-item
|
||||
label="obfs.salamander.password"
|
||||
prop="obfs.salamander.password"
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.obfs.salamander.password')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-input
|
||||
v-model="dataForm.obfs.salamander.password"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-form-item
|
||||
label="obfs.salamander.password"
|
||||
prop="obfs.salamander.password"
|
||||
>
|
||||
<el-input
|
||||
v-model="dataForm.obfs.salamander.password"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template
|
||||
v-if="dataForm.obfs.type === 'gecko' && dataForm.obfs.gecko"
|
||||
>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.obfs.gecko.password')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="obfs.gecko.password"
|
||||
prop="obfs.gecko.password"
|
||||
>
|
||||
<el-input v-model="dataForm.obfs.gecko.password" clearable />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.obfs.gecko.minPacketSize')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="obfs.gecko.minPacketSize"
|
||||
prop="obfs.gecko.minPacketSize"
|
||||
>
|
||||
<el-input
|
||||
v-model.number="dataForm.obfs.gecko.minPacketSize"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.obfs.gecko.maxPacketSize')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="obfs.gecko.maxPacketSize"
|
||||
prop="obfs.gecko.maxPacketSize"
|
||||
>
|
||||
<el-input
|
||||
v-model.number="dataForm.obfs.gecko.maxPacketSize"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('hysteria.quic')" name="quic" v-if="quic">
|
||||
<el-tooltip
|
||||
@@ -470,6 +518,17 @@
|
||||
<el-switch v-model="dataForm.quic.disablePathMTUDiscovery" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.quic.disableStatelessReset')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="quic.disableStatelessReset"
|
||||
prop="quic.disableStatelessReset"
|
||||
>
|
||||
<el-switch v-model="dataForm.quic.disableStatelessReset" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
:label="$t('hysteria.bandwidth')"
|
||||
@@ -494,6 +553,19 @@
|
||||
<el-input v-model="dataForm.bandwidth.down" clearable />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.bandwidth.disableLossCompensation')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="bandwidth.disableLossCompensation"
|
||||
prop="bandwidth.disableLossCompensation"
|
||||
>
|
||||
<el-switch
|
||||
v-model="dataForm.bandwidth.disableLossCompensation"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.ignoreClientBandwidth')"
|
||||
placement="bottom"
|
||||
@@ -506,6 +578,53 @@
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
:label="$t('hysteria.congestion')"
|
||||
name="congestion"
|
||||
v-if="congestion && dataForm.congestion"
|
||||
>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.congestion.type')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item label="congestion.type" prop="congestion.type">
|
||||
<el-select
|
||||
v-model="dataForm.congestion.type"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in congestionTypes"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.congestion.bbrProfile')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="congestion.bbrProfile"
|
||||
prop="congestion.bbrProfile"
|
||||
>
|
||||
<el-select
|
||||
v-model="dataForm.congestion.bbrProfile"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in bbrProfiles"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
:label="$t('hysteria.speedTest')"
|
||||
name="speedTest"
|
||||
@@ -1020,15 +1139,22 @@ const tlsSniGuards = ref<string[]>(["dns-san", "disable", "strict"]);
|
||||
const aclTypes = ref<string[]>(["file", "inline"]);
|
||||
const acmeCas = ref<string[]>(["zerossl", "letsencrypt"]);
|
||||
const acmeTypes = ref<string[]>(["http", "tls", "dns"]);
|
||||
// Актуальный upstream-реестр ACME DNS-провайдеров.
|
||||
// `namedotcom` удалён в Hysteria 2.11.0 вместе с переписанным ACME-стеком:
|
||||
// конфигурация с ним больше не запускается.
|
||||
const dnsNames = ref<string[]>([
|
||||
"cloudflare",
|
||||
"duckdns",
|
||||
"gandi",
|
||||
"godaddy",
|
||||
"namedotcom",
|
||||
"namecheap",
|
||||
"njalla",
|
||||
"porkbun",
|
||||
"vultr",
|
||||
]);
|
||||
const obfsTypes = ref<string[]>(["salamander"]);
|
||||
const obfsTypes = ref<string[]>(["gecko", "salamander"]);
|
||||
const congestionTypes = ref<string[]>(["bbr", "reno"]);
|
||||
const bbrProfiles = ref<string[]>(["standard", "conservative", "aggressive"]);
|
||||
const resolverTypes = ref<string[]>(["tcp", "udp", "tls", "https"]);
|
||||
const masqueradeTypes = ref<string[]>(["file", "proxy", "string"]);
|
||||
|
||||
@@ -1043,6 +1169,7 @@ const state = reactive({
|
||||
obfs: false,
|
||||
quic: false,
|
||||
bandwidth: false,
|
||||
congestion: false,
|
||||
speedTest: false,
|
||||
udp: false,
|
||||
resolver: false,
|
||||
@@ -1067,6 +1194,7 @@ const {
|
||||
obfs,
|
||||
quic,
|
||||
bandwidth,
|
||||
congestion,
|
||||
speedTest,
|
||||
udp,
|
||||
resolver,
|
||||
@@ -1120,6 +1248,7 @@ const setConfig = () => {
|
||||
state.obfs = !!data?.obfs;
|
||||
state.quic = !!data?.quic;
|
||||
state.bandwidth = !!data?.bandwidth;
|
||||
state.congestion = !!data?.congestion;
|
||||
state.speedTest = !!data?.speedTest;
|
||||
state.udp = !!data?.disableUDP || !!data?.udpIdleTimeout;
|
||||
state.resolver = !!data?.resolver;
|
||||
@@ -1130,10 +1259,28 @@ const setConfig = () => {
|
||||
|
||||
state.dataForm = deepCopy(defaultHysteria2ServerConfig);
|
||||
assignWith(state.dataForm, data);
|
||||
dropInactiveObfsSubtype(state.dataForm, data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Форма строится как merge дефолта и ответа API, поэтому неактивная ветка obfs
|
||||
// осталась бы от дефолта и UI показал бы блок, которого нет в конфиге сервера.
|
||||
const dropInactiveObfsSubtype = (
|
||||
form: Hysteria2ServerConfig,
|
||||
data: Hysteria2ServerConfig
|
||||
) => {
|
||||
if (!form.obfs) {
|
||||
return;
|
||||
}
|
||||
if (form.obfs.type !== "gecko" || !data?.obfs?.gecko) {
|
||||
delete form.obfs.gecko;
|
||||
}
|
||||
if (form.obfs.type !== "salamander" || !data?.obfs?.salamander) {
|
||||
delete form.obfs.salamander;
|
||||
}
|
||||
};
|
||||
|
||||
const setHysteria2Monitor = async () => {
|
||||
const { data } = await dashboardSummaryApi();
|
||||
state.hysteria2Monitor.version = data.hysteria.version;
|
||||
|
||||
@@ -65,9 +65,7 @@
|
||||
<el-table-column :label="$t('peer.traffic')" min-width="260">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{
|
||||
formatBytes(scope.row.downloadBytes + scope.row.uploadBytes)
|
||||
}}
|
||||
{{ formatBytes(scope.row.downloadBytes + scope.row.uploadBytes) }}
|
||||
/ {{ quotaText(scope.row.quotaBytes) }}
|
||||
</div>
|
||||
<el-progress
|
||||
@@ -167,18 +165,32 @@
|
||||
:rules="rules"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item :label="$t('peer.name')" prop="name"
|
||||
><el-input v-model="dataForm.name"
|
||||
/></el-form-item>
|
||||
<el-form-item :label="$t('peer.remark')"
|
||||
><el-input v-model="dataForm.remark"
|
||||
/></el-form-item>
|
||||
<el-form-item :label="$t('peer.secret')" prop="secret"
|
||||
><el-input v-model="dataForm.secret" show-password
|
||||
/></el-form-item>
|
||||
<el-form-item :label="$t('peer.quota')"
|
||||
><el-input-number v-model="dataForm.quotaBytes" :min="-1"
|
||||
/></el-form-item>
|
||||
<el-form-item :label="$t('peer.name')" prop="name">
|
||||
<el-input
|
||||
v-model="dataForm.name"
|
||||
:placeholder="$t('peer.form.namePlaceholder')"
|
||||
/>
|
||||
<div class="form-hint">{{ $t("peer.form.nameHint") }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('peer.remark')">
|
||||
<el-input
|
||||
v-model="dataForm.remark"
|
||||
:placeholder="$t('peer.form.remarkPlaceholder')"
|
||||
/>
|
||||
<div class="form-hint">{{ $t("peer.form.remarkHint") }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('peer.secret')" prop="secret">
|
||||
<el-input
|
||||
v-model="dataForm.secret"
|
||||
show-password
|
||||
:placeholder="$t('peer.form.secretPlaceholder')"
|
||||
/>
|
||||
<div class="form-hint">{{ $t("peer.form.secretHint") }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('peer.quota')">
|
||||
<el-input-number v-model="dataForm.quotaBytes" :min="-1" />
|
||||
<div class="form-hint">{{ $t("peer.form.quotaHint") }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('peer.expireTime')"
|
||||
><el-date-picker
|
||||
v-model="dataForm.expiresAt"
|
||||
@@ -573,23 +585,35 @@ onMounted(handleQuery);
|
||||
.peer-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.peer-sub {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.peer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.peer-more-btn {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.qr-dialog-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 0 20px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user