Files
BYW-VoiceHub/old_playlist.vue
T
Henry_Du 3eff710d5d feat(微信订阅): 接入歌曲排期模板并加固部署流程
订阅消息(小程序侧唯一能主动触达用户的通道):
- 新增 server/utils/wechatSubscribeFields.ts:按微信字段类型裁剪值(thing≤20、
  phrase 必须 5 个汉字、time/date/character_string 各自上限),空白压成一行,
  北京时间 time 由 playTime 的开始时间生成,解析不出退化为当天 00:00。
- wechatSubscribeService 内置 SONG_SELECTED 模板(站长新给的那个模板 ID),
  并转发上述纯函数;notificationService.createSongSelectedNotification 现在会带上
  playDate/playTime/sequence,三个排期接口(schedule / publish / bulk-publish)补传 sequence。
- 新增 test/wechatSubscribeFields.test.mjs(10 例,node --test 全绿)。
- test-subscribe 管理接口支持 vars 覆盖并返回真实发送值,方便站长核对字段名。

部署流程(Gitea Actions 容器里没有 buildx 插件,旧流程在此硬失败):
- 每个 job 各自安装/探测 docker-buildx(v0.19.3,走 gh-proxy 再退直连),
  不把 `buildx version` 当唯一依据;装不上时降级为普通 `docker build` 继续发布,
  不再因为缺插件整条流水线失败。

注意:SONG_SELECTED 的字段名(thing2/date4/time1/character_string3/phrase5)是按常见
命名推断的,需在公众平台后台核对;不一致时用环境变量覆盖,模板 ID 变更后老用户需重新授权。
2026-10-11 00:04:14 +08:00

115 lines
4.6 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>
<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-100">鍥剧墖姝屽崟</h2>
<p class="text-sm text-zinc-500">閫夋嫨鏃ユ湡鐢熸垚骞挎挱鎺掓湡鍗$墖骞跺垎浜?/p>
</div>
<div class="flex flex-col gap-8">
<!-- 璁剧疆闈㈡澘 -->
<div class="w-full flex flex-col gap-6">
<div class="bg-zinc-900/40 border border-zinc-800 rounded-xl p-6 space-y-6 shadow-lg">
<h3 class="text-lg font-bold flex items-center gap-2 text-zinc-100 shrink-0">
<Layout class="w-4 h-4 text-blue-500" /> 鍗$墖璁剧疆
</h3>
<div class="flex flex-col gap-5 items-end">
<!-- 鏃ユ湡閫夋嫨 -->
<div class="space-y-2 flex-1 w-full">
<label class="text-[11px] font-black uppercase text-zinc-600 tracking-wider">鎺掓湡鏃ユ湡</label>
<!-- 绫讳技鎺掓湡绠$悊鐨勬棩鏈熼€夋嫨鍣?-->
<AdminDatePicker
:modelValue="selectedDate"
@update:modelValue="selectedDate = $event"
@change="fetchSchedules"
/>
</div>
<button
:disabled="loading || schedules.length === 0"
class="w-full py-3 h-[54px] 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 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 shrink-0"
@click="showModal = true"
>
<ImageIcon class="w-4 h-4" />
{{ loading ? '鍔犺浇涓?..' : (schedules.length === 0 ? '璇ユ棩鏈熸棤鎺掓湡' : '鐢熸垚鍗$墖') }}
</button>
</div>
</div>
</div>
<!-- 棰勮鍖?-->
<div class="w-full flex flex-col gap-4">
<div v-if="schedules.length > 0" class="bg-zinc-900/40 border border-zinc-800 rounded-xl p-6 shadow-lg">
<h3 class="text-lg font-bold text-zinc-100 mb-4">鎺掓湡棰勮 ({{ schedules.length }} 棣?</h3>
<div class="space-y-3">
<div v-for="(schedule, index) in schedules" :key="schedule.id" class="flex items-center gap-3 p-3 bg-zinc-950/50 border border-zinc-800/50 rounded-lg">
<span class="text-blue-500 font-bold w-6">{{ index + 1 }}.</span>
<div class="flex-1 overflow-hidden">
<p class="text-sm text-zinc-200 font-medium truncate">{{ schedule.song?.title || schedule.title }}</p>
<p class="text-xs text-zinc-500 truncate">{{ schedule.song?.artist || schedule.artist }}</p>
</div>
</div>
</div>
</div>
<div v-else-if="!loading" class="flex flex-col items-center justify-center py-20 bg-zinc-900/20 border border-zinc-800/50 rounded-xl border-dashed">
<CalendarDays class="w-12 h-12 text-zinc-700 mb-4" />
<p class="text-zinc-500">璇ユ棩鏈熸病鏈夋帓鏈熺殑姝屾洸</p>
</div>
</div>
</div>
<AdminPlaylistCardGenerator
:show="showModal"
:songs="schedules"
:default-title="`${selectedDate} 鏍″洯骞挎挱姝屽崟`"
@close="showModal = false"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Layout, Image as ImageIcon, CalendarDays } from '@lucide/vue'
import AdminDatePicker from './AdminDatePicker.vue'
import AdminPlaylistCardGenerator from './PlaylistCardGenerator.vue'
import { useAuth } from '~/composables/useAuth'
const auth = useAuth()
const selectedDate = ref(new Date().toISOString().split('T')[0])
const schedules = ref([])
const loading = ref(false)
const showModal = ref(false)
const fetchSchedules = async () => {
loading.value = true
try {
const response = await $fetch('/api/admin/schedule/full', {
...auth.getAuthConfig(),
query: {
date: selectedDate.value,
includeDrafts: 'true'
}
})
if (response.success && response.data && response.data.schedules) {
schedules.value = response.data.schedules
} else {
schedules.value = []
}
} catch (error) {
console.error('Failed to fetch schedules:', error)
if (window.$showNotification) {
window.$showNotification('鑾峰彇鎺掓湡澶辫触', 'error')
}
} finally {
loading.value = false
}
}
onMounted(() => {
fetchSchedules()
})
</script>