Files
BYW-VoiceHub/app/components/Admin/PlaylistCardGenerator.vue
T
Henry_Du 4ce4649aaf
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
初次提交
2026-09-06 08:19:02 +08:00

247 lines
7.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<Transition name="fade">
<div
v-if="show"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
@click="$emit('close')"
>
<Transition name="scale">
<div
v-if="show"
class="bg-zinc-900 border border-zinc-800 rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden flex flex-col"
@click.stop
>
<div class="flex items-center justify-between p-4 border-b border-zinc-800">
<h3 class="text-sm font-black text-zinc-100 uppercase tracking-widest">生成歌单卡片</h3>
<button
class="text-zinc-500 hover:text-zinc-300 transition-colors"
@click="$emit('close')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="p-6 flex flex-col items-center gap-4">
<!-- 标题输入 -->
<div class="w-full space-y-2">
<label class="text-[11px] font-black uppercase text-zinc-600 tracking-wider">歌单标题</label>
<input
v-model="cardTitle"
type="text"
placeholder="例如: 2026-03-21 校园午间广播"
class="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-300 focus:outline-none focus:border-blue-500/50 transition-colors"
@input="drawCanvas"
>
</div>
<!-- Canvas 预览 -->
<div class="relative w-full aspect-[2/3] max-w-[280px] bg-zinc-950 rounded-xl overflow-hidden border border-zinc-800 shadow-inner flex items-center justify-center">
<canvas ref="canvasRef" width="400" :height="canvasHeight" class="w-full h-full object-contain bg-white"></canvas>
</div>
<p class="text-[10px] text-zinc-500 text-center">宽度 400 像素,高度自适应,适合在手机端查看和分享</p>
</div>
<div class="p-4 border-t border-zinc-800 bg-zinc-900/50 flex gap-3">
<button
class="flex-1 py-2.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-xs font-bold rounded-xl transition-colors uppercase tracking-wider"
@click="$emit('close')"
>
取消
</button>
<button
class="flex-1 py-2.5 bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-blue-900/20 transition-colors uppercase tracking-wider"
@click="downloadCard"
>
下载图片
</button>
</div>
</div>
</Transition>
</div>
</Transition>
</template>
<script setup>
import { ref, onMounted, watch, nextTick, computed } from 'vue'
const props = defineProps({
show: Boolean,
songs: {
type: Array,
default: () => []
},
defaultTitle: {
type: String,
default: '校园广播歌单'
}
})
const emit = defineEmits(['close'])
const canvasRef = ref(null)
const cardTitle = ref(props.defaultTitle)
const aiPrompt = ref('anime style school campus sunset beautiful sky highly detailed')
const isGeneratingAi = ref(false)
const bgImageObj = ref(null)
const maxDisplaySongs = 35
const canvasHeight = computed(() => {
const count = Math.min(props.songs.length, maxDisplaySongs)
if (count <= 10) return 600
// 每多一首增加 40px 高度
return 600 + (count - 10) * 40
})
const drawCanvas = () => {
if (!canvasRef.value) return
const canvas = canvasRef.value
const ctx = canvas.getContext('2d')
if (!ctx) return
const height = canvasHeight.value
// 清空画布
if (bgImageObj.value) {
ctx.drawImage(bgImageObj.value, 0, 0, 400, height)
// 增加一层半透明黑色遮罩,确保文字和列表框可见
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)'
ctx.fillRect(0, 0, 400, height)
} else {
ctx.fillStyle = '#F8FAFC' // Background color
ctx.fillRect(0, 0, 400, height)
}
// 绘制渐变头部
const gradient = ctx.createLinearGradient(0, 0, 400, 0)
if (bgImageObj.value) {
gradient.addColorStop(0, 'rgba(0, 0, 0, 0.7)')
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.4)')
} else {
gradient.addColorStop(0, '#2563eb') // Primary Blue
gradient.addColorStop(1, '#3b82f6')
}
ctx.fillStyle = gradient
ctx.fillRect(0, 0, 400, 100)
// 绘制标题
ctx.fillStyle = '#FFFFFF'
ctx.font = 'bold 24px "Inter", "system-ui", sans-serif'
ctx.textAlign = 'center'
ctx.fillText(cardTitle.value || '校园广播歌单', 200, 50)
// 绘制副标题(日期)
ctx.font = '14px "Inter", "system-ui", sans-serif'
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
ctx.fillText(new Date().toLocaleDateString('zh-CN'), 200, 80)
// 绘制列表背景
if (bgImageObj.value) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)' // 半透明白色背景
} else {
ctx.fillStyle = '#FFFFFF'
}
ctx.shadowColor = 'rgba(0,0,0,0.05)'
ctx.shadowBlur = 10
ctx.shadowOffsetY = 4
ctx.beginPath()
ctx.roundRect(20, 120, 360, height - 180, 12)
ctx.fill()
ctx.shadowColor = 'transparent'
// 绘制歌曲列表
ctx.textAlign = 'left'
let y = 160
const displaySongs = props.songs.slice(0, maxDisplaySongs) // 最多显示 35 首
displaySongs.forEach((schedule, index) => {
const song = schedule.song || schedule
// 序号
ctx.fillStyle = '#2563eb'
ctx.font = 'bold 16px "Inter", "system-ui", sans-serif'
ctx.fillText(`${index + 1}.`, 40, y)
// 歌名
ctx.fillStyle = '#0F172A' // Neutral Dark
ctx.font = 'bold 16px "Inter", "system-ui", sans-serif'
// 限制歌名长度
let title = song.title || '未知歌曲'
if (title.length > 12) title = title.substring(0, 12) + '...'
ctx.fillText(title, 70, y)
// 歌手
ctx.fillStyle = '#64748B'
ctx.font = '14px "Inter", "system-ui", sans-serif'
let artist = song.artist || '未知歌手'
if (artist.length > 10) artist = artist.substring(0, 10) + '...'
ctx.fillText(`- ${artist}`, 240, y)
y += 40
})
if (props.songs.length > maxDisplaySongs) {
ctx.fillStyle = '#94A3B8'
ctx.font = '14px "Inter", "system-ui", sans-serif'
ctx.textAlign = 'center'
ctx.fillText(`...及其他 ${props.songs.length - maxDisplaySongs} 首歌曲`, 200, y + 10)
}
// 绘制底部水印
if (bgImageObj.value) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'
} else {
ctx.fillStyle = '#CBD5E1'
}
ctx.font = '12px "Inter", "system-ui", sans-serif'
ctx.textAlign = 'center'
ctx.fillText('Generated by VoiceHub', 200, height - 20)
}
const downloadCard = () => {
if (!canvasRef.value) return
const dataUrl = canvasRef.value.toDataURL('image/png')
const link = document.createElement('a')
link.download = `歌单卡片_${new Date().toISOString().split('T')[0]}.png`
link.href = dataUrl
link.click()
if (window.$showNotification) {
window.$showNotification('图片导出成功', 'success')
}
}
watch(() => props.show, (newVal) => {
if (newVal) {
cardTitle.value = props.defaultTitle
nextTick(() => {
drawCanvas()
})
}
})
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.scale-enter-active,
.scale-leave-active {
transition: transform 0.2s ease, opacity 0.2s ease;
}
.scale-enter-from,
.scale-leave-to {
transform: scale(0.95);
opacity: 0;
}
</style>