|
|
@@ -8,6 +8,24 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
+ <!-- 编辑栏 -->
|
|
|
+ <view class="edit-bar">
|
|
|
+ <view v-if="!isEditing" class="edit-btn" @click="isEditing = true">
|
|
|
+ <text>编辑</text>
|
|
|
+ </view>
|
|
|
+ <view v-else class="edit-actions">
|
|
|
+ <view class="action-btn" @click="toggleSelectAll">
|
|
|
+ <text>{{ isAllSelected ? '取消全选' : '全选' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="action-btn delete" @click="handleBatchDelete" :disabled="selectedIds.length === 0">
|
|
|
+ <text>删除 ({{ selectedIds.length }})</text>
|
|
|
+ </view>
|
|
|
+ <view class="action-btn cancel" @click="cancelEdit">
|
|
|
+ <text>取消</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
<!-- 标签栏 -->
|
|
|
<view class="tab-bar">
|
|
|
<scroll-view class="tab-scroll" scroll-x="true" :show-scrollbar="false">
|
|
|
@@ -38,8 +56,12 @@
|
|
|
v-for="item in audioList"
|
|
|
:key="item._id"
|
|
|
class="audio-card"
|
|
|
- @click="playAudio(item)"
|
|
|
+ :class="{ selected: selectedIds.includes(item._id) }"
|
|
|
+ @click="isEditing ? toggleSelect(item._id) : playAudio(item)"
|
|
|
>
|
|
|
+ <view v-if="isEditing" class="checkbox">
|
|
|
+ <text>{{ selectedIds.includes(item._id) ? '✓' : '' }}</text>
|
|
|
+ </view>
|
|
|
<view class="audio-cover" :style="{ background: getCoverGradient(item.voiceId) }">
|
|
|
<text class="cover-icon">🎵</text>
|
|
|
<view class="play-overlay">
|
|
|
@@ -68,11 +90,16 @@
|
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
import { useAudioStore } from '../../store/audio';
|
|
|
-import { get } from '../../utils/request';
|
|
|
+import { get, post } from '../../utils/request';
|
|
|
import type { AudioItem } from '../../types';
|
|
|
|
|
|
const audioStore = useAudioStore();
|
|
|
|
|
|
+// 编辑模式状态
|
|
|
+const isEditing = ref(false);
|
|
|
+const selectedIds = ref<string[]>([]);
|
|
|
+const isAllSelected = ref(false);
|
|
|
+
|
|
|
// 标签状态
|
|
|
const tabs = ref([
|
|
|
{ id: 1, name: '全部' },
|
|
|
@@ -117,6 +144,56 @@ async function selectTab(tabId: number) {
|
|
|
await fetchHistoryList();
|
|
|
}
|
|
|
|
|
|
+// 切换选择
|
|
|
+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 post('/history/batch-delete', { ids: selectedIds.value });
|
|
|
+ uni.showToast({ title: '删除成功', icon: 'success' });
|
|
|
+ await fetchHistoryList();
|
|
|
+ cancelEdit();
|
|
|
+ } catch (error) {
|
|
|
+ uni.showToast({ title: '删除失败', icon: 'none' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
// 获取历史列表
|
|
|
async function fetchHistoryList(isLoadMore = false) {
|
|
|
if (loading.value) return;
|
|
|
@@ -129,7 +206,7 @@ async function fetchHistoryList(isLoadMore = false) {
|
|
|
// 按时间过滤
|
|
|
let startDate = '';
|
|
|
const now = new Date();
|
|
|
-
|
|
|
+
|
|
|
switch (selectedTab.value) {
|
|
|
case 2: // 今天
|
|
|
startDate = new Date(now.setHours(0, 0, 0, 0)).toISOString();
|
|
|
@@ -249,11 +326,57 @@ onMounted(async () => {
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
}
|
|
|
|
|
|
-.tab-bar {
|
|
|
+.edit-bar {
|
|
|
position: fixed;
|
|
|
top: 100rpx;
|
|
|
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.delete[disabled] {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+
|
|
|
+.action-btn.cancel {
|
|
|
+ background: #f3f4f6;
|
|
|
+ color: #374151;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-bar {
|
|
|
+ position: fixed;
|
|
|
+ top: 168rpx;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
background: #ffffff;
|
|
|
z-index: 99;
|
|
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
|
|
@@ -296,8 +419,8 @@ onMounted(async () => {
|
|
|
}
|
|
|
|
|
|
.audio-list {
|
|
|
- padding-top: 160rpx;
|
|
|
- height: calc(100vh - 160rpx);
|
|
|
+ padding-top: 240rpx;
|
|
|
+ height: calc(100vh - 240rpx);
|
|
|
}
|
|
|
|
|
|
.empty {
|
|
|
@@ -337,6 +460,28 @@ onMounted(async () => {
|
|
|
border-radius: 16rpx;
|
|
|
overflow: hidden;
|
|
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
|
|
+ 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-cover {
|