Skip to content

Commit 081c119

Browse files
committed
更新博客展示UI
1 parent 6d4335a commit 081c119

3 files changed

Lines changed: 283 additions & 38 deletions

File tree

frontend/src/assets/blog.css

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,11 @@ body {
13501350
color: var(--accent);
13511351
}
13521352

1353+
/* 文章详情页:同时有“博客/目录”两个左侧按钮,避免重叠 */
1354+
.article-detail-page .toc-fab {
1355+
top: calc(50% + 54px);
1356+
}
1357+
13531358
.toc-overlay {
13541359
position: fixed;
13551360
inset: 0;
@@ -1446,6 +1451,118 @@ body {
14461451
outline-offset: 2px;
14471452
}
14481453

1454+
/* 全部文章:文章列表抽屉(默认仅左侧按钮) */
1455+
.articles-fab {
1456+
position: fixed;
1457+
left: max(14px, env(safe-area-inset-left, 0px));
1458+
top: 50%;
1459+
transform: translateY(-50%);
1460+
z-index: 9997;
1461+
border: 1px solid var(--border);
1462+
background: var(--card);
1463+
color: var(--text);
1464+
border-radius: 999px;
1465+
padding: 10px 14px;
1466+
font-size: 13px;
1467+
font-weight: 750;
1468+
cursor: pointer;
1469+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
1470+
}
1471+
1472+
.articles-fab:hover {
1473+
border-color: rgba(170, 59, 255, 0.45);
1474+
color: var(--accent);
1475+
}
1476+
1477+
.articles-overlay {
1478+
position: fixed;
1479+
inset: 0;
1480+
background: rgba(0, 0, 0, 0.16);
1481+
z-index: 10000;
1482+
}
1483+
1484+
[data-theme='dark'] .articles-overlay {
1485+
background: rgba(0, 0, 0, 0.45);
1486+
}
1487+
1488+
.articles-drawer {
1489+
position: fixed;
1490+
top: 0;
1491+
left: 0;
1492+
height: 100svh;
1493+
width: min(360px, calc(100vw - 56px));
1494+
background: rgba(255, 255, 255, 0.72);
1495+
border-right: 1px solid var(--border);
1496+
backdrop-filter: saturate(1.15) blur(18px);
1497+
-webkit-backdrop-filter: saturate(1.15) blur(18px);
1498+
z-index: 10001;
1499+
transform: translateX(-102%);
1500+
transition: transform 0.22s ease;
1501+
display: flex;
1502+
flex-direction: column;
1503+
}
1504+
1505+
[data-theme='dark'] .articles-drawer {
1506+
background: rgba(18, 18, 20, 0.82);
1507+
}
1508+
1509+
.articles-drawer.open {
1510+
transform: translateX(0);
1511+
}
1512+
1513+
.articles-drawer-head {
1514+
display: flex;
1515+
align-items: center;
1516+
justify-content: space-between;
1517+
gap: 12px;
1518+
padding: 14px 14px 10px;
1519+
border-bottom: 1px solid var(--border);
1520+
}
1521+
1522+
.articles-drawer-title {
1523+
font-size: 13px;
1524+
font-weight: 650;
1525+
color: var(--text);
1526+
letter-spacing: 0.06em;
1527+
}
1528+
1529+
.articles-drawer-close {
1530+
border: none;
1531+
background: transparent;
1532+
color: var(--muted);
1533+
cursor: pointer;
1534+
font-size: 22px;
1535+
line-height: 1;
1536+
padding: 2px 6px;
1537+
}
1538+
1539+
.articles-drawer-close:hover {
1540+
color: var(--text);
1541+
}
1542+
1543+
.articles-drawer-search {
1544+
padding: 12px 14px 6px;
1545+
}
1546+
1547+
.articles-drawer-body {
1548+
padding: 10px 14px 14px;
1549+
overflow: auto;
1550+
display: flex;
1551+
flex-direction: column;
1552+
gap: 10px;
1553+
}
1554+
1555+
.articles-drawer-item {
1556+
width: 100%;
1557+
text-align: left;
1558+
font: inherit;
1559+
}
1560+
1561+
.articles-drawer-item:focus-visible {
1562+
outline: 2px solid rgba(170, 59, 255, 0.6);
1563+
outline-offset: 2px;
1564+
}
1565+
14491566
.article-content {
14501567
padding: 20px 18px 40px;
14511568
}

frontend/src/views/AllArticlesView.vue

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,6 @@
11
<template>
22
<div class="blog-container all-articles-page">
33
<div class="all-layout">
4-
<!-- 左侧:头像 + 搜索 + 文章列表 -->
5-
<aside class="all-sidebar">
6-
7-
<div class="sidebar-search">
8-
<input
9-
v-model="keyword"
10-
type="text"
11-
class="search-input"
12-
placeholder="find something..."
13-
/>
14-
</div>
15-
16-
<div class="sidebar-list">
17-
<div v-if="loadingList" class="sidebar-hint" style="color: var(--muted)">
18-
加载中...
19-
</div>
20-
<div v-else-if="listItems.length === 0" class="sidebar-hint" style="color: var(--muted)">
21-
暂无文章
22-
</div>
23-
24-
<div
25-
v-for="a in listItems"
26-
:key="a.id"
27-
class="sidebar-item"
28-
:class="{ active: a.id === selectedId }"
29-
@click="selectArticle(a.id)"
30-
>
31-
<div class="sidebar-item-title">{{ a.title }}</div>
32-
<div class="sidebar-item-date">{{ formatDate(a.publishedAt) }}</div>
33-
</div>
34-
</div>
35-
</aside>
36-
374
<!-- 右侧:文章正文 -->
385
<main class="all-main">
396
<div v-if="loadingDetail" class="card">
@@ -69,17 +36,67 @@
6936
</div>
7037
</main>
7138
</div>
39+
40+
<!-- 外挂式文章列表:左侧抽屉 -->
41+
<button
42+
class="articles-fab"
43+
type="button"
44+
:aria-expanded="listOpen ? 'true' : 'false'"
45+
aria-controls="articles-drawer"
46+
@click="openList"
47+
>
48+
博客
49+
</button>
50+
51+
<div v-if="listOpen" class="articles-overlay" @click="closeList" />
52+
<aside
53+
id="articles-drawer"
54+
class="articles-drawer"
55+
:class="{ open: listOpen }"
56+
role="dialog"
57+
aria-label="博客文章列表"
58+
>
59+
<div class="articles-drawer-head">
60+
<div class="articles-drawer-title">博客</div>
61+
<button class="articles-drawer-close" type="button" aria-label="关闭文章列表" @click="closeList">
62+
×
63+
</button>
64+
</div>
65+
66+
<div class="articles-drawer-search">
67+
<input v-model="keyword" type="text" class="search-input" placeholder="find something..." />
68+
</div>
69+
70+
<div class="articles-drawer-body">
71+
<div v-if="loadingList" class="sidebar-hint" style="color: var(--muted)">加载中...</div>
72+
<div v-else-if="listItems.length === 0" class="sidebar-hint" style="color: var(--muted)">暂无文章</div>
73+
74+
<button
75+
v-for="a in listItems"
76+
:key="a.id"
77+
class="sidebar-item articles-drawer-item"
78+
type="button"
79+
@click="goArticle(a.id)"
80+
>
81+
<div class="sidebar-item-title">{{ a.title }}</div>
82+
<div class="sidebar-item-date">{{ formatDate(a.publishedAt) }}</div>
83+
</button>
84+
</div>
85+
</aside>
7286
</div>
7387
</template>
7488

7589
<script setup>
76-
import { computed, onMounted, ref } from 'vue'
90+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
7791
import DOMPurify from 'dompurify'
7892
import { marked } from 'marked'
93+
import { useRouter } from 'vue-router'
7994
8095
import { fetchArticles, fetchArticleDetail, coverUrl } from '../api/article'
8196
import CommentSection from '../components/CommentSection.vue'
8297
98+
const router = useRouter()
99+
83100
const loadingList = ref(true)
84101
const loadingDetail = ref(true)
85102
@@ -120,6 +137,28 @@ function selectArticle(id) {
120137
loadDetail(id)
121138
}
122139
140+
const listOpen = ref(false)
141+
142+
function openList() {
143+
listOpen.value = true
144+
}
145+
146+
function closeList() {
147+
listOpen.value = false
148+
}
149+
150+
function goArticle(id) {
151+
closeList()
152+
router.push(`/article/${id}`)
153+
}
154+
155+
function onKeyDown(e) {
156+
if (e.key === 'Escape' && listOpen.value) closeList()
157+
}
158+
159+
onMounted(() => window.addEventListener('keydown', onKeyDown))
160+
onBeforeUnmount(() => window.removeEventListener('keydown', onKeyDown))
161+
123162
const listItems = computed(() => {
124163
const k = (keyword.value || '').trim().toLowerCase()
125164
if (!k) return allArticles.value

frontend/src/views/ArticleDetailView.vue

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,54 @@
3636
</template>
3737
</div>
3838

39+
<!-- 外挂式文章列表:左侧抽屉 -->
40+
<button
41+
class="articles-fab"
42+
type="button"
43+
:aria-expanded="listOpen ? 'true' : 'false'"
44+
aria-controls="articles-drawer"
45+
@click="openList"
46+
>
47+
博客
48+
</button>
49+
50+
<div v-if="listOpen" class="articles-overlay" @click="closeList" />
51+
<aside
52+
id="articles-drawer"
53+
class="articles-drawer"
54+
:class="{ open: listOpen }"
55+
role="dialog"
56+
aria-label="博客文章列表"
57+
>
58+
<div class="articles-drawer-head">
59+
<div class="articles-drawer-title">博客</div>
60+
<button class="articles-drawer-close" type="button" aria-label="关闭文章列表" @click="closeList">
61+
×
62+
</button>
63+
</div>
64+
65+
<div class="articles-drawer-search">
66+
<input v-model="keyword" type="text" class="search-input" placeholder="find something..." />
67+
</div>
68+
69+
<div class="articles-drawer-body">
70+
<div v-if="loadingList" class="sidebar-hint" style="color: var(--muted)">加载中...</div>
71+
<div v-else-if="listItems.length === 0" class="sidebar-hint" style="color: var(--muted)">暂无文章</div>
72+
73+
<button
74+
v-for="a in listItems"
75+
:key="a.id"
76+
class="sidebar-item articles-drawer-item"
77+
:class="{ active: String(a.id) === String(route.params.id) }"
78+
type="button"
79+
@click="goArticle(a.id)"
80+
>
81+
<div class="sidebar-item-title">{{ a.title }}</div>
82+
<div class="sidebar-item-date">{{ formatDate(a.publishedAt) }}</div>
83+
</button>
84+
</div>
85+
</aside>
86+
3987
<!-- 外挂式目录:左侧抽屉 -->
4088
<button
4189
v-if="tocItems.length"
@@ -79,15 +127,16 @@
79127
</template>
80128

81129
<script setup>
82-
import { onBeforeUnmount, onMounted, ref, watchEffect } from 'vue'
83-
import { useRoute } from 'vue-router'
130+
import { computed, onBeforeUnmount, onMounted, ref, watchEffect } from 'vue'
131+
import { useRoute, useRouter } from 'vue-router'
84132
import DOMPurify from 'dompurify'
85133
import { marked } from 'marked'
86134
87-
import { fetchArticleDetail, coverUrl } from '../api/article'
135+
import { fetchArticleDetail, fetchArticles, coverUrl } from '../api/article'
88136
import CommentSection from '../components/CommentSection.vue'
89137
90138
const route = useRoute()
139+
const router = useRouter()
91140
const loading = ref(true)
92141
const article = ref({})
93142
const html = ref('')
@@ -104,6 +153,43 @@ onMounted(async () => {
104153
}
105154
})
106155
156+
const listOpen = ref(false)
157+
const loadingList = ref(true)
158+
const keyword = ref('')
159+
const allArticles = ref([])
160+
161+
onMounted(async () => {
162+
try {
163+
loadingList.value = true
164+
const resp = await fetchArticles({ page: 0, size: 200 })
165+
allArticles.value = resp?.items || []
166+
} finally {
167+
loadingList.value = false
168+
}
169+
})
170+
171+
const listItems = computed(() => {
172+
const k = (keyword.value || '').trim().toLowerCase()
173+
if (!k) return allArticles.value
174+
return allArticles.value.filter(a => (a.title || '').toLowerCase().includes(k))
175+
})
176+
177+
function openList() {
178+
listOpen.value = true
179+
}
180+
181+
function closeList() {
182+
listOpen.value = false
183+
}
184+
185+
function goArticle(id) {
186+
closeList()
187+
tocOpen.value = false
188+
router.push(`/article/${id}`).then(() => {
189+
window.scrollTo({ top: 0, behavior: 'auto' })
190+
})
191+
}
192+
107193
watchEffect(() => {
108194
const md = article.value?.contentMarkdown || ''
109195
const rawHtml = marked.parse(md)
@@ -149,7 +235,10 @@ function jumpToHeading(id) {
149235
}
150236
151237
function onKeyDown(e) {
152-
if (e.key === 'Escape' && tocOpen.value) tocOpen.value = false
238+
if (e.key === 'Escape') {
239+
if (listOpen.value) listOpen.value = false
240+
if (tocOpen.value) tocOpen.value = false
241+
}
153242
}
154243
155244
onMounted(() => window.addEventListener('keydown', onKeyDown))

0 commit comments

Comments
 (0)