feat(web): replace example page with about project page
CI / verify (push) Canceled after 0s

This commit is contained in:
2026-10-01 17:20:06 +08:00
parent c6f96ffc0f
commit fafde0f709
5 changed files with 216 additions and 219 deletions
+6 -7
View File
@@ -238,17 +238,16 @@ HAVLINE_VERSION=<旧版本> docker compose up -d --no-deps --force-recreate havl
## 10. 在线升级验证
示例页面用于验证升级结果。v1.9.0 的验证流程:
「关于项目」页面可以用于确认升级后的版本信息。v1.9.0 的验证流程:
1. 确认当前线上实例为 v1.3.0,且 `havline-updater` sidecar 已运行。
2. 确认远端 `main` 分支的 `VERSION` 为 `1.9.0`。
3. 在侧栏看到「一键升级」后点击并确认。
4. 等待升级弹窗显示成功,页面自动刷新。
5. 打开侧栏「示例页面」,确认:
- 页面路由为 `/example`;
5. 打开侧栏「关于项目」,确认:
- 页面路由为 `/about`;
- 页面显示的版本为 `1.9.0`;
- 页面显示验证标识 `v1.9.0-example-page`;
- 页面显示独立的「本次升级验证」区块;
- 「升级验证」显示为「通过」。
- 「源项目」显示为 `O96u/Fonu`;
- 开源协议显示为 `GPL-3.0`。
如果示例页面可见且版本正确,说明镜像构建、容器重建、后端版本注入和前端静态资源更新均已生效。
如果「关于项目」页面可见且版本正确,说明镜像构建、容器重建、后端版本注入和前端静态资源更新均已生效。
+2 -2
View File
@@ -123,9 +123,9 @@ import {
CogOutline,
DocumentTextOutline,
Ellipse,
FlashOutline,
GitNetworkOutline,
GridOutline,
InformationCircleOutline,
LockClosedOutline,
LogoGithub,
MoonOutline,
@@ -205,7 +205,7 @@ const menuDefs = [
{ label: 'Cloudflare 隧道', key: 'cloudflare', icon: CloudOutline },
{ label: '日志中心', key: 'logs', icon: DocumentTextOutline },
{ label: '系统设置', key: 'settings', icon: CogOutline },
{ label: '示例页面', key: 'example', icon: FlashOutline },
{ label: '关于项目', key: 'about', icon: InformationCircleOutline },
]
const menuOptions = computed<MenuOption[]>(() =>
+5 -5
View File
@@ -14,7 +14,7 @@ import CloudflareTunnelConfigView from '../views/CloudflareTunnelConfigView.vue'
import AgentView from '../views/AgentView.vue'
import ProxyRemoteView from '../views/ProxyRemoteView.vue'
import StatusPageView from '../views/StatusPageView.vue'
import ExampleView from '../views/ExampleView.vue'
import AboutView from '../views/AboutView.vue'
const router = createRouter({
history: createWebHistory(),
@@ -96,10 +96,10 @@ const router = createRouter({
meta: { title: '系统设置', description: '配置系统参数与运行策略' },
},
{
path: 'example',
name: 'example',
component: ExampleView,
meta: { title: '示例页面', description: '用于验证在线升级是否成功生效' },
path: 'about',
name: 'about',
component: AboutView,
meta: { title: '关于项目', description: '查看 Havline 版本、源开源项目和技术信息' },
},
],
},
+203
View File
@@ -0,0 +1,203 @@
<template>
<PageHeader title="关于项目" description="查看 Havline 版本、源开源项目和技术信息">
<template #actions>
<n-button :loading="refreshing" @click="refreshVersion">
<template #icon><n-icon :component="RefreshOutline" /></template>
刷新版本信息
</n-button>
</template>
</PageHeader>
<div class="stats-row">
<StatCard label="当前版本" :value="`v${version}`" sub="来自 /api/version" tone="brand">
<template #icon><n-icon :component="CloudOutline" /></template>
</StatCard>
<StatCard label="源项目" :value="GITHUB_REPO" sub="上游开源项目地址" tone="blue" value-small>
<template #icon><n-icon :component="LogoGithub" /></template>
</StatCard>
<StatCard label="开源协议" value="GPL-3.0" sub="GNU General Public License" tone="green">
<template #icon><n-icon :component="DocumentTextOutline" /></template>
</StatCard>
<StatCard label="运行状态" value="正常" sub="Havline 服务运行中" tone="teal">
<template #icon><n-icon :component="CheckmarkCircleOutline" /></template>
</StatCard>
</div>
<HavlineCard title="项目信息" subtitle="当前版本、项目定位和最近检查时间">
<div class="about-grid">
<div class="about-item">
<label>项目名称</label>
<strong>Havline</strong>
</div>
<div class="about-item">
<label>项目定位</label>
<span>面向 NAS 的轻量公网访问管理工具</span>
</div>
<div class="about-item">
<label>当前版本</label>
<span class="mono">v{{ version }}</span>
</div>
<div class="about-item">
<label>最近检查</label>
<span>{{ checkedAt || '刚刚' }}</span>
</div>
</div>
<div class="about-links">
<a :href="GITHUB_REPO_URL" target="_blank" rel="noopener noreferrer">
<n-icon :component="LogoGithub" />
源项目:{{ GITHUB_REPO }}
</a>
<a :href="GPL_URL" target="_blank" rel="noopener noreferrer">
<n-icon :component="DocumentTextOutline" />
查看 GPL-3.0 协议
</a>
</div>
</HavlineCard>
<HavlineCard title="源项目与开源说明" subtitle="Havline 的源项目地址保持不变">
<div class="about-copy">
<p>
Havline 的源开源项目为
<a :href="GITHUB_REPO_URL" target="_blank" rel="noopener noreferrer">{{ GITHUB_REPO }}</a>。
该地址用于查阅上游代码、提交记录和 Release 信息。
</p>
<p>
本项目继续采用
<a :href="GPL_URL" target="_blank" rel="noopener noreferrer">GPL-3.0</a>
开源。品牌名称调整不会改变原项目及其衍生版本的许可证义务。
</p>
</div>
</HavlineCard>
<HavlineCard title="技术栈" subtitle="Havline 当前使用的主要技术组件">
<div class="about-stack">
<span v-for="item in stack" :key="item">{{ item }}</span>
</div>
</HavlineCard>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { NButton, NIcon, useMessage } from 'naive-ui'
import {
CheckmarkCircleOutline,
CloudOutline,
DocumentTextOutline,
LogoGithub,
RefreshOutline,
} from '@vicons/ionicons5'
import { api } from '../api/client'
import HavlineCard from '../components/HavlineCard.vue'
import PageHeader from '../components/PageHeader.vue'
import StatCard from '../components/StatCard.vue'
import { GITHUB_REPO, GITHUB_REPO_URL } from '../constants/app'
import { formatDate } from '../utils/format'
const GPL_URL = 'https://www.gnu.org/licenses/gpl-3.0.html'
const message = useMessage()
const version = ref(__APP_VERSION__)
const checkedAt = ref('')
const refreshing = ref(false)
const stack = [
'Go 1.23',
'Vue 3',
'TypeScript',
'Vite',
'Naive UI',
'ECharts',
'SQLite',
'Nginx',
'FRP',
'Cloudflare Tunnel',
]
async function refreshVersion() {
refreshing.value = true
try {
const result = await api.getVersion()
version.value = result.version || version.value
checkedAt.value = formatDate(new Date().toISOString())
} catch (error) {
message.error(error instanceof Error ? error.message : '读取版本失败')
} finally {
refreshing.value = false
}
}
onMounted(() => {
void refreshVersion()
})
</script>
<style scoped>
.about-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--havline-space-4);
}
.about-item {
min-width: 0;
padding: var(--havline-space-4);
border: 1px solid var(--havline-border);
border-radius: var(--havline-radius-sm);
background: var(--havline-bg);
}
.about-item label {
display: block;
margin-bottom: 6px;
color: var(--havline-text-muted);
font-size: 12px;
}
.about-item strong,
.about-item span {
color: var(--havline-text);
}
.about-links,
.about-stack {
display: flex;
flex-wrap: wrap;
gap: var(--havline-space-3);
margin-top: var(--havline-space-4);
}
.about-links a {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
color: var(--havline-brand-text);
border: 1px solid var(--havline-border);
border-radius: var(--havline-radius-sm);
background: var(--havline-surface);
text-decoration: none;
}
.about-copy {
display: grid;
gap: var(--havline-space-3);
color: var(--havline-text-secondary);
}
.about-copy p {
margin: 0;
}
.about-copy a {
color: var(--havline-brand-text);
text-decoration: none;
}
.about-stack span {
padding: 6px 10px;
color: var(--havline-text-secondary);
border: 1px solid var(--havline-border);
border-radius: 999px;
background: var(--havline-bg);
font-size: 12px;
}
</style>
-205
View File
@@ -1,205 +0,0 @@
<template>
<PageHeader title="示例页面" description="用于验证 Web 一键升级是否成功生效">
<template #actions>
<n-button :loading="refreshing" @click="refreshVersion">
<template #icon><n-icon :component="RefreshOutline" /></template>
刷新版本信息
</n-button>
</template>
</PageHeader>
<n-alert type="success" :bordered="false" class="example-alert" title="v1.9.0 示例页面验证标识已生效">
当前页面由 v{{ version }} 提供,验证标识为 <span class="mono">{{ verificationMarker }}</span>。
如果你是在升级后首次看到这行内容,说明前端构建、镜像重建和服务重启均已成功。
</n-alert>
<div class="example-proof">
<div>
<span class="example-proof__label">本次升级验证</span>
<strong class="example-proof__value">{{ verificationRound }}</strong>
</div>
<div class="example-proof__hint">
看到这个新区块,说明前端资源已经从旧版本切换到 v1.9.0。
</div>
</div>
<div class="stats-row">
<StatCard label="当前版本" :value="`v${version}`" sub="来自 /api/version" tone="brand">
<template #icon><n-icon :component="CloudOutline" /></template>
</StatCard>
<StatCard label="升级验证" value="通过" sub="示例路由与菜单已加载" tone="green">
<template #icon><n-icon :component="CheckmarkCircleOutline" /></template>
</StatCard>
<StatCard label="本次验证标识" :value="verificationMarker" sub="用于区分升级前后的前端资源" tone="blue" value-small>
<template #icon><n-icon :component="FlashOutline" /></template>
</StatCard>
<StatCard label="页面状态" value="已更新" sub="v1.9.0 前端资源已加载" tone="teal">
<template #icon><n-icon :component="CheckmarkCircleOutline" /></template>
</StatCard>
</div>
<HavlineCard title="本次示例页变化" subtitle="用于确认升级后的页面内容确实发生变化">
<ul class="example-changes">
<li>页面版本提示更新为 v1.9.0。</li>
<li>「本次升级验证」区块和页面状态同步更新为 v1.9.0。</li>
<li>验证标识更新为 <span class="mono">v1.9.0-example-page</span>。</li>
</ul>
</HavlineCard>
<HavlineCard title="验证步骤" subtitle="用这个页面确认线上实例已经更新到目标版本">
<ol class="example-steps">
<li>在旧版本页面点击侧栏的「一键升级」。</li>
<li>等待容器重建完成,页面自动刷新或恢复访问。</li>
<li>打开侧栏「示例页面」,确认本页可访问。</li>
<li>确认页面上的版本号为 <span class="mono">v{{ version }}</span>。</li>
</ol>
</HavlineCard>
<HavlineCard title="验证结果" subtitle="用于确认当前前端资源与后端版本一致">
<div class="example-result">
<div><label>前端路由</label><span class="mono">/example</span></div>
<div><label>侧栏菜单</label><span>已加载「示例页面」</span></div>
<div><label>后端版本</label><span class="mono">{{ version }}</span></div>
<div><label>验证标识</label><span class="mono">{{ verificationMarker }}</span></div>
<div><label>验证状态</label><StatusBadge kind="success" text="通过" /></div>
<div><label>最近检查</label><span>{{ checkedAt || '刚刚' }}</span></div>
</div>
</HavlineCard>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { NAlert, NButton, NIcon, useMessage } from 'naive-ui'
import {
CheckmarkCircleOutline,
CloudOutline,
FlashOutline,
RefreshOutline,
} from '@vicons/ionicons5'
import { api } from '../api/client'
import HavlineCard from '../components/HavlineCard.vue'
import PageHeader from '../components/PageHeader.vue'
import StatCard from '../components/StatCard.vue'
import StatusBadge from '../components/StatusBadge.vue'
import { formatDate } from '../utils/format'
const message = useMessage()
const version = ref(__APP_VERSION__)
const checkedAt = ref('')
const refreshing = ref(false)
const verificationRound = 'v1.9.0'
const verificationMarker = 'v1.9.0-example-page'
async function refreshVersion() {
refreshing.value = true
try {
const result = await api.getVersion()
version.value = result.version || version.value
checkedAt.value = formatDate(new Date().toISOString())
} catch (error) {
message.error(error instanceof Error ? error.message : '读取版本失败')
} finally {
refreshing.value = false
}
}
onMounted(() => {
void refreshVersion()
})
</script>
<style scoped>
.example-alert {
margin-bottom: var(--havline-space-5);
}
.example-proof {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--havline-space-5);
margin-bottom: var(--havline-space-5);
padding: var(--havline-space-4) var(--havline-space-5);
border: 1px solid var(--havline-brand-soft);
border-radius: var(--havline-radius);
background: var(--havline-brand-soft);
}
.example-proof__label {
display: block;
font-size: 12px;
color: var(--havline-text-secondary);
}
.example-proof__value {
display: block;
margin-top: 2px;
font-size: 28px;
line-height: 1.2;
color: var(--havline-brand-text);
}
.example-proof__hint {
max-width: 360px;
font-size: 13px;
line-height: 1.6;
color: var(--havline-text-secondary);
}
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--havline-space-4);
margin-bottom: var(--havline-space-5);
}
.example-steps {
display: grid;
gap: 10px;
margin: 0;
padding-left: 22px;
color: var(--havline-text-secondary);
line-height: 1.7;
}
.example-changes {
display: grid;
gap: 10px;
margin: 0;
padding-left: 22px;
color: var(--havline-text-secondary);
line-height: 1.7;
}
.example-result {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--havline-space-3);
}
.example-result > div {
display: grid;
gap: 4px;
padding: 12px 14px;
border: 1px solid var(--havline-border);
border-radius: var(--havline-radius-sm);
background: var(--havline-bg);
}
.example-result label {
font-size: 12px;
color: var(--havline-text-muted);
}
@media (max-width: 640px) {
.example-proof {
align-items: flex-start;
flex-direction: column;
gap: 8px;
}
.example-result {
grid-template-columns: 1fr;
}
}
</style>