feat: 更新项目配置和组件,增强功能和用户体验

- 在 .gitignore 中添加了 .specstory 文件的忽略规则。
- 更新 tsconfig.json,修正了 vue-vine/types/macros 的引用路径。
- 在组件声明中新增了 NInput 组件的类型支持。
- 优化了 EventModel 接口,调整了 guard_level 的类型为 GuardLevel。
- 增加了 Follow 事件类型到 EventDataTypes 枚举中。
- 在 ClientAutoAction.vue 中引入了新的 store 和组件,增强了功能。
- 更新了多个设置组件,添加了关键词匹配类型和过滤模式的支持。
- 改进了模板编辑器和测试器的功能,支持更灵活的模板管理。
- 在弹幕客户端中新增了关注事件的处理逻辑,提升了事件响应能力。
This commit is contained in:
2025-04-22 02:30:09 +08:00
parent 2fc8f7fcf8
commit 77cf0c5edc
39 changed files with 3955 additions and 1959 deletions

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { NSpace, NSwitch, NInputNumber, NInput, NCollapseItem } from 'naive-ui';
import { NSpace, NSwitch, NInputNumber, NInput, NCollapseItem, NCollapse } from 'naive-ui';
import { AutoActionItem, TriggerType } from '@/client/store/useAutoAction';
import { computed } from 'vue';
import { createDefaultAutoAction } from '@/client/store/autoAction/utils';
const props = defineProps({
action: {
@@ -14,6 +15,54 @@ const props = defineProps({
const showUserFilter = computed(() => {
return ![TriggerType.SCHEDULED].includes(props.action.triggerType);
});
// 获取默认配置作为比较基准
const defaultAction = computed(() => createDefaultAutoAction(props.action.triggerType));
// 检查设置项是否被修改
const isModified = (path: string, value: any) => {
const pathParts = path.split('.');
let defaultValue: any = defaultAction.value;
let currentValue: any = props.action;
// 遍历路径获取值
for (const part of pathParts) {
defaultValue = defaultValue && typeof defaultValue === 'object' ? defaultValue[part as keyof typeof defaultValue] : undefined;
currentValue = currentValue && typeof currentValue === 'object' ? currentValue[part as keyof typeof currentValue] : undefined;
}
// 处理特殊情况,如果指定了具体值进行比较
if (value !== undefined) {
return value !== defaultValue;
}
return currentValue !== defaultValue;
};
// 检查用户过滤区域是否有修改
const userFilterModified = computed(() => {
if (!showUserFilter.value) return false;
return isModified('triggerConfig.userFilterEnabled', props.action.triggerConfig.userFilterEnabled) ||
isModified('triggerConfig.requireMedal', props.action.triggerConfig.requireMedal) ||
isModified('triggerConfig.requireCaptain', props.action.triggerConfig.requireCaptain);
});
// 检查冷却控制区域是否有修改
const cooldownModified = computed(() => {
return isModified('ignoreCooldown', props.action.ignoreCooldown) ||
isModified('actionConfig.delaySeconds', props.action.actionConfig.delaySeconds) ||
isModified('actionConfig.cooldownSeconds', props.action.actionConfig.cooldownSeconds);
});
// 检查逻辑表达式是否有修改
const logicalExpressionModified = computed(() => {
return isModified('logicalExpression', props.action.logicalExpression);
});
// 检查自定义JS是否有修改
const customJsModified = computed(() => {
return isModified('executeCommand', props.action.executeCommand);
});
</script>
<template>
@@ -21,8 +70,10 @@ const showUserFilter = computed(() => {
<NCollapseItem
v-if="showUserFilter"
key="user-filter"
title="用户过滤"
:title="userFilterModified ? '用户过滤 *' : '用户过滤'"
:title-extra="userFilterModified ? '已修改' : ''"
class="settings-section"
:class="{'section-modified': userFilterModified}"
>
<div>
<NSpace
@@ -34,7 +85,7 @@ const showUserFilter = computed(() => {
align="center"
justify="space-between"
style="width: 100%"
class="setting-item"
:class="['setting-item', {'setting-modified': isModified('triggerConfig.userFilterEnabled', action.triggerConfig.userFilterEnabled)}]"
>
<span>启用用户过滤:</span>
<NSwitch v-model:value="action.triggerConfig.userFilterEnabled" />
@@ -46,7 +97,7 @@ const showUserFilter = computed(() => {
align="center"
justify="space-between"
style="width: 100%"
class="setting-item"
:class="['setting-item', {'setting-modified': isModified('triggerConfig.requireMedal', action.triggerConfig.requireMedal)}]"
>
<span>要求本房间勋章:</span>
<NSwitch v-model:value="action.triggerConfig.requireMedal" />
@@ -57,7 +108,7 @@ const showUserFilter = computed(() => {
align="center"
justify="space-between"
style="width: 100%"
class="setting-item"
:class="['setting-item', {'setting-modified': isModified('triggerConfig.requireCaptain', action.triggerConfig.requireCaptain)}]"
>
<span>要求任意舰长:</span>
<NSwitch v-model:value="action.triggerConfig.requireCaptain" />
@@ -69,8 +120,10 @@ const showUserFilter = computed(() => {
<NCollapseItem
key="cooldown"
title="冷却控制"
:title="cooldownModified ? '冷却控制 *' : '冷却控制'"
:title-extra="cooldownModified ? '已修改' : ''"
class="settings-section"
:class="{'section-modified': cooldownModified}"
>
<div>
<NSpace
@@ -78,7 +131,7 @@ const showUserFilter = computed(() => {
align="center"
justify="space-between"
style="width: 100%"
class="setting-item"
:class="['setting-item', {'setting-modified': isModified('ignoreCooldown', action.ignoreCooldown)}]"
>
<span>忽略全局冷却:</span>
<NSwitch v-model:value="action.ignoreCooldown" />
@@ -89,7 +142,7 @@ const showUserFilter = computed(() => {
align="center"
justify="space-between"
style="width: 100%"
class="setting-item"
:class="['setting-item', {'setting-modified': isModified('actionConfig.delaySeconds', action.actionConfig.delaySeconds)}]"
>
<span>延迟执行():</span>
<NInputNumber
@@ -105,7 +158,7 @@ const showUserFilter = computed(() => {
align="center"
justify="space-between"
style="width: 100%"
class="setting-item"
:class="['setting-item', {'setting-modified': isModified('actionConfig.cooldownSeconds', action.actionConfig.cooldownSeconds)}]"
>
<span>冷却时间():</span>
<NInputNumber
@@ -120,8 +173,10 @@ const showUserFilter = computed(() => {
<NCollapseItem
key="logical-expression"
title="逻辑条件表达式"
:title="logicalExpressionModified ? '逻辑条件表达式 *' : '逻辑条件表达式'"
:title-extra="logicalExpressionModified ? '已修改' : ''"
class="settings-section"
:class="{'section-modified': logicalExpressionModified}"
>
<div>
<NSpace vertical>
@@ -133,6 +188,7 @@ const showUserFilter = computed(() => {
type="textarea"
placeholder="输入表达式,留空则始终为真"
:autosize="{ minRows: 2, maxRows: 5 }"
:class="{'input-modified': isModified('logicalExpression', action.logicalExpression)}"
/>
</NSpace>
</div>
@@ -140,8 +196,10 @@ const showUserFilter = computed(() => {
<NCollapseItem
key="custom-js"
title="自定义JS执行"
:title="customJsModified ? '自定义JS执行 *' : '自定义JS执行'"
:title-extra="customJsModified ? '已修改' : ''"
class="settings-section"
:class="{'section-modified': customJsModified}"
>
<div>
<NSpace vertical>
@@ -153,6 +211,7 @@ const showUserFilter = computed(() => {
type="textarea"
placeholder="输入要执行的JS代码"
:autosize="{ minRows: 3, maxRows: 8 }"
:class="{'input-modified': isModified('executeCommand', action.executeCommand)}"
/>
</NSpace>
</div>
@@ -179,6 +238,31 @@ const showUserFilter = computed(() => {
background-color: rgba(0, 0, 0, 0.02);
}
.setting-modified {
font-weight: bold;
background-color: rgba(255, 230, 186, 0.2);
}
.setting-modified:hover {
background-color: rgba(255, 230, 186, 0.3);
}
.input-modified {
border-color: #faad14;
background-color: rgba(255, 230, 186, 0.1);
}
.section-modified :deep(.n-collapse-item__header-main) {
font-weight: bold;
color: #fa8c16;
}
.section-modified :deep(.n-collapse-item__header-extra) {
font-size: 12px;
color: #fa8c16;
font-weight: normal;
}
.description {
margin-top: 8px;
font-size: 13px;

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
import { ref } from 'vue';
import { NSpace, NInput, NButton, NTag, NDivider, NCollapseItem, useMessage } from 'naive-ui';
import { NSpace, NInput, NButton, NTag, NDivider, NCollapseItem, useMessage, NRadioGroup, NRadioButton } from 'naive-ui';
import { AutoActionItem, TriggerType } from '@/client/store/useAutoAction';
import { KeywordMatchType } from '@/client/store/autoAction/types';
import SingleTemplateEditor from '../SingleTemplateEditor.vue';
const props = defineProps({
action: {
@@ -16,6 +18,15 @@ const message = useMessage();
const tempKeyword = ref('');
const tempBlockword = ref('');
// 初始化匹配类型配置
if (!props.action.triggerConfig.keywordMatchType) {
props.action.triggerConfig.keywordMatchType = KeywordMatchType.Contains;
}
if (!props.action.triggerConfig.blockwordMatchType) {
props.action.triggerConfig.blockwordMatchType = KeywordMatchType.Contains;
}
// 添加关键词
function addKeyword() {
if (!tempKeyword.value.trim()) return;
@@ -61,6 +72,17 @@ function removeBlockword(index: number) {
props.action.triggerConfig.blockwords.splice(index, 1);
}
}
// 定义弹幕相关的模板占位符
const danmakuPlaceholders = [
{ name: '{{user.name}}', description: '发送弹幕的用户名' },
{ name: '{{user.uid}}', description: '用户UID' },
{ name: '{{user.guardLevel}}', description: '用户舰长等级(0-3)' },
{ name: '{{user.medalLevel}}', description: '粉丝勋章等级' },
{ name: '{{user.medalName}}', description: '粉丝勋章名称' },
{ name: '{{message}}', description: '弹幕内容' },
{ name: '{{js: message.substring(0, 10)}}', description: '弹幕内容的前10个字符' },
];
</script>
<template>
@@ -83,6 +105,24 @@ function removeBlockword(index: number) {
</NButton>
</NSpace>
<NSpace align="center">
<span>匹配方式:</span>
<NRadioGroup
v-model:value="action.triggerConfig.keywordMatchType"
size="small"
>
<NRadioButton :value="KeywordMatchType.Full">
完全
</NRadioButton>
<NRadioButton :value="KeywordMatchType.Contains">
包含
</NRadioButton>
<NRadioButton :value="KeywordMatchType.Regex">
正则
</NRadioButton>
</NRadioGroup>
</NSpace>
<NSpace>
<template v-if="action.triggerConfig.keywords">
<NTag
@@ -112,6 +152,24 @@ function removeBlockword(index: number) {
</NButton>
</NSpace>
<NSpace align="center">
<span>匹配方式:</span>
<NRadioGroup
v-model:value="action.triggerConfig.blockwordMatchType"
size="small"
>
<NRadioButton :value="KeywordMatchType.Full">
完全
</NRadioButton>
<NRadioButton :value="KeywordMatchType.Contains">
包含
</NRadioButton>
<NRadioButton :value="KeywordMatchType.Regex">
正则
</NRadioButton>
</NRadioGroup>
</NSpace>
<NSpace>
<template v-if="action.triggerConfig.blockwords">
<NTag

View File

@@ -32,7 +32,7 @@ const enterFilterModeOptions = [
>
<span>入场过滤模式:</span>
<NSelect
v-model:value="action.triggerConfig.enterFilterMode"
v-model:value="action.triggerConfig.filterMode"
style="width: 200px"
:options="enterFilterModeOptions"
/>

View File

@@ -61,13 +61,13 @@ function removeGiftName(index: number) {
>
<span>礼物过滤模式:</span>
<NSelect
v-model:value="action.triggerConfig.giftFilterMode"
v-model:value="action.triggerConfig.filterMode"
style="width: 200px"
:options="giftFilterModeOptions"
/>
</NSpace>
<template v-if="action.triggerConfig.giftFilterMode === 'blacklist' || action.triggerConfig.giftFilterMode === 'whitelist'">
<template v-if="action.triggerConfig.filterMode === 'blacklist' || action.triggerConfig.filterMode === 'whitelist'">
<NSpace>
<NInput
v-model:value="tempGiftName"
@@ -93,7 +93,7 @@ function removeGiftName(index: number) {
</NSpace>
</template>
<template v-if="action.triggerConfig.giftFilterMode === 'value'">
<template v-if="action.triggerConfig.filterMode === 'value'">
<NSpace
align="center"
justify="space-between"

View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import {
NCard,
NSpace,
NInputNumber,
NRadioGroup,
NRadio,
NText,
NDivider
} from 'naive-ui';
import { useAutoAction } from '@/client/store/useAutoAction';
import { watch } from 'vue';
const autoActionStore = useAutoAction();
// 定时模式选项
const schedulingModeOptions = [
{ label: '随机模式', value: 'random' },
{ label: '顺序模式', value: 'sequential' }
];
// 监听变化,触发定时器重启(如果间隔改变)
watch(() => autoActionStore.globalIntervalSeconds, () => {
autoActionStore.restartGlobalTimer(); // 确保间隔改变时定时器更新
});
</script>
<template>
<NCard
title="全局定时设置"
size="small"
style="margin-bottom: 16px;"
>
<NSpace
vertical
:size="16"
>
<NText
type="info"
:depth="3"
style="font-size: 12px;"
>
这里的设置将应用于所有启用了 "使用全局定时器" 选项的定时触发操作
</NText>
<NDivider style="margin: 4px 0;" />
<NSpace
align="center"
justify="space-between"
style="width: 100%"
>
<NText>全局发送间隔 ():</NText>
<NInputNumber
v-model:value="autoActionStore.globalIntervalSeconds"
:min="10"
:max="7200"
style="width: 120px"
/>
</NSpace>
<NSpace
align="center"
justify="space-between"
style="width: 100%"
>
<NText>全局发送模式:</NText>
<NRadioGroup v-model:value="autoActionStore.globalSchedulingMode">
<NSpace>
<NRadio
v-for="option in schedulingModeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</NRadio>
</NSpace>
</NRadioGroup>
</NSpace>
</NSpace>
</NCard>
</template>
<style scoped>
/* 可以添加一些特定样式 */
</style>

View File

@@ -1,7 +1,16 @@
<script setup lang="ts">
import { NSpace, NInputNumber, NRadioGroup, NRadio, NCollapseItem } from 'naive-ui';
import {
NSpace,
NInputNumber,
NRadioGroup,
NRadio,
NCollapseItem,
NSwitch,
NDivider,
NText
} from 'naive-ui';
import { AutoActionItem, TriggerType } from '@/client/store/useAutoAction';
import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
const props = defineProps({
action: {
@@ -10,11 +19,30 @@ const props = defineProps({
}
});
// 初始化配置项
if (props.action.triggerConfig.useGlobalTimer === undefined) {
props.action.triggerConfig.useGlobalTimer = false; // 默认不使用全局定时器
}
if (props.action.triggerConfig.schedulingMode === undefined) {
props.action.triggerConfig.schedulingMode = 'random'; // 默认随机模式
}
if (props.action.triggerConfig.intervalSeconds === undefined) {
props.action.triggerConfig.intervalSeconds = 300; // 默认5分钟
}
const useGlobalTimer = ref(props.action.triggerConfig.useGlobalTimer);
// 同步到 action
watch(useGlobalTimer, (value) => {
props.action.triggerConfig.useGlobalTimer = value;
});
// 定时模式选项
const schedulingModeOptions = [
{ label: '随机模式', value: 'random' },
{ label: '顺序模式', value: 'sequential' }
];
</script>
<template>
@@ -22,11 +50,47 @@ const schedulingModeOptions = [
v-if="action.triggerType === TriggerType.SCHEDULED"
title="定时触发设置"
>
<NSpace vertical>
<NSpace
vertical
:size="16"
>
<NSpace
align="center"
justify="space-between"
style="width: 100%"
>
<NText>使用全局定时器设置:</NText>
<NSwitch v-model:value="useGlobalTimer">
<template #checked>
</template>
<template #unchecked>
</template>
</NSwitch>
</NSpace>
<NText
type="info"
:depth="3"
style="font-size: 12px; margin-top: -8px;"
>
启用后此操作将遵循全局定时设置间隔模式忽略下方独立设置
全局设置需在自动化 -> 全局配置中修改
</NText>
<NDivider
title-placement="left"
style="margin-top: 8px; margin-bottom: 8px;"
>
独立设置 (仅在不使用全局定时器时生效)
</NDivider>
<NSpace
align="center"
justify="space-between"
style="width: 100%"
:class="{ 'disabled-setting': useGlobalTimer }"
>
<span>发送间隔 ():</span>
<NInputNumber
@@ -34,6 +98,7 @@ const schedulingModeOptions = [
:min="60"
:max="3600"
style="width: 120px"
:disabled="useGlobalTimer"
/>
</NSpace>
@@ -41,9 +106,13 @@ const schedulingModeOptions = [
align="center"
justify="space-between"
style="width: 100%"
:class="{ 'disabled-setting': useGlobalTimer }"
>
<span>发送模式:</span>
<NRadioGroup v-model:value="action.triggerConfig.schedulingMode">
<NRadioGroup
v-model:value="action.triggerConfig.schedulingMode"
:disabled="useGlobalTimer"
>
<NSpace>
<NRadio
v-for="option in schedulingModeOptions"
@@ -58,3 +127,10 @@ const schedulingModeOptions = [
</NSpace>
</NCollapseItem>
</template>
<style scoped>
.disabled-setting {
opacity: 0.5;
pointer-events: none;
}
</style>

View File

@@ -43,7 +43,7 @@ const scFilterModeOptions = [
>
<span>最低价格 ():</span>
<NInputNumber
v-model:value="action.triggerConfig.minPrice"
v-model:value="action.triggerConfig.scMinPrice"
:min="0"
style="width: 120px"
/>

View File

@@ -11,59 +11,6 @@ const props = defineProps({
}
});
// 模板变量占位符选项,根据触发类型动态生成
const placeholders = computed(() => {
const commonPlaceholders = [
{ name: '{{user.name}}', description: '用户名称' },
{ name: '{{user.uid}}', description: '用户ID' },
{ name: '{{date.formatted}}', description: '当前日期时间' },
{ name: '{{timeOfDay()}}', description: '当前时段(早上/下午/晚上)' },
];
let specificPlaceholders: { name: string, description: string }[] = [];
switch (props.action.triggerType) {
case TriggerType.GIFT:
specificPlaceholders = [
{ name: '{{gift.name}}', description: '礼物名称' },
{ name: '{{gift.count}}', description: '礼物数量' },
{ name: '{{gift.price}}', description: '礼物单价' },
{ name: '{{gift.totalPrice}}', description: '礼物总价值' },
{ name: '{{gift.summary}}', description: '礼物摘要5个辣条' },
];
break;
case TriggerType.GUARD:
specificPlaceholders = [
{ name: '{{guard.level}}', description: '舰长等级' },
{ name: '{{guard.levelName}}', description: '舰长等级名称' },
{ name: '{{guard.giftCode}}', description: '礼品码(如已配置)' },
];
break;
case TriggerType.SUPER_CHAT:
specificPlaceholders = [
{ name: '{{sc.message}}', description: 'SC消息内容' },
{ name: '{{sc.price}}', description: 'SC价格' },
];
break;
case TriggerType.FOLLOW:
specificPlaceholders = [
{ name: '{{follow.time}}', description: '关注时间' },
{ name: '{{follow.isNew}}', description: '是否新关注' },
];
break;
case TriggerType.ENTER:
specificPlaceholders = [
{ name: '{{enter.time}}', description: '入场时间' },
{ name: '{{enter.guardLevel}}', description: '舰长等级' },
{ name: '{{enter.medalName}}', description: '勋章名称' },
{ name: '{{enter.medalLevel}}', description: '勋章等级' },
];
break;
}
return [...commonPlaceholders, ...specificPlaceholders];
});
// 根据操作类型获取模板标题
const templateTitle = computed(() => {
switch (props.action.actionType) {
@@ -91,6 +38,15 @@ const templateDescription = computed(() => {
return '消息内容模板';
}
});
// Handle template updates from TemplateEditor
function handleTemplateUpdate(payload: { index: number, value: string }) {
// Assuming index will always be 0 here as we only render one editor
// And assuming action.templates is a string based on previous findings
if (payload.index === 0) {
props.action.template = payload.value;
}
}
</script>
<template>
@@ -100,11 +56,13 @@ const templateDescription = computed(() => {
appear
>
<TemplateEditor
:templates="action.templates"
:placeholders="placeholders"
:action="props.action"
:template-index="0"
:title="templateTitle"
:description="templateDescription"
:check-length="action.actionType === ActionType.SEND_DANMAKU"
class="template-editor"
@update:template="handleTemplateUpdate"
/>
</transition>
</div>