mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
- 在 ClientAutoAction.vue 中新增消息队列设置标签页,允许用户配置弹幕和私信发送间隔。 - 更新多个组件以支持新的设置选项,提升用户交互体验。 - 在 useBiliFunction 中实现队列处理逻辑,确保消息按设定间隔发送。 - 优化 CheckInSettings 组件,整合 AutoActionEditor 以简化配置管理。
680 lines
22 KiB
Vue
680 lines
22 KiB
Vue
<script setup lang="ts">
|
||
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 { 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';
|
||
import { Info24Filled, Code24Regular, LiveOff24Regular } from '@vicons/fluent';
|
||
import { EventDataTypes, EventModel } from '@/api/api-models';
|
||
import GraphemeSplitter from 'grapheme-splitter';
|
||
|
||
const props = defineProps({
|
||
template: {
|
||
type: Object as () => AutoActionItem,
|
||
required: true
|
||
},
|
||
title: {
|
||
type: String,
|
||
default: '模板编辑'
|
||
},
|
||
description: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
checkLength: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
customTestContext: {
|
||
type: Object,
|
||
default: undefined
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(['update:template']);
|
||
|
||
const mergedPlaceholders = computed(() => {
|
||
const basePlaceholders = [
|
||
{ name: '{{user.name}}', description: '用户名称' },
|
||
{ name: '{{user.uid}}', description: '用户ID' },
|
||
{ name: '{{user.guardLevel}}', description: '用户舰队等级 (0:无, 1:总督, 2:提督, 3:舰长)' },
|
||
{ name: '{{user.hasMedal}}', description: '用户是否佩戴粉丝勋章 (true/false)' },
|
||
{ name: '{{user.medalLevel}}', description: '用户佩戴的粉丝勋章等级' },
|
||
{ name: '{{user.medalName}}', description: '用户佩戴的粉丝勋章名称' },
|
||
{ name: '{{date.formatted}}', description: '当前日期时间 (格式化)' },
|
||
{ name: '{{date.year}}', description: '当前年份' },
|
||
{ name: '{{date.month}}', description: '当前月份' },
|
||
{ name: '{{date.day}}', description: '当前日期' },
|
||
{ name: '{{date.hour}}', description: '当前小时 (0-23)' },
|
||
{ name: '{{date.minute}}', description: '当前分钟' },
|
||
{ name: '{{date.second}}', description: '当前秒数' },
|
||
{ name: '{{timeOfDay}}', description: '当前时段 (凌晨/早上/上午/中午/下午/晚上/深夜)' },
|
||
{ name: '{{event}}', description: '原始事件对象 (高级用法)' }
|
||
];
|
||
|
||
const specificPlaceholders: { name: string, description: string }[] = [];
|
||
|
||
switch (props.template.triggerType) {
|
||
case TriggerType.DANMAKU:
|
||
specificPlaceholders.push(
|
||
{ name: '{{message}}', description: '弹幕内容' },
|
||
{ name: '{{danmaku}}', description: '弹幕事件对象' }
|
||
);
|
||
break;
|
||
case TriggerType.GIFT:
|
||
specificPlaceholders.push(
|
||
{ 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.push(
|
||
{ name: '{{guard.level}}', description: '开通的舰队等级 (1:总督, 2:提督, 3:舰长)' },
|
||
{ name: '{{guard.levelName}}', description: '开通的舰队等级名称' },
|
||
{ name: '{{guard.giftCode}}', description: '舰长礼物代码 (预留字段)' }
|
||
);
|
||
break;
|
||
case TriggerType.SUPER_CHAT:
|
||
specificPlaceholders.push(
|
||
{ name: '{{sc.message}}', description: 'SC留言内容' },
|
||
{ name: '{{sc.price}}', description: 'SC金额(元)' }
|
||
);
|
||
break;
|
||
}
|
||
|
||
const finalPlaceholders = [...specificPlaceholders, ...basePlaceholders];
|
||
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(() => {
|
||
// 创建默认上下文
|
||
const defaultContext = buildExecutionContext({
|
||
msg: '测试',
|
||
time: 1713542400,
|
||
num: 1,
|
||
price: 100,
|
||
guard_level: 1,
|
||
uname: '测试用户',
|
||
uface: 'https://example.com/test.jpg',
|
||
uid: 12345,
|
||
ouid: '1234567890',
|
||
type: EventDataTypes.Message,
|
||
open_id: '1234567890',
|
||
fans_medal_level: 1,
|
||
fans_medal_name: '测试粉丝勋章',
|
||
fans_medal_wearing_status: true,
|
||
guard_level_name: '测试舰队',
|
||
guard_level_price: 100,
|
||
}, undefined, props.template.triggerType);
|
||
|
||
// 如果有自定义上下文,将其与默认上下文合并
|
||
if (props.customTestContext) {
|
||
return deepMerge(defaultContext, props.customTestContext);
|
||
}
|
||
|
||
return defaultContext;
|
||
});
|
||
|
||
const message = useMessage();
|
||
const activeTab = ref('editor');
|
||
const showLivePreview = ref(true);
|
||
const splitter = new GraphemeSplitter();
|
||
const showSyntaxModal = ref(false);
|
||
|
||
function countGraphemes(value: string) {
|
||
return splitter.countGraphemes(value);
|
||
}
|
||
|
||
function convertPlaceholders() {
|
||
if (!props.template.template) {
|
||
message.warning('请先输入模板内容');
|
||
return;
|
||
}
|
||
const converted = convertToJsExpressions(props.template.template, mergedPlaceholders.value);
|
||
if (converted !== props.template.template) {
|
||
props.template.template = converted;
|
||
message.success('已转换占位符为表达式格式');
|
||
} else {
|
||
message.info('模板中没有需要转换的占位符');
|
||
}
|
||
}
|
||
|
||
function hasJsExpression(template: string): boolean {
|
||
return containsJsExpression(template);
|
||
}
|
||
|
||
const highlightPatterns = computed(() => {
|
||
const simplePlaceholders = mergedPlaceholders.value.map(p => p.name);
|
||
const jsExpressionsInTemplate = extractJsExpressions(props.template.template || '');
|
||
const allPatterns = [...new Set([...simplePlaceholders, ...jsExpressionsInTemplate])];
|
||
return allPatterns;
|
||
});
|
||
|
||
const MAX_LENGTH = 20;
|
||
const WARNING_THRESHOLD = 16;
|
||
|
||
function evaluateTemplateForUI(template: string): string {
|
||
// 深度合并默认上下文和自定义上下文
|
||
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) {
|
||
console.error("Preview evaluation error:", error);
|
||
return `[预览错误: ${(error as Error).message}]`;
|
||
}
|
||
}
|
||
|
||
const evaluatedTemplateResult = computed(() => {
|
||
if (!props.template.template || !showLivePreview.value) return '';
|
||
return evaluateTemplateForUI(props.template.template);
|
||
});
|
||
|
||
const previewResult = computed(() => {
|
||
return evaluatedTemplateResult.value;
|
||
});
|
||
|
||
const lengthStatus = computed(() => {
|
||
if (!props.template.template || !props.checkLength || !showLivePreview.value) {
|
||
return { status: 'normal' as const, message: '' };
|
||
}
|
||
try {
|
||
const formattedText = evaluatedTemplateResult.value;
|
||
if (formattedText.startsWith('[预览错误:')) {
|
||
return { status: 'normal' as const, message: '' };
|
||
}
|
||
|
||
const formattedLength = countGraphemes(formattedText);
|
||
if (formattedLength > MAX_LENGTH) {
|
||
return { status: 'error' as const, message: `格式化后长度超出限制(${formattedLength}/${MAX_LENGTH}字)` };
|
||
} else if (formattedLength >= WARNING_THRESHOLD) {
|
||
return { status: 'warning' as const, message: `格式化后长度接近限制(${formattedLength}/${MAX_LENGTH}字)` };
|
||
}
|
||
return { status: 'normal' as const, message: '' };
|
||
} catch (error) {
|
||
return { status: 'normal' as const, message: '' };
|
||
}
|
||
});
|
||
|
||
const templateExamples = [
|
||
{
|
||
title: '基础变量',
|
||
examples: [
|
||
{ label: '用户名', template: '你好 {{user.name}}!' },
|
||
{ label: '条件回复', template: '{{js: user.guardLevel > 0 ? "欢迎舰长" : "欢迎"}} {{user.name}}!' }
|
||
]
|
||
},
|
||
{
|
||
title: '高级用法',
|
||
examples: [
|
||
{ label: '字符串操作', template: '{{js: user.name.toUpperCase()}} 有 {{js: user.name.length}} 个字' },
|
||
{ label: '随机回复', template: '{{js: ["谢谢", "感谢", "收到"][Math.floor(Math.random() * 3)]}}!' },
|
||
{ label: '日期时间', template: '{{js: new Date().toLocaleTimeString()}},{{timeOfDay}}好!' },
|
||
{
|
||
label: '运行时计数',
|
||
template: '{{js+: const count = (getData(\'messageCount\') || 0) + 1; setData(\'messageCount\', count); return `这是你本次对话的第 ${count} 条消息。`; }}'
|
||
},
|
||
{
|
||
label: '运行时频率检查',
|
||
template: '{{js+: const warns = (getData(\'warnings\') || 0) + 1; setData(\'warnings\', warns); return warns > 3 ? "发言太频繁啦!" : "收到你的消息~"; }}'
|
||
},
|
||
{
|
||
label: '触发持久化计数 (异步)',
|
||
template: '{{js+: const key = `user:${user.uid}:totalMessages`; getStorageData(key, 0).then(c => setStorageData(key, (c || 0) + 1)); return `正在为你累计总发言数...`; }}'
|
||
},
|
||
{
|
||
label: '问候一次 (持久化)',
|
||
template: '{{js+: const key = `greeted:${user.uid}`; hasStorageData(key).then(exists => { if (!exists) { setStorageData(key, true); /* 这里可以接发送欢迎消息的逻辑 */ } }); return \'检查问候状态...\'; }}'
|
||
}
|
||
]
|
||
},
|
||
{
|
||
title: '弹幕功能',
|
||
examples: [
|
||
{ label: '提取内容', template: '你说的是 "{{js: message.substring(0, 5)}}{{js: message.length > 5 ? "..." : ""}}" 吗?' },
|
||
{ label: '回复问候', template: '{{js: message.includes("早上好") ? "早安" : "你好"}},{{user.name}}!' }
|
||
]
|
||
}
|
||
];
|
||
|
||
function insertExample(template: string) {
|
||
props.template.template = template;
|
||
message.success('已插入示例模板');
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<NCard
|
||
:title="title"
|
||
size="small"
|
||
class="template-editor-card"
|
||
>
|
||
<template
|
||
v-if="mergedPlaceholders.length > 0"
|
||
#header-extra
|
||
>
|
||
<NButton
|
||
quaternary
|
||
size="small"
|
||
class="btn-with-transition"
|
||
@click="showSyntaxModal = true"
|
||
>
|
||
<NIcon
|
||
:component="Info24Filled"
|
||
style="margin-right: 4px;"
|
||
/>
|
||
变量与语法说明
|
||
</NButton>
|
||
</template>
|
||
|
||
<p
|
||
v-if="description"
|
||
class="template-description"
|
||
>
|
||
{{ description }}
|
||
</p>
|
||
|
||
<!-- 标签页支持 -->
|
||
<NTabs
|
||
v-model:value="activeTab"
|
||
type="line"
|
||
animated
|
||
class="editor-tabs"
|
||
>
|
||
<NTabPane
|
||
name="editor"
|
||
tab="编辑"
|
||
>
|
||
<NFlex
|
||
vertical
|
||
:size="12"
|
||
>
|
||
<!-- 模板帮助组件 -->
|
||
<TemplateHelper :placeholders="mergedPlaceholders" />
|
||
|
||
<!-- 当前模板预览 -->
|
||
<NInput
|
||
v-model:value="template.template"
|
||
type="textarea"
|
||
placeholder="输入模板内容... 使用 {{变量名}} 插入变量, {{js: 表达式}} 执行JS"
|
||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||
:show-count="checkLength"
|
||
:count-graphemes="countGraphemes"
|
||
:status="checkLength && lengthStatus.status !== 'normal' ? (lengthStatus.status === 'error' ? 'error' : 'warning') : undefined"
|
||
class="template-input"
|
||
/>
|
||
|
||
<!-- 长度检查警告 -->
|
||
<NAlert
|
||
v-if="checkLength && lengthStatus.message && lengthStatus.status !== 'normal'"
|
||
:type="lengthStatus.status === 'error' ? 'error' : 'warning'"
|
||
class="length-alert"
|
||
>
|
||
{{ lengthStatus.message }}
|
||
</NAlert>
|
||
|
||
<!-- 实时预览 -->
|
||
<NFlex
|
||
align="center"
|
||
justify="space-between"
|
||
class="preview-toggle"
|
||
>
|
||
<NButton
|
||
quaternary
|
||
size="small"
|
||
@click="showLivePreview = !showLivePreview"
|
||
>
|
||
<template #icon>
|
||
<NIcon :component="showLivePreview ? LiveOff24Regular : Code24Regular" />
|
||
</template>
|
||
{{ showLivePreview ? '隐藏预览' : '显示预览' }}
|
||
</NButton>
|
||
|
||
<transition name="fade">
|
||
<div
|
||
v-if="showLivePreview && previewResult"
|
||
class="live-preview"
|
||
>
|
||
<NBadge
|
||
dot
|
||
type="info"
|
||
/> 实时预览:
|
||
<NHighlight
|
||
:text="previewResult"
|
||
:patterns="highlightPatterns"
|
||
/>
|
||
</div>
|
||
</transition>
|
||
</NFlex>
|
||
|
||
<!-- 操作按钮 -->
|
||
<NFlex
|
||
justify="end"
|
||
:size="12"
|
||
>
|
||
<NButton
|
||
type="default"
|
||
size="small"
|
||
class="btn-with-transition"
|
||
@click="convertPlaceholders"
|
||
>
|
||
占位符转表达式
|
||
</NButton>
|
||
</NFlex>
|
||
|
||
<!-- 模板示例 -->
|
||
<NCollapse
|
||
class="template-examples"
|
||
>
|
||
<NCollapseItem
|
||
name="examples"
|
||
title="模板示例 (点击展开)"
|
||
>
|
||
<NFlex
|
||
vertical
|
||
:size="8"
|
||
>
|
||
<div
|
||
v-for="(category, idx) in templateExamples"
|
||
:key="idx"
|
||
class="example-category"
|
||
>
|
||
<h4>{{ category.title }}</h4>
|
||
<NFlex
|
||
wrap
|
||
:size="8"
|
||
>
|
||
<NButton
|
||
v-for="(example, i) in category.examples"
|
||
:key="i"
|
||
size="small"
|
||
tertiary
|
||
class="example-button"
|
||
@click="insertExample(example.template)"
|
||
>
|
||
{{ example.label }}
|
||
</NButton>
|
||
</NFlex>
|
||
</div>
|
||
</NFlex>
|
||
</NCollapseItem>
|
||
</NCollapse>
|
||
</NFlex>
|
||
</NTabPane>
|
||
</NTabs>
|
||
|
||
<!-- 新增 Modal 组件 -->
|
||
<NModal
|
||
v-model:show="showSyntaxModal"
|
||
preset="card"
|
||
title="模板语法与变量说明"
|
||
:bordered="false"
|
||
size="huge"
|
||
style="width: 600px; max-width: 90vw;"
|
||
:close-on-esc="true"
|
||
:mask-closable="true"
|
||
>
|
||
<NScrollbar style="max-height: 80vh;">
|
||
<NAlert
|
||
title="模板语法说明"
|
||
type="info"
|
||
:show-icon="false"
|
||
style="margin-bottom: 16px;"
|
||
>
|
||
模板支持插入变量和执行 JavaScript。
|
||
<NDivider style="margin: 8px 0;" />
|
||
<strong>1. 简单变量替换:</strong><br>
|
||
直接使用 <code>{{ '\{\{变量名.属性\}\}' }}</code> 插入值。<br>
|
||
示例: <code>{{ '\{\{user.name\}\}' }}</code> → 显示用户名
|
||
<NDivider style="margin: 8px 0;" />
|
||
<strong>2. JS 表达式求值 (<code>js:</code>):</strong><br>
|
||
使用 <code>{{ '\{\{js: 表达式\}\}' }}</code> 执行单个 JS 表达式并插入结果 (隐式返回)。<br>
|
||
适合简单计算、字符串操作、三元运算等。<br>
|
||
示例: <code>{{ '\{\{js: user.guardLevel > 0 ? "舰长" : "非舰长\}\}' }}</code><br>
|
||
示例: <code>{{ '\{\{js: gift.price * gift.count\}\}' }}</code>
|
||
<NDivider style="margin: 8px 0;" />
|
||
<strong>3. JS 代码块执行 (<code>js+:</code> 或 <code>js-run:</code>):</strong><br>
|
||
使用 <code>{{ '\{\{js+: 代码...\}\}' }}</code> 或 <code>{{ '\{\{js-run: 代码...\}\}' }}</code> 执行多行 JS 代码。<br>
|
||
<strong style="color: var(--warning-color);">需要显式使用 <code>return</code> 语句来指定输出到模板的值。</strong><br>
|
||
适合需要临时变量、多步逻辑或调用 <code>getData/setData</code> 等函数的场景。<br>
|
||
<pre><code>{{ '\{\{js+:\n const count = (getData(\'greetCount\') || 0) + 1;\n setData(\'greetCount\', count);\n return \`这是第 ${count} 次问候!\`;\n\}\}' }}</code></pre>
|
||
</NAlert>
|
||
|
||
<NCollapse arrow-placement="right">
|
||
<NCollapseItem
|
||
title="数据存储函数说明 (在 js+ 或 js-run 中使用)"
|
||
name="data-functions"
|
||
>
|
||
<NAlert
|
||
type="warning"
|
||
:bordered="false"
|
||
size="small"
|
||
style="margin-bottom: 8px;"
|
||
>
|
||
<strong>运行时数据</strong>仅在本次运行有效, 重启后就没了,且操作是<strong>同步</strong>的。
|
||
</NAlert>
|
||
<ul class="function-list">
|
||
<li><code>getData(key, defaultValue?)</code>: 获取运行时数据。</li>
|
||
<li><code>setData(key, value)</code>: 设置运行时数据。</li>
|
||
<li><code>containsData(key)</code>: 检查运行时数据是否存在。</li>
|
||
<li><code>removeData(key)</code>: 移除运行时数据。</li>
|
||
</ul>
|
||
|
||
<NDivider style="margin: 12px 0;" />
|
||
|
||
<NAlert
|
||
type="info"
|
||
:bordered="false"
|
||
size="small"
|
||
style="margin-bottom: 8px;"
|
||
>
|
||
<strong>持久化数据</strong>会长期保留,但操作是<strong>异步</strong>的 (返回 Promise)。<br>
|
||
在 <code>js+</code> 或 <code>js-run</code> 中使用 <code>await</code> 处理或使用 <code>.then()</code>。
|
||
</NAlert>
|
||
<ul class="function-list">
|
||
<li><code>getStorageData(key, defaultValue?)</code>: 获取持久化数据 (异步)。</li>
|
||
<li><code>setStorageData(key, value)</code>: 设置持久化数据 (异步)。</li>
|
||
<li><code>hasStorageData(key)</code>: 检查持久化数据是否存在 (异步)。</li>
|
||
<li><code>removeStorageData(key)</code>: 移除持久化数据 (异步)。</li>
|
||
<li><code>clearStorageData()</code>: 清除所有用户持久化数据 (异步)。</li>
|
||
</ul>
|
||
<pre><code>{{ '\{\{js+:\n // 异步获取并设置持久化数据\n const key = \`user:${user.uid}:visitCount\`;\n const count = (await getStorageData(key, 0)) + 1;\n await setStorageData(key, count);\n return \`你是第 ${count} 次访问!\`;\n\}\}' }}</code></pre>
|
||
</NCollapseItem>
|
||
</NCollapse>
|
||
<br>
|
||
<strong>可用变量 (基础):</strong>
|
||
|
||
<div
|
||
v-for="(ph, idx) in mergedPlaceholders"
|
||
:key="idx"
|
||
class="placeholder-item"
|
||
>
|
||
<NText code>
|
||
{{ ph.name }}
|
||
</NText>: {{ ph.description }}
|
||
</div>
|
||
</NScrollbar>
|
||
</NModal>
|
||
</NCard>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.template-editor-card {
|
||
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: 12px;
|
||
color: var(--n-text-color-disabled);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.alert-header {
|
||
display: flex;
|
||
align-items: center;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.placeholder-item {
|
||
margin-bottom: 4px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.placeholder-item code {
|
||
font-size: 13px;
|
||
padding: 1px 4px;
|
||
}
|
||
|
||
.template-input {
|
||
margin-top: 8px;
|
||
font-family: 'Courier New', Courier, monospace;
|
||
}
|
||
|
||
.preview-toggle {
|
||
margin-top: 4px;
|
||
height: 28px;
|
||
}
|
||
|
||
.length-alert {
|
||
margin-top: 8px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.live-preview {
|
||
background-color: var(--n-color-target);
|
||
border-radius: var(--n-border-radius);
|
||
padding: 4px 8px;
|
||
font-size: 13px;
|
||
border-left: 3px solid var(--n-color-target);
|
||
word-break: break-all;
|
||
transition: all 0.3s ease;
|
||
margin-left: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.live-preview .n-badge {
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.template-examples {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.example-category h4 {
|
||
margin: 0 0 6px 0;
|
||
font-size: 14px;
|
||
color: var(--n-text-color-2);
|
||
}
|
||
|
||
.example-button {
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.example-button:hover {
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.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(-1px);
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.function-list {
|
||
list-style: none;
|
||
padding-left: 10px;
|
||
font-size: 13px;
|
||
}
|
||
.function-list li {
|
||
margin-bottom: 5px;
|
||
}
|
||
.function-list code {
|
||
background-color: var(--n-code-color);
|
||
padding: 1px 4px;
|
||
border-radius: var(--n-border-radius);
|
||
font-family: monospace;
|
||
margin-right: 4px;
|
||
}
|
||
.n-collapse {
|
||
margin-top: 16px;
|
||
}
|
||
.n-alert pre {
|
||
margin: 4px 0 0 0;
|
||
padding: 6px 8px;
|
||
background-color: var(--n-code-color);
|
||
border-radius: var(--n-border-radius);
|
||
overflow-x: auto;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
}
|
||
.n-alert code {
|
||
background-color: transparent;
|
||
padding: 0;
|
||
font-family: monospace;
|
||
font-size: inherit;
|
||
}
|
||
.n-alert pre code {
|
||
background-color: transparent;
|
||
padding: 0;
|
||
}
|
||
</style>
|
||
|