LabelResponseDto.fromJson(JSON.stringify(label))),
- publishedTime: new Date(parsedJson.published_time / 1000),
+ labels: parsedJson.labels.map((label) => LabelResponseDto.fromJson(label)),
+ publishedTime: new Date(parsedJson.published_time / 1000)
});
}
@@ -54,7 +54,7 @@ export class PostInfoResponseDto {
description: this.description,
previewImageUrl: this.previewImageUrl,
labels: this.labels.map((label) => label.toEntity()),
- publishedTime: this.publishedTime,
+ publishedTime: this.publishedTime
});
}
}
diff --git a/frontend/src/lib/post/adapter/presenter/colorViewModel.ts b/frontend/src/lib/post/adapter/presenter/colorViewModel.ts
new file mode 100644
index 0000000..827aa46
--- /dev/null
+++ b/frontend/src/lib/post/adapter/presenter/colorViewModel.ts
@@ -0,0 +1,114 @@
+import type { Color } from '$lib/post/domain/entity/color';
+
+export class ColorViewModel {
+ readonly red: number;
+ readonly green: number;
+ readonly blue: number;
+ readonly alpha: number;
+
+ private constructor(props: { red: number; green: number; blue: number; alpha: number }) {
+ this.red = props.red;
+ this.green = props.green;
+ this.blue = props.blue;
+ this.alpha = props.alpha;
+ }
+
+ private static fromHsl(hsl: Hsl): ColorViewModel {
+ const { h, s, l } = hsl;
+ let r, g, b;
+
+ if (s === 0) {
+ // achromatic (grayscale)
+ r = g = b = l;
+ } else {
+ const hue2rgb = (p: number, q: number, t: number) => {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ };
+
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+ const h_norm = h / 360;
+
+ r = hue2rgb(p, q, h_norm + 1 / 3);
+ g = hue2rgb(p, q, h_norm);
+ b = hue2rgb(p, q, h_norm - 1 / 3);
+ }
+
+ return new ColorViewModel({
+ red: Math.round(r * 255),
+ green: Math.round(g * 255),
+ blue: Math.round(b * 255),
+ alpha: 255
+ });
+ }
+
+ static fromEntity(color: Color): ColorViewModel {
+ return new ColorViewModel({
+ red: color.red,
+ green: color.green,
+ blue: color.blue,
+ alpha: color.alpha
+ });
+ }
+
+ 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)}`;
+ }
+
+ private toHsl(): Hsl {
+ const r = this.red / 255;
+ const g = this.green / 255;
+ const b = this.blue / 255;
+
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+ let h = 0,
+ s = 0;
+ const l = (max + min) / 2;
+
+ if (max === min) {
+ // achromatic (grayscale)
+ h = s = 0;
+ } else {
+ const d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch (max) {
+ case r:
+ h = (g - b) / d + (g < b ? 6 : 0);
+ break;
+ case g:
+ h = (b - r) / d + 2;
+ break;
+ case b:
+ h = (r - g) / d + 4;
+ break;
+ }
+ h /= 6;
+ }
+
+ return { h: h * 360, s: s, l: l };
+ }
+
+ lighten(amount: number): ColorViewModel {
+ const hsl = this.toHsl();
+ hsl.l += amount;
+ hsl.l = Math.max(0, Math.min(1, hsl.l));
+ return ColorViewModel.fromHsl(hsl);
+ }
+
+ darken(amount: number): ColorViewModel {
+ return this.lighten(-amount);
+ }
+}
+
+interface Hsl {
+ h: number;
+ s: number;
+ l: number;
+}
diff --git a/frontend/src/lib/post/adapter/presenter/labelViewModel.ts b/frontend/src/lib/post/adapter/presenter/labelViewModel.ts
index 3ff7d46..4abebd3 100644
--- a/frontend/src/lib/post/adapter/presenter/labelViewModel.ts
+++ b/frontend/src/lib/post/adapter/presenter/labelViewModel.ts
@@ -1,19 +1,22 @@
+import { ColorViewModel } from '$lib/post/adapter/presenter/colorViewModel';
+import type { Label } from '$lib/post/domain/entity/label';
+
export class LabelViewModel {
readonly id: number;
readonly name: string;
- readonly color: string;
+ readonly color: ColorViewModel;
- private constructor(props: { id: number; name: string; color: string }) {
+ private constructor(props: { id: number; name: string; color: ColorViewModel }) {
this.id = props.id;
this.name = props.name;
this.color = props.color;
}
- static fromEntity(label: { id: number; name: string; color: string }): LabelViewModel {
+ static fromEntity(label: Label): LabelViewModel {
return new LabelViewModel({
id: label.id,
name: label.name,
- color: label.color
+ color: ColorViewModel.fromEntity(label.color)
});
}
}
diff --git a/frontend/src/lib/post/domain/entity/color.ts b/frontend/src/lib/post/domain/entity/color.ts
new file mode 100644
index 0000000..8f6afa2
--- /dev/null
+++ b/frontend/src/lib/post/domain/entity/color.ts
@@ -0,0 +1,13 @@
+export class Color {
+ readonly red: number;
+ readonly green: number;
+ readonly blue: number;
+ readonly alpha: number;
+
+ constructor(props: { red: number; green: number; blue: number; alpha: number }) {
+ this.red = props.red;
+ this.green = props.green;
+ this.blue = props.blue;
+ this.alpha = props.alpha;
+ }
+}
diff --git a/frontend/src/lib/post/domain/entity/label.ts b/frontend/src/lib/post/domain/entity/label.ts
index a13de0c..c264ec6 100644
--- a/frontend/src/lib/post/domain/entity/label.ts
+++ b/frontend/src/lib/post/domain/entity/label.ts
@@ -1,9 +1,11 @@
+import type { Color } from '$lib/post/domain/entity/color';
+
export class Label {
readonly id: number;
readonly name: string;
- readonly color: string;
+ readonly color: Color;
- constructor(props: { id: number; name: string; color: string }) {
+ constructor(props: { id: number; name: string; color: Color }) {
this.id = props.id;
this.name = props.name;
this.color = props.color;
diff --git a/frontend/src/lib/post/framework/ui/PostListPage.svelte b/frontend/src/lib/post/framework/ui/PostOverallPage.svelte
similarity index 61%
rename from frontend/src/lib/post/framework/ui/PostListPage.svelte
rename to frontend/src/lib/post/framework/ui/PostOverallPage.svelte
index 5b2d787..85f051f 100644
--- a/frontend/src/lib/post/framework/ui/PostListPage.svelte
+++ b/frontend/src/lib/post/framework/ui/PostOverallPage.svelte
@@ -1,23 +1,22 @@
文章
- {#if state.status === StatusType.Loading || state.status === StatusType.Idle}
-
Loading
- {:else if state.status === StatusType.Success}
-
{JSON.stringify(state.data)}
- {:else}
-
Error loading posts
+ {#if state.status === StatusType.Success}
+
+ {#each state.data as postInfo (postInfo.id)}
+
+ {/each}
+
{/if}
diff --git a/frontend/src/lib/post/framework/ui/PostPreview.svelte b/frontend/src/lib/post/framework/ui/PostPreview.svelte
new file mode 100644
index 0000000..b51844a
--- /dev/null
+++ b/frontend/src/lib/post/framework/ui/PostPreview.svelte
@@ -0,0 +1,41 @@
+
+
+
+
+

+ {#if isImageLoading || isImageError}
+
+ {/if}
+
+
+
+
{postInfo.title}
+
{postInfo.description}
+
查看更多 ⭢
+
+
diff --git a/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte b/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte
new file mode 100644
index 0000000..d9e132a
--- /dev/null
+++ b/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte
@@ -0,0 +1,25 @@
+
+
+
+ {#each labels.slice(0, 2) as label (label.id)}
+
+ {/each}
+ {#if labels.length > 2}
+
+ +{labels.length - 2}
+
+ {/if}
+
diff --git a/frontend/src/routes/post/+page.svelte b/frontend/src/routes/post/+page.svelte
index 370e0e7..3a1860d 100644
--- a/frontend/src/routes/post/+page.svelte
+++ b/frontend/src/routes/post/+page.svelte
@@ -3,7 +3,7 @@
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 PostListPage from '$lib/post/framework/ui/PostListPage.svelte';
+ import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte';
import { setContext } from 'svelte';
const postApiService = new PostApiServiceImpl();
@@ -14,4 +14,4 @@
setContext(PostListBloc.name, postListBloc);
-
+