feat(web): add About page in settings

Add /settings/about page with version display, GitHub release update
checker with markdown-rendered release notes, and external links
(GitHub, Docs, Feedback). Remove version section from Profile page.
This commit is contained in:
Acbox
2026-04-03 01:35:54 +08:00
parent a9a9f7e955
commit c07fa8d423
6 changed files with 227 additions and 28 deletions
@@ -52,7 +52,7 @@ import { computed, type Component } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ChevronLeft, Bot, Boxes, Globe, Brain, Volume2, Mail, AppWindow, ChartLine, User, Store } from 'lucide-vue-next'
import { ChevronLeft, Bot, Boxes, Globe, Brain, Volume2, Mail, AppWindow, ChartLine, User, Store, Info } from 'lucide-vue-next'
import { useChatSelectionStore } from '@/store/chat-selection'
import {
Sidebar,
@@ -143,5 +143,10 @@ const navItems = computed<{ title: string; name: string; icon: Component }[]>(()
name: 'profile',
icon: User,
},
{
title: t('sidebar.about'),
name: 'about',
icon: Info,
},
])
</script>
+14 -1
View File
@@ -70,7 +70,8 @@
"platform": "Platform",
"usage": "Usage",
"browser": "Browser",
"supermarket": "Supermarket"
"supermarket": "Supermarket",
"about": "About"
},
"breadcrumb": {
"main": "Home"
@@ -128,6 +129,18 @@
"versionTag": "v{version}",
"commitHash": "Commit"
},
"about": {
"checkForUpdates": "Check for Updates",
"checking": "Checking...",
"upToDate": "You are up to date",
"newVersionAvailable": "New version available: v{version}",
"releaseNotes": "Release Notes",
"viewOnGitHub": "View on GitHub",
"checkFailed": "Failed to check for updates",
"github": "GitHub",
"docs": "Docs",
"feedback": "Feedback"
},
"chat": {
"greeting": "Hi! How can I help you today?",
"selectBot": "Select a Bot",
+14 -1
View File
@@ -70,7 +70,8 @@
"platform": "接入平台",
"usage": "用量统计",
"browser": "浏览器",
"supermarket": "市场"
"supermarket": "市场",
"about": "关于"
},
"breadcrumb": {
"main": "主页"
@@ -124,6 +125,18 @@
"versionTag": "v{version}",
"commitHash": "提交"
},
"about": {
"checkForUpdates": "检查更新",
"checking": "检查中...",
"upToDate": "已是最新版本",
"newVersionAvailable": "发现新版本: v{version}",
"releaseNotes": "更新日志",
"viewOnGitHub": "在 GitHub 上查看",
"checkFailed": "检查更新失败",
"github": "GitHub",
"docs": "文档",
"feedback": "反馈"
},
"chat": {
"greeting": "你好!有什么我可以帮你的吗?",
"selectBot": "选择一个 Bot",
+185
View File
@@ -0,0 +1,185 @@
<template>
<section class="max-w-7xl mx-auto p-4 pb-12">
<div class="max-w-3xl mx-auto space-y-6">
<!-- Header: Logo + Version + Check Button -->
<div class="flex items-center gap-3">
<img
src="/logo.png"
alt="Memoh"
class="size-10 shrink-0 rounded-lg"
>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold">
Memoh
</p>
<div class="flex items-center gap-2 mt-0.5">
<Badge
v-if="serverVersion"
variant="secondary"
>
{{ $t('settings.versionTag', { version: serverVersion }) }}
</Badge>
<Badge
v-if="commitHash"
variant="outline"
>
{{ commitHash }}
</Badge>
</div>
</div>
<Button
size="sm"
variant="secondary"
:disabled="checking"
@click="checkForUpdates"
>
<Spinner
v-if="checking"
class="size-3"
/>
<RefreshCw
v-else
class="size-3"
/>
{{ checking ? $t('about.checking') : $t('about.checkForUpdates') }}
</Button>
</div>
<!-- Update Result -->
<template v-if="checkResult">
<div
v-if="checkResult.isUpToDate"
class="flex items-center gap-2 text-xs text-muted-foreground"
>
<CircleCheck class="size-3.5 text-green-500" />
{{ $t('about.upToDate') }}
</div>
<template v-else>
<Separator />
<div class="flex items-center gap-2">
<Badge class="bg-[#8B56E3] text-white hover:bg-[#8B56E3]/90">
{{ $t('about.newVersionAvailable', { version: checkResult.latestVersion }) }}
</Badge>
</div>
<div
v-if="checkResult.body"
class="space-y-2"
>
<h4 class="text-xs font-medium text-muted-foreground">
{{ $t('about.releaseNotes') }}
</h4>
<div class="prose prose-xs dark:prose-invert max-w-none *:first:mt-0 text-[0.8rem] leading-relaxed">
<MarkdownRender
:content="checkResult.body"
:is-dark="isDark"
custom-id="release-notes"
/>
</div>
</div>
</template>
</template>
<!-- External Links -->
<section>
<Separator class="mb-4" />
<div class="space-y-1">
<a
href="https://github.com/memohai/memoh"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-xs text-foreground hover:bg-accent transition-colors"
>
<Github class="size-4 text-muted-foreground" />
{{ $t('about.github') }}
<ExternalLink class="size-3 ml-auto text-muted-foreground" />
</a>
<a
href="https://docs.memoh.ai"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-xs text-foreground hover:bg-accent transition-colors"
>
<BookOpen class="size-4 text-muted-foreground" />
{{ $t('about.docs') }}
<ExternalLink class="size-3 ml-auto text-muted-foreground" />
</a>
<a
href="https://github.com/memohai/memoh/issues"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-xs text-foreground hover:bg-accent transition-colors"
>
<MessageSquare class="size-4 text-muted-foreground" />
{{ $t('about.feedback') }}
<ExternalLink class="size-3 ml-auto text-muted-foreground" />
</a>
</div>
</section>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
import { RefreshCw, ExternalLink, Github, BookOpen, MessageSquare, CircleCheck } from 'lucide-vue-next'
import { Badge, Button, Separator, Spinner } from '@memohai/ui'
import MarkdownRender from 'markstream-vue'
import { useCapabilitiesStore } from '@/store/capabilities'
import { useSettingsStore } from '@/store/settings'
const GITHUB_REPO = 'memohai/memoh'
interface CheckResult {
isUpToDate: boolean
latestVersion: string
body: string
htmlUrl: string
}
const { t } = useI18n()
const capabilitiesStore = useCapabilitiesStore()
const { serverVersion, commitHash } = storeToRefs(capabilitiesStore)
const settingsStore = useSettingsStore()
const isDark = computed(() => settingsStore.theme === 'dark')
const checking = ref(false)
const checkResult = ref<CheckResult | null>(null)
onMounted(async () => {
await capabilitiesStore.load()
await checkForUpdates()
})
async function checkForUpdates() {
checking.value = true
checkResult.value = null
try {
const res = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
const tagName: string = data.tag_name ?? ''
const latestVersion = tagName.replace(/^v/, '')
const currentVersion = (serverVersion.value ?? '').replace(/^v/, '')
checkResult.value = {
isUpToDate: latestVersion === currentVersion,
latestVersion,
body: data.body ?? '',
htmlUrl: data.html_url ?? `https://github.com/${GITHUB_REPO}/releases/latest`,
}
} catch {
toast.error(t('about.checkFailed'))
} finally {
checking.value = false
}
}
</script>
-25
View File
@@ -188,26 +188,6 @@
@generate="onGenerateBindCode"
@copy="copyBindCode"
/>
<!-- Version -->
<section>
<Separator class="mb-4" />
<div class="flex items-center gap-2 text-xs text-muted-foreground">
<span>{{ $t('settings.version') }}</span>
<Badge
v-if="serverVersion"
variant="secondary"
>
{{ $t('settings.versionTag', { version: serverVersion }) }}
</Badge>
<Badge
v-if="commitHash"
variant="outline"
>
{{ commitHash }}
</Badge>
</div>
</section>
</div>
</section>
</template>
@@ -243,7 +223,6 @@ import { client } from '@memohai/sdk/client'
import type { AccountsAccount, AccountsUpdateProfileRequest, AccountsUpdatePasswordRequest, IdentitiesChannelIdentity } from '@memohai/sdk'
import { useUserStore } from '@/store/user'
import { useSettingsStore } from '@/store/settings'
import { useCapabilitiesStore } from '@/store/capabilities'
import type { Locale } from '@/i18n'
import { resolveApiErrorMessage } from '@/utils/api-error'
import { formatDateTime } from '@/utils/date-time'
@@ -271,10 +250,6 @@ const settingsStore = useSettingsStore()
const { language, theme } = storeToRefs(settingsStore)
const { setLanguage, setTheme } = settingsStore
// ---- Server version ----
const capabilitiesStore = useCapabilitiesStore()
const { serverVersion, commitHash } = storeToRefs(capabilitiesStore)
// ---- User data ----
const account = ref<UserAccount | null>(null)
const identities = ref<IdentitiesChannelIdentity[]>([])
+8
View File
@@ -137,6 +137,14 @@ const routes = [
breadcrumb: i18nRef('sidebar.supermarket'),
},
},
{
name: 'about',
path: 'about',
component: () => import('@/pages/about/index.vue'),
meta: {
breadcrumb: i18nRef('sidebar.about'),
},
},
],
},
{