Skip to content

Commit 36eac4e

Browse files
authored
Merge pull request #18 from yyyCode/feature/about-us
feat(web): about-us page + header nav reorder + remove profile drawer
2 parents 22ca4ce + afc1736 commit 36eac4e

9 files changed

Lines changed: 696 additions & 83 deletions

File tree

‎docs/superpowers/plans/2026-08-28-about-us-page.md‎

Lines changed: 471 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# 关于我们页面:顶部导航调整 + 独立 `/about-us` 页面
2+
3+
日期:2026-08-28
4+
状态:已确认(用户拍板:源码链接改为「关于我们」并与头像按钮交换位置;新增独立 `/about-us` 页而非小页面;页面内容含作者卡基础项 + 个人简介段落 + 联系方式/社交链接;右侧抽屉整体移除)✅ 已实现(2026-08-28)
5+
6+
## 背景与目标
7+
8+
站点顶部导航目前有:`首页 | 文章 | 项目推荐 | 求职导航 | 论坛 | 反馈 | 源码(外链) | 头像按钮`。其中「源码」是一个跳 GitHub 仓库的外部链接;「个人信息」入口位于右侧悬浮按钮 → 抽屉(ProfileCard + BlogInfoCard)。
9+
10+
目标:
11+
1. 顶部导航把「源码」外链改为**「关于我们」导航项**(跳站内新页面),并与头像按钮**交换位置**。
12+
2. 新增独立整页 `/about-us`,内容为站点作者 yyycode 的自我介绍(即原抽屉 ProfileCard 的「关于我」内容 + 个人简介段落 + 联系方式/社交链接)。
13+
3. 右侧悬浮按钮「个人信息」与挂件抽屉(WidgetsDrawer)**整体移除**。
14+
15+
**范围:纯前端。** 后端零改动;数据全部复用现有接口与 siteConfig。
16+
17+
## 后端现状(已具备,直接复用)
18+
19+
| 能力 | 接口 | 说明 |
20+
|---|---|---|
21+
| 公开资料 | `GET /api/v1/profile` | **匿名调用时返回站点作者**(最近更新的 ACTIVE AUTHOR,回退第一个 AUTHOR,再回退第一个用户);带 JWT 时返回当前登录用户。返回 `PublicProfileResponse`:`userId, username, avatarUrl, bio`。关于我们页需要**始终匿名**请求,确保访客/登录读者看到的都是作者本人 |
22+
| 站点配置 | siteConfig(`inject('siteConfig')`,App.vue 统一加载) | 含 `github_url, csdn_url, nowcoder_url, default_avatar_url, blog_name, source_code_url` 等 |
23+
24+
`request()`(`api/http.js`)不带 `withAuth` 时即匿名请求,直接返回 `ApiResponse.data`。
25+
26+
## 改动清单
27+
28+
| 文件 | 改动 |
29+
|---|---|
30+
| `vue/src/components/BlogHeader.vue` | 导航重排:「源码」外链 `<a>` → 改为「关于我们」`router-link`(跳 `/about-us`),移到最右端;头像按钮移到「关于我们」之前(原「源码」位置) |
31+
| `vue/src/views/AboutUsView.vue` | **新建**。关于我们整页 |
32+
| `vue/src/api/profile.js` | 新增 `fetchOwnerProfile()`:始终匿名 `request('/api/v1/profile')` |
33+
| `vue/src/router/index.js` | 新增 `/about-us` 路由(`/about` 保留不动) |
34+
| `vue/src/components/WidgetsDrawer.vue` | **删除**(整抽屉移除) |
35+
| `vue/src/components/RightDock.vue` | 移除「个人信息」按钮及 `toggle-profile` emit,保留「深浅色」「公告」 |
36+
| `vue/src/App.vue` | 清理抽屉相关:移除 `WidgetsDrawer` import/渲染、`@toggle-profile` 监听、BlogHeader 的 `@toggle-widgets` 监听、`widgetsOpen`/`toggleWidgets`、`profile`/`loadProfile`(全部只为抽屉服务) |
37+
38+
**保留不动**:`ProfileCard.vue`(搜索页 SearchResultsView 仍用)、`BlogInfoCard.vue`(同上)、`PublicLayout.vue`(死代码,无引用)、`ConsoleSiteConfigView.vue`(`source_code_url` 配置表单保留,导航不再使用该字段)。
39+
40+
## 交互与数据流
41+
42+
### 顶部导航(BlogHeader.vue)
43+
44+
调整后顺序:`首页 | 文章 | 项目推荐 | 求职导航 | 论坛 | 反馈 | [头像按钮] [关于我们]`
45+
46+
- **关于我们**:`router-link to="/about-us"`,替换原「源码」外链 `<a>`(`site-nav-link site-nav-link-sm-hide` 类保留,小屏隐藏);图标从 GitHub 换成 info 圆形图标。
47+
- **头像按钮**:从最右端移到「关于我们」之前(原「源码」位置)。点击行为不变:登录 → `/profile`;未登录 → `/login`。
48+
- 视觉上头像按钮 + 关于我们位于导航右侧一组;`site-nav-auth-trailing` 的右对齐行为由实现者按现有 CSS 调整(最终效果:两个入口靠右、其余导航项靠左)。
49+
50+
### 关于我们页(AboutUsView.vue)
51+
52+
独立整页(`page-wrap` 居中 + `.card`,复用 `vue/src/assets/blog.css` 的 CSS 变量)。结构从上到下:
53+
54+
1. **大头像**:`UserAvatar`(`size` 约 112),`owner.avatarUrl`,回退 `siteConfig.default_avatar_url`,再回退占位图。
55+
2. **昵称**:`owner.username`(回退「—」)。
56+
3. **签名**:`owner.bio`(回退「平凡的一枚程序员」)。
57+
4. **菜单**:全部文章(`/all`)、更新日志(`/changelog`)——沿用原 ProfileCard 的卡片基础项。
58+
5. **个人简介段落**:静态文案(可编辑)。默认草稿:
59+
> 你好,我是 yyycode,这个站点的作者。这里记录我在开发与学习中的经历、踩坑和思考,欢迎通过下面的方式联系我交流。
60+
6. **联系方式 + 社交链接**:GitHub / CSDN / 牛客,`siteConfig.github_url` / `csdn_url` / `nowcoder_url`,回退值与 ProfileCard 一致(`https://github.com/yyyCode`、`https://blog.csdn.net/2301_80044822`、`https://www.nowcoder.com/users/597303882`)。
61+
62+
**数据源**:`onMounted` 调 `fetchOwnerProfile()`(`request('/api/v1/profile')` 匿名)回填 `owner`;siteConfig 走 `inject('siteConfig')`。页面不依赖登录态。
63+
64+
### 抽屉与悬浮钮移除
65+
66+
- `WidgetsDrawer.vue` 文件删除;App.vue 不再渲染它。
67+
- RightDock 删除第一个按钮(个人信息)与 `defineEmits(['toggle-profile'])`。
68+
- App.vue 删除:`widgetsOpen` ref、`toggleWidgets()`、`profile` ref、`loadProfile()`、`watch(route.fullPath, loadProfile)`、onMounted 中的 `loadProfile()` 调用、`WidgetsDrawer` import 与模板节点、`@toggle-profile="toggleWidgets"`(RightDock)、`@toggle-widgets="toggleWidgets"`(BlogHeader)。保留 `postSiteVisit()` 与 `fetchSiteConfig()` 逻辑。
69+
70+
## 错误处理
71+
72+
| 场景 | 表现 |
73+
|---|---|
74+
| `fetchOwnerProfile()` 网络/接口失败 | 页面正常渲染,作者区用回退值(占位头像、「—」昵称、回退签名);社交链接仍正常显示。不报错、不跳转 |
75+
| 头像为空 | `UserAvatar` 显示人形兜底图标 |
76+
77+
## 验证
78+
79+
1. `cd vue && npm run build` 通过(无编译/引用错误——特别是删文件后无残留引用)。
80+
2. 手动回归:
81+
- 顶部导航顺序:反馈之后依次是头像按钮、「关于我们」,无「源码」外链。
82+
- 未登录点「关于我们」→ 进入 `/about-us`,显示作者 yyycode 的头像/昵称/签名/简介/社交链接。
83+
- 登录普通读者账号访问 `/about-us` → 仍显示作者 yyycode(而非读者自己)。
84+
- 点头像按钮 → 未登录跳 `/login`,登录后进 `/profile`。
85+
- 右侧悬浮按钮:只剩深浅色 + 公告;「个人信息」抽屉不再出现。
86+
- 搜索页侧栏 ProfileCard / BlogInfoCard 仍正常显示。
87+
88+
## 不做(YAGNI)
89+
90+
- 不加邮箱等联系方式字段(siteConfig 无 email 字段,需后端改动,本期纯前端)。
91+
- 不改后端、不加接口、不加表。
92+
- 不动 `/about` 页、不动 `PublicLayout.vue`(死代码)、不改 `SearchResultsView`。
93+
- 不给「公告」悬浮按钮接新功能(本期只是随抽屉改动顺带保留现状)。

