feat: 添加签到功能及相关设置

- 更新 .gitignore,添加 SpecStory 说明文件
- 在 App.vue 中引入 NGlobalStyle 组件
- 更新 api-models.ts,添加签到相关数据模型
- 在 CheckInSettings.vue 中实现签到功能的配置界面
- 添加签到排行榜功能,允许用户查看签到情况
- 更新 PointHistoryCard.vue,增加签到记录显示
- 在 PointSettings.vue 中添加签到相关设置项
- 更新路由,添加签到排行页面
This commit is contained in:
2025-05-01 08:18:58 +08:00
parent 6160c89c68
commit f525bbb759
20 changed files with 1479 additions and 761 deletions

View File

@@ -1,28 +1,21 @@
import { ref, Ref, computed } from 'vue';
import { EventModel, EventDataTypes } from '@/api/api-models';
import { ActionType, AutoActionItem, RuntimeState, TriggerType, Priority, KeywordMatchType } from '../types';
import { usePointStore } from '@/store/usePointStore';
import { processTemplate, executeActions } from '../actionUtils';
import { buildExecutionContext } from '../utils';
import { v4 as uuidv4 } from 'uuid';
import { CheckInResult, EventDataTypes, EventModel } from '@/api/api-models';
import { QueryGetAPI } from '@/api/query';
import { CHECKIN_API_URL } from '@/data/constants';
import { useIDBKeyval } from '@vueuse/integrations/useIDBKeyval';
import { GuidUtils } from '@/Utils';
import { createDefaultAutoAction } from '../utils';
import { v4 as uuidv4 } from 'uuid';
import { Ref } from 'vue';
import { executeActions } from '../actionUtils';
import { ActionType, AutoActionItem, KeywordMatchType, Priority, RuntimeState, TriggerType } from '../types';
import { buildExecutionContext, createDefaultAutoAction } from '../utils';
import { useAccount } from '@/api/account';
// 签到配置接口
export interface CheckInConfig {
enabled: boolean;
command: string;
points: number;
cooldownSeconds: number;
onlyDuringLive: boolean; // 仅在直播时可签到
sendReply: boolean; // 是否发送签到回复消息
successAction: AutoActionItem; // 使用 AutoActionItem 替代字符串
cooldownAction: AutoActionItem; // 使用 AutoActionItem 替代字符串
earlyBird: {
enabled: boolean;
windowMinutes: number;
bonusPoints: number;
successAction: AutoActionItem; // 使用 AutoActionItem 替代字符串
};
}
@@ -31,7 +24,7 @@ export interface CheckInConfig {
function createDefaultCheckInConfig(): CheckInConfig {
const successAction = createDefaultAutoAction(TriggerType.DANMAKU);
successAction.name = '签到成功回复';
successAction.template = '@{{user.name}} 签到成功,获得 {{checkin.totalPoints}} 积分';
successAction.template = '@{{user.name}} 签到成功,获得 {{checkin.points}} 积分,连续签到 {{checkin.consecutiveDays}} 天';
const cooldownAction = createDefaultAutoAction(TriggerType.DANMAKU);
cooldownAction.name = '签到冷却回复';
@@ -39,43 +32,19 @@ function createDefaultCheckInConfig(): CheckInConfig {
const earlyBirdAction = createDefaultAutoAction(TriggerType.DANMAKU);
earlyBirdAction.name = '早鸟签到回复';
earlyBirdAction.template = '恭喜 {{user.name}} 完成早鸟签到!额外获得 {{bonusPoints}} 积分,共获得 {{totalPoints}} 积分';
earlyBirdAction.template = '恭喜 {{user.name}} 完成早鸟签到!获得 {{checkin.points}} 积分,连续签到 {{checkin.consecutiveDays}} ';
return {
enabled: false,
command: '签到',
points: 10,
cooldownSeconds: 3600, // 1小时
onlyDuringLive: true, // 默认仅在直播时可签到
sendReply: true, // 默认发送回复消息
successAction,
cooldownAction,
earlyBird: {
enabled: false,
windowMinutes: 30,
bonusPoints: 5,
successAction: earlyBirdAction
}
};
}
// 签到记录存储
interface CheckInStorage {
lastCheckIn: Record<string, number>; // ouid -> timestamp
users: Record<string, UserCheckInData>; // 用户签到详细数据
}
// 用户签到数据
export interface UserCheckInData {
ouid: string; // 用户ID
username: string; // 用户名称
totalCheckins: number; // 累计签到次数
streakDays: number; // 连续签到天数
lastCheckInTime: number; // 上次签到时间
earlyBirdCount: number; // 早鸟签到次数
firstCheckInTime: number; // 首次签到时间
}
/**
* 签到功能核心逻辑
*/
@@ -86,8 +55,6 @@ export function useCheckIn(
isTianXuanActive: Ref<boolean>,
sendDanmaku: (roomId: number, message: string) => Promise<boolean>
) {
const pointStore = usePointStore();
// 使用 IndexedDB 持久化存储签到配置
const { data: checkInConfig, isFinished: isConfigLoaded } = useIDBKeyval<CheckInConfig>(
'autoAction.checkin.config',
@@ -97,37 +64,21 @@ export function useCheckIn(
console.error('[CheckIn] IDB 错误 (配置):', err);
}
}
); // 使用 IndexedDB 持久化存储签到记录
const { data: checkInStorage, isFinished: isStorageLoaded } = useIDBKeyval<CheckInStorage>(
'autoAction.checkin.storage',
{
lastCheckIn: {},
users: {}
},
{
onError: (err) => {
console.error('[CheckIn] IDB 错误 (记录):', err);
}
}
);
const accountInfo = useAccount();
// 处理签到弹幕
// 处理签到弹幕 - 调用服务端API
async function processCheckIn(
event: EventModel,
runtimeState: RuntimeState
) {
// 确保配置和存储已加载
if (!isConfigLoaded.value || !isStorageLoaded.value) {
console.log('[CheckIn] 配置或存储尚未加载完成,跳过处理');
// 确保配置已加载
if (!isConfigLoaded.value) {
console.log('[CheckIn] 配置尚未加载完成,跳过处理');
return;
}
if (!roomId.value || !checkInConfig.value.enabled) {
return;
}
// 检查是否仅在直播时可签到
if (checkInConfig.value.onlyDuringLive && !isLive.value) {
if (!accountInfo.value.settings.point.enableCheckIn) {
return;
}
@@ -137,177 +88,107 @@ export function useCheckIn(
}
// 检查弹幕内容是否匹配签到指令
if (event.msg?.trim() !== checkInConfig.value.command) {
if (event.msg?.trim() !== accountInfo.value.settings.point.checkInKeyword.trim()) {
return;
}
const userId = event.ouid;
const username = event.uname || '用户';
const currentTime = Date.now();
const username = event.uname || event.uid || event.open_id || '用户';
// 检查是否已经在今天签到过
const lastCheckInTime = checkInStorage.value.lastCheckIn[userId] || 0;
// 判断上次签到时间是否为今天
const lastCheckInDate = new Date(lastCheckInTime);
const currentDate = new Date(currentTime);
// 比较日期部分是否相同(年、月、日)
const isSameDay = lastCheckInDate.getFullYear() === currentDate.getFullYear() &&
lastCheckInDate.getMonth() === currentDate.getMonth() &&
lastCheckInDate.getDate() === currentDate.getDate();
// 检查是否发送冷却提示
if (lastCheckInTime > 0 && isSameDay) {
// 用户今天已经签到过,发送提示
if (checkInConfig.value.sendReply) {
// 构建上下文
const cooldownContext = buildExecutionContext(event, roomId.value, TriggerType.DANMAKU, {
user: { name: username, uid: userId }
});
// 统一用 executeActions
executeActions(
[checkInConfig.value.cooldownAction],
event,
TriggerType.DANMAKU,
roomId.value,
runtimeState,
{ sendLiveDanmaku: sendDanmaku },
{
customContextBuilder: () => cooldownContext
}
);
}
window.$notification.info({
title: '签到提示',
description: `${username} 重复签到, 已忽略`,
duration: 5000
});
return;
}
// 计算积分奖励
let pointsEarned = checkInConfig.value.points;
let bonusPoints = 0;
let isEarlyBird = false;
// 检查是否符合早鸟奖励条件
if (checkInConfig.value.earlyBird.enabled && liveStartTime.value) {
const earlyBirdWindowMs = checkInConfig.value.earlyBird.windowMinutes * 60 * 1000;
const timeSinceLiveStart = currentTime - liveStartTime.value;
if (timeSinceLiveStart <= earlyBirdWindowMs) {
bonusPoints = checkInConfig.value.earlyBird.bonusPoints;
pointsEarned += bonusPoints;
isEarlyBird = true;
}
}
// 更新用户积分
try {
// 调用积分系统添加积分
const point = await pointStore.addPoints(userId, pointsEarned, `签到奖励 (${format(new Date(), 'yyyy-MM-dd')})`, `${username} 完成签到`);
if (checkInStorage.value) {
if (!checkInStorage.value.lastCheckIn) {
checkInStorage.value.lastCheckIn = {};
}
if (!checkInStorage.value.users) {
checkInStorage.value.users = {};
}
let userData = checkInStorage.value.users[userId];
if (!userData) {
userData = {
ouid: userId,
username: username,
totalCheckins: 0,
streakDays: 0,
lastCheckInTime: 0,
earlyBirdCount: 0,
firstCheckInTime: currentTime
};
}
const lastCheckInDate = new Date(userData.lastCheckInTime);
const currentDate = new Date(currentTime);
const isYesterday =
lastCheckInDate.getFullYear() === currentDate.getFullYear() &&
lastCheckInDate.getMonth() === currentDate.getMonth() &&
lastCheckInDate.getDate() === currentDate.getDate() - 1;
if (!isSameDay) {
if (isYesterday) {
userData.streakDays += 1;
} else if (userData.lastCheckInTime > 0) {
userData.streakDays = 1;
} else {
userData.streakDays = 1;
// 调用服务端API进行签到
const apiUrl = `${CHECKIN_API_URL}check-in-for`;
// 使用query.ts中的QueryGetAPI替代fetch
const response = await QueryGetAPI<CheckInResult>(apiUrl, event.uid ? {
uid: event.uid,
name: username
} : {
oId: event.ouid,
name: username
});
const checkInResult = response.data;
if (checkInResult) {
if (checkInResult.success) {
// 签到成功
if (roomId.value && checkInConfig.value.sendReply) {
const isEarlyBird = liveStartTime.value && (Date.now() - liveStartTime.value < 30 * 60 * 1000);
// 构建签到数据上下文
const checkInData = {
checkin: {
points: checkInResult.points,
consecutiveDays: checkInResult.consecutiveDays,
todayRank: checkInResult.todayRank,
time: new Date()
}
};
// 执行回复动作
const successContext = buildExecutionContext(event, roomId.value, TriggerType.DANMAKU, checkInData);
const action = isEarlyBird && checkInConfig.value.earlyBird.enabled
? checkInConfig.value.earlyBird.successAction
: checkInConfig.value.successAction;
executeActions(
[action],
event,
TriggerType.DANMAKU,
roomId.value,
runtimeState,
{ sendLiveDanmaku: sendDanmaku },
{
customContextBuilder: () => successContext
}
);
}
userData.totalCheckins += 1;
if (isEarlyBird) {
userData.earlyBirdCount += 1;
// 显示签到成功通知
window.$notification.success({
title: '签到成功',
description: `${username} 完成签到, 获得 ${checkInResult.points} 积分, 连续签到 ${checkInResult.consecutiveDays}`,
duration: 5000
});
} else {
// 签到失败 - 今天已经签到过
if (roomId.value && checkInConfig.value.sendReply) {
const cooldownContext = buildExecutionContext(event, roomId.value, TriggerType.DANMAKU);
executeActions(
[checkInConfig.value.cooldownAction],
event,
TriggerType.DANMAKU,
roomId.value,
runtimeState,
{ sendLiveDanmaku: sendDanmaku },
{
customContextBuilder: () => cooldownContext
}
);
}
// 显示签到失败通知
window.$notification.info({
title: '签到提示',
description: checkInResult.message || `${username} 重复签到, 已忽略`,
duration: 5000
});
}
userData.lastCheckInTime = currentTime;
userData.username = username;
checkInStorage.value.users[userId] = userData;
checkInStorage.value.lastCheckIn[userId] = currentTime;
}
if (roomId.value) {
const checkInData = {
checkin: {
points: checkInConfig.value.points,
bonusPoints: isEarlyBird ? bonusPoints : 0,
totalPoints: pointsEarned,
userPoints: point,
isEarlyBird: isEarlyBird,
time: new Date(currentTime),
cooldownSeconds: checkInConfig.value.cooldownSeconds
}
};
if (checkInConfig.value.sendReply) {
const successContext = buildExecutionContext(event, roomId.value, TriggerType.DANMAKU, checkInData);
const action = isEarlyBird ? checkInConfig.value.earlyBird.successAction : checkInConfig.value.successAction;
executeActions(
[action],
event,
TriggerType.DANMAKU,
roomId.value,
runtimeState,
{ sendLiveDanmaku: sendDanmaku },
{
customContextBuilder: () => successContext
}
);
}
window.$notification.success({
title: '签到成功',
description: `${username} 完成签到, 获得 ${pointsEarned} 积分, 累计签到 ${checkInStorage.value.users[userId].totalCheckins}`,
duration: 5000
});
}
} catch (error) {
console.error('[CheckIn] 处理签到失败:', error);
window.$notification.error({
title: '签到错误',
description: `签到请求失败:${error instanceof Error ? error.message : String(error)}`,
duration: 5000
});
}
}
// 监听直播开始事件
function onLiveStart() {
// 直播开始时记录开始时间,用于早鸟奖励计算
if (isLive.value && !liveStartTime.value) {
liveStartTime.value = Date.now();
}
}
// 监听直播结束事件
function onLiveEnd() {
// 直播结束时清空早鸟奖励的时间记录
liveStartTime.value = null;
}
return {
checkInConfig,
checkInStorage,
processCheckIn,
onLiveStart,
onLiveEnd
processCheckIn
};
}
@@ -324,7 +205,7 @@ export function createCheckInAutoActions(): AutoActionItem[] {
enabled: true,
triggerType: TriggerType.DANMAKU,
actionType: ActionType.SEND_DANMAKU,
template: '@{{user.name}} 签到成功,获得 {{points}} 积分',
template: '@{{user.name}} 签到成功,获得 {{checkin.points}} 积分',
priority: Priority.NORMAL,
logicalExpression: '',
ignoreCooldown: false,
@@ -344,7 +225,7 @@ export function createCheckInAutoActions(): AutoActionItem[] {
enabled: true,
triggerType: TriggerType.DANMAKU,
actionType: ActionType.SEND_DANMAKU,
template: '@{{user.name}} 早鸟签到成功,获得 {{totalPoints}} 积分',
template: '@{{user.name}} 早鸟签到成功,获得 {{checkin.points}} 积分',
priority: Priority.HIGH,
logicalExpression: '',
ignoreCooldown: false,