# 🎉 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 个功能的完整代码已准备完毕!**