From ab2cafeb1bec4e310f6758bd9d245f157a5944a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Mon, 3 Aug 2026 11:24:55 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E7=A4=BC=E7=89=A9=E7=89=B9=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hunliji-api/main.go | 1 + hunliji-api/models/models.go | 11 + hunliji-api/router/routers.go | 199 ++++ vite-tailwindcss/src/api/wedding.js | 16 + .../src/components/GiftEffect.vue | 854 ++++++++++++++++++ vite-tailwindcss/src/components/GiftFeed.vue | 143 +++ vite-tailwindcss/src/utils/giftTypes.js | 38 + vite-tailwindcss/src/utils/guestName.js | 17 + .../src/views/admin/AdminEditor.vue | 44 +- .../src/views/hb/templates/ClassicPoster.vue | 10 +- .../src/views/hb/templates/SplitPoster.vue | 28 +- vite-tailwindcss/src/views/index/index.vue | 360 +++++++- 12 files changed, 1686 insertions(+), 35 deletions(-) create mode 100644 vite-tailwindcss/src/components/GiftEffect.vue create mode 100644 vite-tailwindcss/src/components/GiftFeed.vue create mode 100644 vite-tailwindcss/src/utils/giftTypes.js create mode 100644 vite-tailwindcss/src/utils/guestName.js diff --git a/hunliji-api/main.go b/hunliji-api/main.go index 7e1c240..5442d6a 100644 --- a/hunliji-api/main.go +++ b/hunliji-api/main.go @@ -34,6 +34,7 @@ func migrateSchema(db *gorm.DB) { {&models.Rsvp{}, "出席回执", "rsvps"}, {&models.Danmaku{}, "祝福弹幕", "danmakus"}, {&models.SiteLike{}, "站点点赞", "site_likes"}, + {&models.SiteGift{}, "站点礼物", "site_gifts"}, {&models.AiModerationCache{}, "AI审核缓存", "ai_moderation_caches"}, {&visit.SiteVisit{}, "访客记录", "site_visits"}, } diff --git a/hunliji-api/models/models.go b/hunliji-api/models/models.go index db09459..fcc7cf9 100644 --- a/hunliji-api/models/models.go +++ b/hunliji-api/models/models.go @@ -65,6 +65,17 @@ type SiteLike struct { func (SiteLike) TableName() string { return "site_likes" } +// SiteGift 站点礼物记录 +type SiteGift struct { + ID uint `gorm:"primaryKey;comment:主键ID" json:"id"` + GiftType string `gorm:"column:gift_type;size:32;not null;index;comment:礼物类型" json:"gift_type"` + Name string `gorm:"column:name;size:50;not null;default:'';comment:送礼人姓名" json:"name"` + ClientID string `gorm:"column:client_id;size:64;not null;default:'';index;comment:客户端标识" json:"client_id"` + CreatedAt time.Time `gorm:"column:created_at;not null;comment:送礼时间" json:"created_at"` +} + +func (SiteGift) TableName() string { return "site_gifts" } + // AiModerationCache 缓存 AI 审核通过/失败结果 type AiModerationCache struct { ID uint `gorm:"primaryKey;comment:主键ID" json:"id"` diff --git a/hunliji-api/router/routers.go b/hunliji-api/router/routers.go index 37c98d8..313d3ef 100644 --- a/hunliji-api/router/routers.go +++ b/hunliji-api/router/routers.go @@ -47,6 +47,81 @@ var danmakuColorWhitelist = map[string]struct{}{ "gradOcean": {}, "gradAurora": {}, "gradEmber": {}, } +// giftTypeWhitelist 礼物类型白名单(与前端常量表一致) +var giftTypeWhitelist = map[string]string{ + "520": "520", + "1314": "1314", + "forever": "长长久久", + "bald": "白头偕老", + "concentric": "永结同心", + "heaven": "天作之合", + "moisten": "相濡以沫", + "match": "佳偶天成", +} + +func emptyGiftCounts() map[string]int64 { + counts := make(map[string]int64, len(giftTypeWhitelist)) + for k := range giftTypeWhitelist { + counts[k] = 0 + } + return counts +} + +func loadGiftCounts() map[string]int64 { + counts := emptyGiftCounts() + type row struct { + GiftType string `gorm:"column:gift_type"` + Cnt int64 `gorm:"column:cnt"` + } + var rows []row + db.Model(&models.SiteGift{}). + Select("gift_type, COUNT(*) as cnt"). + Group("gift_type"). + Find(&rows) + for _, r := range rows { + if _, ok := giftTypeWhitelist[r.GiftType]; ok { + counts[r.GiftType] = r.Cnt + } + } + return counts +} + +const ( + giftBaseAllow int64 = 1 + giftLikesPerExtra int64 = 50 +) + +// giftQuota 每个 client 基础 1 次赠送,每点赞 50 次多 1 次 +func giftQuota(clientID string) (myLikes, used, allow, remain, likesToNext int64) { + clientID = strings.TrimSpace(clientID) + if clientID != "" { + db.Model(&models.SiteLike{}).Where("client_id = ?", clientID).Count(&myLikes) + db.Model(&models.SiteGift{}).Where("client_id = ?", clientID).Count(&used) + } + allow = giftBaseAllow + myLikes/giftLikesPerExtra + remain = allow - used + if remain < 0 { + remain = 0 + } + likesToNext = giftLikesPerExtra - (myLikes % giftLikesPerExtra) + if likesToNext <= 0 { + likesToNext = giftLikesPerExtra + } + return +} + +func giftQuotaPayload(clientID string) gin.H { + myLikes, used, allow, remain, likesToNext := giftQuota(clientID) + return gin.H{ + "my_likes": myLikes, + "gift_used": used, + "gift_allow": allow, + "gift_remain": remain, + "likes_to_next": likesToNext, + "likes_per_gift": giftLikesPerExtra, + } +} + // Register 挂载静态资源、中间件与全部 /api 路由 func Register(r *gin.Engine, deps Deps) { db = deps.DB @@ -78,6 +153,9 @@ func registerAPI(api *gin.RouterGroup) { api.POST("/danmaku/:id/status", handleDanmakuStatus) api.GET("/like", handleLikeCount) api.POST("/like", handleLike) + api.GET("/gift", handleGiftStatus) + api.POST("/gift", handleCreateGift) + api.GET("/gift/stats", handleGiftStats) visit.Register(api, db, utils.RequireAdmin) configsection.Register(api, db, utils.RequireAdmin) @@ -398,6 +476,127 @@ func handleLike(c *gin.Context) { c.JSON(200, gin.H{"code": 200, "data": gin.H{"count": count}, "msg": "点赞成功"}) } +func handleGiftStatus(c *gin.Context) { + counts := loadGiftCounts() + var recent []models.SiteGift + db.Model(&models.SiteGift{}). + Order("id DESC"). + Limit(20). + Find(&recent) + items := make([]gin.H, 0, len(recent)) + for _, g := range recent { + items = append(items, gin.H{ + "gift_type": g.GiftType, + "name": g.Name, + "created_at": g.CreatedAt, + }) + } + // 回放按时间正序(最早在前) + for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 { + items[i], items[j] = items[j], items[i] + } + var total int64 + for _, n := range counts { + total += n + } + clientID := strings.TrimSpace(c.Query("client_id")) + quota := giftQuotaPayload(clientID) + c.JSON(200, gin.H{ + "code": 200, + "data": gin.H{ + "counts": counts, + "total": total, + "recent": items, + "quota": quota, + }, + }) +} + +func handleCreateGift(c *gin.Context) { + var body struct { + GiftType string `json:"gift_type"` + Name string `json:"name"` + ClientID string `json:"client_id"` + } + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(400, gin.H{"error": "参数错误"}) + return + } + giftType := strings.TrimSpace(body.GiftType) + if _, ok := giftTypeWhitelist[giftType]; !ok { + c.JSON(400, gin.H{"error": "无效的礼物类型"}) + return + } + name := strings.TrimSpace(body.Name) + if name == "" { + c.JSON(400, gin.H{"error": "请填写姓名"}) + return + } + if len([]rune(name)) > 50 { + c.JSON(400, gin.H{"error": "姓名过长"}) + return + } + clientID := strings.TrimSpace(body.ClientID) + if clientID == "" || len(clientID) > 64 { + c.JSON(400, gin.H{"error": "无效的 client_id"}) + return + } + _, _, _, remain, likesToNext := giftQuota(clientID) + if remain <= 0 { + c.JSON(400, gin.H{ + "error": fmt.Sprintf("赠送次数已用完,再点赞 %d 次可多送 1 次", likesToNext), + "data": giftQuotaPayload(clientID), + }) + return + } + if err := db.Create(&models.SiteGift{ + GiftType: giftType, + Name: name, + ClientID: clientID, + }).Error; err != nil { + c.JSON(500, gin.H{"error": "送礼失败"}) + return + } + counts := loadGiftCounts() + var total int64 + for _, n := range counts { + total += n + } + c.JSON(200, gin.H{ + "code": 200, + "data": gin.H{ + "counts": counts, + "total": total, + "gift_type": giftType, + "quota": giftQuotaPayload(clientID), + }, + "msg": "送礼成功", + }) +} + +func handleGiftStats(c *gin.Context) { + if !utils.RequireAdmin(c) { + return + } + counts := loadGiftCounts() + var total int64 + for _, n := range counts { + total += n + } + labels := make(map[string]string, len(giftTypeWhitelist)) + for k, v := range giftTypeWhitelist { + labels[k] = v + } + c.JSON(200, gin.H{ + "code": 200, + "data": gin.H{ + "counts": counts, + "labels": labels, + "total": total, + }, + }) +} + func normalizeDanmakuColor(color string) string { c := strings.TrimSpace(color) if c == "rose" || c == "sage" { diff --git a/vite-tailwindcss/src/api/wedding.js b/vite-tailwindcss/src/api/wedding.js index ec8d577..c2adf6f 100644 --- a/vite-tailwindcss/src/api/wedding.js +++ b/vite-tailwindcss/src/api/wedding.js @@ -138,6 +138,22 @@ export function submitLike(clientId) { return service.post('/like', { client_id: clientId }) } +export function getGiftStatus(clientId) { + return service.get('/gift', { params: { client_id: clientId } }) +} + +export function submitGift({ giftType, name, clientId }) { + return service.post('/gift', { + gift_type: giftType, + name, + client_id: clientId, + }) +} + +export function getGiftStats() { + return service.get('/gift/stats') +} + export function adminLogin(credentials) { return service.post('/admin/login', credentials) } diff --git a/vite-tailwindcss/src/components/GiftEffect.vue b/vite-tailwindcss/src/components/GiftEffect.vue new file mode 100644 index 0000000..0d7acdc --- /dev/null +++ b/vite-tailwindcss/src/components/GiftEffect.vue @@ -0,0 +1,854 @@ + + + + + diff --git a/vite-tailwindcss/src/components/GiftFeed.vue b/vite-tailwindcss/src/components/GiftFeed.vue new file mode 100644 index 0000000..9caa7ec --- /dev/null +++ b/vite-tailwindcss/src/components/GiftFeed.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/vite-tailwindcss/src/utils/giftTypes.js b/vite-tailwindcss/src/utils/giftTypes.js new file mode 100644 index 0000000..4c01658 --- /dev/null +++ b/vite-tailwindcss/src/utils/giftTypes.js @@ -0,0 +1,38 @@ +/** 礼物类型常量表(与后端 giftTypeWhitelist 一致) */ +export const GIFT_TYPES = [ + { key: '520', label: '520' }, + { key: '1314', label: '1314' }, + { key: 'forever', label: '长长久久' }, + { key: 'bald', label: '白头偕老' }, + { key: 'concentric', label: '永结同心' }, + { key: 'heaven', label: '天作之合' }, + { key: 'moisten', label: '相濡以沫' }, + { key: 'match', label: '佳偶天成' }, +] + +export const GIFT_LABEL_MAP = Object.fromEntries(GIFT_TYPES.map((g) => [g.key, g.label])) + +export function emptyGiftCounts() { + return Object.fromEntries(GIFT_TYPES.map((g) => [g.key, 0])) +} + +/** + * 连续相同「姓名 + 礼物」合并为一条(带 count),用于串行特效与左下角展示 + * @param {Array<{ gift_type?: string, name?: string }>} recent + */ +export function aggregateGiftRecent(recent) { + const list = Array.isArray(recent) ? recent : [] + const out = [] + for (const g of list) { + if (!g?.gift_type) continue + const name = String(g.name || '').trim() || '匿名' + const giftType = g.gift_type + const last = out[out.length - 1] + if (last && last.giftType === giftType && last.name === name) { + last.count += 1 + } else { + out.push({ giftType, name, count: 1 }) + } + } + return out +} diff --git a/vite-tailwindcss/src/utils/guestName.js b/vite-tailwindcss/src/utils/guestName.js new file mode 100644 index 0000000..6a0204b --- /dev/null +++ b/vite-tailwindcss/src/utils/guestName.js @@ -0,0 +1,17 @@ +const KEY = 'wedding_guest_name_v1' + +export function getGuestName() { + try { + return String(localStorage.getItem(KEY) || '').trim() + } catch { + return '' + } +} + +export function setGuestName(name) { + const v = String(name || '').trim() + if (!v) return + try { + localStorage.setItem(KEY, v) + } catch { /* ignore */ } +} diff --git a/vite-tailwindcss/src/views/admin/AdminEditor.vue b/vite-tailwindcss/src/views/admin/AdminEditor.vue index 6bee0a1..df0765b 100644 --- a/vite-tailwindcss/src/views/admin/AdminEditor.vue +++ b/vite-tailwindcss/src/views/admin/AdminEditor.vue @@ -76,6 +76,22 @@ +
+
+
礼物统计
+
各礼物收到次数 · 合计 {{ giftStats.total }}
+
+ + 刷新 + +
+
+
+
{{ g.label }}
+
{{ giftStats.counts[g.key] || 0 }}
+
+
+
地区分布
@@ -967,12 +983,13 @@ import { CanvasRenderer } from 'echarts/renderers' import { getConfigSection, saveConfigSection, uploadImage, getRsvpList, getUploadConfig, saveUploadConfig, getDanmakuList, updateDanmakuStatus, getVisitStats, getVisitList, - getPosterConfig, savePosterConfig, + getPosterConfig, savePosterConfig, getGiftStats, } from '@/api/wedding' import { useAdminStore } from '@/stores/admin' import ImageField from '@/components/ImageField.vue' import { BORDER_STYLES, borderClass } from '@/composables/borderStyles' import { resolveDanmakuSwatch } from '@/utils/danmakuColors' +import { GIFT_TYPES } from '@/utils/giftTypes' import { getSlotResizeSpecs, resizeSlotToFile, probeResizedMeta, metaHasFileSize } from '@/composables/resizeImage' import { defaultPosterBundle, normalizePosterBundle, normalizePosterConfig, @@ -1026,6 +1043,12 @@ const danmakuFilter = ref('pending') const visitStatsLoading = ref(false) const visitListLoading = ref(false) +const giftStatsLoading = ref(false) +const giftTypeList = GIFT_TYPES +const giftStats = reactive({ + total: 0, + counts: Object.fromEntries(GIFT_TYPES.map((g) => [g.key, 0])), +}) const visitStats = reactive({ uv: 0, pv: 0, today_uv: 0, regions: [], devices: [] }) const visitList = ref([]) const visitPreviewList = ref([]) @@ -1695,6 +1718,23 @@ function renderDeviceChart() { ) } +async function loadGiftStats() { + giftStatsLoading.value = true + try { + const res = await getGiftStats() + const d = res.data || {} + const counts = d.counts || {} + for (const g of GIFT_TYPES) { + giftStats.counts[g.key] = Number(counts[g.key]) || 0 + } + giftStats.total = Number(d.total) || Object.values(giftStats.counts).reduce((a, b) => a + b, 0) + } catch (e) { + message.error(e?.message || '加载礼物统计失败') + } finally { + giftStatsLoading.value = false + } +} + async function loadVisitStats() { visitStatsLoading.value = true try { @@ -1747,7 +1787,7 @@ async function loadVisitList() { } async function loadOverview() { - await Promise.all([loadVisitStats(), loadVisitPreview()]) + await Promise.all([loadVisitStats(), loadVisitPreview(), loadGiftStats()]) } function onVisitTableChange(pag) { diff --git a/vite-tailwindcss/src/views/hb/templates/ClassicPoster.vue b/vite-tailwindcss/src/views/hb/templates/ClassicPoster.vue index dbaedf4..d8122c8 100644 --- a/vite-tailwindcss/src/views/hb/templates/ClassicPoster.vue +++ b/vite-tailwindcss/src/views/hb/templates/ClassicPoster.vue @@ -51,7 +51,7 @@ :active="stage >= 2" animate-by="letters" :delay="40" - class-name="hb-name" + class-name="hb-name calligraphy" @animation-complete="namesRightReady = true" /> @@ -61,7 +61,7 @@ :active="namesRightReady" animate-by="letters" :delay="40" - class-name="hb-name" + class-name="hb-name calligraphy" @animation-complete="bumpStage(3)" />
@@ -92,7 +92,7 @@ cursor-class-name="hb-cursor" :typing-speed="48" :initial-delay="80" - class-name="hb-line" + class-name="hb-line calligraphy" @animation-complete="onLineDone(i)" /> @@ -114,7 +114,7 @@ :active="stage >= 5" animate-by="letters" :delay="35" - class-name="hb-footer-text" + class-name="hb-footer-text calligraphy" @animation-complete="bumpStage(6)" />
diff --git a/vite-tailwindcss/src/views/hb/templates/SplitPoster.vue b/vite-tailwindcss/src/views/hb/templates/SplitPoster.vue index fa96d0c..19b833b 100644 --- a/vite-tailwindcss/src/views/hb/templates/SplitPoster.vue +++ b/vite-tailwindcss/src/views/hb/templates/SplitPoster.vue @@ -24,8 +24,8 @@ @animation-complete="bumpStage(3)" />
- - + +
@@ -42,7 +42,7 @@ :active="stage >= 3" animate-by="letters" :delay="40" - class-name="sp-greeting" + class-name="sp-greeting calligraphy" @animation-complete="bumpStage(4)" /> @@ -57,7 +57,7 @@ cursor-character="|" cursor-class-name="sp-cursor" :typing-speed="42" - class-name="sp-line" + class-name="sp-line calligraphy" @animation-complete="onLineDone(i)" /> @@ -74,22 +74,22 @@
- 新郎 - + 新郎 +
- 新娘 - + 新娘 +
-
新婚喜宴时间
-
{{ cfg.dateText }}
-
{{ cfg.lunarText }}
-
地点
-
{{ cfg.venueText }}
+
新婚喜宴时间
+
{{ cfg.dateText }}
+
{{ cfg.lunarText }}
+
地点
+
{{ cfg.venueText }}
diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index 70bcb5e..f9b97a6 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -38,7 +38,7 @@ @@ -54,13 +54,32 @@
- -
+ +
+ :class="isAutoScroll ? 'bg-[#a88c6b]/15 !border-[#a88c6b]/60 shadow-sm' : 'bg-white/60 shadow-sm border-[#a88c6b]/30'" + title="自动滚动">
+ +
@@ -89,6 +108,19 @@
+ +
+ +
@@ -253,6 +285,45 @@
+ + + + +
+
+ +
+ 送一份心意 + +
+
+ 还可赠送 {{ giftQuota.remain }} + 赠送次数已用完,再点赞 {{ giftQuota.likes_to_next }} 次可多送 1 次 + 已送 {{ giftQuota.gift_used }} / {{ giftQuota.gift_allow }} · 点赞 {{ giftQuota.my_likes }}(每 {{ giftQuota.likes_per_gift }} 赞 +1 次) +
+ +
+ +
+
@@ -427,7 +498,7 @@ {{ data.address }}
-