@@ -322,6 +322,7 @@ export const api = {
|
||||
deleteFrpProxy: (id: number) => request<void>(`/api/frpmulti/proxies/${id}`, { method: 'DELETE' }),
|
||||
startFrp: () => request<FrpStatus>('/api/frpmulti/start', { method: 'POST' }),
|
||||
stopFrp: () => request<FrpStatus>('/api/frpmulti/stop', { method: 'POST' }),
|
||||
restartFrp: () => request<FrpStatus>('/api/frpmulti/restart', { method: 'POST' }),
|
||||
reloadFrp: () => request<FrpStatus>('/api/frpmulti/reload', { method: 'POST' }),
|
||||
getFrpLogs: (limit = 200, serverID?: number) => {
|
||||
const query = new URLSearchParams({ limit: String(limit) })
|
||||
|
||||
@@ -406,6 +406,8 @@ export interface FrpServerOptions {
|
||||
sub_domain_host?: string
|
||||
/** HTTP vhost 404 页面路径(VPS 本地路径)*/
|
||||
custom_404_page?: string
|
||||
/** 自定义 404 页面内容:填了就不用自己往 VPS 放文件,Havline 会写到 /etc/frp/havline-404.html */
|
||||
custom_404_html?: string
|
||||
/** 强制 TLS:只接受 TLS 连接(frpc 必须开 transport.tls.enable)*/
|
||||
tls_force?: boolean
|
||||
/** 端口白名单:逗号分隔的单端口或范围,如 6000,6005-6010;留空不限制 */
|
||||
|
||||
@@ -724,6 +724,44 @@
|
||||
<n-form-item label="自定义 404 页面(VPS 路径)"><n-input v-model:value="serverForm.options.custom_404_page" placeholder="例如 /var/www/404.html;留空用默认" /></n-form-item>
|
||||
<div class="switch-row"><span>强制 TLS(frps 只接受 TLS 连接)</span><n-switch v-model:value="serverForm.options.tls_force" /></div>
|
||||
</div>
|
||||
<!-- 直接写/导入 404 页面内容:Havline 负责写到 VPS 上,不用自己去放文件 -->
|
||||
<div class="frp-404-block">
|
||||
<div class="frp-404-block__head">
|
||||
<div>
|
||||
<div class="frp-404-block__title">自定义 404 页面内容</div>
|
||||
<div class="frp-404-block__hint">
|
||||
填了这里就不用管上面的 VPS 路径:保存后点「下发配置」,Havline 会把内容写到 VPS 的
|
||||
<code>/etc/frp/havline-404.html</code> 并让 frps 指向它;清空即恢复默认页面。
|
||||
</div>
|
||||
</div>
|
||||
<div class="frp-404-block__actions">
|
||||
<n-button size="small" @click="pick404File">导入 HTML 文件</n-button>
|
||||
<n-button
|
||||
v-if="serverForm.options.custom_404_html"
|
||||
size="small"
|
||||
quaternary
|
||||
type="warning"
|
||||
@click="serverForm.options.custom_404_html = ''"
|
||||
>
|
||||
清空
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<n-input
|
||||
v-model:value="serverForm.options.custom_404_html"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="直接粘贴 HTML,例如:<!doctype html><html>…</html>"
|
||||
class="frp-404-block__editor"
|
||||
/>
|
||||
<input
|
||||
ref="file404Input"
|
||||
type="file"
|
||||
accept=".html,.htm,text/html"
|
||||
class="frp-404-block__file"
|
||||
@change="on404FilePicked"
|
||||
/>
|
||||
</div>
|
||||
<div class="switch-row"><span>frps 写文件日志(/var/log/frps.log)</span><n-switch v-model:value="serverForm.options.frps_log_to_file" /></div>
|
||||
<n-form-item v-if="serverForm.options.frps_log_to_file" label="日志保留天数"><n-input-number v-model:value="serverForm.options.frps_log_max_days" :min="1" :max="365" placeholder="默认 3" class="narrow-input" /></n-form-item>
|
||||
</section>
|
||||
@@ -1656,7 +1694,7 @@ function defaultServerOptions(): FrpServerOptions {
|
||||
dial_server_keepalive: 7200, pool_count: 0, tcp_mux: true, tcp_mux_keepalive_interval: 60,
|
||||
heartbeat_interval: 30, heartbeat_timeout: 90, login_fail_exit: true, tls_disable_custom_first_byte: false, auto_start: false,
|
||||
proxy_url: '', connect_server_local_ip: '', dns_server: '',
|
||||
vhost_http_port: 8080, vhost_https_port: 8443, sub_domain_host: '', custom_404_page: '',
|
||||
vhost_http_port: 8080, vhost_https_port: 8443, sub_domain_host: '', custom_404_page: '', custom_404_html: '',
|
||||
tls_force: false, allow_ports: '', max_ports_per_client: 0, tcpmux_http_connect_port: 0,
|
||||
frps_log_to_file: false, frps_log_max_days: 3,
|
||||
dashboard_addr: '', dashboard_port: 0, dashboard_user: '',
|
||||
@@ -1665,6 +1703,9 @@ function defaultServerOptions(): FrpServerOptions {
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏的文件选择框:导入 404 页面内容用
|
||||
const file404Input = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const serverForm = reactive({
|
||||
name: '', server_addr: '', server_port: 7000, tls_enabled: true, tls_server_name: '', enabled: true, token: '',
|
||||
agent_url: '', agent_token: '', ssh_host: '', ssh_port: 22, ssh_user: 'root', ssh_auth: 'key', ssh_secret: '', mgmt_enabled: true,
|
||||
@@ -1916,6 +1957,28 @@ function openServerModal(server?: FrpServer) {
|
||||
showServerModal.value = true
|
||||
}
|
||||
|
||||
// 404 页面文件导入:只需要把内容读进表单,真正的落盘由 Havline 在下发配置时完成
|
||||
function pick404File() {
|
||||
file404Input.value?.click()
|
||||
}
|
||||
|
||||
async function on404FilePicked(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
input.value = ''
|
||||
if (!file) return
|
||||
if (file.size > 512 * 1024) {
|
||||
message.warning('404 页面文件请控制在 512 KB 以内')
|
||||
return
|
||||
}
|
||||
try {
|
||||
serverForm.options.custom_404_html = await file.text()
|
||||
message.success(`已载入 ${file.name}`)
|
||||
} catch {
|
||||
message.error('读取文件失败')
|
||||
}
|
||||
}
|
||||
|
||||
function openServerDetail(server: FrpServer) {
|
||||
viewingServer.value = server
|
||||
detailStatus.value = { available: false, availability: 0, average_latency_ms: 0, peak_latency_ms: 0, samples: [] }
|
||||
@@ -3093,4 +3156,28 @@ useVisibilityPolling(() => {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义 404 页面内容:直接写 / 导入,Havline 负责写到 VPS */
|
||||
.frp-404-block {
|
||||
margin: 4px 0 var(--havline-space-4);
|
||||
padding: 12px;
|
||||
border: 1px solid var(--havline-border);
|
||||
border-radius: var(--havline-radius-sm);
|
||||
background: var(--havline-surface);
|
||||
}
|
||||
|
||||
.frp-404-block__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--havline-space-3);
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.frp-404-block__title { font-size: 13px; font-weight: 600; color: var(--havline-text); }
|
||||
.frp-404-block__hint { margin-top: 2px; font-size: 12px; line-height: 1.6; color: var(--havline-text-muted); }
|
||||
.frp-404-block__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.frp-404-block__editor :deep(textarea) { font-family: var(--havline-mono); font-size: 12px; line-height: 1.6; }
|
||||
.frp-404-block__file { display: none; }
|
||||
</style>
|
||||
|
||||
@@ -54,6 +54,15 @@
|
||||
<template #icon><n-icon :component="PlayOutline" /></template>
|
||||
启动 frpc
|
||||
</n-button>
|
||||
<!-- 重启:改完配置或进程卡住时一次到位;停掉之后隧道会断,用这个比「停止 → 启动」更稳 -->
|
||||
<n-button
|
||||
:loading="actionLoading === 'restart'"
|
||||
:disabled="servers.length === 0"
|
||||
@click="restartClient"
|
||||
>
|
||||
<template #icon><n-icon :component="RefreshOutline" /></template>
|
||||
重启 frpc
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -800,7 +809,7 @@ const proxies = ref<FrpProxy[]>([])
|
||||
const proxyTraffic = ref<Record<number, FrpProxyTraffic>>({})
|
||||
const proxyTrafficErrors = ref<Record<number, string>>({})
|
||||
const loadError = ref('')
|
||||
const actionLoading = ref<'start' | 'stop' | 'reload' | null>(null)
|
||||
const actionLoading = ref<'start' | 'stop' | 'restart' | 'reload' | null>(null)
|
||||
const savingProxy = ref(false)
|
||||
const showProxyModal = ref(false)
|
||||
const showExport = ref(false)
|
||||
@@ -1227,18 +1236,20 @@ function splitText(value: string) { return value.split(/[\n,\s]+/).map((item) =>
|
||||
function textToMap(value: string) { return Object.fromEntries(value.split('\n').map((line) => line.split('=').map((item) => item.trim())).filter(([key, item]) => key && item)) }
|
||||
function mapToText(value?: Record<string, string>) { return Object.entries(value ?? {}).map(([key, item]) => `${key}=${item}`).join('\n') }
|
||||
|
||||
async function runAction(action: 'start' | 'stop' | 'reload') {
|
||||
async function runAction(action: 'start' | 'stop' | 'restart' | 'reload') {
|
||||
actionLoading.value = action
|
||||
try {
|
||||
const methods = { start: api.startFrp, stop: api.stopFrp, reload: api.reloadFrp }
|
||||
const methods = { start: api.startFrp, stop: api.stopFrp, restart: api.restartFrp, reload: api.reloadFrp }
|
||||
status.value = await methods[action]()
|
||||
message.success(action === 'start' ? 'frpc 已启动' : action === 'stop' ? 'frpc 已停止' : '配置已应用')
|
||||
const tips = { start: 'frpc 已启动', stop: 'frpc 已停止', restart: 'frpc 已重启', reload: '配置已应用' }
|
||||
message.success(tips[action])
|
||||
} catch (error) { message.error(error instanceof Error ? error.message : '操作失败') }
|
||||
finally { actionLoading.value = null; await load() }
|
||||
}
|
||||
|
||||
function startClient() { return runAction('start') }
|
||||
function stopClient() { return runAction('stop') }
|
||||
function restartClient() { return runAction('restart') }
|
||||
function reloadClient() { return runAction('reload') }
|
||||
|
||||
async function openRuntimeModal() {
|
||||
|
||||
Reference in New Issue
Block a user