-
- GoTunnel
+
+
+
+
+ GoTunnel
-
-
-
- {{ serverInfo.bind_addr }}:{{ serverInfo.bind_port }}
-
-
- {{ clientCount }} 客户端
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Server:
+ {{ serverInfo.bind_addr }}:{{ serverInfo.bind_port }}
+
+
+ Clients:
+ {{ clientCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/ClientView.vue b/web/src/views/ClientView.vue
index 5a8dbd2..b3e846f 100644
--- a/web/src/views/ClientView.vue
+++ b/web/src/views/ClientView.vue
@@ -3,13 +3,14 @@ import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
NCard, NButton, NSpace, NTag, NTable, NEmpty,
- NFormItem, NInput, NInputNumber, NSelect, NModal, NSwitch,
- NIcon, useMessage, useDialog, NSpin, NAlert
+ NForm, NFormItem, NInput, NInputNumber, NSelect, NModal, NSwitch,
+ NIcon, useMessage, useDialog, NSpin, NGrid, NGridItem,
+ NStatistic, NDivider, NTooltip, NDropdown, type FormInst, type FormRules
} from 'naive-ui'
import {
ArrowBackOutline, CreateOutline, TrashOutline,
- PushOutline, PowerOutline, AddOutline, SaveOutline, CloseOutline,
- SettingsOutline, StorefrontOutline, RefreshOutline, StopOutline, PlayOutline, DocumentTextOutline
+ PushOutline, AddOutline, StorefrontOutline, DocumentTextOutline,
+ ExtensionPuzzleOutline, SettingsOutline, OpenOutline
} from '@vicons/ionicons5'
import {
getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, restartClient,
@@ -25,35 +26,17 @@ const message = useMessage()
const dialog = useDialog()
const clientId = route.params.id as string
+// Data
const online = ref(false)
const lastPing = ref('')
const remoteAddr = ref('')
const nickname = ref('')
const rules = ref
([])
const clientPlugins = ref([])
-const editing = ref(false)
-const editRules = ref([])
+const loading = ref(false)
-// 重命名相关
-const showRenameModal = ref(false)
-const renameValue = ref('')
-
-// 内置类型
-const builtinTypes = [
- { label: 'TCP', value: 'tcp' },
- { label: 'UDP', value: 'udp' },
- { label: 'HTTP', value: 'http' },
- { label: 'HTTPS', value: 'https' },
- { label: 'SOCKS5', value: 'socks5' }
-]
-
-// 规则类型选项(内置 + 插件)
-const typeOptions = ref([...builtinTypes])
-
-// 插件 RuleSchema 映射(包含内置类型和插件类型)
+// Rule Schemas
const pluginRuleSchemas = ref({})
-
-// 加载规则配置模式
const loadRuleSchemas = async () => {
try {
const { data } = await getRuleSchemas()
@@ -63,32 +46,194 @@ const loadRuleSchemas = async () => {
}
}
-// 判断类型是否需要本地地址
+// Built-in Types (Added WebSocket)
+const builtinTypes = [
+ { label: 'TCP', value: 'tcp' },
+ { label: 'UDP', value: 'udp' },
+ { label: 'HTTP', value: 'http' },
+ { label: 'HTTPS', value: 'https' },
+ { label: 'SOCKS5', value: 'socks5' },
+ { label: 'WebSocket', value: 'websocket' }
+]
+
+// Modal Control for Rules
+const showRuleModal = ref(false)
+const ruleModalType = ref<'create' | 'edit'>('create')
+const ruleFormRef = ref(null)
+// Default Rule Model
+const defaultRule = {
+ name: '',
+ local_ip: '127.0.0.1',
+ local_port: 80,
+ remote_port: 0, // 0 means unset/placeholder
+ type: 'tcp',
+ enabled: true,
+ plugin_config: {} as Record
+}
+const ruleForm = ref({ ...defaultRule })
+
+// Helper: Check if type needs local addr
const needsLocalAddr = (type: string) => {
const schema = pluginRuleSchemas.value[type]
- return schema?.needs_local_addr ?? true // 默认需要
+ return schema?.needs_local_addr ?? true
}
-// 获取类型的额外字段
const getExtraFields = (type: string): ConfigField[] => {
const schema = pluginRuleSchemas.value[type]
return schema?.extra_fields || []
}
-// 插件配置相关
-const showConfigModal = ref(false)
-const configPluginName = ref('')
-const configSchema = ref([])
-const configValues = ref>({})
-const configLoading = ref(false)
+// Validation Rules
+const ruleValidationRules: FormRules = {
+ name: { required: true, message: '请输入规则名称', trigger: 'blur' },
+ type: { required: true, message: '请选择类型', trigger: ['blur', 'change'] },
+ remote_port: [
+ { required: true, type: 'number', message: '请输入远程端口', trigger: ['blur', 'change'] },
+ { type: 'number', min: 1, max: 65535, message: '端口范围 1-65535', trigger: ['blur', 'change'] }
+ ],
+ local_ip: {
+ required: true,
+ validator(_rule, value) {
+ if (needsLocalAddr(ruleForm.value.type || 'tcp')) {
+ if (!value) return new Error('请输入本地IP')
+ }
+ return true
+ },
+ trigger: 'blur'
+ },
+ local_port: {
+ required: true,
+ validator(_rule, value) {
+ if (needsLocalAddr(ruleForm.value.type || 'tcp')) {
+ if (!value && value !== 0) return new Error('请输入本地端口')
+ if (typeof value === 'number' && (value < 1 || value > 65535)) return new Error('端口范围 1-65535')
+ }
+ return true
+ },
+ trigger: ['blur', 'change']
+ }
+}
-// 商店插件安装相关
+// Actions
+const loadClient = async () => {
+ loading.value = true
+ try {
+ const { data } = await getClient(clientId)
+ online.value = data.online
+ lastPing.value = data.last_ping || ''
+ remoteAddr.value = data.remote_addr || ''
+ nickname.value = data.nickname || ''
+ rules.value = data.rules || []
+ clientPlugins.value = data.plugins || []
+ } catch (e) {
+ message.error('加载客户端信息失败')
+ console.error(e)
+ } finally {
+ loading.value = false
+ }
+}
+
+// Client Rename
+const showRenameModal = ref(false)
+const renameValue = ref('')
+const openRenameModal = () => {
+ renameValue.value = nickname.value
+ showRenameModal.value = true
+}
+const saveRename = async () => {
+ try {
+ await updateClient(clientId, {
+ id: clientId,
+ nickname: renameValue.value,
+ rules: rules.value
+ })
+ nickname.value = renameValue.value
+ showRenameModal.value = false
+ message.success('重命名成功')
+ } catch (e) {
+ message.error('重命名失败')
+ }
+}
+
+// Rule Management
+const openCreateRule = () => {
+ ruleModalType.value = 'create'
+ ruleForm.value = { ...defaultRule, remote_port: 8080 } // Reset
+ showRuleModal.value = true
+}
+
+const openEditRule = (rule: ProxyRule) => {
+ if (rule.plugin_managed) return
+ ruleModalType.value = 'edit'
+ // Deep copy to avoid modifying original until saved
+ ruleForm.value = JSON.parse(JSON.stringify(rule))
+ showRuleModal.value = true
+}
+
+const handleDeleteRule = (rule: ProxyRule) => {
+ dialog.warning({
+ title: '确认删除',
+ content: `确定要删除规则 "${rule.name}" 吗?`,
+ positiveText: '删除',
+ negativeText: '取消',
+ onPositiveClick: async () => {
+ const newRules = rules.value.filter(r => r.name !== rule.name)
+ await saveRules(newRules)
+ }
+ })
+}
+
+const saveRules = async (newRules: ProxyRule[]) => {
+ try {
+ await updateClient(clientId, {
+ id: clientId,
+ nickname: nickname.value,
+ rules: newRules
+ })
+ rules.value = newRules
+ message.success('规则保存成功')
+ if (online.value) {
+ await pushConfigToClient(clientId)
+ message.success('配置已推送到客户端')
+ }
+ } catch (e: any) {
+ message.error('保存失败: ' + (e.response?.data || e.message))
+ await loadClient() // Revert on failure
+ }
+}
+
+const handleRuleSubmit = (e: MouseEvent) => {
+ e.preventDefault()
+ ruleFormRef.value?.validate(async (errors) => {
+ if (!errors) {
+ // Logic to merge rule
+ let newRules = [...rules.value]
+ if (ruleModalType.value === 'create') {
+ // Check duplicate name
+ if (newRules.some(r => r.name === ruleForm.value.name)) {
+ message.error('规则名称已存在')
+ return
+ }
+ newRules.push({ ...ruleForm.value })
+ } else {
+ const index = newRules.findIndex(r => r.name === ruleForm.value.name)
+ if (index > -1) {
+ newRules[index] = { ...ruleForm.value }
+ }
+ }
+ await saveRules(newRules)
+ showRuleModal.value = false
+ } else {
+ message.error('请检查表单填写')
+ }
+ })
+}
+
+// Store & Plugin Logic
const showStoreModal = ref(false)
const storePlugins = ref([])
const storeLoading = ref(false)
-const storeInstalling = ref(null) // 正在安装的插件名称
-
-// 安装配置模态框
+const storeInstalling = ref(null)
const showInstallConfigModal = ref(false)
const installPlugin = ref(null)
const installRemotePort = ref(8080)
@@ -96,41 +241,25 @@ const installAuthEnabled = ref(false)
const installAuthUsername = ref('')
const installAuthPassword = ref('')
-// 日志查看相关
-const showLogViewer = ref(false)
-
-// 商店插件相关函数
const openStoreModal = async () => {
showStoreModal.value = true
storeLoading.value = true
try {
const { data } = await getStorePlugins()
- storePlugins.value = (data.plugins || []).filter(p => p.download_url)
+ storePlugins.value = (data.plugins || []).filter((p: any) => p.download_url)
} catch (e) {
- console.error('Failed to load store plugins', e)
- message.error('加载商店插件失败')
+ message.error('加载商店失败')
} finally {
storeLoading.value = false
}
}
-
-const handleInstallStorePlugin = async (plugin: StorePluginInfo) => {
- if (!plugin.download_url) {
- message.error('该插件没有下载地址')
- return
- }
- // 打开配置模态框
+const handleInstallStorePlugin = (plugin: StorePluginInfo) => {
installPlugin.value = plugin
installRemotePort.value = 8080
- installAuthEnabled.value = false
- installAuthUsername.value = ''
- installAuthPassword.value = ''
showInstallConfigModal.value = true
}
-
const confirmInstallPlugin = async () => {
if (!installPlugin.value) return
-
storeInstalling.value = installPlugin.value.name
try {
await installStorePlugin(
@@ -156,193 +285,12 @@ const confirmInstallPlugin = async () => {
}
}
-const loadClient = async () => {
- try {
- const { data } = await getClient(clientId)
- online.value = data.online
- lastPing.value = data.last_ping || ''
- remoteAddr.value = data.remote_addr || ''
- nickname.value = data.nickname || ''
- rules.value = data.rules || []
- clientPlugins.value = data.plugins || []
- } catch (e) {
- console.error('Failed to load client', e)
- }
-}
-
-onMounted(() => {
- loadRuleSchemas() // 加载内置协议配置模式
- loadClient()
-})
-
-// 打开重命名弹窗
-const openRenameModal = () => {
- renameValue.value = nickname.value
- showRenameModal.value = true
-}
-
-// 保存重命名
-const saveRename = async () => {
- try {
- await updateClient(clientId, {
- id: clientId,
- nickname: renameValue.value,
- rules: rules.value
- })
- nickname.value = renameValue.value
- showRenameModal.value = false
- message.success('重命名成功')
- } catch (e) {
- message.error('重命名失败')
- }
-}
-
-const startEdit = () => {
- editRules.value = rules.value.map(rule => ({
- ...rule,
- type: rule.type || 'tcp',
- enabled: rule.enabled !== false
- }))
- editing.value = true
-}
-
-const cancelEdit = () => {
- editing.value = false
-}
-
-const addRule = () => {
- editRules.value.push({
- name: '', local_ip: '127.0.0.1', local_port: 80, remote_port: 8080, type: 'tcp', enabled: true
- })
-}
-
-const removeRule = (index: number) => {
- editRules.value.splice(index, 1)
-}
-
-const saveEdit = async () => {
- try {
- // 合并插件管理的规则和编辑后的规则
- await updateClient(clientId, { id: clientId, nickname: nickname.value, rules: editRules.value })
- editing.value = false
- message.success('保存成功')
- await loadClient()
- // 如果客户端在线,自动推送配置
- if (online.value) {
- try {
- await pushConfigToClient(clientId)
- message.success('配置已自动推送到客户端')
- } catch (e: any) {
- message.warning('配置已保存,但推送失败: ' + (e.response?.data || '未知错误'))
- }
- }
- } catch (e) {
- message.error('保存失败')
- }
-}
-
-const confirmDelete = () => {
- dialog.warning({
- title: '确认删除',
- content: '确定要删除此客户端吗?',
- positiveText: '删除',
- negativeText: '取消',
- onPositiveClick: async () => {
- try {
- await deleteClient(clientId)
- message.success('删除成功')
- router.push('/')
- } catch (e) {
- message.error('删除失败')
- }
- }
- })
-}
-
-const pushConfig = async () => {
- try {
- await pushConfigToClient(clientId)
- message.success('配置已推送')
- } catch (e: any) {
- message.error(e.response?.data || '推送失败')
- }
-}
-
-const disconnect = () => {
- dialog.warning({
- title: '确认断开',
- content: '确定要断开此客户端连接吗?',
- positiveText: '断开',
- negativeText: '取消',
- onPositiveClick: async () => {
- try {
- await disconnectClient(clientId)
- online.value = false
- message.success('已断开连接')
- } catch (e: any) {
- message.error(e.response?.data || '断开失败')
- }
- }
- })
-}
-
-// 重启客户端
-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 handleStartPlugin = async (plugin: ClientPlugin) => {
- const rule = rules.value.find(r => r.type === plugin.name)
- const ruleName = rule?.name || plugin.name
- try {
- await startClientPlugin(clientId, plugin.id, ruleName)
- message.success(`已启动 ${plugin.name}`)
- plugin.running = true
- } catch (e: any) {
- message.error(e.message || '启动失败')
- }
-}
-
-// 重启客户端插件
-const handleRestartPlugin = async (plugin: ClientPlugin) => {
- // 找到使用此插件的规则
- const rule = rules.value.find(r => r.type === plugin.name)
- const ruleName = rule?.name || plugin.name
- try {
- await restartClientPlugin(clientId, plugin.id, ruleName)
- message.success(`已重启 ${plugin.name}`)
- plugin.running = true
- } catch (e: any) {
- message.error(e.message || '重启失败')
- }
-}
-
-// 停止客户端插件
-const handleStopPlugin = async (plugin: ClientPlugin) => {
- const rule = rules.value.find(r => r.type === plugin.name)
- const ruleName = rule?.name || plugin.name
- try {
- await stopClientPlugin(clientId, plugin.id, ruleName)
- message.success(`已停止 ${plugin.name}`)
- plugin.running = false
- } catch (e: any) {
- message.error(e.message || '停止失败')
- }
+// Plugin Actions
+const handleOpenPlugin = (plugin: ClientPlugin) => {
+ if (!plugin.remote_port) return
+ const hostname = window.location.hostname
+ const url = `http://${hostname}:${plugin.remote_port}`
+ window.open(url, '_blank')
}
const toggleClientPlugin = async (plugin: ClientPlugin) => {
@@ -364,33 +312,35 @@ const toggleClientPlugin = async (plugin: ClientPlugin) => {
}
}
-// 打开插件配置模态框
+// Plugin Config Modal
+const showConfigModal = ref(false)
+const configPluginName = ref('')
+const configSchema = ref([])
+const configValues = ref>({})
+const configLoading = ref(false)
const openConfigModal = async (plugin: ClientPlugin) => {
configPluginName.value = plugin.name
configLoading.value = true
showConfigModal.value = true
-
try {
const { data } = await getClientPluginConfig(clientId, plugin.name)
configSchema.value = data.schema || []
configValues.value = { ...data.config }
- // 填充默认值
- for (const field of configSchema.value) {
- if (field.default && !configValues.value[field.key]) {
- configValues.value[field.key] = field.default
+ // Fill defaults
+ configSchema.value.forEach(f => {
+ if (f.default && !configValues.value[f.key]) {
+ configValues.value[f.key] = f.default
}
- }
- } catch (e: any) {
- message.error(e.response?.data || '加载配置失败')
+ })
+ } catch (e) {
+ message.error('加载配置失败')
showConfigModal.value = false
} finally {
configLoading.value = false
}
}
-
-// 保存插件配置
const savePluginConfig = async () => {
- try {
+ try {
await updateClientPluginConfig(clientId, configPluginName.value, configValues.value)
message.success('配置已保存')
showConfigModal.value = false
@@ -400,429 +350,387 @@ const savePluginConfig = async () => {
}
}
-// 删除客户端插件
+// Standard Client Actions
+const confirmDelete = () => {
+ dialog.warning({
+ title: '确认删除', content: '确定要删除此客户端吗?',
+ positiveText: '删除', negativeText: '取消',
+ onPositiveClick: async () => {
+ await deleteClient(clientId); router.push('/')
+ }
+ })
+}
+const disconnect = () => {
+ dialog.warning({
+ title: '确认断开', content: '确定要断开连接吗?',
+ positiveText: '断开', negativeText: '取消',
+ onPositiveClick: async () => {
+ await disconnectClient(clientId); loadClient()
+ }
+ })
+}
+const handleRestartClient = () => {
+ dialog.warning({
+ title: '确认重启', content: '确定要重启客户端吗?',
+ positiveText: '重启', negativeText: '取消',
+ onPositiveClick: async () => {
+ await restartClient(clientId); message.success('重启命令已发送'); setTimeout(loadClient, 3000)
+ }
+ })
+}
+
+// Lifecycle
+onMounted(() => {
+ loadRuleSchemas()
+ loadClient()
+})
+
+// Log Viewer
+const showLogViewer = ref(false)
+
+// Plugin Status Actions
+const handleStartPlugin = async (plugin: ClientPlugin) => {
+ const rule = rules.value.find(r => r.type === plugin.name)
+ const ruleName = rule?.name || plugin.name
+ try { await startClientPlugin(clientId, plugin.id, ruleName); message.success('已启动'); plugin.running = true } catch(e:any){ message.error(e.message) }
+}
+const handleRestartPlugin = async (plugin: ClientPlugin) => {
+ const rule = rules.value.find(r => r.type === plugin.name)
+ const ruleName = rule?.name || plugin.name
+ try { await restartClientPlugin(clientId, plugin.id, ruleName); message.success('已重启'); plugin.running = true } catch(e:any){ message.error(e.message)}
+}
+const handleStopPlugin = async (plugin: ClientPlugin) => {
+ const rule = rules.value.find(r => r.type === plugin.name)
+ const ruleName = rule?.name || plugin.name
+ try { await stopClientPlugin(clientId, plugin.id, ruleName); message.success('已停止'); plugin.running = false } catch(e:any){ message.error(e.message)}
+}
const handleDeletePlugin = (plugin: ClientPlugin) => {
- dialog.warning({
- title: '确认删除',
- content: `确定要删除插件 ${plugin.name} 吗?`,
- positiveText: '删除',
- negativeText: '取消',
- onPositiveClick: async () => {
- try {
- await deleteClientPlugin(clientId, plugin.id)
- message.success(`已删除 ${plugin.name}`)
- await loadClient()
- } catch (e: any) {
- message.error(e.response?.data || '删除失败')
- }
- }
- })
+ dialog.warning({
+ title: '确认删除', content: `确定要删除插件 ${plugin.name} 吗?`,
+ positiveText: '删除', negativeText: '取消',
+ onPositiveClick: async () => {
+ await deleteClientPlugin(clientId, plugin.id); message.success('已删除'); loadClient()
+ }
+ })
}
-
-
-
-
-
-
- 返回
-
-
- {{ nickname || clientId }}
-
-
- {{ clientId }}
-
- {{ online ? '在线' : '离线' }}
-
-
- IP: {{ remoteAddr }}
-
-
- 最后心跳: {{ lastPing }}
-
-
-
-
-
-
- 推送配置
-
-
-
- 查看日志
-
-
-
- 从商店安装
-
-
-
- 断开连接
-
-
-
- 重启客户端
-
-
-
-
-
- 编辑规则
-
-
-
- 删除
-
-
-
-
-
+
+
-
-
-
-
-
-
- 取消
-
-
-
- 保存
-
-
-
+
-
-
-
-
-
-
- | 名称 |
- 本地地址 |
- 远程端口 |
- 类型 |
- 状态 |
- 来源 |
-
-
-
-
- | {{ rule.name || '未命名' }} |
-
-
- {{ rule.local_ip }}:{{ rule.local_port }}
-
- -
- |
- {{ rule.remote_port }} |
- {{ (rule.type || 'tcp').toUpperCase() }} |
-
-
- {{ rule.enabled !== false ? '启用' : '禁用' }}
-
- |
-
- 插件
- 手动
- |
-
-
-
-
+
+
+
+
+
+
+
+ {{ clientId }}
+
+
+ {{ remoteAddr || '-' }}
+
+
+ {{ lastPing ? new Date(lastPing).toLocaleTimeString() : '-' }}
+
+
+
+
+ 断开连接
+ 重启客户端
+
+
+
-
-
-
-
-
- 此规则由插件创建,禁止修改。如需修改请前往插件管理页面。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- { if (!rule.plugin_config) rule.plugin_config = {}; rule.plugin_config[field.key] = v }"
- :placeholder="field.description"
- :disabled="!!rule.plugin_managed"
- />
-
- { if (!rule.plugin_config) rule.plugin_config = {}; rule.plugin_config[field.key] = v }"
- type="password"
- show-password-on="click"
- :placeholder="field.description"
- :disabled="!!rule.plugin_managed"
- />
-
- { 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;"
- :disabled="!!rule.plugin_managed"
- />
-
- { if (!rule.plugin_config) rule.plugin_config = {}; rule.plugin_config[field.key] = String(v) }"
- :disabled="!!rule.plugin_managed"
- />
-
- { 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;"
- :disabled="!!rule.plugin_managed"
- />
-
-
-
-
-
+
+
+
+
-
-
- 添加规则
-
-
-
+
-
-
-
-
-
-
- | 名称 |
- 版本 |
- 状态 |
- 启用 |
- 操作 |
-
-
-
-
- | {{ plugin.name }} |
- v{{ plugin.version }} |
-
- 运行中
- 已停止
- |
-
-
- |
-
-
-
-
- 配置
-
-
-
- 重启
-
-
-
- 启动
-
-
-
- 停止
-
-
-
- 删除
-
-
- |
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- configValues[field.key] = v !== null ? String(v) : ''"
- :placeholder="field.description"
- style="width: 100%;"
- />
-
-
-
- configValues[field.key] = String(v)"
- />
-
- {{ field.description }}
-
+
+
+
+
+
+ 添加规则
+
+
+
+
+
+
+
+ | 名称 |
+ 类型 |
+ 映射 |
+ 状态 |
+ 操作 |
+
+
+
+
+ | {{ rule.name }} |
+ {{ (rule.type || 'tcp').toUpperCase() }} |
+
+ {{ needsLocalAddr(rule.type||'tcp') ? `${rule.local_ip}:${rule.local_port}` : '-' }}
+
+ :{{ rule.remote_port }}
+ |
+
+ { rule.enabled = v; saveRules(rules) }" size="small" />
+ |
+
+
+
+
+ 插件托管
+
+ 此规则由插件管理,无法手动编辑
+
+
+ 编辑
+ 删除
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ 插件商店
+
+
+
+
+
+
+
+ | 名称 |
+ 版本 |
+ 端口 |
+ 状态 |
+ 启用 |
+ 操作 |
+
+
+
+
+ |
+
+
+ {{ plugin.name }}
+
+ |
+ v{{ plugin.version }} |
+ {{ plugin.remote_port || '-' }} |
+
+
+ {{ plugin.running ? '运行中' : '已停止' }}
+
+ |
+
+
+ |
+
+
+
+
+ 打开
+
+ 启动
+ {
+ if(k==='restart') handleRestartPlugin(plugin);
+ if(k==='config') openConfigModal(plugin);
+ if(k==='delete') handleDeletePlugin(plugin);
+ if(k==='stop') handleStopPlugin(plugin);
+ }">
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- 取消
-
- 保存
-
-
-
-
-
-
-
-
-
-
-
- 取消
- 保存
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ plugin.name }}
- v{{ plugin.version }}
-
- {{ plugin.description }}
- 作者: {{ plugin.author }}
-
-
- 安装
-
-
-
-
-
-
-
- 关闭
-
-
-
-
-
-
-
-
-
插件: {{ installPlugin.name }}
-
{{ installPlugin.description }}
-
-
-
-
-
- 启用 HTTP Basic Auth
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+ ruleForm.plugin_config![field.key] = String(v)" />
+
+
+
- 取消
-
- 安装
-
+ 取消
+ 保存
-
-
-
+
+
+
+
+
+
+
+ configValues[field.key] = String(v)" />
+ configValues[field.key] = String(v)" />
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+ {{ plugin.name }}
+ v{{ plugin.version }}
+
+
+ {{ plugin.description }}
+
+
+ 安装
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确认安装
+
+
+
+
+
+
+