mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 03:32:00 +08:00
feat: 新增 frp 远程访问扩展
This commit is contained in:
@@ -24,6 +24,8 @@ import type {
|
||||
GostcConfigUpdate,
|
||||
EasytierConfig,
|
||||
EasytierConfigUpdate,
|
||||
FrpcConfig,
|
||||
FrpcConfigUpdate,
|
||||
WebConfigResponse,
|
||||
WebConfigUpdate,
|
||||
} from '@/types/generated'
|
||||
@@ -159,6 +161,12 @@ export const extensionsApi = {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
|
||||
updateFrpc: (config: FrpcConfigUpdate) =>
|
||||
request<FrpcConfig>('/extensions/frpc/config', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
}
|
||||
|
||||
export interface RustDeskConfigResponse {
|
||||
|
||||
@@ -522,6 +522,7 @@ export default {
|
||||
extRustdeskSubtitle: 'Remote graphical access via RustDesk',
|
||||
extRtspSubtitle: 'Provide an RTSP video stream for external clients',
|
||||
extRemoteAccessSubtitle: 'Remote access through NAT-traversal services',
|
||||
extFrpcSubtitle: 'NAT traversal through the FRP client',
|
||||
aboutDesc: 'Open and Lightweight IP-KVM Solution',
|
||||
deviceInfo: 'Device Info',
|
||||
deviceInfoDesc: 'Host system information',
|
||||
@@ -956,7 +957,7 @@ export default {
|
||||
binaryNotFound: '{path} not found, please install the required program',
|
||||
remoteAccess: {
|
||||
title: 'Remote Access',
|
||||
desc: 'GOSTC NAT traversal and Easytier networking',
|
||||
desc: 'GOSTC/FRPC NAT traversal and Easytier networking',
|
||||
},
|
||||
ttyd: {
|
||||
title: 'Ttyd Web Terminal',
|
||||
@@ -987,6 +988,33 @@ export default {
|
||||
virtualIp: 'Virtual IP',
|
||||
virtualIpHint: 'Leave empty for DHCP, or specify with CIDR (e.g., 10.0.0.1/24)',
|
||||
},
|
||||
frpc: {
|
||||
title: 'FRPC NAT Traversal',
|
||||
desc: 'Connect to an frps server through the FRP client',
|
||||
quickConfig: 'Quick Config',
|
||||
fullConfig: 'Full Config',
|
||||
fullConfigHint: 'Paste the provider TOML configuration file here',
|
||||
fullConfigRequired: 'Enter the full frpc.toml configuration',
|
||||
proxyType: 'Proxy Type',
|
||||
proxyName: 'Proxy Name',
|
||||
proxyNamePlaceholder: 'one-kvm-ssh',
|
||||
proxyNameRequired: 'Enter the FRPC proxy name',
|
||||
serverAddr: 'Server Address',
|
||||
serverAddrPlaceholder: 'frps.example.com',
|
||||
serverAddrRequired: 'Enter the FRPC server address',
|
||||
serverPort: 'Server Port',
|
||||
token: 'Token',
|
||||
tokenRequired: 'Enter the FRPC token',
|
||||
localIp: 'Local Address',
|
||||
localIpRequired: 'Enter the FRPC local address',
|
||||
localPort: 'Local Port',
|
||||
remotePort: 'Remote Port',
|
||||
remotePortRequired: 'TCP/UDP proxies require a remote port',
|
||||
customDomain: 'Custom Domain',
|
||||
customDomainPlaceholder: 'kvm.example.com',
|
||||
secretKey: 'Secret Key',
|
||||
tls: 'Enable TLS',
|
||||
},
|
||||
rustdesk: {
|
||||
title: 'RustDesk Remote',
|
||||
desc: 'Remote access via RustDesk client',
|
||||
|
||||
@@ -521,6 +521,7 @@ export default {
|
||||
extRustdeskSubtitle: '通过 RustDesk 实现远程图形访问',
|
||||
extRtspSubtitle: '提供 RTSP 视频流以供其他客户端拉流',
|
||||
extRemoteAccessSubtitle: '通过内网穿透服务实现远程访问',
|
||||
extFrpcSubtitle: '通过 FRP 客户端实现内网穿透',
|
||||
aboutDesc: '开放轻量的 IP-KVM 解决方案',
|
||||
deviceInfo: '设备信息',
|
||||
deviceInfoDesc: '主机系统信息',
|
||||
@@ -955,7 +956,7 @@ export default {
|
||||
binaryNotFound: '未找到 {path},请先安装对应程序',
|
||||
remoteAccess: {
|
||||
title: '远程访问',
|
||||
desc: 'GOSTC 内网穿透与 Easytier 组网',
|
||||
desc: 'GOSTC/FRPC 内网穿透与 Easytier 组网',
|
||||
},
|
||||
ttyd: {
|
||||
title: 'Ttyd 网页终端',
|
||||
@@ -986,6 +987,33 @@ export default {
|
||||
virtualIp: '虚拟 IP',
|
||||
virtualIpHint: '留空则自动分配,手动指定需包含网段(如 10.0.0.1/24)',
|
||||
},
|
||||
frpc: {
|
||||
title: 'FRPC 内网穿透',
|
||||
desc: '通过 FRP 客户端连接 frps 服务',
|
||||
quickConfig: '快速配置',
|
||||
fullConfig: '完整配置',
|
||||
fullConfigHint: '可在此粘贴供应商 TOML 配置文件',
|
||||
fullConfigRequired: '请填写完整 frpc.toml 配置',
|
||||
proxyType: '代理类型',
|
||||
proxyName: '代理名称',
|
||||
proxyNamePlaceholder: 'one-kvm-ssh',
|
||||
proxyNameRequired: '请填写 FRPC 代理名称',
|
||||
serverAddr: '服务器地址',
|
||||
serverAddrPlaceholder: 'frps.example.com',
|
||||
serverAddrRequired: '请填写 FRPC 服务器地址',
|
||||
serverPort: '服务器端口',
|
||||
token: '认证令牌',
|
||||
tokenRequired: '请填写 FRPC 认证令牌',
|
||||
localIp: '本地地址',
|
||||
localIpRequired: '请填写 FRPC 本地地址',
|
||||
localPort: '本地端口',
|
||||
remotePort: '远程端口',
|
||||
remotePortRequired: 'TCP/UDP 代理需要填写远程端口',
|
||||
customDomain: '自定义域名',
|
||||
customDomainPlaceholder: 'kvm.example.com',
|
||||
secretKey: '访问密钥',
|
||||
tls: '启用 TLS',
|
||||
},
|
||||
rustdesk: {
|
||||
title: 'RustDesk 远程',
|
||||
desc: '使用 RustDesk 客户端进行远程访问',
|
||||
|
||||
@@ -175,10 +175,43 @@ export interface EasytierConfig {
|
||||
virtual_ip?: string;
|
||||
}
|
||||
|
||||
export enum FrpProxyType {
|
||||
Tcp = "tcp",
|
||||
Udp = "udp",
|
||||
Http = "http",
|
||||
Https = "https",
|
||||
Stcp = "stcp",
|
||||
Sudp = "sudp",
|
||||
Xtcp = "xtcp",
|
||||
}
|
||||
|
||||
export enum FrpcConfigMode {
|
||||
Quick = "quick",
|
||||
Full = "full",
|
||||
}
|
||||
|
||||
export interface FrpcConfig {
|
||||
enabled: boolean;
|
||||
config_mode: FrpcConfigMode;
|
||||
proxy_name: string;
|
||||
proxy_type: FrpProxyType;
|
||||
server_addr: string;
|
||||
server_port: number;
|
||||
token: string;
|
||||
local_ip: string;
|
||||
local_port: number;
|
||||
remote_port?: number;
|
||||
custom_domain?: string;
|
||||
secret_key: string;
|
||||
tls: boolean;
|
||||
custom_toml: string;
|
||||
}
|
||||
|
||||
export interface ExtensionsConfig {
|
||||
ttyd: TtydConfig;
|
||||
gostc: GostcConfig;
|
||||
easytier: EasytierConfig;
|
||||
frpc: FrpcConfig;
|
||||
}
|
||||
|
||||
export interface RustDeskConfig {
|
||||
@@ -277,6 +310,23 @@ export interface EasytierConfigUpdate {
|
||||
virtual_ip?: string;
|
||||
}
|
||||
|
||||
export interface FrpcConfigUpdate {
|
||||
enabled?: boolean;
|
||||
config_mode?: FrpcConfigMode;
|
||||
proxy_name?: string;
|
||||
proxy_type?: FrpProxyType;
|
||||
server_addr?: string;
|
||||
server_port?: number;
|
||||
token?: string;
|
||||
local_ip?: string;
|
||||
local_port?: number;
|
||||
remote_port?: number;
|
||||
custom_domain?: string;
|
||||
secret_key?: string;
|
||||
tls?: boolean;
|
||||
custom_toml?: string;
|
||||
}
|
||||
|
||||
export type ExtensionStatus =
|
||||
| { state: "unavailable", data?: undefined }
|
||||
| { state: "stopped", data?: undefined }
|
||||
@@ -299,6 +349,7 @@ export enum ExtensionId {
|
||||
Ttyd = "ttyd",
|
||||
Gostc = "gostc",
|
||||
Easytier = "easytier",
|
||||
Frpc = "frpc",
|
||||
}
|
||||
|
||||
export interface ExtensionLogs {
|
||||
@@ -318,10 +369,17 @@ export interface GostcInfo {
|
||||
config: GostcConfig;
|
||||
}
|
||||
|
||||
export interface FrpcInfo {
|
||||
available: boolean;
|
||||
status: ExtensionStatus;
|
||||
config: FrpcConfig;
|
||||
}
|
||||
|
||||
export interface ExtensionsStatus {
|
||||
ttyd: TtydInfo;
|
||||
gostc: GostcInfo;
|
||||
easytier: EasytierInfo;
|
||||
frpc: FrpcInfo;
|
||||
}
|
||||
|
||||
export interface GostcConfigUpdate {
|
||||
@@ -597,4 +655,3 @@ export enum CanonicalKey {
|
||||
AltRight = "AltRight",
|
||||
MetaRight = "MetaRight",
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import type {
|
||||
OtgHidProfile,
|
||||
OtgHidFunctions,
|
||||
} from '@/types/generated'
|
||||
import { FrpProxyType, FrpcConfigMode } from '@/types/generated'
|
||||
import { formatFpsLabel, toConfigFps } from '@/lib/fps'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { getVideoFormatState } from '@/lib/video-format-support'
|
||||
@@ -218,6 +219,7 @@ function selectSection(id: string) {
|
||||
function normalizeSettingsSection(value: unknown): SettingsSectionId | null {
|
||||
if (typeof value !== 'string') return null
|
||||
if (value === 'access-control') return 'account'
|
||||
if (value === 'ext-frpc') return 'ext-remote-access'
|
||||
return isSettingsSectionId(value) ? value : null
|
||||
}
|
||||
|
||||
@@ -315,11 +317,13 @@ const extensionLogs = ref<Record<string, string[]>>({
|
||||
ttyd: [],
|
||||
gostc: [],
|
||||
easytier: [],
|
||||
frpc: [],
|
||||
})
|
||||
const showLogs = ref<Record<string, boolean>>({
|
||||
ttyd: false,
|
||||
gostc: false,
|
||||
easytier: false,
|
||||
frpc: false,
|
||||
})
|
||||
|
||||
const showTerminalDialog = ref(false)
|
||||
@@ -328,6 +332,22 @@ const extConfig = ref({
|
||||
ttyd: { enabled: false, shell: '/bin/bash' },
|
||||
gostc: { enabled: false, addr: '', key: '', tls: true },
|
||||
easytier: { enabled: false, network_name: '', network_secret: '', peer_urls: [] as string[], virtual_ip: '' },
|
||||
frpc: {
|
||||
enabled: false,
|
||||
config_mode: FrpcConfigMode.Quick,
|
||||
proxy_name: '',
|
||||
proxy_type: FrpProxyType.Tcp,
|
||||
server_addr: '',
|
||||
server_port: 7000,
|
||||
token: '',
|
||||
local_ip: '127.0.0.1',
|
||||
local_port: 22,
|
||||
remote_port: undefined as number | undefined,
|
||||
custom_domain: '',
|
||||
secret_key: '',
|
||||
tls: true,
|
||||
custom_toml: '',
|
||||
},
|
||||
})
|
||||
|
||||
const gostcValidationMessage = computed(() => {
|
||||
@@ -341,6 +361,25 @@ const easytierValidationMessage = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
const frpcRemotePortRequired = computed(() => ['tcp', 'udp'].includes(extConfig.value.frpc.proxy_type))
|
||||
const showFrpcRemotePort = computed(() => ['tcp', 'udp', 'stcp', 'sudp', 'xtcp'].includes(extConfig.value.frpc.proxy_type))
|
||||
const showFrpcCustomDomain = computed(() => ['http', 'https'].includes(extConfig.value.frpc.proxy_type))
|
||||
const showFrpcSecretKey = computed(() => ['stcp', 'sudp', 'xtcp'].includes(extConfig.value.frpc.proxy_type))
|
||||
const frpcQuickMode = computed(() => extConfig.value.frpc.config_mode === FrpcConfigMode.Quick)
|
||||
|
||||
const frpcValidationMessage = computed(() => {
|
||||
if (extConfig.value.frpc.config_mode === FrpcConfigMode.Full) {
|
||||
if (!extConfig.value.frpc.custom_toml?.trim()) return t('extensions.frpc.fullConfigRequired')
|
||||
return ''
|
||||
}
|
||||
if (!extConfig.value.frpc.proxy_name?.trim()) return t('extensions.frpc.proxyNameRequired')
|
||||
if (!extConfig.value.frpc.server_addr?.trim()) return t('extensions.frpc.serverAddrRequired')
|
||||
if (!extConfig.value.frpc.token) return t('extensions.frpc.tokenRequired')
|
||||
if (!extConfig.value.frpc.local_ip?.trim()) return t('extensions.frpc.localIpRequired')
|
||||
if (frpcRemotePortRequired.value && !extConfig.value.frpc.remote_port) return t('extensions.frpc.remotePortRequired')
|
||||
return ''
|
||||
})
|
||||
|
||||
const rustdeskConfig = ref<RustDeskConfigResponse | null>(null)
|
||||
const rustdeskStatus = ref<RustDeskStatusResponse | null>(null)
|
||||
const rustdeskPassword = ref<RustDeskPasswordResponse | null>(null)
|
||||
@@ -1373,6 +1412,23 @@ async function loadExtensions() {
|
||||
peer_urls: easytier.peer_urls || [],
|
||||
virtual_ip: easytier.virtual_ip || '',
|
||||
}
|
||||
const frpc = extensions.value.frpc.config
|
||||
extConfig.value.frpc = {
|
||||
enabled: frpc.enabled,
|
||||
config_mode: frpc.config_mode || FrpcConfigMode.Quick,
|
||||
proxy_name: frpc.proxy_name,
|
||||
proxy_type: frpc.proxy_type,
|
||||
server_addr: frpc.server_addr,
|
||||
server_port: frpc.server_port,
|
||||
token: frpc.token,
|
||||
local_ip: frpc.local_ip,
|
||||
local_port: frpc.local_port,
|
||||
remote_port: frpc.remote_port,
|
||||
custom_domain: frpc.custom_domain || '',
|
||||
secret_key: frpc.secret_key,
|
||||
tls: frpc.tls,
|
||||
custom_toml: frpc.custom_toml || '',
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} finally {
|
||||
@@ -1380,8 +1436,11 @@ async function loadExtensions() {
|
||||
}
|
||||
}
|
||||
|
||||
async function startExtension(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
if ((id === 'gostc' || id === 'easytier') && !validateExtensionConfig(id)) return
|
||||
type ExtensionConfigId = 'ttyd' | 'gostc' | 'easytier' | 'frpc'
|
||||
type ValidatedExtensionConfigId = Exclude<ExtensionConfigId, 'ttyd'>
|
||||
|
||||
async function startExtension(id: ExtensionConfigId) {
|
||||
if (id !== 'ttyd' && !validateExtensionConfig(id)) return
|
||||
|
||||
try {
|
||||
await extensionsApi.start(id)
|
||||
@@ -1390,7 +1449,7 @@ async function startExtension(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
}
|
||||
}
|
||||
|
||||
async function stopExtension(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
async function stopExtension(id: ExtensionConfigId) {
|
||||
try {
|
||||
await extensionsApi.stop(id)
|
||||
await loadExtensions()
|
||||
@@ -1398,7 +1457,7 @@ async function stopExtension(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshExtensionLogs(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
async function refreshExtensionLogs(id: ExtensionConfigId) {
|
||||
try {
|
||||
const result = await extensionsApi.logs(id, 100)
|
||||
extensionLogs.value[id] = result.logs
|
||||
@@ -1406,8 +1465,12 @@ async function refreshExtensionLogs(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveExtensionConfig(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
if ((id === 'gostc' || id === 'easytier') && extConfig.value[id].enabled && !validateExtensionConfig(id)) return
|
||||
async function saveExtensionConfig(id: ExtensionConfigId) {
|
||||
if (id !== 'ttyd') {
|
||||
const shouldValidate = extConfig.value[id].enabled
|
||||
|| (id === 'frpc' && extConfig.value.frpc.config_mode === FrpcConfigMode.Full)
|
||||
if (shouldValidate && !validateExtensionConfig(id)) return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -1417,6 +1480,14 @@ async function saveExtensionConfig(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
await extensionsApi.updateGostc(extConfig.value.gostc)
|
||||
} else if (id === 'easytier') {
|
||||
await extensionsApi.updateEasytier(extConfig.value.easytier)
|
||||
} else if (id === 'frpc') {
|
||||
const frpc = extConfig.value.frpc
|
||||
await extensionsApi.updateFrpc({
|
||||
...frpc,
|
||||
remote_port: frpcQuickMode.value && showFrpcRemotePort.value ? frpc.remote_port : undefined,
|
||||
custom_domain: frpcQuickMode.value && showFrpcCustomDomain.value ? frpc.custom_domain || undefined : undefined,
|
||||
secret_key: frpcQuickMode.value && showFrpcSecretKey.value ? frpc.secret_key : '',
|
||||
})
|
||||
}
|
||||
await loadExtensions()
|
||||
saved.value = true
|
||||
@@ -1662,10 +1733,15 @@ function showValidationError(message: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
function validateExtensionConfig(id: 'gostc' | 'easytier'): boolean {
|
||||
const message = id === 'gostc'
|
||||
? gostcValidationMessage.value
|
||||
: easytierValidationMessage.value
|
||||
function validateExtensionConfig(id: ValidatedExtensionConfigId): boolean {
|
||||
let message = ''
|
||||
if (id === 'gostc') {
|
||||
message = gostcValidationMessage.value
|
||||
} else if (id === 'easytier') {
|
||||
message = easytierValidationMessage.value
|
||||
} else {
|
||||
message = frpcValidationMessage.value
|
||||
}
|
||||
|
||||
return !message || showValidationError(message)
|
||||
}
|
||||
@@ -3982,6 +4058,183 @@ watch(isWindows, () => {
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- FRPC -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<CardTitle>{{ t('extensions.frpc.title') }}</CardTitle>
|
||||
<CardDescription>{{ t('extensions.frpc.desc') }}</CardDescription>
|
||||
</div>
|
||||
<Badge :variant="extensions?.frpc?.available ? 'default' : 'destructive'">
|
||||
{{ extensions?.frpc?.available ? t('extensions.available') : t('extensions.unavailable') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div v-if="!extensions?.frpc?.available" class="text-sm text-muted-foreground bg-muted p-3 rounded-md">
|
||||
{{ t('extensions.binaryNotFound', { path: isWindows ? 'frpc.exe' : '/usr/bin/frpc' }) }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div :class="['w-2 h-2 rounded-full', getExtStatusClass(extensions?.frpc?.status)]" />
|
||||
<span class="text-sm">{{ getExtStatusText(extensions?.frpc?.status) }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
v-if="!isExtRunning(extensions?.frpc?.status)"
|
||||
size="sm"
|
||||
@click="startExtension('frpc')"
|
||||
:disabled="extensionsLoading || !!frpcValidationMessage"
|
||||
>
|
||||
<Play class="h-4 w-4 mr-1" />
|
||||
{{ t('extensions.start') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="stopExtension('frpc')"
|
||||
:disabled="extensionsLoading"
|
||||
>
|
||||
<Square class="h-4 w-4 mr-1" />
|
||||
{{ t('extensions.stop') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="grid gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||
<Switch v-model="extConfig.frpc.enabled" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 rounded-md bg-muted p-1">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'rounded-sm px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
frpcQuickMode ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
||||
]"
|
||||
:disabled="isExtRunning(extensions?.frpc?.status)"
|
||||
@click="extConfig.frpc.config_mode = FrpcConfigMode.Quick"
|
||||
>
|
||||
{{ t('extensions.frpc.quickConfig') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'rounded-sm px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
!frpcQuickMode ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
||||
]"
|
||||
:disabled="isExtRunning(extensions?.frpc?.status)"
|
||||
@click="extConfig.frpc.config_mode = FrpcConfigMode.Full"
|
||||
>
|
||||
{{ t('extensions.frpc.fullConfig') }}
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="frpcQuickMode">
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.proxyType') }}</Label>
|
||||
<div class="sm:col-span-3">
|
||||
<RadioGroup v-model="extConfig.frpc.proxy_type" class="flex flex-wrap gap-4" :disabled="isExtRunning(extensions?.frpc?.status)">
|
||||
<div v-for="type in ['tcp', 'udp', 'http', 'https', 'stcp', 'sudp', 'xtcp']" :key="type" class="flex items-center space-x-2">
|
||||
<RadioGroupItem :value="type" :id="`frpc-${type}`" />
|
||||
<Label :for="`frpc-${type}`" class="cursor-pointer uppercase">{{ type }}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.proxyName') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input v-model="extConfig.frpc.proxy_name" :placeholder="t('extensions.frpc.proxyNamePlaceholder')" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
<p v-if="extConfig.frpc.enabled && !extConfig.frpc.proxy_name?.trim()" class="text-xs text-destructive">{{ t('extensions.frpc.proxyNameRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.serverAddr') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input v-model="extConfig.frpc.server_addr" :placeholder="t('extensions.frpc.serverAddrPlaceholder')" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
<p v-if="extConfig.frpc.enabled && !extConfig.frpc.server_addr?.trim()" class="text-xs text-destructive">{{ t('extensions.frpc.serverAddrRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.serverPort') }}</Label>
|
||||
<Input v-model.number="extConfig.frpc.server_port" class="sm:col-span-3" type="number" min="1" max="65535" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.token') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input v-model="extConfig.frpc.token" type="password" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
<p v-if="extConfig.frpc.enabled && !extConfig.frpc.token" class="text-xs text-destructive">{{ t('extensions.frpc.tokenRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.localIp') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input v-model="extConfig.frpc.local_ip" placeholder="127.0.0.1" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
<p v-if="extConfig.frpc.enabled && !extConfig.frpc.local_ip?.trim()" class="text-xs text-destructive">{{ t('extensions.frpc.localIpRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.localPort') }}</Label>
|
||||
<Input v-model.number="extConfig.frpc.local_port" class="sm:col-span-3" type="number" min="1" max="65535" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
<div v-if="showFrpcRemotePort" class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.remotePort') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input v-model.number="extConfig.frpc.remote_port" type="number" min="1" max="65535" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
<p v-if="extConfig.frpc.enabled && frpcRemotePortRequired && !extConfig.frpc.remote_port" class="text-xs text-destructive">{{ t('extensions.frpc.remotePortRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showFrpcCustomDomain" class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.customDomain') }}</Label>
|
||||
<Input v-model="extConfig.frpc.custom_domain" class="sm:col-span-3" :placeholder="t('extensions.frpc.customDomainPlaceholder')" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
<div v-if="showFrpcSecretKey" class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.secretKey') }}</Label>
|
||||
<Input v-model="extConfig.frpc.secret_key" class="sm:col-span-3" type="password" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.tls') }}</Label>
|
||||
<div class="sm:col-span-3">
|
||||
<Switch v-model="extConfig.frpc.tls" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="space-y-1">
|
||||
<Textarea
|
||||
v-model="extConfig.frpc.custom_toml"
|
||||
class="min-h-[300px] font-mono text-xs"
|
||||
spellcheck="false"
|
||||
:disabled="isExtRunning(extensions?.frpc?.status)"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">{{ t('extensions.frpc.fullConfigHint') }}</p>
|
||||
<p v-if="!extConfig.frpc.custom_toml?.trim()" class="text-xs text-destructive">{{ t('extensions.frpc.fullConfigRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<button type="button" @click="showLogs.frpc = !showLogs.frpc; if (showLogs.frpc) refreshExtensionLogs('frpc')" class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
|
||||
<ChevronRight :class="['h-4 w-4 transition-transform', showLogs.frpc ? 'rotate-90' : '']" />
|
||||
{{ t('extensions.viewLogs') }}
|
||||
</button>
|
||||
<div v-if="showLogs.frpc" class="space-y-2">
|
||||
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.frpc || []).join('\n') || t('extensions.noLogs') }}</pre>
|
||||
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('frpc')">
|
||||
<RefreshCw class="h-3 w-3 mr-1" />
|
||||
{{ t('common.refresh') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div v-if="extensions?.frpc?.available" class="flex justify-end">
|
||||
<Button :disabled="loading || isExtRunning(extensions?.frpc?.status)" @click="saveExtensionConfig('frpc')">
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RTSP Section -->
|
||||
|
||||
Reference in New Issue
Block a user