# 🎉 6 个新功能 - 完整代码实现包
---
## ✅ 功能 32:用户偏好设置增强(已完成)
### 状态:100% 完成 ✅
**已修改/创建的文件:**
1. `server/prisma/schema.prisma` - ✅ 已更新
2. `server/src/modules/preferences/preferences.service.ts` - ✅ 已更新
3. `server/src/modules/preferences/preferences.controller.ts` - ✅ 已更新
4. `my-uniapp-vue3/src/pages/settings/index.vue` - ✅ 已完全重写
5. `my-uniapp-vue3/src/pages/create/index.vue` - ✅ 已更新
**功能特性:**
- 默认音色选择
- 默认音量滑块
- 自动播放下一首开关
- 仅 WiFi 下载开关
- 完整的 UI 交互
- 数据持久化(本地 + 服务器)
---
## 📦 功能 33-37:完整代码实现
### 功能 33:批量删除功能
#### 后端代码
**文件**: `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 { ids } = ctx.request.body as { ids: string[] };
if (!ids || !Array.isArray(ids)) {
ctx.status = 400;
ctx.body = { code: 400, message: 'ids 参数必须是数组' };
return;
}
const result = await prisma.audioRecord.deleteMany({
where: { audioId: { in: ids } },
});
ctx.body = {
code: 0,
message: 'success',
data: { deletedCount: result.count },
};
});
export default router;
```
**注册路由** - 在 `server/src/app.ts` 添加:
```typescript
import historyBatchRoutes from './modules/history/history-batch.controller';
router.use('/api/history', historyBatchRoutes.routes());
```
#### 前端代码
**修改文件**: `my-uniapp-vue3/src/pages/history/index.vue`
在 template 顶部添加:
```vue
编辑
{{ isAllSelected ? '取消全选' : '全选' }}
删除 ({{ selectedIds.length }})
取消
```
修改 audio-card 部分:
```vue
{{ selectedIds.includes(item._id) ? '✓' : '' }}
```
在 script 中添加:
```typescript
const isEditing = ref(false);
const selectedIds = ref([]);
const isAllSelected = ref(false);
function toggleSelect(id: string) {
const index = selectedIds.value.indexOf(id);
if (index > -1) {
selectedIds.value.splice(index, 1);
} else {
selectedIds.value.push(id);
}
isAllSelected.value = selectedIds.value.length === audioList.value.length;
}
function toggleSelectAll() {
if (isAllSelected.value) {
selectedIds.value = [];
} else {
selectedIds.value = audioList.value.map(item => item._id);
}
isAllSelected.value = !isAllSelected.value;
}
function cancelEdit() {
isEditing.value = false;
selectedIds.value = [];
isAllSelected.value = false;
}
async function handleBatchDelete() {
if (selectedIds.value.length === 0) return;
uni.showModal({
title: '确认删除',
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
success: async (res) => {
if (res.confirm) {
try {
await del('/history/batch', { ids: selectedIds.value });
uni.showToast({ title: '删除成功', icon: 'success' });
await fetchHistoryList();
cancelEdit();
} catch (error) {
uni.showToast({ title: '删除失败', icon: 'none' });
}
}
},
});
}
```
添加样式:
```css
.edit-bar {
position: fixed;
top: 160rpx;
left: 0;
right: 0;
z-index: 98;
background: #ffffff;
padding: 16rpx 24rpx;
display: flex;
justify-content: flex-end;
border-bottom: 1rpx solid #f3f4f6;
}
.edit-btn {
padding: 12rpx 24rpx;
background: #4f46e5;
color: white;
border-radius: 8rpx;
font-size: 28rpx;
}
.edit-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
padding: 12rpx 24rpx;
border-radius: 8rpx;
font-size: 28rpx;
}
.action-btn.delete {
background: #ef4444;
color: white;
}
.action-btn.cancel {
background: #f3f4f6;
color: #374151;
}
.audio-card {
position: relative;
}
.audio-card.selected {
background: #eff6ff;
border: 2rpx solid #4f46e5;
}
.checkbox {
position: absolute;
top: 16rpx;
left: 16rpx;
width: 48rpx;
height: 48rpx;
background: #4f46e5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 28rpx;
z-index: 10;
}
.audio-list {
padding-top: 240rpx !important;
}
```
---
### 功能 34:内容管理(重命名、移动)
**文件**: `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 { 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 };
});
export default router;
```
**注册路由** - 在 `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])
}
```
**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.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])
}
```
**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.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:通知功能
**更新 Controller** - 在 `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. **更新 feature_list.json** - 标记功能状态
2. **数据库** - 添加/更新 Schema,运行 Prisma 命令
3. **后端** - 创建 Controller,注册路由
4. **前端** - 创建/更新页面
5. **测试** - 后端 API 测试 + 前端测试
6. **完成** - 标记为 done
### Prisma 命令
```bash
# 在 server 目录下运行
cd server
# 同步数据库(创建表)
npx prisma db push
# 重新生成 Prisma Client
npx prisma generate
```
---
## ✅ 完成状态
| 功能 | 状态 |
|------|------|
| **32. 用户偏好设置增强** | ✅ 100% 完成 |
| **33. 批量删除功能** | 📦 代码就绪 |
| **34. 内容管理** | 📦 代码就绪 |
| **35. 播放列表功能** | 📦 代码就绪 |
| **36. 草稿自动保存** | 📦 代码就绪 |
| **37. 通知功能** | 📦 代码就绪 |
---
🎉 **所有 6 个功能的完整代码已准备完毕!**