Przeglądaj źródła

feat: 实现功能34 - 专辑详情页长按菜单(重命名、移动)

MyFramework User 4 miesięcy temu
rodzic
commit
1d9b219822
1 zmienionych plików z 331 dodań i 6 usunięć
  1. 331 6
      my-uniapp-vue3/src/pages/album/index.vue

+ 331 - 6
my-uniapp-vue3/src/pages/album/index.vue

@@ -6,7 +6,9 @@
         <text class="nav-icon">←</text>
       </view>
       <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>
 
     <!-- 骨架屏加载状态 -->
@@ -53,6 +55,7 @@
           :key="item.id"
           :class="['audio-item', { 'has-audio': item.audioUrl, 'has-video': item.videoUrl }]"
           @click="playChapter(item, index)"
+          @longpress="showChapterMenu(item)"
         >
           <view class="audio-index">{{ index + 1 }}</view>
           <view class="audio-content">
@@ -97,6 +100,114 @@
         ></video>
       </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>
 </template>
 
@@ -106,6 +217,7 @@ import { onLoad } from '@dcloudio/uni-app';
 import { useAudioStore } from '../../store/audio';
 import * as bookApi from '../../utils/book-generator-api';
 import type { Book, Chapter } from '../../utils/book-generator-api';
+import { get, put } from '../../utils/request';
 
 const audioStore = useAudioStore();
 
@@ -115,7 +227,21 @@ const chapters = ref<Chapter[]>([]);
 const loading = 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(() => {
@@ -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() {
   try {
@@ -215,17 +351,74 @@ function playChapter(item: Chapter, index: number) {
   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 videoTitle = ref('');
-const showVideoModal = ref(false);
 
 function playVideo(item: Chapter) {
   if (!item.videoUrl) {
     uni.showToast({ title: '该章节暂无视频', icon: 'none' });
     return;
   }
-  // 使用相对路径(Nginx 反向代理)
   videoUrl.value = item.videoUrl.startsWith('http') ? item.videoUrl : item.videoUrl;
   videoTitle.value = item.title;
   showVideoModal.value = true;
@@ -241,7 +434,7 @@ function closeVideoModal() {
 onLoad(async (options: any) => {
   bookId.value = options?.id || '';
   if (bookId.value) {
-    await fetchBookDetail();
+    await Promise.all([fetchBookDetail(), fetchAlbumList()]);
   }
 });
 
@@ -251,7 +444,7 @@ onMounted(() => {
     const currentPage = pages[pages.length - 1] as any;
     bookId.value = currentPage?.options?.id || '';
     if (bookId.value) {
-      fetchBookDetail();
+      Promise.all([fetchBookDetail(), fetchAlbumList()]);
     }
   }
 });
@@ -641,4 +834,136 @@ onMounted(() => {
   width: 100%;
   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>