All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 3m27s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 5m51s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 9m56s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 7m58s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 6m5s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 3m45s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 9m10s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 7m44s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 4m49s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 10m19s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 7m35s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 10m2s
577 lines
20 KiB
Vue
577 lines
20 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import {
|
||
NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi,
|
||
NEmpty, NSpin, NIcon, NSwitch, NTabs, NTabPane, useMessage,
|
||
NSelect, NModal, NInput, NInputNumber
|
||
} from 'naive-ui'
|
||
import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline, CodeSlashOutline, SettingsOutline } from '@vicons/ionicons5'
|
||
import {
|
||
getPlugins, enablePlugin, disablePlugin, getStorePlugins, getJSPlugins,
|
||
pushJSPluginToClient, getClients, installStorePlugin, updateJSPluginConfig, setJSPluginEnabled
|
||
} from '../api'
|
||
import type { PluginInfo, StorePluginInfo, JSPlugin, ClientStatus } from '../types'
|
||
|
||
const router = useRouter()
|
||
const message = useMessage()
|
||
const plugins = ref<PluginInfo[]>([])
|
||
const storePlugins = ref<StorePluginInfo[]>([])
|
||
const jsPlugins = ref<JSPlugin[]>([])
|
||
const clients = ref<ClientStatus[]>([])
|
||
const loading = ref(true)
|
||
const storeLoading = ref(false)
|
||
const jsLoading = ref(false)
|
||
const activeTab = ref('installed')
|
||
|
||
const loadPlugins = async () => {
|
||
try {
|
||
const { data } = await getPlugins()
|
||
plugins.value = data || []
|
||
} catch (e) {
|
||
console.error('Failed to load plugins', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const loadStorePlugins = async () => {
|
||
storeLoading.value = true
|
||
try {
|
||
const { data } = await getStorePlugins()
|
||
storePlugins.value = data.plugins || []
|
||
} catch (e) {
|
||
console.error('Failed to load store plugins', e)
|
||
} finally {
|
||
storeLoading.value = false
|
||
}
|
||
}
|
||
|
||
const proxyPlugins = computed(() =>
|
||
plugins.value.filter(p => p.type === 'proxy')
|
||
)
|
||
|
||
const appPlugins = computed(() =>
|
||
plugins.value.filter(p => p.type === 'app')
|
||
)
|
||
|
||
const togglePlugin = async (plugin: PluginInfo) => {
|
||
try {
|
||
if (plugin.enabled) {
|
||
await disablePlugin(plugin.name)
|
||
message.success(`已禁用 ${plugin.name}`)
|
||
} else {
|
||
await enablePlugin(plugin.name)
|
||
message.success(`已启用 ${plugin.name}`)
|
||
}
|
||
plugin.enabled = !plugin.enabled
|
||
} catch (e) {
|
||
message.error('操作失败')
|
||
}
|
||
}
|
||
|
||
const getTypeLabel = (type: string) => {
|
||
const labels: Record<string, string> = {
|
||
proxy: '协议',
|
||
app: '应用',
|
||
service: '服务',
|
||
tool: '工具'
|
||
}
|
||
return labels[type] || type
|
||
}
|
||
|
||
const getTypeColor = (type: string) => {
|
||
const colors: Record<string, 'info' | 'success' | 'warning' | 'error' | 'default'> = {
|
||
proxy: 'info',
|
||
app: 'success',
|
||
service: 'warning',
|
||
tool: 'default'
|
||
}
|
||
return colors[type] || 'default'
|
||
}
|
||
|
||
const handleTabChange = (tab: string) => {
|
||
if (tab === 'store' && storePlugins.value.length === 0) {
|
||
loadStorePlugins()
|
||
}
|
||
if (tab === 'js' && jsPlugins.value.length === 0) {
|
||
loadJSPlugins()
|
||
}
|
||
}
|
||
|
||
// JS 插件相关
|
||
/* 安全加固:暂时禁用创建/删除功能
|
||
const showJSModal = ref(false)
|
||
const jsForm = ref<JSPlugin>({...})
|
||
const configItems = ref<Array<{ key: string; value: string }>>([])
|
||
const configToObject = () => {...}
|
||
const handleCreateJSPlugin = async () => {...}
|
||
const handleDeleteJSPlugin = async (name: string) => {...}
|
||
const resetJSForm = () => {...}
|
||
*/
|
||
|
||
const loadJSPlugins = async () => {
|
||
jsLoading.value = true
|
||
try {
|
||
const { data } = await getJSPlugins()
|
||
jsPlugins.value = data || []
|
||
} catch (e) {
|
||
console.error('Failed to load JS plugins', e)
|
||
} finally {
|
||
jsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const loadClients = async () => {
|
||
try {
|
||
const { data } = await getClients()
|
||
clients.value = data || []
|
||
} catch (e) {
|
||
console.error('Failed to load clients', e)
|
||
}
|
||
}
|
||
|
||
// JS 插件推送相关
|
||
const showPushModal = ref(false)
|
||
const selectedJSPlugin = ref<JSPlugin | null>(null)
|
||
const pushClientId = ref('')
|
||
const pushRemotePort = ref<number | null>(8080)
|
||
const pushing = ref(false)
|
||
|
||
const openPushModal = (plugin: JSPlugin) => {
|
||
selectedJSPlugin.value = plugin
|
||
pushClientId.value = ''
|
||
pushRemotePort.value = 8080
|
||
showPushModal.value = true
|
||
}
|
||
|
||
const handlePushJSPlugin = async () => {
|
||
if (!selectedJSPlugin.value || !pushClientId.value) {
|
||
message.warning('请选择要推送到的客户端')
|
||
return
|
||
}
|
||
pushing.value = true
|
||
try {
|
||
await pushJSPluginToClient(selectedJSPlugin.value.name, pushClientId.value, pushRemotePort.value || 0)
|
||
message.success(`已推送 ${selectedJSPlugin.value.name} 到 ${pushClientId.value},监听端口: ${pushRemotePort.value || '未指定'}`)
|
||
showPushModal.value = false
|
||
} catch (e: any) {
|
||
message.error(e.response?.data || '推送失败')
|
||
} finally {
|
||
pushing.value = false
|
||
}
|
||
}
|
||
|
||
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)
|
||
const selectedClientId = ref('')
|
||
const installing = ref(false)
|
||
const installRemotePort = ref<number | null>(8080)
|
||
const installAuthEnabled = ref(false)
|
||
const installAuthUsername = ref('')
|
||
const installAuthPassword = ref('')
|
||
|
||
const openInstallModal = (plugin: StorePluginInfo) => {
|
||
selectedStorePlugin.value = plugin
|
||
selectedClientId.value = ''
|
||
installRemotePort.value = 8080
|
||
installAuthEnabled.value = false
|
||
installAuthUsername.value = ''
|
||
installAuthPassword.value = ''
|
||
showInstallModal.value = true
|
||
}
|
||
|
||
const handleInstallStorePlugin = async () => {
|
||
if (!selectedStorePlugin.value || !selectedClientId.value) {
|
||
message.warning('请选择要安装到的客户端')
|
||
return
|
||
}
|
||
if (!selectedStorePlugin.value.download_url) {
|
||
message.error('该插件没有下载地址')
|
||
return
|
||
}
|
||
if (!selectedStorePlugin.value.signature_url) {
|
||
message.error('该插件没有签名文件')
|
||
return
|
||
}
|
||
installing.value = true
|
||
try {
|
||
await installStorePlugin(
|
||
selectedStorePlugin.value.name,
|
||
selectedStorePlugin.value.download_url,
|
||
selectedStorePlugin.value.signature_url,
|
||
selectedClientId.value,
|
||
installRemotePort.value || 8080,
|
||
selectedStorePlugin.value.version,
|
||
selectedStorePlugin.value.config_schema,
|
||
installAuthEnabled.value,
|
||
installAuthUsername.value,
|
||
installAuthPassword.value
|
||
)
|
||
message.success(`已安装 ${selectedStorePlugin.value.name}`)
|
||
showInstallModal.value = false
|
||
} catch (e: any) {
|
||
message.error(e.response?.data || '安装失败')
|
||
} finally {
|
||
installing.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadPlugins()
|
||
loadClients()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<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>
|
||
</div>
|
||
<n-button quaternary @click="router.push('/')">
|
||
<template #icon><n-icon><ArrowBackOutline /></n-icon></template>
|
||
返回首页
|
||
</n-button>
|
||
</n-space>
|
||
|
||
<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-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' : 'info'">
|
||
{{ plugin.source === 'builtin' ? '内置' : 'JS' }}
|
||
</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="!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>
|
||
<template #header-extra>
|
||
<n-button
|
||
v-if="plugin.download_url && plugin.signature_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>
|
||
<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>
|
||
|
||
<!-- JS 插件 -->
|
||
<n-tab-pane name="js" tab="JS 插件">
|
||
<n-spin :show="jsLoading">
|
||
<n-empty v-if="!jsLoading && jsPlugins.length === 0" description="暂无 JS 插件" />
|
||
|
||
<n-grid v-else :cols="2" :x-gap="16" :y-gap="16" responsive="screen" cols-s="1">
|
||
<n-gi v-for="plugin in jsPlugins" :key="plugin.name">
|
||
<n-card hoverable>
|
||
<template #header>
|
||
<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-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-button
|
||
v-if="onlineClients.length > 0"
|
||
size="small"
|
||
type="primary"
|
||
@click="openPushModal(plugin)"
|
||
>
|
||
推送到客户端
|
||
</n-button>
|
||
</n-space>
|
||
</template>
|
||
</n-card>
|
||
</n-gi>
|
||
</n-grid>
|
||
</n-spin>
|
||
</n-tab-pane>
|
||
</n-tabs>
|
||
|
||
<!-- 安全加固:暂时禁用创建 JS 插件 Modal
|
||
<n-modal v-model:show="showJSModal" preset="card" title="新建 JS 插件" style="width: 600px;">
|
||
... 已屏蔽 ...
|
||
</n-modal>
|
||
-->
|
||
|
||
<!-- 安装商店插件模态框 -->
|
||
<n-modal v-model:show="showInstallModal" preset="card" title="安装插件" style="width: 450px;">
|
||
<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 }))"
|
||
/>
|
||
<div>
|
||
<p style="margin: 0 0 8px 0; color: #666; font-size: 13px;">远程端口:</p>
|
||
<n-input-number
|
||
v-model:value="installRemotePort"
|
||
:min="1"
|
||
:max="65535"
|
||
placeholder="输入端口号"
|
||
style="width: 100%;"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<n-space align="center" :size="8">
|
||
<n-switch v-model:value="installAuthEnabled" />
|
||
<span style="color: #666;">启用 HTTP Basic Auth</span>
|
||
</n-space>
|
||
</div>
|
||
<template v-if="installAuthEnabled">
|
||
<n-input v-model:value="installAuthUsername" placeholder="用户名" />
|
||
<n-input v-model:value="installAuthPassword" type="password" placeholder="密码" show-password-on="click" />
|
||
</template>
|
||
</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>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- JS 插件推送模态框 -->
|
||
<n-modal v-model:show="showPushModal" preset="card" title="推送插件到客户端" style="width: 400px;">
|
||
<n-space vertical :size="16">
|
||
<div v-if="selectedJSPlugin">
|
||
<p style="margin: 0 0 8px 0;"><strong>插件:</strong> {{ selectedJSPlugin.name }}</p>
|
||
<p style="margin: 0; color: #666;">{{ selectedJSPlugin.description || '无描述' }}</p>
|
||
</div>
|
||
<n-select
|
||
v-model:value="pushClientId"
|
||
placeholder="选择要推送到的客户端"
|
||
:options="onlineClients.map(c => ({ label: c.nickname || c.id, value: c.id }))"
|
||
/>
|
||
<div>
|
||
<p style="margin: 0 0 8px 0; color: #666; font-size: 13px;">远程端口(服务端监听端口):</p>
|
||
<n-input-number
|
||
v-model:value="pushRemotePort"
|
||
:min="1"
|
||
:max="65535"
|
||
placeholder="输入端口号"
|
||
style="width: 100%;"
|
||
/>
|
||
<p style="margin: 8px 0 0 0; color: #999; font-size: 12px;">用户可以通过 服务端IP:端口 访问此插件提供的服务</p>
|
||
</div>
|
||
</n-space>
|
||
<template #footer>
|
||
<n-space justify="end">
|
||
<n-button @click="showPushModal = false">取消</n-button>
|
||
<n-button
|
||
type="primary"
|
||
:loading="pushing"
|
||
:disabled="!pushClientId"
|
||
@click="handlePushJSPlugin"
|
||
>
|
||
推送
|
||
</n-button>
|
||
</n-space>
|
||
</template>
|
||
</n-modal>
|
||
</div>
|
||
</template>
|