diff --git a/db.sqlite b/db.sqlite index ce2ac57..29a130e 100644 Binary files a/db.sqlite and b/db.sqlite differ 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 +}