feat(btpanel): 入口改为服务器列表
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / e2e (push) Blocked by required conditions
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run

面板登录页改为全量表格与行内一键登录,侧栏首项更名为服务器列表。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
r
2026-06-21 09:23:26 +08:00
parent d00294c86d
commit 1c4c5af0e4
5 changed files with 165 additions and 27 deletions
+3 -1
View File
@@ -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
# 登录 → 宝塔面板 → 服务器列表 → 表格展示全部服务器 → 行内一键登录
```
+1 -1
View File
@@ -394,7 +394,7 @@ const sysItems = [
]
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/sites', title: '网站列表', icon: 'mdi-web' },
{ to: '/btpanel/sites/create', title: '创建网站', icon: 'mdi-web-plus' },
+131 -25
View File
@@ -1,37 +1,141 @@
<template>
<BtPanelPageShell title="宝塔 · 面板登录" subtitle="生成临时链接,免输密码打开宝塔(A1/A5)">
<template #default="{ serverId, server }">
<v-card variant="outlined" rounded="lg">
<v-card-text>
<v-chip v-if="server" size="small" class="mb-3" :color="server.configured ? 'success' : 'warning'" variant="tonal">
{{ server.configured ? 'API 已配置' : '未配置 API将尝试 SSH 生成链接' }}
</v-chip>
<div class="d-flex flex-wrap ga-2">
<v-btn color="primary" :loading="loading" prepend-icon="mdi-open-in-new" @click="openLogin(serverId!)">
一键登录宝塔
</v-btn>
<v-btn variant="tonal" :to="`/btpanel/settings?server_id=${serverId}`">连接配置</v-btn>
</div>
<p class="text-caption text-medium-emphasis mt-4 mb-0">
链接约 3 小时内有效一次性使用请确保本机浏览器能访问该服务器面板端口
</p>
</v-card-text>
</v-card>
</template>
</BtPanelPageShell>
<v-container fluid class="pa-6">
<div class="d-flex flex-wrap align-center ga-3 mb-4">
<div>
<h1 class="text-h6 font-weight-bold">宝塔 · 服务器列表</h1>
<p class="text-caption text-medium-emphasis mb-0">
选择服务器一键打开宝塔面板临时链接约 3 小时有效
</p>
</div>
<v-spacer />
<v-text-field
v-model="search"
density="compact"
hide-details
clearable
prepend-inner-icon="mdi-magnify"
label="搜索名称 / IP / 分类"
style="max-width: 320px; min-width: 200px"
/>
</div>
<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>
<script setup lang="ts">
import BtPanelPageShell from '@/components/btpanel/BtPanelPageShell.vue'
import { createBtLoginUrl } from '@/api/btpanel'
import { ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { createBtLoginUrl, listBtServers, type BtPanelServerRow } from '@/api/btpanel'
defineOptions({ name: 'BtPanelLoginPage' })
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) {
loading.value = true
loginLoadingId.value = serverId
try {
const { url } = await createBtLoginUrl(serverId)
window.open(url, '_blank', 'noopener,noreferrer')
@@ -39,7 +143,9 @@ async function openLogin(serverId: number) {
} catch (e: unknown) {
window.$snackbar?.(e instanceof Error ? e.message : '生成链接失败', 'error')
} finally {
loading.value = false
loginLoadingId.value = null
}
}
onMounted(() => { void loadServers() })
</script>
+1
View File
@@ -23,6 +23,7 @@ const routes = [
{ 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/servers', redirect: '/btpanel/login' },
{ 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/create', name: 'BtPanelSiteCreate', component: () => import('@/pages/btpanel/BtPanelSiteCreatePage.vue') },