- style(theme): 修复浅色模式不生效问题,为 ClassicLight 和 ModernLight 增加独立差异化 CSS 变量定义(主色/圆角/阴影/背景对比度) - feat(blacklist): 后端放开 LANGUAGE/GENRE 类型校验;check API 新增语种(CJK/拉丁/假名/谚文/西里尔/阿拉伯)和曲风关键词匹配逻辑;前端 BlacklistManager UI 增加 语种/曲风 四类型切换、类型筛选、图标与标签颜色映射、输入提示 - fix(DataAnalysisPanel): 活跃用户列表容器加 max-h-[400px] + overflow-y-auto + 底部渐变遮罩;tooltip 加 max-height + truncate 防止超高;头像失败回退首字母 - feat(admin): 新增 user-avatar.ts 工具(OAuth→Gravatar→DiceBear 三级兜底);active-users/realtime/users 列表返回 avatarUrl;DataAnalysisPanel/UserManager 展示圆形头像;权限分级 ROLE_LEVELS,SUPER_ADMIN 全权限,ADMIN 仅可管理 ≤ USER 级用户的密码与角色,前端按钮联动禁用 - fix+style(schedule): ScheduleManager 标签页 sticky min-h 防抖、间距调优、表单分组、移动端 padding 统一 - feat(schedule): 自动排期新增「按投稿人数优先覆盖」策略(算法:requester + collaborators 总数降序),支持 CustomSelect 切换;新增 localStorage voicehub:schedule:auto-config 持久化 7 项配置(策略/时段/年级/排序/标签/搜索/学期)500ms debounce - fix(schedule): songs/index.get 新增 usePool 参数,从 schedule_song_pool(status=PENDING,targetPlayDate IS NULL) 取歌并去重,排除已排期 songs.played=false;schedule.post 和 draft-bulk 分配后更新池状态为 ASSIGNED 避免重复分配 - feat(player): ScheduleManager 新增「构建播放队列」按钮,按 playDate→时段→sequence 排序后经 useAudioPlayer.playSong 整列载入并 toast 反馈 - fix(db): db.ts 检测 NETLIFY/VERCEL serverless 环境使用最小池 max=1 idle_timeout=1 keep_alive=false,Node Server 保持 max=10 - feat(site-config): site-config.get.ts 与 useSiteConfig.js 导出 statisticsCode 和 statisticsCodeEnabled;index.vue 挂载后解析 HTML 动态重插 script 元素确保第三方统计执行 - fix(schedule-printer): ScheduleItemPrint 封面 aspect-square object-cover;print-fix.css 打印时强制 48px×48px contain、去除圆角阴影 - fix(songRequest): 新增 server/utils/url.ts normalizeCoverUrl(trim/去零宽空格/相对协议补 https/非 http 丢弃/URL.parse 兜底);request.post/add.post/import.post 三处写入 cover/coverUrl 前统一规范化
329 lines
6.6 KiB
Vue
329 lines
6.6 KiB
Vue
<template>
|
||
<div class="schedule-item-print">
|
||
<div class="item-content">
|
||
<!-- 序号 -->
|
||
<div v-if="settings.showSequence" class="sequence-number">
|
||
{{ schedule.sequence || 1 }}
|
||
</div>
|
||
|
||
<!-- 歌曲封面 -->
|
||
<div v-if="settings.showCover" class="cover-section aspect-square">
|
||
<img
|
||
v-if="schedule.song.cover"
|
||
:alt="schedule.song.title"
|
||
:src="convertToHttps(schedule.song.cover)"
|
||
class="song-cover object-cover"
|
||
referrerpolicy="no-referrer"
|
||
@error="handleImageError"
|
||
>
|
||
<div class="cover-placeholder object-contain" :class="{ 'show': !schedule.song.cover }">
|
||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<circle cx="12" cy="12" r="3" />
|
||
<path d="M12 1v6m0 6v6" />
|
||
<path d="m21 12-6-3-6 3-6-3" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 歌曲信息 -->
|
||
<div class="song-info">
|
||
<div v-if="settings.showTitle" class="song-title">
|
||
{{ schedule.song.title }}
|
||
<!-- 重播标识 -->
|
||
<span v-if="schedule.song.replayRequestCount > 0" class="replay-badge-print"> 重播 </span>
|
||
</div>
|
||
<div v-if="settings.showArtist" class="song-artist">
|
||
{{ schedule.song.artist }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 投稿人信息(重播歌曲不显示申请人,只显示原投稿人) -->
|
||
<div v-if="settings.showRequester" class="requester-info">
|
||
<span class="label">投稿人:</span>
|
||
<span class="value">
|
||
{{ schedule.song.requester }}
|
||
<span v-if="schedule.song.collaborators && schedule.song.collaborators.length > 0">
|
||
& {{ schedule.song.collaborators.map((c) => c.displayName || c.name).join(' & ') }}
|
||
</span>
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 人数信息 -->
|
||
<div v-if="settings.showVotes" class="votes-info">
|
||
<span v-if="schedule.song.replayRequestCount > 0" class="label">申请重播:</span>
|
||
<span v-else class="label">热度:</span>
|
||
<span class="value">{{
|
||
schedule.song.replayRequestCount > 0
|
||
? schedule.song.replayRequestCount + '人'
|
||
: schedule.song.voteCount || 0
|
||
}}</span>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { defineProps } from 'vue'
|
||
import { convertToHttps } from '~/utils/url'
|
||
|
||
// 定义props
|
||
defineProps({
|
||
schedule: {
|
||
type: Object,
|
||
required: true
|
||
},
|
||
settings: {
|
||
type: Object,
|
||
required: true
|
||
}
|
||
})
|
||
|
||
// 处理图片加载错误
|
||
const handleImageError = (event) => {
|
||
event.target.style.display = 'none'
|
||
event.target.nextElementSibling?.classList.add('show')
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.schedule-item-print {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
page-break-inside: avoid;
|
||
}
|
||
|
||
.item-content {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
gap: 12px;
|
||
}
|
||
|
||
.sequence-number {
|
||
width: 30px;
|
||
height: 30px;
|
||
background: #f0f0f0;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
color: #333;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.cover-section {
|
||
width: 40px;
|
||
height: 40px;
|
||
aspect-ratio: 1 / 1;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.song-cover {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
border-radius: 4px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.cover-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
aspect-ratio: 1 / 1;
|
||
object-fit: contain;
|
||
background: #f5f5f5;
|
||
border-radius: 4px;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #999;
|
||
}
|
||
|
||
.cover-placeholder.show {
|
||
display: flex;
|
||
}
|
||
|
||
.cover-placeholder svg {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.song-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.song-title {
|
||
font-weight: bold;
|
||
font-size: 16px;
|
||
color: #333;
|
||
margin-bottom: 2px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
/* 打印用重播标识 */
|
||
.replay-badge-print {
|
||
display: inline-block;
|
||
padding: 1px 4px;
|
||
background: #e3f2fd;
|
||
border: 1px solid #2196f3;
|
||
border-radius: 3px;
|
||
color: #1976d2;
|
||
font-size: 10px;
|
||
font-weight: bold;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.song-artist {
|
||
font-size: 14px;
|
||
color: #666;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.requester-info,
|
||
.votes-info {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
color: #666;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.label {
|
||
font-weight: 500;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.value {
|
||
color: #333;
|
||
}
|
||
|
||
/* 打印样式 */
|
||
@media print {
|
||
.schedule-item-print {
|
||
color: #000 !important;
|
||
background: #fff !important;
|
||
width: 100% !important;
|
||
max-width: none !important;
|
||
box-sizing: border-box !important;
|
||
}
|
||
|
||
.item-content {
|
||
display: flex !important;
|
||
flex-direction: row !important;
|
||
align-items: center !important;
|
||
width: 100% !important;
|
||
gap: 12px !important;
|
||
}
|
||
|
||
.sequence-number {
|
||
background: #f0f0f0 !important;
|
||
color: #000 !important;
|
||
width: 30px !important;
|
||
height: 30px !important;
|
||
flex-shrink: 0 !important;
|
||
}
|
||
|
||
.cover-section {
|
||
width: 48px !important;
|
||
height: 48px !important;
|
||
aspect-ratio: 1 / 1 !important;
|
||
flex-shrink: 0 !important;
|
||
}
|
||
|
||
.song-cover {
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
object-fit: contain !important;
|
||
border-radius: 0 !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
.cover-placeholder {
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
object-fit: contain !important;
|
||
border-radius: 0 !important;
|
||
}
|
||
|
||
.song-info {
|
||
flex: 1 !important;
|
||
min-width: 0 !important;
|
||
}
|
||
|
||
.song-title {
|
||
color: #000 !important;
|
||
font-size: 16px !important;
|
||
white-space: nowrap !important;
|
||
overflow: hidden !important;
|
||
text-overflow: ellipsis !important;
|
||
}
|
||
|
||
.song-artist,
|
||
.label,
|
||
.value {
|
||
color: #333 !important;
|
||
}
|
||
|
||
.time-range {
|
||
color: #666 !important;
|
||
}
|
||
|
||
.requester-info,
|
||
.votes-info,
|
||
.playtime-info {
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
font-size: 12px !important;
|
||
white-space: nowrap !important;
|
||
flex-shrink: 0 !important;
|
||
}
|
||
}
|
||
|
||
/* 紧凑模式 */
|
||
.compact .schedule-item-print {
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.compact .item-content {
|
||
gap: 8px;
|
||
}
|
||
|
||
.compact .sequence-number {
|
||
width: 24px;
|
||
height: 24px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.compact .cover-section {
|
||
width: 32px;
|
||
height: 32px;
|
||
}
|
||
|
||
.compact .song-title {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.compact .song-artist {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.compact .requester-info,
|
||
.compact .votes-info,
|
||
.compact .playtime-info {
|
||
font-size: 11px;
|
||
}
|
||
</style>
|