Skip to content

Commit 16566ce

Browse files
committed
feat(article): add article type support — JOB_NAV type, /jobs route, compose type selector, API, migration SQL
1 parent 452b8e3 commit 16566ce

9 files changed

Lines changed: 145 additions & 4 deletions

File tree

OpenBlog-business/src/main/java/com/yqz/openblog/article/dto/ArticleDetailResponse.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.yqz.openblog.article.dto;
22

33
import com.yqz.openblog.article.entity.ArticleStatus;
4+
import com.yqz.openblog.article.entity.ArticleType;
45

56
import java.time.Instant;
67

@@ -15,6 +16,7 @@ public class ArticleDetailResponse {
1516
private String authorNickname;
1617
private Instant publishedAt;
1718
private ArticleStatus status;
19+
private ArticleType type;
1820
private Long likeCount;
1921
private Long viewCount;
2022
private Long favoriteCount;
@@ -105,6 +107,14 @@ public void setStatus(ArticleStatus status) {
105107
this.status = status;
106108
}
107109

110+
public ArticleType getType() {
111+
return type;
112+
}
113+
114+
public void setType(ArticleType type) {
115+
this.type = type;
116+
}
117+
108118
public Long getLikeCount() {
109119
return likeCount;
110120
}

OpenBlog-business/src/main/java/com/yqz/openblog/article/dto/ArticlePublishedContentCachePayload.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.yqz.openblog.article.dto;
22

33
import com.yqz.openblog.article.entity.ArticleStatus;
4+
import com.yqz.openblog.article.entity.ArticleType;
45

56
import java.time.Instant;
67

@@ -19,6 +20,7 @@ public class ArticlePublishedContentCachePayload {
1920
private String authorNickname;
2021
private Instant publishedAt;
2122
private ArticleStatus status;
23+
private ArticleType type;
2224
private Instant createdAt;
2325
private Instant updatedAt;
2426
private Long categoryId;
@@ -37,6 +39,7 @@ public static ArticlePublishedContentCachePayload fromDetail(ArticleDetailRespon
3739
p.setAuthorNickname(d.getAuthorNickname());
3840
p.setPublishedAt(d.getPublishedAt());
3941
p.setStatus(d.getStatus());
42+
p.setType(d.getType());
4043
p.setCreatedAt(d.getCreatedAt());
4144
p.setUpdatedAt(d.getUpdatedAt());
4245
p.setCategoryId(d.getCategoryId());
@@ -125,6 +128,14 @@ public void setStatus(ArticleStatus status) {
125128
this.status = status;
126129
}
127130

131+
public ArticleType getType() {
132+
return type;
133+
}
134+
135+
public void setType(ArticleType type) {
136+
this.type = type;
137+
}
138+
128139
public Instant getCreatedAt() {
129140
return createdAt;
130141
}

OpenBlog-business/src/main/java/com/yqz/openblog/article/service/ArticleService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ public ArticleDetailResponse mapDetail(Article a) {
146146
resp.setAuthorNickname(author == null ? null : author.getUsername());
147147
resp.setPublishedAt(a.getPublishedAt());
148148
resp.setStatus(a.getStatus());
149+
resp.setType(a.getType());
149150
resp.setLikeCount(a.getLikeCount());
150151
resp.setViewCount(a.getViewCount() == null ? 0L : a.getViewCount());
151152
resp.setFavoriteCount(a.getFavoriteCount());
@@ -206,6 +207,7 @@ private ArticleDetailResponse mergePublishedDetailFromCache(ArticlePublishedCont
206207
r.setAuthorNickname(p.getAuthorNickname());
207208
r.setPublishedAt(p.getPublishedAt());
208209
r.setStatus(p.getStatus());
210+
r.setType(p.getType());
209211
r.setCreatedAt(p.getCreatedAt());
210212
r.setUpdatedAt(p.getUpdatedAt());
211213
r.setCategoryId(p.getCategoryId());
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- 为 articles 表添加 type 字段,默认值为 ARTICLE
2+
ALTER TABLE articles ADD COLUMN type VARCHAR(16) NOT NULL DEFAULT 'ARTICLE' AFTER status;

vue/src/api/article.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export function fetchArticleDetail(id) {
1212
return request(`/api/v1/articles/${id}`)
1313
}
1414

15+
export function fetchArticlesByType(type, params = {}) {
16+
const size = params.size ?? 20
17+
const page = params.page ?? 0
18+
return request(`/api/v1/articles/type/${type}?page=${page}&size=${size}`)
19+
}
20+
1521
export function coverUrl(key) {
1622
if (!key) return ''
1723
return `${API_BASE}/api/v1/media/files/${key}`

vue/src/components/BlogHeader.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@
4545
项目推荐
4646
</router-link>
4747

48-
<a href="#" class="site-nav-link">
48+
<router-link to="/jobs" class="site-nav-link" active-class="active">
4949
<span class="site-nav-ico" aria-hidden="true">
5050
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
5151
<path d="M20 7h-4V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2H4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
5252
<path d="M9 7V5h6v2" />
5353
</svg>
5454
</span>
5555
求职导航
56-
</a>
56+
</router-link>
5757

5858
<router-link to="/feedback" class="site-nav-link" active-class="active">
5959
<span class="site-nav-ico" aria-hidden="true">

vue/src/router/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import ConsoleUsersView from '../views/ConsoleUsersView.vue'
2727
import ConsoleUserDetailView from '../views/ConsoleUserDetailView.vue'
2828
import ConsoleFeedbackView from '../views/ConsoleFeedbackView.vue'
2929
import ConsoleSiteConfigView from '../views/ConsoleSiteConfigView.vue'
30+
import JobNavView from '../views/JobNavView.vue'
3031
import ProjectsListView from '../views/ProjectsListView.vue'
3132
import ProjectDetailView from '../views/ProjectDetailView.vue'
3233
import ConsoleProjectsView from '../views/ConsoleProjectsView.vue'
@@ -52,6 +53,11 @@ const routes = [
5253
name: 'search',
5354
component: SearchResultsView
5455
},
56+
{
57+
path: '/jobs',
58+
name: 'jobNav',
59+
component: JobNavView
60+
},
5561
{
5662
path: '/projects',
5763
name: 'projectsList',

vue/src/views/ConsoleArticleComposeView.vue

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,22 @@
186186

187187
<div class="field">
188188

189+
<div class="label">文章类型</div>
190+
191+
<select v-model="form.type" class="input">
192+
193+
<option value="ARTICLE">博客文章</option>
194+
195+
<option value="JOB_NAV">求职导航</option>
196+
197+
</select>
198+
199+
</div>
200+
201+
202+
203+
<div>
204+
189205
<div class="label">分类(可选)</div>
190206

191207
<select v-model="form.categoryId" class="input">
@@ -462,7 +478,9 @@ const form = ref({
462478
463479
coverMediaKey: null,
464480
465-
categoryId: null
481+
categoryId: null,
482+
483+
type: 'ARTICLE'
466484
467485
})
468486
@@ -858,6 +876,8 @@ async function loadEditor(id) {
858876
859877
form.value.categoryId = detail.categoryId ?? null
860878
879+
form.value.type = detail.type || 'ARTICLE'
880+
861881
publishAtInput.value = toLocalDatetimeInput(detail.publishedAt)
862882
863883
} finally {
@@ -1048,7 +1068,9 @@ async function saveDraft() {
10481068
10491069
coverMediaKey: form.value.coverMediaKey,
10501070
1051-
categoryId: form.value.categoryId
1071+
categoryId: form.value.categoryId,
1072+
1073+
type: form.value.type
10521074
10531075
}
10541076

vue/src/views/JobNavView.vue

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<template>
2+
<div class="blog-container jobs-page">
3+
<div class="page-header">
4+
<h1 class="page-title">求职导航</h1>
5+
<p class="page-subtitle">求职经验、面试技巧、行业洞见</p>
6+
</div>
7+
8+
<div v-if="loading" class="article-detail-loading">加载中...</div>
9+
10+
<div v-else-if="articles.length === 0" class="card">
11+
<div class="card-body" style="padding: 22px; color: var(--muted)">暂无内容</div>
12+
</div>
13+
14+
<div v-else class="article-grid">
15+
<ArticleCard v-for="a in articles" :key="a.id" :article="a" />
16+
</div>
17+
18+
<div v-if="total > size" class="pagination" style="margin-top: 24px; display: flex; justify-content: center; gap: 12px">
19+
<button class="btn" :disabled="page <= 0" @click="goPage(page - 1)">上一页</button>
20+
<span style="line-height: 36px; color: var(--muted); font-size: 13px">
21+
{{ page + 1 }} / {{ Math.ceil(total / size) }}
22+
</span>
23+
<button class="btn" :disabled="(page + 1) * size >= total" @click="goPage(page + 1)">下一页</button>
24+
</div>
25+
</div>
26+
</template>
27+
28+
<script setup>
29+
import { onMounted, ref, watch } from 'vue'
30+
import { useRoute, useRouter } from 'vue-router'
31+
import { fetchArticlesByType } from '../api/article'
32+
import ArticleCard from '../components/ArticleCard.vue'
33+
34+
const route = useRoute()
35+
const router = useRouter()
36+
const loading = ref(true)
37+
const articles = ref([])
38+
const page = ref(0)
39+
const size = 20
40+
const total = ref(0)
41+
42+
async function load() {
43+
loading.value = true
44+
try {
45+
const p = Number(route.query.page) || 0
46+
page.value = p
47+
const resp = await fetchArticlesByType('JOB_NAV', { page: p, size })
48+
articles.value = resp?.items || []
49+
total.value = resp?.total ?? 0
50+
} finally {
51+
loading.value = false
52+
}
53+
}
54+
55+
function goPage(p) {
56+
router.push({ query: { page: String(p) } })
57+
}
58+
59+
watch(() => route.query.page, () => load())
60+
onMounted(() => load())
61+
</script>
62+
63+
<style scoped>
64+
.page-header {
65+
margin-bottom: 24px;
66+
}
67+
.page-title {
68+
font-size: 28px;
69+
font-weight: 800;
70+
letter-spacing: -0.02em;
71+
}
72+
.page-subtitle {
73+
color: var(--muted);
74+
font-size: 14px;
75+
margin-top: 6px;
76+
}
77+
.article-grid {
78+
display: flex;
79+
flex-direction: column;
80+
gap: 16px;
81+
}
82+
</style>

0 commit comments

Comments
 (0)