feat: 更新依赖项和配置,添加新通知类型

- 在 package.json 中添加了 @types/md5 和 @vueuse/integrations 依赖。
- 更新了 tsconfig.json 中的模块解析方式为 bundler。
- 在组件声明中移除了不再使用的 Naive UI 组件。
- 在弹幕窗口和设置中添加了启用动画的选项,并更新了相关样式。
- 实现了私信发送失败的通知功能,增强了用户体验。
This commit is contained in:
2025-04-19 22:29:09 +08:00
parent 521cd1eddf
commit 630fe45b47
51 changed files with 5067 additions and 1690 deletions

View File

@@ -1,6 +1,10 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { NInput, NInputNumber, NButton, NSpace, NCard, NDivider, NList, NListItem, NPopconfirm, NTooltip } from 'naive-ui';
import { NButton, NCard, NDivider, NHighlight, NInput, NList, NListItem, NPopconfirm, NScrollbar, NSpace, NTooltip, useMessage, NTabs, NTabPane, NFlex, NAlert, NIcon } from 'naive-ui';
import { computed, ref } from 'vue';
import TemplateHelper from './TemplateHelper.vue';
import TemplateTester from './TemplateTester.vue';
import { containsJsExpression, convertToJsExpressions } from '@/client/store/autoAction/expressionEvaluator';
import { Info24Filled } from '@vicons/fluent';
const props = defineProps({
templates: {
@@ -18,57 +22,133 @@ const props = defineProps({
placeholders: {
type: Array as () => { name: string, description: string }[],
default: () => []
},
// 新增:提供测试上下文对象
testContext: {
type: Object,
default: () => ({
user: { uid: 12345, name: '测试用户' },
gift: { name: '测试礼物', count: 1, price: 100 }
})
}
});
// 添加默认的弹幕相关占位符
const mergedPlaceholders = computed(() => {
const defaultPlaceholders = [
{ name: '{{danmaku.type}}', description: '事件类型' },
{ name: '{{danmaku.uname}}', description: '用户名称' },
{ name: '{{danmaku.uface}}', description: '用户头像URL' },
{ name: '{{danmaku.uid}}', description: '用户ID直接连接' },
{ name: '{{danmaku.open_id}}', description: '用户开放平台ID' },
{ name: '{{danmaku.msg}}', description: '消息内容' },
{ name: '{{danmaku.time}}', description: '时间戳' },
{ name: '{{danmaku.num}}', description: '数量' },
{ name: '{{danmaku.price}}', description: '价格' },
{ name: '{{danmaku.guard_level}}', description: '大航海等级' },
{ name: '{{danmaku.fans_medal_level}}', description: '粉丝牌等级' },
{ name: '{{danmaku.fans_medal_name}}', description: '粉丝牌名称' },
{ name: '{{danmaku.fans_medal_wearing_status}}', description: '是否佩戴粉丝牌' },
{ name: '{{danmaku.emoji}}', description: '表情符号' }
{ name: '{{user.name}}', description: '用户名称' },
{ name: '{{user.uid}}', description: '用户ID' },
{ name: '{{user.nameLength}}', description: '用户名长度' },
{ name: '{{date.formatted}}', description: '当前日期格式化' },
{ name: '{{timeOfDay()}}', description: '获取当前时段(早上/下午/晚上)' }
];
// 返回自定义占位符和默认占位符,但不合并它们
return { custom: props.placeholders, default: defaultPlaceholders };
// 合并自定义占位符和默认占位符
return [...props.placeholders, ...defaultPlaceholders];
});
const newTemplate = ref('');
const message = useMessage();
const activeTab = ref('editor'); // 新增:标签页控制
// 新增:跟踪编辑状态
const isEditing = ref(false);
const editIndex = ref(-1);
const editTemplate = ref('');
// 新增:测试选中的模板
const selectedTemplateForTest = ref('');
function addTemplate() {
if (newTemplate.value.trim()) {
props.templates.push(newTemplate.value.trim());
newTemplate.value = '';
const val = newTemplate.value.trim();
if (!val) return;
if (props.templates.includes(val)) {
message.warning('模板已存在');
return;
}
props.templates.push(val);
newTemplate.value = '';
}
function removeTemplate(index: number) {
props.templates.splice(index, 1);
}
// 新增:开始编辑模板
function startEditTemplate(index: number) {
editIndex.value = index;
editTemplate.value = props.templates[index];
isEditing.value = true;
newTemplate.value = editTemplate.value;
}
// 新增:取消编辑
function cancelEdit() {
isEditing.value = false;
editIndex.value = -1;
newTemplate.value = '';
}
// 新增:保存编辑后的模板
function saveEditedTemplate() {
const val = newTemplate.value.trim();
if (!val) {
message.warning('模板内容不能为空');
return;
}
onMounted(() => {
// 检查是否与其他模板重复(排除当前编辑的模板)
const otherTemplates = props.templates.filter((_, idx) => idx !== editIndex.value);
if (otherTemplates.includes(val)) {
message.warning('模板已存在');
return;
}
})
props.templates[editIndex.value] = val;
message.success('模板更新成功');
cancelEdit();
}
// 新增:转换为表达式
function convertPlaceholders() {
if (!newTemplate.value) {
message.warning('请先输入模板内容');
return;
}
newTemplate.value = convertToJsExpressions(newTemplate.value, mergedPlaceholders.value);
message.success('已转换占位符为表达式格式');
}
// 新增:测试模板
function testTemplate(template: string) {
selectedTemplateForTest.value = template;
activeTab.value = 'test';
}
// 新增高亮JavaScript表达式
function hasJsExpression(template: string): boolean {
return containsJsExpression(template);
}
// 新增:高亮规则
const highlightPatterns = computed(() => {
return [
// 普通占位符高亮
...mergedPlaceholders.value.map(p => p.name),
// JS表达式高亮
'{{js:'
];
});
</script>
<template>
<NCard
:title="title"
size="small"
class="template-editor-card"
>
<template
v-if="mergedPlaceholders.custom.length > 0 || mergedPlaceholders.default.length > 0"
v-if="mergedPlaceholders.length > 0"
#header-extra
>
<NTooltip
@@ -79,29 +159,46 @@ function removeTemplate(index: number) {
<NButton
quaternary
size="small"
class="btn-with-transition"
>
变量说明
</NButton>
</template>
<div style="max-width: 300px">
<NAlert
type="info"
closable
style="margin-bottom: 8px"
>
<template #header>
<div class="alert-header">
<NIcon
:component="Info24Filled"
size="18"
style="margin-right: 8px"
/>
模板支持简单的JavaScript表达式
</div>
</template>
在模板中使用 <code>{{ '\{\{js:\}\}' }}</code> 语法可以执行简单的JavaScript表达式
<NFlex vertical>
<span>
<code>{{ '\{\{js: user.name.toUpperCase()\}\}' }}</code> 将用户名转为大写
</span>
<span>
<code>{{ '\{\{js: gift.count > 10 ? "大量" : "少量"\}\}' }}</code> 根据数量显示不同文本
</span>
</NFlex>
</NAlert>
<NScrollbar style="max-height: 200px; max-width: 300px">
<div
v-for="(ph, idx) in mergedPlaceholders.custom"
:key="'custom-' + idx"
v-for="(ph, idx) in mergedPlaceholders"
:key="idx"
>
<strong>{{ ph.name }}</strong>: {{ ph.description }}
</div>
<NDivider
v-if="mergedPlaceholders.custom.length > 0 && mergedPlaceholders.default.length > 0"
style="margin: 10px 0;">
默认变量
</NDivider>
<div
v-for="(ph, idx) in mergedPlaceholders.default"
:key="'default-' + idx"
>
<strong>{{ ph.name }}</strong>: {{ ph.description }}
</div>
</div>
</NScrollbar>
</NTooltip>
</template>
@@ -112,56 +209,284 @@ function removeTemplate(index: number) {
{{ description }}
</p>
<NList bordered>
<NListItem
v-for="(template, index) in templates"
:key="index"
<!-- 新增添加标签页支持 -->
<NTabs
v-model:value="activeTab"
type="line"
animated
class="editor-tabs"
>
<NTabPane
name="editor"
tab="编辑模板"
>
<NSpace
justify="space-between"
align="center"
style="width: 100%"
<!-- 新增添加模板帮助组件 -->
<transition
name="fade"
mode="out-in"
appear
>
<span>{{ template }}</span>
<NPopconfirm @positive-click="removeTemplate(index)">
<template #trigger>
<NButton
size="small"
quaternary
type="error"
<TemplateHelper :placeholders="mergedPlaceholders" />
</transition>
<NList
bordered
class="template-list"
>
<transition-group
name="list-slide"
tag="div"
appear
>
<NListItem
v-for="(template, index) in templates"
:key="index"
class="template-list-item"
>
<NSpace
justify="space-between"
align="center"
style="width: 100%"
>
删除
</NButton>
</template>
确定要删除此模板吗
</NPopconfirm>
</NSpace>
</NListItem>
</NList>
<!-- 更新使用自定义高亮规则 -->
<div
class="template-content"
:class="{ 'has-js-expr': hasJsExpression(template) }"
>
<NHighlight
:patterns="highlightPatterns"
:text="template"
/>
<div
v-if="hasJsExpression(template)"
class="js-expr-badge"
>
JS
</div>
</div>
<NDivider />
<NSpace>
<NButton
size="small"
class="btn-with-transition"
@click="testTemplate(template)"
>
测试
</NButton>
<NButton
size="small"
class="btn-with-transition"
@click="startEditTemplate(index)"
>
编辑
</NButton>
<NPopconfirm
@positive-click="removeTemplate(index)"
>
<template #trigger>
<NButton
size="small"
class="btn-with-transition"
>
删除
</NButton>
</template>
确定要删除这个模板吗
</NPopconfirm>
</NSpace>
</NSpace>
</NListItem>
</transition-group>
</NList>
<NSpace vertical>
<NInput
v-model:value="newTemplate"
placeholder="输入新模板内容"
clearable
/>
<NButton
type="primary"
block
@click="addTemplate"
<NDivider />
<transition
name="fade-scale"
appear
>
<NSpace
vertical
style="width: 100%"
>
<NInput
v-model:value="newTemplate"
type="textarea"
placeholder="输入新模板"
:autosize="{ minRows: 2, maxRows: 5 }"
class="template-input"
@keydown.enter.ctrl="isEditing ? saveEditedTemplate() : addTemplate()"
/>
<NSpace justify="space-between">
<NSpace>
<NButton
type="default"
class="btn-with-transition"
@click="convertPlaceholders"
>
转换为表达式
</NButton>
</NSpace>
<NSpace>
<NButton
v-if="isEditing"
class="btn-with-transition"
@click="cancelEdit"
>
取消
</NButton>
<NButton
type="primary"
class="btn-with-transition"
@click="isEditing ? saveEditedTemplate() : addTemplate()"
>
{{ isEditing ? '保存' : '添加' }}
</NButton>
</NSpace>
</NSpace>
</NSpace>
</transition>
</NTabPane>
<NTabPane
name="test"
tab="测试模板"
>
添加模板
</NButton>
</NSpace>
<transition
name="fade"
mode="out-in"
appear
>
<TemplateTester
:default-template="selectedTemplateForTest"
:context="testContext"
:placeholders="mergedPlaceholders"
/>
</transition>
</NTabPane>
</NTabs>
</NCard>
</template>
<style scoped>
.template-editor-card {
transition: all 0.3s ease;
animation: card-appear 0.4s ease-out;
}
@keyframes card-appear {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.template-description {
margin-bottom: 16px;
font-size: 14px;
color: #666;
transition: all 0.3s ease;
}
.editor-tabs {
transition: all 0.3s ease;
}
.template-list {
margin-top: 16px;
transition: all 0.3s ease;
}
.template-list-item {
transition: all 0.3s ease;
}
.template-list-item:hover {
background-color: rgba(0, 0, 0, 0.02);
}
.template-content {
position: relative;
padding-right: 30px;
word-break: break-all;
transition: all 0.3s ease;
}
.has-js-expr {
background-color: rgba(64, 158, 255, 0.05);
border-radius: 4px;
padding: 4px 8px;
}
.js-expr-badge {
position: absolute;
top: 0;
right: 0;
background-color: #409EFF;
color: white;
font-size: 12px;
padding: 1px 4px;
border-radius: 3px;
transition: all 0.3s ease;
}
.template-input {
transition: all 0.3s ease;
}
.template-input:focus {
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
/* 列表动画 */
.list-slide-enter-active,
.list-slide-leave-active {
transition: all 0.4s ease;
}
.list-slide-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.list-slide-leave-to {
opacity: 0;
transform: translateX(20px);
}
.list-slide-move {
transition: transform 0.4s ease;
}
/* 淡入缩放 */
.fade-scale-enter-active,
.fade-scale-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-scale-enter-from,
.fade-scale-leave-to {
opacity: 0;
transform: scale(0.95);
}
/* 淡入淡出 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 按钮过渡 */
.btn-with-transition {
transition: all 0.2s ease;
}
.btn-with-transition:hover {
transform: translateY(-2px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
</style>