精简版第六轮补裁(P6): 摘除分组/API文档死入口与首页Xray状态徽标及重启/停止按钮, 清理死i18n键
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "npm run gen:api && vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "oxlint src tools",
|
"lint": "oxlint src tools",
|
||||||
"lint:fix": "oxlint --fix src tools",
|
"lint:fix": "oxlint --fix src tools",
|
||||||
@@ -22,8 +22,7 @@
|
|||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"gen": "npm run gen:zod && npm run gen:api",
|
"gen": "npm run gen:zod",
|
||||||
"gen:api": "node scripts/build-openapi.mjs",
|
|
||||||
"gen:zod": "cd .. && go run ./tools/openapigen",
|
"gen:zod": "cd .. && go run ./tools/openapigen",
|
||||||
"prepare": "cd .. && husky frontend/.husky || true"
|
"prepare": "cd .. && husky frontend/.husky || true"
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,246 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import { writeFileSync } from 'node:fs';
|
|
||||||
import { join, dirname } from 'node:path';
|
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
||||||
|
|
||||||
import { sections } from '../src/pages/api-docs/endpoints.ts';
|
|
||||||
import { EXAMPLES } from '../src/generated/examples.ts';
|
|
||||||
import { SCHEMAS } from '../src/generated/schemas.ts';
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const outPath = join(__dirname, '..', 'public', 'openapi.json');
|
|
||||||
|
|
||||||
const PANEL_VERSION = process.env.X_UI_VERSION || '3.x';
|
|
||||||
|
|
||||||
const SECURITY_SCHEMES = {
|
|
||||||
bearerAuth: {
|
|
||||||
type: 'http',
|
|
||||||
scheme: 'bearer',
|
|
||||||
description: 'API token from Settings → Security → API Token. Send as `Authorization: Bearer <token>`.',
|
|
||||||
},
|
|
||||||
cookieAuth: {
|
|
||||||
type: 'apiKey',
|
|
||||||
in: 'cookie',
|
|
||||||
name: '3x-ui',
|
|
||||||
description: 'Session cookie set by POST /login. Browser-only.',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function ginPathToOpenApi(path) {
|
|
||||||
return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, '{$1}');
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractPathParams(openApiPath) {
|
|
||||||
const params = [];
|
|
||||||
const re = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
||||||
let m;
|
|
||||||
while ((m = re.exec(openApiPath)) !== null) params.push(m[1]);
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapType(t) {
|
|
||||||
const v = String(t || '').toLowerCase();
|
|
||||||
if (v.endsWith('[]')) return 'array';
|
|
||||||
if (v === 'number' || v === 'integer' || v === 'int') return 'integer';
|
|
||||||
if (v === 'float' || v === 'double') return 'number';
|
|
||||||
if (v === 'boolean' || v === 'bool') return 'boolean';
|
|
||||||
if (v === 'array') return 'array';
|
|
||||||
if (v === 'object') return 'object';
|
|
||||||
return 'string';
|
|
||||||
}
|
|
||||||
|
|
||||||
function schemaFromType(t) {
|
|
||||||
const v = String(t || '').toLowerCase();
|
|
||||||
if (v.endsWith('[]')) {
|
|
||||||
const itemType = v.slice(0, -2);
|
|
||||||
return { type: 'array', items: { type: mapType(itemType) } };
|
|
||||||
}
|
|
||||||
return { type: mapType(v) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryParseJson(raw) {
|
|
||||||
if (typeof raw !== 'string') return undefined;
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function paramToOpenApi(p) {
|
|
||||||
const out = {
|
|
||||||
name: p.name,
|
|
||||||
in: p.in,
|
|
||||||
required: p.in === 'path' ? true : !p.optional,
|
|
||||||
description: p.desc || '',
|
|
||||||
schema: schemaFromType(p.type),
|
|
||||||
};
|
|
||||||
if (p.defaultValue !== undefined) out.schema.default = p.defaultValue;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildOperation(ep, tag) {
|
|
||||||
const op = {
|
|
||||||
tags: [tag],
|
|
||||||
summary: ep.summary || '',
|
|
||||||
operationId: `${ep.method.toLowerCase()}_${ep.path.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_|_$/g, '')}`,
|
|
||||||
};
|
|
||||||
if (ep.description) op.description = ep.description;
|
|
||||||
if (ep.deprecated) op.deprecated = true;
|
|
||||||
|
|
||||||
const params = [];
|
|
||||||
const bodyParams = [];
|
|
||||||
for (const p of ep.params || []) {
|
|
||||||
if (p.in === 'body') {
|
|
||||||
bodyParams.push(p);
|
|
||||||
} else if (p.in === 'path' || p.in === 'query' || p.in === 'header') {
|
|
||||||
params.push(paramToOpenApi(p));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openApiPath = ginPathToOpenApi(ep.path);
|
|
||||||
const declared = new Set(params.filter((x) => x.in === 'path').map((x) => x.name));
|
|
||||||
for (const name of extractPathParams(openApiPath)) {
|
|
||||||
if (declared.has(name)) continue;
|
|
||||||
params.push({
|
|
||||||
name,
|
|
||||||
in: 'path',
|
|
||||||
required: true,
|
|
||||||
description: '',
|
|
||||||
schema: { type: 'string' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.length > 0) op.parameters = params;
|
|
||||||
|
|
||||||
if (ep.body || bodyParams.length > 0) {
|
|
||||||
const example = tryParseJson(ep.body);
|
|
||||||
const properties = {};
|
|
||||||
const required = [];
|
|
||||||
for (const bp of bodyParams) {
|
|
||||||
properties[bp.name] = {
|
|
||||||
...schemaFromType(bp.type),
|
|
||||||
description: bp.desc || '',
|
|
||||||
};
|
|
||||||
if (!bp.optional) required.push(bp.name);
|
|
||||||
}
|
|
||||||
const schema = bodyParams.length > 0
|
|
||||||
? { type: 'object', properties, ...(required.length > 0 ? { required } : {}) }
|
|
||||||
: { type: 'object' };
|
|
||||||
|
|
||||||
op.requestBody = {
|
|
||||||
required: required.length > 0 || bodyParams.length === 0,
|
|
||||||
content: {
|
|
||||||
'application/json': {
|
|
||||||
schema,
|
|
||||||
...(example !== undefined ? { example } : {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const responses = {};
|
|
||||||
let successExample = tryParseJson(ep.response);
|
|
||||||
let objSchema = {};
|
|
||||||
if (ep.responseSchema) {
|
|
||||||
const obj = EXAMPLES[ep.responseSchema];
|
|
||||||
if (obj === undefined) {
|
|
||||||
throw new Error(`${ep.method} ${ep.path}: responseSchema "${ep.responseSchema}" has no generated example`);
|
|
||||||
}
|
|
||||||
if (SCHEMAS[ep.responseSchema] === undefined) {
|
|
||||||
throw new Error(`${ep.method} ${ep.path}: responseSchema "${ep.responseSchema}" has no generated schema`);
|
|
||||||
}
|
|
||||||
const ref = { $ref: `#/components/schemas/${ep.responseSchema}` };
|
|
||||||
objSchema = ep.responseSchemaArray ? { type: 'array', items: ref } : ref;
|
|
||||||
if (successExample === undefined) {
|
|
||||||
successExample = { success: true, obj: ep.responseSchemaArray ? [obj] : obj };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
responses['200'] = {
|
|
||||||
description: 'Successful response',
|
|
||||||
content: {
|
|
||||||
'application/json': {
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
success: { type: 'boolean' },
|
|
||||||
msg: { type: 'string' },
|
|
||||||
obj: objSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(successExample !== undefined ? { example: successExample } : {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const errExample = tryParseJson(ep.errorResponse);
|
|
||||||
if (errExample !== undefined || ep.errorStatus) {
|
|
||||||
const code = String(ep.errorStatus || 400);
|
|
||||||
responses[code] = {
|
|
||||||
description: 'Error response',
|
|
||||||
content: {
|
|
||||||
'application/json': {
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
success: { type: 'boolean' },
|
|
||||||
msg: { type: 'string' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(errExample !== undefined ? { example: errExample } : {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
op.responses = responses;
|
|
||||||
return op;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSpec() {
|
|
||||||
const paths = {};
|
|
||||||
for (const section of sections) {
|
|
||||||
const tag = section.title;
|
|
||||||
for (const ep of section.endpoints) {
|
|
||||||
const openApiPath = ginPathToOpenApi(ep.path);
|
|
||||||
if (!paths[openApiPath]) paths[openApiPath] = {};
|
|
||||||
paths[openApiPath][ep.method.toLowerCase()] = buildOperation(ep, tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = sections.map((s) => ({
|
|
||||||
name: s.title,
|
|
||||||
description: s.description || '',
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
openapi: '3.0.3',
|
|
||||||
info: {
|
|
||||||
title: '3X-UI Panel API',
|
|
||||||
version: PANEL_VERSION,
|
|
||||||
description:
|
|
||||||
'Programmatic interface to a 3X-UI panel. Authenticate either by logging in (cookie) or with an API token from Settings → Security → API Token (Bearer). All endpoints under /panel/api/* honour both modes — an API token is a full-admin credential, so treat it like the panel password.',
|
|
||||||
},
|
|
||||||
servers: [
|
|
||||||
{ url: '/', description: 'Current panel (basePath aware)' },
|
|
||||||
],
|
|
||||||
components: {
|
|
||||||
securitySchemes: SECURITY_SCHEMES,
|
|
||||||
schemas: SCHEMAS,
|
|
||||||
},
|
|
||||||
security: [{ bearerAuth: [] }, { cookieAuth: [] }],
|
|
||||||
tags,
|
|
||||||
paths,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const spec = buildSpec();
|
|
||||||
writeFileSync(outPath, JSON.stringify(spec, null, 2) + '\n');
|
|
||||||
|
|
||||||
const pathCount = Object.keys(spec.paths).length;
|
|
||||||
let opCount = 0;
|
|
||||||
for (const ops of Object.values(spec.paths)) opCount += Object.keys(ops).length;
|
|
||||||
console.log(`[openapi] wrote ${outPath}`);
|
|
||||||
console.log(`[openapi] paths: ${pathCount}, operations: ${opCount}, tags: ${spec.tags.length}`);
|
|
||||||
|
|
||||||
void pathToFileURL;
|
|
||||||
@@ -36,7 +36,6 @@ export const keys = {
|
|||||||
onlinesByGuid: () => ['clients', 'onlinesByGuid'] as const,
|
onlinesByGuid: () => ['clients', 'onlinesByGuid'] as const,
|
||||||
activeInbounds: () => ['clients', 'activeInbounds'] as const,
|
activeInbounds: () => ['clients', 'activeInbounds'] as const,
|
||||||
lastOnline: () => ['clients', 'lastOnline'] as const,
|
lastOnline: () => ['clients', 'lastOnline'] as const,
|
||||||
groups: () => ['clients', 'groups'] as const,
|
|
||||||
},
|
},
|
||||||
xray: {
|
xray: {
|
||||||
root: () => ['xray'] as const,
|
root: () => ['xray'] as const,
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ async function fetchDefaults(): Promise<Record<string, unknown>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UseClientsOptions {
|
export interface UseClientsOptions {
|
||||||
// Callers that only need the mutations — the bulk modals, the groups page —
|
// Callers that only need the mutations — the bulk modals —
|
||||||
// pass false. Mounting them used to start a second 5-second poll of the paged
|
// pass false. Mounting them used to start a second 5-second poll of the paged
|
||||||
// list whose result they never read, which on a large panel means a full
|
// list whose result they never read, which on a large panel means a full
|
||||||
// summary aggregate every 5 seconds for nothing.
|
// summary aggregate every 5 seconds for nothing.
|
||||||
@@ -328,22 +328,6 @@ export function useClients(options: UseClientsOptions = {}) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const bulkAddToGroupMut = useMutation({
|
|
||||||
mutationFn: (body: { emails: string[]; group: string }) =>
|
|
||||||
HttpUtil.post('/panel/api/clients/groups/bulkAdd', body, JSON_HEADERS),
|
|
||||||
onSuccess: (msg) => {
|
|
||||||
if (msg?.success) invalidateAll();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bulkRemoveFromGroupMut = useMutation({
|
|
||||||
mutationFn: (body: { emails: string[] }) =>
|
|
||||||
HttpUtil.post('/panel/api/clients/groups/bulkRemove', body, JSON_HEADERS),
|
|
||||||
onSuccess: (msg) => {
|
|
||||||
if (msg?.success) invalidateAll();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateMut = useMutation({
|
const updateMut = useMutation({
|
||||||
mutationFn: ({ email, client }: { email: string; client: unknown }) =>
|
mutationFn: ({ email, client }: { email: string; client: unknown }) =>
|
||||||
HttpUtil.post(`/panel/api/clients/update/${encodeURIComponent(email)}`, client, JSON_HEADERS),
|
HttpUtil.post(`/panel/api/clients/update/${encodeURIComponent(email)}`, client, JSON_HEADERS),
|
||||||
@@ -583,20 +567,6 @@ export function useClients(options: UseClientsOptions = {}) {
|
|||||||
},
|
},
|
||||||
[bulkSetEnableMut],
|
[bulkSetEnableMut],
|
||||||
);
|
);
|
||||||
const bulkAddToGroup = useCallback(
|
|
||||||
(emails: string[], group: string) => {
|
|
||||||
if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve(null);
|
|
||||||
return bulkAddToGroupMut.mutateAsync({ emails, group });
|
|
||||||
},
|
|
||||||
[bulkAddToGroupMut],
|
|
||||||
);
|
|
||||||
const bulkRemoveFromGroup = useCallback(
|
|
||||||
(emails: string[]) => {
|
|
||||||
if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve(null);
|
|
||||||
return bulkRemoveFromGroupMut.mutateAsync({ emails });
|
|
||||||
},
|
|
||||||
[bulkRemoveFromGroupMut],
|
|
||||||
);
|
|
||||||
const attach = useCallback(
|
const attach = useCallback(
|
||||||
(email: string, inboundIds: number[]) => {
|
(email: string, inboundIds: number[]) => {
|
||||||
if (!email) return Promise.resolve(null as unknown as Msg<unknown>);
|
if (!email) return Promise.resolve(null as unknown as Msg<unknown>);
|
||||||
@@ -807,8 +777,6 @@ export function useClients(options: UseClientsOptions = {}) {
|
|||||||
bulkAdjust,
|
bulkAdjust,
|
||||||
bulkEnable,
|
bulkEnable,
|
||||||
bulkDisable,
|
bulkDisable,
|
||||||
bulkAddToGroup,
|
|
||||||
bulkRemoveFromGroup,
|
|
||||||
attach,
|
attach,
|
||||||
setExternalLinks,
|
setExternalLinks,
|
||||||
bulkAttach,
|
bulkAttach,
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ const TITLE_KEYS: Record<string, string> = {
|
|||||||
'/': 'menu.dashboard',
|
'/': 'menu.dashboard',
|
||||||
'/inbounds': 'menu.inbounds',
|
'/inbounds': 'menu.inbounds',
|
||||||
'/clients': 'menu.clients',
|
'/clients': 'menu.clients',
|
||||||
'/groups': 'menu.groups',
|
|
||||||
'/nodes': 'menu.nodes',
|
'/nodes': 'menu.nodes',
|
||||||
'/hosts': 'menu.hosts',
|
'/hosts': 'menu.hosts',
|
||||||
'/settings': 'menu.settings',
|
'/settings': 'menu.settings',
|
||||||
'/xray': 'menu.xray',
|
'/xray': 'menu.xray',
|
||||||
'/outbound': 'menu.outbounds',
|
'/outbound': 'menu.outbounds',
|
||||||
'/routing': 'menu.routing',
|
'/routing': 'menu.routing',
|
||||||
'/api-docs': 'menu.apiDocs',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function usePageTitle() {
|
export function usePageTitle() {
|
||||||
|
|||||||
@@ -214,14 +214,12 @@ export default function AppSidebar() {
|
|||||||
{ key: '/', icon: 'dashboard', title: t('menu.dashboard') },
|
{ key: '/', icon: 'dashboard', title: t('menu.dashboard') },
|
||||||
{ key: '/inbounds', icon: 'inbound', title: t('menu.inbounds') },
|
{ key: '/inbounds', icon: 'inbound', title: t('menu.inbounds') },
|
||||||
{ key: '/clients', icon: 'team', title: t('menu.clients') },
|
{ key: '/clients', icon: 'team', title: t('menu.clients') },
|
||||||
{ key: '/groups', icon: 'groups', title: t('menu.groups') },
|
|
||||||
{ key: '/nodes', icon: 'cluster', title: t('menu.nodes') },
|
{ key: '/nodes', icon: 'cluster', title: t('menu.nodes') },
|
||||||
{ key: '/hosts', icon: 'hosts', title: t('menu.hosts') },
|
{ key: '/hosts', icon: 'hosts', title: t('menu.hosts') },
|
||||||
{ key: '/outbound', icon: 'outbound', title: t('menu.outbounds') },
|
{ key: '/outbound', icon: 'outbound', title: t('menu.outbounds') },
|
||||||
{ key: '/routing', icon: 'routing', title: t('menu.routing') },
|
{ key: '/routing', icon: 'routing', title: t('menu.routing') },
|
||||||
{ key: '/settings', icon: 'setting', title: t('menu.settings') },
|
{ key: '/settings', icon: 'setting', title: t('menu.settings') },
|
||||||
{ key: '/xray', icon: 'tool', title: t('menu.xray') },
|
{ key: '/xray', icon: 'tool', title: t('menu.xray') },
|
||||||
{ key: '/api-docs', icon: 'apidocs', title: t('menu.apiDocs') },
|
|
||||||
{ key: LOGOUT_KEY, icon: 'logout', title: t('logout') },
|
{ key: LOGOUT_KEY, icon: 'logout', title: t('logout') },
|
||||||
],
|
],
|
||||||
[t],
|
[t],
|
||||||
|
|||||||
@@ -1,421 +0,0 @@
|
|||||||
.api-docs-page.is-dark {
|
|
||||||
--sw-bg: #1f2026;
|
|
||||||
--sw-bg-soft: #25272e;
|
|
||||||
--sw-bg-input: #15161a;
|
|
||||||
--sw-bg-code: #0d0e12;
|
|
||||||
--sw-border: rgba(255, 255, 255, 0.08);
|
|
||||||
--sw-border-strong: rgba(255, 255, 255, 0.15);
|
|
||||||
--sw-text: rgba(255, 255, 255, 0.88);
|
|
||||||
--sw-text-muted: rgba(255, 255, 255, 0.6);
|
|
||||||
--sw-text-dim: rgba(255, 255, 255, 0.45);
|
|
||||||
--sw-accent: #58a6ff;
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark.is-ultra {
|
|
||||||
--sw-bg: #0a0a0d;
|
|
||||||
--sw-bg-soft: #131316;
|
|
||||||
--sw-bg-input: #050507;
|
|
||||||
--sw-bg-code: #000;
|
|
||||||
--sw-border: rgba(255, 255, 255, 0.06);
|
|
||||||
--sw-border-strong: rgba(255, 255, 255, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page .content-shell {
|
|
||||||
background: var(--bg-page);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page .content-area {
|
|
||||||
padding: 16px;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.api-docs-page .content-area {
|
|
||||||
padding: 8px;
|
|
||||||
padding-top: 56px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page .docs-wrapper {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--ant-color-border-secondary);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page .swagger-ui {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ──────────────────────────────────────────────────────────────────
|
|
||||||
Dark mode — Swagger UI does not ship a dark theme, so every visual
|
|
||||||
surface needs an explicit override. Method-color chips (GET / POST /
|
|
||||||
…) are left untouched because they carry meaning at a glance.
|
|
||||||
────────────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info .title,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info .title small pre,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info p,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info li,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info table,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-tag,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-tag small,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock .opblock-summary-path,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock .opblock-summary-path__deprecated,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock .opblock-summary-description,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-description-wrapper p,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-external-docs-wrapper p,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-title_normal p,
|
|
||||||
.api-docs-page.is-dark .swagger-ui table thead tr td,
|
|
||||||
.api-docs-page.is-dark .swagger-ui table thead tr th,
|
|
||||||
.api-docs-page.is-dark .swagger-ui table tbody tr td,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .parameter__name,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .parameter__type,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .parameter__in,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .parameter__extension,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .response-col_status,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .response-col_description,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .response-col_links,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .responses-inner h4,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .responses-inner h5,
|
|
||||||
.api-docs-page.is-dark .swagger-ui label,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .tab li,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .tab li button,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .markdown,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .markdown p,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .markdown li,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .renderedmarkdown p,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .renderedmarkdown li,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .model-title,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .model,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .model-toggle:after,
|
|
||||||
.api-docs-page.is-dark .swagger-ui section.models h4,
|
|
||||||
.api-docs-page.is-dark .swagger-ui section.models h5,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .auth-container h4,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .auth-container h6,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .scopes h2,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .dialog-ux .modal-ux-header h3,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .dialog-ux .modal-ux-content h4,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .dialog-ux .modal-ux-content p,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .servers-title {
|
|
||||||
color: var(--sw-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-tag small,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-summary-description,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .parameter__in,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .parameter__type,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .parameter__extension,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-title_normal small,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .servers > label,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .servers > label span,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .response-control-media-type__accept-message {
|
|
||||||
color: var(--sw-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-tag {
|
|
||||||
border-bottom-color: var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock {
|
|
||||||
background: var(--sw-bg-soft);
|
|
||||||
border-color: var(--sw-border);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock .opblock-section-header {
|
|
||||||
background: rgba(255, 255, 255, 0.03);
|
|
||||||
box-shadow: inset 0 -1px 0 var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock .opblock-section-header h4,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock .opblock-section-header label {
|
|
||||||
color: var(--sw-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock .opblock-summary {
|
|
||||||
border-bottom-color: var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-body pre.microlight,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .highlight-code,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .microlight,
|
|
||||||
.api-docs-page.is-dark .swagger-ui pre.example,
|
|
||||||
.api-docs-page.is-dark .swagger-ui code {
|
|
||||||
background: var(--sw-bg-code);
|
|
||||||
color: rgba(255, 255, 255, 0.92);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .highlight-code .copy-to-clipboard {
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui input[type='text'],
|
|
||||||
.api-docs-page.is-dark .swagger-ui input[type='password'],
|
|
||||||
.api-docs-page.is-dark .swagger-ui input[type='search'],
|
|
||||||
.api-docs-page.is-dark .swagger-ui input[type='email'],
|
|
||||||
.api-docs-page.is-dark .swagger-ui input[type='file'],
|
|
||||||
.api-docs-page.is-dark .swagger-ui textarea {
|
|
||||||
background: var(--sw-bg-input);
|
|
||||||
color: var(--sw-text);
|
|
||||||
border-color: var(--sw-border-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui select {
|
|
||||||
background-color: var(--sw-bg-input);
|
|
||||||
background-image: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path fill='%23b7bcbf' d='M13.418 7.859a.695.695 0 0 1 .978 0 .68.68 0 0 1 0 .969l-3.908 3.83a.697.697 0 0 1-.979 0l-3.908-3.83a.68.68 0 0 1 0-.969.695.695 0 0 1 .978 0L10 11z'/></svg>");
|
|
||||||
background-position: right 10px center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 20px;
|
|
||||||
color: var(--sw-text);
|
|
||||||
border-color: var(--sw-border-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui select option {
|
|
||||||
background-color: var(--sw-bg-input);
|
|
||||||
color: var(--sw-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui textarea {
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui input::placeholder,
|
|
||||||
.api-docs-page.is-dark .swagger-ui textarea::placeholder {
|
|
||||||
color: var(--sw-text-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .scheme-container {
|
|
||||||
background: var(--sw-bg-soft);
|
|
||||||
box-shadow: inset 0 -1px 0 var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .auth-wrapper .authorize {
|
|
||||||
color: var(--sw-text);
|
|
||||||
border-color: var(--sw-border-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .auth-wrapper .authorize svg {
|
|
||||||
fill: var(--sw-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .model-box,
|
|
||||||
.api-docs-page.is-dark .swagger-ui section.models {
|
|
||||||
background: var(--sw-bg-soft);
|
|
||||||
border-color: var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui section.models.is-open h4 {
|
|
||||||
border-bottom-color: var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui section.models .model-container {
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
border-color: var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui section.models .model-container:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .prop-type,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .prop-format {
|
|
||||||
color: var(--sw-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .property.primitive {
|
|
||||||
color: var(--sw-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-title_normal h4 {
|
|
||||||
color: var(--sw-text);
|
|
||||||
border-bottom-color: var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui table.parameters,
|
|
||||||
.api-docs-page.is-dark .swagger-ui table.responses-table,
|
|
||||||
.api-docs-page.is-dark .swagger-ui table thead tr {
|
|
||||||
border-color: var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui table tbody tr td {
|
|
||||||
border-bottom-color: var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui table thead tr {
|
|
||||||
border-bottom-color: var(--sw-border-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .response-col_status {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn {
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
color: var(--sw-text);
|
|
||||||
border-color: var(--sw-border-strong);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn.execute {
|
|
||||||
background: var(--sw-accent);
|
|
||||||
color: #0d1117;
|
|
||||||
border-color: var(--sw-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn.execute:hover {
|
|
||||||
background: #79b9ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn.authorize {
|
|
||||||
color: #52c41a;
|
|
||||||
border-color: rgba(82, 196, 26, 0.4);
|
|
||||||
background: rgba(82, 196, 26, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn.authorize svg {
|
|
||||||
fill: #52c41a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .authorization__btn svg,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .expand-operation svg,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-control-arrow svg {
|
|
||||||
fill: var(--sw-text);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .authorization__btn .locked,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .authorization__btn .unlocked {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn.cancel {
|
|
||||||
color: #ff7875;
|
|
||||||
border-color: rgba(255, 120, 117, 0.4);
|
|
||||||
background: rgba(255, 120, 117, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn.btn-clear,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn-clear,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .try-out__btn {
|
|
||||||
color: var(--sw-text);
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
border-color: var(--sw-border-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn.btn-clear:hover,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .btn-clear:hover,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .try-out__btn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .filter .operation-filter-input {
|
|
||||||
background: var(--sw-bg-input);
|
|
||||||
border-color: var(--sw-border-strong);
|
|
||||||
color: var(--sw-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .dialog-ux .modal-ux {
|
|
||||||
background: var(--sw-bg);
|
|
||||||
border-color: var(--sw-border-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .dialog-ux .modal-ux-header {
|
|
||||||
border-bottom-color: var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .dialog-ux .modal-ux-content {
|
|
||||||
color: var(--sw-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .arrow,
|
|
||||||
.api-docs-page.is-dark .swagger-ui svg.arrow {
|
|
||||||
fill: var(--sw-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-summary-control:focus {
|
|
||||||
outline-color: var(--sw-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui a,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info a,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info hgroup.main a,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info .base-url,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info__contact a,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info__license a,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info__tos a {
|
|
||||||
color: var(--sw-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui a:hover {
|
|
||||||
color: #79b9ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info .title small {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
color: var(--sw-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .info .title small pre {
|
|
||||||
color: var(--sw-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .response-control-media-type--accept-controller select {
|
|
||||||
border-color: rgba(82, 196, 26, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .loading-container .loading:before {
|
|
||||||
border-color: var(--sw-accent) transparent transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .json-schema-form-item input,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .json-schema-form-item select {
|
|
||||||
background: var(--sw-bg-input);
|
|
||||||
color: var(--sw-text);
|
|
||||||
border-color: var(--sw-border-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .topbar {
|
|
||||||
background: var(--sw-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .information-container {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .opblock-summary-method {
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .auth-btn-wrapper {
|
|
||||||
border-top-color: var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .servers .computed-url,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .computed-url {
|
|
||||||
background: var(--sw-bg-code);
|
|
||||||
color: var(--sw-text);
|
|
||||||
border: 1px solid var(--sw-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .computed-url code,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .servers .computed-url code {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--sw-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .errors-wrapper {
|
|
||||||
background: rgba(255, 77, 79, 0.08);
|
|
||||||
border-color: rgba(255, 77, 79, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-docs-page.is-dark .swagger-ui .errors-wrapper .errors h4,
|
|
||||||
.api-docs-page.is-dark .swagger-ui .errors-wrapper .errors small {
|
|
||||||
color: var(--sw-text);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { ConfigProvider, Layout } from 'antd';
|
|
||||||
import SwaggerUI from 'swagger-ui-react';
|
|
||||||
import 'swagger-ui-react/swagger-ui.css';
|
|
||||||
|
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
|
||||||
import AppSidebar from '@/layouts/AppSidebar';
|
|
||||||
import './ApiDocsPage.css';
|
|
||||||
|
|
||||||
const basePath = window.X_UI_BASE_PATH || '';
|
|
||||||
const openApiUrl = `${basePath}panel/api/openapi.json`;
|
|
||||||
|
|
||||||
export default function ApiDocsPage() {
|
|
||||||
const { isDark, isUltra, antdThemeConfig } = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const pageClass = useMemo(() => {
|
|
||||||
const classes = ['api-docs-page'];
|
|
||||||
if (isDark) classes.push('is-dark');
|
|
||||||
if (isUltra) classes.push('is-ultra');
|
|
||||||
return classes.join(' ');
|
|
||||||
}, [isDark, isUltra]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigProvider theme={antdThemeConfig}>
|
|
||||||
<Layout className={pageClass}>
|
|
||||||
<AppSidebar />
|
|
||||||
|
|
||||||
<Layout className="content-shell">
|
|
||||||
<Layout.Content className="content-area">
|
|
||||||
<div className="docs-wrapper" role="region" aria-label={t('menu.apiDocs')}>
|
|
||||||
<SwaggerUI
|
|
||||||
url={openApiUrl}
|
|
||||||
docExpansion="list"
|
|
||||||
deepLinking={false}
|
|
||||||
tryItOutEnabled
|
|
||||||
persistAuthorization
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Layout.Content>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
</ConfigProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,89 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { AutoComplete, Form, Modal, message } from 'antd';
|
|
||||||
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { FormField } from '@/components/form/rhf';
|
|
||||||
|
|
||||||
type GroupFormValues = { group: string };
|
|
||||||
|
|
||||||
const EMPTY: GroupFormValues = { group: '' };
|
|
||||||
|
|
||||||
interface BulkAddToGroupModalProps {
|
|
||||||
open: boolean;
|
|
||||||
count: number;
|
|
||||||
groups: string[];
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
onSubmit: (group: string) => Promise<{ affected?: number } | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BulkAddToGroupModal({
|
|
||||||
open,
|
|
||||||
count,
|
|
||||||
groups,
|
|
||||||
onOpenChange,
|
|
||||||
onSubmit,
|
|
||||||
}: BulkAddToGroupModalProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
const methods = useForm<GroupFormValues>({ defaultValues: EMPTY });
|
|
||||||
const group = useWatch({ control: methods.control, name: 'group' });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) methods.reset(EMPTY);
|
|
||||||
}, [open, methods]);
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
const next = (methods.getValues().group ?? '').trim();
|
|
||||||
if (!next) return;
|
|
||||||
setSubmitting(true);
|
|
||||||
try {
|
|
||||||
const result = await onSubmit(next);
|
|
||||||
if (result) {
|
|
||||||
const affected = result.affected ?? 0;
|
|
||||||
messageApi.success(
|
|
||||||
t('pages.clients.addToGroupSuccessToast', { count: affected, group: next }),
|
|
||||||
);
|
|
||||||
onOpenChange(false);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{messageContextHolder}
|
|
||||||
<Modal
|
|
||||||
open={open}
|
|
||||||
title={t('pages.clients.addToGroupTitle', { count })}
|
|
||||||
okText={t('add')}
|
|
||||||
cancelText={t('cancel')}
|
|
||||||
confirmLoading={submitting}
|
|
||||||
okButtonProps={{ disabled: !(group ?? '').trim() }}
|
|
||||||
onCancel={() => onOpenChange(false)}
|
|
||||||
onOk={submit}
|
|
||||||
destroyOnHidden
|
|
||||||
>
|
|
||||||
<FormProvider {...methods}>
|
|
||||||
<Form layout="vertical">
|
|
||||||
<FormField
|
|
||||||
name="group"
|
|
||||||
label={t('pages.clients.group')}
|
|
||||||
tooltip={t('pages.clients.addToGroupTooltip')}
|
|
||||||
transform={{ output: (v) => v ?? '' }}
|
|
||||||
>
|
|
||||||
<AutoComplete
|
|
||||||
placeholder={t('pages.clients.groupName')}
|
|
||||||
options={groups.map((g) => ({ value: g }))}
|
|
||||||
allowClear
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
|
||||||
</FormProvider>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -43,7 +43,6 @@ import {
|
|||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
SortAscendingOutlined,
|
SortAscendingOutlined,
|
||||||
StopOutlined,
|
StopOutlined,
|
||||||
TagsOutlined,
|
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
UsergroupAddOutlined,
|
UsergroupAddOutlined,
|
||||||
@@ -83,7 +82,6 @@ const ClientBulkAddModal = lazy(() => import('./ClientBulkAddModal'));
|
|||||||
const ClientBulkAdjustModal = lazy(() => import('./ClientBulkAdjustModal'));
|
const ClientBulkAdjustModal = lazy(() => import('./ClientBulkAdjustModal'));
|
||||||
const FilterDrawer = lazy(() => import('./FilterDrawer'));
|
const FilterDrawer = lazy(() => import('./FilterDrawer'));
|
||||||
const SubLinksModal = lazy(() => import('./SubLinksModal'));
|
const SubLinksModal = lazy(() => import('./SubLinksModal'));
|
||||||
const BulkAddToGroupModal = lazy(() => import('./BulkAddToGroupModal'));
|
|
||||||
const BulkAttachInboundsModal = lazy(() => import('./BulkAttachInboundsModal'));
|
const BulkAttachInboundsModal = lazy(() => import('./BulkAttachInboundsModal'));
|
||||||
const BulkDetachInboundsModal = lazy(() => import('./BulkDetachInboundsModal'));
|
const BulkDetachInboundsModal = lazy(() => import('./BulkDetachInboundsModal'));
|
||||||
const TextModal = lazy(() => import('@/components/feedback/TextModal'));
|
const TextModal = lazy(() => import('@/components/feedback/TextModal'));
|
||||||
@@ -97,45 +95,6 @@ const FILTER_STATE_KEY = 'clientsFilterState';
|
|||||||
const DISABLED_PAGE_SIZE = 200;
|
const DISABLED_PAGE_SIZE = 200;
|
||||||
const DEFAULT_TABLE_PAGE_SIZE = 25;
|
const DEFAULT_TABLE_PAGE_SIZE = 25;
|
||||||
|
|
||||||
function UngroupIcon() {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '1em',
|
|
||||||
height: '1em',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TagsOutlined />
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
inset: 0,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
width: '125%',
|
|
||||||
height: '1.5px',
|
|
||||||
background: 'currentColor',
|
|
||||||
transform: 'rotate(-45deg)',
|
|
||||||
borderRadius: '1px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The server sends exact counters but caps the email arrays behind them, so a
|
// The server sends exact counters but caps the email arrays behind them, so a
|
||||||
// panel with thousands of depleted clients neither ships nor renders them all.
|
// panel with thousands of depleted clients neither ships nor renders them all.
|
||||||
// The trailing chip reports what the popover left out.
|
// The trailing chip reports what the popover left out.
|
||||||
@@ -314,8 +273,6 @@ export default function ClientsPage() {
|
|||||||
bulkAdjust,
|
bulkAdjust,
|
||||||
bulkEnable,
|
bulkEnable,
|
||||||
bulkDisable,
|
bulkDisable,
|
||||||
bulkAddToGroup,
|
|
||||||
bulkRemoveFromGroup,
|
|
||||||
attach,
|
attach,
|
||||||
setExternalLinks,
|
setExternalLinks,
|
||||||
bulkAttach,
|
bulkAttach,
|
||||||
@@ -363,7 +320,6 @@ export default function ClientsPage() {
|
|||||||
const [bulkAddOpen, setBulkAddOpen] = useState(false);
|
const [bulkAddOpen, setBulkAddOpen] = useState(false);
|
||||||
const [bulkAdjustOpen, setBulkAdjustOpen] = useState(false);
|
const [bulkAdjustOpen, setBulkAdjustOpen] = useState(false);
|
||||||
const [subLinksOpen, setSubLinksOpen] = useState(false);
|
const [subLinksOpen, setSubLinksOpen] = useState(false);
|
||||||
const [bulkGroupOpen, setBulkGroupOpen] = useState(false);
|
|
||||||
const [bulkAttachOpen, setBulkAttachOpen] = useState(false);
|
const [bulkAttachOpen, setBulkAttachOpen] = useState(false);
|
||||||
const [bulkDetachOpen, setBulkDetachOpen] = useState(false);
|
const [bulkDetachOpen, setBulkDetachOpen] = useState(false);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||||
@@ -846,27 +802,6 @@ export default function ClientsPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBulkUngroup() {
|
|
||||||
const emails = [...selectedRowKeys];
|
|
||||||
if (emails.length === 0) return;
|
|
||||||
modal.confirm({
|
|
||||||
title: t('pages.clients.ungroupConfirmTitle', { count: emails.length }),
|
|
||||||
content: t('pages.clients.ungroupConfirmContent'),
|
|
||||||
okText: t('confirm'),
|
|
||||||
okType: 'danger',
|
|
||||||
cancelText: t('cancel'),
|
|
||||||
onOk: async () => {
|
|
||||||
const msg = await bulkRemoveFromGroup(emails);
|
|
||||||
if (msg?.success) {
|
|
||||||
setSelectedRowKeys([]);
|
|
||||||
const affected =
|
|
||||||
(msg.obj as { affected?: number } | undefined)?.affected ?? emails.length;
|
|
||||||
messageApi.success(t('pages.clients.ungroupSuccessToast', { count: affected }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBulkSetEnable(enable: boolean) {
|
function onBulkSetEnable(enable: boolean) {
|
||||||
const emails = [...selectedRowKeys];
|
const emails = [...selectedRowKeys];
|
||||||
if (emails.length === 0) return;
|
if (emails.length === 0) return;
|
||||||
@@ -1382,19 +1317,6 @@ export default function ClientsPage() {
|
|||||||
danger: true,
|
danger: true,
|
||||||
onClick: () => setBulkDetachOpen(true),
|
onClick: () => setBulkDetachOpen(true),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'addToGroup',
|
|
||||||
icon: <TagsOutlined />,
|
|
||||||
label: t('pages.clients.addToGroup'),
|
|
||||||
onClick: () => setBulkGroupOpen(true),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'ungroup',
|
|
||||||
icon: <UngroupIcon />,
|
|
||||||
label: t('pages.clients.ungroup'),
|
|
||||||
danger: true,
|
|
||||||
onClick: onBulkUngroup,
|
|
||||||
},
|
|
||||||
{ type: 'divider' as const },
|
{ type: 'divider' as const },
|
||||||
{
|
{
|
||||||
key: 'enable',
|
key: 'enable',
|
||||||
@@ -1912,22 +1834,6 @@ export default function ClientsPage() {
|
|||||||
onOpenChange={setSubLinksOpen}
|
onOpenChange={setSubLinksOpen}
|
||||||
/>
|
/>
|
||||||
</LazyMount>
|
</LazyMount>
|
||||||
<LazyMount when={bulkGroupOpen}>
|
|
||||||
<BulkAddToGroupModal
|
|
||||||
open={bulkGroupOpen}
|
|
||||||
count={selectedRowKeys.length}
|
|
||||||
groups={allGroups}
|
|
||||||
onOpenChange={setBulkGroupOpen}
|
|
||||||
onSubmit={async (group) => {
|
|
||||||
const msg = await bulkAddToGroup([...selectedRowKeys], group);
|
|
||||||
if (msg?.success) {
|
|
||||||
setSelectedRowKeys([]);
|
|
||||||
return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</LazyMount>
|
|
||||||
<LazyMount when={bulkAttachOpen}>
|
<LazyMount when={bulkAttachOpen}>
|
||||||
<BulkAttachInboundsModal
|
<BulkAttachInboundsModal
|
||||||
open={bulkAttachOpen}
|
open={bulkAttachOpen}
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
import { useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Alert, Input, Modal, Space, Table, Tag, Typography, message } from 'antd';
|
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
|
||||||
|
|
||||||
import type { ClientRecord } from '@/hooks/useClients';
|
|
||||||
|
|
||||||
interface GroupAddClientsModalProps {
|
|
||||||
open: boolean;
|
|
||||||
groupName: string | null;
|
|
||||||
candidates: ClientRecord[];
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (emails: string[]) => Promise<{ affected?: number } | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClientRow {
|
|
||||||
email: string;
|
|
||||||
comment: string;
|
|
||||||
enable: boolean;
|
|
||||||
currentGroup: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GroupAddClientsModal({
|
|
||||||
open,
|
|
||||||
groupName,
|
|
||||||
candidates,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
}: GroupAddClientsModalProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
|
|
||||||
const rows = useMemo<ClientRow[]>(
|
|
||||||
() =>
|
|
||||||
(candidates || [])
|
|
||||||
.map((c) => ({
|
|
||||||
email: (c.email || '').trim(),
|
|
||||||
comment: (c.comment || '').trim(),
|
|
||||||
enable: c.enable !== false,
|
|
||||||
currentGroup: (c.group || '').trim(),
|
|
||||||
}))
|
|
||||||
.filter((r) => r.email),
|
|
||||||
[candidates],
|
|
||||||
);
|
|
||||||
|
|
||||||
// React resets this during render rather than in an effect so the modal's
|
|
||||||
// first open frame already shows cleared fields.
|
|
||||||
const [wasOpen, setWasOpen] = useState(false);
|
|
||||||
if (open !== wasOpen) {
|
|
||||||
setWasOpen(open);
|
|
||||||
if (open) {
|
|
||||||
setSelectedEmails([]);
|
|
||||||
setSearch('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredRows = useMemo(() => {
|
|
||||||
const q = search.trim().toLowerCase();
|
|
||||||
if (!q) return rows;
|
|
||||||
return rows.filter(
|
|
||||||
(r) =>
|
|
||||||
r.email.toLowerCase().includes(q) ||
|
|
||||||
r.comment.toLowerCase().includes(q) ||
|
|
||||||
r.currentGroup.toLowerCase().includes(q),
|
|
||||||
);
|
|
||||||
}, [rows, search]);
|
|
||||||
|
|
||||||
const columns: ColumnsType<ClientRow> = useMemo(
|
|
||||||
() => [
|
|
||||||
{ title: t('pages.inbounds.email'), dataIndex: 'email', key: 'email', ellipsis: true },
|
|
||||||
{ title: t('comment'), dataIndex: 'comment', key: 'comment', ellipsis: true },
|
|
||||||
{
|
|
||||||
title: t('pages.clients.group'),
|
|
||||||
dataIndex: 'currentGroup',
|
|
||||||
key: 'currentGroup',
|
|
||||||
width: 140,
|
|
||||||
ellipsis: true,
|
|
||||||
render: (g: string) =>
|
|
||||||
g ? (
|
|
||||||
<Tag color="geekblue">{g}</Tag>
|
|
||||||
) : (
|
|
||||||
<span style={{ color: 'rgba(0,0,0,0.45)' }}>—</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('enable'),
|
|
||||||
dataIndex: 'enable',
|
|
||||||
key: 'enable',
|
|
||||||
width: 90,
|
|
||||||
render: (enabled: boolean) =>
|
|
||||||
enabled ? (
|
|
||||||
<Tag color="success">{t('enable')}</Tag>
|
|
||||||
) : (
|
|
||||||
<Tag>{t('pages.inbounds.attachClientsStatusDisabled')}</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[t],
|
|
||||||
);
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
if (!groupName || selectedEmails.length === 0) return;
|
|
||||||
setSaving(true);
|
|
||||||
try {
|
|
||||||
const result = await onSubmit(selectedEmails);
|
|
||||||
if (!result) return;
|
|
||||||
const affected = result.affected ?? selectedEmails.length;
|
|
||||||
messageApi.success(t('pages.groups.addToGroupResult', { count: affected, name: groupName }));
|
|
||||||
onClose();
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={open}
|
|
||||||
onCancel={onClose}
|
|
||||||
onOk={submit}
|
|
||||||
okButtonProps={{ disabled: selectedEmails.length === 0, loading: saving }}
|
|
||||||
okText={t('add')}
|
|
||||||
cancelText={t('cancel')}
|
|
||||||
title={t('pages.groups.addToGroupTitle', { name: groupName ?? '' })}
|
|
||||||
width={720}
|
|
||||||
>
|
|
||||||
{messageContextHolder}
|
|
||||||
<Typography.Paragraph type="secondary">
|
|
||||||
{t('pages.groups.addToGroupDesc')}
|
|
||||||
</Typography.Paragraph>
|
|
||||||
|
|
||||||
<Space orientation="vertical" size="small" style={{ width: '100%' }}>
|
|
||||||
<Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
|
|
||||||
<Input.Search
|
|
||||||
allowClear
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
|
|
||||||
style={{ maxWidth: 320 }}
|
|
||||||
/>
|
|
||||||
<Typography.Text type="secondary">
|
|
||||||
{t('pages.inbounds.attachClientsSelectedCount', {
|
|
||||||
selected: selectedEmails.length,
|
|
||||||
total: rows.length,
|
|
||||||
})}
|
|
||||||
</Typography.Text>
|
|
||||||
</Space>
|
|
||||||
{rows.length === 0 ? (
|
|
||||||
<Alert type="info" showIcon title={t('pages.groups.addToGroupEmpty')} />
|
|
||||||
) : (
|
|
||||||
<Table<ClientRow>
|
|
||||||
size="small"
|
|
||||||
rowKey="email"
|
|
||||||
columns={columns}
|
|
||||||
dataSource={filteredRows}
|
|
||||||
pagination={false}
|
|
||||||
scroll={{ y: 320 }}
|
|
||||||
rowSelection={{
|
|
||||||
selectedRowKeys: selectedEmails,
|
|
||||||
onChange: (keys) => setSelectedEmails(keys as string[]),
|
|
||||||
preserveSelectedRowKeys: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Input, Modal, Space, Table, Tag, Typography, message } from 'antd';
|
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
|
||||||
|
|
||||||
import type { ClientRecord } from '@/hooks/useClients';
|
|
||||||
|
|
||||||
interface GroupRemoveClientsModalProps {
|
|
||||||
open: boolean;
|
|
||||||
groupName: string | null;
|
|
||||||
members: ClientRecord[];
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (emails: string[]) => Promise<{ affected?: number } | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClientRow {
|
|
||||||
email: string;
|
|
||||||
comment: string;
|
|
||||||
enable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GroupRemoveClientsModal({
|
|
||||||
open,
|
|
||||||
groupName,
|
|
||||||
members,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
}: GroupRemoveClientsModalProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
|
|
||||||
const rows = useMemo<ClientRow[]>(
|
|
||||||
() =>
|
|
||||||
(members || [])
|
|
||||||
.map((c) => ({
|
|
||||||
email: (c.email || '').trim(),
|
|
||||||
comment: (c.comment || '').trim(),
|
|
||||||
enable: c.enable !== false,
|
|
||||||
}))
|
|
||||||
.filter((r) => r.email),
|
|
||||||
[members],
|
|
||||||
);
|
|
||||||
|
|
||||||
// React resets this during render rather than in an effect so the modal's
|
|
||||||
// first open frame already shows cleared fields.
|
|
||||||
const [wasOpen, setWasOpen] = useState(false);
|
|
||||||
if (open !== wasOpen) {
|
|
||||||
setWasOpen(open);
|
|
||||||
if (open) {
|
|
||||||
setSelectedEmails([]);
|
|
||||||
setSearch('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredRows = useMemo(() => {
|
|
||||||
const q = search.trim().toLowerCase();
|
|
||||||
if (!q) return rows;
|
|
||||||
return rows.filter(
|
|
||||||
(r) => r.email.toLowerCase().includes(q) || r.comment.toLowerCase().includes(q),
|
|
||||||
);
|
|
||||||
}, [rows, search]);
|
|
||||||
|
|
||||||
const columns: ColumnsType<ClientRow> = useMemo(
|
|
||||||
() => [
|
|
||||||
{ title: t('pages.inbounds.email'), dataIndex: 'email', key: 'email', ellipsis: true },
|
|
||||||
{ title: t('comment'), dataIndex: 'comment', key: 'comment', ellipsis: true },
|
|
||||||
{
|
|
||||||
title: t('enable'),
|
|
||||||
dataIndex: 'enable',
|
|
||||||
key: 'enable',
|
|
||||||
width: 90,
|
|
||||||
render: (enabled: boolean) =>
|
|
||||||
enabled ? (
|
|
||||||
<Tag color="success">{t('enable')}</Tag>
|
|
||||||
) : (
|
|
||||||
<Tag>{t('pages.inbounds.attachClientsStatusDisabled')}</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[t],
|
|
||||||
);
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
if (!groupName || selectedEmails.length === 0) return;
|
|
||||||
setSaving(true);
|
|
||||||
try {
|
|
||||||
const result = await onSubmit(selectedEmails);
|
|
||||||
if (!result) return;
|
|
||||||
const affected = result.affected ?? selectedEmails.length;
|
|
||||||
messageApi.success(
|
|
||||||
t('pages.groups.removeFromGroupResult', { count: affected, name: groupName }),
|
|
||||||
);
|
|
||||||
onClose();
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={open}
|
|
||||||
onCancel={onClose}
|
|
||||||
onOk={submit}
|
|
||||||
okButtonProps={{ danger: true, disabled: selectedEmails.length === 0, loading: saving }}
|
|
||||||
okText={t('remove')}
|
|
||||||
cancelText={t('cancel')}
|
|
||||||
title={t('pages.groups.removeFromGroupTitle', { name: groupName ?? '' })}
|
|
||||||
width={680}
|
|
||||||
>
|
|
||||||
{messageContextHolder}
|
|
||||||
<Typography.Paragraph type="secondary">
|
|
||||||
{t('pages.groups.removeFromGroupDesc')}
|
|
||||||
</Typography.Paragraph>
|
|
||||||
|
|
||||||
<Space orientation="vertical" size="small" style={{ width: '100%' }}>
|
|
||||||
<Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
|
|
||||||
<Input.Search
|
|
||||||
allowClear
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
|
|
||||||
style={{ maxWidth: 320 }}
|
|
||||||
/>
|
|
||||||
<Typography.Text type="secondary">
|
|
||||||
{t('pages.inbounds.attachClientsSelectedCount', {
|
|
||||||
selected: selectedEmails.length,
|
|
||||||
total: rows.length,
|
|
||||||
})}
|
|
||||||
</Typography.Text>
|
|
||||||
</Space>
|
|
||||||
<Table<ClientRow>
|
|
||||||
size="small"
|
|
||||||
rowKey="email"
|
|
||||||
columns={columns}
|
|
||||||
dataSource={filteredRows}
|
|
||||||
pagination={false}
|
|
||||||
scroll={{ y: 280 }}
|
|
||||||
rowSelection={{
|
|
||||||
selectedRowKeys: selectedEmails,
|
|
||||||
onChange: (keys) => setSelectedEmails(keys as string[]),
|
|
||||||
preserveSelectedRowKeys: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,708 +0,0 @@
|
|||||||
import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Col,
|
|
||||||
ConfigProvider,
|
|
||||||
Dropdown,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Layout,
|
|
||||||
Modal,
|
|
||||||
Result,
|
|
||||||
Row,
|
|
||||||
Space,
|
|
||||||
Spin,
|
|
||||||
Statistic,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Tooltip,
|
|
||||||
message,
|
|
||||||
} from 'antd';
|
|
||||||
import type { MenuProps, TableColumnsType } from 'antd';
|
|
||||||
import {
|
|
||||||
ArrowDownOutlined,
|
|
||||||
ArrowUpOutlined,
|
|
||||||
ClockCircleOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
MoreOutlined,
|
|
||||||
PieChartOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
RetweetOutlined,
|
|
||||||
TagsOutlined,
|
|
||||||
TeamOutlined,
|
|
||||||
UsergroupAddOutlined,
|
|
||||||
UsergroupDeleteOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
|
||||||
import { usePageTitle } from '@/hooks/usePageTitle';
|
|
||||||
import { useClients } from '@/hooks/useClients';
|
|
||||||
import { HttpUtil, SizeFormatter } from '@/utils';
|
|
||||||
import { setMessageInstance } from '@/utils/messageBus';
|
|
||||||
import AppSidebar from '@/layouts/AppSidebar';
|
|
||||||
import { LazyMount } from '@/components/utility';
|
|
||||||
import { keys } from '@/api/queryKeys';
|
|
||||||
import {
|
|
||||||
ClientRecordSchema,
|
|
||||||
GroupSummaryListSchema,
|
|
||||||
type ClientRecord,
|
|
||||||
type GroupSummary,
|
|
||||||
} from '@/schemas/client';
|
|
||||||
import { parseMsg } from '@/utils/zodValidate';
|
|
||||||
|
|
||||||
const ClientRecordListSchema = z
|
|
||||||
.array(ClientRecordSchema)
|
|
||||||
.nullable()
|
|
||||||
.transform((v) => v ?? []);
|
|
||||||
|
|
||||||
const SubLinksModal = lazy(() => import('../clients/SubLinksModal'));
|
|
||||||
const ClientBulkAdjustModal = lazy(() => import('../clients/ClientBulkAdjustModal'));
|
|
||||||
const GroupAddClientsModal = lazy(() => import('./GroupAddClientsModal'));
|
|
||||||
const GroupRemoveClientsModal = lazy(() => import('./GroupRemoveClientsModal'));
|
|
||||||
|
|
||||||
const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const;
|
|
||||||
|
|
||||||
async function fetchGroups(): Promise<GroupSummary[]> {
|
|
||||||
const msg = await HttpUtil.get('/panel/api/clients/groups', undefined, { silent: true });
|
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to load groups');
|
|
||||||
const validated = parseMsg(msg, GroupSummaryListSchema, 'clients/groups');
|
|
||||||
return validated.obj ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchEmailsForGroup(name: string): Promise<string[]> {
|
|
||||||
const msg = await HttpUtil.get<string[]>(
|
|
||||||
`/panel/api/clients/groups/${encodeURIComponent(name)}/emails`,
|
|
||||||
undefined,
|
|
||||||
{ silent: true },
|
|
||||||
);
|
|
||||||
if (!msg?.success || !Array.isArray(msg.obj)) return [];
|
|
||||||
return msg.obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GroupsPage() {
|
|
||||||
usePageTitle();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { isDark, isUltra, antdThemeConfig } = useTheme();
|
|
||||||
const { isMobile } = useMediaQuery();
|
|
||||||
const [modal, modalContextHolder] = Modal.useModal();
|
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
|
||||||
useEffect(() => {
|
|
||||||
setMessageInstance(messageApi);
|
|
||||||
}, [messageApi]);
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { subSettings, bulkAdjust, bulkAddToGroup, bulkRemoveFromGroup, bulkDelete } = useClients({
|
|
||||||
list: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupsQuery = useQuery({
|
|
||||||
queryKey: keys.clients.groups(),
|
|
||||||
queryFn: fetchGroups,
|
|
||||||
});
|
|
||||||
const groups = useMemo(() => groupsQuery.data ?? [], [groupsQuery.data]);
|
|
||||||
const loading = groupsQuery.isFetching;
|
|
||||||
const fetched = groupsQuery.data !== undefined || groupsQuery.isError;
|
|
||||||
const fetchError = groupsQuery.error ? (groupsQuery.error as Error).message : '';
|
|
||||||
|
|
||||||
const invalidate = useCallback(() => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: keys.clients.root() });
|
|
||||||
}, [queryClient]);
|
|
||||||
|
|
||||||
const createMut = useMutation({
|
|
||||||
mutationFn: (body: { name: string }) =>
|
|
||||||
HttpUtil.post('/panel/api/clients/groups/create', body, JSON_HEADERS),
|
|
||||||
onSuccess: (msg) => {
|
|
||||||
if (msg?.success) invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const renameMut = useMutation({
|
|
||||||
mutationFn: (body: { oldName: string; newName: string }) =>
|
|
||||||
HttpUtil.post('/panel/api/clients/groups/rename', body, JSON_HEADERS),
|
|
||||||
onSuccess: (msg) => {
|
|
||||||
if (msg?.success) invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMut = useMutation({
|
|
||||||
mutationFn: (body: { name: string }) =>
|
|
||||||
HttpUtil.post('/panel/api/clients/groups/delete', body, JSON_HEADERS),
|
|
||||||
onSuccess: (msg) => {
|
|
||||||
if (msg?.success) invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupResetMut = useMutation({
|
|
||||||
mutationFn: (body: { name: string }) =>
|
|
||||||
HttpUtil.post('/panel/api/clients/groups/resetTraffic', body, JSON_HEADERS),
|
|
||||||
onSuccess: (msg) => {
|
|
||||||
if (msg?.success) invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
|
||||||
const [createName, setCreateName] = useState('');
|
|
||||||
|
|
||||||
const [renameOpen, setRenameOpen] = useState(false);
|
|
||||||
const [renameTarget, setRenameTarget] = useState<GroupSummary | null>(null);
|
|
||||||
const [renameValue, setRenameValue] = useState('');
|
|
||||||
|
|
||||||
const [subLinksOpen, setSubLinksOpen] = useState(false);
|
|
||||||
const [adjustOpen, setAdjustOpen] = useState(false);
|
|
||||||
const [addClientsOpen, setAddClientsOpen] = useState(false);
|
|
||||||
const [removeClientsOpen, setRemoveClientsOpen] = useState(false);
|
|
||||||
const [groupEmails, setGroupEmails] = useState<string[]>([]);
|
|
||||||
const [groupForAction, setGroupForAction] = useState<GroupSummary | null>(null);
|
|
||||||
|
|
||||||
const allClientsQuery = useQuery<ClientRecord[]>({
|
|
||||||
queryKey: keys.clients.all(),
|
|
||||||
queryFn: async () => {
|
|
||||||
const msg = await HttpUtil.get('/panel/api/clients/list', undefined, { silent: true });
|
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to load clients');
|
|
||||||
const validated = parseMsg(msg, ClientRecordListSchema, 'clients/list');
|
|
||||||
return validated.obj ?? [];
|
|
||||||
},
|
|
||||||
enabled: addClientsOpen || removeClientsOpen || subLinksOpen,
|
|
||||||
staleTime: 30_000,
|
|
||||||
});
|
|
||||||
const allClients = allClientsQuery.data ?? [];
|
|
||||||
|
|
||||||
const totalGroups = groups.length;
|
|
||||||
const totalClients = useMemo(
|
|
||||||
() => groups.reduce((acc, g) => acc + (g.clientCount || 0), 0),
|
|
||||||
[groups],
|
|
||||||
);
|
|
||||||
const totalTraffic = useMemo(
|
|
||||||
() => groups.reduce((acc, g) => acc + (g.trafficUsed || 0), 0),
|
|
||||||
[groups],
|
|
||||||
);
|
|
||||||
const totalUpload = useMemo(() => groups.reduce((acc, g) => acc + (g.up || 0), 0), [groups]);
|
|
||||||
const totalDownload = useMemo(() => groups.reduce((acc, g) => acc + (g.down || 0), 0), [groups]);
|
|
||||||
|
|
||||||
function openCreate() {
|
|
||||||
setCreateName('');
|
|
||||||
setCreateOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmCreate() {
|
|
||||||
const name = createName.trim();
|
|
||||||
if (!name) return;
|
|
||||||
if (groups.some((g) => g.name.toLowerCase() === name.toLowerCase())) {
|
|
||||||
messageApi.error(t('pages.groups.renameCollision', { name }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const msg = await createMut.mutateAsync({ name });
|
|
||||||
if (msg?.success) {
|
|
||||||
messageApi.success(t('pages.groups.createSuccess', { name }));
|
|
||||||
setCreateOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openRename(g: GroupSummary) {
|
|
||||||
setRenameTarget(g);
|
|
||||||
setRenameValue(g.name);
|
|
||||||
setRenameOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmRename() {
|
|
||||||
if (!renameTarget) return;
|
|
||||||
const next = renameValue.trim();
|
|
||||||
if (!next || next === renameTarget.name) {
|
|
||||||
setRenameOpen(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
groups.some(
|
|
||||||
(g) => g.name.toLowerCase() === next.toLowerCase() && g.name !== renameTarget.name,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
messageApi.error(t('pages.groups.renameCollision', { name: next }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const msg = await renameMut.mutateAsync({ oldName: renameTarget.name, newName: next });
|
|
||||||
if (msg?.success) {
|
|
||||||
const affected = (msg.obj as { affected?: number } | undefined)?.affected ?? 0;
|
|
||||||
messageApi.success(t('pages.groups.renameSuccess', { count: affected }));
|
|
||||||
setRenameOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDelete(g: GroupSummary) {
|
|
||||||
modal.confirm({
|
|
||||||
title: t('pages.groups.deleteConfirmTitle', { name: g.name }),
|
|
||||||
content: t('pages.groups.deleteConfirmContent', { count: g.clientCount }),
|
|
||||||
okText: t('delete'),
|
|
||||||
okType: 'danger',
|
|
||||||
cancelText: t('cancel'),
|
|
||||||
onOk: async () => {
|
|
||||||
const msg = await deleteMut.mutateAsync({ name: g.name });
|
|
||||||
if (msg?.success) {
|
|
||||||
const affected = (msg.obj as { affected?: number } | undefined)?.affected ?? 0;
|
|
||||||
messageApi.success(t('pages.groups.deleteSuccess', { count: affected }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openSubLinksFor(g: GroupSummary) {
|
|
||||||
if (!g.clientCount) {
|
|
||||||
messageApi.info(t('pages.groups.emptyForAction'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const emails = await fetchEmailsForGroup(g.name);
|
|
||||||
if (emails.length === 0) {
|
|
||||||
messageApi.info(t('pages.groups.emptyForAction'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setGroupForAction(g);
|
|
||||||
setGroupEmails(emails);
|
|
||||||
setSubLinksOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openAdjustFor(g: GroupSummary) {
|
|
||||||
if (!g.clientCount) {
|
|
||||||
messageApi.info(t('pages.groups.emptyForAction'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const emails = await fetchEmailsForGroup(g.name);
|
|
||||||
if (emails.length === 0) {
|
|
||||||
messageApi.info(t('pages.groups.emptyForAction'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setGroupForAction(g);
|
|
||||||
setGroupEmails(emails);
|
|
||||||
setAdjustOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAddClientsFor(g: GroupSummary) {
|
|
||||||
setGroupForAction(g);
|
|
||||||
setAddClientsOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openRemoveClientsFor(g: GroupSummary) {
|
|
||||||
if (!g.clientCount) {
|
|
||||||
messageApi.info(t('pages.groups.emptyForAction'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setGroupForAction(g);
|
|
||||||
setRemoveClientsOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeleteClients(g: GroupSummary) {
|
|
||||||
if (!g.clientCount) {
|
|
||||||
messageApi.info(t('pages.groups.emptyForAction'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
modal.confirm({
|
|
||||||
title: t('pages.groups.deleteClientsConfirmTitle', { name: g.name }),
|
|
||||||
content: t('pages.groups.deleteClientsConfirmContent', { count: g.clientCount }),
|
|
||||||
okText: t('delete'),
|
|
||||||
okType: 'danger',
|
|
||||||
cancelText: t('cancel'),
|
|
||||||
onOk: async () => {
|
|
||||||
const emails = await fetchEmailsForGroup(g.name);
|
|
||||||
if (emails.length === 0) return;
|
|
||||||
const msg = await bulkDelete(emails);
|
|
||||||
if (msg?.success) {
|
|
||||||
const ok = msg.obj?.deleted ?? 0;
|
|
||||||
const skipped = msg.obj?.skipped ?? [];
|
|
||||||
const failed = skipped.length;
|
|
||||||
if (failed === 0) {
|
|
||||||
messageApi.success(t('pages.groups.deleteClientsSuccess', { count: ok }));
|
|
||||||
} else {
|
|
||||||
const firstError = skipped[0]?.reason ?? msg?.msg ?? '';
|
|
||||||
messageApi.warning(
|
|
||||||
firstError
|
|
||||||
? `${t('pages.groups.deleteClientsMixed', { ok, failed })} — ${firstError}`
|
|
||||||
: t('pages.groups.deleteClientsMixed', { ok, failed }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onResetTraffic(g: GroupSummary) {
|
|
||||||
if (!g.clientCount) {
|
|
||||||
messageApi.info(t('pages.groups.emptyForAction'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
modal.confirm({
|
|
||||||
title: t('pages.groups.resetConfirmTitle', { name: g.name }),
|
|
||||||
content: t('pages.groups.resetConfirmContent'),
|
|
||||||
okText: t('reset'),
|
|
||||||
okType: 'danger',
|
|
||||||
cancelText: t('cancel'),
|
|
||||||
onOk: async () => {
|
|
||||||
const msg = await groupResetMut.mutateAsync({ name: g.name });
|
|
||||||
if (msg?.success) {
|
|
||||||
messageApi.success(t('pages.groups.resetSuccess', { name: g.name }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function rowActions(row: GroupSummary): MenuProps['items'] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: 'subLinks',
|
|
||||||
icon: <LinkOutlined />,
|
|
||||||
label: t('pages.clients.subLinksSelected', { count: row.clientCount || 0 }),
|
|
||||||
disabled: !row.clientCount,
|
|
||||||
onClick: () => openSubLinksFor(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'adjust',
|
|
||||||
icon: <ClockCircleOutlined />,
|
|
||||||
label: t('pages.clients.adjustSelected', { count: row.clientCount || 0 }),
|
|
||||||
disabled: !row.clientCount,
|
|
||||||
onClick: () => openAdjustFor(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'reset',
|
|
||||||
icon: <RetweetOutlined />,
|
|
||||||
label: t('pages.groups.resetTraffic'),
|
|
||||||
disabled: !row.clientCount,
|
|
||||||
onClick: () => onResetTraffic(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'addClients',
|
|
||||||
icon: <UsergroupAddOutlined />,
|
|
||||||
label: t('pages.groups.addToGroup'),
|
|
||||||
onClick: () => openAddClientsFor(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'rename',
|
|
||||||
icon: <EditOutlined />,
|
|
||||||
label: t('pages.groups.rename'),
|
|
||||||
onClick: () => openRename(row),
|
|
||||||
},
|
|
||||||
{ type: 'divider' },
|
|
||||||
{
|
|
||||||
key: 'removeClients',
|
|
||||||
icon: <UsergroupDeleteOutlined />,
|
|
||||||
label: t('pages.groups.removeFromGroup'),
|
|
||||||
danger: true,
|
|
||||||
disabled: !row.clientCount,
|
|
||||||
onClick: () => openRemoveClientsFor(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'deleteClients',
|
|
||||||
icon: <DeleteOutlined />,
|
|
||||||
label: t('pages.groups.deleteClients'),
|
|
||||||
danger: true,
|
|
||||||
disabled: !row.clientCount,
|
|
||||||
onClick: () => onDeleteClients(row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
icon: <DeleteOutlined />,
|
|
||||||
label: t('pages.groups.deleteGroupOnly'),
|
|
||||||
danger: true,
|
|
||||||
onClick: () => onDelete(row),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns: TableColumnsType<GroupSummary> = [
|
|
||||||
{
|
|
||||||
title: t('pages.clients.actions'),
|
|
||||||
key: 'actions',
|
|
||||||
width: 90,
|
|
||||||
render: (_v, row) => (
|
|
||||||
<Space size={4}>
|
|
||||||
<Dropdown trigger={['click']} menu={{ items: rowActions(row) }}>
|
|
||||||
<Button
|
|
||||||
aria-label={t('more')}
|
|
||||||
size="small"
|
|
||||||
type="text"
|
|
||||||
style={{ fontSize: 16 }}
|
|
||||||
icon={<MoreOutlined />}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
<Tooltip title={t('pages.groups.rename')}>
|
|
||||||
<Button
|
|
||||||
aria-label={t('pages.groups.rename')}
|
|
||||||
size="small"
|
|
||||||
type="text"
|
|
||||||
style={{ fontSize: 16 }}
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => openRename(row)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('pages.groups.name'),
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
render: (name: string) => (
|
|
||||||
<Tag color="geekblue" style={{ margin: 0, fontSize: 13 }}>
|
|
||||||
{name}
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('pages.groups.clientCount'),
|
|
||||||
dataIndex: 'clientCount',
|
|
||||||
key: 'clientCount',
|
|
||||||
width: 180,
|
|
||||||
render: (count: number) => <span>{count || 0}</span>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('pages.groups.upload'),
|
|
||||||
dataIndex: 'up',
|
|
||||||
key: 'up',
|
|
||||||
width: 140,
|
|
||||||
render: (bytes: number) => <span>{SizeFormatter.sizeFormat(bytes || 0)}</span>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('pages.groups.download'),
|
|
||||||
dataIndex: 'down',
|
|
||||||
key: 'down',
|
|
||||||
width: 140,
|
|
||||||
render: (bytes: number) => <span>{SizeFormatter.sizeFormat(bytes || 0)}</span>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('pages.groups.trafficUsed'),
|
|
||||||
dataIndex: 'trafficUsed',
|
|
||||||
key: 'trafficUsed',
|
|
||||||
width: 160,
|
|
||||||
render: (bytes: number) => <span>{SizeFormatter.sizeFormat(bytes || 0)}</span>,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const pageClass = useMemo(() => {
|
|
||||||
const classes = ['groups-page'];
|
|
||||||
if (isDark) classes.push('is-dark');
|
|
||||||
if (isUltra) classes.push('is-ultra');
|
|
||||||
return classes.join(' ');
|
|
||||||
}, [isDark, isUltra]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigProvider theme={antdThemeConfig}>
|
|
||||||
{messageContextHolder}
|
|
||||||
{modalContextHolder}
|
|
||||||
<Layout className={pageClass}>
|
|
||||||
<AppSidebar />
|
|
||||||
<Layout className="content-shell">
|
|
||||||
<Layout.Content id="content-layout" className="content-area">
|
|
||||||
<Spin spinning={!fetched} delay={200} description={t('loading')} size="large">
|
|
||||||
{!fetched ? (
|
|
||||||
<div className="loading-spacer" />
|
|
||||||
) : fetchError ? (
|
|
||||||
<Result
|
|
||||||
status="error"
|
|
||||||
title={t('somethingWentWrong')}
|
|
||||||
subTitle={fetchError}
|
|
||||||
extra={
|
|
||||||
<Button type="primary" loading={loading} onClick={() => groupsQuery.refetch()}>
|
|
||||||
{t('refresh')}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Row gutter={[isMobile ? 8 : 16, isMobile ? 8 : 12]}>
|
|
||||||
<Col span={24}>
|
|
||||||
<Card size="small" hoverable className="summary-card">
|
|
||||||
<Row gutter={[16, isMobile ? 16 : 12]}>
|
|
||||||
<Col xs={12} sm={12} md={6}>
|
|
||||||
<Statistic
|
|
||||||
title={t('pages.groups.totalGroups')}
|
|
||||||
value={String(totalGroups)}
|
|
||||||
prefix={<TagsOutlined />}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col xs={12} sm={12} md={6}>
|
|
||||||
<Statistic
|
|
||||||
title={t('pages.groups.totalGroupedClients')}
|
|
||||||
value={String(totalClients)}
|
|
||||||
prefix={<TeamOutlined />}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col xs={12} sm={12} md={6}>
|
|
||||||
<Statistic
|
|
||||||
title={t('pages.groups.totalUpDown')}
|
|
||||||
value={0}
|
|
||||||
formatter={() => (
|
|
||||||
<span>
|
|
||||||
<ArrowUpOutlined /> {SizeFormatter.sizeFormat(totalUpload)}
|
|
||||||
{' / '}
|
|
||||||
<ArrowDownOutlined /> {SizeFormatter.sizeFormat(totalDownload)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col xs={12} sm={12} md={6}>
|
|
||||||
<Statistic
|
|
||||||
title={t('pages.groups.totalTraffic')}
|
|
||||||
value={SizeFormatter.sizeFormat(totalTraffic)}
|
|
||||||
prefix={<PieChartOutlined />}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col span={24}>
|
|
||||||
<Card
|
|
||||||
size="small"
|
|
||||||
hoverable
|
|
||||||
title={
|
|
||||||
<div className="card-toolbar">
|
|
||||||
<Button
|
|
||||||
aria-label={t('pages.groups.addGroup')}
|
|
||||||
type="primary"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
onClick={openCreate}
|
|
||||||
>
|
|
||||||
{!isMobile && t('pages.groups.addGroup')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Table<GroupSummary>
|
|
||||||
dataSource={groups}
|
|
||||||
columns={columns}
|
|
||||||
rowKey="name"
|
|
||||||
size="small"
|
|
||||||
pagination={false}
|
|
||||||
loading={loading}
|
|
||||||
locale={{
|
|
||||||
emptyText: (
|
|
||||||
<div className="card-empty">
|
|
||||||
<TagsOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
|
||||||
<div>{t('noData')}</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
</Spin>
|
|
||||||
</Layout.Content>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={createOpen}
|
|
||||||
title={t('pages.groups.addGroup')}
|
|
||||||
okText={t('create')}
|
|
||||||
cancelText={t('cancel')}
|
|
||||||
confirmLoading={createMut.isPending}
|
|
||||||
onCancel={() => setCreateOpen(false)}
|
|
||||||
onOk={confirmCreate}
|
|
||||||
destroyOnHidden
|
|
||||||
>
|
|
||||||
<Form layout="vertical">
|
|
||||||
<Form.Item label={t('pages.groups.name')}>
|
|
||||||
<Input
|
|
||||||
value={createName}
|
|
||||||
onChange={(e) => setCreateName(e.target.value)}
|
|
||||||
onPressEnter={confirmCreate}
|
|
||||||
placeholder={t('pages.clients.groupPlaceholder')}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={renameOpen}
|
|
||||||
title={renameTarget ? t('pages.groups.renameTitle', { name: renameTarget.name }) : ''}
|
|
||||||
okText={t('save')}
|
|
||||||
cancelText={t('cancel')}
|
|
||||||
confirmLoading={renameMut.isPending}
|
|
||||||
onCancel={() => setRenameOpen(false)}
|
|
||||||
onOk={confirmRename}
|
|
||||||
destroyOnHidden
|
|
||||||
>
|
|
||||||
<Form layout="vertical">
|
|
||||||
<Form.Item label={t('pages.groups.name')}>
|
|
||||||
<Input
|
|
||||||
value={renameValue}
|
|
||||||
onChange={(e) => setRenameValue(e.target.value)}
|
|
||||||
onPressEnter={confirmRename}
|
|
||||||
placeholder={t('pages.clients.groupPlaceholder')}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<LazyMount when={subLinksOpen}>
|
|
||||||
<SubLinksModal
|
|
||||||
open={subLinksOpen}
|
|
||||||
emails={groupEmails}
|
|
||||||
clients={allClients}
|
|
||||||
subSettings={subSettings}
|
|
||||||
onOpenChange={setSubLinksOpen}
|
|
||||||
/>
|
|
||||||
</LazyMount>
|
|
||||||
|
|
||||||
<LazyMount when={adjustOpen}>
|
|
||||||
<ClientBulkAdjustModal
|
|
||||||
open={adjustOpen}
|
|
||||||
count={groupEmails.length}
|
|
||||||
onOpenChange={setAdjustOpen}
|
|
||||||
onSubmit={async (addDays, addBytes) => {
|
|
||||||
const msg = await bulkAdjust(groupEmails, addDays, addBytes);
|
|
||||||
if (msg?.success) {
|
|
||||||
const obj = msg.obj ?? { adjusted: 0 };
|
|
||||||
messageApi.success(
|
|
||||||
t('pages.groups.adjustSuccess', {
|
|
||||||
count: obj.adjusted ?? 0,
|
|
||||||
name: groupForAction?.name ?? '',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</LazyMount>
|
|
||||||
|
|
||||||
<LazyMount when={addClientsOpen}>
|
|
||||||
<GroupAddClientsModal
|
|
||||||
open={addClientsOpen}
|
|
||||||
groupName={groupForAction?.name ?? null}
|
|
||||||
candidates={allClients.filter((c) => c.group !== groupForAction?.name)}
|
|
||||||
onClose={() => setAddClientsOpen(false)}
|
|
||||||
onSubmit={async (emails) => {
|
|
||||||
const msg = await bulkAddToGroup(emails, groupForAction?.name ?? '');
|
|
||||||
if (msg?.success) {
|
|
||||||
return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</LazyMount>
|
|
||||||
|
|
||||||
<LazyMount when={removeClientsOpen}>
|
|
||||||
<GroupRemoveClientsModal
|
|
||||||
open={removeClientsOpen}
|
|
||||||
groupName={groupForAction?.name ?? null}
|
|
||||||
members={allClients.filter((c) => c.group === groupForAction?.name)}
|
|
||||||
onClose={() => setRemoveClientsOpen(false)}
|
|
||||||
onSubmit={async (emails) => {
|
|
||||||
const msg = await bulkRemoveFromGroup(emails);
|
|
||||||
if (msg?.success) {
|
|
||||||
return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</LazyMount>
|
|
||||||
</Layout>
|
|
||||||
</ConfigProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -53,7 +53,6 @@ const QrCodeModal = lazy(() => import('./qr/QrCodeModal'));
|
|||||||
const AttachClientsModal = lazy(() => import('./clients/AttachClientsModal'));
|
const AttachClientsModal = lazy(() => import('./clients/AttachClientsModal'));
|
||||||
const AttachExistingClientsModal = lazy(() => import('./clients/AttachExistingClientsModal'));
|
const AttachExistingClientsModal = lazy(() => import('./clients/AttachExistingClientsModal'));
|
||||||
const DetachClientsModal = lazy(() => import('./clients/DetachClientsModal'));
|
const DetachClientsModal = lazy(() => import('./clients/DetachClientsModal'));
|
||||||
const AddClientsToGroupModal = lazy(() => import('./clients/AddClientsToGroupModal'));
|
|
||||||
|
|
||||||
type RowAction =
|
type RowAction =
|
||||||
| 'edit'
|
| 'edit'
|
||||||
@@ -68,7 +67,6 @@ type RowAction =
|
|||||||
| 'attachClients'
|
| 'attachClients'
|
||||||
| 'attachExisting'
|
| 'attachExisting'
|
||||||
| 'detachClients'
|
| 'detachClients'
|
||||||
| 'addToGroup'
|
|
||||||
| 'clone';
|
| 'clone';
|
||||||
|
|
||||||
type GeneralAction = 'import' | 'export' | 'subs' | 'resetInbounds';
|
type GeneralAction = 'import' | 'export' | 'subs' | 'resetInbounds';
|
||||||
@@ -165,9 +163,6 @@ export default function InboundsPage() {
|
|||||||
const [detachOpen, setDetachOpen] = useState(false);
|
const [detachOpen, setDetachOpen] = useState(false);
|
||||||
const [detachSource, setDetachSource] = useState<DBInbound | null>(null);
|
const [detachSource, setDetachSource] = useState<DBInbound | null>(null);
|
||||||
|
|
||||||
const [groupOpen, setGroupOpen] = useState(false);
|
|
||||||
const [groupSource, setGroupSource] = useState<DBInbound | null>(null);
|
|
||||||
|
|
||||||
const [cloneOpen, setCloneOpen] = useState(false);
|
const [cloneOpen, setCloneOpen] = useState(false);
|
||||||
const [cloneSource, setCloneSource] = useState<DBInbound | null>(null);
|
const [cloneSource, setCloneSource] = useState<DBInbound | null>(null);
|
||||||
|
|
||||||
@@ -622,7 +617,6 @@ export default function InboundsPage() {
|
|||||||
'clipboard',
|
'clipboard',
|
||||||
'clone',
|
'clone',
|
||||||
'attachClients',
|
'attachClients',
|
||||||
'addToGroup',
|
|
||||||
];
|
];
|
||||||
let target = dbInbound;
|
let target = dbInbound;
|
||||||
if (hydratingKeys.includes(key)) {
|
if (hydratingKeys.includes(key)) {
|
||||||
@@ -672,10 +666,6 @@ export default function InboundsPage() {
|
|||||||
setDetachSource(target);
|
setDetachSource(target);
|
||||||
setDetachOpen(true);
|
setDetachOpen(true);
|
||||||
break;
|
break;
|
||||||
case 'addToGroup':
|
|
||||||
setGroupSource(target);
|
|
||||||
setGroupOpen(true);
|
|
||||||
break;
|
|
||||||
case 'clone':
|
case 'clone':
|
||||||
confirmClone(target);
|
confirmClone(target);
|
||||||
break;
|
break;
|
||||||
@@ -848,14 +838,6 @@ export default function InboundsPage() {
|
|||||||
source={detachSource}
|
source={detachSource}
|
||||||
/>
|
/>
|
||||||
</LazyMount>
|
</LazyMount>
|
||||||
<LazyMount when={groupOpen}>
|
|
||||||
<AddClientsToGroupModal
|
|
||||||
open={groupOpen}
|
|
||||||
onClose={() => setGroupOpen(false)}
|
|
||||||
onAdded={refresh}
|
|
||||||
source={groupSource}
|
|
||||||
/>
|
|
||||||
</LazyMount>
|
|
||||||
<LazyMount when={cloneOpen}>
|
<LazyMount when={cloneOpen}>
|
||||||
<CloneInboundModal
|
<CloneInboundModal
|
||||||
open={cloneOpen}
|
open={cloneOpen}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import { lazy, useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { HttpUtil } from '@/utils';
|
|
||||||
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
|
|
||||||
|
|
||||||
const BulkAddToGroupModal = lazy(() => import('@/pages/clients/BulkAddToGroupModal'));
|
|
||||||
|
|
||||||
interface AddClientsToGroupModalProps {
|
|
||||||
open: boolean;
|
|
||||||
source: DBInbound | null;
|
|
||||||
onClose: () => void;
|
|
||||||
onAdded?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readClientEmails(settings: unknown): string[] {
|
|
||||||
const parsed = coerceInboundJsonField(settings) as { clients?: Array<{ email?: string }> };
|
|
||||||
const clients = Array.isArray(parsed?.clients) ? parsed.clients : [];
|
|
||||||
return clients.map((c) => (c?.email || '').trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AddClientsToGroupModal({
|
|
||||||
open,
|
|
||||||
source,
|
|
||||||
onClose,
|
|
||||||
onAdded,
|
|
||||||
}: AddClientsToGroupModalProps) {
|
|
||||||
const [groups, setGroups] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const emails = useMemo(() => (source ? readClientEmails(source.settings) : []), [source]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
let cancelled = false;
|
|
||||||
(async () => {
|
|
||||||
const msg = await HttpUtil.get('/panel/api/clients/groups', undefined, { silent: true });
|
|
||||||
if (cancelled) return;
|
|
||||||
const list = Array.isArray(msg?.obj) ? (msg.obj as Array<{ name?: string }>) : [];
|
|
||||||
setGroups(list.map((g) => g?.name || '').filter(Boolean));
|
|
||||||
})();
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BulkAddToGroupModal
|
|
||||||
open={open}
|
|
||||||
count={emails.length}
|
|
||||||
groups={groups}
|
|
||||||
onOpenChange={(o) => {
|
|
||||||
if (!o) onClose();
|
|
||||||
}}
|
|
||||||
onSubmit={async (group) => {
|
|
||||||
const msg = await HttpUtil.post(
|
|
||||||
'/panel/api/clients/groups/bulkAdd',
|
|
||||||
{ emails, group },
|
|
||||||
{ headers: { 'Content-Type': 'application/json' } },
|
|
||||||
);
|
|
||||||
if (!msg?.success) return null;
|
|
||||||
onAdded?.();
|
|
||||||
return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
BlockOutlined,
|
BlockOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
InfoCircleOutlined,
|
InfoCircleOutlined,
|
||||||
TagsOutlined,
|
|
||||||
UsergroupAddOutlined,
|
UsergroupAddOutlined,
|
||||||
UsergroupDeleteOutlined,
|
UsergroupDeleteOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
@@ -91,11 +90,6 @@ export function buildRowActionsMenu({
|
|||||||
icon: <UsergroupDeleteOutlined />,
|
icon: <UsergroupDeleteOutlined />,
|
||||||
label: t('pages.inbounds.detachClients'),
|
label: t('pages.inbounds.detachClients'),
|
||||||
});
|
});
|
||||||
items.push({
|
|
||||||
key: 'addToGroup',
|
|
||||||
icon: <TagsOutlined />,
|
|
||||||
label: t('pages.inbounds.addClientsToGroup'),
|
|
||||||
});
|
|
||||||
items.push({ type: 'divider' });
|
items.push({ type: 'divider' });
|
||||||
items.push({
|
items.push({
|
||||||
key: 'delAllClients',
|
key: 'delAllClients',
|
||||||
|
|||||||
@@ -38,53 +38,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ov-state {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border: 1px solid var(--ov-line);
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ov-state-dot {
|
|
||||||
position: relative;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: currentColor;
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ov-state[data-state='running'] .ov-state-dot::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: -1px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid currentColor;
|
|
||||||
animation: ovPulse 1.6s infinite ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes ovPulse {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.9);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(2.4);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.ov-state[data-state='running'] .ov-state-dot::after {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ov-state-version,
|
|
||||||
.ov-panel-version {
|
.ov-panel-version {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -95,8 +48,6 @@
|
|||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ov-state-version:hover,
|
|
||||||
.ov-state-version:focus-visible,
|
|
||||||
.ov-panel-version:hover,
|
.ov-panel-version:hover,
|
||||||
.ov-panel-version:focus-visible {
|
.ov-panel-version:focus-visible {
|
||||||
color: var(--ant-color-primary);
|
color: var(--ant-color-primary);
|
||||||
@@ -112,11 +63,6 @@
|
|||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ov-error-detail {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ov-bar-actions {
|
.ov-bar-actions {
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -99,16 +99,6 @@ export default function IndexPage() {
|
|||||||
if (tip) setLoadingTip(tip);
|
if (tip) setLoadingTip(tip);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const stopXray = useCallback(async () => {
|
|
||||||
await HttpUtil.post('/panel/api/server/stopXrayService');
|
|
||||||
await refresh();
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
const restartXray = useCallback(async () => {
|
|
||||||
await HttpUtil.post('/panel/api/server/restartXrayService');
|
|
||||||
await refresh();
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
async function handleChannelChange(dev: boolean) {
|
async function handleChannelChange(dev: boolean) {
|
||||||
const res = await HttpUtil.post('/panel/api/server/setUpdateChannel', { dev });
|
const res = await HttpUtil.post('/panel/api/server/setUpdateChannel', { dev });
|
||||||
if (!res?.success) return;
|
if (!res?.success) return;
|
||||||
@@ -198,8 +188,6 @@ export default function IndexPage() {
|
|||||||
panelVersion={displayVersion}
|
panelVersion={displayVersion}
|
||||||
latestVersion={panelUpdateInfo.latestVersion}
|
latestVersion={panelUpdateInfo.latestVersion}
|
||||||
updateAvailable={panelUpdateInfo.updateAvailable}
|
updateAvailable={panelUpdateInfo.updateAvailable}
|
||||||
onStopXray={stopXray}
|
|
||||||
onRestartXray={restartXray}
|
|
||||||
onOpenLogs={() => setLogsOpen(true)}
|
onOpenLogs={() => setLogsOpen(true)}
|
||||||
onOpenXrayLogs={() => setXrayLogsOpen(true)}
|
onOpenXrayLogs={() => setXrayLogsOpen(true)}
|
||||||
onOpenAmneziaWGLogs={() => setAmneziawgLogsOpen(true)}
|
onOpenAmneziaWGLogs={() => setAmneziawgLogsOpen(true)}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import {
|
|||||||
CloudServerOutlined,
|
CloudServerOutlined,
|
||||||
ControlOutlined,
|
ControlOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
PoweroffOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
import { formatPanelVersion } from '@/lib/panel-version';
|
import { formatPanelVersion } from '@/lib/panel-version';
|
||||||
@@ -25,8 +23,6 @@ interface OverviewActionBarProps {
|
|||||||
panelVersion: string;
|
panelVersion: string;
|
||||||
latestVersion: string;
|
latestVersion: string;
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
onStopXray: () => void;
|
|
||||||
onRestartXray: () => void;
|
|
||||||
onOpenLogs: () => void;
|
onOpenLogs: () => void;
|
||||||
onOpenXrayLogs: () => void;
|
onOpenXrayLogs: () => void;
|
||||||
onOpenAmneziaWGLogs: () => void;
|
onOpenAmneziaWGLogs: () => void;
|
||||||
@@ -45,12 +41,6 @@ interface BarAction {
|
|||||||
primary?: boolean;
|
primary?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const XRAY_STATE_KEYS: Record<string, string> = {
|
|
||||||
running: 'pages.index.xrayStatusRunning',
|
|
||||||
stop: 'pages.index.xrayStatusStop',
|
|
||||||
error: 'pages.index.xrayStatusError',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function OverviewActionBar({
|
export default function OverviewActionBar({
|
||||||
status,
|
status,
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -58,8 +48,6 @@ export default function OverviewActionBar({
|
|||||||
panelVersion,
|
panelVersion,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
updateAvailable,
|
updateAvailable,
|
||||||
onStopXray,
|
|
||||||
onRestartXray,
|
|
||||||
onOpenLogs,
|
onOpenLogs,
|
||||||
onOpenXrayLogs,
|
onOpenXrayLogs,
|
||||||
onOpenAmneziaWGLogs,
|
onOpenAmneziaWGLogs,
|
||||||
@@ -70,26 +58,9 @@ export default function OverviewActionBar({
|
|||||||
onOpenPanelUpdate,
|
onOpenPanelUpdate,
|
||||||
}: OverviewActionBarProps) {
|
}: OverviewActionBarProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const stateText = t(XRAY_STATE_KEYS[status.xray.state] ?? 'pages.index.xrayStatusUnknown');
|
|
||||||
const hasVersion = !!status.xray.version && status.xray.version !== 'Unknown';
|
|
||||||
const size = isMobile ? ('small' as const) : ('middle' as const);
|
const size = isMobile ? ('small' as const) : ('middle' as const);
|
||||||
|
|
||||||
const actionGroups: BarAction[][] = [
|
const actionGroups: BarAction[][] = [
|
||||||
[
|
|
||||||
{
|
|
||||||
key: 'restart',
|
|
||||||
icon: <ReloadOutlined />,
|
|
||||||
text: t('pages.index.restartXray'),
|
|
||||||
onClick: onRestartXray,
|
|
||||||
primary: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'stop',
|
|
||||||
icon: <PoweroffOutlined />,
|
|
||||||
text: t('pages.index.stopXray'),
|
|
||||||
onClick: onStopXray,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
{ key: 'logs', icon: <BarsOutlined />, text: t('pages.index.logs'), onClick: onOpenLogs },
|
{ key: 'logs', icon: <BarsOutlined />, text: t('pages.index.logs'), onClick: onOpenLogs },
|
||||||
...(accessLogEnable
|
...(accessLogEnable
|
||||||
@@ -141,24 +112,8 @@ export default function OverviewActionBar({
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
const statePill = (
|
|
||||||
<span className="ov-state" data-state={status.xray.state}>
|
|
||||||
<span className="ov-state-dot" style={{ color: status.xray.color }} />
|
|
||||||
<span>{`${t('pages.index.xrayStatus')} · ${stateText}`}</span>
|
|
||||||
{hasVersion && <span className="ov-state-version">{`v${status.xray.version}`}</span>}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ov-bar">
|
<div className="ov-bar">
|
||||||
{status.xray.state === 'error' && status.xray.errorMsg ? (
|
|
||||||
<Tooltip title={<span className="ov-error-detail">{status.xray.errorMsg}</span>}>
|
|
||||||
{statePill}
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
statePill
|
|
||||||
)}
|
|
||||||
|
|
||||||
{updateAvailable ? (
|
{updateAvailable ? (
|
||||||
<Tag
|
<Tag
|
||||||
className="ov-update-tag"
|
className="ov-update-tag"
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import PanelLayout from '@/layouts/PanelLayout';
|
|||||||
const IndexPage = lazy(() => import('@/pages/index/IndexPage'));
|
const IndexPage = lazy(() => import('@/pages/index/IndexPage'));
|
||||||
const InboundsPage = lazy(() => import('@/pages/inbounds/InboundsPage'));
|
const InboundsPage = lazy(() => import('@/pages/inbounds/InboundsPage'));
|
||||||
const ClientsPage = lazy(() => import('@/pages/clients/ClientsPage'));
|
const ClientsPage = lazy(() => import('@/pages/clients/ClientsPage'));
|
||||||
const GroupsPage = lazy(() => import('@/pages/groups/GroupsPage'));
|
|
||||||
const NodesPage = lazy(() => import('@/pages/nodes/NodesPage'));
|
const NodesPage = lazy(() => import('@/pages/nodes/NodesPage'));
|
||||||
const HostsPage = lazy(() => import('@/pages/hosts/HostsPage'));
|
const HostsPage = lazy(() => import('@/pages/hosts/HostsPage'));
|
||||||
const SettingsPage = lazy(() => import('@/pages/settings/SettingsPage'));
|
const SettingsPage = lazy(() => import('@/pages/settings/SettingsPage'));
|
||||||
const XrayPage = lazy(() => import('@/pages/xray/XrayPage'));
|
const XrayPage = lazy(() => import('@/pages/xray/XrayPage'));
|
||||||
const ApiDocsPage = lazy(() => import('@/pages/api-docs/ApiDocsPage'));
|
|
||||||
|
|
||||||
function withSuspense(node: React.ReactNode) {
|
function withSuspense(node: React.ReactNode) {
|
||||||
return (
|
return (
|
||||||
@@ -43,14 +41,12 @@ const routes: RouteObject[] = [
|
|||||||
{ index: true, element: withSuspense(<IndexPage />) },
|
{ index: true, element: withSuspense(<IndexPage />) },
|
||||||
{ path: 'inbounds', element: withSuspense(<InboundsPage />) },
|
{ path: 'inbounds', element: withSuspense(<InboundsPage />) },
|
||||||
{ path: 'clients', element: withSuspense(<ClientsPage />) },
|
{ path: 'clients', element: withSuspense(<ClientsPage />) },
|
||||||
{ path: 'groups', element: withSuspense(<GroupsPage />) },
|
|
||||||
{ path: 'nodes', element: withSuspense(<NodesPage />) },
|
{ path: 'nodes', element: withSuspense(<NodesPage />) },
|
||||||
{ path: 'hosts', element: withSuspense(<HostsPage />) },
|
{ path: 'hosts', element: withSuspense(<HostsPage />) },
|
||||||
{ path: 'settings', element: withSuspense(<SettingsPage />) },
|
{ path: 'settings', element: withSuspense(<SettingsPage />) },
|
||||||
{ path: 'xray', element: withSuspense(<XrayPage />) },
|
{ path: 'xray', element: withSuspense(<XrayPage />) },
|
||||||
{ path: 'outbound', element: withSuspense(<XrayPage />) },
|
{ path: 'outbound', element: withSuspense(<XrayPage />) },
|
||||||
{ path: 'routing', element: withSuspense(<XrayPage />) },
|
{ path: 'routing', element: withSuspense(<XrayPage />) },
|
||||||
{ path: 'api-docs', element: withSuspense(<ApiDocsPage />) },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -259,28 +259,6 @@ export const ActiveInboundsByNodeSchema = z
|
|||||||
.nullable()
|
.nullable()
|
||||||
.transform((v) => v ?? {});
|
.transform((v) => v ?? {});
|
||||||
|
|
||||||
export const GroupSummarySchema = z.object({
|
|
||||||
name: z.string(),
|
|
||||||
clientCount: z.number(),
|
|
||||||
trafficUsed: z
|
|
||||||
.number()
|
|
||||||
.nullable()
|
|
||||||
.transform((v) => v ?? 0),
|
|
||||||
up: z
|
|
||||||
.number()
|
|
||||||
.nullable()
|
|
||||||
.transform((v) => v ?? 0),
|
|
||||||
down: z
|
|
||||||
.number()
|
|
||||||
.nullable()
|
|
||||||
.transform((v) => v ?? 0),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GroupSummaryListSchema = z
|
|
||||||
.array(GroupSummarySchema)
|
|
||||||
.nullable()
|
|
||||||
.transform((v) => v ?? []);
|
|
||||||
|
|
||||||
export function hasForbiddenClientChars(value: string): boolean {
|
export function hasForbiddenClientChars(value: string): boolean {
|
||||||
if (value.includes('/') || value.includes('\\') || value.includes(' ')) return true;
|
if (value.includes('/') || value.includes('\\') || value.includes(' ')) return true;
|
||||||
for (let i = 0; i < value.length; i++) {
|
for (let i = 0; i < value.length; i++) {
|
||||||
@@ -373,4 +351,3 @@ export type BulkDetachResult = z.infer<typeof BulkDetachResultSchema>;
|
|||||||
export type ClientBulkAddFormValues = z.infer<typeof ClientBulkAddFormSchema>;
|
export type ClientBulkAddFormValues = z.infer<typeof ClientBulkAddFormSchema>;
|
||||||
export type ClientBulkAdjustFormValues = z.infer<typeof ClientBulkAdjustFormSchema>;
|
export type ClientBulkAdjustFormValues = z.infer<typeof ClientBulkAdjustFormSchema>;
|
||||||
export type ClientFormValues = z.infer<typeof ClientFormSchema>;
|
export type ClientFormValues = z.infer<typeof ClientFormSchema>;
|
||||||
export type GroupSummary = z.infer<typeof GroupSummarySchema>;
|
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "نظرة عامة",
|
"dashboard": "نظرة عامة",
|
||||||
"inbounds": "الواردات",
|
"inbounds": "الواردات",
|
||||||
"clients": "العملاء",
|
"clients": "العملاء",
|
||||||
"groups": "المجموعات",
|
|
||||||
"nodes": "النودز",
|
"nodes": "النودز",
|
||||||
"settings": "إعدادات اللوحة",
|
"settings": "إعدادات اللوحة",
|
||||||
"xray": "إعدادات Xray",
|
"xray": "إعدادات Xray",
|
||||||
"routing": "التوجيه",
|
"routing": "التوجيه",
|
||||||
"outbounds": "الصادرات",
|
"outbounds": "الصادرات",
|
||||||
"apiDocs": "توثيق API",
|
|
||||||
"donate": "تبرع",
|
"donate": "تبرع",
|
||||||
"hosts": "المضيفات",
|
"hosts": "المضيفات",
|
||||||
"docs": "التوثيق",
|
"docs": "التوثيق",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "التبديل",
|
"swap": "التبديل",
|
||||||
"storage": "تخزين",
|
"storage": "تخزين",
|
||||||
"memory": "الذاكرة",
|
"memory": "الذاكرة",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "إيقاف",
|
|
||||||
"restartXray": "إعادة تشغيل",
|
|
||||||
"updatePanel": "تحديث البانل",
|
"updatePanel": "تحديث البانل",
|
||||||
"panelUpdateDesc": "ده هيحدث 3X-UI لآخر إصدار وهيعيد تشغيل خدمة البانل.",
|
"panelUpdateDesc": "ده هيحدث 3X-UI لآخر إصدار وهيعيد تشغيل خدمة البانل.",
|
||||||
"currentPanelVersion": "إصدار البانل الحالي",
|
"currentPanelVersion": "إصدار البانل الحالي",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "الكومِت الحالي",
|
"currentCommit": "الكومِت الحالي",
|
||||||
"latestCommit": "أحدث كومِت",
|
"latestCommit": "أحدث كومِت",
|
||||||
"updateChannelChanged": "تم تغيير قناة التحديث",
|
"updateChannelChanged": "تم تغيير قناة التحديث",
|
||||||
"xrayStatusUnknown": "مش معروف",
|
|
||||||
"xrayStatusRunning": "شغالة",
|
|
||||||
"xrayStatusStop": "متوقفة",
|
|
||||||
"xrayStatusError": "خطأ",
|
|
||||||
"systemHistoryTitle": "تاريخ النظام",
|
"systemHistoryTitle": "تاريخ النظام",
|
||||||
"historyTitleCpu": "استخدام المعالج",
|
"historyTitleCpu": "استخدام المعالج",
|
||||||
"historyTitleMem": "استخدام الذاكرة",
|
"historyTitleMem": "استخدام الذاكرة",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "حذف جميع العملاء البالغ عددهم {count} من \"{remark}\"؟",
|
"delAllClientsConfirmTitle": "حذف جميع العملاء البالغ عددهم {count} من \"{remark}\"؟",
|
||||||
"delAllClientsConfirmContent": "يزيل كل عميل من هذا الإدخال ويحذف سجلات حركة المرور الخاصة بهم. يتم الاحتفاظ بالإدخال نفسه. لا يمكن التراجع عن هذا.",
|
"delAllClientsConfirmContent": "يزيل كل عميل من هذا الإدخال ويحذف سجلات حركة المرور الخاصة بهم. يتم الاحتفاظ بالإدخال نفسه. لا يمكن التراجع عن هذا.",
|
||||||
"attachClients": "إرفاق عملاء بـ…",
|
"attachClients": "إرفاق عملاء بـ…",
|
||||||
"addClientsToGroup": "إضافة عملاء إلى مجموعة…",
|
|
||||||
"attachClientsTitle": "إرفاق عملاء من «{remark}»",
|
"attachClientsTitle": "إرفاق عملاء من «{remark}»",
|
||||||
"attachClientsDesc": "يربط نفس {count} عميل (UUID/كلمة المرور وحركة المرور المشتركة) بالواردات المحددة. يبقون في هذا الوارد أيضاً.",
|
"attachClientsDesc": "يربط نفس {count} عميل (UUID/كلمة المرور وحركة المرور المشتركة) بالواردات المحددة. يبقون في هذا الوارد أيضاً.",
|
||||||
"attachClientsTargets": "الواردات الهدف",
|
"attachClientsTargets": "الواردات الهدف",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "يُعاد ضبط عدّاد الإرسال/الاستقبال لكل عميل إلى الصفر. لا تتأثر الحصص ومواعيد الانتهاء. لا يمكن التراجع.",
|
"resetAllTrafficsContent": "يُعاد ضبط عدّاد الإرسال/الاستقبال لكل عميل إلى الصفر. لا تتأثر الحصص ومواعيد الانتهاء. لا يمكن التراجع.",
|
||||||
"deleteConfirmTitle": "حذف العميل {email}؟",
|
"deleteConfirmTitle": "حذف العميل {email}؟",
|
||||||
"deleteConfirmContent": "سيؤدي هذا إلى إزالة العميل من جميع الاتصالات الواردة المرتبطة وحذف سجل حركة مروره. لا يمكن التراجع.",
|
"deleteConfirmContent": "سيؤدي هذا إلى إزالة العميل من جميع الاتصالات الواردة المرتبطة وحذف سجل حركة مروره. لا يمكن التراجع.",
|
||||||
"adjustSelected": "تعديل ({count})",
|
|
||||||
"subLinksSelected": "روابط الاشتراك ({count})",
|
|
||||||
"addToGroupTitle": "إضافة {count} عميل إلى مجموعة",
|
|
||||||
"addToGroupTooltip": "اختر مجموعة موجودة أو أدخل اسماً جديداً. استخدم Ungroup لإزالة العملاء من مجموعتهم الحالية.",
|
|
||||||
"groupName": "اسم المجموعة",
|
|
||||||
"addToGroupSuccessToast": "تمت إضافة {count} عميل إلى {group}",
|
|
||||||
"ungroupSuccessToast": "تم مسح المجموعة من {count} عميل",
|
|
||||||
"ungroup": "إزالة من المجموعة",
|
|
||||||
"ungroupConfirmTitle": "إزالة {count} عميل من مجموعتهم؟",
|
|
||||||
"ungroupConfirmContent": "يمسح تسمية المجموعة من كل عميل محدد. يُحفظ العملاء (استخدم Delete للإزالة الكاملة).",
|
|
||||||
"addToGroup": "إضافة إلى مجموعة",
|
|
||||||
"attach": "إرفاق",
|
"attach": "إرفاق",
|
||||||
"adjust": "ضبط",
|
"adjust": "ضبط",
|
||||||
"subLinks": "روابط الاشتراك",
|
"subLinks": "روابط الاشتراك",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "يتم التجديد في هذا اليوم من كل شهر ميلادي، عند منتصف الليل بتوقيت اللوحة، بدلاً من كل N يوم. إذا كان الشهر أقصر من اليوم المختار، يتم التجديد في آخر يوم منه. القيمة 0 تُبقي وضع الفاصل اليومي.",
|
"renewOnDayDesc": "يتم التجديد في هذا اليوم من كل شهر ميلادي، عند منتصف الليل بتوقيت اللوحة، بدلاً من كل N يوم. إذا كان الشهر أقصر من اليوم المختار، يتم التجديد في آخر يوم منه. القيمة 0 تُبقي وضع الفاصل اليومي.",
|
||||||
"renewsUsed": "التجديدات المستخدمة"
|
"renewsUsed": "التجديدات المستخدمة"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "الاسم",
|
|
||||||
"clientCount": "العملاء",
|
|
||||||
"totalGroups": "إجمالي المجموعات",
|
|
||||||
"totalGroupedClients": "العملاء بمجموعة",
|
|
||||||
"trafficUsed": "حركة المرور المستخدمة",
|
|
||||||
"upload": "رفع",
|
|
||||||
"download": "تنزيل",
|
|
||||||
"totalTraffic": "إجمالي حركة المرور",
|
|
||||||
"totalUpDown": "إجمالي الرفع / التنزيل",
|
|
||||||
"addGroup": "إضافة مجموعة",
|
|
||||||
"createSuccess": "تم إنشاء المجموعة «{name}».",
|
|
||||||
"rename": "إعادة تسمية",
|
|
||||||
"renameTitle": "إعادة تسمية {name}",
|
|
||||||
"renameCollision": "مجموعة باسم «{name}» موجودة بالفعل.",
|
|
||||||
"renameSuccess": "تمت إعادة تسمية المجموعة على {count} عميل.",
|
|
||||||
"deleteConfirmTitle": "حذف المجموعة {name}؟",
|
|
||||||
"deleteConfirmContent": "يحذف المجموعة ويمسح تسميتها من {count} عميل. العملاء أنفسهم لا يُحذفون.",
|
|
||||||
"deleteSuccess": "تم مسح المجموعة من {count} عميل.",
|
|
||||||
"resetTraffic": "إعادة تعيين حركة المرور",
|
|
||||||
"resetConfirmTitle": "إعادة تعيين حركة المرور للمجموعة {name}؟",
|
|
||||||
"resetConfirmContent": "يعيد تعيين عداد حركة مرور المجموعة فقط؛ ولا تتأثر عدادات العملاء الفرديين.",
|
|
||||||
"resetSuccess": "تمت إعادة تعيين حركة مرور المجموعة {name}.",
|
|
||||||
"adjustSuccess": "تم ضبط {count} عميل في {name}.",
|
|
||||||
"emptyForAction": "هذه المجموعة فارغة.",
|
|
||||||
"deleteGroupOnly": "حذف المجموعة (مع الاحتفاظ بالعملاء)",
|
|
||||||
"deleteClients": "حذف عملاء المجموعة",
|
|
||||||
"deleteClientsConfirmTitle": "حذف جميع العملاء في {name}؟",
|
|
||||||
"deleteClientsConfirmContent": "يحذف {count} عميل نهائياً مع سجلات حركة المرور. تُمسح تسمية المجموعة أيضاً. لا يمكن التراجع.",
|
|
||||||
"deleteClientsSuccess": "تم حذف {count} عميل.",
|
|
||||||
"deleteClientsMixed": "{ok} حُذف، {failed} تم تخطيه",
|
|
||||||
"addToGroup": "إضافة عملاء…",
|
|
||||||
"addToGroupTitle": "إضافة عملاء إلى المجموعة «{name}»",
|
|
||||||
"addToGroupDesc": "اختر العملاء لإضافتهم إلى هذه المجموعة. يحتفظون بارتباطات الواردات الحالية؛ تتغير تسمية المجموعة فقط. لا تُعرض العملاء الذين هم في هذه المجموعة بالفعل.",
|
|
||||||
"addToGroupEmpty": "لا يوجد عملاء آخرون للإضافة.",
|
|
||||||
"addToGroupResult": "تمت إضافة {count} عميل إلى {name}.",
|
|
||||||
"removeFromGroup": "إزالة عملاء…",
|
|
||||||
"removeFromGroupTitle": "إزالة عملاء من المجموعة «{name}»",
|
|
||||||
"removeFromGroupDesc": "اختر الأعضاء لإزالتهم من هذه المجموعة. يُحفظ العملاء (استخدم «حذف عملاء المجموعة» للإزالة الكاملة).",
|
|
||||||
"removeFromGroupResult": "تمت إزالة {count} عميل من {name}."
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "إضافة نود",
|
"addNode": "إضافة نود",
|
||||||
"editNode": "تحرير العقدة",
|
"editNode": "تحرير العقدة",
|
||||||
|
|||||||
@@ -95,14 +95,12 @@
|
|||||||
"dashboard": "Overview",
|
"dashboard": "Overview",
|
||||||
"inbounds": "Inbounds",
|
"inbounds": "Inbounds",
|
||||||
"clients": "Clients",
|
"clients": "Clients",
|
||||||
"groups": "Groups",
|
|
||||||
"nodes": "Nodes",
|
"nodes": "Nodes",
|
||||||
"hosts": "Hosts",
|
"hosts": "Hosts",
|
||||||
"settings": "Panel Settings",
|
"settings": "Panel Settings",
|
||||||
"xray": "Xray Configs",
|
"xray": "Xray Configs",
|
||||||
"routing": "Routing",
|
"routing": "Routing",
|
||||||
"outbounds": "Outbounds",
|
"outbounds": "Outbounds",
|
||||||
"apiDocs": "API Docs",
|
|
||||||
"donate": "Donate",
|
"donate": "Donate",
|
||||||
"docs": "Documentation",
|
"docs": "Documentation",
|
||||||
"openMenu": "Open menu",
|
"openMenu": "Open menu",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "Swap",
|
"swap": "Swap",
|
||||||
"storage": "Storage",
|
"storage": "Storage",
|
||||||
"memory": "RAM",
|
"memory": "RAM",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "Stop",
|
|
||||||
"restartXray": "Restart",
|
|
||||||
"updatePanel": "Update Panel",
|
"updatePanel": "Update Panel",
|
||||||
"panelUpdateDesc": "This will update 3X-UI itself to the latest release and restart the panel service.",
|
"panelUpdateDesc": "This will update 3X-UI itself to the latest release and restart the panel service.",
|
||||||
"currentPanelVersion": "Current panel version",
|
"currentPanelVersion": "Current panel version",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "Current commit",
|
"currentCommit": "Current commit",
|
||||||
"latestCommit": "Latest commit",
|
"latestCommit": "Latest commit",
|
||||||
"updateChannelChanged": "Update channel changed",
|
"updateChannelChanged": "Update channel changed",
|
||||||
"xrayStatusUnknown": "Unknown",
|
|
||||||
"xrayStatusRunning": "Running",
|
|
||||||
"xrayStatusStop": "Stopped",
|
|
||||||
"xrayStatusError": "Error",
|
|
||||||
"systemHistoryTitle": "System History",
|
"systemHistoryTitle": "System History",
|
||||||
"historyTitleCpu": "CPU Usage",
|
"historyTitleCpu": "CPU Usage",
|
||||||
"historyTitleMem": "Memory Usage",
|
"historyTitleMem": "Memory Usage",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "Delete all {count} clients from \"{remark}\"?",
|
"delAllClientsConfirmTitle": "Delete all {count} clients from \"{remark}\"?",
|
||||||
"delAllClientsConfirmContent": "This removes every client from this inbound and drops their traffic records. The inbound itself is kept. This cannot be undone.",
|
"delAllClientsConfirmContent": "This removes every client from this inbound and drops their traffic records. The inbound itself is kept. This cannot be undone.",
|
||||||
"attachClients": "Attach Clients To…",
|
"attachClients": "Attach Clients To…",
|
||||||
"addClientsToGroup": "Add Clients To Group…",
|
|
||||||
"attachClientsTitle": "Attach clients from \"{remark}\"",
|
"attachClientsTitle": "Attach clients from \"{remark}\"",
|
||||||
"attachClientsDesc": "Attaches the same {count} clients (same UUID/password and shared traffic) to the selected inbound(s). They stay on this inbound too.",
|
"attachClientsDesc": "Attaches the same {count} clients (same UUID/password and shared traffic) to the selected inbound(s). They stay on this inbound too.",
|
||||||
"attachClientsTargets": "Target inbounds",
|
"attachClientsTargets": "Target inbounds",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "Every client's up/down counter drops to zero. Quotas and expiry are not affected. This cannot be undone.",
|
"resetAllTrafficsContent": "Every client's up/down counter drops to zero. Quotas and expiry are not affected. This cannot be undone.",
|
||||||
"deleteConfirmTitle": "Delete client {email}?",
|
"deleteConfirmTitle": "Delete client {email}?",
|
||||||
"deleteConfirmContent": "This removes the client from every attached inbound and drops its traffic record. This cannot be undone.",
|
"deleteConfirmContent": "This removes the client from every attached inbound and drops its traffic record. This cannot be undone.",
|
||||||
"adjustSelected": "Adjust ({count})",
|
|
||||||
"subLinksSelected": "Sub links ({count})",
|
|
||||||
"addToGroupTitle": "Add {count} client(s) to a group",
|
|
||||||
"addToGroupTooltip": "Pick an existing group or type a new name. Use the Ungroup action to remove clients from their current group.",
|
|
||||||
"groupName": "Group name",
|
|
||||||
"addToGroupSuccessToast": "Added {count} client(s) to {group}",
|
|
||||||
"ungroupSuccessToast": "Cleared group from {count} client(s)",
|
|
||||||
"ungroup": "Ungroup",
|
|
||||||
"ungroupConfirmTitle": "Remove {count} client(s) from their group?",
|
|
||||||
"ungroupConfirmContent": "Clears the group label on each selected client. Clients themselves are kept (use Delete to remove them entirely).",
|
|
||||||
"addToGroup": "Add to group",
|
|
||||||
"attach": "Attach",
|
"attach": "Attach",
|
||||||
"adjust": "Adjust",
|
"adjust": "Adjust",
|
||||||
"subLinks": "Sub links",
|
"subLinks": "Sub links",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "Renew on this day of every calendar month, at midnight in the panel's time zone, instead of every N days. A month too short for the chosen day renews on its last day. 0 keeps the day-interval mode.",
|
"renewOnDayDesc": "Renew on this day of every calendar month, at midnight in the panel's time zone, instead of every N days. A month too short for the chosen day renews on its last day. 0 keeps the day-interval mode.",
|
||||||
"renewsUsed": "Renewals used"
|
"renewsUsed": "Renewals used"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "Name",
|
|
||||||
"clientCount": "Clients",
|
|
||||||
"totalGroups": "Total groups",
|
|
||||||
"totalGroupedClients": "Clients with a group",
|
|
||||||
"trafficUsed": "Traffic used",
|
|
||||||
"upload": "Upload",
|
|
||||||
"download": "Download",
|
|
||||||
"totalTraffic": "Total traffic",
|
|
||||||
"totalUpDown": "Total upload / download",
|
|
||||||
"addGroup": "Add Group",
|
|
||||||
"createSuccess": "Group \"{name}\" created.",
|
|
||||||
"rename": "Rename",
|
|
||||||
"renameTitle": "Rename {name}",
|
|
||||||
"renameCollision": "A group named \"{name}\" already exists.",
|
|
||||||
"renameSuccess": "Renamed group on {count} client(s).",
|
|
||||||
"deleteConfirmTitle": "Delete group {name}?",
|
|
||||||
"deleteConfirmContent": "This removes the group and clears its label from {count} client(s). The clients themselves are not deleted.",
|
|
||||||
"deleteSuccess": "Cleared group from {count} client(s).",
|
|
||||||
"resetTraffic": "Reset traffic",
|
|
||||||
"resetConfirmTitle": "Reset traffic for group {name}?",
|
|
||||||
"resetConfirmContent": "This resets only the group's traffic counter. Individual client counters are not affected.",
|
|
||||||
"resetSuccess": "Group {name} traffic reset.",
|
|
||||||
"adjustSuccess": "Adjusted {count} client(s) in {name}.",
|
|
||||||
"emptyForAction": "This group has no clients yet.",
|
|
||||||
"deleteGroupOnly": "Delete group (keep clients)",
|
|
||||||
"deleteClients": "Delete clients in group",
|
|
||||||
"deleteClientsConfirmTitle": "Delete all clients in {name}?",
|
|
||||||
"deleteClientsConfirmContent": "This permanently removes {count} client(s) along with their traffic records. The group label is cleared too. This cannot be undone.",
|
|
||||||
"deleteClientsSuccess": "Deleted {count} client(s).",
|
|
||||||
"deleteClientsMixed": "{ok} deleted, {failed} skipped",
|
|
||||||
"addToGroup": "Add clients…",
|
|
||||||
"addToGroupTitle": "Add clients to group \"{name}\"",
|
|
||||||
"addToGroupDesc": "Select clients to add to this group. They keep their existing inbound attachments; only the group label changes. Clients already in this group are not listed.",
|
|
||||||
"addToGroupEmpty": "No other clients available to add.",
|
|
||||||
"addToGroupResult": "Added {count} client(s) to {name}.",
|
|
||||||
"removeFromGroup": "Remove clients…",
|
|
||||||
"removeFromGroupTitle": "Remove clients from group \"{name}\"",
|
|
||||||
"removeFromGroupDesc": "Select members to remove from this group. Clients themselves are kept (use \"Delete clients in group\" to remove them entirely).",
|
|
||||||
"removeFromGroupResult": "Removed {count} client(s) from {name}."
|
|
||||||
},
|
|
||||||
"hosts": {
|
"hosts": {
|
||||||
"addHost": "Add Host",
|
"addHost": "Add Host",
|
||||||
"editHost": "Edit Host",
|
"editHost": "Edit Host",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "Estado del Sistema",
|
"dashboard": "Estado del Sistema",
|
||||||
"inbounds": "Entradas",
|
"inbounds": "Entradas",
|
||||||
"clients": "Clientes",
|
"clients": "Clientes",
|
||||||
"groups": "Grupos",
|
|
||||||
"nodes": "Nodos",
|
"nodes": "Nodos",
|
||||||
"settings": "Ajustes del panel",
|
"settings": "Ajustes del panel",
|
||||||
"xray": "Configuración Xray",
|
"xray": "Configuración Xray",
|
||||||
"routing": "Enrutamiento",
|
"routing": "Enrutamiento",
|
||||||
"outbounds": "Salidas",
|
"outbounds": "Salidas",
|
||||||
"apiDocs": "Documentación de la API",
|
|
||||||
"donate": "Donar",
|
"donate": "Donar",
|
||||||
"hosts": "Hosts",
|
"hosts": "Hosts",
|
||||||
"docs": "Documentación",
|
"docs": "Documentación",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "Swap",
|
"swap": "Swap",
|
||||||
"storage": "Almacenamiento",
|
"storage": "Almacenamiento",
|
||||||
"memory": "Memoria",
|
"memory": "Memoria",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "Detener",
|
|
||||||
"restartXray": "Reiniciar",
|
|
||||||
"updatePanel": "Actualizar panel",
|
"updatePanel": "Actualizar panel",
|
||||||
"panelUpdateDesc": "Esto actualizará 3X-UI a la última versión y reiniciará el servicio del panel.",
|
"panelUpdateDesc": "Esto actualizará 3X-UI a la última versión y reiniciará el servicio del panel.",
|
||||||
"currentPanelVersion": "Versión actual del panel",
|
"currentPanelVersion": "Versión actual del panel",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "Commit actual",
|
"currentCommit": "Commit actual",
|
||||||
"latestCommit": "Último commit",
|
"latestCommit": "Último commit",
|
||||||
"updateChannelChanged": "Canal de actualización cambiado",
|
"updateChannelChanged": "Canal de actualización cambiado",
|
||||||
"xrayStatusUnknown": "Desconocido",
|
|
||||||
"xrayStatusRunning": "En ejecución",
|
|
||||||
"xrayStatusStop": "Detenido",
|
|
||||||
"xrayStatusError": "Error",
|
|
||||||
"systemHistoryTitle": "Historial del Sistema",
|
"systemHistoryTitle": "Historial del Sistema",
|
||||||
"historyTitleCpu": "Uso de CPU",
|
"historyTitleCpu": "Uso de CPU",
|
||||||
"historyTitleMem": "Uso de Memoria",
|
"historyTitleMem": "Uso de Memoria",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "¿Eliminar los {count} clientes de \"{remark}\"?",
|
"delAllClientsConfirmTitle": "¿Eliminar los {count} clientes de \"{remark}\"?",
|
||||||
"delAllClientsConfirmContent": "Elimina todos los clientes de este inbound y sus registros de tráfico. El inbound se mantiene. Esto no se puede deshacer.",
|
"delAllClientsConfirmContent": "Elimina todos los clientes de este inbound y sus registros de tráfico. El inbound se mantiene. Esto no se puede deshacer.",
|
||||||
"attachClients": "Asociar clientes a…",
|
"attachClients": "Asociar clientes a…",
|
||||||
"addClientsToGroup": "Añadir clientes al grupo…",
|
|
||||||
"attachClientsTitle": "Asociar clientes desde «{remark}»",
|
"attachClientsTitle": "Asociar clientes desde «{remark}»",
|
||||||
"attachClientsDesc": "Asocia los mismos {count} cliente(s) (mismo UUID/contraseña y tráfico compartido) a las entradas seleccionadas. Permanecen también en esta entrada.",
|
"attachClientsDesc": "Asocia los mismos {count} cliente(s) (mismo UUID/contraseña y tráfico compartido) a las entradas seleccionadas. Permanecen también en esta entrada.",
|
||||||
"attachClientsTargets": "Entradas objetivo",
|
"attachClientsTargets": "Entradas objetivo",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "El contador de subida/bajada de cada cliente vuelve a cero. Las cuotas y la expiración no se modifican. Esta acción no se puede deshacer.",
|
"resetAllTrafficsContent": "El contador de subida/bajada de cada cliente vuelve a cero. Las cuotas y la expiración no se modifican. Esta acción no se puede deshacer.",
|
||||||
"deleteConfirmTitle": "¿Eliminar al cliente {email}?",
|
"deleteConfirmTitle": "¿Eliminar al cliente {email}?",
|
||||||
"deleteConfirmContent": "Esto elimina al cliente de cada inbound asociado y descarta su registro de tráfico. No se puede deshacer.",
|
"deleteConfirmContent": "Esto elimina al cliente de cada inbound asociado y descarta su registro de tráfico. No se puede deshacer.",
|
||||||
"adjustSelected": "Ajustar ({count})",
|
|
||||||
"subLinksSelected": "Enlaces sub ({count})",
|
|
||||||
"addToGroupTitle": "Añadir {count} cliente(s) a un grupo",
|
|
||||||
"addToGroupTooltip": "Selecciona un grupo existente o escribe un nombre nuevo. Usa Ungroup para quitar clientes de su grupo actual.",
|
|
||||||
"groupName": "Nombre del grupo",
|
|
||||||
"addToGroupSuccessToast": "Se añadieron {count} cliente(s) a {group}",
|
|
||||||
"ungroupSuccessToast": "Grupo limpiado de {count} cliente(s)",
|
|
||||||
"ungroup": "Desagrupar",
|
|
||||||
"ungroupConfirmTitle": "¿Quitar {count} cliente(s) de su grupo?",
|
|
||||||
"ungroupConfirmContent": "Limpia la etiqueta de grupo en cada cliente seleccionado. Los clientes se conservan (usa Delete para eliminarlos por completo).",
|
|
||||||
"addToGroup": "Añadir al grupo",
|
|
||||||
"attach": "Asociar",
|
"attach": "Asociar",
|
||||||
"adjust": "Ajustar",
|
"adjust": "Ajustar",
|
||||||
"subLinks": "Enlaces sub",
|
"subLinks": "Enlaces sub",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "Renueva este día de cada mes natural, a medianoche en la zona horaria del panel, en lugar de cada N días. Si el mes es demasiado corto para el día elegido, renueva su último día. 0 mantiene el modo de intervalo en días.",
|
"renewOnDayDesc": "Renueva este día de cada mes natural, a medianoche en la zona horaria del panel, en lugar de cada N días. Si el mes es demasiado corto para el día elegido, renueva su último día. 0 mantiene el modo de intervalo en días.",
|
||||||
"renewsUsed": "Renovaciones usadas"
|
"renewsUsed": "Renovaciones usadas"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "Nombre",
|
|
||||||
"clientCount": "Clientes",
|
|
||||||
"totalGroups": "Total de grupos",
|
|
||||||
"totalGroupedClients": "Clientes con grupo",
|
|
||||||
"trafficUsed": "Tráfico usado",
|
|
||||||
"upload": "Subida",
|
|
||||||
"download": "Bajada",
|
|
||||||
"totalTraffic": "Tráfico total",
|
|
||||||
"totalUpDown": "Subida / bajada total",
|
|
||||||
"addGroup": "Añadir grupo",
|
|
||||||
"createSuccess": "Grupo «{name}» creado.",
|
|
||||||
"rename": "Renombrar",
|
|
||||||
"renameTitle": "Renombrar {name}",
|
|
||||||
"renameCollision": "Ya existe un grupo llamado «{name}».",
|
|
||||||
"renameSuccess": "Grupo renombrado en {count} cliente(s).",
|
|
||||||
"deleteConfirmTitle": "¿Eliminar el grupo {name}?",
|
|
||||||
"deleteConfirmContent": "Esto elimina el grupo y limpia su etiqueta de {count} cliente(s). Los clientes en sí no se eliminan.",
|
|
||||||
"deleteSuccess": "Grupo limpiado de {count} cliente(s).",
|
|
||||||
"resetTraffic": "Restablecer tráfico",
|
|
||||||
"resetConfirmTitle": "¿Restablecer tráfico del grupo {name}?",
|
|
||||||
"resetConfirmContent": "Esto restablece solo el contador de tráfico del grupo. Los contadores de cada cliente no se ven afectados.",
|
|
||||||
"resetSuccess": "Tráfico del grupo {name} restablecido.",
|
|
||||||
"adjustSuccess": "Ajustados {count} cliente(s) en {name}.",
|
|
||||||
"emptyForAction": "Este grupo aún no tiene clientes.",
|
|
||||||
"deleteGroupOnly": "Eliminar grupo (conservar clientes)",
|
|
||||||
"deleteClients": "Eliminar clientes del grupo",
|
|
||||||
"deleteClientsConfirmTitle": "¿Eliminar todos los clientes en {name}?",
|
|
||||||
"deleteClientsConfirmContent": "Esto elimina permanentemente {count} cliente(s) junto con sus registros de tráfico. La etiqueta de grupo también se limpia. Esto no se puede deshacer.",
|
|
||||||
"deleteClientsSuccess": "Eliminados {count} cliente(s).",
|
|
||||||
"deleteClientsMixed": "{ok} eliminados, {failed} omitidos",
|
|
||||||
"addToGroup": "Añadir clientes…",
|
|
||||||
"addToGroupTitle": "Añadir clientes al grupo «{name}»",
|
|
||||||
"addToGroupDesc": "Selecciona clientes para añadir a este grupo. Mantienen sus asociaciones de entrada existentes; solo cambia la etiqueta de grupo. Los clientes que ya están en este grupo no se muestran.",
|
|
||||||
"addToGroupEmpty": "No hay otros clientes disponibles para añadir.",
|
|
||||||
"addToGroupResult": "Añadidos {count} cliente(s) a {name}.",
|
|
||||||
"removeFromGroup": "Quitar clientes…",
|
|
||||||
"removeFromGroupTitle": "Quitar clientes del grupo «{name}»",
|
|
||||||
"removeFromGroupDesc": "Selecciona miembros para quitar de este grupo. Los clientes se conservan (usa «Eliminar clientes del grupo» para eliminarlos por completo).",
|
|
||||||
"removeFromGroupResult": "Quitados {count} cliente(s) de {name}."
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "Agregar nodo",
|
"addNode": "Agregar nodo",
|
||||||
"editNode": "Editar nodo",
|
"editNode": "Editar nodo",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "نمای کلی",
|
"dashboard": "نمای کلی",
|
||||||
"inbounds": "ورودیها",
|
"inbounds": "ورودیها",
|
||||||
"clients": "کلاینتها",
|
"clients": "کلاینتها",
|
||||||
"groups": "گروهها",
|
|
||||||
"nodes": "نودها",
|
"nodes": "نودها",
|
||||||
"settings": "تنظیمات پنل",
|
"settings": "تنظیمات پنل",
|
||||||
"xray": "پیکربندی Xray",
|
"xray": "پیکربندی Xray",
|
||||||
"routing": "مسیریابی",
|
"routing": "مسیریابی",
|
||||||
"outbounds": "خروجیها",
|
"outbounds": "خروجیها",
|
||||||
"apiDocs": "مستندات API",
|
|
||||||
"donate": "حمایت مالی",
|
"donate": "حمایت مالی",
|
||||||
"hosts": "میزبانها",
|
"hosts": "میزبانها",
|
||||||
"docs": "مستندات",
|
"docs": "مستندات",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "سواپ",
|
"swap": "سواپ",
|
||||||
"storage": "ذخیرهسازی",
|
"storage": "ذخیرهسازی",
|
||||||
"memory": "حافظه",
|
"memory": "حافظه",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "توقف",
|
|
||||||
"restartXray": "راهاندازی مجدد",
|
|
||||||
"updatePanel": "بهروزرسانی پنل",
|
"updatePanel": "بهروزرسانی پنل",
|
||||||
"panelUpdateDesc": "این عملیات 3X-UI را به آخرین نسخه بهروزرسانی میکند و سرویس پنل را مجدداً راهاندازی میکند.",
|
"panelUpdateDesc": "این عملیات 3X-UI را به آخرین نسخه بهروزرسانی میکند و سرویس پنل را مجدداً راهاندازی میکند.",
|
||||||
"currentPanelVersion": "نسخه فعلی پنل",
|
"currentPanelVersion": "نسخه فعلی پنل",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "کامیت فعلی",
|
"currentCommit": "کامیت فعلی",
|
||||||
"latestCommit": "آخرین کامیت",
|
"latestCommit": "آخرین کامیت",
|
||||||
"updateChannelChanged": "کانال بهروزرسانی تغییر کرد",
|
"updateChannelChanged": "کانال بهروزرسانی تغییر کرد",
|
||||||
"xrayStatusUnknown": "ناشناخته",
|
|
||||||
"xrayStatusRunning": "در حال اجرا",
|
|
||||||
"xrayStatusStop": "متوقف",
|
|
||||||
"xrayStatusError": "خطا",
|
|
||||||
"systemHistoryTitle": "تاریخچه سیستم",
|
"systemHistoryTitle": "تاریخچه سیستم",
|
||||||
"historyTitleCpu": "مصرف پردازنده",
|
"historyTitleCpu": "مصرف پردازنده",
|
||||||
"historyTitleMem": "مصرف حافظه",
|
"historyTitleMem": "مصرف حافظه",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "حذف هر {count} کلاینت اینباند «{remark}»؟",
|
"delAllClientsConfirmTitle": "حذف هر {count} کلاینت اینباند «{remark}»؟",
|
||||||
"delAllClientsConfirmContent": "تمام کلاینتهای این اینباند به همراه رکوردهای ترافیکشان حذف میشوند. خود اینباند باقی میماند. این عمل غیرقابل بازگشت است.",
|
"delAllClientsConfirmContent": "تمام کلاینتهای این اینباند به همراه رکوردهای ترافیکشان حذف میشوند. خود اینباند باقی میماند. این عمل غیرقابل بازگشت است.",
|
||||||
"attachClients": "الصاق کاربران به…",
|
"attachClients": "الصاق کاربران به…",
|
||||||
"addClientsToGroup": "افزودن کاربران به گروه…",
|
|
||||||
"attachClientsTitle": "الصاق کاربران از «{remark}»",
|
"attachClientsTitle": "الصاق کاربران از «{remark}»",
|
||||||
"attachClientsDesc": "همان {count} کاربر (با UUID/رمز یکسان و ترافیک مشترک) را به ورودی(های) انتخابی الصاق میکند. در این ورودی هم باقی میمانند.",
|
"attachClientsDesc": "همان {count} کاربر (با UUID/رمز یکسان و ترافیک مشترک) را به ورودی(های) انتخابی الصاق میکند. در این ورودی هم باقی میمانند.",
|
||||||
"attachClientsTargets": "ورودیهای مقصد",
|
"attachClientsTargets": "ورودیهای مقصد",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "شمارنده ارسال/دریافت همه کلاینتها به صفر میرسد. سهمیه و تاریخ انقضا تغییری نمیکند. این عمل غیرقابل بازگشت است.",
|
"resetAllTrafficsContent": "شمارنده ارسال/دریافت همه کلاینتها به صفر میرسد. سهمیه و تاریخ انقضا تغییری نمیکند. این عمل غیرقابل بازگشت است.",
|
||||||
"deleteConfirmTitle": "حذف کلاینت {email}؟",
|
"deleteConfirmTitle": "حذف کلاینت {email}؟",
|
||||||
"deleteConfirmContent": "این کلاینت از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک میشود. این عمل غیرقابل بازگشت است.",
|
"deleteConfirmContent": "این کلاینت از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک میشود. این عمل غیرقابل بازگشت است.",
|
||||||
"adjustSelected": "تنظیم ({count})",
|
|
||||||
"subLinksSelected": "لینکهای اشتراک ({count})",
|
|
||||||
"addToGroupTitle": "افزودن {count} کاربر به یک گروه",
|
|
||||||
"addToGroupTooltip": "یک گروه موجود را انتخاب کنید یا نام جدیدی تایپ کنید. برای حذف کاربران از گروه فعلی، از Ungroup استفاده کنید.",
|
|
||||||
"groupName": "نام گروه",
|
|
||||||
"addToGroupSuccessToast": "{count} کاربر به {group} اضافه شد",
|
|
||||||
"ungroupSuccessToast": "گروه از {count} کاربر پاک شد",
|
|
||||||
"ungroup": "خارج از گروه",
|
|
||||||
"ungroupConfirmTitle": "حذف {count} کاربر از گروهشان؟",
|
|
||||||
"ungroupConfirmContent": "برچسب گروه را روی هر کاربر انتخابی پاک میکند. کاربران حفظ میشوند (برای حذف کامل از Delete استفاده کنید).",
|
|
||||||
"addToGroup": "افزودن به گروه",
|
|
||||||
"attach": "الصاق",
|
"attach": "الصاق",
|
||||||
"adjust": "تنظیم",
|
"adjust": "تنظیم",
|
||||||
"subLinks": "لینکهای اشتراک",
|
"subLinks": "لینکهای اشتراک",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "در این روز از هر ماه تقویمی، در نیمهشب به وقت پنل تمدید میشود، به جای هر N روز. اگر ماه کوتاهتر از روز انتخابی باشد، در آخرین روز آن ماه تمدید میشود. مقدار ۰ حالت بازهٔ روزانه را حفظ میکند.",
|
"renewOnDayDesc": "در این روز از هر ماه تقویمی، در نیمهشب به وقت پنل تمدید میشود، به جای هر N روز. اگر ماه کوتاهتر از روز انتخابی باشد، در آخرین روز آن ماه تمدید میشود. مقدار ۰ حالت بازهٔ روزانه را حفظ میکند.",
|
||||||
"renewsUsed": "تمدیدهای استفادهشده"
|
"renewsUsed": "تمدیدهای استفادهشده"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "نام",
|
|
||||||
"clientCount": "کاربران",
|
|
||||||
"totalGroups": "تعداد گروهها",
|
|
||||||
"totalGroupedClients": "کاربران دارای گروه",
|
|
||||||
"trafficUsed": "ترافیک مصرفشده",
|
|
||||||
"upload": "آپلود",
|
|
||||||
"download": "دانلود",
|
|
||||||
"totalTraffic": "مجموع ترافیک",
|
|
||||||
"totalUpDown": "مجموع آپلود / دانلود",
|
|
||||||
"addGroup": "افزودن گروه",
|
|
||||||
"createSuccess": "گروه «{name}» ایجاد شد.",
|
|
||||||
"rename": "تغییر نام",
|
|
||||||
"renameTitle": "تغییر نام {name}",
|
|
||||||
"renameCollision": "گروهی به نام «{name}» از قبل وجود دارد.",
|
|
||||||
"renameSuccess": "گروه روی {count} کاربر تغییر نام داده شد.",
|
|
||||||
"deleteConfirmTitle": "حذف گروه {name}؟",
|
|
||||||
"deleteConfirmContent": "این عمل گروه را حذف میکند و برچسب آن را از {count} کاربر پاک میکند. خود کاربران حذف نمیشوند.",
|
|
||||||
"deleteSuccess": "گروه از {count} کاربر پاک شد.",
|
|
||||||
"resetTraffic": "بازنشانی ترافیک",
|
|
||||||
"resetConfirmTitle": "بازنشانی ترافیک گروه {name}؟",
|
|
||||||
"resetConfirmContent": "این فقط شمارندهی ترافیک گروه را صفر میکند؛ شمارندهی تکتک کاربران دستنخورده میماند.",
|
|
||||||
"resetSuccess": "ترافیک گروه {name} صفر شد.",
|
|
||||||
"adjustSuccess": "{count} کاربر در {name} تنظیم شد.",
|
|
||||||
"emptyForAction": "این گروه هنوز کاربری ندارد.",
|
|
||||||
"deleteGroupOnly": "حذف گروه (نگه داشتن کاربران)",
|
|
||||||
"deleteClients": "حذف کاربران گروه",
|
|
||||||
"deleteClientsConfirmTitle": "حذف همه کاربران در {name}؟",
|
|
||||||
"deleteClientsConfirmContent": "این عمل {count} کاربر را به همراه رکورد ترافیکشان برای همیشه حذف میکند. برچسب گروه نیز پاک میشود. این عمل قابل بازگشت نیست.",
|
|
||||||
"deleteClientsSuccess": "{count} کاربر حذف شد.",
|
|
||||||
"deleteClientsMixed": "{ok} حذف شد، {failed} نادیده گرفته شد",
|
|
||||||
"addToGroup": "افزودن کاربران…",
|
|
||||||
"addToGroupTitle": "افزودن کاربران به گروه «{name}»",
|
|
||||||
"addToGroupDesc": "کاربرانی را برای افزودن به این گروه انتخاب کنید. الصاقهای ورودی فعلی حفظ میشود؛ تنها برچسب گروه تغییر میکند. کاربرانی که از قبل در این گروه هستند نشان داده نمیشوند.",
|
|
||||||
"addToGroupEmpty": "کاربر دیگری برای افزودن در دسترس نیست.",
|
|
||||||
"addToGroupResult": "{count} کاربر به {name} اضافه شد.",
|
|
||||||
"removeFromGroup": "حذف کاربران…",
|
|
||||||
"removeFromGroupTitle": "حذف کاربران از گروه «{name}»",
|
|
||||||
"removeFromGroupDesc": "اعضایی را برای حذف از این گروه انتخاب کنید. خود کاربران حفظ میشوند (برای حذف کامل از «حذف کاربران گروه» استفاده کنید).",
|
|
||||||
"removeFromGroupResult": "{count} کاربر از {name} حذف شد."
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "افزودن نود",
|
"addNode": "افزودن نود",
|
||||||
"editNode": "ویرایش نود",
|
"editNode": "ویرایش نود",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "Ikhtisar",
|
"dashboard": "Ikhtisar",
|
||||||
"inbounds": "Inbound",
|
"inbounds": "Inbound",
|
||||||
"clients": "Klien",
|
"clients": "Klien",
|
||||||
"groups": "Grup",
|
|
||||||
"nodes": "Node",
|
"nodes": "Node",
|
||||||
"settings": "Pengaturan Panel",
|
"settings": "Pengaturan Panel",
|
||||||
"xray": "Konfigurasi Xray",
|
"xray": "Konfigurasi Xray",
|
||||||
"routing": "Pengalihan",
|
"routing": "Pengalihan",
|
||||||
"outbounds": "Outbound",
|
"outbounds": "Outbound",
|
||||||
"apiDocs": "Dokumentasi API",
|
|
||||||
"donate": "Donasi",
|
"donate": "Donasi",
|
||||||
"hosts": "Host",
|
"hosts": "Host",
|
||||||
"docs": "Dokumentasi",
|
"docs": "Dokumentasi",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "Swap",
|
"swap": "Swap",
|
||||||
"storage": "Penyimpanan",
|
"storage": "Penyimpanan",
|
||||||
"memory": "Memori",
|
"memory": "Memori",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "Hentikan",
|
|
||||||
"restartXray": "Mulai ulang",
|
|
||||||
"updatePanel": "Perbarui Panel",
|
"updatePanel": "Perbarui Panel",
|
||||||
"panelUpdateDesc": "Ini akan memperbarui 3X-UI ke rilis terbaru dan me-restart layanan panel.",
|
"panelUpdateDesc": "Ini akan memperbarui 3X-UI ke rilis terbaru dan me-restart layanan panel.",
|
||||||
"currentPanelVersion": "Versi panel saat ini",
|
"currentPanelVersion": "Versi panel saat ini",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "Commit saat ini",
|
"currentCommit": "Commit saat ini",
|
||||||
"latestCommit": "Commit terbaru",
|
"latestCommit": "Commit terbaru",
|
||||||
"updateChannelChanged": "Kanal pembaruan diubah",
|
"updateChannelChanged": "Kanal pembaruan diubah",
|
||||||
"xrayStatusUnknown": "Tidak diketahui",
|
|
||||||
"xrayStatusRunning": "Berjalan",
|
|
||||||
"xrayStatusStop": "Berhenti",
|
|
||||||
"xrayStatusError": "Error",
|
|
||||||
"systemHistoryTitle": "Riwayat Sistem",
|
"systemHistoryTitle": "Riwayat Sistem",
|
||||||
"historyTitleCpu": "Penggunaan CPU",
|
"historyTitleCpu": "Penggunaan CPU",
|
||||||
"historyTitleMem": "Penggunaan Memori",
|
"historyTitleMem": "Penggunaan Memori",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "Hapus semua {count} klien dari \"{remark}\"?",
|
"delAllClientsConfirmTitle": "Hapus semua {count} klien dari \"{remark}\"?",
|
||||||
"delAllClientsConfirmContent": "Menghapus setiap klien dari inbound ini dan menghapus catatan trafiknya. Inbound itu sendiri dipertahankan. Tindakan ini tidak dapat dibatalkan.",
|
"delAllClientsConfirmContent": "Menghapus setiap klien dari inbound ini dan menghapus catatan trafiknya. Inbound itu sendiri dipertahankan. Tindakan ini tidak dapat dibatalkan.",
|
||||||
"attachClients": "Lampirkan klien ke…",
|
"attachClients": "Lampirkan klien ke…",
|
||||||
"addClientsToGroup": "Tambah klien ke grup…",
|
|
||||||
"attachClientsTitle": "Lampirkan klien dari «{remark}»",
|
"attachClientsTitle": "Lampirkan klien dari «{remark}»",
|
||||||
"attachClientsDesc": "Melampirkan {count} klien yang sama (UUID/kata sandi sama dan trafik bersama) ke inbound terpilih. Tetap ada di inbound ini juga.",
|
"attachClientsDesc": "Melampirkan {count} klien yang sama (UUID/kata sandi sama dan trafik bersama) ke inbound terpilih. Tetap ada di inbound ini juga.",
|
||||||
"attachClientsTargets": "Inbound tujuan",
|
"attachClientsTargets": "Inbound tujuan",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "Penghitung kirim/terima setiap klien turun ke nol. Kuota dan kedaluwarsa tidak terpengaruh. Tidak dapat dibatalkan.",
|
"resetAllTrafficsContent": "Penghitung kirim/terima setiap klien turun ke nol. Kuota dan kedaluwarsa tidak terpengaruh. Tidak dapat dibatalkan.",
|
||||||
"deleteConfirmTitle": "Hapus klien {email}?",
|
"deleteConfirmTitle": "Hapus klien {email}?",
|
||||||
"deleteConfirmContent": "Tindakan ini menghapus klien dari setiap inbound terlampir dan menghapus catatan lalu lintasnya. Tidak dapat dibatalkan.",
|
"deleteConfirmContent": "Tindakan ini menghapus klien dari setiap inbound terlampir dan menghapus catatan lalu lintasnya. Tidak dapat dibatalkan.",
|
||||||
"adjustSelected": "Sesuaikan ({count})",
|
|
||||||
"subLinksSelected": "Tautan sub ({count})",
|
|
||||||
"addToGroupTitle": "Tambahkan {count} klien ke grup",
|
|
||||||
"addToGroupTooltip": "Pilih grup yang ada atau ketik nama baru. Gunakan Ungroup untuk menghapus klien dari grup saat ini.",
|
|
||||||
"groupName": "Nama grup",
|
|
||||||
"addToGroupSuccessToast": "{count} klien ditambahkan ke {group}",
|
|
||||||
"ungroupSuccessToast": "Grup dihapus dari {count} klien",
|
|
||||||
"ungroup": "Lepaskan grup",
|
|
||||||
"ungroupConfirmTitle": "Hapus {count} klien dari grupnya?",
|
|
||||||
"ungroupConfirmContent": "Menghapus label grup dari setiap klien terpilih. Klien tetap dipertahankan (gunakan Delete untuk menghapus sepenuhnya).",
|
|
||||||
"addToGroup": "Tambahkan ke grup",
|
|
||||||
"attach": "Lampirkan",
|
"attach": "Lampirkan",
|
||||||
"adjust": "Atur",
|
"adjust": "Atur",
|
||||||
"subLinks": "Tautan sub",
|
"subLinks": "Tautan sub",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "Perpanjang pada tanggal ini setiap bulan kalender, pada tengah malam menurut zona waktu panel, alih-alih setiap N hari. Bulan yang terlalu pendek untuk tanggal yang dipilih diperpanjang pada hari terakhirnya. 0 mempertahankan mode interval hari.",
|
"renewOnDayDesc": "Perpanjang pada tanggal ini setiap bulan kalender, pada tengah malam menurut zona waktu panel, alih-alih setiap N hari. Bulan yang terlalu pendek untuk tanggal yang dipilih diperpanjang pada hari terakhirnya. 0 mempertahankan mode interval hari.",
|
||||||
"renewsUsed": "Perpanjangan terpakai"
|
"renewsUsed": "Perpanjangan terpakai"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "Nama",
|
|
||||||
"clientCount": "Klien",
|
|
||||||
"totalGroups": "Total grup",
|
|
||||||
"totalGroupedClients": "Klien dengan grup",
|
|
||||||
"trafficUsed": "Trafik terpakai",
|
|
||||||
"upload": "Unggah",
|
|
||||||
"download": "Unduh",
|
|
||||||
"totalTraffic": "Total trafik",
|
|
||||||
"totalUpDown": "Total unggah / unduh",
|
|
||||||
"addGroup": "Tambah grup",
|
|
||||||
"createSuccess": "Grup «{name}» dibuat.",
|
|
||||||
"rename": "Ubah nama",
|
|
||||||
"renameTitle": "Ubah nama {name}",
|
|
||||||
"renameCollision": "Grup bernama «{name}» sudah ada.",
|
|
||||||
"renameSuccess": "Grup diubah namanya pada {count} klien.",
|
|
||||||
"deleteConfirmTitle": "Hapus grup {name}?",
|
|
||||||
"deleteConfirmContent": "Ini menghapus grup dan label-nya dari {count} klien. Klien itu sendiri tidak dihapus.",
|
|
||||||
"deleteSuccess": "Grup dihapus dari {count} klien.",
|
|
||||||
"resetTraffic": "Reset trafik",
|
|
||||||
"resetConfirmTitle": "Reset trafik grup {name}?",
|
|
||||||
"resetConfirmContent": "Ini hanya mengatur ulang penghitung trafik grup. Penghitung tiap klien tidak terpengaruh.",
|
|
||||||
"resetSuccess": "Trafik grup {name} direset.",
|
|
||||||
"adjustSuccess": "{count} klien di {name} disesuaikan.",
|
|
||||||
"emptyForAction": "Grup ini belum memiliki klien.",
|
|
||||||
"deleteGroupOnly": "Hapus grup (pertahankan klien)",
|
|
||||||
"deleteClients": "Hapus klien di grup",
|
|
||||||
"deleteClientsConfirmTitle": "Hapus semua klien di {name}?",
|
|
||||||
"deleteClientsConfirmContent": "Ini akan menghapus {count} klien secara permanen beserta catatan trafiknya. Label grup juga dihapus. Tidak dapat dibatalkan.",
|
|
||||||
"deleteClientsSuccess": "{count} klien dihapus.",
|
|
||||||
"deleteClientsMixed": "{ok} dihapus, {failed} dilewati",
|
|
||||||
"addToGroup": "Tambah klien…",
|
|
||||||
"addToGroupTitle": "Tambah klien ke grup «{name}»",
|
|
||||||
"addToGroupDesc": "Pilih klien untuk ditambahkan ke grup ini. Lampiran inbound yang ada tetap dipertahankan; hanya label grup yang berubah. Klien yang sudah ada di grup ini tidak ditampilkan.",
|
|
||||||
"addToGroupEmpty": "Tidak ada klien lain untuk ditambahkan.",
|
|
||||||
"addToGroupResult": "{count} klien ditambahkan ke {name}.",
|
|
||||||
"removeFromGroup": "Hapus klien…",
|
|
||||||
"removeFromGroupTitle": "Hapus klien dari grup «{name}»",
|
|
||||||
"removeFromGroupDesc": "Pilih anggota untuk dihapus dari grup ini. Klien tetap dipertahankan (gunakan «Hapus klien di grup» untuk menghapus sepenuhnya).",
|
|
||||||
"removeFromGroupResult": "{count} klien dihapus dari {name}."
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "Tambah Node",
|
"addNode": "Tambah Node",
|
||||||
"editNode": "Edit node",
|
"editNode": "Edit node",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "ダッシュボード",
|
"dashboard": "ダッシュボード",
|
||||||
"inbounds": "インバウンド",
|
"inbounds": "インバウンド",
|
||||||
"clients": "クライアント",
|
"clients": "クライアント",
|
||||||
"groups": "グループ",
|
|
||||||
"nodes": "ノード",
|
"nodes": "ノード",
|
||||||
"settings": "パネル設定",
|
"settings": "パネル設定",
|
||||||
"xray": "Xray 設定",
|
"xray": "Xray 設定",
|
||||||
"routing": "ルーティング",
|
"routing": "ルーティング",
|
||||||
"outbounds": "アウトバウンド",
|
"outbounds": "アウトバウンド",
|
||||||
"apiDocs": "API ドキュメント",
|
|
||||||
"donate": "寄付",
|
"donate": "寄付",
|
||||||
"hosts": "ホスト",
|
"hosts": "ホスト",
|
||||||
"docs": "ドキュメント",
|
"docs": "ドキュメント",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "スワップ",
|
"swap": "スワップ",
|
||||||
"storage": "ストレージ",
|
"storage": "ストレージ",
|
||||||
"memory": "メモリ",
|
"memory": "メモリ",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "停止",
|
|
||||||
"restartXray": "再起動",
|
|
||||||
"updatePanel": "パネルを更新",
|
"updatePanel": "パネルを更新",
|
||||||
"panelUpdateDesc": "これにより3X-UIが最新リリースに更新され、パネルサービスが再起動されます。",
|
"panelUpdateDesc": "これにより3X-UIが最新リリースに更新され、パネルサービスが再起動されます。",
|
||||||
"currentPanelVersion": "現在のパネルバージョン",
|
"currentPanelVersion": "現在のパネルバージョン",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "現在のコミット",
|
"currentCommit": "現在のコミット",
|
||||||
"latestCommit": "最新のコミット",
|
"latestCommit": "最新のコミット",
|
||||||
"updateChannelChanged": "更新チャンネルを変更しました",
|
"updateChannelChanged": "更新チャンネルを変更しました",
|
||||||
"xrayStatusUnknown": "不明",
|
|
||||||
"xrayStatusRunning": "実行中",
|
|
||||||
"xrayStatusStop": "停止",
|
|
||||||
"xrayStatusError": "エラー",
|
|
||||||
"systemHistoryTitle": "システム履歴",
|
"systemHistoryTitle": "システム履歴",
|
||||||
"historyTitleCpu": "CPU 使用率",
|
"historyTitleCpu": "CPU 使用率",
|
||||||
"historyTitleMem": "メモリ使用率",
|
"historyTitleMem": "メモリ使用率",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "「{remark}」から {count} 件のクライアントをすべて削除しますか?",
|
"delAllClientsConfirmTitle": "「{remark}」から {count} 件のクライアントをすべて削除しますか?",
|
||||||
"delAllClientsConfirmContent": "このインバウンドからすべてのクライアントを削除し、トラフィックレコードも破棄します。インバウンド自体は保持されます。この操作は取り消せません。",
|
"delAllClientsConfirmContent": "このインバウンドからすべてのクライアントを削除し、トラフィックレコードも破棄します。インバウンド自体は保持されます。この操作は取り消せません。",
|
||||||
"attachClients": "クライアントをアタッチ…",
|
"attachClients": "クライアントをアタッチ…",
|
||||||
"addClientsToGroup": "クライアントをグループに追加…",
|
|
||||||
"attachClientsTitle": "「{remark}」のクライアントをアタッチ",
|
"attachClientsTitle": "「{remark}」のクライアントをアタッチ",
|
||||||
"attachClientsDesc": "同じ {count} クライアント(同じ UUID/パスワードと共有トラフィック)を選択したインバウンドにアタッチします。このインバウンドにも残ります。",
|
"attachClientsDesc": "同じ {count} クライアント(同じ UUID/パスワードと共有トラフィック)を選択したインバウンドにアタッチします。このインバウンドにも残ります。",
|
||||||
"attachClientsTargets": "ターゲットインバウンド",
|
"attachClientsTargets": "ターゲットインバウンド",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "すべてのクライアントの送受信カウンターがゼロにリセットされます。クォータと有効期限には影響しません。元に戻せません。",
|
"resetAllTrafficsContent": "すべてのクライアントの送受信カウンターがゼロにリセットされます。クォータと有効期限には影響しません。元に戻せません。",
|
||||||
"deleteConfirmTitle": "クライアント {email} を削除しますか?",
|
"deleteConfirmTitle": "クライアント {email} を削除しますか?",
|
||||||
"deleteConfirmContent": "クライアントを関連付けされたすべてのインバウンドから削除し、トラフィック記録も破棄します。元に戻せません。",
|
"deleteConfirmContent": "クライアントを関連付けされたすべてのインバウンドから削除し、トラフィック記録も破棄します。元に戻せません。",
|
||||||
"adjustSelected": "調整 ({count})",
|
|
||||||
"subLinksSelected": "サブリンク ({count})",
|
|
||||||
"addToGroupTitle": "{count} クライアントをグループに追加",
|
|
||||||
"addToGroupTooltip": "既存のグループを選ぶか新しい名前を入力してください。Ungroup で現在のグループから外せます。",
|
|
||||||
"groupName": "グループ名",
|
|
||||||
"addToGroupSuccessToast": "{count} クライアントを {group} に追加しました",
|
|
||||||
"ungroupSuccessToast": "{count} クライアントのグループをクリアしました",
|
|
||||||
"ungroup": "グループ解除",
|
|
||||||
"ungroupConfirmTitle": "{count} クライアントをグループから外しますか?",
|
|
||||||
"ungroupConfirmContent": "選択したクライアントのグループラベルをクリアします。クライアント自体は保持されます (完全に削除するには Delete を使用)。",
|
|
||||||
"addToGroup": "グループに追加",
|
|
||||||
"attach": "アタッチ",
|
"attach": "アタッチ",
|
||||||
"adjust": "調整",
|
"adjust": "調整",
|
||||||
"subLinks": "サブリンク",
|
"subLinks": "サブリンク",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "毎月この日の深夜(パネルのタイムゾーン基準)に更新します。N 日ごとの更新の代わりになります。その日が存在しない月は月末に更新されます。0 で日数間隔モードのままになります。",
|
"renewOnDayDesc": "毎月この日の深夜(パネルのタイムゾーン基準)に更新します。N 日ごとの更新の代わりになります。その日が存在しない月は月末に更新されます。0 で日数間隔モードのままになります。",
|
||||||
"renewsUsed": "使用済み更新回数"
|
"renewsUsed": "使用済み更新回数"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "名前",
|
|
||||||
"clientCount": "クライアント",
|
|
||||||
"totalGroups": "グループ合計",
|
|
||||||
"totalGroupedClients": "グループのあるクライアント",
|
|
||||||
"trafficUsed": "使用済みトラフィック",
|
|
||||||
"upload": "アップロード",
|
|
||||||
"download": "ダウンロード",
|
|
||||||
"totalTraffic": "合計トラフィック",
|
|
||||||
"totalUpDown": "合計アップロード / ダウンロード",
|
|
||||||
"addGroup": "グループ追加",
|
|
||||||
"createSuccess": "グループ「{name}」を作成しました。",
|
|
||||||
"rename": "名前変更",
|
|
||||||
"renameTitle": "{name} の名前を変更",
|
|
||||||
"renameCollision": "「{name}」という名前のグループは既に存在します。",
|
|
||||||
"renameSuccess": "{count} クライアントのグループ名を変更しました。",
|
|
||||||
"deleteConfirmTitle": "グループ {name} を削除?",
|
|
||||||
"deleteConfirmContent": "これはグループを削除し、{count} クライアントのラベルをクリアします。クライアント自体は削除されません。",
|
|
||||||
"deleteSuccess": "{count} クライアントのグループをクリアしました。",
|
|
||||||
"resetTraffic": "トラフィックをリセット",
|
|
||||||
"resetConfirmTitle": "グループ {name} のトラフィックをリセット?",
|
|
||||||
"resetConfirmContent": "グループのトラフィックカウンターのみをリセットします。個々のクライアントのカウンターには影響しません。",
|
|
||||||
"resetSuccess": "グループ {name} のトラフィックをリセットしました。",
|
|
||||||
"adjustSuccess": "{name} 内の {count} クライアントを調整しました。",
|
|
||||||
"emptyForAction": "このグループにはまだクライアントがありません。",
|
|
||||||
"deleteGroupOnly": "グループ削除 (クライアントは保持)",
|
|
||||||
"deleteClients": "グループのクライアントを削除",
|
|
||||||
"deleteClientsConfirmTitle": "{name} 内のすべてのクライアントを削除?",
|
|
||||||
"deleteClientsConfirmContent": "これは {count} クライアントとそのトラフィック記録を永久に削除します。グループラベルもクリアされます。取り消せません。",
|
|
||||||
"deleteClientsSuccess": "{count} クライアントを削除しました。",
|
|
||||||
"deleteClientsMixed": "{ok} 削除、{failed} スキップ",
|
|
||||||
"addToGroup": "クライアントを追加…",
|
|
||||||
"addToGroupTitle": "グループ「{name}」にクライアントを追加",
|
|
||||||
"addToGroupDesc": "このグループに追加するクライアントを選択してください。既存のインバウンドアタッチは保持され、グループラベルのみ変更されます。すでにこのグループにいるクライアントは表示されません。",
|
|
||||||
"addToGroupEmpty": "追加可能な他のクライアントはありません。",
|
|
||||||
"addToGroupResult": "{count} クライアントを {name} に追加しました。",
|
|
||||||
"removeFromGroup": "クライアントを削除…",
|
|
||||||
"removeFromGroupTitle": "グループ「{name}」からクライアントを削除",
|
|
||||||
"removeFromGroupDesc": "このグループから外すメンバーを選択してください。クライアント自体は保持されます (完全に削除するには「グループのクライアントを削除」を使用)。",
|
|
||||||
"removeFromGroupResult": "{count} クライアントを {name} から外しました。"
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "ノードを追加",
|
"addNode": "ノードを追加",
|
||||||
"editNode": "ノード編集",
|
"editNode": "ノード編集",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "Visão Geral",
|
"dashboard": "Visão Geral",
|
||||||
"inbounds": "Entradas",
|
"inbounds": "Entradas",
|
||||||
"clients": "Clientes",
|
"clients": "Clientes",
|
||||||
"groups": "Grupos",
|
|
||||||
"nodes": "Nós",
|
"nodes": "Nós",
|
||||||
"settings": "Configurações do Painel",
|
"settings": "Configurações do Painel",
|
||||||
"xray": "Configurações Xray",
|
"xray": "Configurações Xray",
|
||||||
"routing": "Roteamento",
|
"routing": "Roteamento",
|
||||||
"outbounds": "Saídas",
|
"outbounds": "Saídas",
|
||||||
"apiDocs": "Documentação da API",
|
|
||||||
"donate": "Doar",
|
"donate": "Doar",
|
||||||
"hosts": "Hosts",
|
"hosts": "Hosts",
|
||||||
"docs": "Documentação",
|
"docs": "Documentação",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "Swap",
|
"swap": "Swap",
|
||||||
"storage": "Armazenamento",
|
"storage": "Armazenamento",
|
||||||
"memory": "Memória",
|
"memory": "Memória",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "Parar",
|
|
||||||
"restartXray": "Reiniciar",
|
|
||||||
"updatePanel": "Atualizar painel",
|
"updatePanel": "Atualizar painel",
|
||||||
"panelUpdateDesc": "Isso atualizará o 3X-UI para a versão mais recente e reiniciará o serviço do painel.",
|
"panelUpdateDesc": "Isso atualizará o 3X-UI para a versão mais recente e reiniciará o serviço do painel.",
|
||||||
"currentPanelVersion": "Versão atual do painel",
|
"currentPanelVersion": "Versão atual do painel",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "Commit atual",
|
"currentCommit": "Commit atual",
|
||||||
"latestCommit": "Último commit",
|
"latestCommit": "Último commit",
|
||||||
"updateChannelChanged": "Canal de atualização alterado",
|
"updateChannelChanged": "Canal de atualização alterado",
|
||||||
"xrayStatusUnknown": "Desconhecido",
|
|
||||||
"xrayStatusRunning": "Em execução",
|
|
||||||
"xrayStatusStop": "Parado",
|
|
||||||
"xrayStatusError": "Erro",
|
|
||||||
"systemHistoryTitle": "Histórico do Sistema",
|
"systemHistoryTitle": "Histórico do Sistema",
|
||||||
"historyTitleCpu": "Uso da CPU",
|
"historyTitleCpu": "Uso da CPU",
|
||||||
"historyTitleMem": "Uso de Memória",
|
"historyTitleMem": "Uso de Memória",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "Excluir todos os {count} clientes de \"{remark}\"?",
|
"delAllClientsConfirmTitle": "Excluir todos os {count} clientes de \"{remark}\"?",
|
||||||
"delAllClientsConfirmContent": "Remove todos os clientes deste inbound e descarta seus registros de tráfego. O inbound em si é mantido. Esta ação não pode ser desfeita.",
|
"delAllClientsConfirmContent": "Remove todos os clientes deste inbound e descarta seus registros de tráfego. O inbound em si é mantido. Esta ação não pode ser desfeita.",
|
||||||
"attachClients": "Associar clientes a…",
|
"attachClients": "Associar clientes a…",
|
||||||
"addClientsToGroup": "Adicionar clientes ao grupo…",
|
|
||||||
"attachClientsTitle": "Associar clientes de «{remark}»",
|
"attachClientsTitle": "Associar clientes de «{remark}»",
|
||||||
"attachClientsDesc": "Associa os mesmos {count} cliente(s) (mesmo UUID/senha e tráfego compartilhado) à(s) entrada(s) selecionada(s). Permanecem nesta entrada também.",
|
"attachClientsDesc": "Associa os mesmos {count} cliente(s) (mesmo UUID/senha e tráfego compartilhado) à(s) entrada(s) selecionada(s). Permanecem nesta entrada também.",
|
||||||
"attachClientsTargets": "Entradas de destino",
|
"attachClientsTargets": "Entradas de destino",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "Os contadores de envio/recebimento de cada cliente vão a zero. Cota e expiração não são afetadas. Não é possível desfazer.",
|
"resetAllTrafficsContent": "Os contadores de envio/recebimento de cada cliente vão a zero. Cota e expiração não são afetadas. Não é possível desfazer.",
|
||||||
"deleteConfirmTitle": "Excluir o cliente {email}?",
|
"deleteConfirmTitle": "Excluir o cliente {email}?",
|
||||||
"deleteConfirmContent": "Isto remove o cliente de cada inbound associado e descarta o registro de tráfego. Não é possível desfazer.",
|
"deleteConfirmContent": "Isto remove o cliente de cada inbound associado e descarta o registro de tráfego. Não é possível desfazer.",
|
||||||
"adjustSelected": "Ajustar ({count})",
|
|
||||||
"subLinksSelected": "Links sub ({count})",
|
|
||||||
"addToGroupTitle": "Adicionar {count} cliente(s) a um grupo",
|
|
||||||
"addToGroupTooltip": "Escolha um grupo existente ou digite um novo nome. Use Ungroup para remover clientes do grupo atual.",
|
|
||||||
"groupName": "Nome do grupo",
|
|
||||||
"addToGroupSuccessToast": "{count} cliente(s) adicionado(s) a {group}",
|
|
||||||
"ungroupSuccessToast": "Grupo limpo de {count} cliente(s)",
|
|
||||||
"ungroup": "Desagrupar",
|
|
||||||
"ungroupConfirmTitle": "Remover {count} cliente(s) do grupo?",
|
|
||||||
"ungroupConfirmContent": "Limpa o rótulo de grupo de cada cliente selecionado. Os clientes em si são mantidos (use Delete para remover completamente).",
|
|
||||||
"addToGroup": "Adicionar ao grupo",
|
|
||||||
"attach": "Associar",
|
"attach": "Associar",
|
||||||
"adjust": "Ajustar",
|
"adjust": "Ajustar",
|
||||||
"subLinks": "Links de assinatura",
|
"subLinks": "Links de assinatura",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "Renova neste dia de cada mês do calendário, à meia-noite no fuso horário do painel, em vez de a cada N dias. Se o mês for curto demais para o dia escolhido, renova no último dia dele. 0 mantém o modo de intervalo em dias.",
|
"renewOnDayDesc": "Renova neste dia de cada mês do calendário, à meia-noite no fuso horário do painel, em vez de a cada N dias. Se o mês for curto demais para o dia escolhido, renova no último dia dele. 0 mantém o modo de intervalo em dias.",
|
||||||
"renewsUsed": "Renovações usadas"
|
"renewsUsed": "Renovações usadas"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "Nome",
|
|
||||||
"clientCount": "Clientes",
|
|
||||||
"totalGroups": "Total de grupos",
|
|
||||||
"totalGroupedClients": "Clientes com grupo",
|
|
||||||
"trafficUsed": "Tráfego usado",
|
|
||||||
"upload": "Envio",
|
|
||||||
"download": "Recebimento",
|
|
||||||
"totalTraffic": "Tráfego total",
|
|
||||||
"totalUpDown": "Total de envio / recebimento",
|
|
||||||
"addGroup": "Adicionar grupo",
|
|
||||||
"createSuccess": "Grupo «{name}» criado.",
|
|
||||||
"rename": "Renomear",
|
|
||||||
"renameTitle": "Renomear {name}",
|
|
||||||
"renameCollision": "Já existe um grupo chamado «{name}».",
|
|
||||||
"renameSuccess": "Grupo renomeado em {count} cliente(s).",
|
|
||||||
"deleteConfirmTitle": "Excluir o grupo {name}?",
|
|
||||||
"deleteConfirmContent": "Isso remove o grupo e limpa seu rótulo de {count} cliente(s). Os clientes em si não são excluídos.",
|
|
||||||
"deleteSuccess": "Grupo limpo de {count} cliente(s).",
|
|
||||||
"resetTraffic": "Redefinir tráfego",
|
|
||||||
"resetConfirmTitle": "Redefinir tráfego do grupo {name}?",
|
|
||||||
"resetConfirmContent": "Isso redefine apenas o contador de tráfego do grupo. Os contadores de cada cliente não são afetados.",
|
|
||||||
"resetSuccess": "Tráfego do grupo {name} redefinido.",
|
|
||||||
"adjustSuccess": "Ajustados {count} cliente(s) em {name}.",
|
|
||||||
"emptyForAction": "Este grupo ainda não tem clientes.",
|
|
||||||
"deleteGroupOnly": "Excluir grupo (manter clientes)",
|
|
||||||
"deleteClients": "Excluir clientes do grupo",
|
|
||||||
"deleteClientsConfirmTitle": "Excluir todos os clientes em {name}?",
|
|
||||||
"deleteClientsConfirmContent": "Isso remove permanentemente {count} cliente(s) junto com seus registros de tráfego. O rótulo de grupo também é limpo. Isso não pode ser desfeito.",
|
|
||||||
"deleteClientsSuccess": "Excluídos {count} cliente(s).",
|
|
||||||
"deleteClientsMixed": "{ok} excluídos, {failed} ignorados",
|
|
||||||
"addToGroup": "Adicionar clientes…",
|
|
||||||
"addToGroupTitle": "Adicionar clientes ao grupo «{name}»",
|
|
||||||
"addToGroupDesc": "Selecione clientes para adicionar a este grupo. Mantêm suas associações de entrada atuais; apenas o rótulo de grupo muda. Clientes já neste grupo não são listados.",
|
|
||||||
"addToGroupEmpty": "Não há outros clientes disponíveis para adicionar.",
|
|
||||||
"addToGroupResult": "Adicionados {count} cliente(s) a {name}.",
|
|
||||||
"removeFromGroup": "Remover clientes…",
|
|
||||||
"removeFromGroupTitle": "Remover clientes do grupo «{name}»",
|
|
||||||
"removeFromGroupDesc": "Selecione membros para remover deste grupo. Os clientes em si são mantidos (use «Excluir clientes do grupo» para removê-los por completo).",
|
|
||||||
"removeFromGroupResult": "Removidos {count} cliente(s) de {name}."
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "Adicionar nó",
|
"addNode": "Adicionar nó",
|
||||||
"editNode": "Editar nó",
|
"editNode": "Editar nó",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "Дашборд",
|
"dashboard": "Дашборд",
|
||||||
"inbounds": "Входящие",
|
"inbounds": "Входящие",
|
||||||
"clients": "Клиенты",
|
"clients": "Клиенты",
|
||||||
"groups": "Группы",
|
|
||||||
"nodes": "Узлы",
|
"nodes": "Узлы",
|
||||||
"settings": "Настройки панели",
|
"settings": "Настройки панели",
|
||||||
"xray": "Конфигурации Xray",
|
"xray": "Конфигурации Xray",
|
||||||
"routing": "Маршрутизация",
|
"routing": "Маршрутизация",
|
||||||
"outbounds": "Исходящие",
|
"outbounds": "Исходящие",
|
||||||
"apiDocs": "Документация API",
|
|
||||||
"donate": "Поддержать",
|
"donate": "Поддержать",
|
||||||
"hosts": "Хосты",
|
"hosts": "Хосты",
|
||||||
"docs": "Документация",
|
"docs": "Документация",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "Подкачка",
|
"swap": "Подкачка",
|
||||||
"storage": "Диск",
|
"storage": "Диск",
|
||||||
"memory": "Память",
|
"memory": "Память",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "Стоп",
|
|
||||||
"restartXray": "Перезапуск",
|
|
||||||
"updatePanel": "Обновить панель",
|
"updatePanel": "Обновить панель",
|
||||||
"panelUpdateDesc": "Это обновит 3X-UI до последнего релиза и перезапустит сервис панели.",
|
"panelUpdateDesc": "Это обновит 3X-UI до последнего релиза и перезапустит сервис панели.",
|
||||||
"currentPanelVersion": "Текущая версия панели",
|
"currentPanelVersion": "Текущая версия панели",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "Текущий коммит",
|
"currentCommit": "Текущий коммит",
|
||||||
"latestCommit": "Последний коммит",
|
"latestCommit": "Последний коммит",
|
||||||
"updateChannelChanged": "Канал обновления изменён",
|
"updateChannelChanged": "Канал обновления изменён",
|
||||||
"xrayStatusUnknown": "Неизвестно",
|
|
||||||
"xrayStatusRunning": "Запущен",
|
|
||||||
"xrayStatusStop": "Остановлен",
|
|
||||||
"xrayStatusError": "Ошибка",
|
|
||||||
"systemHistoryTitle": "История системы",
|
"systemHistoryTitle": "История системы",
|
||||||
"historyTitleCpu": "Загрузка ЦП",
|
"historyTitleCpu": "Загрузка ЦП",
|
||||||
"historyTitleMem": "Использование памяти",
|
"historyTitleMem": "Использование памяти",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "Удалить всех {count} клиентов из \"{remark}\"?",
|
"delAllClientsConfirmTitle": "Удалить всех {count} клиентов из \"{remark}\"?",
|
||||||
"delAllClientsConfirmContent": "Удаляет всех клиентов этого подключения и сбрасывает их записи трафика. Само подключение сохраняется. Это действие нельзя отменить.",
|
"delAllClientsConfirmContent": "Удаляет всех клиентов этого подключения и сбрасывает их записи трафика. Само подключение сохраняется. Это действие нельзя отменить.",
|
||||||
"attachClients": "Привязать клиентов к…",
|
"attachClients": "Привязать клиентов к…",
|
||||||
"addClientsToGroup": "Добавить клиентов в группу…",
|
|
||||||
"attachClientsTitle": "Привязать клиентов из «{remark}»",
|
"attachClientsTitle": "Привязать клиентов из «{remark}»",
|
||||||
"attachClientsDesc": "Привязывает тех же {count} клиент(ов) (с тем же UUID/паролем и общим трафиком) к выбранным входящим. Они остаются и на этом входящем.",
|
"attachClientsDesc": "Привязывает тех же {count} клиент(ов) (с тем же UUID/паролем и общим трафиком) к выбранным входящим. Они остаются и на этом входящем.",
|
||||||
"attachClientsTargets": "Целевые входящие",
|
"attachClientsTargets": "Целевые входящие",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "Счётчики отправки/приёма всех клиентов сбрасываются в ноль. Квоты и срок действия не затрагиваются. Это действие нельзя отменить.",
|
"resetAllTrafficsContent": "Счётчики отправки/приёма всех клиентов сбрасываются в ноль. Квоты и срок действия не затрагиваются. Это действие нельзя отменить.",
|
||||||
"deleteConfirmTitle": "Удалить клиента {email}?",
|
"deleteConfirmTitle": "Удалить клиента {email}?",
|
||||||
"deleteConfirmContent": "Клиент будет удалён из всех привязанных входящих, а его запись трафика будет уничтожена. Это действие нельзя отменить.",
|
"deleteConfirmContent": "Клиент будет удалён из всех привязанных входящих, а его запись трафика будет уничтожена. Это действие нельзя отменить.",
|
||||||
"adjustSelected": "Изменить ({count})",
|
|
||||||
"subLinksSelected": "Sub-ссылки ({count})",
|
|
||||||
"addToGroupTitle": "Добавить {count} клиент(ов) в группу",
|
|
||||||
"addToGroupTooltip": "Выберите существующую группу или введите новое имя. Используйте Ungroup, чтобы удалить клиентов из их текущей группы.",
|
|
||||||
"groupName": "Имя группы",
|
|
||||||
"addToGroupSuccessToast": "{count} клиент(ов) добавлено в {group}",
|
|
||||||
"ungroupSuccessToast": "Группа очищена у {count} клиент(ов)",
|
|
||||||
"ungroup": "Разгруппировать",
|
|
||||||
"ungroupConfirmTitle": "Удалить {count} клиент(ов) из их группы?",
|
|
||||||
"ungroupConfirmContent": "Очищает метку группы у каждого выбранного клиента. Сами клиенты сохраняются (используйте Delete для полного удаления).",
|
|
||||||
"addToGroup": "Добавить в группу",
|
|
||||||
"attach": "Привязать",
|
"attach": "Привязать",
|
||||||
"adjust": "Корректировка",
|
"adjust": "Корректировка",
|
||||||
"subLinks": "Sub-ссылки",
|
"subLinks": "Sub-ссылки",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "Продлевать этого числа каждого месяца, в полночь по часовому поясу панели, вместо интервала в днях. Если в месяце такого числа нет, продление придётся на последний день. 0 — оставить режим интервала.",
|
"renewOnDayDesc": "Продлевать этого числа каждого месяца, в полночь по часовому поясу панели, вместо интервала в днях. Если в месяце такого числа нет, продление придётся на последний день. 0 — оставить режим интервала.",
|
||||||
"renewsUsed": "Продлений израсходовано"
|
"renewsUsed": "Продлений израсходовано"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "Имя",
|
|
||||||
"clientCount": "Клиенты",
|
|
||||||
"totalGroups": "Всего групп",
|
|
||||||
"totalGroupedClients": "Клиенты с группой",
|
|
||||||
"trafficUsed": "Использованный трафик",
|
|
||||||
"upload": "Отправлено",
|
|
||||||
"download": "Получено",
|
|
||||||
"totalTraffic": "Общий трафик",
|
|
||||||
"totalUpDown": "Всего отправлено / получено",
|
|
||||||
"addGroup": "Добавить группу",
|
|
||||||
"createSuccess": "Группа «{name}» создана.",
|
|
||||||
"rename": "Переименовать",
|
|
||||||
"renameTitle": "Переименовать {name}",
|
|
||||||
"renameCollision": "Группа с именем «{name}» уже существует.",
|
|
||||||
"renameSuccess": "Группа переименована для {count} клиент(ов).",
|
|
||||||
"deleteConfirmTitle": "Удалить группу {name}?",
|
|
||||||
"deleteConfirmContent": "Это удаляет группу и очищает её метку у {count} клиент(ов). Сами клиенты не удаляются.",
|
|
||||||
"deleteSuccess": "Группа очищена у {count} клиент(ов).",
|
|
||||||
"resetTraffic": "Сбросить трафик",
|
|
||||||
"resetConfirmTitle": "Сбросить трафик группы {name}?",
|
|
||||||
"resetConfirmContent": "Это сбросит только счётчик трафика группы. Счётчики отдельных клиентов не затрагиваются.",
|
|
||||||
"resetSuccess": "Трафик группы {name} сброшен.",
|
|
||||||
"adjustSuccess": "Скорректировано {count} клиент(ов) в {name}.",
|
|
||||||
"emptyForAction": "В этой группе пока нет клиентов.",
|
|
||||||
"deleteGroupOnly": "Удалить группу (сохранить клиентов)",
|
|
||||||
"deleteClients": "Удалить клиентов группы",
|
|
||||||
"deleteClientsConfirmTitle": "Удалить всех клиентов в {name}?",
|
|
||||||
"deleteClientsConfirmContent": "Это безвозвратно удаляет {count} клиент(ов) вместе с их записями трафика. Метка группы также очищается. Это нельзя отменить.",
|
|
||||||
"deleteClientsSuccess": "Удалено {count} клиент(ов).",
|
|
||||||
"deleteClientsMixed": "{ok} удалено, {failed} пропущено",
|
|
||||||
"addToGroup": "Добавить клиентов…",
|
|
||||||
"addToGroupTitle": "Добавить клиентов в группу «{name}»",
|
|
||||||
"addToGroupDesc": "Выберите клиентов для добавления в эту группу. Существующие привязки к входящим сохраняются; меняется только метка группы. Клиенты, уже входящие в эту группу, не показываются.",
|
|
||||||
"addToGroupEmpty": "Нет других клиентов для добавления.",
|
|
||||||
"addToGroupResult": "Добавлено {count} клиент(ов) в {name}.",
|
|
||||||
"removeFromGroup": "Удалить клиентов…",
|
|
||||||
"removeFromGroupTitle": "Удалить клиентов из группы «{name}»",
|
|
||||||
"removeFromGroupDesc": "Выберите участников для удаления из этой группы. Сами клиенты сохраняются (используйте «Удалить клиентов группы» для полного удаления).",
|
|
||||||
"removeFromGroupResult": "Удалено {count} клиент(ов) из {name}."
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "Добавить узел",
|
"addNode": "Добавить узел",
|
||||||
"editNode": "Изменить узел",
|
"editNode": "Изменить узел",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "Genel Bakış",
|
"dashboard": "Genel Bakış",
|
||||||
"inbounds": "Gelen Bağlantılar",
|
"inbounds": "Gelen Bağlantılar",
|
||||||
"clients": "Kullanıcılar",
|
"clients": "Kullanıcılar",
|
||||||
"groups": "Gruplar",
|
|
||||||
"nodes": "Düğümler",
|
"nodes": "Düğümler",
|
||||||
"settings": "Panel Ayarları",
|
"settings": "Panel Ayarları",
|
||||||
"xray": "Xray Yapılandırmaları",
|
"xray": "Xray Yapılandırmaları",
|
||||||
"routing": "Yönlendirme",
|
"routing": "Yönlendirme",
|
||||||
"outbounds": "Giden Bağlantılar",
|
"outbounds": "Giden Bağlantılar",
|
||||||
"apiDocs": "API Belgeleri",
|
|
||||||
"donate": "Bağış Yap",
|
"donate": "Bağış Yap",
|
||||||
"hosts": "Host'lar",
|
"hosts": "Host'lar",
|
||||||
"docs": "Belgeler",
|
"docs": "Belgeler",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "Takas",
|
"swap": "Takas",
|
||||||
"storage": "Depolama",
|
"storage": "Depolama",
|
||||||
"memory": "RAM",
|
"memory": "RAM",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "Durdur",
|
|
||||||
"restartXray": "Yeniden Başlat",
|
|
||||||
"updatePanel": "Paneli Güncelle",
|
"updatePanel": "Paneli Güncelle",
|
||||||
"panelUpdateDesc": "Bu işlem 3X-UI'yi en son sürüme güncelleyecek ve panel servisini yeniden başlatacaktır.",
|
"panelUpdateDesc": "Bu işlem 3X-UI'yi en son sürüme güncelleyecek ve panel servisini yeniden başlatacaktır.",
|
||||||
"currentPanelVersion": "Mevcut panel sürümü",
|
"currentPanelVersion": "Mevcut panel sürümü",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "Geçerli commit",
|
"currentCommit": "Geçerli commit",
|
||||||
"latestCommit": "Son commit",
|
"latestCommit": "Son commit",
|
||||||
"updateChannelChanged": "Güncelleme kanalı değiştirildi",
|
"updateChannelChanged": "Güncelleme kanalı değiştirildi",
|
||||||
"xrayStatusUnknown": "Bilinmiyor",
|
|
||||||
"xrayStatusRunning": "Çalışıyor",
|
|
||||||
"xrayStatusStop": "Durduruldu",
|
|
||||||
"xrayStatusError": "Hata",
|
|
||||||
"systemHistoryTitle": "Sistem Geçmişi",
|
"systemHistoryTitle": "Sistem Geçmişi",
|
||||||
"historyTitleCpu": "CPU Kullanımı",
|
"historyTitleCpu": "CPU Kullanımı",
|
||||||
"historyTitleMem": "Bellek Kullanımı",
|
"historyTitleMem": "Bellek Kullanımı",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "\"{remark}\" içindeki {count} kullanıcının tamamı silinsin mi?",
|
"delAllClientsConfirmTitle": "\"{remark}\" içindeki {count} kullanıcının tamamı silinsin mi?",
|
||||||
"delAllClientsConfirmContent": "Bu gelen bağlantıya ait tüm kullanıcıları ve trafik kayıtlarını siler. Gelen bağlantı silinmez. Bu işlem geri alınamaz.",
|
"delAllClientsConfirmContent": "Bu gelen bağlantıya ait tüm kullanıcıları ve trafik kayıtlarını siler. Gelen bağlantı silinmez. Bu işlem geri alınamaz.",
|
||||||
"attachClients": "Kullanıcıları Şuna Bağla…",
|
"attachClients": "Kullanıcıları Şuna Bağla…",
|
||||||
"addClientsToGroup": "Kullanıcıları Gruba Ekle…",
|
|
||||||
"attachClientsTitle": "\"{remark}\" gelen bağlantındaki kullanıcıları bağla",
|
"attachClientsTitle": "\"{remark}\" gelen bağlantındaki kullanıcıları bağla",
|
||||||
"attachClientsDesc": "Aynı {count} kullanıcıyı (aynı UUID/parola ve paylaşılan trafik) seçilen gelen bağlantılarna bağlar. Bu gelen bağlantında da kalırlar.",
|
"attachClientsDesc": "Aynı {count} kullanıcıyı (aynı UUID/parola ve paylaşılan trafik) seçilen gelen bağlantılarna bağlar. Bu gelen bağlantında da kalırlar.",
|
||||||
"attachClientsTargets": "Hedef Gelen Bağlantılar",
|
"attachClientsTargets": "Hedef Gelen Bağlantılar",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "Her kullanıcının yükleme/indirme sayaçları sıfırlanır. Kotalar ve son kullanma tarihleri etkilenmez. Geri alınamaz.",
|
"resetAllTrafficsContent": "Her kullanıcının yükleme/indirme sayaçları sıfırlanır. Kotalar ve son kullanma tarihleri etkilenmez. Geri alınamaz.",
|
||||||
"deleteConfirmTitle": "{email} Kullanıcısı Silinsin Mi?",
|
"deleteConfirmTitle": "{email} Kullanıcısı Silinsin Mi?",
|
||||||
"deleteConfirmContent": "Bu işlem kullanıcıyı bağlı tüm gelen bağlantılarndan kaldırır ve trafik kaydını siler. Geri alınamaz.",
|
"deleteConfirmContent": "Bu işlem kullanıcıyı bağlı tüm gelen bağlantılarndan kaldırır ve trafik kaydını siler. Geri alınamaz.",
|
||||||
"adjustSelected": "Ayarla ({count})",
|
|
||||||
"subLinksSelected": "Abonelik Bağlantıları ({count})",
|
|
||||||
"addToGroupTitle": "{count} Kullanıcıyı Bir Gruba Ekle",
|
|
||||||
"addToGroupTooltip": "Mevcut bir grubu seçin veya yeni ad girin. Kullanıcıları mevcut gruplarından çıkarmak için Gruptan Çıkar'ı kullanın.",
|
|
||||||
"groupName": "Grup Adı",
|
|
||||||
"addToGroupSuccessToast": "{count} kullanıcı {group} grubuna eklendi",
|
|
||||||
"ungroupSuccessToast": "{count} kullanıcının grubu temizlendi",
|
|
||||||
"ungroup": "Gruptan Çıkar",
|
|
||||||
"ungroupConfirmTitle": "{count} Kullanıcıyı Gruptan Çıkar?",
|
|
||||||
"ungroupConfirmContent": "Seçilen her kullanıcının grup etiketini temizler. Kullanıcılar korunur (tamamen kaldırmak için Sil'i kullanın).",
|
|
||||||
"addToGroup": "Gruba Ekle",
|
|
||||||
"attach": "Bağla",
|
"attach": "Bağla",
|
||||||
"adjust": "Ayarla",
|
"adjust": "Ayarla",
|
||||||
"subLinks": "Abonelik Bağlantıları",
|
"subLinks": "Abonelik Bağlantıları",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "Her N günde bir yerine, her takvim ayının bu gününde, panel saat diliminde gece yarısı yeniler. Seçilen gün için kısa olan aylarda ayın son gününde yeniler. 0 gün aralığı modunu korur.",
|
"renewOnDayDesc": "Her N günde bir yerine, her takvim ayının bu gününde, panel saat diliminde gece yarısı yeniler. Seçilen gün için kısa olan aylarda ayın son gününde yeniler. 0 gün aralığı modunu korur.",
|
||||||
"renewsUsed": "Kullanılan yenileme"
|
"renewsUsed": "Kullanılan yenileme"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "İsim",
|
|
||||||
"clientCount": "Kullanıcılar",
|
|
||||||
"totalGroups": "Toplam grup",
|
|
||||||
"totalGroupedClients": "Grubu olan kullanıcılar",
|
|
||||||
"trafficUsed": "Kullanılan trafik",
|
|
||||||
"upload": "Yükleme",
|
|
||||||
"download": "İndirme",
|
|
||||||
"totalTraffic": "Toplam trafik",
|
|
||||||
"totalUpDown": "Toplam yükleme / indirme",
|
|
||||||
"addGroup": "Grup ekle",
|
|
||||||
"createSuccess": "«{name}» grubu oluşturuldu.",
|
|
||||||
"rename": "Yeniden adlandır",
|
|
||||||
"renameTitle": "{name} yeniden adlandır",
|
|
||||||
"renameCollision": "«{name}» adında bir grup zaten var.",
|
|
||||||
"renameSuccess": "{count} kullanıcının grubu yeniden adlandırıldı.",
|
|
||||||
"deleteConfirmTitle": "{name} Grubunu Sil?",
|
|
||||||
"deleteConfirmContent": "Bu işlem grubu siler ve etiketini {count} kullanıcıdan kaldırır. Kullanıcılar silinmez.",
|
|
||||||
"deleteSuccess": "{count} kullanıcının grubu temizlendi.",
|
|
||||||
"resetTraffic": "Trafiği Sıfırla",
|
|
||||||
"resetConfirmTitle": "{name} Grubunun Trafiğini Sıfırla?",
|
|
||||||
"resetConfirmContent": "Bu yalnızca grubun trafik sayacını sıfırlar. Tek tek kullanıcı sayaçları etkilenmez.",
|
|
||||||
"resetSuccess": "{name} grubunun trafiği sıfırlandı.",
|
|
||||||
"adjustSuccess": "{name} içinde {count} kullanıcı ayarlandı.",
|
|
||||||
"emptyForAction": "Bu grupta henüz kullanıcı yok.",
|
|
||||||
"deleteGroupOnly": "Grubu Sil (Kullanıcıları Tut)",
|
|
||||||
"deleteClients": "Gruptaki Kullanıcıları Sil",
|
|
||||||
"deleteClientsConfirmTitle": "{name} İçindeki Tüm Kullanıcıları Sil?",
|
|
||||||
"deleteClientsConfirmContent": "Bu işlem, {count} kullanıcıyı trafik kayıtlarıyla birlikte kalıcı olarak siler. Grup etiketi de temizlenir. Geri alınamaz.",
|
|
||||||
"deleteClientsSuccess": "{count} kullanıcı silindi.",
|
|
||||||
"deleteClientsMixed": "{ok} silindi, {failed} atlandı",
|
|
||||||
"addToGroup": "Kullanıcı Ekle…",
|
|
||||||
"addToGroupTitle": "«{name}» Grubuna Kullanıcı Ekle",
|
|
||||||
"addToGroupDesc": "Bu gruba eklemek için kullanıcıları seçin. Mevcut gelen bağlantılar korunur; yalnızca grup etiketi değişir. Halihazırda bu grupta olan kullanıcılar listelenmez.",
|
|
||||||
"addToGroupEmpty": "Eklenecek başka kullanıcı yok.",
|
|
||||||
"addToGroupResult": "{count} kullanıcı {name} grubuna eklendi.",
|
|
||||||
"removeFromGroup": "Kullanıcı Çıkar…",
|
|
||||||
"removeFromGroupTitle": "«{name}» Grubundan Kullanıcı Çıkar",
|
|
||||||
"removeFromGroupDesc": "Bu gruptan çıkarılacak üyeleri seçin. Kullanıcılar korunur (tamamen kaldırmak için «Gruptaki kullanıcıları sil» kullanın).",
|
|
||||||
"removeFromGroupResult": "{name} grubundan {count} kullanıcı çıkarıldı."
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "Düğüm Ekle",
|
"addNode": "Düğüm Ekle",
|
||||||
"editNode": "Düğümü Düzenle",
|
"editNode": "Düğümü Düzenle",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "Огляд",
|
"dashboard": "Огляд",
|
||||||
"inbounds": "Вхідні",
|
"inbounds": "Вхідні",
|
||||||
"clients": "Клієнти",
|
"clients": "Клієнти",
|
||||||
"groups": "Групи",
|
|
||||||
"nodes": "Вузли",
|
"nodes": "Вузли",
|
||||||
"settings": "Налаштування панелі",
|
"settings": "Налаштування панелі",
|
||||||
"xray": "Конфігурації Xray",
|
"xray": "Конфігурації Xray",
|
||||||
"routing": "Маршрутизація",
|
"routing": "Маршрутизація",
|
||||||
"outbounds": "Вихідні",
|
"outbounds": "Вихідні",
|
||||||
"apiDocs": "Документація API",
|
|
||||||
"donate": "Підтримати",
|
"donate": "Підтримати",
|
||||||
"hosts": "Хости",
|
"hosts": "Хости",
|
||||||
"docs": "Документація",
|
"docs": "Документація",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "Підкачка",
|
"swap": "Підкачка",
|
||||||
"storage": "Сховище",
|
"storage": "Сховище",
|
||||||
"memory": "Пам’ять",
|
"memory": "Пам’ять",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "Стоп",
|
|
||||||
"restartXray": "Перезапуск",
|
|
||||||
"updatePanel": "Оновити панель",
|
"updatePanel": "Оновити панель",
|
||||||
"panelUpdateDesc": "Це оновить 3X-UI до останнього релізу та перезапустить сервіс панелі.",
|
"panelUpdateDesc": "Це оновить 3X-UI до останнього релізу та перезапустить сервіс панелі.",
|
||||||
"currentPanelVersion": "Поточна версія панелі",
|
"currentPanelVersion": "Поточна версія панелі",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "Поточний коміт",
|
"currentCommit": "Поточний коміт",
|
||||||
"latestCommit": "Останній коміт",
|
"latestCommit": "Останній коміт",
|
||||||
"updateChannelChanged": "Канал оновлення змінено",
|
"updateChannelChanged": "Канал оновлення змінено",
|
||||||
"xrayStatusUnknown": "Невідомо",
|
|
||||||
"xrayStatusRunning": "Запущено",
|
|
||||||
"xrayStatusStop": "Зупинено",
|
|
||||||
"xrayStatusError": "Помилка",
|
|
||||||
"systemHistoryTitle": "Історія системи",
|
"systemHistoryTitle": "Історія системи",
|
||||||
"historyTitleCpu": "Завантаження ЦП",
|
"historyTitleCpu": "Завантаження ЦП",
|
||||||
"historyTitleMem": "Використання пам’яті",
|
"historyTitleMem": "Використання пам’яті",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "Видалити всіх {count} клієнтів із \"{remark}\"?",
|
"delAllClientsConfirmTitle": "Видалити всіх {count} клієнтів із \"{remark}\"?",
|
||||||
"delAllClientsConfirmContent": "Видаляє всіх клієнтів цього вхідного й скидає їхні записи трафіку. Сам вхідний зберігається. Цю дію не можна скасувати.",
|
"delAllClientsConfirmContent": "Видаляє всіх клієнтів цього вхідного й скидає їхні записи трафіку. Сам вхідний зберігається. Цю дію не можна скасувати.",
|
||||||
"attachClients": "Прив'язати клієнтів до…",
|
"attachClients": "Прив'язати клієнтів до…",
|
||||||
"addClientsToGroup": "Додати клієнтів до групи…",
|
|
||||||
"attachClientsTitle": "Прив'язати клієнтів з «{remark}»",
|
"attachClientsTitle": "Прив'язати клієнтів з «{remark}»",
|
||||||
"attachClientsDesc": "Прив'язує тих самих {count} клієнт(ів) (з тим самим UUID/паролем і спільним трафіком) до обраних вхідних. Вони залишаються і на цьому вхідному.",
|
"attachClientsDesc": "Прив'язує тих самих {count} клієнт(ів) (з тим самим UUID/паролем і спільним трафіком) до обраних вхідних. Вони залишаються і на цьому вхідному.",
|
||||||
"attachClientsTargets": "Цільові вхідні",
|
"attachClientsTargets": "Цільові вхідні",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "Лічильники відправлення/отримання кожного клієнта обнулюються. Квоти й термін дії не змінюються. Цю дію неможливо скасувати.",
|
"resetAllTrafficsContent": "Лічильники відправлення/отримання кожного клієнта обнулюються. Квоти й термін дії не змінюються. Цю дію неможливо скасувати.",
|
||||||
"deleteConfirmTitle": "Видалити клієнта {email}?",
|
"deleteConfirmTitle": "Видалити клієнта {email}?",
|
||||||
"deleteConfirmContent": "Клієнт буде вилучений з усіх прив'язаних вхідних, його запис трафіку буде знищено. Цю дію неможливо скасувати.",
|
"deleteConfirmContent": "Клієнт буде вилучений з усіх прив'язаних вхідних, його запис трафіку буде знищено. Цю дію неможливо скасувати.",
|
||||||
"adjustSelected": "Змінити ({count})",
|
|
||||||
"subLinksSelected": "Sub-посилання ({count})",
|
|
||||||
"addToGroupTitle": "Додати {count} клієнт(ів) до групи",
|
|
||||||
"addToGroupTooltip": "Виберіть існуючу групу або введіть нову назву. Використовуйте Ungroup, щоб вилучити клієнтів із поточної групи.",
|
|
||||||
"groupName": "Назва групи",
|
|
||||||
"addToGroupSuccessToast": "{count} клієнт(ів) додано до {group}",
|
|
||||||
"ungroupSuccessToast": "Групу очищено у {count} клієнт(ів)",
|
|
||||||
"ungroup": "Розгрупувати",
|
|
||||||
"ungroupConfirmTitle": "Видалити {count} клієнт(ів) з їхньої групи?",
|
|
||||||
"ungroupConfirmContent": "Очищує мітку групи у кожного обраного клієнта. Самі клієнти зберігаються (використовуйте Delete для повного видалення).",
|
|
||||||
"addToGroup": "Додати до групи",
|
|
||||||
"attach": "Прив'язати",
|
"attach": "Прив'язати",
|
||||||
"adjust": "Коригування",
|
"adjust": "Коригування",
|
||||||
"subLinks": "Sub-посилання",
|
"subLinks": "Sub-посилання",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "Подовжувати цього числа кожного місяця, опівночі за часовим поясом панелі, замість інтервалу в днях. Якщо в місяці такого числа немає, подовження припаде на останній день. 0 — залишити режим інтервалу.",
|
"renewOnDayDesc": "Подовжувати цього числа кожного місяця, опівночі за часовим поясом панелі, замість інтервалу в днях. Якщо в місяці такого числа немає, подовження припаде на останній день. 0 — залишити режим інтервалу.",
|
||||||
"renewsUsed": "Подовжень витрачено"
|
"renewsUsed": "Подовжень витрачено"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "Назва",
|
|
||||||
"clientCount": "Клієнти",
|
|
||||||
"totalGroups": "Всього груп",
|
|
||||||
"totalGroupedClients": "Клієнти з групою",
|
|
||||||
"trafficUsed": "Використаний трафік",
|
|
||||||
"upload": "Вивантаження",
|
|
||||||
"download": "Завантаження",
|
|
||||||
"totalTraffic": "Загальний трафік",
|
|
||||||
"totalUpDown": "Всього вивантажено / завантажено",
|
|
||||||
"addGroup": "Додати групу",
|
|
||||||
"createSuccess": "Групу «{name}» створено.",
|
|
||||||
"rename": "Перейменувати",
|
|
||||||
"renameTitle": "Перейменувати {name}",
|
|
||||||
"renameCollision": "Група з назвою «{name}» вже існує.",
|
|
||||||
"renameSuccess": "Групу перейменовано на {count} клієнт(ах).",
|
|
||||||
"deleteConfirmTitle": "Видалити групу {name}?",
|
|
||||||
"deleteConfirmContent": "Це видаляє групу й очищує її мітку у {count} клієнт(ів). Самі клієнти не видаляються.",
|
|
||||||
"deleteSuccess": "Групу очищено у {count} клієнт(ів).",
|
|
||||||
"resetTraffic": "Скинути трафік",
|
|
||||||
"resetConfirmTitle": "Скинути трафік групи {name}?",
|
|
||||||
"resetConfirmContent": "Це скине лише лічильник трафіку групи. Лічильники окремих клієнтів не змінюються.",
|
|
||||||
"resetSuccess": "Трафік групи {name} скинуто.",
|
|
||||||
"adjustSuccess": "Скориговано {count} клієнт(ів) у {name}.",
|
|
||||||
"emptyForAction": "У цій групі ще немає клієнтів.",
|
|
||||||
"deleteGroupOnly": "Видалити групу (зберегти клієнтів)",
|
|
||||||
"deleteClients": "Видалити клієнтів групи",
|
|
||||||
"deleteClientsConfirmTitle": "Видалити всіх клієнтів у {name}?",
|
|
||||||
"deleteClientsConfirmContent": "Це безповоротно видалить {count} клієнт(ів) разом з їхніми записами трафіку. Мітка групи також очищується. Дію не можна скасувати.",
|
|
||||||
"deleteClientsSuccess": "Видалено {count} клієнт(ів).",
|
|
||||||
"deleteClientsMixed": "{ok} видалено, {failed} пропущено",
|
|
||||||
"addToGroup": "Додати клієнтів…",
|
|
||||||
"addToGroupTitle": "Додати клієнтів до групи «{name}»",
|
|
||||||
"addToGroupDesc": "Виберіть клієнтів для додавання в цю групу. Існуючі прив'язки до вхідних зберігаються; змінюється лише мітка групи. Клієнти, які вже в цій групі, не відображаються.",
|
|
||||||
"addToGroupEmpty": "Немає інших клієнтів для додавання.",
|
|
||||||
"addToGroupResult": "Додано {count} клієнт(ів) до {name}.",
|
|
||||||
"removeFromGroup": "Видалити клієнтів…",
|
|
||||||
"removeFromGroupTitle": "Видалити клієнтів з групи «{name}»",
|
|
||||||
"removeFromGroupDesc": "Виберіть учасників для видалення з цієї групи. Самі клієнти зберігаються (використовуйте «Видалити клієнтів групи» для повного видалення).",
|
|
||||||
"removeFromGroupResult": "Видалено {count} клієнт(ів) з {name}."
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "Додати вузол",
|
"addNode": "Додати вузол",
|
||||||
"editNode": "Змінити вузол",
|
"editNode": "Змінити вузол",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "Trạng thái hệ thống",
|
"dashboard": "Trạng thái hệ thống",
|
||||||
"inbounds": "Inbound",
|
"inbounds": "Inbound",
|
||||||
"clients": "Khách hàng",
|
"clients": "Khách hàng",
|
||||||
"groups": "Nhóm",
|
|
||||||
"nodes": "Nút",
|
"nodes": "Nút",
|
||||||
"settings": "Cài đặt bảng điều khiển",
|
"settings": "Cài đặt bảng điều khiển",
|
||||||
"xray": "Cấu hình Xray",
|
"xray": "Cấu hình Xray",
|
||||||
"routing": "Định tuyến",
|
"routing": "Định tuyến",
|
||||||
"outbounds": "Outbound",
|
"outbounds": "Outbound",
|
||||||
"apiDocs": "Tài liệu API",
|
|
||||||
"donate": "Quyên góp",
|
"donate": "Quyên góp",
|
||||||
"hosts": "Hosts",
|
"hosts": "Hosts",
|
||||||
"docs": "Tài liệu",
|
"docs": "Tài liệu",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "Swap",
|
"swap": "Swap",
|
||||||
"storage": "Lưu trữ",
|
"storage": "Lưu trữ",
|
||||||
"memory": "Bộ nhớ",
|
"memory": "Bộ nhớ",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "Dừng",
|
|
||||||
"restartXray": "Khởi động lại",
|
|
||||||
"updatePanel": "Cập nhật Panel",
|
"updatePanel": "Cập nhật Panel",
|
||||||
"panelUpdateDesc": "Điều này sẽ cập nhật 3X-UI lên bản phát hành mới nhất và khởi động lại dịch vụ panel.",
|
"panelUpdateDesc": "Điều này sẽ cập nhật 3X-UI lên bản phát hành mới nhất và khởi động lại dịch vụ panel.",
|
||||||
"currentPanelVersion": "Phiên bản panel hiện tại",
|
"currentPanelVersion": "Phiên bản panel hiện tại",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "Commit hiện tại",
|
"currentCommit": "Commit hiện tại",
|
||||||
"latestCommit": "Commit mới nhất",
|
"latestCommit": "Commit mới nhất",
|
||||||
"updateChannelChanged": "Đã đổi kênh cập nhật",
|
"updateChannelChanged": "Đã đổi kênh cập nhật",
|
||||||
"xrayStatusUnknown": "Không xác định",
|
|
||||||
"xrayStatusRunning": "Đang chạy",
|
|
||||||
"xrayStatusStop": "Dừng",
|
|
||||||
"xrayStatusError": "Lỗi",
|
|
||||||
"systemHistoryTitle": "Lịch sử hệ thống",
|
"systemHistoryTitle": "Lịch sử hệ thống",
|
||||||
"historyTitleCpu": "Mức sử dụng CPU",
|
"historyTitleCpu": "Mức sử dụng CPU",
|
||||||
"historyTitleMem": "Mức sử dụng bộ nhớ",
|
"historyTitleMem": "Mức sử dụng bộ nhớ",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "Xóa toàn bộ {count} khách hàng khỏi \"{remark}\"?",
|
"delAllClientsConfirmTitle": "Xóa toàn bộ {count} khách hàng khỏi \"{remark}\"?",
|
||||||
"delAllClientsConfirmContent": "Xóa mọi khách hàng khỏi inbound này và hủy bản ghi lưu lượng của họ. Bản thân inbound vẫn được giữ lại. Hành động này không thể hoàn tác.",
|
"delAllClientsConfirmContent": "Xóa mọi khách hàng khỏi inbound này và hủy bản ghi lưu lượng của họ. Bản thân inbound vẫn được giữ lại. Hành động này không thể hoàn tác.",
|
||||||
"attachClients": "Gắn client vào…",
|
"attachClients": "Gắn client vào…",
|
||||||
"addClientsToGroup": "Thêm client vào nhóm…",
|
|
||||||
"attachClientsTitle": "Gắn client từ «{remark}»",
|
"attachClientsTitle": "Gắn client từ «{remark}»",
|
||||||
"attachClientsDesc": "Gắn cùng {count} client (cùng UUID/mật khẩu và lưu lượng chung) vào các inbound đã chọn. Họ vẫn ở trên inbound này.",
|
"attachClientsDesc": "Gắn cùng {count} client (cùng UUID/mật khẩu và lưu lượng chung) vào các inbound đã chọn. Họ vẫn ở trên inbound này.",
|
||||||
"attachClientsTargets": "Inbound đích",
|
"attachClientsTargets": "Inbound đích",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "Bộ đếm gửi/nhận của mỗi khách hàng về 0. Hạn mức và thời hạn không bị ảnh hưởng. Không thể hoàn tác.",
|
"resetAllTrafficsContent": "Bộ đếm gửi/nhận của mỗi khách hàng về 0. Hạn mức và thời hạn không bị ảnh hưởng. Không thể hoàn tác.",
|
||||||
"deleteConfirmTitle": "Xóa khách hàng {email}?",
|
"deleteConfirmTitle": "Xóa khách hàng {email}?",
|
||||||
"deleteConfirmContent": "Hành động này gỡ khách hàng khỏi mọi inbound đã gắn và xóa bản ghi lưu lượng. Không thể hoàn tác.",
|
"deleteConfirmContent": "Hành động này gỡ khách hàng khỏi mọi inbound đã gắn và xóa bản ghi lưu lượng. Không thể hoàn tác.",
|
||||||
"adjustSelected": "Điều chỉnh ({count})",
|
|
||||||
"subLinksSelected": "Liên kết sub ({count})",
|
|
||||||
"addToGroupTitle": "Thêm {count} client vào một nhóm",
|
|
||||||
"addToGroupTooltip": "Chọn nhóm có sẵn hoặc nhập tên mới. Dùng Ungroup để xóa client khỏi nhóm hiện tại.",
|
|
||||||
"groupName": "Tên nhóm",
|
|
||||||
"addToGroupSuccessToast": "Đã thêm {count} client vào {group}",
|
|
||||||
"ungroupSuccessToast": "Đã xóa nhóm khỏi {count} client",
|
|
||||||
"ungroup": "Bỏ nhóm",
|
|
||||||
"ungroupConfirmTitle": "Xóa {count} client khỏi nhóm của họ?",
|
|
||||||
"ungroupConfirmContent": "Xóa nhãn nhóm trên mỗi client đã chọn. Bản thân client được giữ lại (dùng Delete để xóa hoàn toàn).",
|
|
||||||
"addToGroup": "Thêm vào nhóm",
|
|
||||||
"attach": "Gắn",
|
"attach": "Gắn",
|
||||||
"adjust": "Điều chỉnh",
|
"adjust": "Điều chỉnh",
|
||||||
"subLinks": "Liên kết sub",
|
"subLinks": "Liên kết sub",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "Gia hạn vào ngày này của mỗi tháng dương lịch, lúc nửa đêm theo múi giờ của bảng điều khiển, thay vì mỗi N ngày. Tháng không có ngày đã chọn sẽ gia hạn vào ngày cuối cùng của tháng. 0 giữ nguyên chế độ khoảng cách theo ngày.",
|
"renewOnDayDesc": "Gia hạn vào ngày này của mỗi tháng dương lịch, lúc nửa đêm theo múi giờ của bảng điều khiển, thay vì mỗi N ngày. Tháng không có ngày đã chọn sẽ gia hạn vào ngày cuối cùng của tháng. 0 giữ nguyên chế độ khoảng cách theo ngày.",
|
||||||
"renewsUsed": "Số lần gia hạn đã dùng"
|
"renewsUsed": "Số lần gia hạn đã dùng"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "Tên",
|
|
||||||
"clientCount": "Client",
|
|
||||||
"totalGroups": "Tổng số nhóm",
|
|
||||||
"totalGroupedClients": "Client có nhóm",
|
|
||||||
"trafficUsed": "Lưu lượng đã dùng",
|
|
||||||
"upload": "Tải lên",
|
|
||||||
"download": "Tải xuống",
|
|
||||||
"totalTraffic": "Tổng lưu lượng",
|
|
||||||
"totalUpDown": "Tổng tải lên / tải xuống",
|
|
||||||
"addGroup": "Thêm nhóm",
|
|
||||||
"createSuccess": "Đã tạo nhóm «{name}».",
|
|
||||||
"rename": "Đổi tên",
|
|
||||||
"renameTitle": "Đổi tên {name}",
|
|
||||||
"renameCollision": "Nhóm có tên «{name}» đã tồn tại.",
|
|
||||||
"renameSuccess": "Đã đổi tên nhóm trên {count} client.",
|
|
||||||
"deleteConfirmTitle": "Xóa nhóm {name}?",
|
|
||||||
"deleteConfirmContent": "Việc này xóa nhóm và xóa nhãn khỏi {count} client. Bản thân client không bị xóa.",
|
|
||||||
"deleteSuccess": "Đã xóa nhóm khỏi {count} client.",
|
|
||||||
"resetTraffic": "Đặt lại lưu lượng",
|
|
||||||
"resetConfirmTitle": "Đặt lại lưu lượng nhóm {name}?",
|
|
||||||
"resetConfirmContent": "Việc này chỉ đặt lại bộ đếm lưu lượng của nhóm. Bộ đếm của từng client không bị ảnh hưởng.",
|
|
||||||
"resetSuccess": "Đã đặt lại lưu lượng nhóm {name}.",
|
|
||||||
"adjustSuccess": "Đã điều chỉnh {count} client trong {name}.",
|
|
||||||
"emptyForAction": "Nhóm này chưa có client.",
|
|
||||||
"deleteGroupOnly": "Xóa nhóm (giữ client)",
|
|
||||||
"deleteClients": "Xóa client trong nhóm",
|
|
||||||
"deleteClientsConfirmTitle": "Xóa tất cả client trong {name}?",
|
|
||||||
"deleteClientsConfirmContent": "Việc này xóa vĩnh viễn {count} client cùng với hồ sơ lưu lượng. Nhãn nhóm cũng được xóa. Không thể hoàn tác.",
|
|
||||||
"deleteClientsSuccess": "Đã xóa {count} client.",
|
|
||||||
"deleteClientsMixed": "{ok} đã xóa, {failed} bỏ qua",
|
|
||||||
"addToGroup": "Thêm client…",
|
|
||||||
"addToGroupTitle": "Thêm client vào nhóm «{name}»",
|
|
||||||
"addToGroupDesc": "Chọn client để thêm vào nhóm này. Giữ nguyên gắn kết inbound hiện tại; chỉ thay đổi nhãn nhóm. Client đã ở trong nhóm này sẽ không được liệt kê.",
|
|
||||||
"addToGroupEmpty": "Không có client khác để thêm.",
|
|
||||||
"addToGroupResult": "Đã thêm {count} client vào {name}.",
|
|
||||||
"removeFromGroup": "Xóa client…",
|
|
||||||
"removeFromGroupTitle": "Xóa client khỏi nhóm «{name}»",
|
|
||||||
"removeFromGroupDesc": "Chọn thành viên để xóa khỏi nhóm này. Bản thân client được giữ lại (dùng «Xóa client trong nhóm» để xóa hoàn toàn).",
|
|
||||||
"removeFromGroupResult": "Đã xóa {count} client khỏi {name}."
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "Thêm nút",
|
"addNode": "Thêm nút",
|
||||||
"editNode": "Sửa node",
|
"editNode": "Sửa node",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "系统状态",
|
"dashboard": "系统状态",
|
||||||
"inbounds": "入站",
|
"inbounds": "入站",
|
||||||
"clients": "客户端",
|
"clients": "客户端",
|
||||||
"groups": "分组",
|
|
||||||
"nodes": "节点",
|
"nodes": "节点",
|
||||||
"settings": "面板设置",
|
"settings": "面板设置",
|
||||||
"xray": "Xray 配置",
|
"xray": "Xray 配置",
|
||||||
"routing": "路由",
|
"routing": "路由",
|
||||||
"outbounds": "出站",
|
"outbounds": "出站",
|
||||||
"apiDocs": "API 文档",
|
|
||||||
"donate": "捐赠",
|
"donate": "捐赠",
|
||||||
"hosts": "主机",
|
"hosts": "主机",
|
||||||
"docs": "文档",
|
"docs": "文档",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "交换空间",
|
"swap": "交换空间",
|
||||||
"storage": "存储",
|
"storage": "存储",
|
||||||
"memory": "内存",
|
"memory": "内存",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "停止",
|
|
||||||
"restartXray": "重启",
|
|
||||||
"updatePanel": "更新面板",
|
"updatePanel": "更新面板",
|
||||||
"panelUpdateDesc": "这将把 3X-UI 更新到最新版本并重启面板服务。",
|
"panelUpdateDesc": "这将把 3X-UI 更新到最新版本并重启面板服务。",
|
||||||
"currentPanelVersion": "当前面板版本",
|
"currentPanelVersion": "当前面板版本",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "当前提交",
|
"currentCommit": "当前提交",
|
||||||
"latestCommit": "最新提交",
|
"latestCommit": "最新提交",
|
||||||
"updateChannelChanged": "更新通道已切换",
|
"updateChannelChanged": "更新通道已切换",
|
||||||
"xrayStatusUnknown": "未知",
|
|
||||||
"xrayStatusRunning": "运行中",
|
|
||||||
"xrayStatusStop": "停止",
|
|
||||||
"xrayStatusError": "错误",
|
|
||||||
"systemHistoryTitle": "系统历史",
|
"systemHistoryTitle": "系统历史",
|
||||||
"historyTitleCpu": "CPU 使用率",
|
"historyTitleCpu": "CPU 使用率",
|
||||||
"historyTitleMem": "内存使用率",
|
"historyTitleMem": "内存使用率",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "从 \"{remark}\" 中删除全部 {count} 个客户端?",
|
"delAllClientsConfirmTitle": "从 \"{remark}\" 中删除全部 {count} 个客户端?",
|
||||||
"delAllClientsConfirmContent": "从此入站中移除每个客户端并丢弃其流量记录。入站本身将保留。此操作无法撤销。",
|
"delAllClientsConfirmContent": "从此入站中移除每个客户端并丢弃其流量记录。入站本身将保留。此操作无法撤销。",
|
||||||
"attachClients": "附加客户端到…",
|
"attachClients": "附加客户端到…",
|
||||||
"addClientsToGroup": "将客户端添加到分组…",
|
|
||||||
"attachClientsTitle": "从 “{remark}” 附加客户端",
|
"attachClientsTitle": "从 “{remark}” 附加客户端",
|
||||||
"attachClientsDesc": "将相同的 {count} 个客户端(相同 UUID/密码和共享流量)附加到选定的入站。它们仍保留在此入站中。",
|
"attachClientsDesc": "将相同的 {count} 个客户端(相同 UUID/密码和共享流量)附加到选定的入站。它们仍保留在此入站中。",
|
||||||
"attachClientsTargets": "目标入站",
|
"attachClientsTargets": "目标入站",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "所有客户端的上下行计数器将归零。配额与过期时间不受影响。该操作不可撤销。",
|
"resetAllTrafficsContent": "所有客户端的上下行计数器将归零。配额与过期时间不受影响。该操作不可撤销。",
|
||||||
"deleteConfirmTitle": "删除客户端 {email}?",
|
"deleteConfirmTitle": "删除客户端 {email}?",
|
||||||
"deleteConfirmContent": "将从所有关联入站中移除该客户端并删除其流量记录。该操作不可撤销。",
|
"deleteConfirmContent": "将从所有关联入站中移除该客户端并删除其流量记录。该操作不可撤销。",
|
||||||
"adjustSelected": "调整 ({count})",
|
|
||||||
"subLinksSelected": "订阅链接 ({count})",
|
|
||||||
"addToGroupTitle": "将 {count} 个客户端添加到分组",
|
|
||||||
"addToGroupTooltip": "选择现有分组或输入新名称。使用 Ungroup 操作从当前分组移除客户端。",
|
|
||||||
"groupName": "分组名称",
|
|
||||||
"addToGroupSuccessToast": "已将 {count} 个客户端添加到 {group}",
|
|
||||||
"ungroupSuccessToast": "已清除 {count} 个客户端的分组",
|
|
||||||
"ungroup": "取消分组",
|
|
||||||
"ungroupConfirmTitle": "将 {count} 个客户端从其分组中移除?",
|
|
||||||
"ungroupConfirmContent": "清除每个选中客户端的分组标签。客户端本身保留(使用 Delete 完全移除)。",
|
|
||||||
"addToGroup": "添加到分组",
|
|
||||||
"attach": "附加",
|
"attach": "附加",
|
||||||
"adjust": "调整",
|
"adjust": "调整",
|
||||||
"subLinks": "订阅链接",
|
"subLinks": "订阅链接",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "每个自然月的这一天午夜(按面板时区)续期,而不是每 N 天续期一次。若当月没有该日期,则在当月最后一天续期。填 0 保持按天间隔模式。",
|
"renewOnDayDesc": "每个自然月的这一天午夜(按面板时区)续期,而不是每 N 天续期一次。若当月没有该日期,则在当月最后一天续期。填 0 保持按天间隔模式。",
|
||||||
"renewsUsed": "已用续期次数"
|
"renewsUsed": "已用续期次数"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "名称",
|
|
||||||
"clientCount": "客户端",
|
|
||||||
"totalGroups": "分组总数",
|
|
||||||
"totalGroupedClients": "有分组的客户端",
|
|
||||||
"trafficUsed": "已用流量",
|
|
||||||
"upload": "上传",
|
|
||||||
"download": "下载",
|
|
||||||
"totalTraffic": "总流量",
|
|
||||||
"totalUpDown": "总上传 / 下载",
|
|
||||||
"addGroup": "添加分组",
|
|
||||||
"createSuccess": "已创建分组 “{name}”。",
|
|
||||||
"rename": "重命名",
|
|
||||||
"renameTitle": "重命名 {name}",
|
|
||||||
"renameCollision": "已存在名为 “{name}” 的分组。",
|
|
||||||
"renameSuccess": "已为 {count} 个客户端重命名分组。",
|
|
||||||
"deleteConfirmTitle": "删除分组 {name}?",
|
|
||||||
"deleteConfirmContent": "这将删除分组并清除 {count} 个客户端的标签。客户端本身不会被删除。",
|
|
||||||
"deleteSuccess": "已清除 {count} 个客户端的分组。",
|
|
||||||
"resetTraffic": "重置流量",
|
|
||||||
"resetConfirmTitle": "重置分组 {name} 的流量?",
|
|
||||||
"resetConfirmContent": "这只会清零该分组的流量计数器,不影响各个客户端的计数器。",
|
|
||||||
"resetSuccess": "已重置分组 {name} 的流量。",
|
|
||||||
"adjustSuccess": "已调整 {name} 中的 {count} 个客户端。",
|
|
||||||
"emptyForAction": "此分组尚无客户端。",
|
|
||||||
"deleteGroupOnly": "删除分组(保留客户端)",
|
|
||||||
"deleteClients": "删除分组中的客户端",
|
|
||||||
"deleteClientsConfirmTitle": "删除 {name} 中的所有客户端?",
|
|
||||||
"deleteClientsConfirmContent": "这将永久删除 {count} 个客户端及其流量记录。分组标签也会被清除。此操作无法撤销。",
|
|
||||||
"deleteClientsSuccess": "已删除 {count} 个客户端。",
|
|
||||||
"deleteClientsMixed": "已删除 {ok},已跳过 {failed}",
|
|
||||||
"addToGroup": "添加客户端…",
|
|
||||||
"addToGroupTitle": "添加客户端到分组 “{name}”",
|
|
||||||
"addToGroupDesc": "选择要添加到此分组的客户端。保留其现有入站附加;仅更改分组标签。已在此分组中的客户端不会列出。",
|
|
||||||
"addToGroupEmpty": "没有其他可添加的客户端。",
|
|
||||||
"addToGroupResult": "已将 {count} 个客户端添加到 {name}。",
|
|
||||||
"removeFromGroup": "移除客户端…",
|
|
||||||
"removeFromGroupTitle": "从分组 “{name}” 移除客户端",
|
|
||||||
"removeFromGroupDesc": "选择要从此分组中移除的成员。客户端本身保留(使用 “删除分组中的客户端” 完全移除)。",
|
|
||||||
"removeFromGroupResult": "已从 {name} 移除 {count} 个客户端。"
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "添加节点",
|
"addNode": "添加节点",
|
||||||
"editNode": "编辑节点",
|
"editNode": "编辑节点",
|
||||||
|
|||||||
@@ -95,13 +95,11 @@
|
|||||||
"dashboard": "系統狀態",
|
"dashboard": "系統狀態",
|
||||||
"inbounds": "入站",
|
"inbounds": "入站",
|
||||||
"clients": "客戶端",
|
"clients": "客戶端",
|
||||||
"groups": "群組",
|
|
||||||
"nodes": "節點",
|
"nodes": "節點",
|
||||||
"settings": "面板設定",
|
"settings": "面板設定",
|
||||||
"xray": "Xray 設定",
|
"xray": "Xray 設定",
|
||||||
"routing": "路由",
|
"routing": "路由",
|
||||||
"outbounds": "出站",
|
"outbounds": "出站",
|
||||||
"apiDocs": "API 文件",
|
|
||||||
"donate": "捐贈",
|
"donate": "捐贈",
|
||||||
"hosts": "主機",
|
"hosts": "主機",
|
||||||
"docs": "文件",
|
"docs": "文件",
|
||||||
@@ -128,9 +126,6 @@
|
|||||||
"swap": "交換空間",
|
"swap": "交換空間",
|
||||||
"storage": "儲存",
|
"storage": "儲存",
|
||||||
"memory": "記憶體",
|
"memory": "記憶體",
|
||||||
"xrayStatus": "Xray",
|
|
||||||
"stopXray": "停止",
|
|
||||||
"restartXray": "重新啟動",
|
|
||||||
"updatePanel": "更新面板",
|
"updatePanel": "更新面板",
|
||||||
"panelUpdateDesc": "這將把 3X-UI 更新到最新版本並重新啟動面板服務。",
|
"panelUpdateDesc": "這將把 3X-UI 更新到最新版本並重新啟動面板服務。",
|
||||||
"currentPanelVersion": "目前面板版本",
|
"currentPanelVersion": "目前面板版本",
|
||||||
@@ -141,10 +136,6 @@
|
|||||||
"currentCommit": "目前提交",
|
"currentCommit": "目前提交",
|
||||||
"latestCommit": "最新提交",
|
"latestCommit": "最新提交",
|
||||||
"updateChannelChanged": "更新通道已切換",
|
"updateChannelChanged": "更新通道已切換",
|
||||||
"xrayStatusUnknown": "未知",
|
|
||||||
"xrayStatusRunning": "運行中",
|
|
||||||
"xrayStatusStop": "停止",
|
|
||||||
"xrayStatusError": "錯誤",
|
|
||||||
"systemHistoryTitle": "系統歷史",
|
"systemHistoryTitle": "系統歷史",
|
||||||
"historyTitleCpu": "CPU 使用率",
|
"historyTitleCpu": "CPU 使用率",
|
||||||
"historyTitleMem": "記憶體使用率",
|
"historyTitleMem": "記憶體使用率",
|
||||||
@@ -295,7 +286,6 @@
|
|||||||
"delAllClientsConfirmTitle": "從「{remark}」中刪除全部 {count} 個客戶端?",
|
"delAllClientsConfirmTitle": "從「{remark}」中刪除全部 {count} 個客戶端?",
|
||||||
"delAllClientsConfirmContent": "從此入站中移除每個客戶端並捨棄其流量記錄。入站本身將保留。此操作無法復原。",
|
"delAllClientsConfirmContent": "從此入站中移除每個客戶端並捨棄其流量記錄。入站本身將保留。此操作無法復原。",
|
||||||
"attachClients": "附加客戶端到…",
|
"attachClients": "附加客戶端到…",
|
||||||
"addClientsToGroup": "將客戶端加入群組…",
|
|
||||||
"attachClientsTitle": "從「{remark}」附加客戶端",
|
"attachClientsTitle": "從「{remark}」附加客戶端",
|
||||||
"attachClientsDesc": "將相同的 {count} 個客戶端(相同 UUID/密碼與共享流量)附加到選定入站。它們仍保留於此入站。",
|
"attachClientsDesc": "將相同的 {count} 個客戶端(相同 UUID/密碼與共享流量)附加到選定入站。它們仍保留於此入站。",
|
||||||
"attachClientsTargets": "目標入站",
|
"attachClientsTargets": "目標入站",
|
||||||
@@ -768,17 +758,6 @@
|
|||||||
"resetAllTrafficsContent": "所有客戶端的上下行計數器將歸零。配額與到期時間不受影響。此操作無法復原。",
|
"resetAllTrafficsContent": "所有客戶端的上下行計數器將歸零。配額與到期時間不受影響。此操作無法復原。",
|
||||||
"deleteConfirmTitle": "刪除客戶端 {email}?",
|
"deleteConfirmTitle": "刪除客戶端 {email}?",
|
||||||
"deleteConfirmContent": "將從所有關聯入站中移除該客戶端並刪除其流量紀錄。此操作無法復原。",
|
"deleteConfirmContent": "將從所有關聯入站中移除該客戶端並刪除其流量紀錄。此操作無法復原。",
|
||||||
"adjustSelected": "調整 ({count})",
|
|
||||||
"subLinksSelected": "訂閱連結 ({count})",
|
|
||||||
"addToGroupTitle": "將 {count} 個客戶端加入群組",
|
|
||||||
"addToGroupTooltip": "選擇現有群組或輸入新名稱。使用 Ungroup 操作從當前群組移除客戶端。",
|
|
||||||
"groupName": "群組名稱",
|
|
||||||
"addToGroupSuccessToast": "已將 {count} 個客戶端加入 {group}",
|
|
||||||
"ungroupSuccessToast": "已清除 {count} 個客戶端的群組",
|
|
||||||
"ungroup": "取消群組",
|
|
||||||
"ungroupConfirmTitle": "將 {count} 個客戶端從其群組中移除?",
|
|
||||||
"ungroupConfirmContent": "清除每個選取客戶端的群組標籤。客戶端本身保留(用 Delete 完全移除)。",
|
|
||||||
"addToGroup": "加入群組",
|
|
||||||
"attach": "附加",
|
"attach": "附加",
|
||||||
"adjust": "調整",
|
"adjust": "調整",
|
||||||
"subLinks": "訂閱連結",
|
"subLinks": "訂閱連結",
|
||||||
@@ -880,47 +859,6 @@
|
|||||||
"renewOnDayDesc": "每個自然月的這一天午夜(依面板時區)續期,而不是每 N 天續期一次。若當月沒有該日期,則在當月最後一天續期。填 0 保持按天間隔模式。",
|
"renewOnDayDesc": "每個自然月的這一天午夜(依面板時區)續期,而不是每 N 天續期一次。若當月沒有該日期,則在當月最後一天續期。填 0 保持按天間隔模式。",
|
||||||
"renewsUsed": "已用續期次數"
|
"renewsUsed": "已用續期次數"
|
||||||
},
|
},
|
||||||
"groups": {
|
|
||||||
"name": "名稱",
|
|
||||||
"clientCount": "客戶端",
|
|
||||||
"totalGroups": "群組總數",
|
|
||||||
"totalGroupedClients": "有群組的客戶端",
|
|
||||||
"trafficUsed": "已用流量",
|
|
||||||
"upload": "上傳",
|
|
||||||
"download": "下載",
|
|
||||||
"totalTraffic": "總流量",
|
|
||||||
"totalUpDown": "總上傳 / 下載",
|
|
||||||
"addGroup": "新增群組",
|
|
||||||
"createSuccess": "已建立群組「{name}」。",
|
|
||||||
"rename": "重新命名",
|
|
||||||
"renameTitle": "重新命名 {name}",
|
|
||||||
"renameCollision": "已存在名為「{name}」的群組。",
|
|
||||||
"renameSuccess": "已為 {count} 個客戶端重新命名群組。",
|
|
||||||
"deleteConfirmTitle": "刪除群組 {name}?",
|
|
||||||
"deleteConfirmContent": "這將刪除群組並清除 {count} 個客戶端的標籤。客戶端本身不會被刪除。",
|
|
||||||
"deleteSuccess": "已清除 {count} 個客戶端的群組。",
|
|
||||||
"resetTraffic": "重置流量",
|
|
||||||
"resetConfirmTitle": "重置群組 {name} 的流量?",
|
|
||||||
"resetConfirmContent": "這只會將此群組的流量計數器歸零,不影響各個客戶端的計數器。",
|
|
||||||
"resetSuccess": "已重置群組 {name} 的流量。",
|
|
||||||
"adjustSuccess": "已調整 {name} 中的 {count} 個客戶端。",
|
|
||||||
"emptyForAction": "此群組尚無客戶端。",
|
|
||||||
"deleteGroupOnly": "刪除群組(保留客戶端)",
|
|
||||||
"deleteClients": "刪除群組中的客戶端",
|
|
||||||
"deleteClientsConfirmTitle": "刪除 {name} 中的所有客戶端?",
|
|
||||||
"deleteClientsConfirmContent": "這將永久刪除 {count} 個客戶端及其流量記錄。群組標籤亦會被清除。此操作無法復原。",
|
|
||||||
"deleteClientsSuccess": "已刪除 {count} 個客戶端。",
|
|
||||||
"deleteClientsMixed": "已刪除 {ok},已略過 {failed}",
|
|
||||||
"addToGroup": "新增客戶端…",
|
|
||||||
"addToGroupTitle": "將客戶端加入群組「{name}」",
|
|
||||||
"addToGroupDesc": "選擇要加入此群組的客戶端。保留其現有入站附加;僅更改群組標籤。已在此群組中的客戶端不會列出。",
|
|
||||||
"addToGroupEmpty": "沒有其他可加入的客戶端。",
|
|
||||||
"addToGroupResult": "已將 {count} 個客戶端加入 {name}。",
|
|
||||||
"removeFromGroup": "移除客戶端…",
|
|
||||||
"removeFromGroupTitle": "從群組「{name}」移除客戶端",
|
|
||||||
"removeFromGroupDesc": "選擇要從此群組移除的成員。客戶端本身保留(用「刪除群組中的客戶端」完全移除)。",
|
|
||||||
"removeFromGroupResult": "已從 {name} 移除 {count} 個客戶端。"
|
|
||||||
},
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"addNode": "新增節點",
|
"addNode": "新增節點",
|
||||||
"editNode": "編輯節點",
|
"editNode": "編輯節點",
|
||||||
|
|||||||
Reference in New Issue
Block a user