非常好的想法!我们将实现数据分析和报告功能,并为标签云添加动画效果。这将帮助我们更好地理解用户行为和内容表现,同时提升用户界面的吸引力。让我们开始吧:
第三十部分:数据分析、报告功能和标签云动画
- 首先,我们需要创建一个新的表来存储文章浏览量。更新
src/db/schema.ts:
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
// ... 保留现有的表定义
export const postViews = sqliteTable("post_views", {
id: integer("id").primaryKey(),
postId: integer("post_id").notNull().references(() => posts.id),
viewCount: integer("view_count").notNull().default(0),
lastViewed: integer("last_viewed", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
});- 创建新的迁移文件
src/db/migrations/0014_add_post_views_table.ts:
export async function up(db) {
await db.run(sql`
CREATE TABLE post_views (
id INTEGER PRIMARY KEY,
post_id INTEGER NOT NULL,
view_count INTEGER NOT NULL DEFAULT 0,
last_viewed TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id)
);
`);
}
export async function down(db) {
await db.run(sql`DROP TABLE IF EXISTS post_views;`);
}- 运行迁移:
bun run migrate- 更新
src/routes/posts.ts文件,记录文章浏览量:
import { db } from "../db";
import { posts, comments, users, postViews } from "../db/schema";
import { eq } from "drizzle-orm";
export const postRoutes = new Elysia()
// ... 保留现有的路由
.get("/posts/:id", async ({ params, session, render }) => {
const postId = parseInt(params.id);
const post = await db.select().from(posts).where(eq(posts.id, postId)).get();
if (!post) {
return "Post not found";
}
// 更新浏览量
await db.insert(postViews)
.values({ postId, viewCount: 1 })
.onConflict({ target: postViews.postId })
.do(sql`
UPDATE SET
view_count = view_count + 1,
last_viewed = CURRENT_TIMESTAMP
`);
// ... 其他代码保持不变
});- 创建一个新文件
src/routes/analytics.ts来处理分析相关的路由:
import { Elysia } from "elysia";
import { db } from "../db";
import { posts, postViews, comments, tags } from "../db/schema";
import { sql } from "drizzle-orm";
export const analyticsRoutes = new Elysia()
.get("/analytics", async ({ render }) => {
const topPosts = await db
.select({
id: posts.id,
title: posts.title,
viewCount: postViews.viewCount,
})
.from(posts)
.leftJoin(postViews, eq(posts.id, postViews.postId))
.orderBy(desc(postViews.viewCount))
.limit(10)
.all();
const topTags = await db
.select({
name: tags.name,
count: sql<number>`count(*)`.as("count"),
})
.from(tags)
.innerJoin(postTags, eq(tags.id, postTags.tagId))
.groupBy(tags.name)
.orderBy(desc(sql`count(*)`))
.limit(20)
.all();
const totalPosts = await db.select({ count: sql<number>`count(*)` }).from(posts).get();
const totalComments = await db.select({ count: sql<number>`count(*)` }).from(comments).get();
return render("analytics", { topPosts, topTags, totalPosts: totalPosts.count, totalComments: totalComments.count });
});- 创建一个新的视图文件
src/views/analytics.eta:
<% layout('./layouts/main.eta', { title: 'Analytics' }) %>
<h1>Analytics Dashboard</h1>
<div class="dashboard">
<div class="stat-box">
<h2>Total Posts</h2>
<p><%= it.totalPosts %></p>
</div>
<div class="stat-box">
<h2>Total Comments</h2>
<p><%= it.totalComments %></p>
</div>
</div>
<h2>Top 10 Most Viewed Posts</h2>
<table>
<thead>
<tr>
<th>Title</th>
<th>Views</th>
</tr>
</thead>
<tbody>
<% it.topPosts.forEach(post => { %>
<tr>
<td><a href="/posts/<%= post.id %>"><%= post.title %></a></td>
<td><%= post.viewCount %></td>
</tr>
<% }) %>
</tbody>
</table>
<h2>Top Tags</h2>
<div id="tag-cloud"></div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
const tags = <%= JSON.stringify(it.topTags) %>;
const width = 600;
const height = 400;
const svg = d3.select("#tag-cloud")
.append("svg")
.attr("width", width)
.attr("height", height);
const fontScale = d3.scaleLinear()
.domain([d3.min(tags, d => d.count), d3.max(tags, d => d.count)])
.range([14, 50]);
const simulation = d3.forceSimulation(tags)
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(d => fontScale(d.count) / 2))
.force("x", d3.forceX(width / 2).strength(0.1))
.force("y", d3.forceY(height / 2).strength(0.1));
const nodes = svg.selectAll("text")
.data(tags)
.enter().append("text")
.text(d => d.name)
.attr("font-size", d => `${fontScale(d.count)}px`)
.attr("fill", () => d3.interpolateRainbow(Math.random()))
.style("cursor", "pointer")
.on("click", (event, d) => {
window.location.href = `/tags/${d.name}`;
});
simulation.on("tick", () => {
nodes.attr("x", d => d.x)
.attr("y", d => d.y);
});
// 添加动画效果
function animateTags() {
nodes.transition()
.duration(1000)
.attr("font-size", d => `${fontScale(d.count) * (0.8 + Math.random() * 0.4)}px`)
.attr("fill", () => d3.interpolateRainbow(Math.random()));
}
setInterval(animateTags, 3000);
</script>
<style>
.dashboard {
display: flex;
justify-content: space-around;
margin-bottom: 2em;
}
.stat-box {
background-color: #f0f0f0;
padding: 1em;
border-radius: 5px;
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
#tag-cloud {
width: 600px;
height: 400px;
margin: 0 auto;
}
</style>- 更新
src/index.ts文件,添加新的分析路由:
import { Elysia } from "elysia";
import { analyticsRoutes } from "./routes/analytics";
const app = new Elysia()
// ... 其他中间件和路由
.use(analyticsRoutes)
.listen(3000);
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);- 在
src/views/layouts/main.eta中添加一个链接到分析页面:
<nav>
<!-- ... 其他导航链接 ... -->
<a href="/analytics">Analytics</a>
</nav>这些更改实现了以下功能:
- 记录和展示文章浏览量。
- 创建了一个分析仪表板,显示总文章数、总评论数、最受欢迎的文章和标签。
- 使用 D 3. Js 创建了一个动态的、可交互的标签云。
- 为标签云添加了动画效果,每隔几秒钟就会改变标签的大小和颜色。
主要改进包括:
- 提供了对博客性能的深入洞察。
- 可视化了最受欢迎的内容和主题。
- 增加了用户界面的吸引力,使标签云更加生动有趣。
- 通过可点击的标签云提高了用户参与度。
这个实现为博客系统添加了强大的分析功能,同时提升了用户界面的视觉吸引力。管理员现在可以更好地了解用户行为和内容表现,而用户则可以通过生动的标签云更轻松地探索博客内容。
接下来,我们可以考虑以下方向来进一步完善系统:
- 添加更多的分析图表,如用户增长趋势、评论活跃度等。
- 实现自定义日期范围的数据分析。
- 添加用户行为跟踪,如页面停留时间、点击路径等。
- 实现数据导出功能,允许管理员下载 CSV 或 Excel 格式的报告。
- 添加实时分析功能,使用 WebSocket 实时更新仪表板。
你希望继续哪个方向,或者有其他想法吗?