diff --git a/hunliji-api/hl-api b/hunliji-api/hl-api index d7d8ddb..200e855 100644 Binary files a/hunliji-api/hl-api and b/hunliji-api/hl-api differ diff --git a/hunliji-api/main.go b/hunliji-api/main.go index 497e26b..b370aa3 100644 --- a/hunliji-api/main.go +++ b/hunliji-api/main.go @@ -63,7 +63,7 @@ type Danmaku struct { ID uint `gorm:"primaryKey;comment:主键ID" json:"id"` Name string `gorm:"column:name;size:50;not null;default:'';comment:发送者姓名" json:"name"` Content string `gorm:"column:content;type:text;not null;comment:祝福内容" json:"content"` - Color string `gorm:"column:color;size:20;not null;default:champagne;comment:弹幕颜色key" json:"color"` + Color string `gorm:"column:color;size:32;not null;default:champagne;comment:弹幕颜色key" json:"color"` Source string `gorm:"column:source;size:20;not null;default:danmaku;comment:来源 danmaku|rsvp" json:"source"` RsvpID *uint `gorm:"column:rsvp_id;comment:关联回执ID" json:"rsvp_id"` Status string `gorm:"column:status;size:20;not null;default:pending;index;comment:审核状态 pending|approved|rejected" json:"status"` @@ -116,15 +116,21 @@ func saveModerationCache(typ, content, status, reason string) { } var danmakuColorWhitelist = map[string]struct{}{ - "champagne": {}, - "blush": {}, - "apricot": {}, - "gold": {}, - "lilac": {}, - "sky": {}, - "mauve": {}, - "slate": {}, - "ink": {}, + "champagne": {}, + "blush": {}, + "apricot": {}, + "gold": {}, + "lilac": {}, + "sky": {}, + "mauve": {}, + "slate": {}, + "ink": {}, + "gradSunset": {}, + "gradChampagne": {}, + "gradBlush": {}, + "gradOcean": {}, + "gradAurora": {}, + "gradEmber": {}, } func normalizeDanmakuColor(color string) string { @@ -636,7 +642,6 @@ func main() { return } var body struct { - Name string `json:"name"` Style string `json:"style"` } if err := c.ShouldBindJSON(&body); err != nil { @@ -647,7 +652,7 @@ func main() { c.JSON(400, gin.H{"error": "请选择祝福风格"}) return } - text, err := sparkClient.GenerateBlessing(body.Name, body.Style) + text, err := sparkClient.GenerateBlessing(body.Style) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return diff --git a/hunliji-api/spark/blessing.go b/hunliji-api/spark/blessing.go index 10f6ae7..cf98398 100644 --- a/hunliji-api/spark/blessing.go +++ b/hunliji-api/spark/blessing.go @@ -18,30 +18,34 @@ func NormalizeStyle(style string) (string, bool) { return s, ok } -func (c *Client) GenerateBlessing(name, style string) (string, error) { +func (c *Client) GenerateBlessing(style string) (string, error) { styleKey, ok := NormalizeStyle(style) if !ok { return "", fmt.Errorf("无效的祝福风格") } label := BlessingStyles[styleKey] - guest := strings.TrimSpace(name) - guestHint := "宾客未署名" - if guest != "" { - guestHint = "宾客姓名:" + guest - } styleGuide := map[string]string{ - "classic": "用文言雅致、含蓄温润的古风表达,可适度用典故意象,忌生僻堆砌。", - "modern": "用简洁真诚的现代汉语,自然得体,适合婚礼现场朗读。", - "funny": "轻松幽默、俏皮可爱,但保持祝福善意,不低俗。", - "roast": "损友互怼口吻,先轻度调侃再真心祝福,尺度友好不人身攻击。", + "classic": "文言雅致、含蓄温润;可点到意象(花月、琴瑟、白首),忌生僻堆砌与整段文言文。", + "modern": "口语真诚、干净利落;像朋友当面说的话,不鸡汤、不口号堆叠。", + "funny": "俏皮轻松、会心一笑;可玩梗但保持善意,忌低俗与尴尬烂梗。", + "roast": "损友口吻:先轻怼一句再落真心祝福;尺度友好,不人身攻击、不揭隐私。", } - sys := "你是婚礼祝福文案助手。只输出一条中文祝福正文,不要标题、不要解释、不要引号包裹。" - user := fmt.Sprintf(`请写一条婚礼祝福。 -风格:%s(%s) -%s -要求:30到60个汉字;积极祝福新人;不要出现违法、色情、歧视或辱骂内容。`, label, styleGuide[styleKey], guestHint) + sys := `你是婚礼短祝福文案助手。 +只输出一条可直接填写的中文祝福正文。 +禁止:标题、序号、引号包裹、解释说明、表情符号、英文、话题标签。 +禁止:出现任何真实姓名、网名、称呼占位(如某某、X先生/女士)、新郎新娘具体姓氏。 +称呼统一用「你们」「新人」等泛称即可。 +文案要有意境:有画面感与余韵,可用轻巧意象(光、风、茶、路、灯火、四季等)点染,忌空泛套话与意象堆砌。` + + user := fmt.Sprintf(`请写一条送给婚礼新人的短祝福。 +风格:%s +写法:%s +意境:先落一两处可感的画面或氛围,再落到真诚祝福,读完有余味。 +篇幅:28~48个汉字,一两句说完。 +内容:祝福婚姻美满、相守与日常,语气自然,适合弹幕或回执留言。 +请直接输出祝福正文。`, label, styleGuide[styleKey]) return c.Chat([]Message{ {Role: "system", Content: sys}, diff --git a/vite-tailwindcss/src/api/wedding.js b/vite-tailwindcss/src/api/wedding.js index 334f044..fa07756 100644 --- a/vite-tailwindcss/src/api/wedding.js +++ b/vite-tailwindcss/src/api/wedding.js @@ -88,8 +88,8 @@ export function submitDanmaku(form) { return service.post('/danmaku', form) } -export function generateAiBlessing({ name, style }) { - return service.post('/ai/blessing', { name, style }, { timeout: 45000 }) +export function generateAiBlessing({ style }) { + return service.post('/ai/blessing', { style }, { timeout: 45000 }) } export function getDanmakuList(params) { diff --git a/vite-tailwindcss/src/components/DanmakuLayer.vue b/vite-tailwindcss/src/components/DanmakuLayer.vue index 7de42e4..fa33374 100644 --- a/vite-tailwindcss/src/components/DanmakuLayer.vue +++ b/vite-tailwindcss/src/components/DanmakuLayer.vue @@ -4,13 +4,18 @@ v-for="row in rows" :key="row.id" class="danmaku-item" + :class="{ 'danmaku-item--grad': row.textLight }" :style="row.style" @animationend="onEnded(row.id)" > {{ row.name }} {{ row.content }} - {{ row.timeLabel }} + {{ row.timeLabel }} @@ -29,7 +34,9 @@ const props = defineProps({ const POLL_MS = 30000 const DEDUPE_MS = 30000 -const TRACKS = 6 +const TRACKS = 5 +/** 同轨占用结束前不可再发,避免重叠 */ +const TRACK_GAP_MS = 420 const items = ref([]) const rows = ref([]) @@ -41,6 +48,43 @@ let uid = 0 const lastShownAt = new Map() /** 本批已飘过的业务弹幕 id */ const shownIds = new Set() +/** 各轨道最早可再发时间(performance.now) */ +const trackFreeAt = Array.from({ length: TRACKS }, () => 0) + +const resetTracks = () => { + for (let i = 0; i < TRACKS; i += 1) trackFreeAt[i] = 0 +} + +/** @returns {{ track: number, extraDelay: number } | null} */ +const reserveTrack = (durationSec, delaySec, { force = false } = {}) => { + const now = performance.now() + let free = -1 + let soonest = 0 + let soonestAt = Infinity + for (let i = 0; i < TRACKS; i += 1) { + if (trackFreeAt[i] <= now) { + free = i + break + } + if (trackFreeAt[i] < soonestAt) { + soonestAt = trackFreeAt[i] + soonest = i + } + } + + if (free >= 0) { + const start = now + delaySec * 1000 + trackFreeAt[free] = start + durationSec * 1000 + TRACK_GAP_MS + return { track: free, extraDelay: 0 } + } + + if (!force) return null + + const extraDelay = Math.max(0, (soonestAt - now) / 1000) + const start = soonestAt + delaySec * 1000 + trackFreeAt[soonest] = start + durationSec * 1000 + TRACK_GAP_MS + return { track: soonest, extraDelay } +} const itemKey = (item) => { if (item?.id != null) return String(item.id) @@ -90,14 +134,21 @@ const markShown = (item) => { const buildRow = (item, { force = false } = {}) => { const key = itemKey(item) if (!force && isCooling(key)) return null + + // 速度区间收窄,降低同屏追尾概率;轨道占用保证同轨不叠 + const duration = 12 + Math.random() * 4 + const delay = force ? 0 : Math.random() * 0.25 + const slot = reserveTrack(duration, delay, { force }) + if (!slot) return null + lastShownAt.set(key, Date.now()) markShown(item) const tint = resolveDanmakuTint(item.color) - const track = Math.floor(Math.random() * TRACKS) - const duration = 10 + Math.random() * 8 - const delay = force ? 0 : Math.random() * 0.4 const id = ++uid + const totalDelay = delay + slot.extraDelay + // 5 轨均匀分布在上半屏,轨间距约 14% + const topPct = 7 + slot.track * 14 const timeLabel = force ? '刚刚' @@ -108,13 +159,15 @@ const buildRow = (item, { force = false } = {}) => { name: item.name, content: item.content, color: resolveDanmakuColor(item.color), + textLight: !!tint.textLight, timeLabel, style: { - top: `${8 + track * 12}%`, + top: `${topPct}%`, animationDuration: `${duration}s`, - animationDelay: `${delay}s`, + animationDelay: `${totalDelay}s`, background: tint.background, borderColor: tint.borderColor, + boxShadow: tint.boxShadow, }, } } @@ -156,7 +209,7 @@ const fetchList = async () => { const spawnOne = () => { if (!props.active || !props.enabled || !items.value.length) return - if (rows.value.length >= TRACKS * 2) return + if (rows.value.length >= TRACKS) return const item = pickNextItem() if (!item) return @@ -186,7 +239,7 @@ const startSpawn = () => { spawnTimer = null if (!props.enabled || !props.active || !items.value.length) return spawnOne() - spawnTimer = setInterval(spawnOne, 1800) + spawnTimer = setInterval(spawnOne, 2200) } const stopSpawn = () => { @@ -219,6 +272,7 @@ watch( items.value = [] lastShownAt.clear() shownIds.clear() + resetTracks() } } } @@ -238,6 +292,7 @@ watch( items.value = [] lastShownAt.clear() shownIds.clear() + resetTracks() } } ) @@ -269,7 +324,7 @@ defineExpose({ refresh: fetchList, pushItem }) position: absolute; left: 100%; white-space: nowrap; - padding: 4px 12px; + padding: 5px 14px; border-radius: 999px; border: 1px solid rgba(168, 140, 107, 0.28); box-shadow: 0 4px 14px rgba(88, 68, 42, 0.08); @@ -282,6 +337,11 @@ defineExpose({ refresh: fetchList, pushItem }) width: max-content; max-width: none; } +.danmaku-item--grad { + backdrop-filter: saturate(1.25) blur(8px); + text-shadow: 0 1px 2px rgba(40, 20, 30, 0.28); + font-weight: 500; +} .danmaku-name { font-weight: 600; } .danmaku-sep { opacity: 0.9; } .danmaku-text { opacity: 0.95; } @@ -291,6 +351,9 @@ defineExpose({ refresh: fetchList, pushItem }) font-size: 10px; letter-spacing: 0.04em; } +.danmaku-time--on-grad { + color: rgba(255, 255, 255, 0.82); +} @keyframes danmaku-fly { from { transform: translateX(0); } to { transform: translateX(calc(-100vw - 100%)); } diff --git a/vite-tailwindcss/src/components/PhotoGallery.vue b/vite-tailwindcss/src/components/PhotoGallery.vue index c5922ea..2ddacbd 100644 --- a/vite-tailwindcss/src/components/PhotoGallery.vue +++ b/vite-tailwindcss/src/components/PhotoGallery.vue @@ -38,24 +38,24 @@ - -
+ +
@@ -98,91 +98,110 @@