update
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 59s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m8s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 53s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m14s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 2m8s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m10s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 57s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m11s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 55s
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 59s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m8s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 53s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m14s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 2m8s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m10s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 57s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m11s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 55s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { get, post, put, del } from '../config/axios'
|
||||
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo, PluginConfigResponse, JSPlugin } from '../types'
|
||||
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo, PluginConfigResponse, JSPlugin, RuleSchemasMap } from '../types'
|
||||
|
||||
// 重新导出 token 管理方法
|
||||
export { getToken, setToken, removeToken } from '../config/axios'
|
||||
@@ -23,9 +23,21 @@ export const reloadConfig = () => post('/config/reload')
|
||||
// 客户端控制
|
||||
export const pushConfigToClient = (id: string) => post(`/client/${id}/push`)
|
||||
export const disconnectClient = (id: string) => post(`/client/${id}/disconnect`)
|
||||
export const restartClient = (id: string) => post(`/client/${id}/restart`)
|
||||
export const installPluginsToClient = (id: string, plugins: string[]) =>
|
||||
post(`/client/${id}/install-plugins`, { plugins })
|
||||
|
||||
// 规则配置模式
|
||||
export const getRuleSchemas = () => get<RuleSchemasMap>('/rule-schemas')
|
||||
|
||||
// 客户端插件控制
|
||||
export const stopClientPlugin = (clientId: string, pluginName: string, ruleName: string) =>
|
||||
post(`/client/${clientId}/plugin/${pluginName}/stop`, { rule_name: ruleName })
|
||||
export const restartClientPlugin = (clientId: string, pluginName: string, ruleName: string) =>
|
||||
post(`/client/${clientId}/plugin/${pluginName}/restart`, { rule_name: ruleName })
|
||||
export const updateClientPluginConfigWithRestart = (clientId: string, pluginName: string, ruleName: string, config: Record<string, string>, restart: boolean) =>
|
||||
post(`/client/${clientId}/plugin/${pluginName}/config`, { rule_name: ruleName, config, restart })
|
||||
|
||||
// 插件管理
|
||||
export const getPlugins = () => get<PluginInfo[]>('/plugins')
|
||||
export const enablePlugin = (name: string) => post(`/plugin/${name}/enable`)
|
||||
@@ -50,3 +62,7 @@ export const updateJSPlugin = (name: string, plugin: JSPlugin) => put(`/js-plugi
|
||||
export const deleteJSPlugin = (name: string) => del(`/js-plugin/${name}`)
|
||||
export const pushJSPluginToClient = (pluginName: string, clientId: string) =>
|
||||
post(`/js-plugin/${pluginName}/push/${clientId}`)
|
||||
export const updateJSPluginConfig = (name: string, config: Record<string, string>) =>
|
||||
put(`/js-plugin/${name}/config`, { config })
|
||||
export const setJSPluginEnabled = (name: string, enabled: boolean) =>
|
||||
post(`/js-plugin/${name}/${enabled ? 'enable' : 'disable'}`)
|
||||
|
||||
@@ -117,10 +117,15 @@ export interface StorePluginInfo {
|
||||
export interface JSPlugin {
|
||||
name: string
|
||||
source: string
|
||||
signature?: string
|
||||
description: string
|
||||
author: string
|
||||
version?: string
|
||||
auto_push: string[]
|
||||
config: Record<string, string>
|
||||
auto_start: boolean
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
// 规则配置模式集合
|
||||
export type RuleSchemasMap = Record<string, RuleSchema>
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
import {
|
||||
ArrowBackOutline, CreateOutline, TrashOutline,
|
||||
PushOutline, PowerOutline, AddOutline, SaveOutline, CloseOutline,
|
||||
DownloadOutline, SettingsOutline, StorefrontOutline
|
||||
DownloadOutline, SettingsOutline, StorefrontOutline, RefreshOutline, StopOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import {
|
||||
getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient,
|
||||
getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, restartClient,
|
||||
getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig,
|
||||
getStorePlugins, installStorePlugin
|
||||
getStorePlugins, installStorePlugin, getRuleSchemas, restartClientPlugin, stopClientPlugin
|
||||
} from '../api'
|
||||
import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField, RuleSchema, StorePluginInfo } from '../types'
|
||||
import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField, StorePluginInfo, RuleSchemasMap } from '../types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -49,16 +49,23 @@ const builtinTypes = [
|
||||
// 规则类型选项(内置 + 插件)
|
||||
const typeOptions = ref([...builtinTypes])
|
||||
|
||||
// 插件 RuleSchema 映射
|
||||
const pluginRuleSchemas = ref<Record<string, RuleSchema>>({})
|
||||
// 插件 RuleSchema 映射(包含内置类型和插件类型)
|
||||
const pluginRuleSchemas = ref<RuleSchemasMap>({})
|
||||
|
||||
// 加载规则配置模式
|
||||
const loadRuleSchemas = async () => {
|
||||
try {
|
||||
const { data } = await getRuleSchemas()
|
||||
pluginRuleSchemas.value = data || {}
|
||||
} catch (e) {
|
||||
console.error('Failed to load rule schemas', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 判断类型是否需要本地地址
|
||||
const needsLocalAddr = (type: string) => {
|
||||
// 内置类型
|
||||
if (['tcp', 'udp'].includes(type)) return true
|
||||
// 插件类型:查询 RuleSchema
|
||||
const schema = pluginRuleSchemas.value[type]
|
||||
return schema?.needs_local_addr ?? false
|
||||
return schema?.needs_local_addr ?? true // 默认需要
|
||||
}
|
||||
|
||||
// 获取类型的额外字段
|
||||
@@ -97,14 +104,12 @@ const loadPlugins = async () => {
|
||||
.map(p => ({ label: `${p.name.toUpperCase()} (插件)`, value: p.name }))
|
||||
typeOptions.value = [...builtinTypes, ...proxyPlugins]
|
||||
|
||||
// 保存插件的 RuleSchema
|
||||
const schemas: Record<string, RuleSchema> = {}
|
||||
// 合并插件的 RuleSchema 到 pluginRuleSchemas
|
||||
for (const p of availablePlugins.value) {
|
||||
if (p.rule_schema) {
|
||||
schemas[p.name] = p.rule_schema
|
||||
pluginRuleSchemas.value[p.name] = p.rule_schema
|
||||
}
|
||||
}
|
||||
pluginRuleSchemas.value = schemas
|
||||
} catch (e) {
|
||||
console.error('Failed to load plugins', e)
|
||||
}
|
||||
@@ -175,6 +180,7 @@ const loadClient = async () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRuleSchemas() // 加载内置协议配置模式
|
||||
loadClient()
|
||||
loadPlugins()
|
||||
})
|
||||
@@ -289,6 +295,50 @@ const disconnect = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 重启客户端
|
||||
const handleRestartClient = () => {
|
||||
dialog.warning({
|
||||
title: '确认重启',
|
||||
content: '确定要重启此客户端吗?客户端将断开连接并自动重连。',
|
||||
positiveText: '重启',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await restartClient(clientId)
|
||||
message.success('重启命令已发送,客户端将自动重连')
|
||||
setTimeout(() => loadClient(), 3000)
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data || '重启失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重启客户端插件
|
||||
const handleRestartPlugin = async (plugin: ClientPlugin) => {
|
||||
// 找到使用此插件的规则
|
||||
const rule = rules.value.find(r => r.type === plugin.name)
|
||||
const ruleName = rule?.name || ''
|
||||
try {
|
||||
await restartClientPlugin(clientId, plugin.name, ruleName)
|
||||
message.success(`已重启 ${plugin.name}`)
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data || '重启失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 停止客户端插件
|
||||
const handleStopPlugin = async (plugin: ClientPlugin) => {
|
||||
const rule = rules.value.find(r => r.type === plugin.name)
|
||||
const ruleName = rule?.name || ''
|
||||
try {
|
||||
await stopClientPlugin(clientId, plugin.name, ruleName)
|
||||
message.success(`已停止 ${plugin.name}`)
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data || '停止失败')
|
||||
}
|
||||
}
|
||||
|
||||
const installPlugins = async () => {
|
||||
if (selectedPlugins.value.length === 0) {
|
||||
message.warning('请选择要安装的插件')
|
||||
@@ -403,6 +453,10 @@ const savePluginConfig = async () => {
|
||||
<template #icon><n-icon><PowerOutline /></n-icon></template>
|
||||
断开连接
|
||||
</n-button>
|
||||
<n-button type="error" @click="handleRestartClient">
|
||||
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||
重启客户端
|
||||
</n-button>
|
||||
</template>
|
||||
<template v-if="!editing">
|
||||
<n-button type="primary" @click="startEdit">
|
||||
@@ -496,12 +550,45 @@ const savePluginConfig = async () => {
|
||||
<!-- 插件额外字段 -->
|
||||
<template v-for="field in getExtraFields(rule.type || '')" :key="field.key">
|
||||
<n-form-item :label="field.label" :show-feedback="false">
|
||||
<!-- 字符串输入 -->
|
||||
<n-input
|
||||
v-if="field.type === 'string'"
|
||||
:value="rule.plugin_config?.[field.key] || field.default || ''"
|
||||
@update:value="(v: string) => { if (!rule.plugin_config) rule.plugin_config = {}; rule.plugin_config[field.key] = v }"
|
||||
:placeholder="field.description"
|
||||
/>
|
||||
<!-- 密码输入 -->
|
||||
<n-input
|
||||
v-else-if="field.type === 'password'"
|
||||
:value="rule.plugin_config?.[field.key] || ''"
|
||||
@update:value="(v: string) => { if (!rule.plugin_config) rule.plugin_config = {}; rule.plugin_config[field.key] = v }"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="field.description"
|
||||
/>
|
||||
<!-- 数字输入 -->
|
||||
<n-input-number
|
||||
v-else-if="field.type === 'number'"
|
||||
:value="rule.plugin_config?.[field.key] ? Number(rule.plugin_config[field.key]) : undefined"
|
||||
@update:value="(v: number | null) => { if (!rule.plugin_config) rule.plugin_config = {}; rule.plugin_config[field.key] = v !== null ? String(v) : '' }"
|
||||
:placeholder="field.description"
|
||||
:show-button="false"
|
||||
style="width: 120px;"
|
||||
/>
|
||||
<!-- 布尔开关 -->
|
||||
<n-switch
|
||||
v-else-if="field.type === 'bool'"
|
||||
:value="rule.plugin_config?.[field.key] === 'true'"
|
||||
@update:value="(v: boolean) => { if (!rule.plugin_config) rule.plugin_config = {}; rule.plugin_config[field.key] = String(v) }"
|
||||
/>
|
||||
<!-- 下拉选择 -->
|
||||
<n-select
|
||||
v-else-if="field.type === 'select'"
|
||||
:value="rule.plugin_config?.[field.key] || field.default"
|
||||
@update:value="(v: string) => { if (!rule.plugin_config) rule.plugin_config = {}; rule.plugin_config[field.key] = v }"
|
||||
:options="(field.options || []).map(o => ({ label: o, value: o }))"
|
||||
style="width: 120px;"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
<n-button v-if="editRules.length > 1" quaternary type="error" @click="removeRule(i)">
|
||||
@@ -537,10 +624,20 @@ const savePluginConfig = async () => {
|
||||
<n-switch :value="plugin.enabled" @update:value="toggleClientPlugin(plugin)" />
|
||||
</td>
|
||||
<td>
|
||||
<n-button size="small" quaternary @click="openConfigModal(plugin)">
|
||||
<template #icon><n-icon><SettingsOutline /></n-icon></template>
|
||||
配置
|
||||
</n-button>
|
||||
<n-space :size="4">
|
||||
<n-button size="small" quaternary @click="openConfigModal(plugin)">
|
||||
<template #icon><n-icon><SettingsOutline /></n-icon></template>
|
||||
配置
|
||||
</n-button>
|
||||
<n-button v-if="online && plugin.enabled" size="small" quaternary type="info" @click="handleRestartPlugin(plugin)">
|
||||
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||
重启
|
||||
</n-button>
|
||||
<n-button v-if="online && plugin.enabled" size="small" quaternary type="warning" @click="handleStopPlugin(plugin)">
|
||||
<template #icon><n-icon><StopOutline /></n-icon></template>
|
||||
停止
|
||||
</n-button>
|
||||
</n-space>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useRouter } from 'vue-router'
|
||||
import {
|
||||
NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi,
|
||||
NEmpty, NSpin, NIcon, NSwitch, NTabs, NTabPane, useMessage,
|
||||
NSelect, NModal
|
||||
NSelect, NModal, NInput
|
||||
} from 'naive-ui'
|
||||
import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline, CodeSlashOutline } from '@vicons/ionicons5'
|
||||
import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline, CodeSlashOutline, SettingsOutline } from '@vicons/ionicons5'
|
||||
import {
|
||||
getPlugins, enablePlugin, disablePlugin, getStorePlugins, getJSPlugins,
|
||||
pushJSPluginToClient, getClients, installStorePlugin
|
||||
pushJSPluginToClient, getClients, installStorePlugin, updateJSPluginConfig, setJSPluginEnabled
|
||||
} from '../api'
|
||||
import type { PluginInfo, StorePluginInfo, JSPlugin, ClientStatus } from '../types'
|
||||
|
||||
@@ -142,6 +142,68 @@ const handlePushJSPlugin = async (pluginName: string, clientId: string) => {
|
||||
|
||||
const onlineClients = computed(() => clients.value.filter(c => c.online))
|
||||
|
||||
// JS 插件配置相关
|
||||
const showJSConfigModal = ref(false)
|
||||
const currentJSPlugin = ref<JSPlugin | null>(null)
|
||||
const jsConfigItems = ref<Array<{ key: string; value: string }>>([])
|
||||
const jsConfigSaving = ref(false)
|
||||
|
||||
const openJSConfigModal = (plugin: JSPlugin) => {
|
||||
currentJSPlugin.value = plugin
|
||||
// 将 config 转换为数组形式便于编辑
|
||||
jsConfigItems.value = Object.entries(plugin.config || {}).map(([key, value]) => ({ key, value }))
|
||||
if (jsConfigItems.value.length === 0) {
|
||||
jsConfigItems.value.push({ key: '', value: '' })
|
||||
}
|
||||
showJSConfigModal.value = true
|
||||
}
|
||||
|
||||
const addJSConfigItem = () => {
|
||||
jsConfigItems.value.push({ key: '', value: '' })
|
||||
}
|
||||
|
||||
const removeJSConfigItem = (index: number) => {
|
||||
jsConfigItems.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const saveJSPluginConfig = async () => {
|
||||
if (!currentJSPlugin.value) return
|
||||
|
||||
jsConfigSaving.value = true
|
||||
try {
|
||||
// 将数组转换回对象
|
||||
const config: Record<string, string> = {}
|
||||
for (const item of jsConfigItems.value) {
|
||||
if (item.key.trim()) {
|
||||
config[item.key.trim()] = item.value
|
||||
}
|
||||
}
|
||||
await updateJSPluginConfig(currentJSPlugin.value.name, config)
|
||||
// 更新本地数据
|
||||
const plugin = jsPlugins.value.find(p => p.name === currentJSPlugin.value!.name)
|
||||
if (plugin) {
|
||||
plugin.config = config
|
||||
}
|
||||
message.success('配置已保存')
|
||||
showJSConfigModal.value = false
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data || '保存失败')
|
||||
} finally {
|
||||
jsConfigSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换 JS 插件启用状态
|
||||
const toggleJSPlugin = async (plugin: JSPlugin) => {
|
||||
try {
|
||||
await setJSPluginEnabled(plugin.name, !plugin.enabled)
|
||||
plugin.enabled = !plugin.enabled
|
||||
message.success(plugin.enabled ? `已启用 ${plugin.name}` : `已禁用 ${plugin.name}`)
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 商店插件安装相关
|
||||
const showInstallModal = ref(false)
|
||||
const selectedStorePlugin = ref<StorePluginInfo | null>(null)
|
||||
@@ -246,8 +308,8 @@ onMounted(() => {
|
||||
<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 size="small" :type="plugin.source === 'builtin' ? 'default' : 'info'">
|
||||
{{ plugin.source === 'builtin' ? '内置' : 'JS' }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<p style="margin: 0; color: #666;">{{ plugin.description }}</p>
|
||||
@@ -301,15 +363,6 @@ onMounted(() => {
|
||||
|
||||
<!-- JS 插件 -->
|
||||
<n-tab-pane name="js" tab="JS 插件">
|
||||
<!-- 安全加固:暂时禁用 Web UI 创建功能
|
||||
<n-space justify="end" style="margin-bottom: 16px;">
|
||||
<n-button type="primary" @click="showJSModal = true">
|
||||
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||||
新建 JS 插件
|
||||
</n-button>
|
||||
</n-space>
|
||||
-->
|
||||
|
||||
<n-spin :show="jsLoading">
|
||||
<n-empty v-if="!jsLoading && jsPlugins.length === 0" description="暂无 JS 插件" />
|
||||
|
||||
@@ -320,36 +373,47 @@ onMounted(() => {
|
||||
<n-space align="center">
|
||||
<n-icon size="24" color="#f0a020"><CodeSlashOutline /></n-icon>
|
||||
<span>{{ plugin.name }}</span>
|
||||
<n-tag v-if="plugin.version" size="small">v{{ plugin.version }}</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-select
|
||||
v-if="onlineClients.length > 0"
|
||||
placeholder="推送到..."
|
||||
size="small"
|
||||
style="width: 120px;"
|
||||
:options="onlineClients.map(c => ({ label: c.nickname || c.id, value: c.id }))"
|
||||
@update:value="(v: string) => handlePushJSPlugin(plugin.name, v)"
|
||||
/>
|
||||
<!-- 安全加固:暂时禁用删除功能
|
||||
<n-popconfirm @positive-click="handleDeleteJSPlugin(plugin.name)">
|
||||
<template #trigger>
|
||||
<n-button size="small" type="error" quaternary>删除</n-button>
|
||||
</template>
|
||||
确定删除此插件?
|
||||
</n-popconfirm>
|
||||
-->
|
||||
</n-space>
|
||||
<n-switch :value="plugin.enabled" @update:value="toggleJSPlugin(plugin)" />
|
||||
</template>
|
||||
<n-space vertical :size="8">
|
||||
<n-space>
|
||||
<n-tag size="small" type="warning">JS</n-tag>
|
||||
<n-tag v-if="plugin.auto_start" size="small" type="success">自动启动</n-tag>
|
||||
<n-tag v-if="plugin.signature" size="small" type="info">已签名</n-tag>
|
||||
</n-space>
|
||||
<p style="margin: 0; color: #666;">{{ plugin.description || '无描述' }}</p>
|
||||
<p v-if="plugin.author" style="margin: 0; color: #999; font-size: 12px;">作者: {{ plugin.author }}</p>
|
||||
|
||||
<!-- 配置预览 -->
|
||||
<div v-if="Object.keys(plugin.config || {}).length > 0" style="margin-top: 8px;">
|
||||
<p style="margin: 0 0 4px 0; color: #999; font-size: 12px;">配置:</p>
|
||||
<n-space :size="4" wrap>
|
||||
<n-tag v-for="(value, key) in plugin.config" :key="key" size="small" type="default">
|
||||
{{ key }}: {{ value.length > 10 ? value.slice(0, 10) + '...' : value }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</div>
|
||||
</n-space>
|
||||
<template #action>
|
||||
<n-space justify="space-between">
|
||||
<n-button size="small" quaternary @click="openJSConfigModal(plugin)">
|
||||
<template #icon><n-icon><SettingsOutline /></n-icon></template>
|
||||
配置
|
||||
</n-button>
|
||||
<n-select
|
||||
v-if="onlineClients.length > 0"
|
||||
placeholder="推送到..."
|
||||
size="small"
|
||||
style="width: 140px;"
|
||||
:options="onlineClients.map(c => ({ label: c.nickname || c.id, value: c.id }))"
|
||||
@update:value="(v: string) => handlePushJSPlugin(plugin.name, v)"
|
||||
/>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
@@ -390,5 +454,28 @@ onMounted(() => {
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- JS 插件配置模态框 -->
|
||||
<n-modal v-model:show="showJSConfigModal" preset="card" :title="`${currentJSPlugin?.name || ''} 配置`" style="width: 500px;">
|
||||
<n-space vertical :size="12">
|
||||
<p style="margin: 0; color: #666; font-size: 13px;">编辑插件配置参数(键值对形式)</p>
|
||||
<div v-for="(item, index) in jsConfigItems" :key="index">
|
||||
<n-space :size="8" align="center">
|
||||
<n-input v-model:value="item.key" placeholder="参数名" style="width: 150px;" />
|
||||
<n-input v-model:value="item.value" placeholder="参数值" style="width: 200px;" />
|
||||
<n-button v-if="jsConfigItems.length > 1" quaternary type="error" size="small" @click="removeJSConfigItem(index)">
|
||||
删除
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<n-button dashed size="small" @click="addJSConfigItem">添加配置项</n-button>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showJSConfigModal = false">取消</n-button>
|
||||
<n-button type="primary" :loading="jsConfigSaving" @click="saveJSPluginConfig">保存</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user