Files
vtsuru.live/src/client/components/autoaction/settings/CheckInSettings.vue
Megghy 4bcb966bdc feat: 添加自定义测试上下文和更新模板设置
- 在 AutoActionEditor 和 TemplateSettings 组件中添加 customTestContext 属性
- 更新 CheckInSettings 组件以使用 customTestContext
- 移除 TemplateEditor 中的操作按钮部分
2025-05-02 02:34:56 +08:00

799 lines
24 KiB
Vue
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.
<template>
<NCard
v-if="config"
title="弹幕签到设置"
size="small"
>
<NTabs
type="line"
animated
>
<NTabPane
name="settings"
tab="签到设置"
>
<NSpin :show="isLoading">
<NAlert
v-if="!canEdit"
type="warning"
>
加载中或无法编辑设置请稍后再试
</NAlert>
<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>
<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 label="允许自己签到">
<NSwitch
v-model:value="serverSetting.allowSelfCheckIn"
@update:value="updateServerSettings"
/>
<template #feedback>
启用后主播自己也可以签到获得积分
</template>
</NFormItem>
<NFormItem label="要求用户已认证">
<NSwitch
v-model:value="serverSetting.requireAuth"
@update:value="updateServerSettings"
/>
<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>
<NFormItem label="发送签到回复">
<NSwitch v-model:value="config.sendReply" />
<template #feedback>
启用后签到成功或重复签到时会发送弹幕回复关闭则只显示通知不发送弹幕
</template>
</NFormItem>
<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>
<NDivider title-placement="left">
签到成功回复
</NDivider>
<AutoActionEditor
:action="config.successAction"
:hide-name="true"
:hide-enabled="true"
:custom-test-context="customTestContext"
/>
<NDivider title-placement="left">
签到冷却回复
</NDivider>
<AutoActionEditor
:action="config.cooldownAction"
:hide-name="true"
:hide-enabled="true"
:custom-test-context="customTestContext"
/>
</template>
<NFormItem>
<NButton
type="primary"
:disabled="!canEdit"
:loading="isLoading"
@click="updateSettings"
>
保存所有设置
</NButton>
</NFormItem>
</NForm>
</NSpin>
</NTabPane>
<NTabPane
name="checkInRanking"
tab="签到排行榜"
>
<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="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
/>
<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>
<NTabPane
name="testCheckIn"
tab="测试签到"
>
<div class="test-checkin">
<NSpace vertical>
<NAlert type="info">
在此可以模拟用户签到测试签到功能是否正常工作
</NAlert>
<NForm :label-width="100">
<NFormItem label="用户UID">
<NInputNumber
v-model:value="testUid"
:min="1"
style="width: 100%"
placeholder="输入用户数字ID"
/>
</NFormItem>
<NFormItem label="用户名">
<NInput
v-model:value="testUsername"
placeholder="输入用户名,默认为'测试用户'"
/>
</NFormItem>
<NFormItem>
<NButton
type="primary"
:disabled="!testUid || !serverSetting.enableCheckIn"
@click="handleTestCheckIn"
>
模拟签到
</NButton>
</NFormItem>
</NForm>
<NDivider title-placement="left">
测试结果
</NDivider>
<NCard
v-if="testResult"
size="small"
:title="testResult.success ? '签到成功' : '签到失败'"
>
<NText>{{ testResult.message }}</NText>
</NCard>
</NSpace>
</div>
</NTabPane>
</NTabs>
<NText
:depth="3"
style="font-size: 12px; margin-top: 15px; display: block"
>
提示签到成功发送的回复消息会遵循全局的弹幕发送设置如频率限制弹幕长度等
</NText>
</NCard>
<NCard
v-else
title="加载中..."
size="small"
>
<NText>正在加载签到设置...</NText>
</NCard>
</template>
<script lang="ts" setup>
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 { CHECKIN_API_URL } from '@/data/constants';
import { GuidUtils } from '@/Utils';
import { Info24Filled } from '@vicons/fluent';
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 accountInfo = useAccount();
const isLoading = ref(false);
const customTestContext = ref({
checkin: {
points: 0,
consecutiveDays: 0,
todayRank: 0,
time: new Date()
}
});
// 签到模板的特定占位符
const checkInPlaceholders = [
{ name: '{{checkin.points}}', description: '获得的总积分' },
{ name: '{{checkin.consecutiveDays}}', description: '连续签到天数' },
{ name: '{{checkin.todayRank}}', description: '今日签到排名' },
{ name: '{{checkin.time}}', description: '签到时间对象' }
];
// 服务端签到设置
const serverSetting = computed(() => {
return accountInfo.value?.settings?.point || {};
});
// 是否可以编辑设置
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: '排名',
key: 'rank',
render: (row: CheckInRankingInfo, index: number) => h('span', {}, index + 1)
},
{
title: '用户名',
key: 'name'
},
{
title: '连续签到天数',
key: 'consecutiveDays',
sorter: 'default'
},
{
title: '积分',
key: 'points',
sorter: 'default'
},
{
title: '最近签到时间',
key: 'lastCheckInTime',
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: '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: () => '确定要重置该用户的所有签到数据吗?此操作不可撤销。'
}
);
}
}
];
// 加载签到排行榜数据
async function loadCheckInRanking() {
if (isLoadingRanking.value) return;
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>();
const testUsername = ref<string>('测试用户');
const testResult = ref<{ success: boolean; message: string }>();
// 处理测试签到
async function handleTestCheckIn() {
if (!testUid.value || !serverSetting.value.enableCheckIn) {
testResult.value = {
success: false,
message: '请输入有效的UID或确保签到功能已启用'
};
return;
}
try {
// 直接调用服务端签到API
const response = await QueryGetAPI<CheckInResult>(`${CHECKIN_API_URL}check-in-for`, {
uId: testUid.value,
name: testUsername.value || '测试用户'
});
if (response.code === 200 && response.data) {
const result = response.data;
testResult.value = {
success: result.success,
message: result.success
? `签到成功!用户 ${testUsername.value || '测试用户'} 获得 ${result.points} 积分,连续签到 ${result.consecutiveDays}`
: result.message || '签到失败,可能今天已经签到过了'
};
// 显示通知
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>