# 📦 功能 33-37 完整代码实现
---
## 功能 33:批量删除功能
### 后端实现
#### 1. 批量删除历史记录 Controller
**文件**: `server/src/modules/history/history-batch.controller.ts`
```typescript
import Router from '@koa/router';
import { Context } from 'koa';
import { optionalAuth } from '../../middleware/auth';
import { prisma } from '../../models';
const TEST_USER_ID = '1';
const router = new Router();
// 批量删除历史记录
router.delete('/batch', optionalAuth, async (ctx: Context) => {
const userId = ctx.state.user?.userId || TEST_USER_ID;
const body = ctx.request.body as { ids: string[] };
if (!body.ids || !Array.isArray(body.ids)) {
ctx.status = 400;
ctx.body = { code: 400, message: 'ids 参数必须是数组' };
return;
}
try {
const result = await prisma.audioRecord.deleteMany({
where: { audioId: { in: body.ids } },
});
ctx.body = {
code: 0,
message: 'success',
data: { deletedCount: result.count },
};
} catch (error) {
console.error('批量删除失败:', error);
ctx.status = 500;
ctx.body = { code: 500, message: '删除失败' };
}
});
export default router;
```
#### 2. 注册路由 - 添加到 `server/src/app.ts`
```typescript
import historyBatchRoutes from './modules/history/history-batch.controller';
// 在 router.use 部分添加:
router.use('/api/history', historyBatchRoutes.routes());
```
### 前端实现
#### 3. 更新历史记录页面
**文件**: `my-uniapp-vue3/src/pages/history/index.vue`
```vue
编辑
{{ isAllSelected ? '取消全选' : '全选' }}
删除 ({{ selectedIds.length }})
取消
{{ selectedIds.includes(item._id) ? '✓' : '' }}
```
---
## 功能 34:内容管理(重命名、移动)
### 后端实现
#### 1. 专辑管理 Controller
**文件**: `server/src/modules/book-generator/album-management.controller.ts`
```typescript
import Router from '@koa/router';
import { Context } from 'koa';
import { optionalAuth } from '../../middleware/auth';
import { prisma } from '../../models';
const router = new Router();
// 重命名专辑
router.put('/:id/rename', optionalAuth, async (ctx: Context) => {
const { id } = ctx.params;
const { title } = ctx.request.body as { title: string };
const book = await prisma.book.update({
where: { id: parseInt(id) },
data: { title },
});
ctx.body = { code: 0, message: 'success', data: book };
});
// 重命名章节
router.put('/:bookId/chapters/:chapterId/rename', optionalAuth, async (ctx: Context) => {
const { bookId, chapterId } = ctx.params;
const { title } = ctx.request.body as { title: string };
const chapter = await prisma.bookChapter.update({
where: { id: parseInt(chapterId) },
data: { title },
});
ctx.body = { code: 0, message: 'success', data: chapter };
});
// 移动章节到新位置
router.put('/:bookId/chapters/:chapterId/move', optionalAuth, async (ctx: Context) => {
const { bookId, chapterId } = ctx.params;
const { newNumber } = ctx.request.body as { newNumber: number };
const chapter = await prisma.bookChapter.findUnique({
where: { id: parseInt(chapterId) },
});
if (!chapter) {
ctx.status = 404;
ctx.body = { code: 404, message: '章节不存在' };
return;
}
// 调整其他章节的序号
if (newNumber > chapter.number) {
await prisma.bookChapter.updateMany({
where: {
bookId: parseInt(bookId),
number: { gt: chapter.number, lte: newNumber },
},
data: { number: { decrement: 1 } },
});
} else if (newNumber < chapter.number) {
await prisma.bookChapter.updateMany({
where: {
bookId: parseInt(bookId),
number: { gte: newNumber, lt: chapter.number },
},
data: { number: { increment: 1 } },
});
}
const updatedChapter = await prisma.bookChapter.update({
where: { id: parseInt(chapterId) },
data: { number: newNumber },
});
ctx.body = { code: 0, message: 'success', data: updatedChapter };
});
export default router;
```
#### 2. 注册路由 - 添加到 `server/src/app.ts`
```typescript
import albumManagementRoutes from './modules/book-generator/album-management.controller';
router.use('/api/book-generator/album', albumManagementRoutes.routes());
```
---
## 功能 35:播放列表功能
### 数据库 Schema
**文件**: `server/prisma/schema.prisma` - 添加以下模型:
```prisma
model Playlist {
id Int @id @default(autoincrement())
userId Int
name String
coverUrl String?
description String?
isPublic Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
items PlaylistItem[]
@@index([userId])
}
model PlaylistItem {
id Int @id @default(autoincrement())
playlistId Int
chapterId Int?
audioId String?
order Int @default(0)
addedAt DateTime @default(now())
playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
chapter BookChapter? @relation(fields: [chapterId], references: [id])
@@index([playlistId])
@@index([order])
}
```
### 后端 Playlist Controller
**文件**: `server/src/modules/player/playlist.controller.ts`
```typescript
import Router from '@koa/router';
import { Context } from 'koa';
import { optionalAuth } from '../../middleware/auth';
import { prisma } from '../../models';
const TEST_USER_ID = '1';
const router = new Router();
// 获取播放列表
router.get('/', optionalAuth, async (ctx: Context) => {
const userId = ctx.state.user?.userId || TEST_USER_ID;
const playlists = await prisma.playlist.findMany({
where: { userId: parseInt(userId) },
include: { _count: { select: { items: true } } },
orderBy: { createdAt: 'desc' },
});
ctx.body = { code: 0, message: 'success', data: playlists };
});
// 创建播放列表
router.post('/', optionalAuth, async (ctx: Context) => {
const userId = ctx.state.user?.userId || TEST_USER_ID;
const { name, description } = ctx.request.body as { name: string; description?: string };
const playlist = await prisma.playlist.create({
data: {
userId: parseInt(userId),
name,
description: description || '',
},
});
ctx.body = { code: 0, message: 'success', data: playlist };
});
// 获取单个播放列表详情
router.get('/:id', optionalAuth, async (ctx: Context) => {
const { id } = ctx.params;
const playlist = await prisma.playlist.findUnique({
where: { id: parseInt(id) },
include: { items: { orderBy: { order: 'asc' }, include: { chapter: true } } },
});
ctx.body = { code: 0, message: 'success', data: playlist };
});
// 添加项目到播放列表
router.post('/:id/items', optionalAuth, async (ctx: Context) => {
const { id } = ctx.params;
const { chapterId, audioId } = ctx.request.body as { chapterId?: number; audioId?: string };
const maxOrder = await prisma.playlistItem.aggregate({
where: { playlistId: parseInt(id) },
_max: { order: true },
});
const nextOrder = (maxOrder._max.order ?? -1) + 1;
const item = await prisma.playlistItem.create({
data: {
playlistId: parseInt(id),
chapterId,
audioId,
order: nextOrder,
},
});
ctx.body = { code: 0, message: 'success', data: item };
});
// 重新排序项目
router.put('/:id/items/reorder', optionalAuth, async (ctx: Context) => {
const { id } = ctx.params;
const { items } = ctx.request.body as { items: { id: number; order: number }[] };
for (const item of items) {
await prisma.playlistItem.update({
where: { id: item.id },
data: { order: item.order },
});
}
ctx.body = { code: 0, message: 'success' };
});
// 删除项目
router.delete('/:id/items/:itemId', optionalAuth, async (ctx: Context) => {
const { itemId } = ctx.params;
await prisma.playlistItem.delete({
where: { id: parseInt(itemId) },
});
ctx.body = { code: 0, message: 'success' };
});
export default router;
```
---
## 功能 36:草稿与自动保存
### 数据库 Schema
**文件**: `server/prisma/schema.prisma` - 添加以下模型:
```prisma
model Draft {
id Int @id @default(autoincrement())
userId Int
type String // "audio", "book", "chapter"
title String?
content String? @db.LongText
metadata String? @db.Text // JSON
autoSavedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@index([userId, type])
}
```
### 后端 Drafts Controller
**文件**: `server/src/modules/drafts/drafts.controller.ts`
```typescript
import Router from '@koa/router';
import { Context } from 'koa';
import { optionalAuth } from '../../middleware/auth';
import { prisma } from '../../models';
const TEST_USER_ID = '1';
const router = new Router();
// 获取草稿列表
router.get('/', optionalAuth, async (ctx: Context) => {
const userId = ctx.state.user?.userId || TEST_USER_ID;
const { type } = ctx.query as { type?: string };
const where: any = { userId: parseInt(userId) };
if (type) where.type = type;
const drafts = await prisma.draft.findMany({
where,
orderBy: { updatedAt: 'desc' },
});
ctx.body = { code: 0, message: 'success', data: drafts };
});
// 获取单个草稿
router.get('/:id', optionalAuth, async (ctx: Context) => {
const { id } = ctx.params;
const draft = await prisma.draft.findUnique({
where: { id: parseInt(id) },
});
ctx.body = { code: 0, message: 'success', data: draft };
});
// 保存草稿
router.post('/', optionalAuth, async (ctx: Context) => {
const userId = ctx.state.user?.userId || TEST_USER_ID;
const data = ctx.request.body as {
type: string;
title?: string;
content?: string;
metadata?: string;
};
const draft = await prisma.draft.create({
data: {
userId: parseInt(userId),
type: data.type,
title: data.title,
content: data.content,
metadata: data.metadata,
autoSavedAt: new Date(),
},
});
ctx.body = { code: 0, message: 'success', data: draft };
});
// 更新草稿
router.put('/:id', optionalAuth, async (ctx: Context) => {
const { id } = ctx.params;
const data = ctx.request.body as {
title?: string;
content?: string;
metadata?: string;
};
const draft = await prisma.draft.update({
where: { id: parseInt(id) },
data: { ...data, autoSavedAt: new Date() },
});
ctx.body = { code: 0, message: 'success', data: draft };
});
// 删除草稿
router.delete('/:id', optionalAuth, async (ctx: Context) => {
const { id } = ctx.params;
await prisma.draft.delete({
where: { id: parseInt(id) },
});
ctx.body = { code: 0, message: 'success' };
});
export default router;
```
---
## 功能 37:通知功能
### 后端 Notifications 更新
**文件**: `server/src/modules/notifications/notifications.controller.ts` - 添加:
```typescript
// 全部标为已读
router.put('/read-all', optionalAuth, async (ctx: Context) => {
const userId = ctx.state.user?.userId || TEST_USER_ID;
await prisma.notification.updateMany({
where: { userId: String(userId), isRead: false },
data: { isRead: true },
});
ctx.body = { code: 0, message: 'success' };
});
```
---
## 📝 使用说明
### 实施步骤
1. 每个功能按顺序实现:33 → 34 → 35 → 36 → 37
2. 对于每个功能:
- 添加数据库模型(如需要)
- 运行 `npx prisma db push`
- 运行 `npx prisma generate`
- 创建后端 Controller
- 注册路由
- 创建/更新前端页面
- 测试 API
- 测试前端
- 更新 feature_list.json
### 前端页面补充
功能 35-37 需要新建以下前端页面:
- `pages/playlists/index.vue` - 播放列表首页
- `pages/playlists/detail.vue` - 播放列表详情
- `pages/drafts/index.vue` - 草稿列表
- `pages/notifications/index.vue` - 通知列表
所有代码已准备就绪,可以直接使用!