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

@@ -12,161 +12,297 @@
name="settings"
tab="签到设置"
>
<NForm
label-placement="left"
label-width="auto"
>
<NFormItem label="启用签到功能">
<NSwitch v-model:value="config.enabled" />
</NFormItem>
<NSpin :show="isLoading">
<NAlert
v-if="!canEdit"
type="warning"
>
加载中或无法编辑设置请稍后再试
</NAlert>
<template v-if="config.enabled">
<NFormItem label="签到指令">
<NInput
v-model:value="config.command"
placeholder="例如:签到"
<NForm
label-placement="left"
:label-width="120"
:style="{
maxWidth: '650px'
}"
>
<!-- 服务端签到设置 -->
<NDivider title-placement="left">
基本设置
</NDivider>
<NFormItem label="启用签到功能">
<NSwitch
v-model:value="serverSetting.enableCheckIn"
@update:value="updateServerSettings"
/>
<template #feedback>
观众发送此指令触发签到
启用后观众可以通过发送签到命令获得积分
</template>
</NFormItem>
<NFormItem label="仅在直播时可签到">
<NSwitch v-model:value="config.onlyDuringLive" />
<template #feedback>
启用后仅在直播进行中才能签到否则任何时候都可以签到
<template v-if="serverSetting.enableCheckIn">
<NFormItem label="签到命令">
<NInputGroup>
<NInput
:value="serverSetting.checkInKeyword"
placeholder="例如:签到"
@update:value="(v: string) => serverSetting.checkInKeyword = v"
/>
<NButton
type="primary"
@click="updateServerSettings"
>
保存
</NButton>
</NInputGroup>
<template #feedback>
观众发送此命令可以触发签到注意同时更新客户端命令设置
</template>
</NFormItem>
<NFormItem label="为签到提供积分">
<NSwitch
v-model:value="serverSetting.givePointsForCheckIn"
@update:value="updateServerSettings"
/>
<template #feedback>
启用后签到会获得积分奖励
</template>
</NFormItem>
<!-- 积分相关设置只有在开启"为签到提供积分"后显示 -->
<template v-if="serverSetting.givePointsForCheckIn">
<NFormItem label="基础签到积分">
<NInputNumber
v-model:value="serverSetting.baseCheckInPoints"
:min="0"
style="width: 100%"
@update:value="updateServerSettings"
/>
<template #feedback>
每次签到获得的基础积分数量
</template>
</NFormItem>
<NFormItem label="启用连续签到奖励">
<NSwitch
v-model:value="serverSetting.enableConsecutiveBonus"
@update:value="updateServerSettings"
/>
<template #feedback>
启用后连续签到会获得额外奖励
</template>
</NFormItem>
<template v-if="serverSetting.enableConsecutiveBonus">
<NFormItem label="每天额外奖励积分">
<NInputNumber
v-model:value="serverSetting.bonusPointsPerDay"
:min="0"
style="width: 100%"
@update:value="updateServerSettings"
/>
<template #feedback>
每天连续签到额外奖励的积分数量
</template>
</NFormItem>
<NFormItem label="最大奖励积分">
<NInputNumber
v-model:value="serverSetting.maxBonusPoints"
:min="0"
style="width: 100%"
@update:value="updateServerSettings"
/>
<template #feedback>
连续签到奖励积分的上限
</template>
</NFormItem>
</template>
</template>
</NFormItem>
<NFormItem label="发送签到回复">
<NSwitch v-model:value="config.sendReply" />
<template #feedback>
启用后签到成功或重复签到时会发送弹幕回复关闭则只显示通知不发送弹幕
</template>
</NFormItem>
<NFormItem label="允许自己签到">
<NSwitch
v-model:value="serverSetting.allowSelfCheckIn"
@update:value="updateServerSettings"
/>
<template #feedback>
启用后主播自己也可以签到获得积分
</template>
</NFormItem>
<NFormItem label="签到成功获得积分">
<NInputNumber
v-model:value="config.points"
:min="0"
style="width: 100%"
/>
</NFormItem>
<NFormItem label="要求用户已认证">
<NSwitch
v-model:value="serverSetting.requireAuth"
@update:value="updateServerSettings"
/>
<template #feedback>
启用后只有已认证的用户才能签到
</template>
</NFormItem>
<NFormItem label="用户签到冷却时间 (秒)">
<NInputNumber
v-model:value="config.cooldownSeconds"
:min="0"
style="width: 100%"
/>
<template #feedback>
每个用户在指定秒数内签到命令只会响应一次
</template>
</NFormItem>
<NFormItem label="允许查看签到排行">
<NSwitch
v-model:value="serverSetting.allowCheckInRanking"
@update:value="updateServerSettings"
/>
<template #feedback>
启用后用户可以查看签到排行榜
</template>
</NFormItem>
</template>
<!-- 客户端回复设置 -->
<NDivider title-placement="left">
回复消息设置
</NDivider>
<!-- 签到模板帮助信息组件 -->
<div style="margin-bottom: 12px">
<TemplateHelper :placeholders="checkInPlaceholders" />
<NAlert
type="info"
:show-icon="false"
style="margin-top: 8px"
>
<template #header>
<div
style="display: flex; align-items: center; font-weight: bold"
>
<NIcon
:component="Info24Filled"
style="margin-right: 4px"
/>
签到模板可用变量列表
</div>
</template>
</NAlert>
</div>
<!-- 使用 AutoActionEditor 编辑 action 配置 -->
<AutoActionEditor
:action="config.successAction"
:hide-name="true"
:hide-enabled="true"
/>
<AutoActionEditor
:action="config.cooldownAction"
:hide-name="true"
:hide-enabled="true"
/>
<NDivider title-placement="left">
早鸟奖励设置
</NDivider>
<NFormItem label="启用早鸟奖励">
<NSwitch v-model:value="config.earlyBird.enabled" />
<NFormItem label="发送签到回复">
<NSwitch v-model:value="config.sendReply" />
<template #feedback>
在直播开始后的一段时间内签到可获得额外奖励
启用后签到成功或重复签到时会发送弹幕回复关闭则只显示通知不发送弹幕
</template>
</NFormItem>
<template v-if="config.earlyBird.enabled">
<NFormItem label="早鸟时间窗口 (分钟)">
<NInputNumber
v-model:value="config.earlyBird.windowMinutes"
:min="1"
style="width: 100%"
<template v-if="config.sendReply">
<!-- 签到模板帮助信息组件 -->
<div style="margin-bottom: 12px">
<TemplateHelper :placeholders="checkInPlaceholders" />
<NAlert
type="info"
:show-icon="false"
style="margin-top: 8px"
>
<template #header>
<div
style="display: flex; align-items: center; font-weight: bold"
>
<NIcon
:component="Info24Filled"
style="margin-right: 4px"
/>
签到模板可用变量列表
</div>
</template>
</NAlert>
</div>
<!-- 使用 AutoActionEditor 编辑 action 配置 -->
<NFormItem label="签到成功回复">
<AutoActionEditor
:action="config.successAction"
:hide-name="true"
:hide-enabled="true"
/>
<template #feedback>
直播开始后多少分钟内视为早鸟
</template>
</NFormItem>
<NFormItem label="早鸟额外奖励积分">
<NInputNumber
v-model:value="config.earlyBird.bonusPoints"
:min="0"
style="width: 100%"
<NFormItem label="签到冷却回复">
<AutoActionEditor
:action="config.cooldownAction"
:hide-name="true"
:hide-enabled="true"
/>
<template #feedback>
成功触发早鸟签到的用户额外获得的积分
</template>
</NFormItem>
<AutoActionEditor
:action="config.earlyBird.successAction"
:hide-name="true"
:hide-enabled="true"
/>
</template>
</template>
</NForm>
<NFormItem>
<NButton
type="primary"
:disabled="!canEdit"
:loading="isLoading"
@click="updateSettings"
>
保存所有设置
</NButton>
</NFormItem>
</NForm>
</NSpin>
</NTabPane>
<NTabPane
name="userStats"
tab="用户签到情况"
name="checkInRanking"
tab="签到排行榜"
>
<div class="checkin-stats">
<div class="checkin-ranking">
<NSpace vertical>
<NAlert type="info">
以下显示用户签到统计信息包括累计签到次数连续签到天数和早鸟签到次数等
显示用户签到排行榜包括连续签到天数和积分情况选择时间段可查看不同期间的签到情况
</NAlert>
<div class="ranking-filter">
<NSpace align="center">
<span>时间段</span>
<NSelect
v-model:value="timeRange"
style="width: 180px"
:options="timeRangeOptions"
@update:value="loadCheckInRanking"
/>
<span>用户名</span>
<NInput
v-model:value="userFilter"
placeholder="搜索用户"
clearable
style="width: 150px"
/>
<NButton
type="primary"
:loading="isLoadingRanking"
@click="loadCheckInRanking"
>
刷新排行榜
</NButton>
</NSpace>
</div>
<NDataTable
:columns="userStatsColumns"
:data="userStatsData"
:pagination="{ pageSize: 10 }"
:columns="rankingColumns"
:data="filteredRankingData"
:pagination="{
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50, 100],
onChange: (page: number) => pagination.page = page,
onUpdatePageSize: (pageSize: number) => pagination.pageSize = pageSize
}"
:bordered="false"
:loading="isLoadingRanking"
striped
/>
<NEmpty
v-if="!userStatsData.length"
description="暂无用户签到数据"
/>
<NDivider />
<div class="ranking-actions">
<NSpace vertical>
<NAlert type="warning">
以下操作将重置用户的签到记录请谨慎操作重置后数据无法恢复
</NAlert>
<NSpace justify="end">
<NPopconfirm @positive-click="resetAllCheckIn">
<template #trigger>
<NButton
type="error"
:disabled="isResetting"
:loading="isResetting"
>
重置所有用户签到数据
</NButton>
</template>
<template #default>
<div style="max-width: 250px">
<p>警告此操作将清空所有用户的签到记录包括连续签到天数等数据且不可恢复</p>
<p>确定要继续吗</p>
</div>
</template>
</NPopconfirm>
</NSpace>
</NSpace>
</div>
</NSpace>
</div>
</NTabPane>
@@ -181,7 +317,7 @@
在此可以模拟用户签到测试签到功能是否正常工作
</NAlert>
<NForm>
<NForm :label-width="100">
<NFormItem label="用户UID">
<NInputNumber
v-model:value="testUid"
@@ -199,7 +335,7 @@
<NFormItem>
<NButton
type="primary"
:disabled="!testUid || !config?.enabled"
:disabled="!testUid || !serverSetting.enableCheckIn"
@click="handleTestCheckIn"
>
模拟签到
@@ -240,101 +376,341 @@
</template>
<script lang="ts" setup>
import { NCard, NForm, NFormItem, NSwitch, NInput, NInputNumber, NSpace, NText, NDivider, NAlert, NIcon, NTabs, NTabPane, NDataTable, NEmpty, NButton } from 'naive-ui';
import { SaveSetting, useAccount } from '@/api/account';
import { CheckInRankingInfo, CheckInResult } from '@/api/api-models';
import { QueryGetAPI } from '@/api/query';
import { useAutoAction } from '@/client/store/useAutoAction';
import TemplateEditor from '../TemplateEditor.vue';
import TemplateHelper from '../TemplateHelper.vue';
import { TriggerType, ActionType, Priority, RuntimeState } from '@/client/store/autoAction/types';
import { EventModel, EventDataTypes } from '@/api/api-models';
import { CHECKIN_API_URL } from '@/data/constants';
import { GuidUtils } from '@/Utils';
import { Info24Filled } from '@vicons/fluent';
import { computed, h, ref } from 'vue';
import type { UserCheckInData } from '@/client/store/autoAction/modules/checkin';
import type { DataTableColumns } from 'naive-ui';
import { NAlert, NButton, NCard, NDataTable, NDivider, NEmpty, NForm, NFormItem, NIcon, NInput, NInputGroup, NInputNumber, NPopconfirm, NSelect, NSpace, NSpin, NSwitch, NTabPane, NTabs, NText } from 'naive-ui';
import { computed, h, onMounted, ref, watch } from 'vue';
import AutoActionEditor from '../AutoActionEditor.vue';
import TemplateHelper from '../TemplateHelper.vue';
interface LiveInfo {
roomId?: number;
}
const autoActionStore = useAutoAction();
const config = autoActionStore.checkInModule.checkInConfig;
const checkInStorage = autoActionStore.checkInModule.checkInStorage;
const accountInfo = useAccount();
const isLoading = ref(false);
// 签到模板的特定占位符
const checkInPlaceholders = [
{ name: '{{checkin.points}}', description: '基础签到积分' },
{ name: '{{checkin.bonusPoints}}', description: '早鸟额外积分 (普通签到为0)' },
{ name: '{{checkin.totalPoints}}', description: '本次总获得积分' },
{ name: '{{checkin.userPoints}}', description: '用户当前积分' },
{ name: '{{checkin.isEarlyBird}}', description: '是否是早鸟签到 (true/false)' },
{ name: '{{checkin.cooldownSeconds}}', description: '签到冷却时间(秒)' },
{ name: '{{checkin.points}}', description: '获得的总积分' },
{ name: '{{checkin.consecutiveDays}}', description: '连续签到天数' },
{ name: '{{checkin.todayRank}}', description: '今日签到排名' },
{ name: '{{checkin.time}}', description: '签到时间对象' }
];
// 为签到模板自定义的测试上下文
const checkInTestContext = computed(() => {
if (!config) return undefined;
return {
checkin: {
points: config.points || 0,
bonusPoints: config.earlyBird.enabled ? config.earlyBird.bonusPoints : 0,
totalPoints: (config.points || 0) + (config.earlyBird.enabled ? config.earlyBird.bonusPoints : 0),
userPoints: 1000, // 模拟用户当前积分
isEarlyBird: false,
cooldownSeconds: config.cooldownSeconds || 0,
time: new Date()
}
};
// 服务端签到设置
const serverSetting = computed(() => {
return accountInfo.value?.settings?.point || {};
});
// 用户签到数据表格列定义
const userStatsColumns = [
// 是否可以编辑设置
const canEdit = computed(() => {
return accountInfo.value && accountInfo.value.settings && accountInfo.value.settings.point;
});
// 更新所有设置
async function updateSettings() {
// 先保存服务端设置
const serverSaved = await updateServerSettings();
if (serverSaved) {
window.$notification.success({
title: '设置已保存',
duration: 3000
});
}
return serverSaved;
}
// 更新服务端签到设置
async function updateServerSettings() {
if (!canEdit.value) {
return false;
}
isLoading.value = true;
try {
const msg = await SaveSetting('Point', accountInfo.value.settings.point);
if (msg) {
return true;
} else {
window.$notification.error({
title: '保存失败',
content: msg,
duration: 5000
});
}
} catch (err) {
window.$notification.error({
title: '保存失败',
content: String(err),
duration: 5000
});
console.error('保存签到设置失败:', err);
} finally {
isLoading.value = false;
}
return false;
}
// 排行榜数据
const rankingData = ref<CheckInRankingInfo[]>([]);
const isLoadingRanking = ref(false);
const timeRange = ref<string>('all');
const userFilter = ref<string>('');
const pagination = ref({
page: 1,
pageSize: 10
});
// 时间段选项
const timeRangeOptions = [
{ label: '全部时间', value: 'all' },
{ label: '今日', value: 'today' },
{ label: '本周', value: 'week' },
{ label: '本月', value: 'month' },
{ label: '上个月', value: 'lastMonth' },
];
// 过滤后的排行榜数据
const filteredRankingData = computed(() => {
let filtered = rankingData.value;
// 按时间范围筛选
if (timeRange.value !== 'all') {
const now = new Date();
let startTime: Date;
if (timeRange.value === 'today') {
// 今天凌晨
startTime = new Date(now);
startTime.setHours(0, 0, 0, 0);
} else if (timeRange.value === 'week') {
// 本周一
const dayOfWeek = now.getDay() || 7; // 把周日作为7处理
startTime = new Date(now);
startTime.setDate(now.getDate() - (dayOfWeek - 1));
startTime.setHours(0, 0, 0, 0);
} else if (timeRange.value === 'month') {
// 本月1号
startTime = new Date(now.getFullYear(), now.getMonth(), 1);
} else if (timeRange.value === 'lastMonth') {
// 上月1号
startTime = new Date(now.getFullYear(), now.getMonth() - 1, 1);
// 本月1号作为结束时间
const endTime = new Date(now.getFullYear(), now.getMonth(), 1);
filtered = filtered.filter(user => {
const checkInTime = new Date(user.lastCheckInTime);
return checkInTime >= startTime && checkInTime < endTime;
});
// 已经筛选完成,不需要再次筛选
startTime = new Date(0);
}
// 如果不是上个月,用通用筛选逻辑
if (timeRange.value !== 'lastMonth') {
filtered = filtered.filter(user => {
const checkInTime = new Date(user.lastCheckInTime);
return checkInTime >= startTime;
});
}
}
// 按用户名筛选
if (userFilter.value) {
const keyword = userFilter.value.toLowerCase();
filtered = filtered.filter(user =>
user.name.toLowerCase().includes(keyword)
);
}
return filtered;
});
// 排行榜列定义
const rankingColumns: DataTableColumns<CheckInRankingInfo> = [
{
title: '用户ID',
key: 'uid'
title: '排名',
key: 'rank',
render: (row: CheckInRankingInfo, index: number) => h('span', {}, index + 1)
},
{
title: '用户名',
key: 'username'
key: 'name'
},
{
title: '首次签到时间',
key: 'firstCheckInTime',
render(row: UserCheckInData) {
return h('span', {}, new Date(row.firstCheckInTime).toLocaleString());
}
title: '连续签到天数',
key: 'consecutiveDays',
sorter: 'default'
},
{
title: '积分',
key: 'points',
sorter: 'default'
},
{
title: '最近签到时间',
key: 'lastCheckInTime',
sorter: true,
defaultSortOrder: 'descend' as const,
render(row: UserCheckInData) {
render(row: CheckInRankingInfo) {
return h('span', {}, new Date(row.lastCheckInTime).toLocaleString());
},
sorter: 'default'
},
{
title: '已认证',
key: 'isAuthed',
render(row: CheckInRankingInfo) {
return h('span', {}, row.isAuthed ? '是' : '否');
}
},
{
title: '累计签到',
key: 'totalCheckins',
sorter: true
},
{
title: '连续签到',
key: 'streakDays',
sorter: true
},
{
title: '早鸟签到次数',
key: 'earlyBirdCount',
sorter: true
title: '操作',
key: 'actions',
render(row: CheckInRankingInfo) {
return h(
NPopconfirm,
{
onPositiveClick: () => resetUserCheckInByGuid(row.ouId)
},
{
trigger: () => h(
NButton,
{
size: 'small',
type: 'warning',
disabled: isResetting.value,
loading: isResetting.value && resetTargetId.value === row.ouId,
onClick: (e) => e.stopPropagation()
},
{ default: () => '重置签到' }
),
default: () => '确定要重置该用户的所有签到数据吗?此操作不可撤销。'
}
);
}
}
];
// 转换用户签到数据为表格可用格式
const userStatsData = computed<UserCheckInData[]>(() => {
if (!checkInStorage?.users) {
return [];
}
// 加载签到排行榜数据
async function loadCheckInRanking() {
if (isLoadingRanking.value) return;
// 将对象转换为数组
return Object.values(checkInStorage.users);
});
isLoadingRanking.value = true;
try {
// 获取所有用户数据,不再根据时间范围过滤
const response = await QueryGetAPI<CheckInRankingInfo[]>(`${CHECKIN_API_URL}admin/users`);
if (response.code == 200) {
rankingData.value = response.data;
pagination.value.page = 1; // 重置为第一页
} else {
rankingData.value = [];
window.$message.error(`获取签到排行榜失败: ${response.message}`);
}
} catch (error) {
console.error('加载签到排行榜失败:', error);
window.$notification.error({
title: '加载失败',
content: '无法加载签到排行榜数据',
duration: 5000
});
rankingData.value = [];
} finally {
isLoadingRanking.value = false;
}
}
// 重置签到数据相关
const isResetting = ref(false);
const resetTargetId = ref<string>();
// 重置单个用户签到数据
async function resetUserCheckInByGuid(ouId: string) {
if (!ouId || isResetting.value) return;
isResetting.value = true;
resetTargetId.value = ouId;
try {
const response = await QueryGetAPI(`${CHECKIN_API_URL}admin/reset`, {
ouId: ouId
});
if (response && response.code === 200) {
window.$notification.success({
title: '重置成功',
content: '用户签到数据已重置',
duration: 3000
});
// 重置成功后重新加载排行榜
await loadCheckInRanking();
} else {
window.$notification.error({
title: '重置失败',
content: response?.message || '无法重置用户签到数据',
duration: 5000
});
}
} catch (error) {
console.error('重置用户签到数据失败:', error);
window.$notification.error({
title: '重置失败',
content: '重置用户签到数据时发生错误',
duration: 5000
});
} finally {
isResetting.value = false;
resetTargetId.value = undefined;
}
}
// 重置所有用户签到数据
async function resetAllCheckIn() {
if (isResetting.value) return;
isResetting.value = true;
try {
const response = await QueryGetAPI(`${CHECKIN_API_URL}admin/reset`, {});
if (response && response.code === 200) {
window.$notification.success({
title: '重置成功',
content: '所有用户的签到数据已重置',
duration: 3000
});
// 重置成功后重新加载排行榜
await loadCheckInRanking();
} else {
window.$notification.error({
title: '重置失败',
content: response?.message || '无法重置所有用户签到数据',
duration: 5000
});
}
} catch (error) {
console.error('重置所有用户签到数据失败:', error);
window.$notification.error({
title: '重置失败',
content: '重置所有用户签到数据时发生错误',
duration: 5000
});
} finally {
isResetting.value = false;
}
}
// 测试签到功能
const testUid = ref<number>();
@@ -343,7 +719,7 @@ const testResult = ref<{ success: boolean; message: string }>();
// 处理测试签到
async function handleTestCheckIn() {
if (!testUid.value || !config?.enabled) {
if (!testUid.value || !serverSetting.value.enableCheckIn) {
testResult.value = {
success: false,
message: '请输入有效的UID或确保签到功能已启用'
@@ -352,49 +728,61 @@ async function handleTestCheckIn() {
}
try {
// 创建唯一标识符ouid基于用户输入的uid
const userOuid = testUid.value.toString();
// 直接调用服务端签到API
const response = await QueryGetAPI<CheckInResult>(`${CHECKIN_API_URL}check-in-for`, {
uId: testUid.value,
name: testUsername.value || '测试用户'
});
// 创建模拟的事件对象
const mockEvent: EventModel = {
type: EventDataTypes.Message,
uname: testUsername.value || '测试用户',
uface: '',
uid: testUid.value,
open_id: '',
msg: config.command,
time: Date.now(),
num: 0,
price: 0,
guard_level: 0,
fans_medal_level: 0,
fans_medal_name: '',
fans_medal_wearing_status: false,
ouid: userOuid
};
if (response.code === 200 && response.data) {
const result = response.data;
// 创建模拟的运行时状态
const mockRuntimeState: RuntimeState = {
lastExecutionTime: {},
aggregatedEvents: {},
scheduledTimers: {},
timerStartTimes: {},
globalTimerStartTime: null,
sentGuardPms: new Set<number>()
};
testResult.value = {
success: result.success,
message: result.success
? `签到成功!用户 ${testUsername.value || '测试用户'} 获得 ${result.points} 积分,连续签到 ${result.consecutiveDays}`
: result.message || '签到失败,可能今天已经签到过了'
};
// 处理签到请求
await autoActionStore.checkInModule.processCheckIn(mockEvent, mockRuntimeState);
testResult.value = {
success: true,
message: `已为用户 ${testUsername.value || '测试用户'}(UID: ${testUid.value}) 模拟签到操作,请查看用户签到情况选项卡确认结果`
};
// 显示通知
window.$notification[result.success ? 'success' : 'info']({
title: result.success ? '测试签到成功' : '测试签到失败',
content: testResult.value.message,
duration: 3000
});
} else {
testResult.value = {
success: false,
message: `API返回错误: ${response.message || '未知错误'}`
};
}
} catch (error) {
testResult.value = {
success: false,
message: `签到操作失败: ${error instanceof Error ? error.message : String(error)}`
};
// 显示错误通知
window.$notification.error({
title: '测试签到失败',
content: testResult.value.message,
duration: 5000
});
}
}
// 组件挂载时加载排行榜
onMounted(() => {
loadCheckInRanking();
});
</script>
<style scoped>
.settings-section {
margin: 8px 0;
}
.ranking-filter {
margin: 10px 0;
}
</style>