feat(songs): 歌曲管理支持批量驳回、投稿留言自动审查与排期范围可见性过滤
- 新增 /api/admin/songs/batch-reject.post 批量驳回接口(事务清排期/投票/扣接纳数/黑名单去重/异步通知) - requireSongAdmin 三角色校验 + createApiError 统一错误包装 - scheduleDaysBefore/After 四字段排期可见范围限制(开放 API 与公开排期接口三处缓存路径 + DB 条件过滤) - submissionNoteAutoModeration 基于黑名单 KEYWORD/LANGUAGE/GENRE 自动判定留言 publicStatus,命中自动关闭 submissionNotePublic - 更新 songs/request.post 与 songs/[id]/update.put 接入自动审查逻辑
This commit is contained in:
@@ -61,6 +61,12 @@
|
||||
>
|
||||
<RotateCcw :size="14" class="text-blue-500/70" /> 未播放 ({{ selectedSongs.length }})
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-amber-400 text-[11px] font-black rounded-lg border border-zinc-700/50 transition-all active:scale-95 uppercase tracking-widest"
|
||||
@click="batchReject"
|
||||
>
|
||||
<X :size="14" class="text-amber-500/70" /> 驳回 ({{ selectedSongs.length }})
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-red-400 text-[11px] font-black rounded-lg border border-zinc-700/50 transition-all active:scale-95 uppercase tracking-widest"
|
||||
@click="batchDelete"
|
||||
@@ -408,7 +414,7 @@
|
||||
@click.stop
|
||||
>
|
||||
<div class="px-8 py-6 border-b border-zinc-800/50 flex items-center justify-between">
|
||||
<h3 class="text-xl font-black text-zinc-100">驳回歌曲</h3>
|
||||
<h3 class="text-xl font-black text-zinc-100">{{ isBatchReject ? `批量驳回歌曲 (${selectedSongs.length} 首)` : '驳回歌曲' }}</h3>
|
||||
<button
|
||||
class="text-zinc-500 hover:text-zinc-300 transition-colors"
|
||||
@click="cancelReject"
|
||||
@@ -418,7 +424,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-8 space-y-6">
|
||||
<div class="p-5 bg-zinc-950 border border-zinc-800 rounded-lg flex items-center gap-4">
|
||||
<div v-if="!isBatchReject" class="p-5 bg-zinc-950 border border-zinc-800 rounded-lg flex items-center gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg bg-red-500/10 text-red-500 flex items-center justify-center border border-red-500/10"
|
||||
>
|
||||
@@ -429,6 +435,17 @@
|
||||
<p class="text-xs text-zinc-500">投稿人: {{ rejectSongInfo.requester }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="p-5 bg-zinc-950 border border-zinc-800 rounded-lg flex items-center gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg bg-amber-500/10 text-amber-500 flex items-center justify-center border border-amber-500/10"
|
||||
>
|
||||
<ListMusic :size="18" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-bold text-zinc-100 text-sm">即将批量驳回 {{ selectedSongs.length }} 首歌曲</h4>
|
||||
<p class="text-xs text-zinc-500">驳回后将一并清理投票、排期记录并发送通知给对应投稿人</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
|
||||
@@ -961,6 +978,7 @@ import {
|
||||
X,
|
||||
Trash2,
|
||||
Music,
|
||||
ListMusic,
|
||||
Heart,
|
||||
Download,
|
||||
ChevronDown,
|
||||
@@ -1040,6 +1058,7 @@ const showRejectDialog = ref(false)
|
||||
const rejectLoading = ref(false)
|
||||
const rejectReason = ref('')
|
||||
const addToBlacklist = ref(false)
|
||||
const isBatchReject = ref(false)
|
||||
const rejectSongInfo = ref({
|
||||
id: null,
|
||||
title: '',
|
||||
@@ -1475,6 +1494,7 @@ const rejectSong = (songId) => {
|
||||
const song = songs.value.find((s) => s.id === songId)
|
||||
if (!song) return
|
||||
|
||||
isBatchReject.value = false
|
||||
rejectSongInfo.value = {
|
||||
id: song.id,
|
||||
title: song.title || '',
|
||||
@@ -1487,6 +1507,22 @@ const rejectSong = (songId) => {
|
||||
showRejectDialog.value = true
|
||||
}
|
||||
|
||||
// 批量驳回歌曲
|
||||
const batchReject = () => {
|
||||
if (selectedSongs.value.length === 0) return
|
||||
|
||||
isBatchReject.value = true
|
||||
rejectSongInfo.value = {
|
||||
id: null,
|
||||
title: '',
|
||||
artist: '',
|
||||
requester: ''
|
||||
}
|
||||
rejectReason.value = ''
|
||||
addToBlacklist.value = false
|
||||
showRejectDialog.value = true
|
||||
}
|
||||
|
||||
// 确认驳回
|
||||
const confirmReject = async () => {
|
||||
if (!rejectReason.value.trim()) {
|
||||
@@ -1496,26 +1532,40 @@ const confirmReject = async () => {
|
||||
|
||||
rejectLoading.value = true
|
||||
try {
|
||||
await $fetch('/api/admin/songs/reject', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
songId: rejectSongInfo.value.id,
|
||||
reason: rejectReason.value.trim(),
|
||||
addToBlacklist: addToBlacklist.value
|
||||
if (isBatchReject.value) {
|
||||
await $fetch('/api/admin/songs/batch-reject', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
songIds: selectedSongs.value,
|
||||
reason: rejectReason.value.trim(),
|
||||
addToBlacklist: addToBlacklist.value
|
||||
}
|
||||
})
|
||||
await refreshSongs(true)
|
||||
selectedSongs.value = []
|
||||
showNotification(`批量驳回成功,共处理 ${selectedSongs.value.length} 首歌曲`, 'success')
|
||||
} else {
|
||||
await $fetch('/api/admin/songs/reject', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
songId: rejectSongInfo.value.id,
|
||||
reason: rejectReason.value.trim(),
|
||||
addToBlacklist: addToBlacklist.value
|
||||
}
|
||||
})
|
||||
|
||||
await refreshSongs(true)
|
||||
|
||||
const index = selectedSongs.value.indexOf(rejectSongInfo.value.id)
|
||||
if (index > -1) {
|
||||
selectedSongs.value.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
await refreshSongs(true)
|
||||
|
||||
const index = selectedSongs.value.indexOf(rejectSongInfo.value.id)
|
||||
if (index > -1) {
|
||||
selectedSongs.value.splice(index, 1)
|
||||
showNotification('歌曲驳回成功,已通知投稿人', 'success')
|
||||
}
|
||||
|
||||
showRejectDialog.value = false
|
||||
|
||||
showNotification('歌曲驳回成功,已通知投稿人', 'success')
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('驳回歌曲失败:', error)
|
||||
showNotification('驳回失败: ' + (error.data?.message || error.message), 'error')
|
||||
} finally {
|
||||
@@ -1528,6 +1578,7 @@ const cancelReject = () => {
|
||||
showRejectDialog.value = false
|
||||
rejectReason.value = ''
|
||||
addToBlacklist.value = false
|
||||
isBatchReject.value = false
|
||||
rejectSongInfo.value = {
|
||||
id: null,
|
||||
title: '',
|
||||
|
||||
Reference in New Issue
Block a user