‎vue/src/App.vue‎

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,25 @@
11
<script setup>
2-
import { computed, onMounted, provide, ref, watch } from 'vue'
2+
import { computed, onMounted, provide, ref } from 'vue'
33
import { useRoute } from 'vue-router'
44
import BlogHeader from './components/BlogHeader.vue'
55
import WelcomeGate from './components/WelcomeGate.vue'
66
import BackToTop from './components/BackToTop.vue'
7-
import WidgetsDrawer from './components/WidgetsDrawer.vue'
87
import RightDock from './components/RightDock.vue'
98
import Live2dCharacter from './components/Live2dCharacter.vue'
10-
import { postSiteVisit } from './api/site'
11-
import { fetchSiteConfig } from './api/site'
12-
import { fetchPublicProfile } from './api/profile'
9+
import { postSiteVisit, fetchSiteConfig } from './api/site'
1310
1411
const route = useRoute()
1512
const isConsole = computed(() => route.path.startsWith('/console'))
16-
const widgetsOpen = ref(false)
17-
const profile = ref(null)
1813
const siteConfig = ref({})
1914
2015
// 提供 siteConfig 给所有子组件
2116
provide('siteConfig', siteConfig)
2217
23-
function toggleWidgets() {
24-
widgetsOpen.value = !widgetsOpen.value
25-
}
26-
27-
async function loadProfile() {
28-
if (route.path.startsWith('/console')) return
29-
try {
30-
profile.value = await fetchPublicProfile()
31-
} catch {
32-
profile.value = null
33-
}
34-
}
35-
36-
watch(
37-
() => route.fullPath,
38-
() => loadProfile()
39-
)
40-
4118
onMounted(() => {
4219
postSiteVisit().catch(() => {})
4320
})
4421
4522
onMounted(async () => {
46-
await loadProfile()
4723
// 加载站点配置
4824
try {
4925
siteConfig.value = await fetchSiteConfig()
@@ -56,9 +32,8 @@ onMounted(async () => {
5632
<template>
5733
<div class="blog-app">
5834
<WelcomeGate v-if="!isConsole" />
59-
<BlogHeader v-if="!isConsole" @toggle-widgets="toggleWidgets" />
60-
<RightDock v-if="!isConsole" @toggle-profile="toggleWidgets" />
61-
<WidgetsDrawer v-if="!isConsole" :open="widgetsOpen" :profile="profile" @close="widgetsOpen = false" />
35+
<BlogHeader v-if="!isConsole" />
36+
<RightDock v-if="!isConsole" />
6237
<BackToTop v-if="!isConsole" />
6338
<Live2dCharacter v-if="!isConsole" />
6439
<router-view />

‎vue/src/api/profile.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ export function fetchPublicProfile() {
1010
return request(`/api/v1/profile`)
1111
}
1212

13+
export function fetchOwnerProfile() {
14+
// 关于我们页:始终匿名请求,确保返回站点作者(而非当前登录用户)
15+
return request(`/api/v1/profile`)
16+
}
17+

‎vue/src/components/BlogHeader.vue‎

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,6 @@
7474
反馈
7575
</router-link>
7676

77-
<a
78-
class="site-nav-link site-nav-link-sm-hide"
79-
:href="(siteConfig && siteConfig.source_code_url) || 'https://github.com/yyyCode/OpenBlog.git'"
80-
target="_blank"
81-
rel="noopener noreferrer"
82-
>
83-
<span class="site-nav-ico" aria-hidden="true">
84-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
85-
<path
86-
d="M12 .5C5.73.5.75 5.67.75 12.09c0 5.15 3.28 9.52 7.83 11.06.57.11.78-.25.78-.55 0-.27-.01-1.17-.02-2.12-3.18.71-3.85-1.39-3.85-1.39-.52-1.36-1.28-1.72-1.28-1.72-1.05-.74.08-.73.08-.73 1.16.08 1.77 1.23 1.77 1.23 1.03 1.8 2.7 1.28 3.36.98.1-.77.4-1.28.73-1.57-2.54-.3-5.21-1.3-5.21-5.79 0-1.28.44-2.33 1.16-3.15-.12-.29-.5-1.48.11-3.08 0 0 .95-.31 3.11 1.2.9-.26 1.86-.39 2.82-.39.96 0 1.92.13 2.82.39 2.16-1.51 3.11-1.2 3.11-1.2.61 1.6.23 2.79.11 3.08.72.82 1.16 1.87 1.16 3.15 0 4.5-2.68 5.49-5.23 5.79.41.36.78 1.07.78 2.17 0 1.57-.02 2.83-.02 3.22 0 .3.21.67.79.55 4.55-1.54 7.83-5.91 7.83-11.06C23.25 5.67 18.27.5 12 .5z"
87-
/>
88-
</svg>
89-
</span>
90-
源码
91-
</a>
92-
9377
<div class="site-nav-auth site-nav-auth-trailing" aria-label="账户">
9478
<button
9579
type="button"
@@ -100,6 +84,17 @@
10084
<UserAvatar :url="me ? me.avatarUrl : ''" :size="32" />
10185
</button>
10286
</div>
87+
88+
<router-link to="/about-us" class="site-nav-link site-nav-link-sm-hide" active-class="active">
89+
<span class="site-nav-ico" aria-hidden="true">
90+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
91+
<circle cx="12" cy="12" r="9" />
92+
<path d="M12 8h.01" />
93+
<path d="M11 12h1v5h1" />
94+
</svg>
95+
</span>
96+
关于我们
97+
</router-link>
10398
</nav>
10499
</div>
105100
</header>
@@ -112,8 +107,6 @@ import { fetchMe } from '../api/admin'
112107
import { getStoredAccessToken, isJwtExpired, isLikelyJwt } from '../auth/session'
113108
import UserAvatar from './UserAvatar.vue'
114109
115-
defineEmits(['toggle-widgets'])
116-
117110
const route = useRoute()
118111
const router = useRouter()
119112
const me = ref(null)

‎vue/src/components/RightDock.vue‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
11
<template>
22
<div class="right-dock" :class="{ open: expanded }" @mouseenter="expanded = true" @mouseleave="onLeave">
3-
<button type="button" class="right-dock-btn" @click="emit('toggle-profile')" :aria-label="'打开个人信息'">
4-
<span class="right-dock-ico" aria-hidden="true">
5-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
6-
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
7-
<circle cx="12" cy="7" r="4" />
8-
</svg>
9-
</span>
10-
<span class="right-dock-text">个人信息</span>
11-
</button>
12-
133
<button type="button" class="right-dock-btn" @click="onToggle" :aria-label="isDark ? '切换到浅色模式' : '切换到深色模式'">
144
<span class="right-dock-ico" aria-hidden="true">{{ isDark ? '☀' : '☽' }}</span>
155
<span class="right-dock-text">{{ isDark ? '浅色' : '深色' }}</span>
@@ -31,7 +21,6 @@
3121
import { onMounted, ref } from 'vue'
3222
import { toggleTheme as applyToggle } from '../theme'
3323
34-
const emit = defineEmits(['toggle-profile'])
3524
const expanded = ref(false)
3625
const isDark = ref(false)
3726

‎vue/src/components/WidgetsDrawer.vue‎

Lines changed: 0 additions & 25 deletions
This file was deleted.

‎vue/src/router/index.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import AdminLoginView from '../views/AdminLoginView.vue'
99
import ChangelogListView from '../views/ChangelogListView.vue'
1010
import ChangelogDetailView from '../views/ChangelogDetailView.vue'
1111
import AboutView from '../views/AboutView.vue'
12+
import AboutUsView from '../views/AboutUsView.vue'
1213
import FeedbackView from '../views/FeedbackView.vue'
1314
import SearchResultsView from '../views/SearchResultsView.vue'
1415
import SiteAuthView from '../views/SiteAuthView.vue'
@@ -53,6 +54,11 @@ const routes = [
5354
name: 'about',
5455
component: AboutView
5556
},
57+
{
58+
path: '/about-us',
59+
name: 'aboutUs',
60+
component: AboutUsView
61+
},
5662
{
5763
path: '/search',
5864
name: 'search',

‎vue/src/views/AboutUsView.vue‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<template>
2+
<div class="page-wrap">
3+
<div class="card about-us-card">
4+
<div class="profile-avatar-wrap">
5+
<img class="profile-avatar" :src="avatarUrl" alt="avatar" />
6+
</div>
7+
8+
<div class="profile-name">{{ name }}</div>
9+
<div class="profile-signature">{{ signature }}</div>
10+
11+
<div class="profile-menu">
12+
<router-link class="profile-menu-item" to="/all">
13+
全部文章
14+
</router-link>
15+
<router-link class="profile-menu-item" to="/changelog">
16+
更新日志
17+
</router-link>
18+
</div>
19+
20+
<div class="about-us-intro">
21+
<p class="about-us-intro-text">
22+
你好,我是 yyycode,这个站点的作者。这里记录我在开发与学习中的经历、踩坑和思考,欢迎通过下面的方式联系我交流。
23+
</p>
24+
</div>
25+
26+
<div class="links-row">
27+
<a class="icon-link" :href="githubUrl" target="_blank" rel="noreferrer">
28+
GitHub
29+
</a>
30+
<a class="icon-link" :href="csdnUrl" target="_blank" rel="noreferrer">
31+
CSDN
32+
</a>
33+
<a class="icon-link" :href="nowcoderUrl" target="_blank" rel="noreferrer">
34+
牛客
35+
</a>
36+
</div>
37+
</div>
38+
</div>
39+
</template>
40+
41+
<script setup>
42+
import { computed, inject, onMounted, ref } from 'vue'
43+
import { fetchOwnerProfile } from '../api/profile'
44+
45+
const siteConfig = inject('siteConfig')
46+
47+
const owner = ref(null)
48+
49+
const avatarUrl = computed(() => {
50+
const a = owner.value?.avatarUrl
51+
if (a) return a
52+
return (
53+
(siteConfig.value && siteConfig.value.default_avatar_url) ||
54+
'https://via.placeholder.com/120x120.png?text=OpenBlog'
55+
)
56+
})
57+
const name = computed(() => (owner.value && owner.value.username) || '—')
58+
const signature = computed(() => (owner.value && owner.value.bio) || '平凡的一枚程序员')
59+
60+
const githubUrl = computed(() =>
61+
(siteConfig.value && siteConfig.value.github_url) || 'https://github.com/yyyCode'
62+
)
63+
const csdnUrl = computed(() =>
64+
(siteConfig.value && siteConfig.value.csdn_url) || 'https://blog.csdn.net/2301_80044822'
65+
)
66+
const nowcoderUrl = computed(() =>
67+
(siteConfig.value && siteConfig.value.nowcoder_url) || 'https://www.nowcoder.com/users/597303882'
68+
)
69+
70+
onMounted(async () => {
71+
try {
72+
owner.value = await fetchOwnerProfile()
73+
} catch {
74+
owner.value = null
75+
}
76+
})
77+
</script>
78+
79+
<style scoped>
80+
.page-wrap {
81+
display: flex;
82+
justify-content: center;
83+
padding: 28px 16px;
84+
}
85+
.about-us-card {
86+
width: 100%;
87+
max-width: 520px;
88+
padding: 28px 18px 20px;
89+
display: flex;
90+
flex-direction: column;
91+
align-items: center;
92+
text-align: center;
93+
}
94+
.about-us-intro {
95+
margin-top: 26px;
96+
}
97+
.about-us-intro-text {
98+
margin: 0;
99+
color: var(--muted);
100+
font-size: 14px;
101+
line-height: 1.9;
102+
}
103+
.about-us-card .links-row {
104+
margin-top: 22px;
105+
}
106+
</style>

0 commit comments

Comments
 (0)