From 8fafd7d8ecbb57ec598efa04c0dd12fed43e7cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AF=92=E5=AF=92?= <2596194220@qq.com> Date: Sun, 11 Jan 2026 15:13:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20=E2=9C=A8=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E9=98=85=E8=AF=BB=E5=A2=99=E5=B1=95=E7=A4=BA=E9=98=85=E8=AF=BB?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db.sqlite | Bin 135168 -> 180224 bytes .../db/entities/ReadingReflectionTaskItem.ts | 6 +- .../components/ContributionWall.vue | 132 ++++++++++++++++++ .../components/ReflectionDrawer.vue | 74 ++++++++++ .../pages/menus/views/userPersona/index.vue | 30 ++-- src/rpc/router/persona.router.ts | 47 ++++++- src/shared/types/IUserReadingPersona.ts | 9 ++ 7 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 src/renderer/src/pages/menus/views/userPersona/components/ContributionWall.vue create mode 100644 src/renderer/src/pages/menus/views/userPersona/components/ReflectionDrawer.vue diff --git a/db.sqlite b/db.sqlite index ce2ac57d798e02a3440062f3d44f3c09ec745498..29a130e3a4df3ff87158ac5475c5d0c94c9b96cc 100644 GIT binary patch delta 1330 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 diff --git a/src/main/db/entities/ReadingReflectionTaskItem.ts b/src/main/db/entities/ReadingReflectionTaskItem.ts index 8ae30af..ed24ed5 100644 --- a/src/main/db/entities/ReadingReflectionTaskItem.ts +++ b/src/main/db/entities/ReadingReflectionTaskItem.ts @@ -1,4 +1,4 @@ -import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm' +import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryColumn } from 'typeorm' import { ReadingReflectionTaskBatch } from './ReadingReflectionTaskBatch' import { IReadingReflectionTaskItem } from '@shared/types/IReadingReflectionTask' @@ -24,6 +24,10 @@ export class ReadingReflectionTaskItem implements IReadingReflectionTaskItem { @Column({ type: 'simple-json', nullable: true }) keywords?: string[] + + @CreateDateColumn() // 增加这一行,TypeORM 会自动处理时间 + createdAt: Date + // 多对一关联 @ManyToOne(() => ReadingReflectionTaskBatch, (batch) => batch.items) batch!: ReadingReflectionTaskBatch diff --git a/src/renderer/src/pages/menus/views/userPersona/components/ContributionWall.vue b/src/renderer/src/pages/menus/views/userPersona/components/ContributionWall.vue new file mode 100644 index 0000000..2288b4c --- /dev/null +++ b/src/renderer/src/pages/menus/views/userPersona/components/ContributionWall.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/renderer/src/pages/menus/views/userPersona/components/ReflectionDrawer.vue b/src/renderer/src/pages/menus/views/userPersona/components/ReflectionDrawer.vue new file mode 100644 index 0000000..ffd8b5f --- /dev/null +++ b/src/renderer/src/pages/menus/views/userPersona/components/ReflectionDrawer.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/src/renderer/src/pages/menus/views/userPersona/index.vue b/src/renderer/src/pages/menus/views/userPersona/index.vue index fe5ab0e..bd41021 100644 --- a/src/renderer/src/pages/menus/views/userPersona/index.vue +++ b/src/renderer/src/pages/menus/views/userPersona/index.vue @@ -6,10 +6,12 @@ import BackPage from '@renderer/components/BackPage.vue' import { trpc } from '@renderer/lib/trpc' import { IUserReadingPersona } from '@shared/types/IUserReadingPersona' import useLoading from '@renderer/hooks/useLoading' +import ContributionWall from './components/ContributionWall.vue' const chartRef = ref(null) const personaData = ref(null) const { loading, setLoading } = useLoading() +const contributionData = ref<{ date: string; count: number }[]>([]) let myChart: echarts.ECharts | null = null // 核心方法:初始化或更新图表 @@ -66,17 +68,25 @@ const renderChart = (data: IUserReadingPersona) => { const fetchData = async (force = false) => { setLoading(true) try { - if (force) { - await trpc.persona.forceRefreshUserPersona.mutate() - } - const res = await trpc.persona.getUserPersona.query() - if (res) { - personaData.value = res + if (force) await trpc.persona.forceRefreshUserPersona.mutate() + + // 并行请求:提高响应速度 + const [persona, contributions] = await Promise.all([ + trpc.persona.getUserPersona.query(), + trpc.persona.getContributionData.query() + ]) + + if (persona) { + personaData.value = persona await nextTick() - renderChart(res) + renderChart(persona) + } + + if (contributions) { + contributionData.value = contributions } } catch (error) { - console.error('获取画像失败:', error) + console.error('数据同步失败:', error) } finally { setLoading(false) } @@ -125,11 +135,13 @@ onMounted(() => { +

