@@ -0,0 +1,630 @@
< template >
< div class = "flex flex-col space-y-8 pb-12 lg:pb-20 animate-in fade-in slide-in-from-bottom-4 duration-500" >
< div class = "flex flex-col space-y-2" >
< h2 class = "text-2xl font-bold tracking-tight text-zinc-900" > 导入酷狗 < / h2 >
< p class = "text-sm text-zinc-500" >
绑定管理员酷狗账号后 , 可一键根据排期日期自动创建歌单并同步歌曲到酷狗歌单
< / p >
< / div >
< div class = "grid grid-cols-1 xl:grid-cols-3 gap-6" >
<!-- 左 : 账号绑定 & sidecar 状态 -- >
< div class = "xl:col-span-1 flex flex-col gap-6" >
< div class = "bg-white/90 border border-zinc-200 rounded-2xl p-6 shadow-sm" >
< div class = "flex items-center gap-2 mb-4" >
< div class = "w-9 h-9 rounded-xl bg-blue-50 flex items-center justify-center text-blue-600" >
< Icon name = "user" :size = "18" / >
< / div >
< div >
< h3 class = "text-base font-bold text-zinc-900" > 管理员酷狗账号 < / h3 >
< p class = "text-[11px] text-zinc-500" > 扫码绑定后 , 歌单会同步到此账号的收藏列表 < / p >
< / div >
< / div >
< div v-if = "status" class="space-y-3 text-[12px]" >
< div class = "flex items-center justify-between" >
< span class = "text-zinc-500" > Sidecar 地址 < / span >
< code class = "text-[11px] px-2 py-0.5 rounded-lg bg-zinc-100 text-zinc-700 font-mono" > { { status . service . baseUrl } } < / code >
< / div >
< div class = "flex items-center gap-2" >
< span class = "text-zinc-500" > 服务状态 < / span >
< span
: class = "[
'inline-flex items-center gap-1 px-2 py-0.5 rounded-lg text-[11px] font-bold',
status.service.serviceOk
? 'bg-emerald-50 text-emerald-700'
: 'bg-amber-50 text-amber-700',
]"
>
< span
: class = "[
'w-1.5 h-1.5 rounded-full',
status.service.serviceOk ? 'bg-emerald-500' : 'bg-amber-500',
]"
> < / span >
{ { status . service . serviceOk ? '在线' : '待启动' } }
< / span >
< / div >
< p class = "text-[11px] leading-relaxed text-zinc-600 whitespace-pre-wrap break-words" >
{ { status . service . serviceMessage } }
< / p >
< div class = "flex items-center justify-between" >
< span class = "text-zinc-500" > 绑定状态 < / span >
< span
: class = "[
'inline-flex items-center gap-1 px-2 py-0.5 rounded-lg text-[11px] font-bold',
status.bound.hasToken || status.bound.hasCookie
? 'bg-blue-50 text-blue-700'
: 'bg-zinc-100 text-zinc-500',
]"
>
{ { status . bound . hasToken || status . bound . hasCookie ? '已绑定' : '未绑定' } }
< / span >
< / div >
< div v-if = "status.bound.boundAt" class="text-[11px] text-zinc-500" >
绑定时间 : {{ formatDateTime ( status.bound.boundAt ) }}
< / div >
< / div >
< div v-else class = "space-y-2 text-[12px]" >
< LoadingState :height = "72" / >
< / div >
< div class = "mt-5 space-y-3" >
< div class = "flex gap-2" >
< button
class = "flex-1 h-[44px] px-3 rounded-xl text-[12px] font-bold bg-zinc-100 hover:bg-zinc-200 text-zinc-700 transition-colors flex items-center justify-center gap-1.5"
@click ="reloadStatus"
>
< Icon name = "rotate-cw" :size = "14" / > 刷新状态
< / button >
< button
v-if = "status?.bound?.hasToken || status?.bound?.hasCookie"
class = "flex-1 h-[44px] px-3 rounded-xl text-[12px] font-bold bg-red-50 hover:bg-red-100 text-red-600 transition-colors flex items-center justify-center gap-1.5 border border-red-100"
@click ="handleUnbind"
>
< Icon name = "log-out" :size = "14" / > 解除绑定
< / button >
< / div >
< button
v-if = "!(status?.bound?.hasToken || status?.bound?.hasCookie)"
:disabled = "qrLoading"
class = "w-full h-[48px] px-3 rounded-xl text-[12px] font-bold bg-blue-600 hover:bg-blue-500 disabled:bg-blue-600/60 text-white transition-colors flex items-center justify-center gap-2 shadow-md shadow-blue-900/10"
@click ="startQrLogin"
>
< Icon name = "qr-code" :size = "16" / >
{ { qrLoading ? '正在获取二维码...' : '扫码绑定管理员酷狗账号' } }
< / button >
< / div >
< / div >
<!-- 扫码弹窗 -- >
< div
v-if = "qrOpen"
class = "bg-white/90 border border-zinc-200 rounded-2xl p-6 shadow-sm flex flex-col items-center text-center"
>
< h3 class = "text-base font-bold text-zinc-900 mb-1" > 扫码绑定酷狗账号 < / h3 >
< p class = "text-xs text-zinc-500 mb-4" > 使用酷狗 App 扫描下方二维码完成绑定 < / p >
< div
class = "w-[260px] h-[260px] rounded-2xl border border-zinc-200 bg-zinc-50 flex items-center justify-center overflow-hidden"
>
< img
v-if = "qrImg"
:src = "qrImg"
alt = "kugou-qr"
class = "w-full h-full object-contain"
/ >
< LoadingState v-else :height = "260" / >
< / div >
< p class = "mt-4 text-xs font-semibold" :class = "qrStateColor" >
{ { qrHint || '等待扫码...' } }
< / p >
< button
class = "mt-4 h-[40px] px-4 rounded-xl text-[12px] font-bold bg-zinc-100 hover:bg-zinc-200 text-zinc-700 transition-colors"
@click ="closeQr"
>
取消
< / button >
< / div >
<!-- sidecar 部署说明 -- >
< div class = "bg-zinc-50 border border-dashed border-zinc-300 rounded-2xl p-5 text-[12px] leading-relaxed text-zinc-600 space-y-2" >
< h4 class = "text-zinc-900 font-bold flex items-center gap-1.5 text-sm" >
< Icon name = "info" :size = "14" / > 如何启动酷狗 sidecar ?
< / h4 >
< p > 推荐方式 B : 部署开源 KuGouMusicApi 项目到本机 3000 端口 ( 与 VoiceHub 同一台机器即可 ) 。 < / p >
< pre class = "bg-zinc-900 text-zinc-100 text-[11px] rounded-xl p-3 overflow-x-auto font-mono" > < code > git clone https : //github.com/MakcRe/KuGouMusicApi
cd KuGouMusicApi
npm install
PORT = 3000 npm start < / code > < / pre >
< p > 启动后点 「 刷新状态 」 验证连通性 ; 如需自定义端口 , 可在站点配置管理中设置 kugouApiBaseUrl 。 < / p >
< / div >
< / div >
<!-- 中 : 排期同步主面板 -- >
< div class = "xl:col-span-2 flex flex-col gap-6" >
< div class = "bg-white/90 border border-zinc-200 rounded-2xl p-6 shadow-sm" >
< div class = "flex flex-wrap items-center justify-between gap-3 mb-5" >
< div class = "flex items-center gap-2" >
< div class = "w-9 h-9 rounded-xl bg-indigo-50 flex items-center justify-center text-indigo-600" >
< Icon name = "music" :size = "18" / >
< / div >
< div >
< h3 class = "text-base font-bold text-zinc-900" > 按排期日期创建酷狗歌单 < / h3 >
< p class = "text-[11px] text-zinc-500" >
每个日期创建 1 个歌单 , 命名 : < span class = "font-mono text-zinc-700" > BYW广播 YYYY - MM - DD ( 周X ) < / span >
< / p >
< / div >
< / div >
< button
: disabled = "syncing || selectedDates.length === 0 || !(status?.bound?.hasToken || status?.bound?.hasCookie)"
class = "h-[48px] px-5 rounded-xl text-[12px] font-bold bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 text-white transition-colors inline-flex items-center justify-center gap-2 shadow-md shadow-indigo-900/10"
@click ="syncToKugou"
>
< Icon name = "upload-cloud" :size = "16" / >
{ { syncing ? '同步中...' : ` 同步 ${ selectedDates . length } 个排期日到酷狗 ` } }
< / button >
< / div >
< div class = "space-y-3" >
< div >
< label class = "text-[11px] font-black uppercase tracking-wider text-zinc-600" >
选择排期日期 ( 可多选 )
< / label >
< div class = "mt-2" >
< AdminMultiDatePicker v-model = "selectedDates" / >
< / div >
< / div >
< div class = "pt-2 grid grid-cols-1 md:grid-cols-3 gap-3 text-[12px]" >
< div class = "rounded-xl p-3 bg-zinc-50 border border-zinc-200" >
< p class = "text-zinc-500" > 已选排期数 < / p >
< p class = "mt-1 text-lg font-bold text-zinc-900" > { { selectedDates . length } } 天 < / p >
< / div >
< div class = "rounded-xl p-3 bg-zinc-50 border border-zinc-200" >
< p class = "text-zinc-500" > 读取到歌曲总数 < / p >
< p class = "mt-1 text-lg font-bold text-zinc-900" > { { totalLoadedSongs } } 首 < / p >
< / div >
< div class = "rounded-xl p-3 bg-zinc-50 border border-zinc-200" >
< p class = "text-zinc-500" > 最近一次同步 < / p >
< p class = "mt-1 text-[13px] font-bold text-zinc-900 truncate" >
{ { lastSyncText } }
< / p >
< / div >
< / div >
< div class = "flex gap-2" >
< button
: disabled = "loadingSchedules || selectedDates.length === 0"
class = "h-[42px] px-4 rounded-xl text-[12px] font-bold bg-zinc-100 hover:bg-zinc-200 text-zinc-700 transition-colors inline-flex items-center gap-1.5 disabled:opacity-60"
@click ="loadSchedules"
>
< Icon name = "calendar" :size = "14" / >
{ { loadingSchedules ? '读取排期中...' : '读取所选日期排期' } }
< / button >
< button
class = "h-[42px] px-4 rounded-xl text-[12px] font-bold bg-zinc-100 hover:bg-zinc-200 text-zinc-700 transition-colors inline-flex items-center gap-1.5"
@click ="loadSyncHistory"
>
< Icon name = "list" :size = "14" / >
刷新历史
< / button >
< / div >
< / div >
<!-- 预览 -- >
< div v-if = "Object.keys(schedulesByDate).length > 0" class="mt-6 space-y-4" >
< div
v-for = "(items, date) in schedulesByDate"
:key = "String(date)"
class = "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm"
>
< div class = "flex items-center justify-between mb-3" >
< div >
< h4 class = "text-sm font-bold text-zinc-900" > { { date } } < / h4 >
< p class = "text-[11px] text-zinc-500" > 共 { { items . length } } 首已发布排期歌曲 < / p >
< / div >
< button
class = "h-[38px] px-3 rounded-xl text-[11px] font-bold bg-emerald-50 hover:bg-emerald-100 text-emerald-700 transition-colors inline-flex items-center gap-1.5 border border-emerald-100"
: disabled = "syncing || !(status?.bound?.hasToken || status?.bound?.hasCookie) || items.length === 0"
@click ="syncSingleDate(String(date))"
>
< Icon name = "play" :size = "13" / > 单独同步本日
< / button >
< / div >
< div class = "space-y-2 max-h-[260px] overflow-y-auto custom-scrollbar pr-1" >
< div
v-for = "(s, i) in items"
: key = "s.scheduleId ?? i"
class = "flex items-center gap-3 p-2.5 rounded-xl bg-zinc-50 border border-zinc-200"
>
< span class = "w-6 text-center text-[11px] font-bold text-indigo-600" > { { i + 1 } } < / span >
< img
v-if = "s.coverUrl || s.cover"
: src = "s.coverUrl || s.cover"
class = "w-9 h-9 rounded-lg object-cover bg-zinc-200 border border-zinc-200 shrink-0"
@error ="($event.target as any).style.display = 'none'"
/ >
< div
v-else
class = "w-9 h-9 rounded-lg bg-indigo-50 text-indigo-600 flex items-center justify-center border border-zinc-200 shrink-0"
>
< Icon name = "music" :size = "14" / >
< / div >
< div class = "min-w-0 flex-1" >
< p class = "text-[13px] font-semibold text-zinc-900 truncate" >
{ { s . title } }
< / p >
< p class = "text-[11px] text-zinc-500 truncate" >
{ { s . singer || s . artist || '未知歌手' } }
< / p >
< / div >
< span
v-if = "s.kgHash"
class = "text-[10px] font-bold text-emerald-700 bg-emerald-50 rounded-md px-1.5 py-0.5 border border-emerald-100"
title = "已绑定酷狗 hash,可精确添加"
>
KG
< / span >
< / div >
< / div >
< / div >
< / div >
< div
v-else-if = "!loadingSchedules && selectedDates.length > 0"
class = "mt-6 rounded-2xl border border-dashed border-zinc-300 py-14 flex flex-col items-center justify-center text-zinc-500"
>
< Icon name = "calendar-x" :size = "36" class = "mb-2 text-zinc-400" / >
< p class = "text-sm" > 所选日期还没有已发布排期歌曲 < / p >
< / div >
< / div >
<!-- 同步历史 -- >
< div class = "bg-white/90 border border-zinc-200 rounded-2xl p-6 shadow-sm" >
< div class = "flex items-center justify-between mb-4" >
< div class = "flex items-center gap-2" >
< div class = "w-9 h-9 rounded-xl bg-emerald-50 flex items-center justify-center text-emerald-600" >
< Icon name = "history" :size = "18" / >
< / div >
< div >
< h3 class = "text-base font-bold text-zinc-900" > 最近同步记录 < / h3 >
< p class = "text-[11px] text-zinc-500" > 最多显示 100 条 , 缺失歌曲可在 “ 详情 ” 里手动补录 kgHash 后重试 < / p >
< / div >
< / div >
< / div >
< div v-if = "historyLoading" class="space-y-2" >
< LoadingState :height = "96" / >
< / div >
< div v-else-if = "!syncHistory.length" class="text-center py-10 text-zinc-500 text-sm" >
暂无同步记录
< / div >
< div v-else class = "space-y-2 max-h-[420px] overflow-y-auto custom-scrollbar pr-1" >
< div
v-for = "h in syncHistory"
:key = "h.id"
class = "flex flex-col sm:flex-row sm:items-center gap-3 p-3 rounded-xl border border-zinc-200 bg-zinc-50"
>
< div class = "min-w-0 flex-1" >
< div class = "flex flex-wrap items-center gap-2" >
< span
: class = "[
'px-2 py-0.5 rounded-md text-[10px] font-bold border',
statusBadge(h.status).cls,
]"
>
{ { statusBadge ( h . status ) . text } }
< / span >
< span class = "text-[13px] font-bold text-zinc-900 truncate" >
{ { h . playlistName } }
< / span >
< a
v-if = "h.playlistUrl"
:href = "h.playlistUrl"
target = "_blank"
rel = "noopener noreferrer"
class = "text-[11px] text-indigo-600 hover:underline truncate"
>
在酷狗打开 →
< / a >
< / div >
< div class = "mt-1 text-[11px] text-zinc-500 flex flex-wrap gap-x-3 gap-y-1" >
< span > 排期 : { { formatDateOnly ( h . playDate ) } } < / span >
< span > 成功 { { h . addedSongs } } / { { h . totalSongs } } < / span >
< span v-if = "h.missingSongs?.length" class="text-amber-700" >
缺 {{ h.missingSongs.length }} 首
< / span >
< span > 触发 : { { formatDateTime ( h . createdAt ) } } < / span >
< / div >
< p v-if = "h.error" class="mt-1 text-[11px] text-red-600 whitespace-pre-wrap break-words" >
{{ h.error }}
< / p >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / template >
< script setup lang = "ts" >
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 { useAuth } from '~/composables/useAuth'
import { useToast } from '~/composables/useToast'
const auth = useAuth ( )
const toast = useToast ( )
const WEEKDAY = [ '周日' , '周一' , '周二' , '周三' , '周四' , '周五' , '周六' ]
const today = new Date ( )
const todayStr = ` ${ today . getFullYear ( ) } - ${ pad2 ( today . getMonth ( ) + 1 ) } - ${ pad2 ( today . getDate ( ) ) } `
const selectedDates = ref < string [ ] > ( [ todayStr ] )
const status = ref < any > ( null )
const schedulesByDate = ref < Record < string , any [ ] > > ( { } )
const syncHistory = ref < any [ ] > ( [ ] )
const loadingSchedules = ref ( false )
const historyLoading = ref ( false )
const syncing = ref ( false )
const qrLoading = ref ( false )
const qrOpen = ref ( false )
const qrImg = ref ( '' )
const qrContext = ref < any > ( null )
const qrHint = ref ( '' )
const qrState = ref < number > ( 0 )
const qrPollTimer = ref < any > ( null )
const totalLoadedSongs = computed (
( ) => Object . values ( schedulesByDate . value ) . reduce ( ( n , arr ) => n + arr . length , 0 ) ,
)
const lastSyncText = computed ( ( ) => {
if ( ! syncHistory . value . length ) return '暂无同步'
const first = syncHistory . value [ 0 ]
return formatDateTime ( first . createdAt )
} )
const qrStateColor = computed ( ( ) => {
const s = qrState . value
if ( s === 1 ) return 'text-emerald-600'
if ( s === 2 ) return 'text-amber-600'
if ( s === - 1 ) return 'text-red-600'
return 'text-zinc-500'
} )
function pad2 ( n : number ) {
return n < 10 ? '0' + n : '' + n
}
function formatDateTime ( s : any ) {
if ( ! s ) return '-'
const d = new Date ( s )
if ( Number . isNaN ( d . getTime ( ) ) ) return String ( s )
return ` ${ d . getFullYear ( ) } - ${ pad2 ( d . getMonth ( ) + 1 ) } - ${ pad2 ( d . getDate ( ) ) } ${ pad2 ( d . getHours ( ) ) } : ${ pad2 ( d . getMinutes ( ) ) } `
}
function formatDateOnly ( s : any ) {
if ( ! s ) return '-'
const d = new Date ( s )
if ( Number . isNaN ( d . getTime ( ) ) ) return String ( s )
return ` ${ d . getFullYear ( ) } - ${ pad2 ( d . getMonth ( ) + 1 ) } - ${ pad2 ( d . getDate ( ) ) } ( ${ WEEKDAY [ d . getDay ( ) ] } ) `
}
function statusBadge ( s : string ) {
switch ( s ) {
case 'success' :
return { cls : 'bg-emerald-50 text-emerald-700 border-emerald-200' , text : '同步成功' }
case 'partial' :
return { cls : 'bg-amber-50 text-amber-700 border-amber-200' , text : '部分歌曲缺失' }
case 'partial_add_failed' :
return { cls : 'bg-amber-50 text-amber-700 border-amber-200' , text : '添加歌曲失败' }
case 'failed_create' :
return { cls : 'bg-red-50 text-red-700 border-red-200' , text : '歌单创建失败' }
case 'skipped_empty' :
return { cls : 'bg-zinc-100 text-zinc-500 border-zinc-200' , text : '无排期跳过' }
case 'error' :
return { cls : 'bg-red-50 text-red-700 border-red-200' , text : '同步出错' }
default :
return { cls : 'bg-zinc-100 text-zinc-500 border-zinc-200' , text : '等待中' }
}
}
async function reloadStatus ( ) {
try {
const r = await $fetch ( '/api/admin/kugou/status' , { ... auth . getAuthConfig ( ) } )
status . value = r
} catch ( e : any ) {
toast . show ? . ( e ? . message || '状态刷新失败' , 'error' )
}
}
async function startQrLogin ( ) {
qrLoading . value = true
qrOpen . value = true
qrImg . value = ''
qrHint . value = ''
qrState . value = 0
qrContext . value = null
try {
const r = await $fetch ( '/api/admin/kugou/qr-create' , {
... auth . getAuthConfig ( ) ,
query : { platform : 'web' } ,
} )
const d = r ? . data
qrContext . value = d
const base64 = d ? . img || d ? . qrimg || d ? . base64 || ''
if ( base64 ) qrImg . value = base64 . startsWith ( 'data:image' ) ? base64 : ` data:image/png;base64, ${ base64 } `
else if ( d ? . qr _url ) qrImg . value = d . qr _url
startQrPoll ( )
} catch ( e : any ) {
qrHint . value = e ? . message || '二维码获取失败'
qrState . value = - 1
} finally {
qrLoading . value = false
}
}
function startQrPoll ( ) {
if ( qrPollTimer . value ) clearInterval ( qrPollTimer . value )
let count = 0
qrPollTimer . value = setInterval ( async ( ) => {
count ++
if ( count > 120 ) {
clearInterval ( qrPollTimer . value )
qrPollTimer . value = null
qrHint . value = '二维码已过期,请重试'
qrState . value = - 1
return
}
try {
const ctx = qrContext . value || { }
const r : any = await $fetch ( '/api/admin/kugou/qr-check' , {
method : 'POST' ,
... auth . getAuthConfig ( ) ,
body : {
qr _id : ctx . qr _id ,
login _id : ctx . login _id ,
key : ctx . key || ctx . login _id || ctx . qr _id ,
cookie : ctx . cookie || '' ,
} ,
} )
qrState . value = Number ( r ? . state ? ? 0 )
qrHint . value = r ? . hint || ''
if ( r ? . bound ) {
clearInterval ( qrPollTimer . value )
qrPollTimer . value = null
qrOpen . value = false
toast . show ? . ( '酷狗账号绑定成功' , 'success' )
await reloadStatus ( )
await loadSyncHistory ( )
} else if ( qrState . value === - 1 ) {
clearInterval ( qrPollTimer . value )
qrPollTimer . value = null
}
} catch ( e : any ) {
qrHint . value = e ? . message || '轮询失败'
}
} , 2500 )
}
function closeQr ( ) {
if ( qrPollTimer . value ) clearInterval ( qrPollTimer . value )
qrPollTimer . value = null
qrOpen . value = false
qrImg . value = ''
qrContext . value = null
}
async function handleUnbind ( ) {
if ( ! window . confirm ( '确定解除酷狗管理员账号绑定吗?' ) ) return
try {
await $fetch ( '/api/admin/kugou/unbind' , { method : 'POST' , ... auth . getAuthConfig ( ) } )
toast . show ? . ( '已解除酷狗账号绑定' , 'success' )
await reloadStatus ( )
} catch ( e : any ) {
toast . show ? . ( e ? . message || '解除绑定失败' , 'error' )
}
}
async function loadSchedules ( ) {
if ( ! selectedDates . value . length ) {
schedulesByDate . value = { }
return
}
loadingSchedules . value = true
const out : Record < string , any [ ] > = { }
try {
const sorted = [ ... selectedDates . value ] . sort ( )
for ( const d of sorted ) {
try {
const r : any = await $fetch ( '/api/admin/schedule/full' , {
... auth . getAuthConfig ( ) ,
query : { date : d , includeDrafts : 'false' } ,
} )
const list : any [ ] = r ? . success ? r ? . data ? . schedules ? ? [ ] : [ ]
const normalized = list
. filter ( ( s ) => ! s . isDraft )
. map ( ( s ) => {
const song = s . song || s
return {
scheduleId : s . id ,
sequence : s . sequence ,
title : song ? . title ,
singer : song ? . singer || song ? . artist ,
artist : song ? . artist || song ? . singer ,
kgHash : song ? . kgHash || song ? . kg _hash ,
musicId : song ? . musicId ,
coverUrl : song ? . coverUrl || song ? . cover _url ,
cover : song ? . cover ,
}
} )
if ( normalized . length ) out [ d ] = normalized
} catch { }
}
schedulesByDate . value = out
if ( ! Object . keys ( out ) . length ) toast . show ? . ( '所选日期没有已发布排期歌曲' , 'info' )
} catch ( e : any ) {
toast . show ? . ( e ? . message || '读取排期失败' , 'error' )
} finally {
loadingSchedules . value = false
}
}
async function syncToKugou ( dates ? : string [ ] ) {
const useDates = dates && dates . length ? dates : selectedDates . value
if ( ! useDates . length ) {
toast . show ? . ( '请先选择排期日期' , 'warning' )
return
}
syncing . value = true
try {
const r : any = await $fetch ( '/api/admin/kugou/sync-schedule-playlists' , {
method : 'POST' ,
... auth . getAuthConfig ( ) ,
body : { dates : useDates } ,
} )
const results : any [ ] = r ? . results || [ ]
const ok = results . filter ( ( x ) => x . status === 'success' ) . length
const partial = results . filter ( ( x ) => x . status === 'partial' ) . length
const fail = results . length - ok - partial
toast . show ? . (
` 同步完成:成功 ${ ok } 个,部分 ${ partial } 个,失败 ${ fail } 个 ` ,
fail ? 'error' : partial ? 'warning' : 'success' ,
)
await loadSyncHistory ( )
} catch ( e : any ) {
toast . show ? . ( e ? . message || '同步失败' , 'error' )
} finally {
syncing . value = false
}
}
async function syncSingleDate ( date : string ) {
await syncToKugou ( [ date ] )
}
async function loadSyncHistory ( ) {
historyLoading . value = true
try {
const r : any = await $fetch ( '/api/admin/kugou/sync-history' , { ... auth . getAuthConfig ( ) } )
syncHistory . value = r ? . rows || [ ]
} catch ( e : any ) {
toast . show ? . ( e ? . message || '读取同步历史失败' , 'error' )
} finally {
historyLoading . value = false
}
}
watch (
( ) => selectedDates . value . slice ( ) . sort ( ) . join ( ',' ) ,
( ) => loadSchedules ( ) ,
)
onMounted ( async ( ) => {
await reloadStatus ( )
await Promise . all ( [ loadSchedules ( ) , loadSyncHistory ( ) ] )
} )
< / script >