mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 更新项目配置和依赖,增强功能和用户体验
- 完成弹幕机功能 - 在 .editorconfig 中新增对 vine.ts 文件的支持。 - 更新 package.json 中多个依赖的版本,提升稳定性和性能。 - 在 vite.config.mts 中引入 @guolao/vue-monaco-editor 插件,增强代码编辑功能。 - 在 App.vue 中调整内容填充的样式,优化界面布局。 - 新增获取配置文件哈希的 API 方法,提升配置管理能力。 - 在多个组件中优化了样式和逻辑,提升用户交互体验。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
[*.{js,jsx,ts,tsx,vue,vine.ts}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
|
||||
49
package.json
49
package.json
@@ -11,32 +11,33 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@antfu/ni": "^24.3.0",
|
||||
"@guolao/vue-monaco-editor": "^1.5.5",
|
||||
"@hyperdx/browser": "^0.21.2",
|
||||
"@hyperdx/cli": "^0.1.0",
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@microsoft/signalr-protocol-msgpack": "^8.0.7",
|
||||
"@mixer/postmessage-rpc": "^1.1.4",
|
||||
"@oneidentity/zstd-js": "^1.0.3",
|
||||
"@tauri-apps/api": "^2.4.0",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-autostart": "^2.3.0",
|
||||
"@tauri-apps/plugin-http": "^2.4.2",
|
||||
"@tauri-apps/plugin-log": "^2.3.1",
|
||||
"@tauri-apps/plugin-http": "^2.4.3",
|
||||
"@tauri-apps/plugin-log": "^2.4.0",
|
||||
"@tauri-apps/plugin-notification": "^2.2.2",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-process": "^2.2.1",
|
||||
"@tauri-apps/plugin-store": "^2.2.0",
|
||||
"@tauri-apps/plugin-updater": "^2.7.0",
|
||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.27.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@vicons/fluent": "^0.13.0",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/cli": "^5.0.8",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@vueuse/integrations": "^13.1.0",
|
||||
"@vueuse/router": "^13.0.0",
|
||||
"@vueuse/router": "^13.1.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"bilibili-live-ws": "^6.3.1",
|
||||
@@ -46,32 +47,32 @@
|
||||
"easy-speech": "^2.4.0",
|
||||
"echarts": "^5.6.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-oxlint": "^0.16.2",
|
||||
"eslint-plugin-prettier": "^5.2.4",
|
||||
"fast-xml-parser": "^5.0.9",
|
||||
"eslint-plugin-oxlint": "^0.16.7",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"fast-xml-parser": "^5.2.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"html2canvas": "^1.4.1",
|
||||
"idb-keyval": "^6",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"linqts": "^2.0.0",
|
||||
"md5": "^2.3.0",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"music-metadata-browser": "^2.5.11",
|
||||
"nanoid": "^5.1.5",
|
||||
"oxlint": "^0.16.2",
|
||||
"oxlint": "^0.16.7",
|
||||
"peerjs": "^1.5.4",
|
||||
"pinia": "^3.0.1",
|
||||
"pinia": "^3.0.2",
|
||||
"prettier": "^3.5.3",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"queue-typescript": "^1.0.1",
|
||||
"unplugin-auto-import": "^19.1.1",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
"unplugin-vue-components": "^28.5.0",
|
||||
"unplugin-vue-markdown": "^28.3.1",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "6.2.2",
|
||||
"vite": "6.3.3",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-oxlint": "^1.3.0",
|
||||
"vite-plugin-oxlint": "^1.3.1",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue": "3.5.13",
|
||||
"vue-echarts": "^7.0.3",
|
||||
@@ -81,28 +82,28 @@
|
||||
"vue3-aplayer": "^1.7.3",
|
||||
"vue3-marquee": "^4.2.2",
|
||||
"vueuc": "^0.4.64",
|
||||
"worker-timers": "^8.0.19",
|
||||
"worker-timers": "^8.0.20",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@types/bun": "^1.2.5",
|
||||
"@types/bun": "^1.2.10",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/obs-studio": "^2.17.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/parser": "^8.27.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||
"@vue-vine/eslint-config": "^0.2.19",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"knip": "^5.50.4",
|
||||
"knip": "^5.50.5",
|
||||
"naive-ui": "^2.41.0",
|
||||
"stylus": "^0.64.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vue-vine": "^0.3.19"
|
||||
"vue-vine": "^0.3.21"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
:root {
|
||||
font-feature-settings: 'liga' 1, 'calt' 1;
|
||||
--vtsuru-header-height: 50px;
|
||||
--vtsuru-content-padding: 16px;
|
||||
--vtsuru-content-padding: 12px;
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
|
||||
@@ -254,6 +254,22 @@ export async function UploadConfig(name: string, data: unknown) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export async function GetConfigHash(name: string) {
|
||||
try {
|
||||
const resp = await QueryGetAPI<string>(VTSURU_API_URL + 'get-config-hash', {
|
||||
name: name
|
||||
});
|
||||
if (resp.code == 200) {
|
||||
return resp.data;
|
||||
} else {
|
||||
console.error(`获取配置文件hash失败: ` + resp.message);
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`获取配置文件hash失败: ` + err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export async function EnableFunction(func: FunctionTypes) {
|
||||
if (ACCOUNT.value) {
|
||||
if (ACCOUNT.value.settings.enableFunctions.includes(func)) {
|
||||
|
||||
@@ -717,6 +717,7 @@ export interface BiliAuthBaseModel {
|
||||
export interface BiliAuthModel extends BiliAuthBaseModel {
|
||||
address?: AddressInfo[]
|
||||
token: string
|
||||
guardInfo: Record<number, GuardLevel>
|
||||
}
|
||||
export interface ResponsePointUserModel {
|
||||
point: number
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
html:not(.style-scope) {
|
||||
@layer yt {
|
||||
|
||||
html:not(.style-scope) {
|
||||
--yt-live-chat-background-color: hsl(0, 0%, 100%);
|
||||
--yt-live-chat-action-panel-background-color: hsla(0, 0%, 93.3%, .4);
|
||||
--yt-live-chat-action-panel-background-color-transparent: hsla(0, 0%, 97%, .8);
|
||||
@@ -29,7 +31,7 @@ html:not(.style-scope) {
|
||||
--yt-live-chat-reconnect-message-color: hsla(0, 0%, 7%, 0.2);
|
||||
--yt-live-chat-moderator-color: hsl(225, 84%, 66%);
|
||||
--yt-live-chat-owner-color: hsl(40, 76%, 55%);
|
||||
--yt-live-chat-author-chip-owner-text-color: rgba(0,0,0,0.87);
|
||||
--yt-live-chat-author-chip-owner-text-color: rgba(0, 0, 0, 0.87);
|
||||
--yt-live-chat-author-chip-verified-background-color: #CCCCCC;
|
||||
--yt-live-chat-author-chip-verified-text-color: #606060;
|
||||
--yt-live-chat-message-highlight-background-color: #f8f8f8;
|
||||
@@ -77,9 +79,9 @@ html:not(.style-scope) {
|
||||
--yt-pdg-paid-stickers-tab-selection-bar-color: #065FD4;
|
||||
--yt-pdg-paid-stickers-author-name-font-size: 13px;
|
||||
--yt-pdg-paid-stickers-margin-left: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
html:not(.style-scope) {
|
||||
html:not(.style-scope) {
|
||||
--layout_-_display: flex;
|
||||
;
|
||||
|
||||
@@ -359,4 +361,6 @@ html:not(.style-scope) {
|
||||
--layout-fixed-left_-_bottom: 0;
|
||||
--layout-fixed-left_-_left: 0;
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -508,8 +508,26 @@ onMounted(() => {
|
||||
v-for="(label, type) in typeMap"
|
||||
:key="type"
|
||||
:name="type"
|
||||
:tab="label"
|
||||
>
|
||||
<template #tab>
|
||||
<NSpace
|
||||
align="center"
|
||||
size="small"
|
||||
inline
|
||||
>
|
||||
{{ label }}
|
||||
<span
|
||||
:style="{
|
||||
color: enabledTriggerTypes && enabledTriggerTypes[type] ? '#18a058' : '#d03050',
|
||||
fontSize: '14px', // Adjust size as needed
|
||||
verticalAlign: 'middle'
|
||||
}"
|
||||
:title="enabledTriggerTypes && enabledTriggerTypes[type] ? '已启用' : '已禁用'"
|
||||
>
|
||||
●
|
||||
</span>
|
||||
</NSpace>
|
||||
</template>
|
||||
<NSpace vertical>
|
||||
<NSpace
|
||||
v-if="enabledTriggerTypes"
|
||||
|
||||
@@ -30,6 +30,7 @@ export function filterValidActions(
|
||||
options?: {
|
||||
actionType?: ActionType; // 特定操作类型
|
||||
customFilter?: (action: AutoActionItem) => boolean; // 自定义过滤器
|
||||
enabledTriggerTypes?: Ref<Record<TriggerType, boolean>> // 触发类型启用状态
|
||||
}
|
||||
): AutoActionItem[] {
|
||||
return actions.filter(action => {
|
||||
@@ -38,6 +39,11 @@ export function filterValidActions(
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查触发类型是否启用
|
||||
if (options?.enabledTriggerTypes && !options.enabledTriggerTypes.value[triggerType]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 直播状态过滤
|
||||
if (action.triggerConfig.onlyDuringLive && !isLive.value) {
|
||||
return false;
|
||||
@@ -146,6 +152,38 @@ export function processTemplate(
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:发送弹幕并记录日志
|
||||
async function sendAndLogDanmaku(
|
||||
sendHandler: (roomId: number, message: string) => Promise<boolean>,
|
||||
action: AutoActionItem,
|
||||
roomId: number,
|
||||
message: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const success = await sendHandler(roomId, message);
|
||||
logDanmakuHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
message,
|
||||
roomId,
|
||||
success,
|
||||
success ? undefined : '发送失败'
|
||||
).catch(err => console.error('记录弹幕历史失败:', err));
|
||||
return success;
|
||||
} catch (err) {
|
||||
console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err);
|
||||
logDanmakuHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
message,
|
||||
roomId,
|
||||
false,
|
||||
err instanceof Error ? err.toString() : String(err) // 确保err是字符串
|
||||
).catch(e => console.error('记录弹幕历史失败:', e));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行操作的通用函数
|
||||
* @param actions 过滤后的操作列表
|
||||
@@ -220,61 +258,13 @@ export function executeActions(
|
||||
// 更新冷却时间
|
||||
runtimeState.lastExecutionTime[action.id] = Date.now();
|
||||
|
||||
const sendAction = () => sendAndLogDanmaku(handlers.sendLiveDanmaku!, action, roomId, message);
|
||||
|
||||
// 延迟发送
|
||||
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
|
||||
setTimeout(() => {
|
||||
handlers.sendLiveDanmaku!(roomId, message)
|
||||
.then(success => {
|
||||
// 记录弹幕发送历史
|
||||
logDanmakuHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
message,
|
||||
roomId,
|
||||
success,
|
||||
success ? undefined : '发送失败'
|
||||
).catch(err => console.error('记录弹幕历史失败:', err));
|
||||
return success;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err);
|
||||
// 记录失败的发送
|
||||
logDanmakuHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
message,
|
||||
roomId,
|
||||
false,
|
||||
err.toString()
|
||||
).catch(e => console.error('记录弹幕历史失败:', e));
|
||||
});
|
||||
}, action.actionConfig.delaySeconds * 1000);
|
||||
setTimeout(sendAction, action.actionConfig.delaySeconds * 1000);
|
||||
} else {
|
||||
handlers.sendLiveDanmaku(roomId, message)
|
||||
.then(success => {
|
||||
// 记录弹幕发送历史
|
||||
logDanmakuHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
message,
|
||||
roomId,
|
||||
success,
|
||||
success ? undefined : '发送失败'
|
||||
).catch(err => console.error('记录弹幕历史失败:', err));
|
||||
return success;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err);
|
||||
// 记录失败的发送
|
||||
logDanmakuHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
message,
|
||||
roomId,
|
||||
false,
|
||||
err.toString()
|
||||
).catch(e => console.error('记录弹幕历史失败:', e));
|
||||
});
|
||||
sendAction();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -321,7 +311,7 @@ export function executeActions(
|
||||
msg,
|
||||
uid,
|
||||
false,
|
||||
err.toString()
|
||||
err instanceof Error ? err.toString() : String(err) // 确保err是字符串
|
||||
).catch(e => console.error('记录私信历史失败:', e));
|
||||
return false; // 明确返回 false 表示失败
|
||||
});
|
||||
|
||||
@@ -28,19 +28,21 @@ import {
|
||||
createDefaultRuntimeState
|
||||
} from './autoAction/utils';
|
||||
import { evaluateTemplateExpressions } from './autoAction/expressionEvaluator';
|
||||
// 导入 actionUtils 工具函数
|
||||
import { filterValidActions, checkUserFilters, checkCooldown, processTemplate, executeActions } from './autoAction/actionUtils';
|
||||
// 导入 nanoid 用于生成唯一 ID
|
||||
import { nanoid } from 'nanoid';
|
||||
// 导入开发环境判断标志
|
||||
import { isDev } from '@/data/constants.js';
|
||||
|
||||
// 导入所有自动操作子模块
|
||||
import { useGiftThank } from './autoAction/modules/giftThank.js';
|
||||
import { useGuardPm } from './autoAction/modules/guardPm.js';
|
||||
import { useFollowThank } from './autoAction/modules/followThank.js';
|
||||
import { useEntryWelcome } from './autoAction/modules/entryWelcome.js';
|
||||
import { useAutoReply } from './autoAction/modules/autoReply.js';
|
||||
import { useScheduledDanmaku } from './autoAction/modules/scheduledDanmaku.js';
|
||||
import { useSuperChatThank } from './autoAction/modules/superChatThank.js';
|
||||
import { useGiftThank } from './autoAction/modules/giftThank';
|
||||
import { useGuardPm } from './autoAction/modules/guardPm';
|
||||
import { useFollowThank } from './autoAction/modules/followThank';
|
||||
import { useEntryWelcome } from './autoAction/modules/entryWelcome';
|
||||
import { useAutoReply } from './autoAction/modules/autoReply';
|
||||
import { useScheduledDanmaku } from './autoAction/modules/scheduledDanmaku';
|
||||
import { useSuperChatThank } from './autoAction/modules/superChatThank';
|
||||
|
||||
// 定义名为 'autoAction' 的 Pinia store
|
||||
export const useAutoAction = defineStore('autoAction', () => {
|
||||
@@ -540,38 +542,6 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
// 定时检查天选状态 (每5分钟)
|
||||
const tianXuanTimer = setInterval(checkTianXuanStatus, 5 * 60 * 1000);
|
||||
|
||||
/**
|
||||
* 判断是否应处理某个操作项 (基于事件和配置)
|
||||
* @param action 操作项配置
|
||||
* @param event 可选的事件数据
|
||||
* @returns 是否应该处理
|
||||
*/
|
||||
function shouldProcessAction(action: AutoActionItem, event?: EventModel | null): boolean {
|
||||
if (!action.enabled) return false; // 未启用则跳过
|
||||
if (!enabledTriggerTypes.value[action.triggerType]) return false; // 触发类型未启用则跳过
|
||||
|
||||
// 检查模板是否为空 (添加新的检查)
|
||||
if (!action.template || action.template.trim() === '') {
|
||||
console.warn(`[AutoAction] 跳过操作 "${action.name}":未设置有效模板`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 根据配置检查条件
|
||||
if (action.triggerConfig.onlyDuringLive && !isLive.value) return false; // 仅直播时
|
||||
if (action.triggerConfig.ignoreTianXuan && isTianXuanActive.value) return false; // 忽略天选时
|
||||
// 用户过滤条件
|
||||
if (event && action.triggerConfig.userFilterEnabled) {
|
||||
if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) return false; // 要求粉丝牌
|
||||
if (action.triggerConfig.requireCaptain && event.guard_level === GuardLevel.None) return false; // 要求舰长
|
||||
}
|
||||
// 逻辑表达式判断
|
||||
if (action.logicalExpression && event) {
|
||||
const context = buildExecutionContext(event, roomId.value, action.triggerType);
|
||||
if (!evaluateExpression(action.logicalExpression, context)) return false; // 表达式不满足
|
||||
}
|
||||
return true; // 所有条件满足
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的事件
|
||||
* @param event 事件数据
|
||||
@@ -579,10 +549,8 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
*/
|
||||
function processEvent(event: EventModel, triggerType: TriggerType) {
|
||||
if (!roomId.value) return; // 房间 ID 无效则跳过
|
||||
|
||||
// 检查触发类型是否启用
|
||||
if (!enabledTriggerTypes.value[triggerType]) return;
|
||||
|
||||
// 根据触发类型调用相应模块的处理函数
|
||||
switch (triggerType) {
|
||||
case TriggerType.DANMAKU:
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
<div
|
||||
ref="editorContainer"
|
||||
:style="`height: ${height}px;`"
|
||||
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { editor } from 'monaco-editor'; // 全部导入
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
|
||||
const value = defineModel<string>('value')
|
||||
|
||||
@@ -16,10 +17,13 @@ const { language, height = 400 } = defineProps<{
|
||||
height?: number
|
||||
}>()
|
||||
|
||||
const editorContainer = ref()
|
||||
const editorContainer = ref<HTMLElement>()
|
||||
let editorInstance: editor.IStandaloneCodeEditor | null = null
|
||||
|
||||
onMounted(() => {
|
||||
const e = editor.create(editorContainer.value, {
|
||||
if (!editorContainer.value) return
|
||||
|
||||
editorInstance = editor.create(editorContainer.value, {
|
||||
value: value.value,
|
||||
language: language,
|
||||
minimap: {
|
||||
@@ -28,8 +32,36 @@ onMounted(() => {
|
||||
colorDecorators: true,
|
||||
automaticLayout: true
|
||||
})
|
||||
e.onDidChangeModelContent(() => {
|
||||
value.value = e.getValue()
|
||||
|
||||
editorInstance.onDidChangeModelContent(() => {
|
||||
if (editorInstance) {
|
||||
const currentValue = editorInstance.getValue()
|
||||
if (currentValue !== value.value) {
|
||||
value.value = currentValue
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (editorInstance) {
|
||||
editorInstance.dispose()
|
||||
editorInstance = null
|
||||
}
|
||||
})
|
||||
|
||||
watch(value, (newValue) => {
|
||||
if (editorInstance && newValue !== editorInstance.getValue()) {
|
||||
editorInstance.setValue(newValue ?? '')
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => language, (newLang) => {
|
||||
if (editorInstance) {
|
||||
const model = editorInstance.getModel()
|
||||
if (model) {
|
||||
editor.setModelLanguage(model, newLang)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -167,9 +167,7 @@ function onLoginButtonClick() {
|
||||
refreshDate: Date.now()
|
||||
}
|
||||
message.success(`成功登陆为 ${data?.data.account.name}`)
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 1000)
|
||||
} else {
|
||||
message.error(data.message)
|
||||
}
|
||||
|
||||
@@ -95,12 +95,8 @@ function InitVersionCheck() {
|
||||
localStorage.setItem('Version', currentVersion)
|
||||
console.log(`[vtsuru] 发现新版本: ${currentVersion}`)
|
||||
|
||||
const url = new URL(window.location.href)
|
||||
const path = url.pathname
|
||||
|
||||
if (!path.startsWith('/obs')) {
|
||||
if (isTauri()) {
|
||||
location.reload();
|
||||
if (window.$route.meta.forceReload || isTauri()) {
|
||||
location.reload()
|
||||
}
|
||||
else {
|
||||
const n = notification.info({
|
||||
@@ -132,7 +128,6 @@ function InitVersionCheck() {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}, 60 * 1000)
|
||||
|
||||
@@ -4,6 +4,26 @@ import { VNode } from "vue";
|
||||
import { FETCH_API } from "./constants";
|
||||
|
||||
export const updateNotes: updateNoteType[] = [
|
||||
{
|
||||
ver: 5,
|
||||
date: '2025.4.24',
|
||||
items: [
|
||||
{
|
||||
type: 'new',
|
||||
title: '新增弹幕姬管理页面',
|
||||
content: [
|
||||
[
|
||||
'弹幕姬现在可用,兼容 blivechat 样式',
|
||||
() => h(NImage, { src: 'https://pan.suki.club/d/vtsuru/imgur/3c5a6f68-1aa4-4b96-a25e-dba2581ac898.png', width: 300 }),
|
||||
],
|
||||
[
|
||||
'大部分功能都和 blivechat 一致, 不过目前还无法提供本地文件访问, 部分css中需要使用图片等本地资源样式的需要等 EventFetcher 更新相关功能后才能使用\r\n',
|
||||
'配置上传之后会自动同步到obs中'
|
||||
]
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
ver: 4,
|
||||
date: '2025.4.22',
|
||||
|
||||
@@ -118,3 +118,301 @@ export const IndexTemplateMap: TemplateMapType = {
|
||||
component: DefaultIndexTemplateVue
|
||||
}
|
||||
};
|
||||
|
||||
export const defaultDanmujiCss = `@import url("https://fonts.googleapis.com/css?family=Changa%20One");
|
||||
@import url("https://fonts.googleapis.com/css?family=Imprima");
|
||||
|
||||
/* Transparent background */
|
||||
yt-live-chat-renderer {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-renderer {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip #author-name {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
yt-live-chat-item-list-renderer #item-scroller {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
yt-live-chat-interact-message-renderer #content,
|
||||
yt-live-chat-text-message-renderer #content,
|
||||
yt-live-chat-membership-item-renderer #content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Hide header and input */
|
||||
yt-live-chat-header-renderer,
|
||||
yt-live-chat-message-input-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide unimportant messages */
|
||||
yt-live-chat-interact-message-renderer[is-deleted],
|
||||
yt-live-chat-text-message-renderer[is-deleted],
|
||||
yt-live-chat-membership-item-renderer[is-deleted] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-mode-change-message-renderer,
|
||||
yt-live-chat-viewer-engagement-message-renderer,
|
||||
yt-live-chat-restricted-participation-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer a,
|
||||
yt-live-chat-membership-item-renderer a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* Global Setting */
|
||||
yt-live-chat-renderer {
|
||||
|
||||
}
|
||||
#item-scroller {
|
||||
|
||||
}
|
||||
|
||||
/* Reduce side padding */
|
||||
yt-live-chat-interact-message-renderer,
|
||||
yt-live-chat-text-message-renderer {
|
||||
padding-left: 4px !important;
|
||||
padding-right: 4px !important;
|
||||
}
|
||||
|
||||
/* Outlines */
|
||||
yt-live-chat-renderer * {
|
||||
text-shadow: -2px -2px #000000, -2px -1px #000000, -2px 0px #000000, -2px 1px #000000, -2px 2px #000000, -1px -2px #000000, -1px -1px #000000, -1px 0px #000000, -1px 1px #000000, -1px 2px #000000, 0px -2px #000000, 0px -1px #000000, 0px 0px #000000, 0px 1px #000000, 0px 2px #000000, 1px -2px #000000, 1px -1px #000000, 1px 0px #000000, 1px 1px #000000, 1px 2px #000000, 2px -2px #000000, 2px -1px #000000, 2px 0px #000000, 2px 1px #000000, 2px 2px #000000;
|
||||
font-family: "Imprima", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 18px !important;
|
||||
line-height: 20px !important;
|
||||
}
|
||||
|
||||
/* Avatars */
|
||||
yt-live-chat-interact-message-renderer #author-photo,
|
||||
yt-live-chat-interact-message-renderer #author-photo img,
|
||||
yt-live-chat-text-message-renderer #author-photo,
|
||||
yt-live-chat-text-message-renderer #author-photo img,
|
||||
yt-live-chat-paid-message-renderer #author-photo,
|
||||
yt-live-chat-paid-message-renderer #author-photo img,
|
||||
yt-live-chat-membership-item-renderer #author-photo,
|
||||
yt-live-chat-membership-item-renderer #author-photo img {
|
||||
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 24px !important;
|
||||
margin-right: 6px !important;
|
||||
}
|
||||
|
||||
/* Channel names */
|
||||
yt-live-chat-interact-message-renderer #content #author-name,
|
||||
yt-live-chat-text-message-renderer #content #author-name {
|
||||
|
||||
}
|
||||
yt-live-chat-interact-message-renderer #author-name[type="owner"],
|
||||
yt-live-chat-interact-message-renderer yt-live-chat-author-badge-renderer[type="owner"],
|
||||
yt-live-chat-text-message-renderer #author-name[type="owner"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="owner"] {
|
||||
color: #ffd600 !important;
|
||||
}
|
||||
yt-live-chat-interact-message-renderer #author-name[type="moderator"],
|
||||
yt-live-chat-interact-message-renderer yt-live-chat-author-badge-renderer[type="moderator"],
|
||||
yt-live-chat-text-message-renderer #author-name[type="moderator"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
|
||||
color: #5e84f1 !important;
|
||||
}
|
||||
yt-live-chat-interact-message-renderer #author-name[type="member"],
|
||||
yt-live-chat-interact-message-renderer yt-live-chat-author-badge-renderer[type="member"],
|
||||
yt-live-chat-text-message-renderer #author-name[type="member"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="member"] {
|
||||
color: #0f9d58 !important;
|
||||
}
|
||||
|
||||
yt-live-chat-interact-message-renderer #author-name,
|
||||
yt-live-chat-text-message-renderer #author-name {
|
||||
|
||||
color: #cccccc !important;
|
||||
font-family: "Changa One", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 20px !important;
|
||||
line-height: 20px !important;
|
||||
}
|
||||
|
||||
/* Show colon */
|
||||
yt-live-chat-text-message-renderer #author-name::after {
|
||||
content: ":";
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* Hide badges */
|
||||
yt-live-chat-interact-message-renderer #chat-badges,
|
||||
yt-live-chat-text-message-renderer #chat-badges {
|
||||
|
||||
vertical-align: text-top !important;
|
||||
}
|
||||
img.yt-live-chat-author-badge-renderer, yt-icon.yt-live-chat-author-badge-renderer {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Medal */
|
||||
yt-live-chat-author-medal-renderer {
|
||||
display: none;
|
||||
|
||||
}
|
||||
yt-live-chat-author-medal-renderer[is-fan-group] {
|
||||
display: flex;
|
||||
}
|
||||
#medal-name.yt-live-chat-author-medal-renderer {
|
||||
|
||||
font-size: 14px !important;
|
||||
line-height: 14px !important;
|
||||
}
|
||||
|
||||
#medal-level.yt-live-chat-author-medal-renderer {
|
||||
|
||||
font-size: 14px !important;
|
||||
line-height: 14px !important;
|
||||
}
|
||||
|
||||
|
||||
/* Messages */
|
||||
yt-live-chat-interact-message-renderer #message,
|
||||
yt-live-chat-interact-message-renderer #message *,
|
||||
yt-live-chat-text-message-renderer #message,
|
||||
yt-live-chat-text-message-renderer #message * {
|
||||
color: #ffffff !important;
|
||||
font-family: "Imprima", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 18px !important;
|
||||
line-height: 18px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #image-and-message {
|
||||
display: inline !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
yt-live-chat-text-message-renderer #message {
|
||||
display: inline !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #image-and-message .emoji {
|
||||
width: auto !important;
|
||||
height: 48px !important;
|
||||
}
|
||||
|
||||
#image-and-message img[display="block"] {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#image-and-message img[display="inline"] {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
/* Timestamps */
|
||||
|
||||
|
||||
|
||||
/* Background colors */
|
||||
body {
|
||||
overflow: hidden;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer,
|
||||
yt-live-chat-text-message-renderer[is-highlighted] {
|
||||
background-color: rgba(204, 204, 204, 0) !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[author-type="owner"],
|
||||
yt-live-chat-text-message-renderer[author-type="owner"][is-highlighted] {
|
||||
background-color: rgba(255, 214, 0, 0) !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[author-type="moderator"],
|
||||
yt-live-chat-text-message-renderer[author-type="moderator"][is-highlighted] {
|
||||
background-color: rgba(94, 132, 241, 0) !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[author-type="member"],
|
||||
yt-live-chat-text-message-renderer[author-type="member"][is-highlighted] {
|
||||
background-color: rgba(15, 157, 88, 0) !important;
|
||||
}
|
||||
|
||||
/* SuperChat/Fan Funding Messages */
|
||||
yt-live-chat-paid-message-renderer {
|
||||
margin: 4px 0 !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #author-name,
|
||||
yt-live-chat-paid-message-renderer #author-name *,
|
||||
yt-live-chat-membership-item-renderer #header-content-inner-column,
|
||||
yt-live-chat-membership-item-renderer #header-content-inner-column * {
|
||||
color: #ffffff !important;
|
||||
font-family: "Changa One", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 20px !important;
|
||||
line-height: 20px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #purchase-amount,
|
||||
yt-live-chat-paid-message-renderer #purchase-amount *,
|
||||
yt-live-chat-membership-item-renderer #header-subtext,
|
||||
yt-live-chat-membership-item-renderer #header-subtext * {
|
||||
color: #ffffff !important;
|
||||
font-family: "Imprima", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 18px !important;
|
||||
line-height: 18px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #content,
|
||||
yt-live-chat-paid-message-renderer #content * {
|
||||
color: #ffffff !important;
|
||||
font-family: "Imprima", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 18px !important;
|
||||
line-height: 18px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer #card,
|
||||
yt-live-chat-membership-item-renderer #header {
|
||||
background-color: #0f9d58 !important;
|
||||
margin: 4px 0 !important;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* SuperChat Ticker */
|
||||
yt-live-chat-ticker-paid-message-item-renderer,
|
||||
yt-live-chat-ticker-paid-message-item-renderer *,
|
||||
yt-live-chat-ticker-sponsor-item-renderer,
|
||||
yt-live-chat-ticker-sponsor-item-renderer * {
|
||||
color: #ffffff !important;
|
||||
font-family: "Imprima", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Animation */
|
||||
@keyframes anim {
|
||||
|
||||
}
|
||||
|
||||
yt-live-chat-interact-message-renderer,
|
||||
yt-live-chat-text-message-renderer,
|
||||
yt-live-chat-membership-item-renderer,
|
||||
yt-live-chat-paid-message-renderer {
|
||||
animation: anim 0ms;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
`
|
||||
12
src/main.ts
12
src/main.ts
@@ -5,6 +5,18 @@ import App from './App.vue';
|
||||
import { InitVTsuru } from './data/Initializer';
|
||||
import emitter from './mitt';
|
||||
import router from './router';
|
||||
import { loader } from '@guolao/vue-monaco-editor'
|
||||
|
||||
loader.config({
|
||||
paths: {
|
||||
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor/min/vs'
|
||||
},
|
||||
'vs/nls': {
|
||||
availableLanguages: {
|
||||
'*': 'zh-cn'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
export const getPinia = () => pinia
|
||||
|
||||
@@ -8,6 +8,7 @@ export default {
|
||||
component: () => import('@/client/ClientIndex.vue'),
|
||||
meta: {
|
||||
title: '首页',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -16,6 +17,7 @@ export default {
|
||||
component: () => import('@/client/ClientFetcher.vue'),
|
||||
meta: {
|
||||
title: 'EventFetcher',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -24,6 +26,7 @@ export default {
|
||||
component: () => import('@/client/ClientSettings.vue'),
|
||||
meta: {
|
||||
title: '设置',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -32,6 +35,7 @@ export default {
|
||||
component: () => import('@/client/DanmakuWindowManager.vue'),
|
||||
meta: {
|
||||
title: '弹幕窗口管理',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -40,6 +44,7 @@ export default {
|
||||
component: () => import('@/client/ClientAutoAction.vue'),
|
||||
meta: {
|
||||
title: '自动操作管理',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -55,6 +60,7 @@ export default {
|
||||
component: () => import('@/client/ClientTest.vue'),
|
||||
meta: {
|
||||
title: '测试',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
@@ -138,7 +138,7 @@ export default //管理页面
|
||||
name: 'manage-danmuji',
|
||||
component: () => import('@/views/manage/DanmujiManageView.vue'),
|
||||
meta: {
|
||||
title: '点歌',
|
||||
title: '弹幕姬',
|
||||
keepAlive: true,
|
||||
danmaku: true,
|
||||
isNew: true
|
||||
|
||||
@@ -7,7 +7,8 @@ export default {
|
||||
name: 'obs-live-lottery',
|
||||
component: () => import('@/views/obs/LiveLotteryOBS.vue'),
|
||||
meta: {
|
||||
title: '直播抽奖'
|
||||
title: '直播抽奖',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -16,7 +17,8 @@ export default {
|
||||
alias: 'song-request',
|
||||
component: () => import('@/views/obs/LiveRequestOBS.vue'),
|
||||
meta: {
|
||||
title: '弹幕点播'
|
||||
title: '弹幕点播',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -24,7 +26,8 @@ export default {
|
||||
name: 'obs-live-request-today',
|
||||
component: () => import('@/views/obs/LiveRequestProcessedOBS.vue'),
|
||||
meta: {
|
||||
title: '弹幕点播-今日'
|
||||
title: '弹幕点播-今日',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -32,7 +35,8 @@ export default {
|
||||
name: 'obs-queue',
|
||||
component: () => import('@/views/obs/QueueOBS.vue'),
|
||||
meta: {
|
||||
title: '弹幕排队'
|
||||
title: '弹幕排队',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -40,7 +44,8 @@ export default {
|
||||
name: 'obs-music-request',
|
||||
component: () => import('@/views/obs/MusicRequestOBS.vue'),
|
||||
meta: {
|
||||
title: '弹幕排队 (播放'
|
||||
title: '弹幕排队 (播放列表)',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -48,7 +53,8 @@ export default {
|
||||
name: 'obs-question-display',
|
||||
component: () => import('@/views/obs/QuestionDisplayOBS.vue'),
|
||||
meta: {
|
||||
title: '棉花糖展示'
|
||||
title: '棉花糖展示',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -56,7 +62,8 @@ export default {
|
||||
name: 'obs-web-fetcher',
|
||||
component: () => import('@/views/obs/WebFetcherOBS.vue'),
|
||||
meta: {
|
||||
title: '弹幕收集器 (OBS版'
|
||||
title: '弹幕收集器 (OBS版)',
|
||||
forceReload: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -64,7 +71,8 @@ export default {
|
||||
name: 'obs-danmuji',
|
||||
component: () => import('@/views/obs/DanmujiOBS.vue'),
|
||||
meta: {
|
||||
title: '弹幕机'
|
||||
title: '弹幕姬',
|
||||
forceReload: true,
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -21,7 +21,8 @@ export default [
|
||||
component: () => import('@/client/ClientDanmakuWindow.vue'),
|
||||
meta: {
|
||||
title: '弹幕窗口',
|
||||
ignoreLogin: true
|
||||
ignoreLogin: true,
|
||||
forceReload: true,
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -243,7 +243,7 @@ onMounted(async () => {
|
||||
style="width: 100%"
|
||||
>
|
||||
<NAlert type="success">
|
||||
你已完成验证! 请妥善保存你的登陆链接, 请勿让其他人获取. 丢失后可以再次通过认证流程获得
|
||||
你已完成验证! 请妥善保存你的登陆链接, 请勿让其他人获取. 丢失后可以再次通过认证流程获得. 把这个链接复制到浏览器打开即可登录
|
||||
</NAlert>
|
||||
<NText> 你的登陆链接为: </NText>
|
||||
<NInputGroup>
|
||||
|
||||
@@ -84,12 +84,14 @@ watch(aplayer, () => {
|
||||
|
||||
// 邮箱验证相关
|
||||
const canResendEmail = ref(false)
|
||||
const isBiliVerified = computed(() => accountInfo.value?.isBiliVerified)
|
||||
|
||||
// 图标渲染函数 - 用于菜单项
|
||||
const renderIcon = (icon: any) => () => h(NIcon, null, { default: () => h(icon) })
|
||||
|
||||
// 菜单配置
|
||||
const menuOptions = [
|
||||
const menuOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'manage-history' } }, { default: () => '历史' }),
|
||||
key: 'manage-history',
|
||||
@@ -233,17 +235,17 @@ const menuOptions = [
|
||||
),
|
||||
]
|
||||
),
|
||||
default: () => accountInfo.value?.isBiliVerified
|
||||
default: () => isBiliVerified.value
|
||||
? '需要使用直播弹幕的功能'
|
||||
: '你尚未进行 Bilibili 认证, 请前往面板进行绑定',
|
||||
},
|
||||
),
|
||||
key: 'manage-danmaku',
|
||||
icon: renderIcon(Chat24Filled),
|
||||
disabled: accountInfo.value?.isEmailVerified === false,
|
||||
disabled: accountInfo.value?.isEmailVerified === false || !isBiliVerified.value,
|
||||
children: [
|
||||
{
|
||||
label: () => h(
|
||||
label: () => !isBiliVerified.value ? '弹幕机' : h(
|
||||
NBadge,
|
||||
{ value: '新', offset: [15, 12], type: 'info' },
|
||||
() => h(
|
||||
@@ -260,19 +262,21 @@ const menuOptions = [
|
||||
)
|
||||
),
|
||||
key: 'manage-danmuji',
|
||||
disabled: !isBiliVerified.value,
|
||||
icon: renderIcon(Lottery24Filled),
|
||||
},
|
||||
{
|
||||
label: () => h(
|
||||
label: () => !isBiliVerified.value ? '抽奖' : h(
|
||||
RouterLink,
|
||||
{ to: { name: 'manage-liveLottery' } },
|
||||
{ default: () => '抽奖' },
|
||||
),
|
||||
key: 'manage-liveLottery',
|
||||
icon: renderIcon(Lottery24Filled),
|
||||
disabled: !isBiliVerified.value,
|
||||
},
|
||||
{
|
||||
label: () => h(
|
||||
label: () => !isBiliVerified.value ? '点播' : h(
|
||||
NTooltip,
|
||||
{},
|
||||
{
|
||||
@@ -286,9 +290,10 @@ const menuOptions = [
|
||||
),
|
||||
key: 'manage-liveRequest',
|
||||
icon: renderIcon(MusicalNote),
|
||||
disabled: !isBiliVerified.value,
|
||||
},
|
||||
{
|
||||
label: () => h(
|
||||
label: () => !isBiliVerified.value ? '点歌' : h(
|
||||
NTooltip,
|
||||
{},
|
||||
{
|
||||
@@ -302,28 +307,32 @@ const menuOptions = [
|
||||
),
|
||||
key: 'manage-musicRequest',
|
||||
icon: renderIcon(MusicalNote),
|
||||
disabled: !isBiliVerified.value,
|
||||
},
|
||||
{
|
||||
label: () => h(
|
||||
label: () => !isBiliVerified.value ? '排队' : h(
|
||||
RouterLink,
|
||||
{ to: { name: 'manage-liveQueue' } },
|
||||
{ default: () => '排队' },
|
||||
),
|
||||
key: 'manage-liveQueue',
|
||||
icon: renderIcon(PeopleQueue24Filled),
|
||||
disabled: !isBiliVerified.value,
|
||||
},
|
||||
{
|
||||
label: () => h(
|
||||
label: () => !isBiliVerified.value ? '读弹幕' : h(
|
||||
RouterLink,
|
||||
{ to: { name: 'manage-speech' } },
|
||||
{ default: () => '读弹幕' },
|
||||
),
|
||||
key: 'manage-speech',
|
||||
icon: renderIcon(TabletSpeaker24Filled),
|
||||
disabled: !isBiliVerified.value,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
// 重发验证邮件
|
||||
async function resendEmail() {
|
||||
@@ -385,7 +394,7 @@ onMounted(() => {
|
||||
<!-- 顶部导航栏 -->
|
||||
<NLayoutHeader
|
||||
bordered
|
||||
style="height: 50px; padding: 10px 15px 5px 15px"
|
||||
style="height: var(--vtsuru-header-height); padding: 10px 15px 5px 15px"
|
||||
>
|
||||
<NPageHeader>
|
||||
<template #title>
|
||||
@@ -432,6 +441,7 @@ onMounted(() => {
|
||||
>
|
||||
<!-- 侧边导航栏 -->
|
||||
<NLayoutSider
|
||||
v-if="accountInfo?.isEmailVerified"
|
||||
ref="sider"
|
||||
bordered
|
||||
show-trigger
|
||||
@@ -541,8 +551,8 @@ onMounted(() => {
|
||||
<!-- 内容区域 -->
|
||||
<NLayout>
|
||||
<!-- 主内容区域 -->
|
||||
<NScrollbar :style="`height: calc(100vh - 50px - ${aplayerHeight}px)`">
|
||||
<NLayoutContent content-style="margin: 12px; margin-right: 16px">
|
||||
<NScrollbar :style="`height: calc(100vh - var(--vtsuru-header-height) - ${aplayerHeight}px)`">
|
||||
<NLayoutContent content-style="margin: var(--vtsuru-content-padding); margin-right: calc(var(--vtsuru-content-padding) + 4px)">
|
||||
<NElement>
|
||||
<!-- 已验证邮箱的用户显示内容 -->
|
||||
<RouterView
|
||||
@@ -706,12 +716,13 @@ onMounted(() => {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
"
|
||||
:class="isDarkMode ? 'login-dark-bg' : ''"
|
||||
>
|
||||
<template v-if="!isLoadingAccount">
|
||||
<NCard
|
||||
style="max-width: 520px; width: 100%; min-width: 350px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); margin: 16px;"
|
||||
class="login-card"
|
||||
:bordered="false"
|
||||
>
|
||||
<template #header>
|
||||
@@ -753,7 +764,7 @@ onMounted(() => {
|
||||
size="small"
|
||||
>
|
||||
<div style="text-align: center;">
|
||||
如果你不是主播且不发送棉花糖(提问)的话则不需要注册登录
|
||||
如果你不是主播且不发送棉花糖(提问)的话则不需要注册登录, 直接访问认证完成后给出的链接即可
|
||||
</div>
|
||||
<NFlex
|
||||
justify="center"
|
||||
@@ -762,7 +773,7 @@ onMounted(() => {
|
||||
<NButton
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="$router.push({ name: 'bili-user'})"
|
||||
@click="$router.push({ name: 'bili-user' })"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="BrowsersOutline" />
|
||||
@@ -792,8 +803,8 @@ onMounted(() => {
|
||||
</template>
|
||||
<template v-else>
|
||||
<NCard
|
||||
class="loading-card"
|
||||
:bordered="false"
|
||||
style="min-width: 300px; width: 100%; max-width: 400px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); margin: 16px;"
|
||||
>
|
||||
<NFlex
|
||||
vertical
|
||||
@@ -816,6 +827,31 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.login-dark-bg {
|
||||
background: linear-gradient(135deg, rgba(30,30,35,0.9) 0%, rgba(20,20,25,0.95) 100%) !important;
|
||||
background: linear-gradient(135deg, rgba(30, 30, 35, 0.9) 0%, rgba(20, 20, 25, 0.95) 100%) !important;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
max-width: 520px;
|
||||
width: 90%;
|
||||
min-width: 300px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.loading-card {
|
||||
min-width: 280px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-card, .loading-card {
|
||||
width: 95%;
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { useDanmakuClient } from '@/store/useDanmakuClient';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import MessageRender from './blivechat/MessageRender.vue';
|
||||
// @ts-ignore
|
||||
import * as constants from './blivechat/constants';
|
||||
// @ts-ignore
|
||||
import * as pronunciation from './blivechat/utils/pronunciation';
|
||||
import { DownloadConfig, useAccount } from '@/api/account';
|
||||
import { useAccount, DownloadConfig, GetConfigHash } from '@/api/account';
|
||||
import { QueryGetAPI } from '@/api/query';
|
||||
import { VTSURU_API_URL } from '@/data/constants';
|
||||
import { DanmakuInfo, GiftInfo, GuardInfo, SCInfo } from '@/data/DanmakuClients/OpenLiveClient';
|
||||
import { defaultDanmujiCss, VTSURU_API_URL } from '@/data/constants';
|
||||
import { useWebRTC } from '@/store/useRTC';
|
||||
import { NAlert } from 'naive-ui';
|
||||
import { useRoute } from 'vue-router';
|
||||
// @ts-ignore
|
||||
import * as pronunciation from './blivechat/utils/pronunciation';
|
||||
import { EventDataTypes, EventModel } from '@/api/api-models';
|
||||
// @ts-ignore
|
||||
import * as trie from './blivechat/utils/trie';
|
||||
|
||||
export interface DanmujiConfig {
|
||||
@@ -39,20 +40,6 @@ export interface DanmujiConfig {
|
||||
}[]
|
||||
}
|
||||
|
||||
defineExpose({ setCss })
|
||||
const { customCss, isOBS = true } = defineProps<{
|
||||
customCss?: string
|
||||
isOBS?: boolean,
|
||||
active?: boolean,
|
||||
visible?: boolean,
|
||||
}>()
|
||||
|
||||
const messageRender = ref()
|
||||
const client = await useDanmakuClient().initOpenlive()
|
||||
const pronunciationConverter = new pronunciation.PronunciationConverter()
|
||||
const accountInfo = useAccount()
|
||||
const route = useRoute()
|
||||
|
||||
// 默认配置
|
||||
const defaultConfig: DanmujiConfig = {
|
||||
minGiftPrice: 0.1,
|
||||
@@ -74,9 +61,29 @@ const defaultConfig: DanmujiConfig = {
|
||||
emoticons: []
|
||||
}
|
||||
|
||||
defineExpose({ setCss, testAddMessage })
|
||||
const props = defineProps<{
|
||||
active?: boolean,
|
||||
visible?: boolean,
|
||||
config?: DanmujiConfig
|
||||
}>()
|
||||
|
||||
const customCss = useStorage('danmuji-css', '');
|
||||
|
||||
const isOBS = computed(() => {
|
||||
// @ts-ignore
|
||||
return window.obsstudio !== undefined
|
||||
})
|
||||
|
||||
const messageRender = ref()
|
||||
const client = await useDanmakuClient().initOpenlive()
|
||||
const pronunciationConverter = new pronunciation.PronunciationConverter()
|
||||
const accountInfo = useAccount()
|
||||
const route = useRoute()
|
||||
|
||||
const config = computed(() => props.config ?? defaultConfig)
|
||||
|
||||
let textEmoticons: { keyword: string, url: string }[] = []
|
||||
const config = ref<DanmujiConfig>(JSON.parse(JSON.stringify(defaultConfig)))
|
||||
const rtc = await useWebRTC().Init('slave')
|
||||
|
||||
// 表情词典树计算
|
||||
const emoticonsTrie = computed(() => {
|
||||
@@ -113,27 +120,27 @@ function setCss(css: string) {
|
||||
/**
|
||||
* 处理弹幕消息
|
||||
*/
|
||||
async function onAddText(data: DanmakuInfo, command: unknown) {
|
||||
if (!config.value.showDanmaku || !filterTextMessage(data)) {
|
||||
async function onAddText(event: EventModel, command: unknown) {
|
||||
if (!config.value.showDanmaku || !filterTextMessage(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
const richContent = await getRichContent(data)
|
||||
const richContent = await getRichContent(event)
|
||||
// 合并要放在异步调用后面,因为异步调用后可能有新的消息,会漏合并
|
||||
if (mergeSimilarText(data.msg)) {
|
||||
if (mergeSimilarText(event.msg)) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: data.msg_id,
|
||||
id: `msg-${new Date().getTime()}-${event.uid}`,
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
avatarUrl: data.uface,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
authorName: data.uname,
|
||||
authorType: getAuthorType(data.open_id, data.guard_level),
|
||||
content: data.msg,
|
||||
avatarUrl: event.uface,
|
||||
time: new Date(),
|
||||
authorName: event.uname,
|
||||
authorType: getAuthorType(event.open_id, event.guard_level),
|
||||
content: event.msg,
|
||||
richContent: richContent,
|
||||
privilegeType: data.guard_level,
|
||||
privilegeType: event.guard_level,
|
||||
repeated: 1,
|
||||
translation: ''
|
||||
}
|
||||
@@ -143,32 +150,32 @@ async function onAddText(data: DanmakuInfo, command: unknown) {
|
||||
/**
|
||||
* 处理礼物消息
|
||||
*/
|
||||
function onAddGift(data: GiftInfo, command: unknown) {
|
||||
function onAddGift(event: EventModel, command: unknown) {
|
||||
if (!config.value.showGift) {
|
||||
return
|
||||
}
|
||||
|
||||
const price = (data.price * data.gift_num) / 1000
|
||||
const price = (event.price * event.num) / 1000
|
||||
// 价格过滤
|
||||
if (price < (config.value.minGiftPrice ?? 0)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试合并相似礼物
|
||||
if (mergeSimilarGift(data.uname, price, !data.paid ? price : 0, data.gift_name, data.gift_num)) {
|
||||
if (mergeSimilarGift(event.uname, price, !event.price ? price : 0, event.msg, event.num)) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: data.msg_id,
|
||||
id: `gift-${new Date().getTime()}-${event.uid}`,
|
||||
type: constants.MESSAGE_TYPE_GIFT,
|
||||
avatarUrl: data.uface,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
authorName: data.uname,
|
||||
authorNamePronunciation: getPronunciation(data.uname),
|
||||
avatarUrl: event.uface,
|
||||
time: new Date(),
|
||||
authorName: event.uname,
|
||||
authorNamePronunciation: getPronunciation(event.uname),
|
||||
price: price,
|
||||
giftName: data.gift_name,
|
||||
num: data.gift_num
|
||||
giftName: event.msg,
|
||||
num: event.num
|
||||
}
|
||||
messageRender.value.addMessage(message)
|
||||
}
|
||||
@@ -176,19 +183,19 @@ function onAddGift(data: GiftInfo, command: unknown) {
|
||||
/**
|
||||
* 处理舰长上舰消息
|
||||
*/
|
||||
function onAddMember(data: GuardInfo, command: unknown) {
|
||||
if (!config.value.showGift || !filterNewMemberMessage(data)) {
|
||||
function onAddMember(event: EventModel, command: unknown) {
|
||||
if (!config.value.showGift || !filterNewMemberMessage(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: data.msg_id,
|
||||
id: `${event.type}-${new Date().getTime()}-${event.uid}`,
|
||||
type: constants.MESSAGE_TYPE_MEMBER,
|
||||
avatarUrl: data.user_info.uface,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
authorName: data.user_info.uname,
|
||||
authorNamePronunciation: getPronunciation(data.user_info.uname),
|
||||
privilegeType: data.guard_level,
|
||||
avatarUrl: event.uface,
|
||||
time: new Date(),
|
||||
authorName: event.uname,
|
||||
authorNamePronunciation: getPronunciation(event.uname),
|
||||
privilegeType: event.guard_level,
|
||||
title: '新舰长'
|
||||
}
|
||||
messageRender.value.addMessage(message)
|
||||
@@ -197,24 +204,24 @@ function onAddMember(data: GuardInfo, command: unknown) {
|
||||
/**
|
||||
* 处理醒目留言消息
|
||||
*/
|
||||
function onAddSuperChat(data: SCInfo) {
|
||||
if (!config.value.showGift || !filterSuperChatMessage(data)) {
|
||||
function onAddSuperChat(event: EventModel, command: unknown) {
|
||||
if (!config.value.showGift || !filterSuperChatMessage(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.rmb < (config.value.minGiftPrice ?? 0)) {
|
||||
if (event.price < (config.value.minGiftPrice ?? 0)) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: data.msg_id,
|
||||
id: `${event.type}-${new Date().getTime()}-${event.uid}`,
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
avatarUrl: data.uface,
|
||||
authorName: data.uname,
|
||||
authorNamePronunciation: getPronunciation(data.uname),
|
||||
price: data.rmb,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
content: data.msg_id.trim(),
|
||||
avatarUrl: event.uface,
|
||||
authorName: event.uname,
|
||||
authorNamePronunciation: getPronunciation(event.uname),
|
||||
price: event.price,
|
||||
time: new Date(),
|
||||
content: event.msg.trim(),
|
||||
translation: ''
|
||||
}
|
||||
messageRender.value.addMessage(message)
|
||||
@@ -223,8 +230,31 @@ function onAddSuperChat(data: SCInfo) {
|
||||
/**
|
||||
* 处理SC撤回
|
||||
*/
|
||||
function onDelSuperChat(data: { id: string }) {
|
||||
messageRender.value.deleteMessage(data.id)
|
||||
function onDelSuperChat(event: EventModel, command: unknown) {
|
||||
let messageIdsToDelete: string[] = [];
|
||||
|
||||
// 尝试从command中获取需要删除的SC ID
|
||||
if (command && typeof command === 'object' && 'data' in command) {
|
||||
const commandData = command.data;
|
||||
if (commandData && typeof commandData === 'object') {
|
||||
if ('message_ids' in commandData && Array.isArray(commandData.message_ids)) {
|
||||
messageIdsToDelete = commandData.message_ids.map(id => String(id));
|
||||
} else if ('message_id' in commandData) {
|
||||
messageIdsToDelete.push(String(commandData.message_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 尝试使用消息内容作为ID
|
||||
else if (event.msg) {
|
||||
messageIdsToDelete.push(event.msg);
|
||||
}
|
||||
|
||||
if (messageIdsToDelete.length > 0) {
|
||||
console.log(`正在删除SC,ID: ${messageIdsToDelete.join(', ')}`);
|
||||
messageIdsToDelete.forEach(id => messageRender.value.deleteMessage(id));
|
||||
} else {
|
||||
console.warn("收到删除SC事件但无法确定要删除的消息ID", event, command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,15 +281,15 @@ type RichContentType = {
|
||||
/**
|
||||
* 获取富文本内容(处理表情等)
|
||||
*/
|
||||
async function getRichContent(data: DanmakuInfo): Promise<RichContentType[]> {
|
||||
async function getRichContent(data: EventModel): Promise<RichContentType[]> {
|
||||
const richContent: RichContentType[] = []
|
||||
|
||||
// 官方的非文本表情
|
||||
if (data.emoji_img_url) {
|
||||
if (data.emoji) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_IMAGE,
|
||||
text: data.msg,
|
||||
url: data.emoji_img_url + '@256w_256h_1e_1c',
|
||||
url: data.emoji + '@256w_256h_1e_1c',
|
||||
width: 256,
|
||||
height: 256
|
||||
})
|
||||
@@ -379,15 +409,15 @@ function getPronunciation(text: string): string {
|
||||
/**
|
||||
* 过滤SC消息
|
||||
*/
|
||||
function filterSuperChatMessage(data: SCInfo): boolean {
|
||||
return filterByContent(data.message) && filterByAuthorName(data.uname)
|
||||
function filterSuperChatMessage(data: EventModel): boolean {
|
||||
return filterByContent(data.msg) && filterByAuthorName(data.uname)
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤新舰长消息
|
||||
*/
|
||||
function filterNewMemberMessage(data: GuardInfo): boolean {
|
||||
return filterByAuthorName(data.user_info.uname)
|
||||
function filterNewMemberMessage(data: EventModel): boolean {
|
||||
return filterByAuthorName(data.uname)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,13 +437,13 @@ function filterByContent(content: string): boolean {
|
||||
* 根据用户名过滤消息
|
||||
*/
|
||||
function filterByAuthorName(id: string): boolean {
|
||||
return !(id in accountInfo.value.biliBlackList)
|
||||
return !(accountInfo.value && accountInfo.value.biliBlackList && id in accountInfo.value.biliBlackList)
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤弹幕消息
|
||||
*/
|
||||
function filterTextMessage(data: DanmakuInfo): boolean {
|
||||
function filterTextMessage(data: EventModel): boolean {
|
||||
// 舰长等级过滤
|
||||
if (config.value.blockLevel > 0 && data.guard_level < config.value.blockLevel) {
|
||||
return false
|
||||
@@ -445,26 +475,139 @@ function mergeSimilarGift(authorName: string, price: number, freePrice: number,
|
||||
return messageRender.value.mergeSimilarGift(authorName, price, freePrice, giftName, num)
|
||||
}
|
||||
|
||||
// --- 修改测试方法 ---
|
||||
/**
|
||||
* 接收配置更新
|
||||
* 用于测试,手动触发消息添加
|
||||
* @param rawEventData 测试用的 EventModel 部分数据和可选的 data 负载
|
||||
*/
|
||||
function onReceiveConfig(data: DanmujiConfig) {
|
||||
config.value = data
|
||||
async function testAddMessage(rawEventData: Partial<EventModel> & { type: EventDataTypes, data?: any }) {
|
||||
const event: EventModel = {
|
||||
type: rawEventData.type,
|
||||
uname: rawEventData.uname ?? '测试用户',
|
||||
uface: rawEventData.uface ?? '',
|
||||
uid: rawEventData.uid ?? 1000,
|
||||
open_id: rawEventData.open_id ?? 'test_open_id',
|
||||
msg: rawEventData.msg ?? '',
|
||||
time: rawEventData.time ?? Date.now() / 1000,
|
||||
num: rawEventData.num ?? 1,
|
||||
price: rawEventData.price ?? 0,
|
||||
guard_level: rawEventData.guard_level ?? 0,
|
||||
fans_medal_level: rawEventData.fans_medal_level ?? 0,
|
||||
fans_medal_name: rawEventData.fans_medal_name ?? '',
|
||||
fans_medal_wearing_status: rawEventData.fans_medal_wearing_status ?? false,
|
||||
emoji: rawEventData.emoji,
|
||||
ouid: rawEventData.ouid ?? '',
|
||||
...(rawEventData.data ? { data: rawEventData.data } : {})
|
||||
};
|
||||
|
||||
switch (event.type) {
|
||||
case EventDataTypes.Message:
|
||||
await onAddText(event, null);
|
||||
break;
|
||||
case EventDataTypes.Gift:
|
||||
onAddGift(event, null);
|
||||
break;
|
||||
case EventDataTypes.Guard:
|
||||
onAddMember(event, null);
|
||||
break;
|
||||
case EventDataTypes.SC:
|
||||
onAddSuperChat(event, null);
|
||||
break;
|
||||
case EventDataTypes.SCDel:
|
||||
onDelSuperChat(event, null);
|
||||
break;
|
||||
default:
|
||||
console.warn('Unsupported test event type:', event.type);
|
||||
}
|
||||
}
|
||||
// --- 结束修改测试方法 ---
|
||||
|
||||
/**
|
||||
* 添加系统通知消息
|
||||
*/
|
||||
function addSystemNotice(message: string) {
|
||||
if (!messageRender.value) return;
|
||||
|
||||
const systemMessage = {
|
||||
id: `system-${Date.now()}`,
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
avatarUrl: '',
|
||||
time: new Date(),
|
||||
authorName: '系统通知',
|
||||
authorType: 2, // 使用特殊类型标识系统消息
|
||||
content: message,
|
||||
richContent: [{
|
||||
type: constants.CONTENT_TYPE_TEXT,
|
||||
text: message
|
||||
}],
|
||||
privilegeType: 0,
|
||||
repeated: 1,
|
||||
translation: '',
|
||||
isSystem: true // 添加标记以便在UI中特殊处理
|
||||
}
|
||||
|
||||
messageRender.value.addMessage(systemMessage)
|
||||
}
|
||||
|
||||
let configHashCheckTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let currentConfigHash: string | null = null;
|
||||
|
||||
// 从服务器获取配置
|
||||
async function getConfigFromServer() {
|
||||
try {
|
||||
const result = await DownloadConfig<DanmujiConfig>('danmuji-config');
|
||||
if (result.status === 'success' && result.data) {
|
||||
Object.assign(config.value, result.data);
|
||||
console.log('已从服务器获取弹幕姬配置');
|
||||
addSystemNotice('配置已从服务器更新');
|
||||
return true;
|
||||
} else if (result.status === 'notfound') {
|
||||
console.log('服务器上未找到弹幕姬配置');
|
||||
} else {
|
||||
console.error(`获取配置失败: ${result.msg}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置文件出错:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查配置文件哈希值
|
||||
async function checkConfigHash() {
|
||||
if (!isOBS.value) return;
|
||||
|
||||
try {
|
||||
const hash = await GetConfigHash('danmuji-config');
|
||||
if (hash && hash !== currentConfigHash) {
|
||||
console.log('配置文件已更新,正在获取新配置...');
|
||||
currentConfigHash = hash;
|
||||
await getConfigFromServer();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查配置哈希值出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动定时检查配置
|
||||
function startConfigHashCheck() {
|
||||
if (!isOBS.value) return;
|
||||
|
||||
// 先获取一次当前哈希值
|
||||
GetConfigHash('danmuji-config').then(hash => {
|
||||
currentConfigHash = hash;
|
||||
});
|
||||
|
||||
// 设置定时检查,每5秒检查一次
|
||||
configHashCheckTimer = setInterval(checkConfigHash, 5000);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 注册事件监听
|
||||
client.on('danmaku', onAddText)
|
||||
client.on('gift', onAddGift)
|
||||
client.on('sc', onAddSuperChat)
|
||||
client.on('guard', onAddMember)
|
||||
client.onEvent('danmaku', onAddText)
|
||||
client.onEvent('gift', onAddGift)
|
||||
client.onEvent('sc', onAddSuperChat)
|
||||
client.onEvent('guard', onAddMember)
|
||||
client.onEvent('scDel', onDelSuperChat)
|
||||
|
||||
// 注册RTC配置接收
|
||||
if (rtc) {
|
||||
rtc.on('danmuji.config', onReceiveConfig)
|
||||
}
|
||||
|
||||
// 加载表情包
|
||||
try {
|
||||
const result = await QueryGetAPI<{ keyword: string, url: string }[]>(VTSURU_API_URL + 'blivechat/emoticon')
|
||||
if (result.code === 200) {
|
||||
@@ -473,18 +616,41 @@ onMounted(async () => {
|
||||
} catch (error) {
|
||||
console.error('加载表情包失败:', error)
|
||||
}
|
||||
|
||||
// 监听CSS变化
|
||||
watch(customCss, (newVal) => {
|
||||
messageRender.value?.setCss(newVal)
|
||||
})
|
||||
|
||||
// 显示弹幕姬加载完成的通知
|
||||
setTimeout(() => {
|
||||
addSystemNotice('加载完成');
|
||||
}, 300);
|
||||
|
||||
// 在OBS环境下,获取配置并启动配置检查
|
||||
// @ts-ignore
|
||||
if (window.obsstudio) {
|
||||
await getConfigFromServer();
|
||||
startConfigHashCheck();
|
||||
|
||||
messageRender.value?.setCss(defaultDanmujiCss)
|
||||
console.log('设置默认CSS')
|
||||
} else {
|
||||
messageRender.value?.setCss(customCss.value)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 取消事件监听
|
||||
client.off('danmaku', onAddText)
|
||||
client.off('gift', onAddGift)
|
||||
client.off('sc', onAddSuperChat)
|
||||
client.off('guard', onAddMember)
|
||||
client.offEvent('danmaku', onAddText)
|
||||
client.offEvent('gift', onAddGift)
|
||||
client.offEvent('sc', onAddSuperChat)
|
||||
client.offEvent('guard', onAddMember)
|
||||
client.offEvent('scDel', onDelSuperChat)
|
||||
|
||||
// 取消RTC配置接收
|
||||
if (rtc) {
|
||||
rtc.off('danmuji.config', onReceiveConfig)
|
||||
// 清除定时器
|
||||
if (configHashCheckTimer) {
|
||||
clearInterval(configHashCheckTimer);
|
||||
configHashCheckTimer = null;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -504,3 +670,10 @@ onUnmounted(() => {
|
||||
style="height: 100%; width: 100%"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.body {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -22,38 +22,34 @@
|
||||
</yt-live-chat-author-badge-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NTooltip } from 'naive-ui';
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { NTooltip } from 'naive-ui'
|
||||
import * as constants from './constants'
|
||||
import { FILE_BASE_URL } from '@/data/constants';
|
||||
import { FILE_BASE_URL } from '@/data/constants'
|
||||
|
||||
export default {
|
||||
name: 'AuthorBadge',
|
||||
props: {
|
||||
const props = defineProps({
|
||||
isAdmin: Boolean,
|
||||
privilegeType: Number
|
||||
},
|
||||
components: {
|
||||
NTooltip
|
||||
},
|
||||
computed: {
|
||||
authorTypeText() {
|
||||
if (this.isAdmin) {
|
||||
})
|
||||
|
||||
const authorTypeText = computed(() => {
|
||||
if (props.isAdmin) {
|
||||
return 'moderator'
|
||||
}
|
||||
return this.privilegeType > 0 ? 'member' : ''
|
||||
},
|
||||
readableAuthorTypeText() {
|
||||
if (this.isAdmin) {
|
||||
return props.privilegeType > 0 ? 'member' : ''
|
||||
})
|
||||
|
||||
const readableAuthorTypeText = computed(() => {
|
||||
if (props.isAdmin) {
|
||||
return '管理员'
|
||||
}
|
||||
return constants.getShowGuardLevelText(this.privilegeType)
|
||||
},
|
||||
fileServerUrl() {
|
||||
return constants.getShowGuardLevelText(props.privilegeType)
|
||||
})
|
||||
|
||||
const fileServerUrl = computed(() => {
|
||||
return FILE_BASE_URL
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style src="@/assets/css/youtube/yt-live-chat-author-badge-renderer.css"></style>
|
||||
|
||||
@@ -1,50 +1,63 @@
|
||||
<template>
|
||||
<yt-live-chat-author-chip>
|
||||
<span id="author-name" dir="auto" class="style-scope yt-live-chat-author-chip"
|
||||
:class="{ member: isInMemberMessage }" :type="authorTypeText">
|
||||
<span
|
||||
id="author-name"
|
||||
dir="auto"
|
||||
class="style-scope yt-live-chat-author-chip"
|
||||
:class="{ member: isInMemberMessage }"
|
||||
:type="authorTypeText"
|
||||
>
|
||||
{{ authorName }}
|
||||
<!-- 这里是已验证勋章 -->
|
||||
<span id="chip-badges" class="style-scope yt-live-chat-author-chip"></span>
|
||||
<span
|
||||
id="chip-badges"
|
||||
class="style-scope yt-live-chat-author-chip"
|
||||
/>
|
||||
</span>
|
||||
<span id="chat-badges" class="style-scope yt-live-chat-author-chip">
|
||||
<author-badge v-if="isInMemberMessage" class="style-scope yt-live-chat-author-chip" :isAdmin="false"
|
||||
:privilegeType="privilegeType"></author-badge>
|
||||
<span
|
||||
id="chat-badges"
|
||||
class="style-scope yt-live-chat-author-chip"
|
||||
>
|
||||
<author-badge
|
||||
v-if="isInMemberMessage"
|
||||
class="style-scope yt-live-chat-author-chip"
|
||||
:is-admin="false"
|
||||
:privilege-type="privilegeType"
|
||||
/>
|
||||
<template v-else>
|
||||
<author-badge v-if="authorType === AUTHOR_TYPE_ADMIN" class="style-scope yt-live-chat-author-chip" isAdmin
|
||||
:privilegeType="0"></author-badge>
|
||||
<author-badge v-if="privilegeType > 0" class="style-scope yt-live-chat-author-chip" :isAdmin="false"
|
||||
:privilegeType="privilegeType"></author-badge>
|
||||
<author-badge
|
||||
v-if="authorType === AUTHOR_TYPE_ADMIN"
|
||||
class="style-scope yt-live-chat-author-chip"
|
||||
is-admin
|
||||
:privilege-type="0"
|
||||
/>
|
||||
<author-badge
|
||||
v-if="privilegeType > 0"
|
||||
class="style-scope yt-live-chat-author-chip"
|
||||
:is-admin="false"
|
||||
:privilege-type="privilegeType"
|
||||
/>
|
||||
</template>
|
||||
</span>
|
||||
</yt-live-chat-author-chip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import AuthorBadge from './AuthorBadge.vue'
|
||||
import * as constants from './constants'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AuthorChip',
|
||||
components: {
|
||||
AuthorBadge
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
isInMemberMessage: Boolean,
|
||||
authorName: String,
|
||||
authorType: Number,
|
||||
privilegeType: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
AUTHOR_TYPE_ADMIN: constants.AUTHOR_TYPE_ADMIN
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
authorTypeText() {
|
||||
return constants.AUTHOR_TYPE_TO_TEXT[this.authorType]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const AUTHOR_TYPE_ADMIN = constants.AUTHOR_TYPE_ADMIN
|
||||
|
||||
const authorTypeText = computed(() => {
|
||||
return constants.AUTHOR_TYPE_TO_TEXT[props.authorType]
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,36 +1,43 @@
|
||||
<template>
|
||||
<yt-img-shadow class="no-transition" :height="height" :width="width" style="background-color: transparent;" loaded>
|
||||
<img id="img" class="style-scope yt-img-shadow" alt="" :height="height" :width="width" :src="showImgUrl"
|
||||
@error="onLoadError" referrerpolicy="no-referrer">
|
||||
<yt-img-shadow
|
||||
class="no-transition"
|
||||
:height="height"
|
||||
:width="width"
|
||||
style="background-color: transparent;"
|
||||
loaded
|
||||
>
|
||||
<img
|
||||
id="img"
|
||||
class="style-scope yt-img-shadow"
|
||||
alt=""
|
||||
:height="height"
|
||||
:width="width"
|
||||
:src="showImgUrl"
|
||||
referrerpolicy="no-referrer"
|
||||
@error="onLoadError"
|
||||
>
|
||||
</yt-img-shadow>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import * as models from '../../../data/chat/models'
|
||||
|
||||
export default {
|
||||
name: 'ImgShadow',
|
||||
props: {
|
||||
const props = defineProps({
|
||||
imgUrl: String,
|
||||
height: String,
|
||||
width: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showImgUrl: this.imgUrl
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imgUrl(val) {
|
||||
this.showImgUrl = val
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onLoadError() {
|
||||
if (this.showImgUrl !== models.DEFAULT_AVATAR_URL) {
|
||||
this.showImgUrl = models.DEFAULT_AVATAR_URL
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const showImgUrl = ref(props.imgUrl)
|
||||
|
||||
watch(() => props.imgUrl, (val) => {
|
||||
showImgUrl.value = val
|
||||
})
|
||||
|
||||
function onLoadError() {
|
||||
if (showImgUrl.value !== models.DEFAULT_AVATAR_URL) {
|
||||
showImgUrl.value = models.DEFAULT_AVATAR_URL
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,52 +1,80 @@
|
||||
<template>
|
||||
<yt-live-chat-membership-item-renderer class="style-scope yt-live-chat-item-list-renderer" show-only-header
|
||||
<yt-live-chat-membership-item-renderer
|
||||
class="style-scope yt-live-chat-item-list-renderer"
|
||||
show-only-header
|
||||
:blc-guard-level="privilegeType"
|
||||
>
|
||||
<div id="card" class="style-scope yt-live-chat-membership-item-renderer">
|
||||
<div id="header" class="style-scope yt-live-chat-membership-item-renderer">
|
||||
<img-shadow id="author-photo" height="40" width="40" class="style-scope yt-live-chat-membership-item-renderer"
|
||||
:imgUrl="avatarUrl"
|
||||
></img-shadow>
|
||||
<div id="header-content" class="style-scope yt-live-chat-membership-item-renderer">
|
||||
<div id="header-content-primary-column" class="style-scope yt-live-chat-membership-item-renderer">
|
||||
<div id="header-content-inner-column" class="style-scope yt-live-chat-membership-item-renderer">
|
||||
<author-chip class="style-scope yt-live-chat-membership-item-renderer"
|
||||
isInMemberMessage :authorName="authorName" :authorType="0" :privilegeType="privilegeType"
|
||||
></author-chip>
|
||||
<div
|
||||
id="card"
|
||||
class="style-scope yt-live-chat-membership-item-renderer"
|
||||
>
|
||||
<div
|
||||
id="header"
|
||||
class="style-scope yt-live-chat-membership-item-renderer"
|
||||
>
|
||||
<img-shadow
|
||||
id="author-photo"
|
||||
height="40"
|
||||
width="40"
|
||||
class="style-scope yt-live-chat-membership-item-renderer"
|
||||
:img-url="avatarUrl"
|
||||
/>
|
||||
<div
|
||||
id="header-content"
|
||||
class="style-scope yt-live-chat-membership-item-renderer"
|
||||
>
|
||||
<div
|
||||
id="header-content-primary-column"
|
||||
class="style-scope yt-live-chat-membership-item-renderer"
|
||||
>
|
||||
<div
|
||||
id="header-content-inner-column"
|
||||
class="style-scope yt-live-chat-membership-item-renderer"
|
||||
>
|
||||
<author-chip
|
||||
class="style-scope yt-live-chat-membership-item-renderer"
|
||||
is-in-member-message
|
||||
:author-name="authorName"
|
||||
:author-type="0"
|
||||
:privilege-type="privilegeType"
|
||||
/>
|
||||
</div>
|
||||
<div id="header-subtext" class="style-scope yt-live-chat-membership-item-renderer">{{ title }}</div>
|
||||
<div
|
||||
id="header-subtext"
|
||||
class="style-scope yt-live-chat-membership-item-renderer"
|
||||
>
|
||||
{{ title }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="timestamp"
|
||||
class="style-scope yt-live-chat-membership-item-renderer"
|
||||
>
|
||||
{{ timeText }}
|
||||
</div>
|
||||
<div id="timestamp" class="style-scope yt-live-chat-membership-item-renderer">{{ timeText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</yt-live-chat-membership-item-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
import AuthorChip from './AuthorChip.vue'
|
||||
import * as utils from './utils'
|
||||
|
||||
export default {
|
||||
name: 'MembershipItem',
|
||||
components: {
|
||||
ImgShadow,
|
||||
AuthorChip
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
avatarUrl: String,
|
||||
authorName: String,
|
||||
privilegeType: Number,
|
||||
title: String,
|
||||
time: Date
|
||||
},
|
||||
computed: {
|
||||
timeText() {
|
||||
return utils.getTimeTextHourMin(this.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const timeText = computed(() => {
|
||||
return utils.getTimeTextHourMin(props.time)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style src="@/assets/css/youtube/yt-live-chat-membership-item-renderer.css"></style>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<yt-live-chat-paid-message-renderer class="style-scope yt-live-chat-item-list-renderer" allow-animations
|
||||
:show-only-header="!content || undefined" :style="{
|
||||
<yt-live-chat-paid-message-renderer
|
||||
class="style-scope yt-live-chat-item-list-renderer"
|
||||
allow-animations
|
||||
:show-only-header="!content || undefined"
|
||||
:style="{
|
||||
'--yt-live-chat-paid-message-primary-color': color.contentBg,
|
||||
'--yt-live-chat-paid-message-secondary-color': color.headerBg,
|
||||
'--yt-live-chat-paid-message-header-color': color.header,
|
||||
@@ -10,59 +13,94 @@
|
||||
}"
|
||||
:blc-price-level="priceConfig.priceLevel"
|
||||
>
|
||||
<div id="card" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<div id="header" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<img-shadow id="author-photo" height="40" width="40" class="style-scope yt-live-chat-paid-message-renderer"
|
||||
:imgUrl="avatarUrl"
|
||||
></img-shadow>
|
||||
<div id="header-content" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<div id="header-content-primary-column" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<div id="author-name" class="style-scope yt-live-chat-paid-message-renderer">{{ authorName }}</div>
|
||||
<div id="purchase-amount" class="style-scope yt-live-chat-paid-message-renderer">{{ showPriceText }}</div>
|
||||
<div
|
||||
id="card"
|
||||
class="style-scope yt-live-chat-paid-message-renderer"
|
||||
>
|
||||
<div
|
||||
id="header"
|
||||
class="style-scope yt-live-chat-paid-message-renderer"
|
||||
>
|
||||
<img-shadow
|
||||
id="author-photo"
|
||||
height="40"
|
||||
width="40"
|
||||
class="style-scope yt-live-chat-paid-message-renderer"
|
||||
:img-url="avatarUrl"
|
||||
/>
|
||||
<div
|
||||
id="header-content"
|
||||
class="style-scope yt-live-chat-paid-message-renderer"
|
||||
>
|
||||
<div
|
||||
id="header-content-primary-column"
|
||||
class="style-scope yt-live-chat-paid-message-renderer"
|
||||
>
|
||||
<div
|
||||
id="author-name"
|
||||
class="style-scope yt-live-chat-paid-message-renderer"
|
||||
>
|
||||
{{ authorName }}
|
||||
</div>
|
||||
<span id="timestamp" class="style-scope yt-live-chat-paid-message-renderer">{{ timeText }}</span>
|
||||
<div
|
||||
id="purchase-amount"
|
||||
class="style-scope yt-live-chat-paid-message-renderer"
|
||||
>
|
||||
{{ showPriceText }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="content" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<div id="message" dir="auto" class="style-scope yt-live-chat-paid-message-renderer">{{ content }}</div>
|
||||
<span
|
||||
id="timestamp"
|
||||
class="style-scope yt-live-chat-paid-message-renderer"
|
||||
>{{ timeText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="content"
|
||||
class="style-scope yt-live-chat-paid-message-renderer"
|
||||
>
|
||||
<div
|
||||
id="message"
|
||||
dir="auto"
|
||||
class="style-scope yt-live-chat-paid-message-renderer"
|
||||
>
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</yt-live-chat-paid-message-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
import * as constants from './constants'
|
||||
import * as utils from './utils'
|
||||
|
||||
export default {
|
||||
name: 'PaidMessage',
|
||||
components: {
|
||||
ImgShadow
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
avatarUrl: String,
|
||||
authorName: String,
|
||||
price: Number, // 价格,人民币
|
||||
priceText: String,
|
||||
time: Date,
|
||||
content: String
|
||||
},
|
||||
computed: {
|
||||
priceConfig() {
|
||||
return constants.getPriceConfig(this.price)
|
||||
},
|
||||
color() {
|
||||
return this.priceConfig.colors
|
||||
},
|
||||
showPriceText() {
|
||||
return this.priceText || `CN¥${utils.formatCurrency(this.price)}`
|
||||
},
|
||||
timeText() {
|
||||
return utils.getTimeTextHourMin(this.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const priceConfig = computed(() => {
|
||||
return constants.getPriceConfig(props.price)
|
||||
})
|
||||
|
||||
const color = computed(() => {
|
||||
return priceConfig.value.colors
|
||||
})
|
||||
|
||||
const showPriceText = computed(() => {
|
||||
return props.priceText || `CN¥${utils.formatCurrency(props.price)}`
|
||||
})
|
||||
|
||||
const timeText = computed(() => {
|
||||
return utils.getTimeTextHourMin(props.time)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style src="@/assets/css/youtube/yt-live-chat-paid-message-renderer.css"></style>
|
||||
|
||||
@@ -1,34 +1,68 @@
|
||||
<template>
|
||||
<yt-live-chat-text-message-renderer :author-type="authorTypeText" :blc-guard-level="privilegeType">
|
||||
<img-shadow id="author-photo" height="24" width="24" class="style-scope yt-live-chat-text-message-renderer"
|
||||
:imgUrl="avatarUrl"
|
||||
></img-shadow>
|
||||
<div id="content" class="style-scope yt-live-chat-text-message-renderer">
|
||||
<span id="timestamp" class="style-scope yt-live-chat-text-message-renderer">{{ timeText }}</span>
|
||||
<author-chip class="style-scope yt-live-chat-text-message-renderer"
|
||||
:isInMemberMessage="false" :authorName="authorName" :authorType="authorType" :privilegeType="privilegeType"
|
||||
></author-chip>
|
||||
<span id="message" class="style-scope yt-live-chat-text-message-renderer">
|
||||
<yt-live-chat-text-message-renderer
|
||||
:author-type="authorTypeText"
|
||||
:blc-guard-level="privilegeType"
|
||||
>
|
||||
<img-shadow
|
||||
id="author-photo"
|
||||
height="24"
|
||||
width="24"
|
||||
class="style-scope yt-live-chat-text-message-renderer"
|
||||
:img-url="avatarUrl"
|
||||
/>
|
||||
<div
|
||||
id="content"
|
||||
class="style-scope yt-live-chat-text-message-renderer"
|
||||
>
|
||||
<span
|
||||
id="timestamp"
|
||||
class="style-scope yt-live-chat-text-message-renderer"
|
||||
>{{ timeText }}</span>
|
||||
<author-chip
|
||||
class="style-scope yt-live-chat-text-message-renderer"
|
||||
:is-in-member-message="false"
|
||||
:author-name="authorName"
|
||||
:author-type="authorType"
|
||||
:privilege-type="privilegeType"
|
||||
/>
|
||||
<span
|
||||
id="message"
|
||||
class="style-scope yt-live-chat-text-message-renderer"
|
||||
>
|
||||
<template v-for="(content, index) in richContent">
|
||||
<span :key="index" v-if="content.type === CONTENT_TYPE_TEXT">{{ content.text }}</span>
|
||||
<span
|
||||
v-if="content.type === CONTENT_TYPE_TEXT"
|
||||
:key="index"
|
||||
>{{ content.text }}</span>
|
||||
<!-- 如果CSS设置的尺寸比属性设置的尺寸还大,在图片加载完后布局会变化,可能导致滚动卡住,没什么好的解决方法 -->
|
||||
<img :key="'_' + index" v-else-if="content.type === CONTENT_TYPE_IMAGE"
|
||||
<img
|
||||
v-else-if="content.type === CONTENT_TYPE_IMAGE"
|
||||
:id="`emoji-${content.text}`"
|
||||
:key="'_' + index"
|
||||
class="emoji yt-formatted-string style-scope yt-live-chat-text-message-renderer"
|
||||
:src="content.url" :alt="content.text" :shared-tooltip-text="content.text" :id="`emoji-${content.text}`"
|
||||
:width="content.width" :height="content.height"
|
||||
:src="content.url"
|
||||
:alt="content.text"
|
||||
:shared-tooltip-text="content.text"
|
||||
:width="content.width"
|
||||
:height="content.height"
|
||||
:class="{ 'blc-large-emoji': content.height >= 100 }"
|
||||
referrerpolicy="no-referrer"
|
||||
>
|
||||
</template>
|
||||
<NBadge :value="repeated" :max="99" v-if="repeated > 1" class="style-scope yt-live-chat-text-message-renderer"
|
||||
<NBadge
|
||||
v-if="repeated > 1"
|
||||
:value="repeated"
|
||||
:max="99"
|
||||
class="style-scope yt-live-chat-text-message-renderer"
|
||||
:style="{ '--repeated-mark-color': repeatedMarkColor }"
|
||||
></NBadge>
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</yt-live-chat-text-message-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
import AuthorChip from './AuthorChip.vue'
|
||||
import * as constants from './constants'
|
||||
@@ -39,14 +73,10 @@ import { NBadge } from 'naive-ui'
|
||||
const REPEATED_MARK_COLOR_START = [210, 100.0, 62.5]
|
||||
const REPEATED_MARK_COLOR_END = [360, 87.3, 69.2]
|
||||
|
||||
export default {
|
||||
name: 'TextMessage',
|
||||
components: {
|
||||
ImgShadow,
|
||||
AuthorChip,
|
||||
NBadge
|
||||
},
|
||||
props: {
|
||||
const CONTENT_TYPE_TEXT = constants.CONTENT_TYPE_TEXT
|
||||
const CONTENT_TYPE_IMAGE = constants.CONTENT_TYPE_IMAGE
|
||||
|
||||
const props = defineProps({
|
||||
avatarUrl: String,
|
||||
time: Date,
|
||||
authorName: String,
|
||||
@@ -54,37 +84,31 @@ export default {
|
||||
richContent: Array,
|
||||
privilegeType: Number,
|
||||
repeated: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
CONTENT_TYPE_TEXT: constants.CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_IMAGE: constants.CONTENT_TYPE_IMAGE
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
timeText() {
|
||||
return utils.getTimeTextHourMin(this.time)
|
||||
},
|
||||
authorTypeText() {
|
||||
return constants.AUTHOR_TYPE_TO_TEXT[this.authorType]
|
||||
},
|
||||
repeatedMarkColor() {
|
||||
})
|
||||
|
||||
const timeText = computed(() => {
|
||||
return utils.getTimeTextHourMin(props.time)
|
||||
})
|
||||
|
||||
const authorTypeText = computed(() => {
|
||||
return constants.AUTHOR_TYPE_TO_TEXT[props.authorType]
|
||||
})
|
||||
|
||||
const repeatedMarkColor = computed(() => {
|
||||
let color
|
||||
if (this.repeated <= 2) {
|
||||
if (props.repeated <= 2) {
|
||||
color = REPEATED_MARK_COLOR_START
|
||||
} else if (this.repeated >= 10) {
|
||||
} else if (props.repeated >= 10) {
|
||||
color = REPEATED_MARK_COLOR_END
|
||||
} else {
|
||||
color = [0, 0, 0]
|
||||
let t = (this.repeated - 2) / (10 - 2)
|
||||
let t = (props.repeated - 2) / (10 - 2)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
color[i] = REPEATED_MARK_COLOR_START[i] + ((REPEATED_MARK_COLOR_END[i] - REPEATED_MARK_COLOR_START[i]) * t)
|
||||
}
|
||||
}
|
||||
return `hsl(${color[0]}, ${color[1]}%, ${color[2]}%)`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,102 +1,141 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<template>
|
||||
<yt-live-chat-ticker-renderer :hidden="showMessages.length === 0">
|
||||
<div id="container" dir="ltr" class="style-scope yt-live-chat-ticker-renderer">
|
||||
<transition-group tag="div" :css="false" @enter="onTickerItemEnter" @leave="onTickerItemLeave" id="items"
|
||||
class="style-scope yt-live-chat-ticker-renderer">
|
||||
<yt-live-chat-ticker-paid-message-item-renderer v-for="message in showMessages" :key="message.raw.id"
|
||||
tabindex="0" class="style-scope yt-live-chat-ticker-renderer" style="overflow: hidden;"
|
||||
@click="onItemClick(message.raw)">
|
||||
<div id="container" dir="ltr" class="style-scope yt-live-chat-ticker-paid-message-item-renderer" :style="{
|
||||
background: message.bgColor,
|
||||
}">
|
||||
<div id="content" class="style-scope yt-live-chat-ticker-paid-message-item-renderer" :style="{
|
||||
color: message.color
|
||||
}">
|
||||
<img-shadow id="author-photo" height="24" width="24"
|
||||
<div
|
||||
id="container"
|
||||
dir="ltr"
|
||||
class="style-scope yt-live-chat-ticker-renderer"
|
||||
>
|
||||
<transition-group
|
||||
id="items"
|
||||
tag="div"
|
||||
:css="false"
|
||||
class="style-scope yt-live-chat-ticker-renderer"
|
||||
@enter="onTickerItemEnter"
|
||||
@leave="onTickerItemLeave"
|
||||
>
|
||||
<yt-live-chat-ticker-paid-message-item-renderer
|
||||
v-for="message in showMessages"
|
||||
:key="message.raw.id"
|
||||
tabindex="0"
|
||||
class="style-scope yt-live-chat-ticker-renderer"
|
||||
style="overflow: hidden;"
|
||||
@click="onItemClick(message.raw)"
|
||||
>
|
||||
<div
|
||||
id="container"
|
||||
dir="ltr"
|
||||
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
|
||||
:imgUrl="message.raw.avatarUrl"></img-shadow>
|
||||
<span id="text" dir="ltr"
|
||||
class="style-scope yt-live-chat-ticker-paid-message-item-renderer">{{ message.text }}</span>
|
||||
:style="{
|
||||
background: message.bgColor,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
id="content"
|
||||
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
|
||||
:style="{
|
||||
color: message.color
|
||||
}"
|
||||
>
|
||||
<img-shadow
|
||||
id="author-photo"
|
||||
height="24"
|
||||
width="24"
|
||||
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
|
||||
:img-url="message.raw.avatarUrl"
|
||||
/>
|
||||
<span
|
||||
id="text"
|
||||
dir="ltr"
|
||||
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
|
||||
>{{ message.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</yt-live-chat-ticker-paid-message-item-renderer>
|
||||
</transition-group>
|
||||
</div>
|
||||
<template v-if="pinnedMessage">
|
||||
<membership-item :key="pinnedMessage.id" v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
|
||||
class="style-scope yt-live-chat-ticker-renderer" :avatarUrl="pinnedMessage.avatarUrl"
|
||||
:authorName="getShowAuthorName(pinnedMessage)" :privilegeType="pinnedMessage.privilegeType"
|
||||
:title="pinnedMessage.title" :time="pinnedMessage.time"></membership-item>
|
||||
<paid-message :key="pinnedMessage.id" v-else class="style-scope yt-live-chat-ticker-renderer"
|
||||
:price="pinnedMessage.price" :avatarUrl="pinnedMessage.avatarUrl" :authorName="getShowAuthorName(pinnedMessage)"
|
||||
:time="pinnedMessage.time" :content="pinnedMessageShowContent"></paid-message>
|
||||
<membership-item
|
||||
v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
|
||||
:key="pinnedMessage.id"
|
||||
class="style-scope yt-live-chat-ticker-renderer"
|
||||
:avatar-url="pinnedMessage.avatarUrl"
|
||||
:author-name="getShowAuthorName(pinnedMessage)"
|
||||
:privilege-type="pinnedMessage.privilegeType"
|
||||
:title="pinnedMessage.title"
|
||||
:time="pinnedMessage.time"
|
||||
/>
|
||||
<paid-message
|
||||
v-else
|
||||
:key="pinnedMessage.id"
|
||||
class="style-scope yt-live-chat-ticker-renderer"
|
||||
:price="pinnedMessage.price"
|
||||
:avatar-url="pinnedMessage.avatarUrl"
|
||||
:author-name="getShowAuthorName(pinnedMessage)"
|
||||
:time="pinnedMessage.time"
|
||||
:content="pinnedMessageShowContent"
|
||||
/>
|
||||
</template>
|
||||
</yt-live-chat-ticker-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
// @ts-nocheck
|
||||
import { ref, computed, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { formatCurrency } from './utils'
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
import MembershipItem from './MembershipItem.vue'
|
||||
import PaidMessage from './PaidMessage.vue'
|
||||
import * as constants from './constants'
|
||||
|
||||
export default {
|
||||
name: 'Ticker',
|
||||
components: {
|
||||
ImgShadow,
|
||||
MembershipItem,
|
||||
PaidMessage
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
messages: Array,
|
||||
showGiftName: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
MESSAGE_TYPE_MEMBER: constants.MESSAGE_TYPE_MEMBER,
|
||||
})
|
||||
|
||||
curTime: new Date(),
|
||||
updateTimerId: window.setInterval(this.updateProgress, 1000),
|
||||
pinnedMessage: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showMessages() {
|
||||
const emit = defineEmits(['update:messages'])
|
||||
|
||||
const MESSAGE_TYPE_MEMBER = constants.MESSAGE_TYPE_MEMBER
|
||||
const curTime = ref(new Date())
|
||||
const pinnedMessage = ref(null)
|
||||
|
||||
// 定时更新进度
|
||||
const updateTimerId = window.setInterval(updateProgress, 1000)
|
||||
onBeforeUnmount(() => {
|
||||
window.clearInterval(updateTimerId)
|
||||
})
|
||||
|
||||
const showMessages = computed(() => {
|
||||
let res = []
|
||||
for (let message of this.messages) {
|
||||
if (!this.needToShow(message)) {
|
||||
for (let message of props.messages) {
|
||||
if (!needToShow(message)) {
|
||||
continue
|
||||
}
|
||||
res.push({
|
||||
raw: message,
|
||||
bgColor: this.getBgColor(message),
|
||||
color: this.getColor(message),
|
||||
text: this.getText(message)
|
||||
bgColor: getBgColor(message),
|
||||
color: getColor(message),
|
||||
text: getText(message)
|
||||
})
|
||||
}
|
||||
return res
|
||||
},
|
||||
pinnedMessageShowContent() {
|
||||
if (!this.pinnedMessage) {
|
||||
})
|
||||
|
||||
const pinnedMessageShowContent = computed(() => {
|
||||
if (!pinnedMessage.value) {
|
||||
return ''
|
||||
}
|
||||
if (this.pinnedMessage.type === constants.MESSAGE_TYPE_GIFT) {
|
||||
return constants.getGiftShowContent(this.pinnedMessage, this.showGiftName)
|
||||
if (pinnedMessage.value.type === constants.MESSAGE_TYPE_GIFT) {
|
||||
return constants.getGiftShowContent(pinnedMessage.value, props.showGiftName)
|
||||
} else {
|
||||
return constants.getShowContent(this.pinnedMessage)
|
||||
return constants.getShowContent(pinnedMessage.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.clearInterval(this.updateTimerId)
|
||||
},
|
||||
methods: {
|
||||
async onTickerItemEnter(el, done) {
|
||||
})
|
||||
|
||||
async function onTickerItemEnter(el, done) {
|
||||
let width = el.clientWidth
|
||||
if (width === 0) {
|
||||
// CSS指定了不显示固定栏
|
||||
@@ -104,11 +143,12 @@ export default {
|
||||
return
|
||||
}
|
||||
el.style.width = 0
|
||||
await this.$nextTick()
|
||||
await nextTick()
|
||||
el.style.width = `${width}px`
|
||||
window.setTimeout(done, 200)
|
||||
},
|
||||
onTickerItemLeave(el, done) {
|
||||
}
|
||||
|
||||
function onTickerItemLeave(el, done) {
|
||||
el.classList.add('sliding-down')
|
||||
window.setTimeout(() => {
|
||||
el.classList.add('collapsing')
|
||||
@@ -120,14 +160,16 @@ export default {
|
||||
done()
|
||||
}, 200)
|
||||
}, 200)
|
||||
},
|
||||
}
|
||||
|
||||
getShowAuthorName: constants.getShowAuthorName,
|
||||
needToShow(message) {
|
||||
let pinTime = this.getPinTime(message)
|
||||
const getShowAuthorName = constants.getShowAuthorName
|
||||
|
||||
function needToShow(message) {
|
||||
let pinTime = getPinTime(message)
|
||||
return (new Date() - message.addTime) / (60 * 1000) < pinTime
|
||||
},
|
||||
getBgColor(message) {
|
||||
}
|
||||
|
||||
function getBgColor(message) {
|
||||
let color1, color2
|
||||
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
|
||||
color1 = 'rgba(15,157,88,1)'
|
||||
@@ -137,62 +179,65 @@ export default {
|
||||
color1 = config.colors.contentBg
|
||||
color2 = config.colors.headerBg
|
||||
}
|
||||
let pinTime = this.getPinTime(message)
|
||||
let progress = (1 - ((this.curTime - message.addTime) / (60 * 1000) / pinTime)) * 100
|
||||
let pinTime = getPinTime(message)
|
||||
let progress = (1 - ((curTime.value - message.addTime) / (60 * 1000) / pinTime)) * 100
|
||||
if (progress < 0) {
|
||||
progress = 0
|
||||
} else if (progress > 100) {
|
||||
progress = 100
|
||||
}
|
||||
return `linear-gradient(90deg, ${color1}, ${color1} ${progress}%, ${color2} ${progress}%, ${color2})`
|
||||
},
|
||||
getColor(message) {
|
||||
}
|
||||
|
||||
function getColor(message) {
|
||||
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
|
||||
return 'rgb(255,255,255)'
|
||||
}
|
||||
return constants.getPriceConfig(message.price).colors.header
|
||||
},
|
||||
getText(message) {
|
||||
}
|
||||
|
||||
function getText(message) {
|
||||
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
|
||||
return this.$t('chat.tickerMembership')
|
||||
return '舰长'
|
||||
}
|
||||
return `CN¥${formatCurrency(message.price)}`
|
||||
},
|
||||
getPinTime(message) {
|
||||
}
|
||||
|
||||
function getPinTime(message) {
|
||||
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
|
||||
return 2
|
||||
}
|
||||
return constants.getPriceConfig(message.price).pinTime
|
||||
},
|
||||
updateProgress() {
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
// 更新进度
|
||||
this.curTime = new Date()
|
||||
curTime.value = new Date()
|
||||
|
||||
// 删除过期的消息
|
||||
let filteredMessages = []
|
||||
let messagesChanged = false
|
||||
for (let message of this.messages) {
|
||||
let pinTime = this.getPinTime(message)
|
||||
if ((this.curTime - message.addTime) / (60 * 1000) >= pinTime) {
|
||||
for (let message of props.messages) {
|
||||
let pinTime = getPinTime(message)
|
||||
if ((curTime.value - message.addTime) / (60 * 1000) >= pinTime) {
|
||||
messagesChanged = true
|
||||
if (this.pinnedMessage === message) {
|
||||
this.pinnedMessage = null
|
||||
if (pinnedMessage.value === message) {
|
||||
pinnedMessage.value = null
|
||||
}
|
||||
continue
|
||||
}
|
||||
filteredMessages.push(message)
|
||||
}
|
||||
if (messagesChanged) {
|
||||
this.$emit('update:messages', filteredMessages)
|
||||
emit('update:messages', filteredMessages)
|
||||
}
|
||||
},
|
||||
onItemClick(message) {
|
||||
if (this.pinnedMessage == message) {
|
||||
this.pinnedMessage = null
|
||||
}
|
||||
|
||||
function onItemClick(message) {
|
||||
if (pinnedMessage.value == message) {
|
||||
pinnedMessage.value = null
|
||||
} else {
|
||||
this.pinnedMessage = message
|
||||
}
|
||||
}
|
||||
pinnedMessage.value = message
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export const AUTHOR_TYPE_NORMAL = 0
|
||||
export const AUTHOR_TYPE_MEMBER = 1
|
||||
export const AUTHOR_TYPE_ADMIN = 2
|
||||
@@ -181,6 +180,17 @@ export function getShowRichContent(message) {
|
||||
return richContent
|
||||
}
|
||||
|
||||
export function getShowContentParts(message) {
|
||||
let contentParts = [...message.contentParts || []]
|
||||
if (message.translation) {
|
||||
contentParts.push({
|
||||
type: CONTENT_TYPE_TEXT,
|
||||
text: `(${message.translation})`
|
||||
})
|
||||
}
|
||||
return contentParts
|
||||
}
|
||||
|
||||
export function getGiftShowContent(message, showGiftName) {
|
||||
if (!showGiftName) {
|
||||
return ''
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
|
||||
import { format } from 'date-fns'
|
||||
|
||||
export function mergeConfig(config, defaultConfig) {
|
||||
let res = {}
|
||||
for (let i in defaultConfig) {
|
||||
@@ -36,9 +39,7 @@ export function formatCurrency(price) {
|
||||
}
|
||||
|
||||
export function getTimeTextHourMin(date) {
|
||||
let hour = date.getHours()
|
||||
let min = `00${date.getMinutes()}`.slice(-2)
|
||||
return `${hour}:${min}`
|
||||
return format(date, 'H:mm')
|
||||
}
|
||||
|
||||
export function getUuid4Hex() {
|
||||
|
||||
@@ -129,10 +129,44 @@ const selectedItems = computed(() => {
|
||||
// --- 方法 ---
|
||||
|
||||
// 获取礼物兑换按钮的提示文本
|
||||
function getTooltip(goods: ResponsePointGoodModel): '开始兑换' | '当前积分不足' | '请先进行账号认证' | '库存不足' {
|
||||
function getTooltip(goods: ResponsePointGoodModel): '开始兑换' | '当前积分不足' | '请先进行账号认证' | '库存不足' | '舰长等级不足' | '兑换时间未到' | '已达兑换上限' | '需要设置地址' {
|
||||
if (!biliAuth.value.id) return '请先进行账号认证' // 未认证
|
||||
if ((goods?.count ?? Number.MAX_VALUE) <= 0) return '库存不足' // 库存不足
|
||||
if ((currentPoint.value ?? 0) < goods.price) return '当前积分不足' // 积分不足
|
||||
if ((currentPoint.value ?? 0) < goods.price && !goods.canFreeBuy) return '当前积分不足' // 积分不足且不能免费兑换
|
||||
// 检查舰长等级要求
|
||||
// 使用 guardInfo 判断用户在当前主播房间的舰长等级
|
||||
const currentGuardLevel = biliAuth.value.guardInfo?.[props.userInfo.id] ?? 0
|
||||
if (goods.allowGuardLevel > 0 && currentGuardLevel < goods.allowGuardLevel) {
|
||||
return '舰长等级不足'
|
||||
}
|
||||
|
||||
// 在当前模型中没有兑换时间限制字段,可以根据需要添加相关功能
|
||||
// 如果将来添加了时间限制功能,可以取消下面注释并调整代码
|
||||
/*
|
||||
if (goods.startTime && new Date() < new Date(goods.startTime)) {
|
||||
return '兑换时间未到'
|
||||
}
|
||||
if (goods.endTime && new Date() > new Date(goods.endTime)) {
|
||||
return '兑换已结束'
|
||||
}
|
||||
*/
|
||||
|
||||
// 检查用户兑换上限
|
||||
// 注意:当前模型中没有 userBoughtCount 属性,
|
||||
// 需要后端提供已购买数量信息才能实现此功能
|
||||
/*
|
||||
if (goods.userBoughtCount !== undefined && goods.maxBuyCount !== undefined &&
|
||||
goods.userBoughtCount >= goods.maxBuyCount && goods.maxBuyCount > 0) {
|
||||
return '已达兑换上限'
|
||||
}
|
||||
*/
|
||||
|
||||
// 检查实物礼物的地址要求
|
||||
if (goods.type === GoodsTypes.Physical && !goods.collectUrl &&
|
||||
(!biliAuth.value.address || biliAuth.value.address.length === 0)) {
|
||||
return '需要设置地址'
|
||||
}
|
||||
|
||||
return '开始兑换' // 可以兑换
|
||||
}
|
||||
|
||||
@@ -420,7 +454,7 @@ onMounted(async () => {
|
||||
<NSelect
|
||||
v-model:value="priceOrder"
|
||||
:options="[
|
||||
{ label: '默认排序', value: null },
|
||||
{ label: '默认排序', value: 'null' },
|
||||
{ label: '价格 低→高', value: 'asc' },
|
||||
{ label: '价格 高→低', value: 'desc' }
|
||||
]"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function TestVIneComponent() {
|
||||
function TestVineComponent() {
|
||||
return vine`
|
||||
<div>
|
||||
<h1>Test Vine</h1>
|
||||
@@ -6,7 +6,8 @@ function TestVIneComponent() {
|
||||
<p>Vine is a new way to build web applications.</p>
|
||||
<p>Enjoy building with Vine!</p>
|
||||
<footer>Footer content goes here.</footer>
|
||||
</div>`
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
export default TestVIneComponent
|
||||
export default TestVineComponent
|
||||
@@ -12,6 +12,7 @@ import Components from 'unplugin-vue-components/vite';
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||
import oxlintPlugin from 'vite-plugin-oxlint';
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
import { install as VueMonacoEditorPlugin } from '@guolao/vue-monaco-editor'
|
||||
|
||||
const isObjectWithDefaultFunction = (
|
||||
module: unknown
|
||||
@@ -21,10 +22,6 @@ const isObjectWithDefaultFunction = (
|
||||
'default' in module &&
|
||||
typeof module.default === 'function';
|
||||
|
||||
const monacoEditorPlugin = isObjectWithDefaultFunction(monacoEditorPluginModule)
|
||||
? monacoEditorPluginModule.default
|
||||
: monacoEditorPluginModule;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
@@ -58,11 +55,10 @@ export default defineConfig({
|
||||
extensions: ['vue', 'md'],
|
||||
|
||||
// allow auto import and register components used in markdown
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.vine$/],
|
||||
}),
|
||||
monacoEditorPlugin({ languageWorkers: ['css'] }),
|
||||
oxlintPlugin(),
|
||||
VineVitePlugin()
|
||||
VineVitePlugin(),
|
||||
],
|
||||
server: { port: 51000 },
|
||||
resolve: { alias: { '@': path.resolve(__dirname, 'src') } },
|
||||
|
||||
Reference in New Issue
Block a user