阅读用户画像

+
diff --git a/src/rpc/router/persona.router.ts b/src/rpc/router/persona.router.ts index a4c5a9c..8baf7c5 100644 --- a/src/rpc/router/persona.router.ts +++ b/src/rpc/router/persona.router.ts @@ -3,9 +3,12 @@ import { router, t } from '@rpc/index' import { entityToUserReadingPersona, PersonaService } from '@main/services/persona.service' import { ReadingReflectionTaskBatch } from '@main/db/entities/ReadingReflectionTaskBatch' import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem' +import { z } from 'zod' export const personaRouter = router({ - // 获取用户画像:直接从数据库读取缓存结果,极快 + /** + * 获取用户画像:直接从数据库读取缓存结果,极快 + * */ getUserPersona: t.procedure.query(async ({ ctx }) => { const entity = await ctx.db.getRepository(ReadingPersona).findOneBy({ id: 'current_user_persona' @@ -17,7 +20,9 @@ export const personaRouter = router({ return entityToUserReadingPersona(entity) }), - // 手动强制重新刷新画像 + /** + * 刷新用户画像 + * */ forceRefreshUserPersona: t.procedure.mutation(async ({ ctx }) => { const items = await ctx.db .getRepository(ReadingReflectionTaskItem) @@ -26,5 +31,41 @@ export const personaRouter = router({ const personaService = new PersonaService(ctx.db.getRepository(ReadingPersona)) return await personaService.refreshPersona(items, batches) - }) + }), + + /** + * 聚合统计:从数据库中聚合数据并计算画像分值 + * */ + getContributionData: t.procedure.query(async ({ ctx }) => { + const itemRepo = ctx.db.getRepository(ReadingReflectionTaskItem) + + // 关键:关联 batch 获取时间,并强制格式化为 YYYY-MM-DD + const rawData = await itemRepo + .createQueryBuilder('item') + .leftJoin('reading_reflection_task_batches', 'batch', 'item.batchId = batch.id') + .select("strftime('%Y-%m-%d', batch.createdAt)", 'date') + .addSelect('COUNT(item.id)', 'count') + .where("item.status = 'COMPLETED'") + .groupBy('date') + .getRawMany() + + return rawData as { date: string; count: number }[] + }), + /** + * 获取指定日期的心得 + * */ + getReflectionsByDate: t.procedure + .input(z.object({ date: z.string() })) + .query(async ({ ctx, input }) => { + const repo = ctx.db.getRepository(ReadingReflectionTaskItem) + + // 关键修正:必须在 batch 表上进行日期过滤 + return await repo + .createQueryBuilder('item') + // 这里的 'batchId' 必须是你数据库中 item 表关联 batch 的实际外键列名 + .leftJoinAndSelect('reading_reflection_task_batches', 'batch', 'item.batchId = batch.id') + .where('date(batch.createdAt) = :date', { date: input.date }) + .andWhere("item.status = 'COMPLETED'") + .getMany() + }) }) diff --git a/src/shared/types/IUserReadingPersona.ts b/src/shared/types/IUserReadingPersona.ts index 84cb67e..6a4a4d9 100644 --- a/src/shared/types/IUserReadingPersona.ts +++ b/src/shared/types/IUserReadingPersona.ts @@ -34,3 +34,12 @@ export interface IUserReadingPersona { mostUsedOccupation: string // 最常使用的阅读者身份 } } + +/** + * 每日阅读贡献统计模型 + * */ +export interface IContributionDay { + date: string + count: number + level: number +}