From 6f5aa625e60281d124bc9ce3b727f496b348d9d0 Mon Sep 17 00:00:00 2001
From: HenryDu8133 <813367384@qq.com>
Date: Sat, 19 Sep 2026 14:15:45 +0800
Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=AF=BC=E5=85=A5=E9=85=B7?=
=?UTF-8?q?=E7=8B=97=E9=A1=B5=E5=90=8C=E6=97=B6=E4=BF=9D=E7=95=99=E3=80=8C?=
=?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5=E3=80=8D=E4=B8=8E=E3=80=8C?=
=?UTF-8?q?=E6=89=8B=E5=8A=A8=E7=94=9F=E6=88=90=E7=85=A7=E7=89=87=E3=80=8D?=
=?UTF-8?q?=E4=B8=A4=E7=A7=8D=E6=A8=A1=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
KuGouImporter.client.vue 顶部新增模式切换 Tab(默认自动同步),切换到「手动生成照片」即复用原有 PlaylistCardPage 流程:AdminMultiDatePicker 多选日期 → 逐日出排期列表 → 每一日独立「生成该日图片」按钮 → PlaylistCardGenerator 弹窗下载 2:3 卡片 PNG。两套模式各自维护日期选择/排期读取状态,互不阻塞。
UI/Icon.vue 补 layout / image 两个 SVG 图标适配新入口。旧 PlaylistCardPage.client.vue 仍保留不作删除以免回退需求。
---
app/components/Admin/KuGouImporter.client.vue | 174 +++++++++++++++++-
app/components/UI/Icon.vue | 14 ++
2 files changed, 185 insertions(+), 3 deletions(-)
diff --git a/app/components/Admin/KuGouImporter.client.vue b/app/components/Admin/KuGouImporter.client.vue
index 6f87e1c..e2410c2 100644
--- a/app/components/Admin/KuGouImporter.client.vue
+++ b/app/components/Admin/KuGouImporter.client.vue
@@ -3,11 +3,38 @@
导入酷狗
- 绑定管理员酷狗账号后,可一键根据排期日期自动创建歌单并同步歌曲到酷狗歌单
+ 支持两种方式:自动同步(sidecar 一键建歌单加歌)/ 手动生成照片卡片后扫码导入
-
+
+
+
+
+
+
+
+
@@ -395,6 +422,88 @@ PORT=3000 npm start
+
+
+
+
+
+ 选择日期
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ date }} 排期预览 ({{ schedules.length }} 首)
+
+
+
+
+
+
{{ index + 1 }}.
+
+
{{ schedule?.song?.title || schedule?.title }}
+
{{ schedule?.song?.artist || schedule?.artist }}
+
+
+
+
+ 该日期没有排期歌曲
+
+
+
+
+
+
+
+
@@ -403,6 +512,7 @@ import { computed, onMounted, ref, watch } from 'vue'
import Icon from '~/components/UI/Icon.vue'
import LoadingState from '~/components/UI/Common/LoadingState.vue'
import AdminMultiDatePicker from './AdminMultiDatePicker.vue'
+import AdminPlaylistCardGenerator from './PlaylistCardGenerator.vue'
import { useAuth } from '~/composables/useAuth'
import { useToast } from '~/composables/useToast'
@@ -412,6 +522,8 @@ const toast = useToast()
const WEEKDAY = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const today = new Date()
const todayStr = `${today.getFullYear()}-${pad2(today.getMonth() + 1)}-${pad2(today.getDate())}`
+
+const mode = ref<'auto' | 'manual-card'>('auto')
const selectedDates = ref([todayStr])
const status = ref(null)
@@ -431,6 +543,13 @@ const baseUrlInput = ref('')
const baseUrlSaving = ref(false)
const baseUrlSavedTick = ref(0)
+const cardSelectedDates = ref([todayStr])
+const cardSchedulesByDate = ref>({})
+const cardLoading = ref(false)
+const showCardModal = ref(false)
+const currentGeneratorDate = ref('')
+const currentGeneratorSongs = ref([])
+
const totalLoadedSongs = computed(
() => Object.values(schedulesByDate.value).reduce((n, arr) => n + arr.length, 0),
)
@@ -696,13 +815,62 @@ async function loadSyncHistory() {
}
}
+async function fetchCardSchedulesForDate(date: string) {
+ try {
+ const response: any = await $fetch('/api/admin/schedule/full', {
+ ...auth.getAuthConfig(),
+ query: { date, includeDrafts: 'true' },
+ })
+ if (response?.success && response?.data?.schedules) {
+ return response.data.schedules
+ }
+ } catch (e: any) {
+ console.error(`Failed to fetch schedules for ${date}:`, e)
+ }
+ return []
+}
+
+async function fetchCardSchedules() {
+ if (cardSelectedDates.value.length === 0) {
+ cardSchedulesByDate.value = {}
+ return
+ }
+ cardLoading.value = true
+ const newSchedules: Record = {}
+ try {
+ const sortedDates = [...cardSelectedDates.value].sort()
+ for (const d of sortedDates) {
+ const items = await fetchCardSchedulesForDate(d)
+ if (items.length > 0) newSchedules[d] = items
+ }
+ cardSchedulesByDate.value = newSchedules
+ if (Object.keys(newSchedules).length === 0) {
+ toast.show?.('所选日期没有排期歌曲', 'info')
+ }
+ } catch (e: any) {
+ toast.show?.(e?.message || '获取排期失败', 'error')
+ } finally {
+ cardLoading.value = false
+ }
+}
+
+function openGenerator(date: string, songs: any[]) {
+ currentGeneratorDate.value = date
+ currentGeneratorSongs.value = songs
+ showCardModal.value = true
+}
+
watch(
() => selectedDates.value.slice().sort().join(','),
() => loadSchedules(),
)
+watch(
+ () => cardSelectedDates.value.slice().sort().join(','),
+ () => fetchCardSchedules(),
+)
onMounted(async () => {
await reloadStatus()
- await Promise.all([loadSchedules(), loadSyncHistory()])
+ await Promise.all([loadSchedules(), loadSyncHistory(), fetchCardSchedules()])
})
diff --git a/app/components/UI/Icon.vue b/app/components/UI/Icon.vue
index ccf44d2..9090c46 100644
--- a/app/components/UI/Icon.vue
+++ b/app/components/UI/Icon.vue
@@ -399,6 +399,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+