Files
dpb/frontend/web/src/hooks/core/useCeremony.ts
T
34047007@qq.com 86126ea16f checkpoint: 架构治理四件套——统一 async / 测试真 PG / Redis 降级可选 / DPB 扶正
- A1 业务层统一 async:gencode 表内省改 run_sync 异步(不再阻塞事件循环)、
  check_db 改 async_engine、dict_util 启动预热异步载入;psycopg 降为
  APScheduler SQLAlchemyJobStore 专用同步孤岛(注释标注)
- A2 测试真 PG 化:conftest 弃 SQLite + mock Redis,改连真实 PG16(dpb_test)+
  Redis(number_gen advisory lock 路径真被测);新增 docker/test-compose.yaml
  测试依赖栈(postgres:16:5433 + redis:7:6380);run_ci 前置探测 + 容器兜底,
  pytest -m pg 10 套正式 tc 全过(真实 PG16 + Redis)
- B1 Redis 启动不强依赖:redis_connect 失败降级启动,缓存类回源 DB、存储类
  (会话/AI 配置/调度)经 require_redis 守卫返回 503;真机 3 阶段降级测试
  PASS=11 FAIL=0(独立 Redis 6390 + 后端 8091,不动共享 dev 服务)
- C 扶正 DPB:后端横幅/日志(dpb.log)/README/pyproject/ai_factory agent 名、
  前端 package.json/署名注释/链接文案/deploy.sh/docker 注释全部去 fastapiadmin;
  grep backend/app + frontend/web/src 零残留
- 验证:全量回归 pass=72 fail=0
2026-08-07 09:06:19 +08:00

