From 991d545f65837c58e2c6a6af2a50da26c9266f1f Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Thu, 24 Jul 2025 04:53:53 +0800 Subject: [PATCH 1/9] BLOG-45 refactor: rename workflow name --- .gitea/workflows/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/deployment.yaml b/.gitea/workflows/deployment.yaml index 1f61d0d..fa9bbb8 100644 --- a/.gitea/workflows/deployment.yaml +++ b/.gitea/workflows/deployment.yaml @@ -5,7 +5,7 @@ on: - published jobs: - frontend-deployment: + deployment: runs-on: ubuntu-latest steps: - name: Checkout -- 2.47.2 From 901d367d9ddcfeefc324455d152faa083a3ef179 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Thu, 24 Jul 2025 04:54:44 +0800 Subject: [PATCH 2/9] BLOG-45 fix: trivial style --- frontend/src/lib/common/framework/ui/Footer.svelte | 2 +- frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts | 4 ++++ frontend/src/lib/post/framework/ui/PostOverallPage.svelte | 2 +- frontend/src/lib/post/framework/ui/PostPreview.svelte | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/common/framework/ui/Footer.svelte b/frontend/src/lib/common/framework/ui/Footer.svelte index b0814d1..3d80588 100644 --- a/frontend/src/lib/common/framework/ui/Footer.svelte +++ b/frontend/src/lib/common/framework/ui/Footer.svelte @@ -1,6 +1,6 @@
diff --git a/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts b/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts index ae6ed7d..68ae230 100644 --- a/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts +++ b/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts @@ -35,4 +35,8 @@ export class PostInfoViewModel { publishedTime: postInfo.publishedTime }); } + + get formattedPublishedTime(): string { + return this.publishedTime.toISOString().slice(0, 10); + } } diff --git a/frontend/src/lib/post/framework/ui/PostOverallPage.svelte b/frontend/src/lib/post/framework/ui/PostOverallPage.svelte index 85f051f..c9d3153 100644 --- a/frontend/src/lib/post/framework/ui/PostOverallPage.svelte +++ b/frontend/src/lib/post/framework/ui/PostOverallPage.svelte @@ -10,7 +10,7 @@ onMount(() => postListBloc.dispatch({ event: PostListEventType.PostListLoadedEvent })); -
+
文章
{#if state.status === StatusType.Success}
diff --git a/frontend/src/lib/post/framework/ui/PostPreview.svelte b/frontend/src/lib/post/framework/ui/PostPreview.svelte index b51844a..189c375 100644 --- a/frontend/src/lib/post/framework/ui/PostPreview.svelte +++ b/frontend/src/lib/post/framework/ui/PostPreview.svelte @@ -34,8 +34,8 @@
- {postInfo.title} + {postInfo.title} {postInfo.description} - 查看更多 ⭢ + {postInfo.formattedPublishedTime}
-- 2.47.2 From 25e9e470a642586028bf02cebff0c98a90c3dd25 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Thu, 24 Jul 2025 08:01:53 +0800 Subject: [PATCH 3/9] BLOG-45 fix: fixed post info order from backend --- .../src/framework/db/post_db_service_impl.rs | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/backend/feature/post/src/framework/db/post_db_service_impl.rs b/backend/feature/post/src/framework/db/post_db_service_impl.rs index 73d5a06..77311c1 100644 --- a/backend/feature/post/src/framework/db/post_db_service_impl.rs +++ b/backend/feature/post/src/framework/db/post_db_service_impl.rs @@ -68,21 +68,23 @@ impl PostDbService for PostDbServiceImpl { let mut post_info_mappers_map = HashMap::::new(); - for record in records { + for record in &records { let post_info = post_info_mappers_map .entry(record.post_id) .or_insert_with(|| PostInfoMapper { id: record.post_id, - title: record.title, - description: record.description, - preview_image_url: record.preview_image_url, + title: record.title.clone(), + description: record.description.clone(), + preview_image_url: record.preview_image_url.clone(), labels: Vec::new(), published_time: record.published_time, }); - if let (Some(label_id), Some(label_name), Some(label_color)) = - (record.label_id, record.label_name, record.label_color) - { + if let (Some(label_id), Some(label_name), Some(label_color)) = ( + record.label_id, + record.label_name.clone(), + record.label_color, + ) { post_info.labels.push(LabelMapper { id: label_id, name: label_name, @@ -93,7 +95,14 @@ impl PostDbService for PostDbServiceImpl { } } - Ok(post_info_mappers_map.into_values().collect()) + let mut ordered_posts = Vec::new(); + for record in &records { + if let Some(post_info) = post_info_mappers_map.remove(&record.post_id) { + ordered_posts.push(post_info); + } + } + + Ok(ordered_posts) } async fn get_full_post(&self, id: i32) -> Result { @@ -135,25 +144,27 @@ impl PostDbService for PostDbServiceImpl { let mut post_mappers_map = HashMap::::new(); - for record in records { + for record in &records { let post = post_mappers_map .entry(record.post_id) .or_insert_with(|| PostMapper { id: record.post_id, info: PostInfoMapper { id: record.post_id, - title: record.title, - description: record.description, - preview_image_url: record.preview_image_url, + title: record.title.clone(), + description: record.description.clone(), + preview_image_url: record.preview_image_url.clone(), labels: Vec::new(), published_time: record.published_time, }, - content: record.content, + content: record.content.clone(), }); - if let (Some(label_id), Some(label_name), Some(label_color)) = - (record.label_id, record.label_name, record.label_color) - { + if let (Some(label_id), Some(label_name), Some(label_color)) = ( + record.label_id, + record.label_name.clone(), + record.label_color, + ) { post.info.labels.push(LabelMapper { id: label_id, name: label_name, -- 2.47.2 From a73c3c33549166d9a34bdaeefe3e44f7271feb54 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Thu, 24 Jul 2025 08:03:15 +0800 Subject: [PATCH 4/9] BLOG-45 feat: improve efficiency with ssr --- frontend/src/app.d.ts | 6 ++- frontend/src/hooks.server.ts | 13 +++++++ .../common/adapter/presenter/asyncState.ts | 11 ++++-- .../post/adapter/presenter/colorViewModel.ts | 20 ++++++++++ .../post/adapter/presenter/labelViewModel.ts | 27 +++++++++++++- .../adapter/presenter/postInfoViewModel.ts | 36 +++++++++++++++++- .../post/adapter/presenter/postListBloc.ts | 37 ++++++++++++------- .../post/framework/api/postApiServiceImpl.ts | 4 +- .../src/lib/post/framework/ui/Label.svelte | 13 +++++++ .../post/framework/ui/PostContentPage.svelte | 6 +++ .../post/framework/ui/PostOverallPage.svelte | 12 +++--- .../framework/ui/PostPreviewLabels.svelte | 12 +----- frontend/src/routes/post/+page.server.ts | 12 ++++++ frontend/src/routes/post/+page.svelte | 12 ++++-- frontend/src/routes/post/[id]/+page.svelte | 7 ++++ frontend/src/routes/post/[id]/+page.ts | 7 ++++ 16 files changed, 194 insertions(+), 41 deletions(-) create mode 100644 frontend/src/hooks.server.ts create mode 100644 frontend/src/lib/post/framework/ui/Label.svelte create mode 100644 frontend/src/lib/post/framework/ui/PostContentPage.svelte create mode 100644 frontend/src/routes/post/+page.server.ts create mode 100644 frontend/src/routes/post/[id]/+page.svelte create mode 100644 frontend/src/routes/post/[id]/+page.ts diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index 1af894e..cac9cce 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -3,7 +3,11 @@ declare global { namespace App { // interface Error {} - // interface Locals {} + + interface Locals { + postListBloc: import('$lib/post/adapter/presenter/postListBloc').PostListBloc; + } + // interface PageData {} // interface PageState {} // interface Platform {} diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts new file mode 100644 index 0000000..b2b4de9 --- /dev/null +++ b/frontend/src/hooks.server.ts @@ -0,0 +1,13 @@ +import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl'; +import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc'; +import { GetAllPostUseCase } from '$lib/post/application/useCase/getAllPostsUseCase'; +import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl'; +import type { Handle } from '@sveltejs/kit'; + +export const handle: Handle = ({ event, resolve }) => { + const postApiService = new PostApiServiceImpl(event.fetch); + const postRepository = new PostRepositoryImpl(postApiService); + const getAllPostsUseCase = new GetAllPostUseCase(postRepository); + event.locals.postListBloc = new PostListBloc(getAllPostsUseCase); + return resolve(event); +}; diff --git a/frontend/src/lib/common/adapter/presenter/asyncState.ts b/frontend/src/lib/common/adapter/presenter/asyncState.ts index b80e9af..a058935 100644 --- a/frontend/src/lib/common/adapter/presenter/asyncState.ts +++ b/frontend/src/lib/common/adapter/presenter/asyncState.ts @@ -5,12 +5,14 @@ export enum StatusType { Error } -export interface IdleState { +export interface IdleState { status: StatusType.Idle; + data?: T; } -export interface LoadingState { +export interface LoadingState { status: StatusType.Loading; + data?: T; } export interface SuccessState { @@ -18,9 +20,10 @@ export interface SuccessState { data: T; } -export interface ErrorState { +export interface ErrorState { status: StatusType.Error; + data?: T; error: Error; } -export type AsyncState = IdleState | LoadingState | SuccessState | ErrorState; +export type AsyncState = IdleState | LoadingState | SuccessState | ErrorState; diff --git a/frontend/src/lib/post/adapter/presenter/colorViewModel.ts b/frontend/src/lib/post/adapter/presenter/colorViewModel.ts index 827aa46..cf08d2f 100644 --- a/frontend/src/lib/post/adapter/presenter/colorViewModel.ts +++ b/frontend/src/lib/post/adapter/presenter/colorViewModel.ts @@ -56,6 +56,10 @@ export class ColorViewModel { }); } + static rehydrate(props: DehydratedColorProps): ColorViewModel { + return new ColorViewModel(props); + } + get hex(): string { const toHex = (value: number) => value.toString(16).padStart(2, '0'); return `#${toHex(this.red)}${toHex(this.green)}${toHex(this.blue)}${toHex(this.alpha)}`; @@ -105,6 +109,15 @@ export class ColorViewModel { darken(amount: number): ColorViewModel { return this.lighten(-amount); } + + dehydrate(): DehydratedColorProps { + return { + red: this.red, + green: this.green, + blue: this.blue, + alpha: this.alpha + }; + } } interface Hsl { @@ -112,3 +125,10 @@ interface Hsl { s: number; l: number; } + +export interface DehydratedColorProps { + red: number; + green: number; + blue: number; + alpha: number; +} diff --git a/frontend/src/lib/post/adapter/presenter/labelViewModel.ts b/frontend/src/lib/post/adapter/presenter/labelViewModel.ts index 4abebd3..c4ba633 100644 --- a/frontend/src/lib/post/adapter/presenter/labelViewModel.ts +++ b/frontend/src/lib/post/adapter/presenter/labelViewModel.ts @@ -1,4 +1,7 @@ -import { ColorViewModel } from '$lib/post/adapter/presenter/colorViewModel'; +import { + ColorViewModel, + type DehydratedColorProps +} from '$lib/post/adapter/presenter/colorViewModel'; import type { Label } from '$lib/post/domain/entity/label'; export class LabelViewModel { @@ -19,4 +22,26 @@ export class LabelViewModel { color: ColorViewModel.fromEntity(label.color) }); } + + static rehydrate(props: DehydratedLabelProps): LabelViewModel { + return new LabelViewModel({ + id: props.id, + name: props.name, + color: ColorViewModel.rehydrate(props.color) + }); + } + + dehydrate(): DehydratedLabelProps { + return { + id: this.id, + name: this.name, + color: this.color.dehydrate() + }; + } +} + +export interface DehydratedLabelProps { + id: number; + name: string; + color: DehydratedColorProps; } diff --git a/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts b/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts index 68ae230..670acb7 100644 --- a/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts +++ b/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts @@ -1,4 +1,7 @@ -import { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel'; +import { + LabelViewModel, + type DehydratedLabelProps +} from '$lib/post/adapter/presenter/labelViewModel'; import type { PostInfo } from '$lib/post/domain/entity/postInfo'; export class PostInfoViewModel { @@ -36,7 +39,38 @@ export class PostInfoViewModel { }); } + static rehydrate(props: DehydratedPostInfoProps): PostInfoViewModel { + return new PostInfoViewModel({ + id: props.id, + title: props.title, + description: props.description, + previewImageUrl: new URL(props.previewImageUrl), + labels: props.labels.map((label) => LabelViewModel.rehydrate(label)), + publishedTime: new Date(props.publishedTime) + }); + } + get formattedPublishedTime(): string { return this.publishedTime.toISOString().slice(0, 10); } + + dehydrate(): DehydratedPostInfoProps { + return { + id: this.id, + title: this.title, + description: this.description, + previewImageUrl: this.previewImageUrl.href, + labels: this.labels.map((label) => label.dehydrate()), + publishedTime: this.publishedTime.getTime() + }; + } +} + +export interface DehydratedPostInfoProps { + id: number; + title: string; + description: string; + previewImageUrl: string; + labels: DehydratedLabelProps[]; + publishedTime: number; } diff --git a/frontend/src/lib/post/adapter/presenter/postListBloc.ts b/frontend/src/lib/post/adapter/presenter/postListBloc.ts index 3bccf0d..11829fe 100644 --- a/frontend/src/lib/post/adapter/presenter/postListBloc.ts +++ b/frontend/src/lib/post/adapter/presenter/postListBloc.ts @@ -1,35 +1,48 @@ import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel'; import type { GetAllPostUseCase } from '$lib/post/application/useCase/getAllPostsUseCase'; -import { writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; + +export type PostListState = AsyncState; +export type PostListEvent = PostListLoadedEvent; export class PostListBloc { - constructor(private readonly getAllPostsUseCase: GetAllPostUseCase) {} - - private readonly state = writable>({ + private readonly state = writable({ status: StatusType.Idle }); + constructor( + private readonly getAllPostsUseCase: GetAllPostUseCase, + initialData?: readonly PostInfoViewModel[] + ) { + this.state.set({ + status: StatusType.Idle, + data: initialData + }); + } + get subscribe() { return this.state.subscribe; } - dispatch(event: PostListEvent) { + async dispatch(event: PostListEvent): Promise { switch (event.event) { case PostListEventType.PostListLoadedEvent: - this.loadPosts(); - break; + return this.loadPosts(); } } - private async loadPosts() { - this.state.set({ status: StatusType.Loading }); + private async loadPosts(): Promise { + this.state.set({ status: StatusType.Loading, data: get(this.state).data }); const posts = await this.getAllPostsUseCase.execute(); const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post)); - this.state.set({ + const result: PostListState = { status: StatusType.Success, data: postViewModels - }); + }; + + this.state.set(result); + return result; } } @@ -40,5 +53,3 @@ export enum PostListEventType { export interface PostListLoadedEvent { event: PostListEventType.PostListLoadedEvent; } - -export type PostListEvent = PostListLoadedEvent; diff --git a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts index e1d4040..93091a2 100644 --- a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts +++ b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts @@ -3,10 +3,12 @@ import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto'; export class PostApiServiceImpl implements PostApiService { + constructor(private fetchFn: typeof fetch) {} + async getAllPosts(): Promise { const url = new URL('post/all', Environment.API_BASE_URL); - const response = await fetch(url.href); + const response = await this.fetchFn(url.href); if (!response.ok) { return []; diff --git a/frontend/src/lib/post/framework/ui/Label.svelte b/frontend/src/lib/post/framework/ui/Label.svelte new file mode 100644 index 0000000..58763c6 --- /dev/null +++ b/frontend/src/lib/post/framework/ui/Label.svelte @@ -0,0 +1,13 @@ + + +
+
+ {label.name} +
diff --git a/frontend/src/lib/post/framework/ui/PostContentPage.svelte b/frontend/src/lib/post/framework/ui/PostContentPage.svelte new file mode 100644 index 0000000..f428674 --- /dev/null +++ b/frontend/src/lib/post/framework/ui/PostContentPage.svelte @@ -0,0 +1,6 @@ + + diff --git a/frontend/src/lib/post/framework/ui/PostOverallPage.svelte b/frontend/src/lib/post/framework/ui/PostOverallPage.svelte index c9d3153..19cdd4d 100644 --- a/frontend/src/lib/post/framework/ui/PostOverallPage.svelte +++ b/frontend/src/lib/post/framework/ui/PostOverallPage.svelte @@ -12,11 +12,9 @@
文章
- {#if state.status === StatusType.Success} -
- {#each state.data as postInfo (postInfo.id)} - - {/each} -
- {/if} +
+ {#each state.data ?? [] as postInfo (postInfo.id)} + + {/each} +
diff --git a/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte b/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte index d9e132a..470271e 100644 --- a/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte +++ b/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte @@ -1,21 +1,13 @@
{#each labels.slice(0, 2) as label (label.id)} -
-
- {label.name} -
+