feat: 更新组件和配置,增强功能和用户体验, 添加签到功能

- 在 .editorconfig 中调整文件格式设置,统一代码风格。
- 在 default.d.ts 中为 naive-ui 添加 TabPaneSlots 接口声明,增强类型支持。
- 在多个组件中优化了模板和样式,提升用户交互体验。
- 在 ClientAutoAction.vue 中新增签到设置标签页,丰富功能选项。
- 在 Utils.ts 中增强 GUID 处理逻辑,增加输入验证和错误处理。
- 更新多个组件的逻辑,简化代码结构,提升可读性和维护性。
This commit is contained in:
2025-04-26 01:35:59 +08:00
parent e48b3df236
commit 8bed5bbc1a
24 changed files with 2004 additions and 328 deletions

View File

@@ -0,0 +1,155 @@
<template>
<div class="checkin-template-helper">
<TemplateHelper :placeholders="checkInPlaceholders" />
<NAlert
type="info"
:show-icon="false"
style="margin-top: 8px;"
>
<template #header>
<div class="alert-header">
<NIcon
:component="Info24Filled"
style="margin-right: 4px;"
/>
签到模板可用变量列表
</div>
</template>
<NDivider style="margin: 6px 0;" />
<div class="placeholder-groups">
<div class="placeholder-group">
<div class="group-title">
用户信息
</div> <div class="placeholder-item">
<code>&#123;&#123;user.name&#125;&#125;</code> - 用户名称
</div>
<div class="placeholder-item">
<code>&#123;&#123;user.uid&#125;&#125;</code> - 用户ID
</div>
</div>
<div class="placeholder-group">
<div class="group-title">
签到信息
</div> <div class="placeholder-item">
<code>&#123;&#123;checkin.points&#125;&#125;</code> - 基础签到积分
</div>
<div class="placeholder-item">
<code>&#123;&#123;checkin.bonusPoints&#125;&#125;</code> - 早鸟额外积分 (普通签到为0)
</div>
<div class="placeholder-item">
<code>&#123;&#123;checkin.totalPoints&#125;&#125;</code> - 总获得积分
</div>
<div class="placeholder-item">
<code>&#123;&#123;checkin.isEarlyBird&#125;&#125;</code> - 是否是早鸟签到 (true/false)
</div>
<div class="placeholder-item">
<code>&#123;&#123;checkin.cooldownSeconds&#125;&#125;</code> - 签到冷却时间()
</div>
<div class="placeholder-item">
<code>&#123;&#123;checkin.time&#125;&#125;</code> - 签到时间对象
</div>
</div>
</div>
<NDivider style="margin: 6px 0;" />
<div class="placeholder-example">
<div class="example-title">
示例模板:
</div> <div class="example-item">
普通签到: <code>&#123;&#123;user.name&#125;&#125; 签到成功获得 &#123;&#123;checkin.totalPoints&#125;&#125; 积分</code>
</div>
<div class="example-item">
早鸟签到: <code>恭喜 &#123;&#123;user.name&#125;&#125; 完成早鸟签到额外获得 &#123;&#123;checkin.bonusPoints&#125;&#125; 积分共获得 &#123;&#123;checkin.totalPoints&#125;&#125; 积分</code>
</div>
<div class="example-item">
条件表达式: <code>&#123;&#123;js: checkin.isEarlyBird ? `恭喜 ${user.name} 获得早鸟奖励!` : `${user.name} 签到成功!`&#125;&#125; 获得 &#123;&#123;checkin.totalPoints&#125;&#125; 积分</code>
</div>
</div>
</NAlert>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { NAlert, NDivider, NIcon } from 'naive-ui';
import { Info24Filled } from '@vicons/fluent';
import TemplateHelper from './TemplateHelper.vue';
// 签到模板的特定占位符
const checkInPlaceholders = [
{ name: '{{user.name}}', description: '用户名称' },
{ name: '{{user.uid}}', description: '用户ID' },
{ name: '{{checkin.points}}', description: '基础签到积分' },
{ name: '{{checkin.bonusPoints}}', description: '早鸟额外积分 (普通签到为0)' },
{ name: '{{checkin.totalPoints}}', description: '总获得积分' },
{ name: '{{checkin.isEarlyBird}}', description: '是否是早鸟签到 (true/false)' },
{ name: '{{checkin.cooldownSeconds}}', description: '签到冷却时间(秒)' },
{ name: '{{checkin.time}}', description: '签到时间对象' }
];
</script>
<style scoped>
.checkin-template-helper {
margin-bottom: 12px;
}
.alert-header {
display: flex;
align-items: center;
font-weight: bold;
}
.placeholder-groups {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.placeholder-group {
flex: 1;
min-width: 200px;
}
.group-title {
font-weight: bold;
margin-bottom: 6px;
font-size: 14px;
}
.placeholder-item {
margin-bottom: 4px;
font-size: 13px;
}
.placeholder-item code {
padding: 1px 4px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 3px;
font-size: 12px;
}
.placeholder-example {
margin-top: 8px;
}
.example-title {
font-weight: bold;
margin-bottom: 6px;
font-size: 14px;
}
.example-item {
margin-bottom: 4px;
font-size: 13px;
}
.example-item code {
display: block;
padding: 4px 8px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 3px;
margin-top: 2px;
font-size: 12px;
white-space: nowrap;
overflow: auto;
}
</style>

View File

@@ -31,7 +31,7 @@ function handleTemplateUpdate(payload: { index: number, value: string }) {
<template>
<TemplateEditor
:action="props.action"
:template="props.action"
:template-index="0"
:title="title"
:description="description"

View File

@@ -2,7 +2,6 @@
import { NButton, NCard, NDivider, NHighlight, NInput, NScrollbar, NSpace, NModal, useMessage, NTabs, NTabPane, NFlex, NAlert, NIcon, NCollapse, NCollapseItem, NBadge, NText } from 'naive-ui';
import { computed, ref, watch } from 'vue';
import TemplateHelper from './TemplateHelper.vue';
import TemplateTester from './TemplateTester.vue';
import { containsJsExpression, convertToJsExpressions, evaluateTemplateExpressions, extractJsExpressions, JS_EXPRESSION_REGEX } from '@/client/store/autoAction/expressionEvaluator';
import { buildExecutionContext } from '@/client/store/autoAction/utils';
import { AutoActionItem, TriggerType } from '@/client/store/autoAction/types';
@@ -11,7 +10,7 @@ import { EventDataTypes, EventModel } from '@/api/api-models';
import GraphemeSplitter from 'grapheme-splitter';
const props = defineProps({
action: {
template: {
type: Object as () => AutoActionItem,
required: true
},
@@ -26,6 +25,10 @@ const props = defineProps({
checkLength: {
type: Boolean,
default: true
},
customTestContext: {
type: Object,
default: undefined
}
});
@@ -52,7 +55,7 @@ const mergedPlaceholders = computed(() => {
const specificPlaceholders: { name: string, description: string }[] = [];
switch (props.action.triggerType) {
switch (props.template.triggerType) {
case TriggerType.DANMAKU:
specificPlaceholders.push(
{ name: '{{message}}', description: '弹幕内容' },
@@ -87,8 +90,27 @@ const mergedPlaceholders = computed(() => {
return Array.from(new Map(finalPlaceholders.map(item => [item.name, item])).values());
});
// 深度合并两个对象的辅助函数
function deepMerge(target: any, source: any): any {
if (!source) return target;
if (!target) return source;
const result = { ...target };
Object.keys(source).forEach(key => {
if (typeof source[key] === 'object' && source[key] !== null && typeof target[key] === 'object' && target[key] !== null) {
result[key] = deepMerge(target[key], source[key]);
} else if (source[key] !== undefined) {
result[key] = source[key];
}
});
return result;
}
const testContext = computed(() => {
return buildExecutionContext({
// 创建默认上下文
const defaultContext = buildExecutionContext({
msg: '测试',
time: 1713542400,
num: 1,
@@ -105,8 +127,14 @@ const testContext = computed(() => {
fans_medal_wearing_status: true,
guard_level_name: '测试舰队',
guard_level_price: 100,
}, undefined, props.template.triggerType);
}, undefined, props.action.triggerType);
// 如果有自定义上下文,将其与默认上下文合并
if (props.customTestContext) {
return deepMerge(defaultContext, props.customTestContext);
}
return defaultContext;
});
const message = useMessage();
@@ -120,13 +148,13 @@ function countGraphemes(value: string) {
}
function convertPlaceholders() {
if (!props.action.template) {
if (!props.template.template) {
message.warning('请先输入模板内容');
return;
}
const converted = convertToJsExpressions(props.action.template, mergedPlaceholders.value);
if (converted !== props.action.template) {
props.action.template = converted;
const converted = convertToJsExpressions(props.template.template, mergedPlaceholders.value);
if (converted !== props.template.template) {
props.template.template = converted;
message.success('已转换占位符为表达式格式');
} else {
message.info('模板中没有需要转换的占位符');
@@ -139,7 +167,7 @@ function hasJsExpression(template: string): boolean {
const highlightPatterns = computed(() => {
const simplePlaceholders = mergedPlaceholders.value.map(p => p.name);
const jsExpressionsInTemplate = extractJsExpressions(props.action.template || '');
const jsExpressionsInTemplate = extractJsExpressions(props.template.template || '');
const allPatterns = [...new Set([...simplePlaceholders, ...jsExpressionsInTemplate])];
return allPatterns;
});
@@ -148,7 +176,20 @@ const MAX_LENGTH = 20;
const WARNING_THRESHOLD = 16;
function evaluateTemplateForUI(template: string): string {
const executionContext = buildExecutionContext(testContext.value.event, undefined, props.action.triggerType);
// 深度合并默认上下文和自定义上下文
const executionContext = buildExecutionContext(testContext.value.event, undefined, props.template.triggerType);
// 如果有自定义上下文,将其深度合并到执行上下文中
if (props.customTestContext) {
Object.keys(props.customTestContext).forEach(key => {
if (typeof props.customTestContext?.[key] === 'object' && props.customTestContext[key] !== null) {
executionContext.variables[key] = deepMerge(executionContext.variables[key] || {}, props.customTestContext[key]);
} else {
executionContext.variables[key] = props.customTestContext?.[key];
}
});
}
try {
return evaluateTemplateExpressions(template, executionContext);
} catch (error) {
@@ -158,8 +199,8 @@ function evaluateTemplateForUI(template: string): string {
}
const evaluatedTemplateResult = computed(() => {
if (!props.action.template || !showLivePreview.value) return '';
return evaluateTemplateForUI(props.action.template);
if (!props.template.template || !showLivePreview.value) return '';
return evaluateTemplateForUI(props.template.template);
});
const previewResult = computed(() => {
@@ -167,7 +208,7 @@ const previewResult = computed(() => {
});
const lengthStatus = computed(() => {
if (!props.action.template || !props.checkLength || !showLivePreview.value) {
if (!props.template.template || !props.checkLength || !showLivePreview.value) {
return { status: 'normal' as const, message: '' };
}
try {
@@ -230,7 +271,7 @@ const templateExamples = [
];
function insertExample(template: string) {
props.action.template = template;
props.template.template = template;
message.success('已插入示例模板');
}
</script>
@@ -286,7 +327,7 @@ function insertExample(template: string) {
<!-- 当前模板预览 -->
<NInput
v-model:value="action.template"
v-model:value="template.template"
type="textarea"
placeholder="输入模板内容... 使用 {{变量名}} 插入变量, {{js: 表达式}} 执行JS"
:autosize="{ minRows: 3, maxRows: 6 }"
@@ -352,15 +393,6 @@ function insertExample(template: string) {
>
占位符转表达式
</NButton>
<NButton
type="primary"
size="small"
class="btn-with-transition"
@click="activeTab = 'test'"
>
测试模板
</NButton>
</NFlex>
<!-- 模板示例 -->
@@ -403,17 +435,6 @@ function insertExample(template: string) {
</NCollapse>
</NFlex>
</NTabPane>
<NTabPane
name="test"
tab="测试"
>
<TemplateTester
:default-template="action.template"
:context="testContext"
:placeholders="mergedPlaceholders"
/>
</NTabPane>
</NTabs>
<!-- 新增 Modal 组件 -->

View File

@@ -0,0 +1,398 @@
<template>
<NCard
v-if="config"
title="弹幕签到设置"
size="small"
>
<NTabs
type="line"
animated
>
<NTabPane
name="settings"
tab="签到设置"
>
<NForm
label-placement="left"
label-width="auto"
>
<NFormItem label="启用签到功能">
<NSwitch v-model:value="config.enabled" />
</NFormItem>
<template v-if="config.enabled">
<NFormItem label="签到指令">
<NInput
v-model:value="config.command"
placeholder="例如:签到"
/>
<template #feedback>
观众发送此指令触发签到
</template>
</NFormItem>
<NFormItem label="仅在直播时可签到">
<NSwitch v-model:value="config.onlyDuringLive" />
<template #feedback>
启用后仅在直播进行中才能签到否则任何时候都可以签到
</template>
</NFormItem>
<NFormItem label="发送签到回复">
<NSwitch v-model:value="config.sendReply" />
<template #feedback>
启用后签到成功或重复签到时会发送弹幕回复关闭则只显示通知不发送弹幕
</template>
</NFormItem>
<NFormItem label="签到成功获得积分">
<NInputNumber
v-model:value="config.points"
:min="0"
style="width: 100%"
/>
</NFormItem>
<NFormItem label="用户签到冷却时间 (秒)">
<NInputNumber
v-model:value="config.cooldownSeconds"
:min="0"
style="width: 100%"
/>
<template #feedback>
每个用户在指定秒数内只能签到一次
</template>
</NFormItem>
<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>
<TemplateEditor
v-model:template="config.successAction"
title="签到成功回复模板"
:custom-test-context="checkInTestContext"
/>
<TemplateEditor
v-model:template="config.cooldownAction"
title="冷却中回复模板"
:custom-test-context="checkInTestContext"
/>
<NDivider title-placement="left">
早鸟奖励设置
</NDivider>
<NFormItem label="启用早鸟奖励">
<NSwitch v-model:value="config.earlyBird.enabled" />
<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 #feedback>
直播开始后多少分钟内视为早鸟
</template>
</NFormItem>
<NFormItem label="早鸟额外奖励积分">
<NInputNumber
v-model:value="config.earlyBird.bonusPoints"
:min="0"
style="width: 100%"
/>
<template #feedback>
成功触发早鸟签到的用户额外获得的积分
</template>
</NFormItem> <TemplateEditor
v-model:template="config.earlyBird.successAction"
title="早鸟成功回复模板"
description="用户成功触发早鸟奖励时发送的回复消息,可用变量: {{user.name}}, {{checkin.bonusPoints}}, {{checkin.totalPoints}}, {{checkin.userPoints}}"
:custom-test-context="checkInTestContext"
/>
</template>
</template>
</NForm>
</NTabPane>
<NTabPane
name="userStats"
tab="用户签到情况"
>
<div class="checkin-stats">
<NSpace vertical>
<NAlert type="info">
以下显示用户的签到统计信息包括累计签到次数连续签到天数和早鸟签到次数等
</NAlert>
<NDataTable
:columns="userStatsColumns"
:data="userStatsData"
:pagination="{ pageSize: 10 }"
:bordered="false"
striped
/>
<NEmpty
v-if="!userStatsData.length"
description="暂无用户签到数据"
/>
</NSpace>
</div>
</NTabPane>
<NTabPane
name="testCheckIn"
tab="测试签到"
>
<div class="test-checkin">
<NSpace vertical>
<NAlert type="info">
在此可以模拟用户签到测试签到功能是否正常工作
</NAlert>
<NForm>
<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 || !config?.enabled"
@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 { NCard, NForm, NFormItem, NSwitch, NInput, NInputNumber, NSpace, NText, NDivider, NAlert, NIcon, NTabs, NTabPane, NDataTable, NEmpty, NButton } from 'naive-ui';
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 { Info24Filled } from '@vicons/fluent';
import { computed, h, ref } from 'vue';
import type { UserCheckInData } from '@/client/store/autoAction/modules/checkin';
const autoActionStore = useAutoAction();
const config = autoActionStore.checkInModule.checkInConfig;
const checkInStorage = autoActionStore.checkInModule.checkInStorage;
// 签到模板的特定占位符
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.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 userStatsColumns = [
{
title: '用户ID',
key: 'uid'
},
{
title: '用户名',
key: 'username'
},
{
title: '首次签到时间',
key: 'firstCheckInTime',
render(row: UserCheckInData) {
return h('span', {}, new Date(row.firstCheckInTime).toLocaleString());
}
},
{
title: '最近签到时间',
key: 'lastCheckInTime',
sorter: true,
defaultSortOrder: 'descend' as const,
render(row: UserCheckInData) {
return h('span', {}, new Date(row.lastCheckInTime).toLocaleString());
}
},
{
title: '累计签到',
key: 'totalCheckins',
sorter: true
},
{
title: '连续签到',
key: 'streakDays',
sorter: true
},
{
title: '早鸟签到次数',
key: 'earlyBirdCount',
sorter: true
}
];
// 转换用户签到数据为表格可用格式
const userStatsData = computed<UserCheckInData[]>(() => {
if (!checkInStorage?.users) {
return [];
}
// 将对象转换为数组
return Object.values(checkInStorage.users);
});
// 测试签到功能
const testUid = ref<number>();
const testUsername = ref<string>('测试用户');
const testResult = ref<{ success: boolean; message: string }>();
// 处理测试签到
async function handleTestCheckIn() {
if (!testUid.value || !config?.enabled) {
testResult.value = {
success: false,
message: '请输入有效的UID或确保签到功能已启用'
};
return;
}
try {
// 创建唯一标识符ouid基于用户输入的uid
const userOuid = testUid.value.toString();
// 创建模拟的事件对象
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
};
// 创建模拟的运行时状态
const mockRuntimeState: RuntimeState = {
lastExecutionTime: {},
aggregatedEvents: {},
scheduledTimers: {},
timerStartTimes: {},
globalTimerStartTime: null,
sentGuardPms: new Set<number>()
};
// 处理签到请求
await autoActionStore.checkInModule.processCheckIn(mockEvent, mockRuntimeState);
testResult.value = {
success: true,
message: `已为用户 ${testUsername.value || '测试用户'}(UID: ${testUid.value}) 模拟签到操作,请查看用户签到情况选项卡确认结果`
};
} catch (error) {
testResult.value = {
success: false,
message: `签到操作失败: ${error instanceof Error ? error.message : String(error)}`
};
}
}
</script>

View File

@@ -56,7 +56,7 @@ function handleTemplateUpdate(payload: { index: number, value: string }) {
appear
>
<TemplateEditor
:action="props.action"
:template="props.action"
:template-index="0"
:title="templateTitle"
:description="templateDescription"