This commit is contained in:
Flik
2025-12-27 22:49:07 +08:00
parent 622f30b381
commit 4b71ff1604
10 changed files with 351 additions and 87 deletions

View File

@@ -1,5 +1,5 @@
import { get, post, put, del } from '../config/axios'
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo } from '../types'
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo } from '../types'
// 重新导出 token 管理方法
export { getToken, setToken, removeToken } from '../config/axios'
@@ -30,3 +30,6 @@ export const installPluginsToClient = (id: string, plugins: string[]) =>
export const getPlugins = () => get<PluginInfo[]>('/plugins')
export const enablePlugin = (name: string) => post(`/plugin/${name}/enable`)
export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`)
// 扩展商店
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[], store_url: string }>('/store/plugins')

View File

@@ -5,6 +5,14 @@ export interface ProxyRule {
local_port: number
remote_port: number
type?: string
enabled?: boolean
}
// 客户端已安装的插件
export interface ClientPlugin {
name: string
version: string
enabled: boolean
}
// 客户端配置
@@ -12,6 +20,7 @@ export interface ClientConfig {
id: string
nickname?: string
rules: ProxyRule[]
plugins?: ClientPlugin[]
}
// 客户端状态
@@ -28,6 +37,7 @@ export interface ClientDetail {
id: string
nickname?: string
rules: ProxyRule[]
plugins?: ClientPlugin[]
online: boolean
last_ping?: string
}
@@ -58,5 +68,17 @@ export interface PluginInfo {
type: string
description: string
source: string
icon?: string
enabled: boolean
}
// 扩展商店插件信息
export interface StorePluginInfo {
name: string
version: string
type: string
description: string
author: string
icon?: string
download_url?: string
}

View File

@@ -3,7 +3,7 @@ import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
NCard, NButton, NSpace, NTag, NTable, NEmpty,
NFormItem, NInput, NInputNumber, NSelect, NModal, NCheckbox,
NFormItem, NInput, NInputNumber, NSelect, NModal, NCheckbox, NSwitch,
NIcon, useMessage, useDialog
} from 'naive-ui'
import {
@@ -12,7 +12,7 @@ import {
DownloadOutline
} from '@vicons/ionicons5'
import { getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, getPlugins, installPluginsToClient } from '../api'
import type { ProxyRule, PluginInfo } from '../types'
import type { ProxyRule, PluginInfo, ClientPlugin } from '../types'
const route = useRoute()
const router = useRouter()
@@ -24,6 +24,7 @@ const online = ref(false)
const lastPing = ref('')
const nickname = ref('')
const rules = ref<ProxyRule[]>([])
const clientPlugins = ref<ClientPlugin[]>([])
const editing = ref(false)
const editNickname = ref('')
const editRules = ref<ProxyRule[]>([])
@@ -68,6 +69,7 @@ const loadClient = async () => {
lastPing.value = data.last_ping || ''
nickname.value = data.nickname || ''
rules.value = data.rules || []
clientPlugins.value = data.plugins || []
} catch (e) {
console.error('Failed to load client', e)
}
@@ -79,7 +81,8 @@ const startEdit = () => {
editNickname.value = nickname.value
editRules.value = rules.value.map(rule => ({
...rule,
type: rule.type || 'tcp'
type: rule.type || 'tcp',
enabled: rule.enabled !== false
}))
editing.value = true
}
@@ -90,7 +93,7 @@ const cancelEdit = () => {
const addRule = () => {
editRules.value.push({
name: '', local_ip: '127.0.0.1', local_port: 80, remote_port: 8080, type: 'tcp'
name: '', local_ip: '127.0.0.1', local_port: 80, remote_port: 8080, type: 'tcp', enabled: true
})
}
@@ -167,6 +170,25 @@ const installPlugins = async () => {
message.error(e.response?.data || '安装失败')
}
}
const toggleClientPlugin = async (plugin: ClientPlugin) => {
const newEnabled = !plugin.enabled
const updatedPlugins = clientPlugins.value.map(p =>
p.name === plugin.name ? { ...p, enabled: newEnabled } : p
)
try {
await updateClient(clientId, {
id: clientId,
nickname: nickname.value,
rules: rules.value,
plugins: updatedPlugins
})
plugin.enabled = newEnabled
message.success(newEnabled ? `已启用 ${plugin.name}` : `已禁用 ${plugin.name}`)
} catch (e) {
message.error('操作失败')
}
}
</script>
<template>
@@ -242,6 +264,7 @@ const installPlugins = async () => {
<th>本地地址</th>
<th>远程端口</th>
<th>类型</th>
<th>状态</th>
</tr>
</thead>
<tbody>
@@ -250,6 +273,11 @@ const installPlugins = async () => {
<td>{{ rule.local_ip }}:{{ rule.local_port }}</td>
<td>{{ rule.remote_port }}</td>
<td><n-tag size="small">{{ rule.type || 'tcp' }}</n-tag></td>
<td>
<n-tag size="small" :type="rule.enabled !== false ? 'success' : 'default'">
{{ rule.enabled !== false ? '启用' : '禁用' }}
</n-tag>
</td>
</tr>
</tbody>
</n-table>
@@ -263,6 +291,9 @@ const installPlugins = async () => {
</n-form-item>
<n-card v-for="(rule, i) in editRules" :key="i" size="small">
<n-space align="center">
<n-form-item label="启用" :show-feedback="false">
<n-switch v-model:value="rule.enabled" />
</n-form-item>
<n-form-item label="名称" :show-feedback="false">
<n-input v-model:value="rule.name" placeholder="规则名称" />
</n-form-item>
@@ -291,6 +322,29 @@ const installPlugins = async () => {
</template>
</n-card>
<!-- 已安装插件卡片 -->
<n-card title="已安装扩展" style="margin-top: 16px;">
<n-empty v-if="clientPlugins.length === 0" description="暂无已安装扩展" />
<n-table v-else :bordered="false" :single-line="false">
<thead>
<tr>
<th>名称</th>
<th>版本</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr v-for="plugin in clientPlugins" :key="plugin.name">
<td>{{ plugin.name }}</td>
<td>v{{ plugin.version }}</td>
<td>
<n-switch :value="plugin.enabled" @update:value="toggleClientPlugin(plugin)" />
</td>
</tr>
</tbody>
</n-table>
</n-card>
<!-- 安装插件模态框 -->
<n-modal v-model:show="showInstallModal" preset="card" title="安装插件到客户端" style="width: 500px;">
<n-empty v-if="availablePlugins.length === 0" description="暂无可用插件" />

View File

@@ -3,16 +3,20 @@ import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import {
NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi,
NEmpty, NSpin, NIcon, NSwitch, useMessage
NEmpty, NSpin, NIcon, NSwitch, NTabs, NTabPane, useMessage
} from 'naive-ui'
import { ArrowBackOutline, ExtensionPuzzleOutline } from '@vicons/ionicons5'
import { getPlugins, enablePlugin, disablePlugin } from '../api'
import type { PluginInfo } from '../types'
import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline } from '@vicons/ionicons5'
import { getPlugins, enablePlugin, disablePlugin, getStorePlugins } from '../api'
import type { PluginInfo, StorePluginInfo } from '../types'
const router = useRouter()
const message = useMessage()
const plugins = ref<PluginInfo[]>([])
const storePlugins = ref<StorePluginInfo[]>([])
const storeUrl = ref('')
const loading = ref(true)
const storeLoading = ref(false)
const activeTab = ref('installed')
const loadPlugins = async () => {
try {
@@ -25,6 +29,19 @@ const loadPlugins = async () => {
}
}
const loadStorePlugins = async () => {
storeLoading.value = true
try {
const { data } = await getStorePlugins()
storePlugins.value = data.plugins || []
storeUrl.value = data.store_url || ''
} catch (e) {
console.error('Failed to load store plugins', e)
} finally {
storeLoading.value = false
}
}
const proxyPlugins = computed(() =>
plugins.value.filter(p => p.type === 'proxy')
)
@@ -68,6 +85,12 @@ const getTypeColor = (type: string) => {
return colors[type] || 'default'
}
const handleTabChange = (tab: string) => {
if (tab === 'store' && storePlugins.value.length === 0) {
loadStorePlugins()
}
}
onMounted(loadPlugins)
</script>
@@ -75,8 +98,8 @@ onMounted(loadPlugins)
<div class="plugins-view">
<n-space justify="space-between" align="center" style="margin-bottom: 24px;">
<div>
<h2 style="margin: 0 0 8px 0;">插件管理</h2>
<p style="margin: 0; color: #666;">查看和管理已注册的插件</p>
<h2 style="margin: 0 0 8px 0;">扩展商店</h2>
<p style="margin: 0; color: #666;">管理已安装扩展和浏览扩展商店</p>
</div>
<n-button quaternary @click="router.push('/')">
<template #icon><n-icon><ArrowBackOutline /></n-icon></template>
@@ -84,54 +107,92 @@ onMounted(loadPlugins)
</n-button>
</n-space>
<n-spin :show="loading">
<n-grid :cols="3" :x-gap="16" :y-gap="16" style="margin-bottom: 24px;">
<n-gi>
<n-card>
<n-statistic label="总插件数" :value="plugins.length" />
</n-card>
</n-gi>
<n-gi>
<n-card>
<n-statistic label="协议插件" :value="proxyPlugins.length" />
</n-card>
</n-gi>
<n-gi>
<n-card>
<n-statistic label="应用插件" :value="appPlugins.length" />
</n-card>
</n-gi>
</n-grid>
<n-tabs v-model:value="activeTab" type="line" @update:value="handleTabChange">
<!-- 已安装扩展 -->
<n-tab-pane name="installed" tab="已安装">
<n-spin :show="loading">
<n-grid :cols="3" :x-gap="16" :y-gap="16" style="margin-bottom: 24px;">
<n-gi>
<n-card>
<n-statistic label="总插件数" :value="plugins.length" />
</n-card>
</n-gi>
<n-gi>
<n-card>
<n-statistic label="协议插件" :value="proxyPlugins.length" />
</n-card>
</n-gi>
<n-gi>
<n-card>
<n-statistic label="应用插件" :value="appPlugins.length" />
</n-card>
</n-gi>
</n-grid>
<n-empty v-if="!loading && plugins.length === 0" description="暂无插件" />
<n-empty v-if="!loading && plugins.length === 0" description="暂无已安装扩展" />
<n-grid v-else :cols="3" :x-gap="16" :y-gap="16" responsive="screen" cols-s="1" cols-m="2">
<n-gi v-for="plugin in plugins" :key="plugin.name">
<n-card hoverable>
<template #header>
<n-space align="center">
<n-icon size="24" color="#18a058"><ExtensionPuzzleOutline /></n-icon>
<span>{{ plugin.name }}</span>
</n-space>
</template>
<template #header-extra>
<n-switch :value="plugin.enabled" @update:value="togglePlugin(plugin)" />
</template>
<n-space vertical :size="8">
<n-space>
<n-tag size="small">v{{ plugin.version }}</n-tag>
<n-tag size="small" :type="getTypeColor(plugin.type)">
{{ getTypeLabel(plugin.type) }}
</n-tag>
<n-tag size="small" :type="plugin.source === 'builtin' ? 'default' : 'warning'">
{{ plugin.source === 'builtin' ? '内置' : 'WASM' }}
</n-tag>
</n-space>
<p style="margin: 0; color: #666;">{{ plugin.description }}</p>
</n-space>
</n-card>
</n-gi>
</n-grid>
</n-spin>
<n-grid v-else :cols="3" :x-gap="16" :y-gap="16" responsive="screen" cols-s="1" cols-m="2">
<n-gi v-for="plugin in plugins" :key="plugin.name">
<n-card hoverable>
<template #header>
<n-space align="center">
<img v-if="plugin.icon" :src="plugin.icon" style="width: 24px; height: 24px;" />
<n-icon v-else size="24" color="#18a058"><ExtensionPuzzleOutline /></n-icon>
<span>{{ plugin.name }}</span>
</n-space>
</template>
<template #header-extra>
<n-switch :value="plugin.enabled" @update:value="togglePlugin(plugin)" />
</template>
<n-space vertical :size="8">
<n-space>
<n-tag size="small">v{{ plugin.version }}</n-tag>
<n-tag size="small" :type="getTypeColor(plugin.type)">
{{ getTypeLabel(plugin.type) }}
</n-tag>
<n-tag size="small" :type="plugin.source === 'builtin' ? 'default' : 'warning'">
{{ plugin.source === 'builtin' ? '内置' : 'WASM' }}
</n-tag>
</n-space>
<p style="margin: 0; color: #666;">{{ plugin.description }}</p>
</n-space>
</n-card>
</n-gi>
</n-grid>
</n-spin>
</n-tab-pane>
<!-- 扩展商店 -->
<n-tab-pane name="store" tab="扩展商店">
<n-spin :show="storeLoading">
<n-empty v-if="!storeUrl" description="未配置扩展商店URL请在配置文件中设置 plugin_store.url" />
<n-empty v-else-if="!storeLoading && storePlugins.length === 0" description="扩展商店暂无可用扩展" />
<n-grid v-else :cols="3" :x-gap="16" :y-gap="16" responsive="screen" cols-s="1" cols-m="2">
<n-gi v-for="plugin in storePlugins" :key="plugin.name">
<n-card hoverable>
<template #header>
<n-space align="center">
<img v-if="plugin.icon" :src="plugin.icon" style="width: 24px; height: 24px;" />
<n-icon v-else size="24" color="#18a058"><StorefrontOutline /></n-icon>
<span>{{ plugin.name }}</span>
</n-space>
</template>
<n-space vertical :size="8">
<n-space>
<n-tag size="small">v{{ plugin.version }}</n-tag>
<n-tag size="small" :type="getTypeColor(plugin.type)">
{{ getTypeLabel(plugin.type) }}
</n-tag>
</n-space>
<p style="margin: 0; color: #666;">{{ plugin.description }}</p>
<p style="margin: 0; color: #999; font-size: 12px;">作者: {{ plugin.author }}</p>
</n-space>
</n-card>
</n-gi>
</n-grid>
</n-spin>
</n-tab-pane>
</n-tabs>
</div>
</template>