2026-02-07 12:24:42 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="space-y-6 pb-8">
|
2026-02-08 13:33:08 +08:00
|
|
|
|
<Card
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
class="overflow-hidden"
|
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<ProxyNodeHeader
|
|
|
|
|
|
v-model:search-query="searchQuery"
|
|
|
|
|
|
v-model:filter-status="filterStatus"
|
|
|
|
|
|
:loading="store.loading"
|
|
|
|
|
|
:status-options="proxyNodeStatusFilterOptions"
|
|
|
|
|
|
@open-distribution="showPoolProxyDistributionDialog = true"
|
|
|
|
|
|
@open-batch-upgrade="showBatchUpgradeDialog = true"
|
|
|
|
|
|
@open-add="openAddDialog"
|
|
|
|
|
|
@refresh="refresh"
|
|
|
|
|
|
/>
|
2026-02-07 12:24:42 +08:00
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<ProxyNodeList
|
|
|
|
|
|
v-model:filter-status="filterStatus"
|
|
|
|
|
|
v-model:current-page="currentPage"
|
|
|
|
|
|
v-model:page-size="pageSize"
|
|
|
|
|
|
:nodes="paginatedNodes"
|
2026-02-07 12:24:42 +08:00
|
|
|
|
:total="filteredNodes.length"
|
2026-06-30 14:14:12 +08:00
|
|
|
|
:loading="store.loading"
|
|
|
|
|
|
:status-options="proxyNodeStatusFilterOptions"
|
|
|
|
|
|
:expanded-node-ids="expandedNodeIds"
|
|
|
|
|
|
:testing-node-ids="testingNodes"
|
|
|
|
|
|
:node-details="nodeDetails"
|
|
|
|
|
|
@toggle-details="toggleNodeDetails"
|
|
|
|
|
|
@refresh-details="loadNodeDetails"
|
|
|
|
|
|
@test="handleTest"
|
|
|
|
|
|
@edit="handleEdit"
|
|
|
|
|
|
@config="handleConfig"
|
|
|
|
|
|
@view-events="handleViewEvents"
|
|
|
|
|
|
@delete="handleDelete"
|
2026-02-07 12:24:42 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<ProxyNodeFormDialog
|
|
|
|
|
|
:open="showAddDialog"
|
|
|
|
|
|
:editing-node="editingNode"
|
|
|
|
|
|
:add-mode="addMode"
|
|
|
|
|
|
:add-form="addForm"
|
|
|
|
|
|
:batch-form="batchForm"
|
|
|
|
|
|
:install-form="installForm"
|
|
|
|
|
|
:install-system="installSystem"
|
|
|
|
|
|
:install-loading="installLoading"
|
|
|
|
|
|
:install-copied="installCopied"
|
|
|
|
|
|
:proxy-install-command="proxyInstallCommand"
|
|
|
|
|
|
:proxy-install-hint="proxyInstallHint"
|
|
|
|
|
|
:batch-parse-result="batchParseResult"
|
|
|
|
|
|
:adding-node="addingNode"
|
|
|
|
|
|
:testing-url="testingUrl"
|
|
|
|
|
|
@update:open="handleDialogClose"
|
|
|
|
|
|
@update:add-mode="addMode = $event"
|
|
|
|
|
|
@update:add-form="addForm = $event"
|
|
|
|
|
|
@update:batch-form="batchForm = $event"
|
|
|
|
|
|
@update:install-form="installForm = $event"
|
|
|
|
|
|
@update:install-system="installSystem = $event"
|
|
|
|
|
|
@refresh-install-command="refreshProxyInstallCommand"
|
|
|
|
|
|
@copy-install-command="copyProxyInstallCommand"
|
|
|
|
|
|
@submit-manual="editingNode ? handleUpdateManualNode() : handleAddManualNode()"
|
|
|
|
|
|
@submit-batch="handleBatchAddManualNodes"
|
|
|
|
|
|
@test-url="handleTestUrl"
|
|
|
|
|
|
/>
|
2026-05-14 19:35:16 +08:00
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<ProxyNodeRemoteConfigDialog
|
|
|
|
|
|
:open="showConfigDialog"
|
|
|
|
|
|
:node="configNode"
|
|
|
|
|
|
:form="configForm"
|
|
|
|
|
|
:saving="savingConfig"
|
|
|
|
|
|
@update:open="handleConfigDialogClose"
|
|
|
|
|
|
@update:form="configForm = $event"
|
|
|
|
|
|
@save="handleSaveConfig"
|
|
|
|
|
|
/>
|
2026-05-14 19:35:16 +08:00
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<ProxyNodeBatchUpgradeDialog
|
|
|
|
|
|
:open="showBatchUpgradeDialog"
|
|
|
|
|
|
:version="batchUpgradeVersion"
|
|
|
|
|
|
:upgrading="batchUpgrading"
|
|
|
|
|
|
@update:open="handleBatchUpgradeDialogOpen"
|
|
|
|
|
|
@update:version="batchUpgradeVersion = $event"
|
|
|
|
|
|
@submit="handleBatchUpgrade"
|
|
|
|
|
|
/>
|
2026-03-02 12:58:41 +08:00
|
|
|
|
|
2026-05-23 10:40:25 +08:00
|
|
|
|
<PoolProxyDistributionDialog
|
|
|
|
|
|
v-model="showPoolProxyDistributionDialog"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<ProxyNodeEventsDialog
|
2026-02-28 13:52:32 +08:00
|
|
|
|
:open="showEventsDialog"
|
2026-06-30 14:14:12 +08:00
|
|
|
|
:node="eventsNode"
|
|
|
|
|
|
:events="nodeEvents"
|
|
|
|
|
|
:loading="loadingEvents"
|
|
|
|
|
|
@update:open="handleEventsDialogClose"
|
|
|
|
|
|
/>
|
2026-02-07 12:24:42 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-05-14 19:35:16 +08:00
|
|
|
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
2026-02-07 12:24:42 +08:00
|
|
|
|
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
|
|
|
|
|
import { useToast } from '@/composables/useToast'
|
|
|
|
|
|
import { useConfirm } from '@/composables/useConfirm'
|
2026-05-14 19:35:16 +08:00
|
|
|
|
import { useClipboard } from '@/composables/useClipboard'
|
2026-06-30 14:14:12 +08:00
|
|
|
|
import { useI18n } from '@/i18n'
|
2026-07-31 00:50:33 +08:00
|
|
|
|
import { clearModelsDevCache } from '@/api/models-dev'
|
2026-04-12 16:02:38 +08:00
|
|
|
|
import {
|
|
|
|
|
|
proxyNodesApi,
|
|
|
|
|
|
type ProxyNode,
|
|
|
|
|
|
type ProxyNodeEvent,
|
2026-05-14 19:35:16 +08:00
|
|
|
|
type ProxyNodeInstallSession,
|
2026-05-08 23:20:50 +08:00
|
|
|
|
type ProxyNodeMetricsResponse,
|
2026-04-12 16:02:38 +08:00
|
|
|
|
type ProxyNodeRemoteConfig,
|
|
|
|
|
|
type ProxyNodeSchedulingState,
|
2026-04-14 22:51:02 +08:00
|
|
|
|
type ProxyNodeTestResult,
|
2026-04-12 16:02:38 +08:00
|
|
|
|
} from '@/api/proxy-nodes'
|
2026-02-07 12:24:42 +08:00
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
import { Card } from '@/components/ui'
|
2026-02-22 00:43:41 +08:00
|
|
|
|
import { parseApiError } from '@/utils/errorParser'
|
2026-05-23 10:40:25 +08:00
|
|
|
|
import { parseBatchProxyNodeInput } from './proxy-node-batch'
|
2026-07-31 00:50:33 +08:00
|
|
|
|
import { proxyNodeDeleteSuccessMessage } from './proxy-node-delete-feedback'
|
2026-06-30 14:14:12 +08:00
|
|
|
|
import ProxyNodeBatchUpgradeDialog from './components/ProxyNodeBatchUpgradeDialog.vue'
|
|
|
|
|
|
import ProxyNodeEventsDialog from './components/ProxyNodeEventsDialog.vue'
|
|
|
|
|
|
import ProxyNodeFormDialog from './components/ProxyNodeFormDialog.vue'
|
|
|
|
|
|
import ProxyNodeHeader from './components/ProxyNodeHeader.vue'
|
|
|
|
|
|
import ProxyNodeList from './components/ProxyNodeList.vue'
|
|
|
|
|
|
import ProxyNodeRemoteConfigDialog from './components/ProxyNodeRemoteConfigDialog.vue'
|
2026-05-23 10:40:25 +08:00
|
|
|
|
import PoolProxyDistributionDialog from '@/features/pool/components/PoolProxyDistributionDialog.vue'
|
2026-02-07 12:24:42 +08:00
|
|
|
|
|
|
|
|
|
|
const { success, error: toastError } = useToast()
|
2026-04-19 13:46:21 +08:00
|
|
|
|
const { confirmDanger } = useConfirm()
|
2026-05-14 19:35:16 +08:00
|
|
|
|
const { copyToClipboard } = useClipboard()
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const { legacyT, locale } = useI18n()
|
2026-02-07 12:24:42 +08:00
|
|
|
|
const store = useProxyNodesStore()
|
|
|
|
|
|
|
|
|
|
|
|
const searchQuery = ref('')
|
|
|
|
|
|
const filterStatus = ref('all')
|
2026-05-01 02:14:49 +08:00
|
|
|
|
const proxyNodeStatusFilterOptions = [
|
|
|
|
|
|
{ value: 'all', label: '全部状态' },
|
|
|
|
|
|
{ value: 'online', label: '在线' },
|
|
|
|
|
|
{ value: 'offline', label: '离线' },
|
|
|
|
|
|
]
|
2026-02-07 12:24:42 +08:00
|
|
|
|
const currentPage = ref(1)
|
|
|
|
|
|
const pageSize = ref(20)
|
|
|
|
|
|
|
2026-02-07 14:30:46 +08:00
|
|
|
|
// 手动添加/编辑对话框
|
|
|
|
|
|
const showAddDialog = ref(false)
|
2026-05-23 10:40:25 +08:00
|
|
|
|
const showPoolProxyDistributionDialog = ref(false)
|
2026-02-07 14:30:46 +08:00
|
|
|
|
const addingNode = ref(false)
|
|
|
|
|
|
const editingNode = ref<ProxyNode | null>(null)
|
2026-05-23 10:40:25 +08:00
|
|
|
|
const addMode = ref<'script' | 'manual' | 'batch'>('script')
|
2026-02-07 14:30:46 +08:00
|
|
|
|
const addForm = ref({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
proxy_url: '',
|
|
|
|
|
|
username: '',
|
|
|
|
|
|
password: '',
|
|
|
|
|
|
region: '',
|
|
|
|
|
|
})
|
2026-05-23 10:40:25 +08:00
|
|
|
|
const batchForm = ref({
|
|
|
|
|
|
content: '',
|
|
|
|
|
|
})
|
2026-05-14 19:35:16 +08:00
|
|
|
|
const installForm = ref({
|
|
|
|
|
|
node_name: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
const installSystem = ref<'unix' | 'windows'>('unix')
|
|
|
|
|
|
const installLoading = ref(false)
|
|
|
|
|
|
const installCopied = ref(false)
|
|
|
|
|
|
const proxyInstallSession = ref<ProxyNodeInstallSession | null>(null)
|
|
|
|
|
|
let installCopiedResetTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
|
|
|
|
|
|
|
const proxyInstallCommand = computed(() => {
|
|
|
|
|
|
if (!proxyInstallSession.value) return ''
|
|
|
|
|
|
return installSystem.value === 'windows'
|
|
|
|
|
|
? proxyInstallSession.value.powershell_command
|
|
|
|
|
|
: proxyInstallSession.value.unix_command
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const proxyInstallHint = computed(() => {
|
|
|
|
|
|
if (!proxyInstallSession.value) {
|
2026-05-15 01:21:31 +08:00
|
|
|
|
return '脚本会自动安装或更新代理程序,并保留已有配置。'
|
2026-05-14 19:35:16 +08:00
|
|
|
|
}
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const minutes = Math.floor(proxyInstallSession.value.expires_in_seconds / 60)
|
|
|
|
|
|
return locale.value === 'en-US'
|
|
|
|
|
|
? `This command is valid for ${minutes} minutes and expires immediately after successful use.`
|
|
|
|
|
|
: `这条命令将在 ${minutes} 分钟内有效,成功使用后立即失效。`
|
2026-05-14 19:35:16 +08:00
|
|
|
|
})
|
2026-02-07 14:30:46 +08:00
|
|
|
|
|
2026-05-23 10:40:25 +08:00
|
|
|
|
const batchParseResult = computed(() => parseBatchProxyNodeInput(batchForm.value.content))
|
|
|
|
|
|
|
2026-05-20 01:02:01 +08:00
|
|
|
|
// 远程配置对话框 (aether-tunnel 节点)
|
2026-02-07 19:20:09 +08:00
|
|
|
|
const showConfigDialog = ref(false)
|
|
|
|
|
|
const savingConfig = ref(false)
|
|
|
|
|
|
const configNode = ref<ProxyNode | null>(null)
|
|
|
|
|
|
const configForm = ref({
|
|
|
|
|
|
allowed_ports: '',
|
|
|
|
|
|
log_level: 'info',
|
|
|
|
|
|
heartbeat_interval: '30',
|
2026-04-12 16:02:38 +08:00
|
|
|
|
scheduling_state: 'active' as ProxyNodeSchedulingState,
|
2026-03-02 12:58:41 +08:00
|
|
|
|
upgrade_to: '',
|
2026-02-07 19:20:09 +08:00
|
|
|
|
})
|
2026-03-02 12:58:41 +08:00
|
|
|
|
const showBatchUpgradeDialog = ref(false)
|
|
|
|
|
|
const batchUpgradeVersion = ref('')
|
|
|
|
|
|
const batchUpgrading = ref(false)
|
2026-02-07 19:20:09 +08:00
|
|
|
|
|
2026-02-28 13:52:32 +08:00
|
|
|
|
// 连接事件对话框
|
|
|
|
|
|
const showEventsDialog = ref(false)
|
|
|
|
|
|
const eventsNode = ref<ProxyNode | null>(null)
|
|
|
|
|
|
const nodeEvents = ref<ProxyNodeEvent[]>([])
|
|
|
|
|
|
const loadingEvents = ref(false)
|
|
|
|
|
|
|
2026-05-08 23:20:50 +08:00
|
|
|
|
interface ProxyNodeDetailState {
|
|
|
|
|
|
loading: boolean
|
|
|
|
|
|
error: string | null
|
|
|
|
|
|
node: ProxyNode | null
|
|
|
|
|
|
metrics: ProxyNodeMetricsResponse | null
|
|
|
|
|
|
events: ProxyNodeEvent[]
|
|
|
|
|
|
loadedAt: number | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const expandedNodeIds = ref(new Set<string>())
|
|
|
|
|
|
const nodeDetails = ref<Record<string, ProxyNodeDetailState>>({})
|
|
|
|
|
|
|
2026-02-07 19:20:09 +08:00
|
|
|
|
// 测试连通性
|
|
|
|
|
|
const testingNodes = ref(new Set<string>())
|
2026-02-11 23:24:59 +08:00
|
|
|
|
const testingUrl = ref(false)
|
2026-02-07 19:20:09 +08:00
|
|
|
|
|
2026-02-07 12:24:42 +08:00
|
|
|
|
const filteredNodes = computed(() => {
|
|
|
|
|
|
let filtered = [...store.nodes]
|
|
|
|
|
|
|
|
|
|
|
|
if (searchQuery.value) {
|
|
|
|
|
|
const keywords = searchQuery.value.toLowerCase().split(/\s+/).filter(k => k.length > 0)
|
|
|
|
|
|
filtered = filtered.filter(node => {
|
|
|
|
|
|
const text = `${node.name} ${node.ip} ${node.region || ''}`.toLowerCase()
|
|
|
|
|
|
return keywords.every(kw => text.includes(kw))
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (filterStatus.value !== 'all') {
|
|
|
|
|
|
filtered = filtered.filter(node => node.status === filterStatus.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return filtered
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const paginatedNodes = computed(() => {
|
|
|
|
|
|
const start = (currentPage.value - 1) * pageSize.value
|
|
|
|
|
|
return filteredNodes.value.slice(start, start + pageSize.value)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-19 13:38:59 +08:00
|
|
|
|
watch([searchQuery, filterStatus], () => {
|
2026-02-07 12:24:42 +08:00
|
|
|
|
currentPage.value = 1
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-14 19:35:16 +08:00
|
|
|
|
watch(() => installForm.value.node_name, () => {
|
|
|
|
|
|
resetProxyInstallState()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(installSystem, () => {
|
|
|
|
|
|
installCopied.value = false
|
|
|
|
|
|
clearInstallCopiedResetTimer()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-07 12:24:42 +08:00
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await store.fetchNodes()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-14 19:35:16 +08:00
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
clearInstallCopiedResetTimer()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-07 12:24:42 +08:00
|
|
|
|
async function refresh() {
|
|
|
|
|
|
await store.fetchNodes()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 22:51:02 +08:00
|
|
|
|
function formatConnectivityTestParts(result: ProxyNodeTestResult): string[] {
|
2026-04-18 21:13:37 +08:00
|
|
|
|
const parts = [
|
2026-06-30 14:14:12 +08:00
|
|
|
|
`${legacyT('探测')}: ${formatConnectivityProbe(result.probe_url)}`,
|
|
|
|
|
|
`${legacyT('超时')}: ${result.timeout_secs}s`,
|
|
|
|
|
|
`${legacyT('延迟')}: ${result.latency_ms != null ? `${result.latency_ms}ms` : legacyT('暂无样本')}`,
|
2026-04-18 21:13:37 +08:00
|
|
|
|
]
|
2026-06-30 14:14:12 +08:00
|
|
|
|
if (result.exit_ip) parts.push(`${legacyT('出口 IP')}: ${result.exit_ip}`)
|
2026-04-14 22:51:02 +08:00
|
|
|
|
return parts
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
function formatConnectivityResult(message: string, result: ProxyNodeTestResult): string {
|
|
|
|
|
|
const separator = locale.value === 'en-US' ? ', ' : ','
|
|
|
|
|
|
return `${legacyT(message)}${separator}${formatConnectivityTestParts(result).join(separator)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 21:13:37 +08:00
|
|
|
|
function formatConnectivityProbe(probeUrl: string) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const url = new URL(probeUrl)
|
|
|
|
|
|
return `${url.host}${url.pathname === '/' ? '' : url.pathname}`
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return probeUrl
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 23:24:59 +08:00
|
|
|
|
async function handleTestUrl() {
|
|
|
|
|
|
if (!addForm.value.proxy_url || testingUrl.value) return
|
|
|
|
|
|
testingUrl.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await proxyNodesApi.testProxyUrl({
|
|
|
|
|
|
proxy_url: addForm.value.proxy_url,
|
|
|
|
|
|
username: addForm.value.username || undefined,
|
|
|
|
|
|
password: addForm.value.password || undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
if (result.success) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(formatConnectivityResult('连通性测试通过', result))
|
2026-02-11 23:24:59 +08:00
|
|
|
|
} else {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const details = formatConnectivityTestParts(result).join(locale.value === 'en-US' ? ', ' : ',')
|
|
|
|
|
|
toastError(locale.value === 'en-US'
|
|
|
|
|
|
? `Connectivity test failed (${details}): ${result.error || legacyT('未知错误')}`
|
|
|
|
|
|
: `连通性测试失败(${details}): ${result.error || legacyT('未知错误')}`)
|
2026-02-11 23:24:59 +08:00
|
|
|
|
}
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('测试请求失败')))
|
2026-02-11 23:24:59 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
testingUrl.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 19:35:16 +08:00
|
|
|
|
function clearInstallCopiedResetTimer() {
|
|
|
|
|
|
if (installCopiedResetTimer) {
|
|
|
|
|
|
clearTimeout(installCopiedResetTimer)
|
|
|
|
|
|
installCopiedResetTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetProxyInstallState() {
|
|
|
|
|
|
proxyInstallSession.value = null
|
|
|
|
|
|
installCopied.value = false
|
|
|
|
|
|
clearInstallCopiedResetTimer()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openAddDialog() {
|
|
|
|
|
|
editingNode.value = null
|
|
|
|
|
|
addMode.value = 'script'
|
|
|
|
|
|
addForm.value = { name: '', proxy_url: '', username: '', password: '', region: '' }
|
2026-05-23 10:40:25 +08:00
|
|
|
|
batchForm.value = { content: '' }
|
2026-05-14 19:35:16 +08:00
|
|
|
|
installForm.value = { node_name: '' }
|
|
|
|
|
|
resetProxyInstallState()
|
|
|
|
|
|
showAddDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshProxyInstallCommand() {
|
|
|
|
|
|
const nodeName = installForm.value.node_name.trim()
|
|
|
|
|
|
if (!nodeName || installLoading.value) return
|
|
|
|
|
|
installLoading.value = true
|
|
|
|
|
|
resetProxyInstallState()
|
|
|
|
|
|
try {
|
|
|
|
|
|
proxyInstallSession.value = await store.createInstallSession({ node_name: nodeName })
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('代理节点安装命令已生成'))
|
2026-05-14 19:35:16 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('生成代理节点安装命令失败')))
|
2026-05-14 19:35:16 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
installLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function copyProxyInstallCommand() {
|
|
|
|
|
|
if (!proxyInstallCommand.value) return
|
|
|
|
|
|
const copied = await copyToClipboard(proxyInstallCommand.value, false)
|
|
|
|
|
|
if (!copied) return
|
|
|
|
|
|
installCopied.value = true
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('安装命令已复制到剪贴板'))
|
2026-05-14 19:35:16 +08:00
|
|
|
|
clearInstallCopiedResetTimer()
|
|
|
|
|
|
installCopiedResetTimer = setTimeout(() => {
|
|
|
|
|
|
installCopied.value = false
|
|
|
|
|
|
installCopiedResetTimer = null
|
|
|
|
|
|
}, 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 22:51:02 +08:00
|
|
|
|
async function handleEdit(node: ProxyNode) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { node: detail } = await proxyNodesApi.getNode(node.id)
|
|
|
|
|
|
editingNode.value = detail
|
|
|
|
|
|
addForm.value = {
|
|
|
|
|
|
name: detail.name,
|
|
|
|
|
|
proxy_url: detail.proxy_url || '',
|
|
|
|
|
|
username: detail.proxy_username || '',
|
|
|
|
|
|
password: detail.proxy_password || '',
|
|
|
|
|
|
region: detail.region || '',
|
|
|
|
|
|
}
|
2026-05-14 19:35:16 +08:00
|
|
|
|
addMode.value = 'manual'
|
|
|
|
|
|
resetProxyInstallState()
|
2026-04-14 22:51:02 +08:00
|
|
|
|
showAddDialog.value = true
|
|
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('读取代理节点详情失败')))
|
2026-02-07 14:30:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDialogClose(open: boolean) {
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
showAddDialog.value = false
|
|
|
|
|
|
editingNode.value = null
|
2026-05-14 19:35:16 +08:00
|
|
|
|
addMode.value = 'script'
|
2026-02-07 14:30:46 +08:00
|
|
|
|
addForm.value = { name: '', proxy_url: '', username: '', password: '', region: '' }
|
2026-05-23 10:40:25 +08:00
|
|
|
|
batchForm.value = { content: '' }
|
2026-05-14 19:35:16 +08:00
|
|
|
|
installForm.value = { node_name: '' }
|
|
|
|
|
|
resetProxyInstallState()
|
2026-02-07 14:30:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleUpdateManualNode() {
|
|
|
|
|
|
if (!editingNode.value || !addForm.value.name || !addForm.value.proxy_url) return
|
|
|
|
|
|
|
|
|
|
|
|
addingNode.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await proxyNodesApi.updateManualNode(editingNode.value.id, {
|
|
|
|
|
|
name: addForm.value.name,
|
|
|
|
|
|
proxy_url: addForm.value.proxy_url,
|
|
|
|
|
|
username: addForm.value.username || undefined,
|
|
|
|
|
|
// 空密码不发送(保留原值)
|
|
|
|
|
|
password: addForm.value.password || undefined,
|
|
|
|
|
|
region: addForm.value.region || undefined,
|
|
|
|
|
|
})
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('代理节点已更新'))
|
2026-02-07 14:30:46 +08:00
|
|
|
|
handleDialogClose(false)
|
|
|
|
|
|
await store.fetchNodes()
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('更新失败')))
|
2026-02-07 14:30:46 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
addingNode.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleAddManualNode() {
|
|
|
|
|
|
if (!addForm.value.name || !addForm.value.proxy_url) return
|
|
|
|
|
|
|
|
|
|
|
|
addingNode.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await store.createManualNode({
|
|
|
|
|
|
name: addForm.value.name,
|
|
|
|
|
|
proxy_url: addForm.value.proxy_url,
|
|
|
|
|
|
username: addForm.value.username || undefined,
|
|
|
|
|
|
password: addForm.value.password || undefined,
|
|
|
|
|
|
region: addForm.value.region || undefined,
|
|
|
|
|
|
})
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('代理节点已添加'))
|
2026-02-07 14:30:46 +08:00
|
|
|
|
handleDialogClose(false)
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('添加失败')))
|
2026-02-07 14:30:46 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
addingNode.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 10:40:25 +08:00
|
|
|
|
async function handleBatchAddManualNodes() {
|
|
|
|
|
|
const { nodes, errors } = batchParseResult.value
|
|
|
|
|
|
if (!batchForm.value.content.trim() || addingNode.value) return
|
|
|
|
|
|
if (errors.length > 0) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(legacyT(`批量输入存在 ${errors.length} 条格式错误,请先修正后再添加`))
|
2026-05-23 10:40:25 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nodes.length === 0) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(legacyT('请先输入至少一条代理地址'))
|
2026-05-23 10:40:25 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addingNode.value = true
|
|
|
|
|
|
const failures: string[] = []
|
|
|
|
|
|
let successCount = 0
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
for (const node of nodes) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await proxyNodesApi.createManualNode(node)
|
|
|
|
|
|
successCount += 1
|
|
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
failures.push(`${node.name}: ${parseApiError(err, legacyT('添加失败'))}`)
|
2026-05-23 10:40:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await store.fetchNodes()
|
|
|
|
|
|
|
|
|
|
|
|
if (successCount > 0 && failures.length === 0) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT(`已添加 ${successCount} 个代理节点`))
|
2026-05-23 10:40:25 +08:00
|
|
|
|
handleDialogClose(false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (successCount > 0) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT(`已添加 ${successCount} 个代理节点,${failures.length} 个失败`))
|
2026-05-23 10:40:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (failures.length > 0) {
|
|
|
|
|
|
toastError(failures.slice(0, 3).join(';'))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('批量添加失败')))
|
2026-05-23 10:40:25 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
addingNode.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 19:20:09 +08:00
|
|
|
|
function handleConfig(node: ProxyNode) {
|
|
|
|
|
|
configNode.value = node
|
|
|
|
|
|
const rc: ProxyNodeRemoteConfig = node.remote_config ?? {}
|
|
|
|
|
|
configForm.value = {
|
|
|
|
|
|
allowed_ports: rc.allowed_ports?.join(', ') || '',
|
|
|
|
|
|
log_level: rc.log_level || 'info',
|
|
|
|
|
|
heartbeat_interval: String(rc.heartbeat_interval || node.heartbeat_interval || 30),
|
2026-04-12 16:02:38 +08:00
|
|
|
|
scheduling_state: rc.scheduling_state || 'active',
|
2026-03-02 12:58:41 +08:00
|
|
|
|
upgrade_to: rc.upgrade_to || '',
|
2026-02-07 19:20:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
showConfigDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleConfigDialogClose(open: boolean) {
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
showConfigDialog.value = false
|
|
|
|
|
|
configNode.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSaveConfig() {
|
|
|
|
|
|
if (!configNode.value) return
|
|
|
|
|
|
savingConfig.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data: Partial<ProxyNodeRemoteConfig> = {}
|
|
|
|
|
|
const portsInput = configForm.value.allowed_ports.trim()
|
|
|
|
|
|
if (portsInput) {
|
|
|
|
|
|
data.allowed_ports = portsInput
|
|
|
|
|
|
.split(',')
|
|
|
|
|
|
.map((s: string) => parseInt(s.trim()))
|
|
|
|
|
|
.filter((n: number) => !isNaN(n) && n >= 1 && n <= 65535)
|
|
|
|
|
|
} else if (configNode.value.remote_config?.allowed_ports) {
|
|
|
|
|
|
// 输入清空 → 显式发送空数组以清除已有端口白名单
|
|
|
|
|
|
data.allowed_ports = []
|
|
|
|
|
|
}
|
|
|
|
|
|
if (configForm.value.log_level) {
|
|
|
|
|
|
data.log_level = configForm.value.log_level
|
|
|
|
|
|
}
|
|
|
|
|
|
const hb = parseInt(configForm.value.heartbeat_interval)
|
|
|
|
|
|
if (!isNaN(hb) && hb >= 5) {
|
|
|
|
|
|
data.heartbeat_interval = hb
|
|
|
|
|
|
}
|
2026-04-12 16:02:38 +08:00
|
|
|
|
data.scheduling_state = configForm.value.scheduling_state
|
2026-03-02 12:58:41 +08:00
|
|
|
|
const targetVersion = configForm.value.upgrade_to.trim()
|
|
|
|
|
|
if (targetVersion) {
|
|
|
|
|
|
data.upgrade_to = targetVersion
|
|
|
|
|
|
} else if (configNode.value.remote_config?.upgrade_to) {
|
|
|
|
|
|
data.upgrade_to = null
|
|
|
|
|
|
}
|
2026-02-07 19:20:09 +08:00
|
|
|
|
await proxyNodesApi.updateNodeConfig(configNode.value.id, data)
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('远程配置已保存,将在下次心跳时生效'))
|
2026-02-07 19:20:09 +08:00
|
|
|
|
handleConfigDialogClose(false)
|
|
|
|
|
|
await store.fetchNodes()
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('保存失败')))
|
2026-02-07 19:20:09 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
savingConfig.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 12:58:41 +08:00
|
|
|
|
async function handleBatchUpgrade() {
|
|
|
|
|
|
const version = batchUpgradeVersion.value.trim()
|
|
|
|
|
|
if (!version || batchUpgrading.value) return
|
|
|
|
|
|
batchUpgrading.value = true
|
|
|
|
|
|
try {
|
2026-04-19 13:38:59 +08:00
|
|
|
|
const result = await proxyNodesApi.batchUpgrade(version)
|
|
|
|
|
|
if (result.updated > 0) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT(`已向 ${result.updated} 个节点写入升级目标 ${result.version},${result.skipped} 个节点无需变更`))
|
2026-04-12 16:02:38 +08:00
|
|
|
|
} else {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT(`当前没有需要变更的 tunnel 节点,目标版本仍为 ${result.version}`))
|
2026-04-12 16:02:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
resetBatchUpgradeDialog()
|
2026-03-02 12:58:41 +08:00
|
|
|
|
await store.fetchNodes()
|
|
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('批量升级下发失败')))
|
2026-03-02 12:58:41 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
batchUpgrading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 16:02:38 +08:00
|
|
|
|
function resetBatchUpgradeDialog() {
|
|
|
|
|
|
showBatchUpgradeDialog.value = false
|
|
|
|
|
|
batchUpgradeVersion.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
function handleBatchUpgradeDialogOpen(open: boolean) {
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
showBatchUpgradeDialog.value = true
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
resetBatchUpgradeDialog()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:24:42 +08:00
|
|
|
|
async function handleDelete(node: ProxyNode) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const address = node.tunnel_mode ? node.ip : `${node.ip}:${node.port}`
|
2026-02-07 12:24:42 +08:00
|
|
|
|
const confirmed = await confirmDanger(
|
2026-06-30 14:14:12 +08:00
|
|
|
|
locale.value === 'en-US'
|
|
|
|
|
|
? `Delete proxy node "${node.name}" (${address})?`
|
|
|
|
|
|
: `确定要删除代理节点 "${node.name}" (${address}) 吗?`,
|
|
|
|
|
|
legacyT('删除节点')
|
2026-02-07 12:24:42 +08:00
|
|
|
|
)
|
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-07 14:30:46 +08:00
|
|
|
|
const result = await proxyNodesApi.deleteProxyNode(node.id)
|
2026-07-31 00:50:33 +08:00
|
|
|
|
if (result.cleared_external_models_proxy) {
|
|
|
|
|
|
clearModelsDevCache()
|
2026-02-07 14:30:46 +08:00
|
|
|
|
}
|
2026-07-31 00:50:33 +08:00
|
|
|
|
await store.fetchNodes()
|
|
|
|
|
|
success(legacyT(proxyNodeDeleteSuccessMessage(
|
|
|
|
|
|
result.cleared_system_proxy,
|
|
|
|
|
|
result.cleared_external_models_proxy,
|
|
|
|
|
|
)))
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('删除失败')))
|
2026-02-07 12:24:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 19:20:09 +08:00
|
|
|
|
async function handleTest(node: ProxyNode) {
|
|
|
|
|
|
if (testingNodes.value.has(node.id)) return
|
|
|
|
|
|
|
|
|
|
|
|
testingNodes.value.add(node.id)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await proxyNodesApi.testNode(node.id)
|
|
|
|
|
|
if (result.success) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(formatConnectivityResult('连通性测试通过', result))
|
2026-02-07 19:20:09 +08:00
|
|
|
|
} else {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const details = formatConnectivityTestParts(result).join(locale.value === 'en-US' ? ', ' : ',')
|
|
|
|
|
|
toastError(locale.value === 'en-US'
|
|
|
|
|
|
? `Connectivity test failed (${details}): ${result.error || legacyT('未知错误')}`
|
|
|
|
|
|
: `连通性测试失败(${details}): ${result.error || legacyT('未知错误')}`)
|
2026-02-07 19:20:09 +08:00
|
|
|
|
}
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('测试请求失败')))
|
2026-02-07 19:20:09 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
testingNodes.value.delete(node.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 23:20:50 +08:00
|
|
|
|
function createNodeDetailState(): ProxyNodeDetailState {
|
|
|
|
|
|
return {
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
node: null,
|
|
|
|
|
|
metrics: null,
|
|
|
|
|
|
events: [],
|
|
|
|
|
|
loadedAt: null,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateNodeDetailState(nodeId: string, patch: Partial<ProxyNodeDetailState>) {
|
|
|
|
|
|
nodeDetails.value = {
|
|
|
|
|
|
...nodeDetails.value,
|
|
|
|
|
|
[nodeId]: {
|
|
|
|
|
|
...(nodeDetails.value[nodeId] ?? createNodeDetailState()),
|
|
|
|
|
|
...patch,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleNodeDetails(node: ProxyNode) {
|
|
|
|
|
|
const next = new Set(expandedNodeIds.value)
|
|
|
|
|
|
if (next.has(node.id)) {
|
|
|
|
|
|
next.delete(node.id)
|
|
|
|
|
|
expandedNodeIds.value = next
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
next.add(node.id)
|
|
|
|
|
|
expandedNodeIds.value = next
|
|
|
|
|
|
|
|
|
|
|
|
const detailState = nodeDetails.value[node.id]
|
|
|
|
|
|
if (!detailState?.loadedAt && !detailState?.loading) {
|
|
|
|
|
|
void loadNodeDetails(node)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadNodeDetails(node: ProxyNode) {
|
|
|
|
|
|
updateNodeDetailState(node.id, { loading: true, error: null })
|
|
|
|
|
|
const to = Math.floor(Date.now() / 1000)
|
|
|
|
|
|
const from = to - 24 * 60 * 60
|
|
|
|
|
|
const eventsFrom = to - 7 * 24 * 60 * 60
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [detail, metrics, events] = await Promise.all([
|
|
|
|
|
|
proxyNodesApi.getNode(node.id),
|
|
|
|
|
|
proxyNodesApi.listNodeMetrics(node.id, { from, to, step: '1h' }),
|
|
|
|
|
|
proxyNodesApi.listNodeEvents(node.id, { limit: 8, from: eventsFrom, to }),
|
|
|
|
|
|
])
|
|
|
|
|
|
updateNodeDetailState(node.id, {
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
node: detail.node,
|
|
|
|
|
|
metrics,
|
|
|
|
|
|
events: events.items,
|
|
|
|
|
|
loadedAt: Date.now(),
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
|
updateNodeDetailState(node.id, {
|
|
|
|
|
|
loading: false,
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error: parseApiError(err, legacyT('加载节点数据失败')),
|
2026-05-08 23:20:50 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 13:52:32 +08:00
|
|
|
|
async function handleViewEvents(node: ProxyNode) {
|
|
|
|
|
|
eventsNode.value = node
|
|
|
|
|
|
showEventsDialog.value = true
|
|
|
|
|
|
loadingEvents.value = true
|
|
|
|
|
|
try {
|
2026-05-08 23:20:50 +08:00
|
|
|
|
const res = await proxyNodesApi.listNodeEvents(node.id, { limit: 50 })
|
2026-02-28 13:52:32 +08:00
|
|
|
|
nodeEvents.value = res.items
|
|
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
toastError(parseApiError(err, legacyT('加载事件失败')))
|
2026-02-28 13:52:32 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loadingEvents.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
function handleEventsDialogClose(open: boolean) {
|
|
|
|
|
|
showEventsDialog.value = open
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
eventsNode.value = null
|
|
|
|
|
|
nodeEvents.value = []
|
2026-04-12 16:02:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-07 12:24:42 +08:00
|
|
|
|
</script>
|