feat(btpanel): 入口改为服务器列表
面板登录页改为全量表格与行内一键登录,侧栏首项更名为服务器列表。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
## 摘要
|
## 摘要
|
||||||
|
|
||||||
将 commit `82426b19`(独立宝塔模块 + SSH 自动获取 API)部署至生产 `api.synaglobal.vip`。
|
将 `82426b19` + `d00294c8`(独立宝塔模块 + SSH 自动获取 API)部署至生产 `api.synaglobal.vip`。
|
||||||
|
|
||||||
|
**部署结果(2026-06-21)**:Gitea push → 生产 `git reset --hard origin/main` → `nexus-1panel.sh upgrade` 成功;容器 `nexus-prod-nexus-1` healthy;`/health` ok;容器内注册 28 条 `/api/btpanel/*` 路由。
|
||||||
|
|
||||||
## 变更范围
|
## 变更范围
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Changelog — 宝塔入口改为服务器列表(2026-06-21)
|
||||||
|
|
||||||
|
**日期**:2026-06-21
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
将「宝塔 · 面板登录」从单台下拉选择改为主入口**服务器列表**,侧栏首项更名为「服务器列表」。
|
||||||
|
|
||||||
|
## 动机
|
||||||
|
|
||||||
|
- 2000+ 子机需一览配置状态与一键登录,单台 picker 不符合运维习惯
|
||||||
|
- 与 Nexus 服务器列表心智一致:先选机再操作
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `frontend/src/pages/btpanel/BtPanelLoginPage.vue` — `v-data-table` 全量列表 + 搜索 + 行内「一键登录」
|
||||||
|
- `frontend/src/App.vue` — 侧栏文案与图标
|
||||||
|
- `frontend/src/router/index.ts` — `/btpanel/servers` 重定向至 `/btpanel/login`
|
||||||
|
|
||||||
|
## 迁移 / 重启
|
||||||
|
|
||||||
|
- 仅前端;`vite build` 或 Docker upgrade 后生效
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend && npx vite build
|
||||||
|
# 登录 → 宝塔面板 → 服务器列表 → 表格展示全部服务器 → 行内一键登录
|
||||||
|
```
|
||||||
@@ -394,7 +394,7 @@ const sysItems = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const btPanelItems = [
|
const btPanelItems = [
|
||||||
{ to: '/btpanel/login', title: '面板登录', icon: 'mdi-login' },
|
{ to: '/btpanel/login', title: '服务器列表', icon: 'mdi-server' },
|
||||||
{ to: '/btpanel/monitor', title: '系统监控', icon: 'mdi-speedometer' },
|
{ to: '/btpanel/monitor', title: '系统监控', icon: 'mdi-speedometer' },
|
||||||
{ to: '/btpanel/sites', title: '网站列表', icon: 'mdi-web' },
|
{ to: '/btpanel/sites', title: '网站列表', icon: 'mdi-web' },
|
||||||
{ to: '/btpanel/sites/create', title: '创建网站', icon: 'mdi-web-plus' },
|
{ to: '/btpanel/sites/create', title: '创建网站', icon: 'mdi-web-plus' },
|
||||||
|
|||||||
@@ -1,37 +1,141 @@
|
|||||||
<template>
|
<template>
|
||||||
<BtPanelPageShell title="宝塔 · 面板登录" subtitle="生成临时链接,免输密码打开宝塔(A1/A5)">
|
<v-container fluid class="pa-6">
|
||||||
<template #default="{ serverId, server }">
|
<div class="d-flex flex-wrap align-center ga-3 mb-4">
|
||||||
<v-card variant="outlined" rounded="lg">
|
<div>
|
||||||
<v-card-text>
|
<h1 class="text-h6 font-weight-bold">宝塔 · 服务器列表</h1>
|
||||||
<v-chip v-if="server" size="small" class="mb-3" :color="server.configured ? 'success' : 'warning'" variant="tonal">
|
<p class="text-caption text-medium-emphasis mb-0">
|
||||||
{{ server.configured ? 'API 已配置' : '未配置 API,将尝试 SSH 生成链接' }}
|
选择服务器一键打开宝塔面板(临时链接约 3 小时有效)
|
||||||
</v-chip>
|
</p>
|
||||||
<div class="d-flex flex-wrap ga-2">
|
</div>
|
||||||
<v-btn color="primary" :loading="loading" prepend-icon="mdi-open-in-new" @click="openLogin(serverId!)">
|
<v-spacer />
|
||||||
一键登录宝塔
|
<v-text-field
|
||||||
</v-btn>
|
v-model="search"
|
||||||
<v-btn variant="tonal" :to="`/btpanel/settings?server_id=${serverId}`">连接配置</v-btn>
|
density="compact"
|
||||||
</div>
|
hide-details
|
||||||
<p class="text-caption text-medium-emphasis mt-4 mb-0">
|
clearable
|
||||||
链接约 3 小时内有效、一次性使用。请确保本机浏览器能访问该服务器面板端口。
|
prepend-inner-icon="mdi-magnify"
|
||||||
</p>
|
label="搜索名称 / IP / 分类"
|
||||||
</v-card-text>
|
style="max-width: 320px; min-width: 200px"
|
||||||
</v-card>
|
/>
|
||||||
</template>
|
</div>
|
||||||
</BtPanelPageShell>
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="filteredServers"
|
||||||
|
:loading="loading"
|
||||||
|
density="compact"
|
||||||
|
item-value="id"
|
||||||
|
hover
|
||||||
|
>
|
||||||
|
<template #item.configured="{ item }">
|
||||||
|
<v-chip size="x-small" :color="item.configured ? 'success' : 'warning'" variant="tonal">
|
||||||
|
{{ item.configured ? 'API 已配置' : '未配置' }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
<template #item.bootstrap_state="{ item }">
|
||||||
|
<v-chip
|
||||||
|
v-if="item.bootstrap_state"
|
||||||
|
size="x-small"
|
||||||
|
:color="bootstrapChipColor(item)"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ bootstrapLabel(item.bootstrap_state) }}
|
||||||
|
</v-chip>
|
||||||
|
<span v-else class="text-medium-emphasis text-caption">—</span>
|
||||||
|
</template>
|
||||||
|
<template #item.base_url="{ item }">
|
||||||
|
<span class="text-caption text-truncate d-inline-block" style="max-width: 220px">
|
||||||
|
{{ item.base_url || '—' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-open-in-new"
|
||||||
|
:loading="loginLoadingId === item.id"
|
||||||
|
@click="openLogin(item.id)"
|
||||||
|
>
|
||||||
|
一键登录
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
class="ml-1"
|
||||||
|
:to="`/btpanel/settings?server_id=${item.id}`"
|
||||||
|
>
|
||||||
|
连接配置
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BtPanelPageShell from '@/components/btpanel/BtPanelPageShell.vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { createBtLoginUrl } from '@/api/btpanel'
|
import { createBtLoginUrl, listBtServers, type BtPanelServerRow } from '@/api/btpanel'
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'BtPanelLoginPage' })
|
defineOptions({ name: 'BtPanelLoginPage' })
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const loginLoadingId = ref<number | null>(null)
|
||||||
|
const search = ref('')
|
||||||
|
const servers = ref<BtPanelServerRow[]>([])
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: '名称', key: 'name', minWidth: 120 },
|
||||||
|
{ title: '地址', key: 'domain', minWidth: 120 },
|
||||||
|
{ title: '分类', key: 'category', minWidth: 80 },
|
||||||
|
{ title: 'API', key: 'configured', width: 110, sortable: false },
|
||||||
|
{ title: '引导', key: 'bootstrap_state', width: 100, sortable: false },
|
||||||
|
{ title: '面板地址', key: 'base_url', minWidth: 160, sortable: false },
|
||||||
|
{ title: '操作', key: 'actions', width: 200, sortable: false, align: 'end' as const },
|
||||||
|
]
|
||||||
|
|
||||||
|
const bootstrapStateLabels: Record<string, string> = {
|
||||||
|
pending: '等待获取',
|
||||||
|
installing: '安装中',
|
||||||
|
ready: '已完成',
|
||||||
|
failed: 'SSH 失败',
|
||||||
|
disabled: '已关闭',
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootstrapLabel(state: string) {
|
||||||
|
return bootstrapStateLabels[state] ?? state
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootstrapChipColor(item: BtPanelServerRow) {
|
||||||
|
if (item.configured || item.bootstrap_state === 'ready') return 'success'
|
||||||
|
if (item.bootstrap_state === 'failed') return 'error'
|
||||||
|
if (item.bootstrap_state === 'installing') return 'warning'
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredServers = computed(() => {
|
||||||
|
const q = search.value.trim().toLowerCase()
|
||||||
|
if (!q) return servers.value
|
||||||
|
return servers.value.filter(s =>
|
||||||
|
[s.name, s.domain, s.category ?? '', s.base_url ?? '']
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(q),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadServers() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
servers.value = await listBtServers()
|
||||||
|
} catch (e: unknown) {
|
||||||
|
window.$snackbar?.(e instanceof Error ? e.message : '加载服务器列表失败', 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function openLogin(serverId: number) {
|
async function openLogin(serverId: number) {
|
||||||
loading.value = true
|
loginLoadingId.value = serverId
|
||||||
try {
|
try {
|
||||||
const { url } = await createBtLoginUrl(serverId)
|
const { url } = await createBtLoginUrl(serverId)
|
||||||
window.open(url, '_blank', 'noopener,noreferrer')
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
@@ -39,7 +143,9 @@ async function openLogin(serverId: number) {
|
|||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
window.$snackbar?.(e instanceof Error ? e.message : '生成链接失败', 'error')
|
window.$snackbar?.(e instanceof Error ? e.message : '生成链接失败', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loginLoadingId.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => { void loadServers() })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const routes = [
|
|||||||
{ path: '/login', name: 'Login', component: () => import('@/pages/LoginPage.vue'), meta: { public: true } },
|
{ path: '/login', name: 'Login', component: () => import('@/pages/LoginPage.vue'), meta: { public: true } },
|
||||||
// ── 宝塔面板(独立模块,不与运维页合并)──
|
// ── 宝塔面板(独立模块,不与运维页合并)──
|
||||||
{ path: '/btpanel/login', name: 'BtPanelLogin', component: () => import('@/pages/btpanel/BtPanelLoginPage.vue') },
|
{ path: '/btpanel/login', name: 'BtPanelLogin', component: () => import('@/pages/btpanel/BtPanelLoginPage.vue') },
|
||||||
|
{ path: '/btpanel/servers', redirect: '/btpanel/login' },
|
||||||
{ path: '/btpanel/monitor', name: 'BtPanelMonitor', component: () => import('@/pages/btpanel/BtPanelMonitorPage.vue') },
|
{ path: '/btpanel/monitor', name: 'BtPanelMonitor', component: () => import('@/pages/btpanel/BtPanelMonitorPage.vue') },
|
||||||
{ path: '/btpanel/sites', name: 'BtPanelSites', component: () => import('@/pages/btpanel/BtPanelSitesPage.vue') },
|
{ path: '/btpanel/sites', name: 'BtPanelSites', component: () => import('@/pages/btpanel/BtPanelSitesPage.vue') },
|
||||||
{ path: '/btpanel/sites/create', name: 'BtPanelSiteCreate', component: () => import('@/pages/btpanel/BtPanelSiteCreatePage.vue') },
|
{ path: '/btpanel/sites/create', name: 'BtPanelSiteCreate', component: () => import('@/pages/btpanel/BtPanelSiteCreatePage.vue') },
|
||||||
|
|||||||
Reference in New Issue
Block a user