feat(songs): 歌曲管理支持批量驳回、投稿留言自动审查与排期范围可见性过滤
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 6m31s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s

- 新增 /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:
2026-09-06 17:03:20 +08:00
parent 43e2a05666
commit bd8df1f026
53 changed files with 10486 additions and 245 deletions
+68 -17
View File
@@ -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: '',
+11 -2
View File
@@ -26,7 +26,17 @@
<AuthProvidersGoogleIcon />
</button>
<!-- 其他提供商可在此添加 -->
<button
v-if="config.public.oauth.aggregate"
type="button"
class="w-12 h-12 rounded-full border border-[var(--border-color)] bg-[var(--bg-secondary)] text-[var(--text-primary)] flex items-center justify-center cursor-pointer transition-all duration-200 hover:-translate-y-0.5 hover:shadow-sm hover:bg-[var(--bg-tertiary)] hover:bg-[#12B7F5] hover:text-white hover:border-[#12B7F5]"
title="使用 QQ / 微信登录"
@click="loginWith('aggregate')"
>
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true">
<path d="M8.5 4C4.9 4 2 6.8 2 10.3c0 2 1.1 3.7 2.8 4.9L4 18l3-1.5c.5.1 1 .2 1.5.2h.6c-.1-.4-.1-.8 0-1.2H8.5C5.5 15.5 3 13.2 3 10.3 3 7.4 5.5 5 8.5 5h1.6c.3-1.1 1.3-1.9 2.5-1.9.5 0 1 .2 1.4.5C13.3 3 10.7 4 8.5 4zM10.1 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm8.4 1.2c-2.5 0-4.5 2-4.5 4.5s2 4.5 4.5 4.5c.6 0 1.2-.1 1.7-.3L22 17.5l-.8-2.3c1.4-1 2.3-2.6 2.3-4.3C23.5 9.2 21.5 7.2 19 7.2c-.5 0-1 0-1.4.1C16.9 7.8 17.9 7.2 19 7.2zM17 9.7c-.8 0-1.5.7-1.5 1.5s.7 1.5 1.5 1.5 1.5-.7 1.5-1.5-.7-1.5-1.5-1.5z"/>
</svg>
</button>
</div>
</div>
</template>
@@ -39,7 +49,6 @@ const hasEnabledProviders = computed(() => {
})
const loginWith = (provider: string) => {
// 外部导航到 API 端点
navigateTo(`/api/auth/${provider}`, { external: true })
}
</script>