diff --git a/apps/api/src/user-course-progress/tests/user-course-progress.e2e-spec.ts b/apps/api/src/user-course-progress/tests/user-course-progress.e2e-spec.ts index fb831eddb..e90b6a727 100644 --- a/apps/api/src/user-course-progress/tests/user-course-progress.e2e-spec.ts +++ b/apps/api/src/user-course-progress/tests/user-course-progress.e2e-spec.ts @@ -43,12 +43,14 @@ describe("user-progress e2e", () => { const courseIdFirst = createId(); const coursePackIdSecond = createId(); const courseIdSecond = createId(); - await insertUserCourseProgress(db, coursePackIdFirst, courseIdFirst, 0); - await insertUserCourseProgress(db, coursePackIdSecond, courseIdSecond, 10); + const userId = createId(); + await insertUserCourseProgress(db, coursePackIdFirst, courseIdFirst, 0, userId); + await insertUserCourseProgress(db, coursePackIdSecond, courseIdSecond, 10, userId); await request(app.getHttpServer()) .get("/user-course-progress/recent-course-packs") .set("Authorization", `Bearer ${token}`) + .query({ userId }) .expect(200) .expect(({ body }) => { expect(body.length).toBe(2); diff --git a/apps/api/src/user-course-progress/user-course-progress.controller.ts b/apps/api/src/user-course-progress/user-course-progress.controller.ts index 743ec4a6d..f08688290 100644 --- a/apps/api/src/user-course-progress/user-course-progress.controller.ts +++ b/apps/api/src/user-course-progress/user-course-progress.controller.ts @@ -12,11 +12,10 @@ import { UserCourseProgressService } from "./user-course-progress.service"; export class UserProgressController { constructor(private readonly userCourseProgressService: UserCourseProgressService) {} - @UseGuards(AuthGuard) @Get("/recent-course-packs") - async getUserRecentCoursePacks(@User() user: UserEntity, @Query("limit") limit: number) { + async getUserRecentCoursePacks(@Query("userId") userId?: string, @Query("limit") limit?: number) { const recentCoursePacks = await this.userCourseProgressService.getUserRecentCoursePacks( - user.userId, + userId, limit || 3, ); return recentCoursePacks; diff --git a/apps/api/src/user-learn-record/user-learn-record.controller.ts b/apps/api/src/user-learn-record/user-learn-record.controller.ts index a809757e3..06e47819d 100644 --- a/apps/api/src/user-learn-record/user-learn-record.controller.ts +++ b/apps/api/src/user-learn-record/user-learn-record.controller.ts @@ -9,9 +9,8 @@ import { UserLearnRecordService } from "./user-learn-record.service"; export class UserLearnRecordController { constructor(private userLearnRecordService: UserLearnRecordService) {} - @UseGuards(AuthGuard) @Get("finishCount") - finishCount(@User() user: UserEntity, @Query() dto?: GetUserLearnRecordDto) { - return this.userLearnRecordService.find(user.userId, dto); + finishCount(@Query("userId") userId: string, @Query() dto?: GetUserLearnRecordDto) { + return this.userLearnRecordService.find(userId, dto); } } diff --git a/apps/api/src/user/user.controller.ts b/apps/api/src/user/user.controller.ts index a8d3820cd..36772c7b4 100644 --- a/apps/api/src/user/user.controller.ts +++ b/apps/api/src/user/user.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common"; +import { Body, Controller, Get, Param, Patch, Post, UseGuards } from "@nestjs/common"; import { AuthGuard } from "../guards/auth.guard"; import { User, UserEntity } from "../user/user.decorators"; @@ -21,6 +21,11 @@ export class UserController { return userInfo; } + @Get(":username") + getUserByUsername(@Param("username") username: string) { + return this.userService.getUserByUsername(username); + } + // 给新用户第一次登录使用 // 目前使用 email 和 github 登录的用户 都不存在 username // 所以这个接口有两个目的 diff --git a/apps/api/src/user/user.service.ts b/apps/api/src/user/user.service.ts index 037b359be..2a12eccb7 100644 --- a/apps/api/src/user/user.service.ts +++ b/apps/api/src/user/user.service.ts @@ -69,6 +69,21 @@ export class UserService { throw new HttpException(e.response.data.message, e.response.status); } } + async getUserByUsername(username: string) { + const params = new URLSearchParams([["search.username", username]]).toString(); + + try { + const res = await this.logtoService.logtoApi.get(`/api/users/?${params}`); + if (res.status === 200) { + const user = res.data.at(0); + return user; + } else { + return res.data; + } + } catch (e) { + throw new HttpException(e.response.data.message, e.response.status); + } + } async setupNewUser(user: UserEntity, dto: { username: string; avatar: string }) { if (!dto.avatar) { diff --git a/apps/api/test/fixture/db.ts b/apps/api/test/fixture/db.ts index 212b83b34..b5331965f 100644 --- a/apps/api/test/fixture/db.ts +++ b/apps/api/test/fixture/db.ts @@ -80,11 +80,12 @@ export async function insertUserCourseProgress( coursePackId: string, courseId: string, statementIndex: number, + userId?: string, ) { const [entity] = await db .insert(userCourseProgress) .values({ - userId: getTokenOwner(), + userId: userId || getTokenOwner(), coursePackId, courseId, statementIndex, diff --git a/apps/client/api/user.ts b/apps/client/api/user.ts index cb3d0f9d6..ece9d2932 100644 --- a/apps/client/api/user.ts +++ b/apps/client/api/user.ts @@ -18,3 +18,7 @@ export async function fetchCurrentUser() { avatar: logtoUserInfo!.picture || "", // 添加 avatar 字段,默认值为 picture ( picture 这个属性不够清晰 不喜欢) } as User; } +export async function getUserByUsername(username: string) { + // `/user/username/${username}` is better ? + return await http.get(`/user/${username}`); +} diff --git a/apps/client/api/userCourseProgress.ts b/apps/client/api/userCourseProgress.ts index ec7385ac7..0ce5f1569 100644 --- a/apps/client/api/userCourseProgress.ts +++ b/apps/client/api/userCourseProgress.ts @@ -26,8 +26,14 @@ export async function fetchUpdateCourseProgress(userProgressUpdate: UserProgress ); } -export async function fetchUserRecentCoursePacks() { - return await http.get( +export async function fetchUserRecentCoursePacks(userId: string, limit = 4) { + return await http.get( `/user-course-progress/recent-course-packs`, + { + params: { + userId, + limit, + }, + }, ); } diff --git a/apps/client/api/userLearnRecord.ts b/apps/client/api/userLearnRecord.ts index 6aba035ee..36c3d5c52 100644 --- a/apps/client/api/userLearnRecord.ts +++ b/apps/client/api/userLearnRecord.ts @@ -10,11 +10,8 @@ export interface UserLearnRecordResponse { list: Array<{ day: string; count: number }>; } -export async function fetchLearnRecord(params: UserLearnRecord) { - return await http.get( - `/user-learn-record/finishCount`, - { - params, - }, - ); +export async function fetchLearnRecord(params: UserLearnRecord & { userId: string }) { + return await http.get(`/user-learn-record/finishCount`, { + params, + }); } diff --git a/apps/client/components/rank/RankingItem.vue b/apps/client/components/rank/RankingItem.vue index 1749a46cb..7a0c518b6 100644 --- a/apps/client/components/rank/RankingItem.vue +++ b/apps/client/components/rank/RankingItem.vue @@ -6,15 +6,29 @@ class="w-16" :rank="rank" /> -
{{ username || "匿名" }}
+
+ {{ username || "匿名" }} +
{{ count }} 课
diff --git a/apps/client/components/Home/CalendarGraph.vue b/apps/client/components/user-home/CalendarGraph.vue similarity index 100% rename from apps/client/components/Home/CalendarGraph.vue rename to apps/client/components/user-home/CalendarGraph.vue diff --git a/apps/client/components/Home/RecentCoursePack.vue b/apps/client/components/user-home/RecentCoursePack.vue similarity index 87% rename from apps/client/components/Home/RecentCoursePack.vue rename to apps/client/components/user-home/RecentCoursePack.vue index 9f6b7c1b7..6544e9963 100644 --- a/apps/client/components/Home/RecentCoursePack.vue +++ b/apps/client/components/user-home/RecentCoursePack.vue @@ -29,7 +29,10 @@

{{ coursePack.description }}

-
+
-
-
{{ userStore.user?.username }}
- -
+
{{ user?.username }}
- {{ userStore.user?.name }} + {{ user?.name }}

@@ -36,47 +33,46 @@
最近使用的课程包
更多课程包 + > + 更多课程包
- - +
- diff --git a/apps/client/composables/learnRecord.ts b/apps/client/composables/learnRecord.ts index c4491abec..89d92204a 100644 --- a/apps/client/composables/learnRecord.ts +++ b/apps/client/composables/learnRecord.ts @@ -1,27 +1,38 @@ -import { ref } from "vue"; +import type { MaybeRef } from "vue"; + +import { refDebounced } from "@vueuse/core"; +import { ref, toValue, watch } from "vue"; import type { UserLearnRecordResponse } from "~/api/userLearnRecord"; import { fetchLearnRecord } from "~/api/userLearnRecord"; +import { useUserStore } from "~/store/user"; -let year: number | undefined = 0; -const learnRecord = ref({ - list: [], - totalCount: 0, -}); -let isSetup = false; - -export function useLearnRecord() { - function setQueryYear(val?: number) { - if (year !== val) { - year = val; - isSetup = false; - } - } +interface UseLearnRecordOptions { + year?: MaybeRef; + /** + * @default string current user + */ + userId?: string; +} + +export function useLearnRecord(options: UseLearnRecordOptions) { + const userStore = useUserStore(); + const { userId = userStore.userInfo?.sub! } = options || {}; + + const learnRecord = ref({ + list: [], + totalCount: 0, + }); + + const year = ref(options?.year || new Date().getFullYear()); + const debouncedYear = refDebounced(year, 1500); function getQuery() { + const yearStr = toValue(year); return { - startDate: year ? `${year}-01-01` : undefined, - endDate: year ? `${year}-12-31` : undefined, + userId, + startDate: yearStr ? `${yearStr}-01-01` : undefined, + endDate: yearStr ? `${yearStr}-12-31` : undefined, }; } @@ -29,18 +40,19 @@ export function useLearnRecord() { const res = await fetchLearnRecord(getQuery()); learnRecord.value = res; } - - async function setupLearnRecord() { - if (isSetup) return; - isSetup = true; - const res = await fetchLearnRecord(getQuery()); - learnRecord.value = res; - } + watch( + debouncedYear, + () => { + updateLearnRecord(); + }, + { + immediate: true, + }, + ); return { + year, learnRecord, updateLearnRecord, - setQueryYear, - setupLearnRecord, }; } diff --git a/apps/client/pages/[username].vue b/apps/client/pages/[username].vue new file mode 100644 index 000000000..add654347 --- /dev/null +++ b/apps/client/pages/[username].vue @@ -0,0 +1,12 @@ + + + + + diff --git a/apps/client/pages/index.vue b/apps/client/pages/index.vue index 447f54daf..2f5ebf9c0 100644 --- a/apps/client/pages/index.vue +++ b/apps/client/pages/index.vue @@ -1,8 +1,14 @@ diff --git a/apps/client/services/auth.ts b/apps/client/services/auth.ts index 5edc9af3b..04b2548ee 100644 --- a/apps/client/services/auth.ts +++ b/apps/client/services/auth.ts @@ -1,6 +1,8 @@ import { useLogto } from "@logto/vue"; import { useRuntimeConfig } from "nuxt/app"; +import { useUserStore } from "~/store/user"; + let logto: ReturnType; let runtimeConfig: ReturnType; export async function setupAuth() { @@ -21,6 +23,11 @@ export function isAuthenticated() { return logto.isAuthenticated.value; } +export function isUserLoaded() { + const userStore = useUserStore(); + return Boolean(userStore.userInfo); +} + export async function getToken() { const accessToken = await logto.getAccessToken(runtimeConfig.public.backendEndpoint); diff --git a/apps/client/store/user.ts b/apps/client/store/user.ts index b0f45bcf4..160c7a8ab 100644 --- a/apps/client/store/user.ts +++ b/apps/client/store/user.ts @@ -1,5 +1,7 @@ +import type { MaybeRefOrGetter } from "vue"; + import { defineStore } from "pinia"; -import { ref } from "vue"; +import { computed, ref, toValue } from "vue"; import type { User } from "~/types"; import { fetchSetupNewUser } from "~/api/user"; @@ -12,6 +14,12 @@ export const useUserStore = defineStore("user", () => { user.value = val; } + function isSelf(userId: MaybeRefOrGetter) { + return computed(() => { + return user.value?.sub === toValue(userId); + }); + } + function isNewUser() { return !user.value?.username || !user.value?.avatar; } @@ -38,5 +46,6 @@ export const useUserStore = defineStore("user", () => { initUser, setupNewUser, isFounderMembership, + isSelf, }; });