diff --git a/web/src/views/FrpView.vue b/web/src/views/FrpView.vue index f255d10..bd024f6 100644 --- a/web/src/views/FrpView.vue +++ b/web/src/views/FrpView.vue @@ -23,37 +23,31 @@ @@ -887,6 +891,7 @@ import { AddOutline, CloudOutline, CloseOutline, DocumentOutline, DocumentTextOutline, GitNetworkOutline, InformationCircleOutline, FlashOutline, LayersOutline, LocationOutline, LockClosedOutline, PeopleOutline, PlayOutline, RefreshOutline, ServerOutline, SettingsOutline, StopOutline, SwapHorizontalOutline, TerminalOutline, TimeOutline, } from '@vicons/ionicons5' +import { RouterLink } from 'vue-router' import { api, asList } from '../api/client' import { formatBytes, formatDate, formatRate } from '../utils/format' @@ -1076,7 +1081,9 @@ const transportOptions = [ ] const proxyServerOptions = computed(() => servers.value.map((server) => ({ label: `${server.name} (${server.server_addr}:${server.server_port})`, value: server.id }))) const statusLabel = computed(() => ({ running: '运行中', stopped: '已停止', unknown: '未配置' })[status.value.status] ?? '异常') -const statusKind = computed(() => status.value.status === 'running' ? 'success' : status.value.status === 'stopped' ? 'default' : 'warning') +// 返回的必须是 StatusKind 合法值(success/warning/error/disabled/unknown), +// 之前用 'default' 且当 value 传给 StatusBadge,导致“运行中”被判为 unknown(灰色) +const statusKind = computed(() => (status.value.status === 'running' ? 'success' : status.value.status === 'stopped' ? 'disabled' : 'warning') as 'success' | 'disabled' | 'warning') const downloadInProgress = computed(() => ['queued', 'downloading', 'verifying', 'extracting'].includes(downloadTask.value.status)) const downloadPercent = computed(() => downloadTask.value.total > 0 ? Math.min(100, Math.round(downloadTask.value.downloaded / downloadTask.value.total * 100)) : 0) const downloadProgressStatus = computed(() => downloadTask.value.status === 'completed' ? 'success' : downloadTask.value.status === 'failed' ? 'error' : 'default') @@ -1093,6 +1100,32 @@ const frpTypeTagType: Record>(() => [ + { title: '名称', key: 'name', width: 190, render: (row) => h('div', { class: 'cell-name' }, [h(NIcon, { component: CloudOutline }), h('span', row.name)]) }, + { title: '主机', key: 'host', width: 170, render: (row) => h('span', { class: 'mono cell-wrap' }, row.server_addr) }, + { title: '端口', key: 'port', width: 90, render: (row) => h(NTag, { size: 'small', bordered: false }, { default: () => String(row.server_port) }) }, + { title: '延迟', key: 'latency', width: 130, render: (row) => h('div', { class: 'cell-inline', title: latencyDetail(row) }, [h(NIcon, { component: FlashOutline }), h('span', latencyText(row))]) }, + { title: '位置', key: 'location', width: 150, render: (row) => h('div', { class: 'cell-inline' }, [h(NIcon, { component: LocationOutline }), h('span', locationText(row))]) }, + { title: '配置', key: 'config', width: 230, render: (row) => h('div', { class: 'cell-inline' }, [ + h(NTag, { size: 'small', bordered: false, type: row.tls_enabled ? 'success' : 'warning' }, { default: () => `TLS ${row.tls_enabled ? '已启用' : '未启用'}` }), + h(NTag, { size: 'small', bordered: false, type: row.has_token ? 'success' : 'error' }, { default: () => `Token ${row.has_token ? '已配置' : '缺失'}` }), + ]) }, + { title: '启动状态', key: 'startup', width: 170, render: (row) => h(NTag, { size: 'small', bordered: false, type: serverStartupType(row), title: serverStateTooltip(row) }, { default: () => `${serverStartupText(row)} · ${connectionStateText(row)}` }) }, + { title: '操作', key: 'actions', width: 560, align: 'right', render: (row) => h('div', { class: 'cell-actions' }, [ + row.boot_status === 'running' + ? h(NButton, { size: 'small', type: 'error', ghost: true, loading: serverStartingId.value === row.id, onClick: () => stopServer(row) }, { default: () => '停止' }) + : h(NButton, { size: 'small', type: 'success', ghost: true, loading: serverStartingId.value === row.id, onClick: () => startServer(row) }, { default: () => '启动' }), + h(NButton, { size: 'small', quaternary: true, onClick: () => openServerDetail(row) }, { default: () => '查看' }), + h(NButton, { size: 'small', quaternary: true, onClick: () => openServerLogs(row) }, { default: () => '日志' }), + h(NButton, { size: 'small', quaternary: true, loading: measuringId.value === row.id, onClick: () => measureServer(row) }, { default: () => '测速' }), + h(NButton, { size: 'small', quaternary: true, loading: testingId.value === row.id, onClick: () => testServer(row) }, { default: () => '校验' }), + row.agent_configured ? h(RouterLink, { to: '/frp-agent' }, { default: () => h(NButton, { size: 'small', quaternary: true }, { default: () => 'Agent 管理' }) }) : null, + h(NButton, { size: 'small', quaternary: true, onClick: () => openServerModal(row) }, { default: () => '编辑' }), + h(NButton, { size: 'small', quaternary: true, type: 'error', onClick: () => confirmDeleteServer(row) }, { default: () => '删除' }), + ]) }, +]) + const columns = computed>(() => [ { title: '名称', key: 'name', minWidth: 150, render: (row) => h('div', [h('strong', row.name), row.remark ? h('div', { class: 'table-muted' }, row.remark) : null]) }, { title: '服务端', key: 'server_id', minWidth: 130, render: (row) => servers.value.find((server) => server.id === row.server_id)?.name ?? `#${row.server_id}` }, @@ -1789,33 +1822,31 @@ onUnmounted(() => { if (timer) window.clearInterval(timer); stopDownloadPolling(