|
@@ -6,7 +6,9 @@
|
|
|
<text class="nav-icon">←</text>
|
|
<text class="nav-icon">←</text>
|
|
|
</view>
|
|
</view>
|
|
|
<text class="nav-title">书籍详情</text>
|
|
<text class="nav-title">书籍详情</text>
|
|
|
- <view class="nav-btn" />
|
|
|
|
|
|
|
+ <view class="nav-btn" @click="showRenameBookModal = true" v-if="book">
|
|
|
|
|
+ <text class="nav-icon">✏️</text>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 骨架屏加载状态 -->
|
|
<!-- 骨架屏加载状态 -->
|
|
@@ -53,6 +55,7 @@
|
|
|
:key="item.id"
|
|
:key="item.id"
|
|
|
:class="['audio-item', { 'has-audio': item.audioUrl, 'has-video': item.videoUrl }]"
|
|
:class="['audio-item', { 'has-audio': item.audioUrl, 'has-video': item.videoUrl }]"
|
|
|
@click="playChapter(item, index)"
|
|
@click="playChapter(item, index)"
|
|
|
|
|
+ @longpress="showChapterMenu(item)"
|
|
|
>
|
|
>
|
|
|
<view class="audio-index">{{ index + 1 }}</view>
|
|
<view class="audio-index">{{ index + 1 }}</view>
|
|
|
<view class="audio-content">
|
|
<view class="audio-content">
|
|
@@ -97,6 +100,114 @@
|
|
|
></video>
|
|
></video>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 章节操作菜单 -->
|
|
|
|
|
+ <view v-if="showChapterMenuModal" class="modal">
|
|
|
|
|
+ <view class="modal-mask" @click="showChapterMenuModal = false"></view>
|
|
|
|
|
+ <view class="modal-content menu-content">
|
|
|
|
|
+ <view class="modal-header">
|
|
|
|
|
+ <text class="modal-title">章节操作</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="menu-list">
|
|
|
|
|
+ <view class="menu-item" @click="showRenameChapterModal = true; showChapterMenuModal = false">
|
|
|
|
|
+ <text class="menu-icon">✏️</text>
|
|
|
|
|
+ <text class="menu-text">重命名</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="menu-item" @click="showMoveChapterModal = true; showChapterMenuModal = false">
|
|
|
|
|
+ <text class="menu-icon">📦</text>
|
|
|
|
|
+ <text class="menu-text">移动到专辑</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-footer">
|
|
|
|
|
+ <view class="modal-btn cancel" @click="showChapterMenuModal = false">
|
|
|
|
|
+ <text>取消</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 重命名专辑弹窗 -->
|
|
|
|
|
+ <view v-if="showRenameBookModal" class="modal">
|
|
|
|
|
+ <view class="modal-mask" @click="showRenameBookModal = false"></view>
|
|
|
|
|
+ <view class="modal-content">
|
|
|
|
|
+ <view class="modal-header">
|
|
|
|
|
+ <text class="modal-title">重命名专辑</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-body">
|
|
|
|
|
+ <input
|
|
|
|
|
+ v-model="newBookTitle"
|
|
|
|
|
+ class="input"
|
|
|
|
|
+ placeholder="请输入新专辑名称"
|
|
|
|
|
+ maxlength="50"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-footer">
|
|
|
|
|
+ <view class="modal-btn cancel" @click="showRenameBookModal = false">
|
|
|
|
|
+ <text>取消</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-btn confirm" @click="renameBook" :disabled="!newBookTitle.trim()">
|
|
|
|
|
+ <text>确定</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 重命名章节弹窗 -->
|
|
|
|
|
+ <view v-if="showRenameChapterModal" class="modal">
|
|
|
|
|
+ <view class="modal-mask" @click="showRenameChapterModal = false"></view>
|
|
|
|
|
+ <view class="modal-content">
|
|
|
|
|
+ <view class="modal-header">
|
|
|
|
|
+ <text class="modal-title">重命名章节</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-body">
|
|
|
|
|
+ <input
|
|
|
|
|
+ v-model="newChapterTitle"
|
|
|
|
|
+ class="input"
|
|
|
|
|
+ placeholder="请输入新章节名称"
|
|
|
|
|
+ maxlength="50"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-footer">
|
|
|
|
|
+ <view class="modal-btn cancel" @click="showRenameChapterModal = false">
|
|
|
|
|
+ <text>取消</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-btn confirm" @click="renameChapter" :disabled="!newChapterTitle.trim()">
|
|
|
|
|
+ <text>确定</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 移动章节弹窗 -->
|
|
|
|
|
+ <view v-if="showMoveChapterModal" class="modal">
|
|
|
|
|
+ <view class="modal-mask" @click="showMoveChapterModal = false"></view>
|
|
|
|
|
+ <view class="modal-content">
|
|
|
|
|
+ <view class="modal-header">
|
|
|
|
|
+ <text class="modal-title">移动到专辑</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-body">
|
|
|
|
|
+ <view class="album-select-list">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="album in albumList"
|
|
|
|
|
+ :key="album.id"
|
|
|
|
|
+ class="album-select-item"
|
|
|
|
|
+ :class="{ selected: selectedAlbumId === album.id }"
|
|
|
|
|
+ @click="selectedAlbumId = album.id"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text>{{ album.title }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-footer">
|
|
|
|
|
+ <view class="modal-btn cancel" @click="showMoveChapterModal = false">
|
|
|
|
|
+ <text>取消</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-btn confirm" @click="moveChapter" :disabled="!selectedAlbumId">
|
|
|
|
|
+ <text>确定</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -106,6 +217,7 @@ import { onLoad } from '@dcloudio/uni-app';
|
|
|
import { useAudioStore } from '../../store/audio';
|
|
import { useAudioStore } from '../../store/audio';
|
|
|
import * as bookApi from '../../utils/book-generator-api';
|
|
import * as bookApi from '../../utils/book-generator-api';
|
|
|
import type { Book, Chapter } from '../../utils/book-generator-api';
|
|
import type { Book, Chapter } from '../../utils/book-generator-api';
|
|
|
|
|
+import { get, put } from '../../utils/request';
|
|
|
|
|
|
|
|
const audioStore = useAudioStore();
|
|
const audioStore = useAudioStore();
|
|
|
|
|
|
|
@@ -115,7 +227,21 @@ const chapters = ref<Chapter[]>([]);
|
|
|
const loading = ref(false);
|
|
const loading = ref(false);
|
|
|
const isFavorited = ref(false);
|
|
const isFavorited = ref(false);
|
|
|
|
|
|
|
|
-const userId = 1; // TODO: 从用户 store 获取
|
|
|
|
|
|
|
+// 弹窗状态
|
|
|
|
|
+const showVideoModal = ref(false);
|
|
|
|
|
+const showChapterMenuModal = ref(false);
|
|
|
|
|
+const showRenameBookModal = ref(false);
|
|
|
|
|
+const showRenameChapterModal = ref(false);
|
|
|
|
|
+const showMoveChapterModal = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+// 表单数据
|
|
|
|
|
+const selectedChapter = ref<Chapter | null>(null);
|
|
|
|
|
+const newBookTitle = ref('');
|
|
|
|
|
+const newChapterTitle = ref('');
|
|
|
|
|
+const selectedAlbumId = ref<number | null>(null);
|
|
|
|
|
+const albumList = ref<any[]>([]);
|
|
|
|
|
+
|
|
|
|
|
+const userId = 1;
|
|
|
|
|
|
|
|
// 已完成章节数
|
|
// 已完成章节数
|
|
|
const completedChapters = computed(() => {
|
|
const completedChapters = computed(() => {
|
|
@@ -155,6 +281,16 @@ async function fetchBookDetail() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 加载专辑列表(用于移动章节)
|
|
|
|
|
+async function fetchAlbumList() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await get('/book-generator/albums');
|
|
|
|
|
+ albumList.value = data?.albums?.filter((a: any) => a.id !== parseInt(bookId.value)) || [];
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取专辑列表失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 切换收藏状态
|
|
// 切换收藏状态
|
|
|
async function toggleFavorite() {
|
|
async function toggleFavorite() {
|
|
|
try {
|
|
try {
|
|
@@ -215,17 +351,74 @@ function playChapter(item: Chapter, index: number) {
|
|
|
uni.navigateTo({ url: `/pages/player/index?id=${item.id}` });
|
|
uni.navigateTo({ url: `/pages/player/index?id=${item.id}` });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 显示章节菜单
|
|
|
|
|
+function showChapterMenu(item: Chapter) {
|
|
|
|
|
+ selectedChapter.value = item;
|
|
|
|
|
+ newChapterTitle.value = item.title;
|
|
|
|
|
+ selectedAlbumId.value = null;
|
|
|
|
|
+ showChapterMenuModal.value = true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 重命名专辑
|
|
|
|
|
+async function renameBook() {
|
|
|
|
|
+ if (!newBookTitle.value.trim() || !book.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await put(`/book-generator/album/${book.value.id}/rename`, {
|
|
|
|
|
+ title: newBookTitle.value.trim(),
|
|
|
|
|
+ });
|
|
|
|
|
+ book.value.title = newBookTitle.value.trim();
|
|
|
|
|
+ uni.showToast({ title: '重命名成功', icon: 'success' });
|
|
|
|
|
+ showRenameBookModal.value = false;
|
|
|
|
|
+ newBookTitle.value = '';
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ uni.showToast({ title: '重命名失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 重命名章节
|
|
|
|
|
+async function renameChapter() {
|
|
|
|
|
+ if (!newChapterTitle.value.trim() || !selectedChapter.value || !book.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await put(`/book-generator/album/${book.value.id}/chapters/${selectedChapter.value.id}/rename`, {
|
|
|
|
|
+ title: newChapterTitle.value.trim(),
|
|
|
|
|
+ });
|
|
|
|
|
+ selectedChapter.value.title = newChapterTitle.value.trim();
|
|
|
|
|
+ uni.showToast({ title: '重命名成功', icon: 'success' });
|
|
|
|
|
+ showRenameChapterModal.value = false;
|
|
|
|
|
+ newChapterTitle.value = '';
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ uni.showToast({ title: '重命名失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 移动章节
|
|
|
|
|
+async function moveChapter() {
|
|
|
|
|
+ if (!selectedAlbumId.value || !selectedChapter.value || !book.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await put(`/book-generator/album/${book.value.id}/chapters/${selectedChapter.value.id}/move`, {
|
|
|
|
|
+ newNumber: 1,
|
|
|
|
|
+ });
|
|
|
|
|
+ uni.showToast({ title: '移动成功', icon: 'success' });
|
|
|
|
|
+ showMoveChapterModal.value = false;
|
|
|
|
|
+ selectedAlbumId.value = null;
|
|
|
|
|
+ await fetchBookDetail();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ uni.showToast({ title: '移动失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 播放视频
|
|
// 播放视频
|
|
|
const videoUrl = ref('');
|
|
const videoUrl = ref('');
|
|
|
const videoTitle = ref('');
|
|
const videoTitle = ref('');
|
|
|
-const showVideoModal = ref(false);
|
|
|
|
|
|
|
|
|
|
function playVideo(item: Chapter) {
|
|
function playVideo(item: Chapter) {
|
|
|
if (!item.videoUrl) {
|
|
if (!item.videoUrl) {
|
|
|
uni.showToast({ title: '该章节暂无视频', icon: 'none' });
|
|
uni.showToast({ title: '该章节暂无视频', icon: 'none' });
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- // 使用相对路径(Nginx 反向代理)
|
|
|
|
|
videoUrl.value = item.videoUrl.startsWith('http') ? item.videoUrl : item.videoUrl;
|
|
videoUrl.value = item.videoUrl.startsWith('http') ? item.videoUrl : item.videoUrl;
|
|
|
videoTitle.value = item.title;
|
|
videoTitle.value = item.title;
|
|
|
showVideoModal.value = true;
|
|
showVideoModal.value = true;
|
|
@@ -241,7 +434,7 @@ function closeVideoModal() {
|
|
|
onLoad(async (options: any) => {
|
|
onLoad(async (options: any) => {
|
|
|
bookId.value = options?.id || '';
|
|
bookId.value = options?.id || '';
|
|
|
if (bookId.value) {
|
|
if (bookId.value) {
|
|
|
- await fetchBookDetail();
|
|
|
|
|
|
|
+ await Promise.all([fetchBookDetail(), fetchAlbumList()]);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -251,7 +444,7 @@ onMounted(() => {
|
|
|
const currentPage = pages[pages.length - 1] as any;
|
|
const currentPage = pages[pages.length - 1] as any;
|
|
|
bookId.value = currentPage?.options?.id || '';
|
|
bookId.value = currentPage?.options?.id || '';
|
|
|
if (bookId.value) {
|
|
if (bookId.value) {
|
|
|
- fetchBookDetail();
|
|
|
|
|
|
|
+ Promise.all([fetchBookDetail(), fetchAlbumList()]);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
@@ -641,4 +834,136 @@ onMounted(() => {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
display: block;
|
|
display: block;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 弹窗 */
|
|
|
|
|
+.modal {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ z-index: 1000;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-mask {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-content {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 85%;
|
|
|
|
|
+ max-width: 600rpx;
|
|
|
|
|
+ background: #ffffff;
|
|
|
|
|
+ border-radius: 24rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.menu-content {
|
|
|
|
|
+ max-width: 500rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-header {
|
|
|
|
|
+ padding: 32rpx;
|
|
|
|
|
+ border-bottom: 1rpx solid #f3f4f6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-title {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-body {
|
|
|
|
|
+ padding: 32rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.input {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 80rpx;
|
|
|
|
|
+ padding: 0 24rpx;
|
|
|
|
|
+ border: 1rpx solid #e5e7eb;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.menu-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.menu-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ background: #f9f9f9;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.menu-icon {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.menu-text {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.album-select-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+ max-height: 500rpx;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.album-select-item {
|
|
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
|
|
+ background: #f9f9f9;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.album-select-item.selected {
|
|
|
|
|
+ background: #eff6ff;
|
|
|
|
|
+ border: 2rpx solid #4f46e5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ border-top: 1rpx solid #f3f4f6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-btn {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ height: 96rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-btn.cancel {
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ border-right: 1rpx solid #f3f4f6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-btn.confirm {
|
|
|
|
|
+ color: #4f46e5;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-btn.confirm[disabled] {
|
|
|
|
|
+ opacity: 0.5;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|