307 lines
9.0 KiB
TypeScript
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.
/**
* useCeremony - 节日庆祝管理
*
* 提供节日烟花效果和祝福文本展示功能,为系统增添节日氛围。
* 自动检测当前日期是否为节日,并在首次进入时播放烟花动画和显示祝福语。
*
* ## 主要功能
*
* 1. 节日检测 - 「内置 + 手动」节日优先;其次周六/日周末提示;最后 `isResidentBanner` 常驻。同级节日区间更短优先,同长度手动优先于内置
* 2. 烟花动画 - 播放节日烟花特效,支持自定义图片和触发次数
* 3. 祝福文本 - 烟花结束后显示节日祝福文本
* 4. 状态管理 - 记录烟花播放状态,避免重复播放
* 5. 清理机制 - 提供清理方法,支持手动停止和重置
*
* ## 手动礼花快捷键
*
* 全局 **Ctrl+Shift+P** / **⌘+Shift+P** 由 `art-fireworks-effect` 监听;礼花图与当天节日配置一致,
* 详见 `src/config/modules/festival.ts` 文件头说明。
*
* ## 使用示例
*
* ```typescript
* // 在配置文件中定义节日
* // 单日节日
* {
* date: '2024-12-25',
* name: '圣诞节',
* image: christmasImage,
* count: 3 // 可选,不设置则使用默认值 3 次
* scrollText: 'Merry Christmas!',
* }
*
* // 跨日期节日
* {
* date: '2025-11-07',
* endDate: '2025-11-10',
* name: 'v3.0 测试阶段',
* image: '',
* count: 5 // 自定义烟花播放次数
* scrollText: '系统 v3.0 测试阶段正式开启!',
* }
* ```
*
* @module useCeremony
* @author DPB 团队
*/
import { useTimeoutFn, useIntervalFn, useDateFormat } from "@vueuse/core";
import { storeToRefs } from "pinia";
import { computed } from "vue";
import { useSettingsStore } from "@stores";
import { mittBus, formatToDate } from "@utils";
import { festivalConfigList } from "@/config/modules/festival";
import { buildBuiltinSolarFestivals } from "@/config/modules/festival.builtin";
import type { FestivalConfig } from "@/types/config";
/** 手动配置 + 内置公历节日合并项(内部排序用) */
type TaggedFestival = FestivalConfig & { _origin: "manual" | "builtin" };
/**
* 节日庆祝配置常量
*/
const FESTIVAL_CONFIG = {
/** 初始延迟(毫秒) */
INITIAL_DELAY: 300,
/** 烟花播放间隔(毫秒) */
FIREWORK_INTERVAL: 1000,
/** 文本显示延迟(毫秒) */
TEXT_DELAY: 2000,
/** 默认烟花播放次数 */
DEFAULT_FIREWORKS_COUNT: 3,
} as const;
/**
* 节日庆祝功能
* 提供节日烟花效果和祝福文本展示
*/
export function useCeremony() {
const settingStore = useSettingsStore();
const { holidayFireworksLoaded, isShowFireworks } = storeToRefs(settingStore);
let fireworksInterval: { pause: () => void } | null = null;
/**
* 检查日期是否在节日范围内
* @param currentDate 当前日期
* @param festivalDate 节日开始日期
* @param festivalEndDate 节日结束日期(可选)
*/
const isDateInRange = (
currentDate: string,
festivalDate: string,
festivalEndDate?: string
): boolean => {
if (!festivalEndDate) {
// 单日节日
return currentDate === festivalDate;
}
// 跨日期节日
const current = new Date(currentDate);
const start = new Date(festivalDate);
const end = new Date(festivalEndDate);
return current >= start && current <= end;
};
/** 区间越短越「具体」,重叠时优先展示(如单日元旦优先于全年公告) */
const festivalRangeSpanDays = (item: FestivalConfig): number => {
if (!item.endDate) return 1;
const s = new Date(item.date).getTime();
const e = new Date(item.endDate).getTime();
return Math.max(1, Math.round((e - s) / 86400000) + 1);
};
/** 是否为周六、周日(本地时区) */
const isWeekendDate = (dateStr: string): boolean => {
const w = new Date(dateStr).getDay();
return w === 0 || w === 6;
};
/** 无其它节日命中时的周末顶栏(仅文案,与常驻同为 skipFireworks */
const buildWeekendPrompt = (dateStr: string): FestivalConfig => {
const w = new Date(dateStr).getDay();
const isSat = w === 6;
return {
name: isSat ? "周六愉快" : "周日愉快",
date: dateStr,
image: "",
scrollText: isSat
? "🌤️ 周末到啦,适当放松,记得分支与构建状态也要照顾好~"
: "🌅 周日休整好,新一周迭代加油。",
skipFireworks: true,
count: 3,
};
};
const pickFestivalWinner = (candidates: TaggedFestival[]): TaggedFestival | undefined => {
if (!candidates.length) return undefined;
const sorted = [...candidates];
sorted.sort((a, b) => {
const spanDiff = festivalRangeSpanDays(a) - festivalRangeSpanDays(b);
if (spanDiff !== 0) return spanDiff;
if (a._origin !== b._origin) return a._origin === "manual" ? -1 : 1;
return 0;
});
return sorted[0];
};
/**
* 内置公历 + 手动配置(当年)
*/
const mergedFestivalEntries = computed<TaggedFestival[]>(() => {
const year = new Date().getFullYear();
const builtin = buildBuiltinSolarFestivals(year).map((f) => ({
...f,
_origin: "builtin" as const,
}));
const manual = festivalConfigList.map((f) => ({
...f,
_origin: "manual" as const,
}));
return [...builtin, ...manual];
});
/**
* 当前展示配置:节日(非常驻)> 周末提示 > 常驻 `isResidentBanner`
*/
const currentFestivalData = computed((): FestivalConfig | undefined => {
const currentDate = useDateFormat(new Date(), "YYYY-MM-DD").value;
const matches = mergedFestivalEntries.value.filter((item) =>
isDateInRange(currentDate, item.date, item.endDate)
);
const nonResident = matches.filter((m) => !m.isResidentBanner);
const winnerTagged = pickFestivalWinner(nonResident);
if (winnerTagged) {
const row = { ...winnerTagged };
delete (row as Partial<TaggedFestival>)._origin;
return row as FestivalConfig;
}
if (isWeekendDate(currentDate)) {
return buildWeekendPrompt(currentDate);
}
const residents = matches.filter((m) => m.isResidentBanner);
const residentWinner = pickFestivalWinner(residents);
if (residentWinner) {
const row = { ...residentWinner };
delete (row as Partial<TaggedFestival>)._origin;
return row as FestivalConfig;
}
return undefined;
});
/**
* 标记「今日已完成烟花流程」,用于 store 按自然日防抖(与节日配置的 date 字段无关)
*/
const updateFestivalDate = () => {
settingStore.setFestivalDate(formatToDate(new Date()));
};
/**
* 触发烟花效果
*/
const triggerFirework = () => {
mittBus.emit("triggerFireworks", currentFestivalData.value?.image);
};
/**
* 完成烟花效果后显示文本
*/
const showFestivalText = () => {
settingStore.setholidayFireworksLoaded(true);
useTimeoutFn(() => {
settingStore.setShowFestivalText(true);
updateFestivalDate();
}, FESTIVAL_CONFIG.TEXT_DELAY);
};
/**
* 启动烟花循环
*/
const startFireworksLoop = () => {
const cur = currentFestivalData.value;
let playedCount = 0;
const count = cur?.count ?? FESTIVAL_CONFIG.DEFAULT_FIREWORKS_COUNT;
const intervalMs = cur?.fireworkInterval ?? FESTIVAL_CONFIG.FIREWORK_INTERVAL;
const { pause } = useIntervalFn(() => {
triggerFirework();
playedCount++;
if (playedCount >= count) {
pause();
showFestivalText();
}
}, intervalMs);
fireworksInterval = { pause };
};
/**
* 顶栏纯文案关闭(仅 skipFireworks)持久化 key
*/
const festivalScrollDismissKey = (cur: FestivalConfig): string =>
`festival_scroll_dismissed_${cur.date}_${cur.endDate ?? ""}`;
/**
* 开启节日庆祝:skipFireworks 时直接显示滚动条;否则走烟花再出字
*/
const openFestival = () => {
const cur = currentFestivalData.value;
if (!cur) return;
if (cur.skipFireworks) {
if (
typeof localStorage !== "undefined" &&
localStorage.getItem(festivalScrollDismissKey(cur)) === "1"
) {
return;
}
settingStore.setShowFestivalText(true);
return;
}
if (!isShowFireworks.value) {
return;
}
const { start } = useTimeoutFn(startFireworksLoop, FESTIVAL_CONFIG.INITIAL_DELAY);
start();
};
/**
* 清理烟花效果
*/
const cleanup = () => {
if (fireworksInterval) {
fireworksInterval.pause();
fireworksInterval = null;
}
settingStore.setShowFestivalText(false);
};
/** 关闭顶栏滚动:skipFireworks 时写入 localStorage,下次不再自动展开 */
const closeFestivalScroll = () => {
const cur = currentFestivalData.value;
settingStore.setShowFestivalText(false);
if (cur?.skipFireworks && typeof localStorage !== "undefined") {
localStorage.setItem(festivalScrollDismissKey(cur), "1");
}
};
return {
openFestival,
cleanup,
holidayFireworksLoaded,
currentFestivalData,
isShowFireworks,
festivalScrollDismissKey,
closeFestivalScroll,
};
}