feat(api): 添加从商店安装插件功能
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 49s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 57s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 51s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m7s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 51s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 51s

- 添加 /store/install API 端点用于从商店安装插件
- 实现 StorePluginInstallRequest 请求结构体
- 添加 handleStoreInstall 处理函数实现插件下载和安装逻辑
- 在客户端视图中添加商店插件安装模态框和相关功能
- 在插件视图中添加商店插件安装按钮和安装到客户端功能
- 添加 installStorePlugin API 函数用于前端调用
- 实现客户端在线状态检查和插件安装流程
This commit is contained in:
Flik
2025-12-29 19:40:34 +08:00
parent ab81e08100
commit d4984c8d78
4 changed files with 241 additions and 6 deletions

View File

@@ -33,6 +33,8 @@ export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`)
// 扩展商店
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[], store_url: string }>('/store/plugins')
export const installStorePlugin = (pluginName: string, downloadUrl: string, clientId: string) =>
post('/store/install', { plugin_name: pluginName, download_url: downloadUrl, client_id: clientId })
// 客户端插件配置
export const getClientPluginConfig = (clientId: string, pluginName: string) =>

View File

@@ -4,18 +4,19 @@ import { useRoute, useRouter } from 'vue-router'
import {
NCard, NButton, NSpace, NTag, NTable, NEmpty,
NFormItem, NInput, NInputNumber, NSelect, NModal, NCheckbox, NSwitch,
NIcon, useMessage, useDialog
NIcon, useMessage, useDialog, NSpin
} from 'naive-ui'
import {
ArrowBackOutline, CreateOutline, TrashOutline,
PushOutline, PowerOutline, AddOutline, SaveOutline, CloseOutline,
DownloadOutline, SettingsOutline
DownloadOutline, SettingsOutline, StorefrontOutline
} from '@vicons/ionicons5'
import {
getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient,
getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig
getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig,
getStorePlugins, installStorePlugin
} from '../api'
import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField, RuleSchema } from '../types'
import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField, RuleSchema, StorePluginInfo } from '../types'
const route = useRoute()
const router = useRouter()
@@ -77,6 +78,13 @@ const configSchema = ref<ConfigField[]>([])
const configValues = ref<Record<string, string>>({})
const configLoading = ref(false)
// 商店插件安装相关
const showStoreModal = ref(false)
const storePlugins = ref<StorePluginInfo[]>([])
const storeLoading = ref(false)
const selectedStorePlugin = ref<StorePluginInfo | null>(null)
const storeInstalling = ref(false)
const loadPlugins = async () => {
try {
const { data } = await getPlugins()
@@ -110,6 +118,42 @@ const openInstallModal = async () => {
showInstallModal.value = true
}
// 商店插件相关函数
const openStoreModal = async () => {
showStoreModal.value = true
storeLoading.value = true
selectedStorePlugin.value = null
try {
const { data } = await getStorePlugins()
storePlugins.value = (data.plugins || []).filter(p => p.download_url)
} catch (e) {
console.error('Failed to load store plugins', e)
message.error('加载商店插件失败')
} finally {
storeLoading.value = false
}
}
const handleInstallStorePlugin = async (plugin: StorePluginInfo) => {
if (!plugin.download_url) {
message.error('该插件没有下载地址')
return
}
storeInstalling.value = true
selectedStorePlugin.value = plugin
try {
await installStorePlugin(plugin.name, plugin.download_url, clientId)
message.success(`已安装 ${plugin.name}`)
showStoreModal.value = false
await loadClient()
} catch (e: any) {
message.error(e.response?.data || '安装失败')
} finally {
storeInstalling.value = false
selectedStorePlugin.value = null
}
}
const getTypeLabel = (type: string) => {
const labels: Record<string, string> = { proxy: '协议', app: '应用', service: '服务', tool: '工具' }
return labels[type] || type
@@ -350,6 +394,10 @@ const savePluginConfig = async () => {
<template #icon><n-icon><DownloadOutline /></n-icon></template>
安装插件
</n-button>
<n-button @click="openStoreModal">
<template #icon><n-icon><StorefrontOutline /></n-icon></template>
从商店安装
</n-button>
<n-button type="warning" @click="disconnect">
<template #icon><n-icon><PowerOutline /></n-icon></template>
断开连接
@@ -598,5 +646,39 @@ const savePluginConfig = async () => {
</n-space>
</template>
</n-modal>
<!-- 商店插件安装模态框 -->
<n-modal v-model:show="showStoreModal" preset="card" title="从商店安装插件" style="width: 500px;">
<n-spin :show="storeLoading">
<n-empty v-if="!storeLoading && storePlugins.length === 0" description="商店暂无可用插件" />
<n-space v-else vertical :size="12">
<n-card v-for="plugin in storePlugins" :key="plugin.name" size="small">
<n-space justify="space-between" align="center">
<n-space vertical :size="4">
<n-space align="center">
<span style="font-weight: 500;">{{ plugin.name }}</span>
<n-tag size="small">v{{ plugin.version }}</n-tag>
</n-space>
<span style="color: #666; font-size: 12px;">{{ plugin.description }}</span>
<span style="color: #999; font-size: 12px;">作者: {{ plugin.author }}</span>
</n-space>
<n-button
size="small"
type="primary"
:loading="storeInstalling && selectedStorePlugin?.name === plugin.name"
@click="handleInstallStorePlugin(plugin)"
>
安装
</n-button>
</n-space>
</n-card>
</n-space>
</n-spin>
<template #footer>
<n-space justify="end">
<n-button @click="showStoreModal = false">关闭</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>

View File

@@ -4,10 +4,13 @@ import { useRouter } from 'vue-router'
import {
NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi,
NEmpty, NSpin, NIcon, NSwitch, NTabs, NTabPane, useMessage,
NSelect
NSelect, NModal
} from 'naive-ui'
import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline, CodeSlashOutline } from '@vicons/ionicons5'
import { getPlugins, enablePlugin, disablePlugin, getStorePlugins, getJSPlugins, pushJSPluginToClient, getClients } from '../api'
import {
getPlugins, enablePlugin, disablePlugin, getStorePlugins, getJSPlugins,
pushJSPluginToClient, getClients, installStorePlugin
} from '../api'
import type { PluginInfo, StorePluginInfo, JSPlugin, ClientStatus } from '../types'
const router = useRouter()
@@ -141,6 +144,43 @@ const handlePushJSPlugin = async (pluginName: string, clientId: string) => {
const onlineClients = computed(() => clients.value.filter(c => c.online))
// 商店插件安装相关
const showInstallModal = ref(false)
const selectedStorePlugin = ref<StorePluginInfo | null>(null)
const selectedClientId = ref('')
const installing = ref(false)
const openInstallModal = (plugin: StorePluginInfo) => {
selectedStorePlugin.value = plugin
selectedClientId.value = ''
showInstallModal.value = true
}
const handleInstallStorePlugin = async () => {
if (!selectedStorePlugin.value || !selectedClientId.value) {
message.warning('请选择要安装到的客户端')
return
}
if (!selectedStorePlugin.value.download_url) {
message.error('该插件没有下载地址')
return
}
installing.value = true
try {
await installStorePlugin(
selectedStorePlugin.value.name,
selectedStorePlugin.value.download_url,
selectedClientId.value
)
message.success(`已安装 ${selectedStorePlugin.value.name} 到客户端`)
showInstallModal.value = false
} catch (e: any) {
message.error(e.response?.data || '安装失败')
} finally {
installing.value = false
}
}
onMounted(() => {
loadPlugins()
loadClients()
@@ -231,6 +271,16 @@ onMounted(() => {
<span>{{ plugin.name }}</span>
</n-space>
</template>
<template #header-extra>
<n-button
v-if="plugin.download_url && onlineClients.length > 0"
size="small"
type="primary"
@click="openInstallModal(plugin)"
>
安装
</n-button>
</template>
<n-space vertical :size="8">
<n-space>
<n-tag size="small">v{{ plugin.version }}</n-tag>
@@ -310,5 +360,33 @@ onMounted(() => {
... 已屏蔽 ...
</n-modal>
-->
<!-- 安装商店插件模态框 -->
<n-modal v-model:show="showInstallModal" preset="card" title="安装插件" style="width: 400px;">
<n-space vertical :size="16">
<div v-if="selectedStorePlugin">
<p style="margin: 0 0 8px 0;"><strong>插件:</strong> {{ selectedStorePlugin.name }}</p>
<p style="margin: 0; color: #666;">{{ selectedStorePlugin.description }}</p>
</div>
<n-select
v-model:value="selectedClientId"
placeholder="选择要安装到的客户端"
:options="onlineClients.map(c => ({ label: c.nickname || c.id, value: c.id }))"
/>
</n-space>
<template #footer>
<n-space justify="end">
<n-button @click="showInstallModal = false">取消</n-button>
<n-button
type="primary"
:loading="installing"
:disabled="!selectedClientId"
@click="handleInstallStorePlugin"
>
安装
</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>