chore: remove unused steering docs and update point settings model

This commit is contained in:
2025-10-16 00:52:05 +08:00
parent 26273a4afc
commit 55d3b31146
58 changed files with 2491 additions and 3480 deletions

View File

@@ -1,36 +0,0 @@
---
inclusion: manual
---
# API集成
该项目使用多个API接口与后端服务和直播平台进行交互。
## 主要API模块
- [src/api/api-models.ts](mdc:src/api/api-models.ts): 定义了系统中使用的数据模型
- [src/api/query.ts](mdc:src/api/query.ts): 提供了API请求的基础函数
- [src/api/account.ts](mdc:src/api/account.ts): 账户管理相关API
## 数据模型
- `SongRequestInfo`: 点歌请求信息
- `DanmakuUserInfo`: 弹幕用户信息
- `EventModel`: 事件数据模型用于处理弹幕、SC等事件
- `Setting_LiveRequest`: 点歌系统设置
## API请求类型
- `QueryGetAPI`: GET请求
- `QueryPostAPI`: POST请求
- `QueryPostAPIWithParams`: 带参数的POST请求
## 直播平台集成
系统集成了直播平台如B站的API通过`useDanmakuClient()`获取直播间的弹幕、SC等数据。主要事件类型
- `danmaku`: 弹幕事件
- `sc`: SuperChat事件
## 数据存储
系统使用`useStorage`进行本地数据存储,`useAccount`获取账户信息。远程数据通过API请求获取和更新。

View File

@@ -1,35 +0,0 @@
---
inclusion: always
---
# 开发工作流
## 项目配置
- TypeScript: 项目使用TypeScript进行类型检查
- Vite: 使用Vite作为构建工具
- ESLint: 代码质量检查
- Prettier: 代码格式化
## 主要配置文件
- [package.json](mdc:package.json): 项目依赖和脚本
- [tsconfig.json](mdc:tsconfig.json): TypeScript配置
- [vite.config.mts](mdc:vite.config.mts): Vite构建配置
- [.prettierrc.json](mdc:.prettierrc.json): Prettier格式化配置
- [eslint.config.mjs](mdc:eslint.config.mjs): ESLint配置
## 开发环境
项目运行在Windows环境中使用PowerShell作为默认shell。
## 代码风格
- 使用中文作为用户界面和日志语言
- 注释应尽量简短,必要时使用中文
- 遵循Vue 3组合式API的最佳实践
## 部署流程
项目包含Docker配置可以使用Docker进行部署
- [Dockerfile](mdc:Dockerfile): Docker构建配置

View File

@@ -1,28 +0,0 @@
---
inclusion: manual
---
# 点歌系统
点歌系统是主要功能之一允许观众在直播过程中通过弹幕、SuperChat或网页界面请求歌曲。
## 主要文件
- [src/views/open_live/LiveRequest.vue](mdc:src/views/open_live/LiveRequest.vue): 点歌系统的主要界面组件
- [src/views/obs/LiveRequestOBS.vue](mdc:src/views/obs/LiveRequestOBS.vue): 用于OBS的点歌系统显示组件
## 主要功能
- 支持多种点歌方式弹幕、SuperChat、网页、手动添加
- 歌曲队列管理:等待、演唱中、已完成、已取消等状态管理
- 权限控制:可配置只允许舰长、提督、总督或粉丝牌用户点歌
- 冷却时间:可设置不同用户类型的点歌冷却时间
- OBS集成提供适用于OBS的显示组件可展示当前点歌队列
- 黑名单:可将特定用户加入黑名单
## 数据流
1. 接收来自直播平台的弹幕或SuperChat
2. 通过前缀识别点歌请求(如"点播"
3. 根据规则验证请求是否有效
4. 将有效请求添加到点歌队列
5. 主播可以管理队列:开始演唱、标记完成、取消请求等

View File

@@ -1,3 +0,0 @@
---
inclusion: always
---

View File

@@ -1,29 +0,0 @@
---
inclusion: manual
---
# 项目结构
该项目是一个基于Vue的直播辅助工具主要用于管理直播相关功能如点歌系统、弹幕互动等。
## 主要目录结构
- `src/`: 源代码目录
- `api/`: API调用和模型定义
- `assets/`: 静态资源文件
- `client/`: 客户端相关组件和服务
- `components/`: Vue组件
- `composables/`: Vue组合式API函数
- `data/`: 数据相关模块,包括聊天和弹幕客户端
- `router/`: 路由配置
- `store/`: 状态管理
- `views/`: 页面视图组件
- `open_live/`: 直播相关视图,包括点歌系统
- `obs/`: OBS相关视图组件
- `public/`: 公共静态资源
- `plugins/`: 插件目录
## 主要功能模块
- 点歌系统允许观众通过弹幕或SuperChat点歌
- 直播互动:弹幕互动和自动化操作
- OBS集成为OBS提供overlays和组件

View File

@@ -1,50 +0,0 @@
---
inclusion: fileMatch
fileMatchPattern: ['*.vue']
---
# UI组件
项目使用Vue 3和Naive UI作为主要UI框架采用组件化设计。
## 主要UI框架
- Vue 3: 使用`<script setup>`语法和组合式API
- Naive UI: 提供各种预设UI组件
- VueUse: 提供实用的组合式函数,如`useStorage`
## 常用组件
### Naive UI组件
项目广泛使用Naive UI组件
- `NCard`: 卡片容器
- `NSpace`: 间距布局
- `NButton`: 按钮
- `NInput`: 输入框
- `NTabs`: 标签页
- `NDataTable`: 数据表格
- `NModal`: 模态框
- `NAlert`: 警告提示
- `NTag`: 标签
- `NIcon`: 图标容器
### 自定义组件
- [SongPlayer.vue](mdc:src/components/SongPlayer.vue): 歌曲播放器组件
- [LiveRequestOBS.vue](mdc:src/views/obs/LiveRequestOBS.vue): OBS点歌显示组件
## 状态管理
项目使用组合式API和本地存储管理状态
- `ref`/`computed`: 响应式状态
- `useStorage`: 持久化存储
- `useAccount`: 账户状态管理
## UI设计模式
- 使用`NFlex``NCard`进行布局
- 通过`NTabs`组织不同功能区域
- 使用状态颜色区分不同状态(如等待中、处理中、已完成)
- 响应式设计适应不同屏幕尺寸

BIN
bun.lockb

Binary file not shown.

View File

@@ -29,7 +29,6 @@
"@tauri-apps/plugin-updater": "^2.9.0",
"@types/crypto-js": "^4.2.2",
"@types/md5": "^2.3.5",
"@types/vue-cropperjs": "^4.1.6",
"@vicons/fluent": "^0.13.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vueuse/core": "^13.9.0",
@@ -38,7 +37,6 @@
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"bilibili-live-danmaku": "^0.7.14",
"cropperjs": "^2.0.1",
"crypto-js": "^4.2.0",
"date-fns": "^4.1.0",
"easy-speech": "^2.4.0",
@@ -68,8 +66,8 @@
"vite-plugin-monaco-editor-nls": "^3.0.1",
"vite-svg-loader": "^5.1.0",
"vue": "3.5.22",
"vue-cropperjs": "^5.0.0",
"vue-echarts": "^8.0.0",
"vue-img-cutter": "^3.0.7",
"vue-request": "^2.0.4",
"vue-router": "^4.5.1",
"vue-toastification": "^1.7.14",

View File

@@ -96,7 +96,7 @@ export function downloadImage(imageSrc: string, filename: string) {
canvas.width = image.width
canvas.height = image.height
const ctx = canvas.getContext('2d')
ctx!.drawImage(image, 0, 0)
ctx.drawImage(image, 0, 0)
canvas.toBlob((blob) => {
if (blob) {
const link = document.createElement('a')

View File

@@ -244,6 +244,14 @@ export interface Setting_Point {
maxBonusPoints: number // 最大奖励积分
allowSelfCheckIn: boolean // 是否允许自己签到
requireAuth: boolean // 是否需要认证
// 每日首次互动奖励设置
enableDailyFirstDanmaku: boolean // 是否启用每日首次弹幕奖励
dailyFirstDanmakuPoints: number // 每日首次弹幕积分
enableDailyFirstGift: boolean // 是否启用每日首次礼物奖励
dailyFirstGiftPoints: number // 每日首次礼物积分(固定积分)
useDailyFirstGiftPercent: boolean // 是否使用礼物价值比例计算
dailyFirstGiftPercent: number // 每日首次礼物价值比例
}
export interface Setting_QuestionDisplay {
font?: string // Optional string, with a maximum length of 30 characters
@@ -823,6 +831,22 @@ export enum PointOrderStatus {
Shipped, // 订单已发货
Completed, // 订单已完成
}
// 积分历史记录的 extra 字段类型定义
// 为了保持向后兼容并避免类型检查问题,使用通用的 extra 接口,但提供详细注释
export interface PointHistoryExtraBase {
user?: UserBasicInfo
// Danmaku 类型特有字段
danmaku?: DanmakuModel
// Manual 类型特有字段
reason?: string
// Use 类型特有字段
goods?: ResponsePointGoodModel | null
isDiscontinued?: boolean
remark?: string
// DailyFirstInteraction 类型特有字段
interactionType?: string // 'danmaku' | 'gift'
}
export interface ResponsePointHisrotyModel {
point: number
ouId: string
@@ -831,7 +855,15 @@ export interface ResponsePointHisrotyModel {
createAt: number
count: number
extra?: any // Use 时包含: { user, goods, isDiscontinued, remark }; Manual 时包含: { user, reason }; Danmaku 时包含: { user, danmaku }; CheckIn 时包含: { user }
/**
* 根据 from 字段extra 包含不同的数据:
* - PointFrom.Danmaku: { user: UserBasicInfo, danmaku: DanmakuModel }
* - PointFrom.Manual: { user: UserBasicInfo, reason?: string }
* - PointFrom.Use: { user: UserBasicInfo, goods: ResponsePointGoodModel | null, isDiscontinued: boolean, remark?: string }
* - PointFrom.CheckIn: { user: UserBasicInfo }
* - PointFrom.DailyFirstInteraction: { user: UserBasicInfo, interactionType: string, danmaku?: DanmakuModel | null }
*/
extra?: PointHistoryExtraBase
}
export enum PointFrom {
@@ -839,6 +871,7 @@ export enum PointFrom {
Manual,
Use,
CheckIn,
DailyFirstInteraction,
}
export interface ResponseUserIndexModel {

View File

@@ -1,7 +1,7 @@
import type { APIRoot, PaginationResponse } from './api-models'
import { apiFail } from '@/data/constants'
import { cookie } from './account'
import { useBiliAuth } from '@/store/useBiliAuth';
import { useBiliAuth } from '@/store/useBiliAuth'
export async function QueryPostAPI<T>(
urlString: string,
@@ -58,9 +58,9 @@ async function QueryPostAPIWithParamsInternal<T>(
h[header[0]] = header[1]
})
if (cookie.value.cookie) h.Authorization = `Bearer ${cookie.value.cookie}`
const biliAuth = useBiliAuth();
const biliAuth = useBiliAuth()
if (biliAuth.currentToken) {
h['Bili-Auth'] = biliAuth.currentToken;
h['Bili-Auth'] = biliAuth.currentToken
}
// 当使用FormData时不手动设置Content-Type让浏览器自动添加boundary
@@ -122,9 +122,9 @@ async function QueryGetAPIInternal<T>(
if (cookie.value.cookie) {
h.Authorization = `Bearer ${cookie.value.cookie}`
}
const biliAuth = useBiliAuth();
const biliAuth = useBiliAuth()
if (biliAuth.currentToken) {
h['Bili-Auth'] = biliAuth.currentToken;
h['Bili-Auth'] = biliAuth.currentToken
}
return await QueryAPIInternal<T>(url, { method: 'get', headers: h })
} catch (err) {

View File

@@ -325,7 +325,7 @@ const sortedTodayTypes = computed(() => {
.sort(([, countA], [, countB]) => countB - countA)
})
type TodayTypeRow = {
interface TodayTypeRow {
key: string
rank: number
type: string
@@ -1036,9 +1036,9 @@ onUnmounted(() => {
:type="biliCookie.cookieCloudState === 'valid' ? 'success' : biliCookie.cookieCloudState === 'syncing' ? 'info' : biliCookie.cookieCloudState === 'invalid' ? 'error' : 'default'"
>
{{
biliCookie.cookieCloudState === 'valid' ? '已配置' :
biliCookie.cookieCloudState === 'syncing' ? '同步中' :
biliCookie.cookieCloudState === 'invalid' ? '配置无效' : '未配置'
biliCookie.cookieCloudState === 'valid' ? '已配置'
: biliCookie.cookieCloudState === 'syncing' ? '同步中'
: biliCookie.cookieCloudState === 'invalid' ? '配置无效' : '未配置'
}}
</NTag>
</template>

View File

@@ -1,8 +1,7 @@
<script setup lang="ts">
import ReadDanmaku from '@/views/open_live/ReadDanmaku.vue';
import ReadDanmaku from '@/views/open_live/ReadDanmaku.vue'
</script>
<template>
<ReadDanmaku />
</template>
<ReadDanmaku />
</template>

View File

@@ -211,8 +211,7 @@ const separatorOptions = [
>
<NGi>
<NFormItem label="背景颜色">
<NColorPicker
/>
<NColorPicker />
</NFormItem>
</NGi>
<NGi>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { HistoryItem } from '../../store/autoAction/utils/historyLogger'
import { ArrowClockwise16Filled, CheckmarkCircle16Filled, Delete16Filled, DismissCircle16Filled } from '@vicons/fluent'
import {
NButton,
@@ -16,6 +17,8 @@ import {
NTooltip,
useMessage,
} from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { h, onMounted, onUnmounted, ref } from 'vue'
import { clearAllHistory, clearHistory, getHistoryByType, HistoryType } from '../../store/autoAction/utils/historyLogger'
@@ -40,7 +43,7 @@ const refreshInterval = 10000
let refreshTimer: number | null = null
// 列定义
const columns = [
const columns: DataTableColumns<HistoryItem> = [
{
title: '时间',
key: 'timestamp',
@@ -67,7 +70,7 @@ const columns = [
key: 'content',
ellipsis: {
tooltip: true,
},
} as const,
},
{
title: '目标',

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { DataTableColumns } from 'naive-ui'
import type { CheckInRankingInfo, CheckInResult } from '@/api/api-models'
import type { CheckInRankingInfo, CheckInResult, Setting_Point } from '@/api/api-models'
import { Info24Filled } from '@vicons/fluent'
import { NAlert, NButton, NCard, NDataTable, NDivider, NForm, NFormItem, NIcon, NInput, NInputGroup, NInputNumber, NPopconfirm, NSelect, NSpace, NSpin, NSwitch, NTabPane, NTabs, NText, NTime, NTooltip } from 'naive-ui'
import { computed, h, onMounted, ref } from 'vue'
@@ -38,9 +39,35 @@ const checkInPlaceholders = [
{ name: '{{checkin.time}}', description: '签到时间对象' },
]
// 服务端签到设置
const serverSetting = computed(() => {
return accountInfo.value?.settings?.point || {}
// 服务端签到设置(提供强类型默认值,避免模板中访问属性时报错)
const defaultPointSetting: Setting_Point = {
allowType: [],
jianzhangPoint: 0,
tiduPoint: 0,
zongduPoint: 0,
giftPercentMap: {},
scPointPercent: 0,
giftPointPercent: 0,
giftAllowType: 0,
shouldDiscontinueWhenSoldOut: false,
enableCheckIn: false,
checkInKeyword: '',
givePointsForCheckIn: false,
baseCheckInPoints: 0,
enableConsecutiveBonus: false,
bonusPointsPerDay: 0,
maxBonusPoints: 0,
allowSelfCheckIn: false,
requireAuth: false,
enableDailyFirstDanmaku: false,
dailyFirstDanmakuPoints: 5,
enableDailyFirstGift: false,
dailyFirstGiftPoints: 10,
useDailyFirstGiftPercent: false,
dailyFirstGiftPercent: 0.1,
}
const serverSetting = computed<Setting_Point>(() => {
return (accountInfo.value?.settings?.point ?? defaultPointSetting)
})
// 是否可以编辑设置
@@ -184,12 +211,12 @@ const rankingColumns: DataTableColumns<CheckInRankingInfo> = [
{
title: '连续签到天数',
key: 'consecutiveDays',
sorter: 'default',
sorter: true,
},
{
title: '积分',
key: 'points',
sorter: 'default',
sorter: true,
},
{
title: '最近签到时间',
@@ -204,7 +231,7 @@ const rankingColumns: DataTableColumns<CheckInRankingInfo> = [
default: () => new Date(row.lastCheckInTime).toLocaleString(),
})
},
sorter: 'default',
sorter: true,
},
{
title: '已认证',

View File

@@ -144,7 +144,7 @@ export function useDanmakuUtils(
const emojiInfo = (availableEmojis.inline?.[emojiFullName]
?? availableEmojis.inline?.[emojiName]
?? availableEmojis.plain?.[emojiFullName]
?? availableEmojis.plain?.[emojiName]) as string | undefined
?? availableEmojis.plain?.[emojiName])
if (emojiInfo) {
// 找到了表情

View File

@@ -37,7 +37,7 @@ let updateNotificationRef: any = null
async function sendHeartbeat() {
try {
await invoke('heartbeat', undefined, {
headers: [['Origin', location.host]]
headers: [['Origin', location.host]],
})
} catch (error) {
console.error('发送心跳失败:', error)
@@ -439,7 +439,7 @@ export async function callStartDanmakuClient() {
if (settings.settings.useDanmakuClientType === 'direct') {
info('开始初始化弹幕客户端 [direct]')
const key = await getRoomKey(
accountInfo.value.biliRoomId!,
accountInfo.value.biliRoomId,
await biliCookie.getBiliCookie() || '',
)
if (!key) {
@@ -452,10 +452,10 @@ export async function callStartDanmakuClient() {
return { success: false, message: '无法获取buvid' }
}
return webFetcher.Start('direct', {
roomId: accountInfo.value.biliRoomId!,
roomId: accountInfo.value.biliRoomId,
buvid: buvid.data,
token: key,
tokenUserId: biliCookie.uId!,
tokenUserId: biliCookie.uId,
}, true)
} else {
info('开始初始化弹幕客户端 [openlive]')

View File

@@ -259,7 +259,7 @@ export function executeActions(
// 更新冷却时间
runtimeState.lastExecutionTime[action.id] = Date.now()
const sendAction = async () => sendAndLogDanmaku(handlers.sendLiveDanmaku!, action, roomId, message)
const sendAction = async () => sendAndLogDanmaku(handlers.sendLiveDanmaku, action, roomId, message)
// 延迟发送
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
@@ -285,7 +285,7 @@ export function executeActions(
runtimeState.lastExecutionTime[action.id] = Date.now()
const sendPmPromise = async (uid: number, msg: string) => {
return handlers.sendPrivateMessage!(uid, msg)
return handlers.sendPrivateMessage(uid, msg)
.then((success) => {
// 记录私信发送历史
logPrivateMsgHistory(

View File

@@ -1,6 +1,6 @@
import type {
AutoActionItem,
RuntimeState
RuntimeState,
} from './autoAction/types.js'
import type { EventModel } from '@/api/api-models.js'
import { useIDBKeyval } from '@vueuse/integrations/useIDBKeyval'
@@ -141,7 +141,7 @@ export const useAutoAction = defineStore('autoAction', () => {
runtimeState.value.lastExecutionTime[actionToExecute.id] = Date.now()
if (actionToExecute.actionConfig.delaySeconds && actionToExecute.actionConfig.delaySeconds > 0) {
setTimeout(() => {
biliFunc.sendLiveDanmaku(roomId.value!, formattedContent).catch(err => console.error('[AutoAction] 发送弹幕失败:', err))
biliFunc.sendLiveDanmaku(roomId.value, formattedContent).catch(err => console.error('[AutoAction] 发送弹幕失败:', err))
}, actionToExecute.actionConfig.delaySeconds * 1000)
} else {
biliFunc.sendLiveDanmaku(roomId.value, formattedContent).catch(err => console.error('[AutoAction] 发送弹幕失败:', err))
@@ -251,7 +251,7 @@ export const useAutoAction = defineStore('autoAction', () => {
runtimeState.value.lastExecutionTime[currentAction.id] = Date.now()
if (currentAction.actionConfig.delaySeconds && currentAction.actionConfig.delaySeconds > 0) {
setTimeout(() => {
biliFunc.sendLiveDanmaku(roomId.value!, formattedContent).catch(err => console.error('[AutoAction] 发送弹幕失败:', err))
biliFunc.sendLiveDanmaku(roomId.value, formattedContent).catch(err => console.error('[AutoAction] 发送弹幕失败:', err))
}, currentAction.actionConfig.delaySeconds * 1000)
} else {
biliFunc.sendLiveDanmaku(roomId.value, formattedContent).catch(err => console.error('[AutoAction] 发送弹幕失败:', err))
@@ -705,7 +705,7 @@ export const useAutoAction = defineStore('autoAction', () => {
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
console.log(`[定时任务测试] 将在 ${action.actionConfig.delaySeconds} 秒后发送弹幕`)
setTimeout(() => {
biliFunc.sendLiveDanmaku(roomId.value!, formattedContent)
biliFunc.sendLiveDanmaku(roomId.value, formattedContent)
.catch(err => console.error('[AutoAction] 发送弹幕失败:', err))
}, action.actionConfig.delaySeconds * 1000)
} else {

View File

@@ -21,7 +21,7 @@ export class StoreTarget<T> {
if (result === undefined && this.defaultValue !== undefined) {
await this.set(this.defaultValue)
return this.defaultValue as T
return this.defaultValue
}
return result
}

10
src/components.d.ts vendored
View File

@@ -18,18 +18,14 @@ declare module 'vue' {
LabelItem: typeof import('./components/LabelItem.vue')['default']
LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default']
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NAlert: typeof import('naive-ui')['NAlert']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGridItem: typeof import('naive-ui')['NGridItem']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch']
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
PointGoodsItem: typeof import('./components/manage/PointGoodsItem.vue')['default']

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import * as monaco from 'monaco-editor'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
@@ -7,7 +7,6 @@ import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
const { language, height = 400, theme = 'vs-dark', options, path } = defineProps<{
language: string
height?: number
@@ -110,12 +109,14 @@ onBeforeUnmount(() => {
<template>
<div :style="`height: ${height}px; width: 100%; position: relative;`">
<div v-if="!ready" :style="`position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color: var(--text-color, #888); text-align:center; padding:8px;`">
<div v-if="!ready" style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color: var(--text-color, #888); text-align:center; padding:8px;">
<div>
<div>正在加载编辑器</div>
<div v-if="initError" style="margin-top:6px; color:#d9534f; font-size:12px;">{{ initError }}</div>
<div v-if="initError" style="margin-top:6px; color:#d9534f; font-size:12px;">
{{ initError }}
</div>
</div>
</div>
<div ref="containerRef" :style="`height: ${height}px; width: 100%;`"></div>
<div ref="containerRef" :style="`height: ${height}px; width: 100%;`" />
</div>
</template>

View File

@@ -8,12 +8,13 @@ import {
} from '@vicons/fluent'
import { refDebounced, useLocalStorage } from '@vueuse/core' // VueUse 工具函数
import { List } from 'linqts' // LINQ for TypeScript
import {
import type {
DataTableBaseColumn,
DataTableColumns,
DataTableRowKey,
FormInst,
FormRules,
FormRules} from 'naive-ui';
import {
NButton,
NCard,
NCheckbox,
@@ -38,10 +39,12 @@ import {
NTooltip,
useMessage, // Naive UI 组件
} from 'naive-ui'
import { computed, h, onMounted, ref, VNodeChild, watch } from 'vue' // Vue 核心 API
import type { VNodeChild} from 'vue';
import { computed, h, onMounted, ref, watch } from 'vue' // Vue 核心 API
// [导入] 依赖项和类型
import { SongFrom, SongRequestOption, SongsInfo } from '@/api/api-models' // API 数据模型
import type { SongRequestOption, SongsInfo } from '@/api/api-models';
import { SongFrom } from '@/api/api-models' // API 数据模型
import { QueryGetAPI, QueryPostAPI } from '@/api/query' // API 请求方法
import { SONG_API_URL } from '@/data/constants' // API 地址常量
import { GetPlayButton } from '@/Utils' // 公用方法:获取播放/信息按钮
@@ -198,7 +201,7 @@ const languageSelectOption = computed(() => {
'韩语',
'法语',
'西语',
'其他'
'其他',
])
songsInternal.value.forEach((s) => {
s.language?.forEach(l => languages.add(l))
@@ -252,9 +255,9 @@ const authorColumn = ref<DataTableBaseColumn<SongsInfo>>({
render(data) {
// 渲染作者按钮,点击时更新列筛选状态
return h(NSpace, { size: 5 }, () =>
data.author?.map(a => // 使用 ?. 防止 author 为空
(data.author?.map(a => // 使用 ?. 防止 author 为空
h(NButton, { size: 'tiny', type: 'info', secondary: true, onClick: () => onAuthorClick(a) }, () => a),
) ?? null, // 如果 author 为空则不渲染
) ?? null) // 如果 author 为空则不渲染
)
},
})
@@ -319,7 +322,7 @@ function createColumns(): DataTableColumns<SongsInfo> {
// 使用 NTag 显示语言
return data.language?.length // 使用 ?.length 检查
? h(NSpace, { size: 5 }, () =>
data.language?.map(a => h(NTag, { bordered: false, size: 'small' }, () => a)) )
data.language?.map(a => h(NTag, { bordered: false, size: 'small' }, () => a)) )
: null
},
},
@@ -516,7 +519,7 @@ async function updateSong() {
return
}
isLoading.value = true // 开始加载
const { code, data, message: errMsg } = await QueryPostAPI<SongsInfo>(`${SONG_API_URL }update`, {
const { code, data, message: errMsg } = await QueryPostAPI<SongsInfo>(`${SONG_API_URL}update`, {
key: updateSongModel.value.key,
song: updateSongModel.value,
})
@@ -543,7 +546,7 @@ async function updateSong() {
async function delSong(song: SongsInfo) {
isLoading.value = true // 开始加载 (虽然删除很快,但保持一致性)
try {
const { code, message: errMsg } = await QueryGetAPI<SongsInfo>(`${SONG_API_URL }del`, { key: song.key })
const { code, message: errMsg } = await QueryGetAPI<SongsInfo>(`${SONG_API_URL}del`, { key: song.key })
if (code === 200) {
// 从内部列表中移除
songsInternal.value = songsInternal.value.filter(s => s.key !== song.key)
@@ -573,7 +576,7 @@ async function delBatchSong() {
const ids = selectedColumn.value.map(s => s.toString())
isLoading.value = true
try {
const { code, message: errMsg } = await QueryPostAPI<SongsInfo>(`${SONG_API_URL }del-batch`, ids)
const { code, message: errMsg } = await QueryPostAPI<SongsInfo>(`${SONG_API_URL}del-batch`, ids)
if (code === 200) {
songsInternal.value = songsInternal.value.filter(s => !ids.includes(s.key))
message.success(`已删除 ${ids.length} 首歌曲`)

View File

@@ -77,6 +77,10 @@ const historyColumn: DataTableColumns<ResponsePointHisrotyModel> = [
label: '签到',
value: PointFrom.CheckIn,
},
{
label: '每日首次互动',
value: PointFrom.DailyFirstInteraction,
},
],
render: (row: ResponsePointHisrotyModel) => {
const get = () => {
@@ -153,6 +157,23 @@ const historyColumn: DataTableColumns<ResponsePointHisrotyModel> = [
)
: null,
])
case PointFrom.DailyFirstInteraction:
return h(NFlex, { align: 'center' }, () => [
h(NTag, { type: 'primary', bordered: false, size: 'small' }, () => '首次互动'),
row.extra?.user
? h(
NButton,
{
tag: 'a',
href: `/@${row.extra.user?.name}`,
target: '_blank',
text: true,
type: 'info',
},
() => row.extra.user?.name,
)
: null,
])
}
}
return h(NFlex, {}, () => get())
@@ -204,6 +225,17 @@ const historyColumn: DataTableColumns<ResponsePointHisrotyModel> = [
h(NTag, { type: 'info', size: 'small', style: { margin: '0' }, bordered: false }, () => '备注'),
h(NText, {}, () => row.extra.reason ?? h(NText, { italic: true, depth: '3' }, () => '未提供')),
])
case PointFrom.DailyFirstInteraction:
// 每日首次互动奖励
const interactionType = row.extra?.interactionType
return h(NFlex, { align: 'center' }, () => [
h(NTag, {
type: interactionType === 'danmaku' ? 'info' : 'warning',
size: 'small',
bordered: false
}, () => interactionType === 'danmaku' ? '弹幕' : '礼物'),
h('span', {}, interactionType === 'danmaku' ? row.extra?.danmaku?.msg : `${row.extra?.danmaku?.msg} x ${row.extra?.danmaku?.num}`)
])
case PointFrom.Use:
return h(NFlex, { align: 'center' }, () => [
h(NTag, { type: 'success', size: 'small', style: { margin: '0' }, strong: true }, () => '兑换'),

View File

@@ -1,4 +1,4 @@
import { LiveWS } from "bilibili-live-danmaku";
import type { LiveWS } from 'bilibili-live-danmaku'
// BaseDanmakuClient.ts
import type { EventModel } from '@/api/api-models'
// 导入事件模型和类型枚举

View File

@@ -1,4 +1,5 @@
import { DataEvent, LiveWS, MessageData } from 'bilibili-live-danmaku'
import type { MessageData } from 'bilibili-live-danmaku'
import { LiveWS } from 'bilibili-live-danmaku'
import { EventDataTypes, GuardLevel } from '@/api/api-models'
import { GuidUtils } from '@/Utils'
import { AVATAR_URL } from '../constants'
@@ -43,8 +44,8 @@ export default class DirectClient extends BaseDanmakuClient {
chatClient.addEventListener('SEND_GIFT', data => this.onGift(data.data))
chatClient.addEventListener('GUARD_BUY', data => this.onGuard(data.data))
chatClient.addEventListener('SUPER_CHAT_MESSAGE', data => this.onSC(data.data))
//chatClient.addEventListener('INTERACT_WORD', data => this.onEnter(data.data))
chatClient.addEventListener('MESSAGE', data => {
// chatClient.addEventListener('INTERACT_WORD', data => this.onEnter(data.data))
chatClient.addEventListener('MESSAGE', (data) => {
switch (data.data.cmd) {
case 'INTERACT_WORD_V2':
this.onEnter(data.data)
@@ -56,7 +57,7 @@ export default class DirectClient extends BaseDanmakuClient {
break
}
})
//chatClient.addEventListener('SUPER_CHAT_MESSAGE_DELETE', data => this.onScDel(data))
// chatClient.addEventListener('SUPER_CHAT_MESSAGE_DELETE', data => this.onScDel(data))
return super.initClientInner(chatClient)
} else {

View File

@@ -45,7 +45,7 @@ export default class OpenLiveClient extends BaseDanmakuClient {
authBody: JSON.parse(auth.data.websocket_info.auth_body),
address: auth.data.websocket_info.wss_link[0],
})
chatClient.addEventListener('MESSAGE', cmd => {
chatClient.addEventListener('MESSAGE', (cmd) => {
switch (cmd.data.cmd as string) {
case 'LIVE_OPEN_PLATFORM_DM':
this.onDanmaku(cmd.data)

View File

@@ -18,9 +18,9 @@ void import('./data/Initializer').then(m => m.InitVTsuru())
const isTauri = () => (window as any).__TAURI__ !== undefined || (window as any).__TAURI_INTERNAL__ !== undefined || '__TAURI__' in window
if (isTauri()) {
// 仅在 Tauri 环境下才动态加载相关初始化,避免把 @tauri-apps/* 打入入口
void import('./client/data/initialize').then(m => {
m.startHeartbeat();
m.checkUpdate();
void import('./client/data/initialize').then((m) => {
m.startHeartbeat()
m.checkUpdate()
})
}

View File

@@ -1,6 +1,9 @@
import { RouterView } from 'vue-router'
export default {
path: '/client',
name: 'client',
component: RouterView,
children: [
{
path: '',

View File

@@ -1,7 +1,6 @@
import { useLoadingBarStore } from '@/store/useLoadingBarStore'
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import { useLoadingBarStore } from '@/store/useLoadingBarStore'
import IndexView from '../views/IndexView.vue'
import client from './client'
import manage from './manage'
import obs from './obs'
@@ -92,11 +91,13 @@ const routes: Array<RouteRecordRaw> = [
obs,
open_live,
obs_store,
// @ts-expect-error
client,
{
path: '/@:id',
name: 'user',
alias: '/user/:id',
// @ts-expect-error
children: user,
},
{

View File

@@ -1,7 +1,10 @@
import { RouterView } from 'vue-router'
export default // 管理页面
{
path: '/manage',
name: 'manage',
component: RouterView,
children: [
{
path: '',

View File

@@ -1,6 +1,9 @@
import { RouterView } from 'vue-router'
export default {
path: '/obs',
name: 'obs',
component: RouterView,
children: [
{
path: 'live-lottery',

View File

@@ -1,6 +1,9 @@
import { RouterView } from 'vue-router'
export default {
path: '/obs-store',
name: 'obs-store',
component: RouterView,
children: [
{
path: 'gamepad-manage',

View File

@@ -1,6 +1,9 @@
import { RouterView } from 'vue-router'
export default {
path: '/open-live',
name: 'open-live',
component: RouterView,
children: [
{
path: '',

View File

@@ -1,3 +1,5 @@
import type { RouteRecordRaw } from 'vue-router'
export default [
{
path: '/question-display',
@@ -25,4 +27,4 @@ export default [
forceReload: true,
},
},
]
] satisfies RouteRecordRaw[]

View File

@@ -1,3 +1,5 @@
import type { RouteRecordRaw } from 'vue-router'
export default [
{
path: '',
@@ -92,4 +94,4 @@ export default [
keepAlive: true,
},
},
]
] satisfies RouteRecordRaw[]

View File

@@ -119,7 +119,7 @@ export const useGamepadStore = defineStore('gamepad', () => {
if (connectedGamepadInfo.value) {
connectedHandlers.forEach((handler) => {
try {
handler(connectedGamepadInfo.value!, index)
handler(connectedGamepadInfo.value, index)
} catch (err) {
console.error('手柄连接事件处理器执行错误:', err)
}
@@ -169,7 +169,7 @@ export const useGamepadStore = defineStore('gamepad', () => {
// 如果自动切换到其他手柄,也触发连接事件
connectedHandlers.forEach((handler) => {
try {
handler(connectedGamepadInfo.value!, nextGamepad.index)
handler(connectedGamepadInfo.value, nextGamepad.index)
} catch (err) {
console.error('手柄连接事件处理器执行错误:', err)
}

View File

@@ -1,4 +1,5 @@
import type { IDeductionSetting, UserConsumptionSetting } from '@/api/models/consumption'
import { defineStore } from 'pinia'
import { computed } from 'vue'
import { useAccount } from '@/api/account'
@@ -18,7 +19,7 @@ export const useConsumptionSettingStore = defineStore(
name: '弹幕存储',
key: 'danmakuStorage',
},
}
} as const satisfies Record<ConsumptionTypes, { name: string, key: keyof UserConsumptionSetting }>
async function UpdateConsumptionSetting(
type: ConsumptionTypes,
@@ -33,8 +34,8 @@ export const useConsumptionSettingStore = defineStore(
)
}
function GetSetting(type: ConsumptionTypes) {
// @ts-expect-error 直接从对象获取key
return consumptionSetting.value[consumptionTypeMap[type].key] as IDeductionSetting
const key = consumptionTypeMap[type].key
return consumptionSetting.value[key] as IDeductionSetting
}
return { consumptionSetting, consumptionTypeMap, UpdateConsumptionSetting, GetSetting }

View File

@@ -383,7 +383,7 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
.then((data) => {
if (data.code == 200) {
message.success('已标记为正常')
question.reviewResult!.isApproved = true
question.reviewResult.isApproved = true
}
})
.catch((err) => {

View File

@@ -192,7 +192,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
if (client.connected) {
console.log(`${prefix.value}弹幕客户端连接成功, 开始监听弹幕`)
danmakuClientState.value = 'connected' // 明确设置状态
danmakuServerUrl.value = client.danmakuClient!.serverUrl // 获取服务器地址
danmakuServerUrl.value = client.danmakuClient.serverUrl // 获取服务器地址
// 启动事件发送定时器 (如果之前没有启动)
timer ??= setInterval(sendEvents, 2000) // 每 2 秒尝试发送一次事件
return { success: true, message: '弹幕客户端已启动' }

View File

@@ -171,7 +171,7 @@ const shadowSystem = computed(() => ({
: '0 8px 32px rgba(0, 0, 0, 0.16), 0 4px 12px rgba(0, 0, 0, 0.20)',
hover: isDarkMode.value
? '0 12px 48px rgba(0, 0, 0, 0.7), 0 6px 16px rgba(0, 0, 0, 0.5)'
: '0 12px 48px rgba(0, 0, 0, 0.20), 0 6px 16px rgba(0, 0, 0, 0.24)'
: '0 12px 48px rgba(0, 0, 0, 0.20), 0 6px 16px rgba(0, 0, 0, 0.24)',
}))
// 统一的边框系统
@@ -184,7 +184,7 @@ const borderSystem = computed(() => ({
: '1px solid rgba(255, 255, 255, 0.25)',
accent: isDarkMode.value
? '2px solid rgba(255, 255, 255, 0.15)'
: '2px solid rgba(255, 255, 255, 0.3)'
: '2px solid rgba(255, 255, 255, 0.3)',
}))
// 功能图标颜色映射 - 优化为统一的色系,与背景渐变协调
@@ -192,53 +192,53 @@ const iconColors = computed(() => {
// 基于背景渐变色调的统一色板
const baseColors = isDarkMode.value ? {
// 暗色模式:更柔和的色调,降低饱和度
teal: '#4ECDC4', // 青绿色 - 接近背景起始色
purple: '#9B7EDE', // 紫色 - 接近背景结束色
blue: '#6BB6FF', // 蓝色
green: '#7ED321', // 绿色
orange: '#F5A623', // 橙色
pink: '#D63384', // 粉色
indigo: '#6F42C1', // 靛蓝
cyan: '#17A2B8', // 青色
mint: '#20C997', // 薄荷绿
lavender: '#B794F6', // 薰衣草紫
coral: '#FF6B6B', // 珊瑚色
sage: '#8FBC8F' // 鼠尾草绿
teal: '#4ECDC4', // 青绿色 - 接近背景起始色
purple: '#9B7EDE', // 紫色 - 接近背景结束色
blue: '#6BB6FF', // 蓝色
green: '#7ED321', // 绿色
orange: '#F5A623', // 橙色
pink: '#D63384', // 粉色
indigo: '#6F42C1', // 靛蓝
cyan: '#17A2B8', // 青色
mint: '#20C997', // 薄荷绿
lavender: '#B794F6', // 薰衣草紫
coral: '#FF6B6B', // 珊瑚色
sage: '#8FBC8F', // 鼠尾草绿
} : {
// 亮色模式:更鲜艳的色调,保持活力
teal: '#2EBFA5', // 青绿色 - 与背景起始色呼应
purple: '#8B5CF6', // 紫色 - 与背景结束色呼应
blue: '#3B82F6', // 蓝色
green: '#10B981', // 绿色
orange: '#F59E0B', // 橙色
pink: '#EC4899', // 粉色
indigo: '#6366F1', // 靛蓝
cyan: '#06B6D4', // 青色
mint: '#14B8A6', // 薄荷绿
lavender: '#A855F7', // 薰衣草紫
coral: '#EF4444', // 珊瑚色
sage: '#22C55E' // 鼠尾草绿
teal: '#2EBFA5', // 青绿色 - 与背景起始色呼应
purple: '#8B5CF6', // 紫色 - 与背景结束色呼应
blue: '#3B82F6', // 蓝色
green: '#10B981', // 绿色
orange: '#F59E0B', // 橙色
pink: '#EC4899', // 粉色
indigo: '#6366F1', // 靛蓝
cyan: '#06B6D4', // 青色
mint: '#14B8A6', // 薄荷绿
lavender: '#A855F7', // 薰衣草紫
coral: '#EF4444', // 珊瑚色
sage: '#22C55E', // 鼠尾草绿
}
return {
VehicleShip24Filled: baseColors.teal, // 直播事件记录 - 青绿色
BookCoins20Filled: baseColors.orange, // 积分兑换 - 橙色
Chat24Filled: baseColors.green, // 弹幕机 - 绿色
Calendar: baseColors.pink, // 日程表 - 粉色
MusicalNote: baseColors.purple, // 歌单 - 紫色
Chatbox: baseColors.blue, // 棉花糖 - 蓝色
Lottery24Filled: baseColors.coral, // 抽奖功能 - 珊瑚色
ListCircle: baseColors.sage, // 点歌/排队功能 - 鼠尾草绿
TabletSpeaker24Filled: baseColors.cyan, // 读弹幕 - 青色
VideoAdd20Filled: baseColors.lavender, // 视频征集 - 薰衣草紫
AnalyticsSharp: baseColors.mint, // 数据跟踪 - 薄荷绿
MoreHorizontal24Filled: baseColors.indigo, // 更多功能 - 靛蓝
PersonFeedback24Filled: baseColors.coral, // 自动操作 - 珊瑚色
VehicleShip24Filled: baseColors.teal, // 直播事件记录 - 青绿色
BookCoins20Filled: baseColors.orange, // 积分兑换 - 橙色
Chat24Filled: baseColors.green, // 弹幕机 - 绿色
Calendar: baseColors.pink, // 日程表 - 粉色
MusicalNote: baseColors.purple, // 歌单 - 紫色
Chatbox: baseColors.blue, // 棉花糖 - 蓝色
Lottery24Filled: baseColors.coral, // 抽奖功能 - 珊瑚色
ListCircle: baseColors.sage, // 点歌/排队功能 - 鼠尾草绿
TabletSpeaker24Filled: baseColors.cyan, // 读弹幕 - 青色
VideoAdd20Filled: baseColors.lavender, // 视频征集 - 薰衣草紫
AnalyticsSharp: baseColors.mint, // 数据跟踪 - 薄荷绿
MoreHorizontal24Filled: baseColors.indigo, // 更多功能 - 靛蓝
PersonFeedback24Filled: baseColors.coral, // 自动操作 - 珊瑚色
}
})
// 处理功能卡片点击
const handleFunctionClick = (item: typeof functions[0]) => {
function handleFunctionClick(item: typeof functions[0]) {
if (item.route) {
// 跳转到对应的管理页面
$router.push({ name: item.route })
@@ -257,46 +257,56 @@ onMounted(async () => {
<div class="index-background">
<NSpace vertical justify="center" align="center" class="main-container">
<!-- 顶部标题部分 -->
<NCard :style="{
background: cardBgLight,
backdropFilter: 'blur(10px)',
border: 'none',
width: '90vw',
maxWidth: '1400px',
borderRadius: borderRadius.xlarge,
}" class="hero-card">
<NCard
:style="{
background: cardBgLight,
backdropFilter: 'blur(10px)',
border: 'none',
width: '90vw',
maxWidth: '1400px',
borderRadius: borderRadius.xlarge,
}" class="hero-card"
>
<NSpace justify="center" align="center" :size="width > 700 ? 50 : 0" :vertical="width <= 700">
<vtb class="hero-icon" />
<NSpace vertical justify="center" :align="width <= 700 ? 'center' : 'start'">
<NGradientText :size="width > 700 ? '3rem' : '2.5rem'" :gradient="{
deg: 180,
...gradientColors,
}" style="font-weight: 700">
<NGradientText
:size="width > 700 ? '3rem' : '2.5rem'" :gradient="{
deg: 180,
...gradientColors,
}" style="font-weight: 700"
>
VTSURU.LIVE
</NGradientText>
<NText :style="{
fontSize: width > 700 ? '1.5em' : '1.2em',
fontWeight: 500,
color: textColor,
textAlign: width <= 700 ? 'center' : 'left',
}">
<NText
:style="{
fontSize: width > 700 ? '1.5em' : '1.2em',
fontWeight: 500,
color: textColor,
textAlign: width <= 700 ? 'center' : 'left',
}"
>
一个给主播提供便利功能的网站 😊
</NText>
<!-- 主播 / 观众入口 -->
<NFlex :wrap="width <= 700" justify="center" align="center"
:style="{ gap: width > 700 ? '24px' : '16px', marginTop: '20px' }">
<NFlex
:wrap="width <= 700" justify="center" align="center"
:style="{ gap: width > 700 ? '24px' : '16px', marginTop: '20px' }"
>
<!-- 主播入口 -->
<NTooltip placement="bottom">
<template #trigger>
<NCard hoverable :style="{
width: width > 700 ? '240px' : '100%',
minWidth: '200px',
background: cardBgMedium,
cursor: 'pointer',
border: 'none',
borderRadius: borderRadius.large,
transition: 'all 0.3s ease',
}" class="entry-card" @click="$router.push({ name: 'manage-index' })">
<NCard
hoverable :style="{
width: width > 700 ? '240px' : '100%',
minWidth: '200px',
background: cardBgMedium,
cursor: 'pointer',
border: 'none',
borderRadius: borderRadius.large,
transition: 'all 0.3s ease',
}" class="entry-card" @click="$router.push({ name: 'manage-index' })"
>
<NFlex vertical align="center" justify="center" :size="8">
<NIcon :component="PersonFeedback24Filled" size="36" :color="textColor" />
<NText :style="{ fontSize: '1.2rem', fontWeight: 500, color: textColor }">
@@ -314,15 +324,17 @@ onMounted(async () => {
<!-- 观众入口 -->
<NTooltip placement="bottom">
<template #trigger>
<NCard hoverable :style="{
width: width > 700 ? '240px' : '100%',
minWidth: '200px',
background: cardBgMedium,
cursor: 'pointer',
border: 'none',
borderRadius: borderRadius.large,
transition: 'all 0.3s ease',
}" class="entry-card" @click="$router.push({ name: 'bili-user' })">
<NCard
hoverable :style="{
width: width > 700 ? '240px' : '100%',
minWidth: '200px',
background: cardBgMedium,
cursor: 'pointer',
border: 'none',
borderRadius: borderRadius.large,
transition: 'all 0.3s ease',
}" class="entry-card" @click="$router.push({ name: 'bili-user' })"
>
<NFlex vertical align="center" justify="center" :size="8">
<NIcon :component="Chat24Filled" size="36" :color="textColor" />
<NText :style="{ fontSize: '1.2rem', fontWeight: 500, color: textColor }">
@@ -340,16 +352,22 @@ onMounted(async () => {
<!-- 其他操作按钮 -->
<NFlex justify="center" align="center" :wrap="width <= 700" :style="{ marginTop: '20px', gap: '12px' }">
<NButton size="large" secondary :style="{ borderRadius: borderRadius.large }"
@click="$router.push('/@Megghy')">
<NButton
size="large" secondary :style="{ borderRadius: borderRadius.large }"
@click="$router.push('/@Megghy')"
>
展示
</NButton>
<NButton size="large" tag="a" href="https://play-live.bilibili.com/details/1698742711771" target="_blank"
color="#ff778f" :style="{ borderRadius: borderRadius.large }">
<NButton
size="large" tag="a" href="https://play-live.bilibili.com/details/1698742711771" target="_blank"
color="#ff778f" :style="{ borderRadius: borderRadius.large }"
>
幻星平台
</NButton>
<NButton type="info" size="large" :style="{ borderRadius: borderRadius.large }"
@click="$router.push({ name: 'about' })">
<NButton
type="info" size="large" :style="{ borderRadius: borderRadius.large }"
@click="$router.push({ name: 'about' })"
>
关于
</NButton>
</NFlex>
@@ -358,21 +376,24 @@ onMounted(async () => {
</NCard>
<!-- 用户统计部分 -->
<NCard :style="{
background: isDarkMode ? 'rgba(255, 255, 255, 0.03)' : 'rgba(255, 255, 255, 0.08)',
backdropFilter: 'blur(10px)',
border: 'none',
width: '90vw',
maxWidth: '1400px',
borderRadius: borderRadius.medium,
}" size="small">
<NCard
:style="{
background: isDarkMode ? 'rgba(255, 255, 255, 0.03)' : 'rgba(255, 255, 255, 0.08)',
backdropFilter: 'blur(10px)',
border: 'none',
width: '90vw',
maxWidth: '1400px',
borderRadius: borderRadius.medium,
}" size="small"
>
<NFlex justify="center" align="center">
<div class="stats-item">
<NText :style="{ fontSize: '0.8rem', color: textColorSecondary, display: 'block', textAlign: 'center' }">
注册用户
</NText>
<NText
:style="{ fontSize: '1.2rem', fontWeight: 600, color: textColor, display: 'block', textAlign: 'center' }">
:style="{ fontSize: '1.2rem', fontWeight: 600, color: textColor, display: 'block', textAlign: 'center' }"
>
<NNumberAnimation :from="0" :to="indexData?.userCount" show-separator />
</NText>
</div>
@@ -380,15 +401,17 @@ onMounted(async () => {
</NCard>
<!-- 功能列表部分 -->
<NCard :style="{
background: cardBgLight,
backdropFilter: 'blur(10px)',
border: 'none',
width: '90vw',
maxWidth: '1400px',
marginBottom: '20px',
borderRadius: borderRadius.xlarge,
}">
<NCard
:style="{
background: cardBgLight,
backdropFilter: 'blur(10px)',
border: 'none',
width: '90vw',
maxWidth: '1400px',
marginBottom: '20px',
borderRadius: borderRadius.xlarge,
}"
>
<NFlex vertical>
<NFlex justify="center" align="center" style="margin-bottom: 30px;">
<div class="section-header">
@@ -404,20 +427,24 @@ onMounted(async () => {
</NFlex>
<NFlex :wrap="true" justify="center" style="gap: 15px;">
<NCard v-for="item in functions" :key="item.name" :style="{
width: '300px',
maxWidth: '100%',
background: cardBgMedium,
border: borderSystem.medium,
borderRadius: borderRadius.large,
boxShadow: 'none',
cursor: item.route ? 'pointer' : 'default',
}" hoverable class="feature-card" @click="handleFunctionClick(item)">
<NCard
v-for="item in functions" :key="item.name" :style="{
width: '300px',
maxWidth: '100%',
background: cardBgMedium,
border: borderSystem.medium,
borderRadius: borderRadius.large,
boxShadow: 'none',
cursor: item.route ? 'pointer' : 'default',
}" hoverable class="feature-card" @click="handleFunctionClick(item)"
>
<NFlex vertical>
<NFlex align="center" style="margin-bottom: 10px;">
<div class="icon-wrapper">
<NIcon :component="item.icon" size="24"
:color="iconColors[item.icon.name as keyof typeof iconColors] || textColor" />
<NIcon
:component="item.icon" size="24"
:color="iconColors[item.icon.name as keyof typeof iconColors] || textColor"
/>
</div>
<NText :style="{ fontSize: '1.1rem', fontWeight: 500, marginLeft: '12px', color: textColor }">
{{ item.name }}
@@ -433,15 +460,17 @@ onMounted(async () => {
</NCard>
<!-- 客户端专属功能部分 -->
<NCard :style="{
background: cardBgLight,
backdropFilter: 'blur(10px)',
border: 'none',
width: '90vw',
maxWidth: '1400px',
marginBottom: '20px',
borderRadius: borderRadius.xlarge,
}">
<NCard
:style="{
background: cardBgLight,
backdropFilter: 'blur(10px)',
border: 'none',
width: '90vw',
maxWidth: '1400px',
marginBottom: '20px',
borderRadius: borderRadius.xlarge,
}"
>
<NFlex vertical>
<NFlex justify="center" align="center" style="margin-bottom: 30px;">
<div class="section-header">
@@ -457,14 +486,16 @@ onMounted(async () => {
</NFlex>
<NFlex :wrap="true" justify="center" style="gap: 20px;">
<NCard :style="{
width: '380px',
maxWidth: '100%',
background: cardBgMedium,
border: borderSystem.light,
borderRadius: borderRadius.large,
boxShadow: 'none',
}" hoverable class="feature-card">
<NCard
:style="{
width: '380px',
maxWidth: '100%',
background: cardBgMedium,
border: borderSystem.light,
borderRadius: borderRadius.large,
boxShadow: 'none',
}" hoverable class="feature-card"
>
<NFlex vertical>
<NFlex align="center" style="margin-bottom: 10px;">
<div class="icon-wrapper">
@@ -480,14 +511,16 @@ onMounted(async () => {
</NFlex>
</NCard>
<NCard :style="{
width: '380px',
maxWidth: '100%',
background: cardBgMedium,
border: borderSystem.light,
borderRadius: borderRadius.large,
boxShadow: 'none',
}" hoverable class="feature-card">
<NCard
:style="{
width: '380px',
maxWidth: '100%',
background: cardBgMedium,
border: borderSystem.light,
borderRadius: borderRadius.large,
boxShadow: 'none',
}" hoverable class="feature-card"
>
<NFlex vertical>
<NFlex align="center" style="margin-bottom: 10px;">
<div class="icon-wrapper">
@@ -506,19 +539,25 @@ onMounted(async () => {
<NFlex justify="center" style="margin-top: 20px;">
<NSpace>
<NButton type="primary" tag="a" href="https://www.wolai.com/carN6qvUm3FErze9Xo53ii" target="_blank"
:style="{ borderRadius: borderRadius.medium }">
<NButton
type="primary" tag="a" href="https://www.wolai.com/carN6qvUm3FErze9Xo53ii" target="_blank"
:style="{ borderRadius: borderRadius.medium }"
>
<template #icon>
<NIcon :component="Info24Filled" />
</template>
客户端安装说明
</NButton>
<NButton ghost tag="a" href="https://github.com/Megghy/vtsuru-fetvher-client" target="_blank"
color="white" :style="{ borderRadius: borderRadius.medium }">
<NButton
ghost tag="a" href="https://github.com/Megghy/vtsuru-fetvher-client" target="_blank"
color="white" :style="{ borderRadius: borderRadius.medium }"
>
客户端代码
</NButton>
<NButton ghost tag="a" href="https://github.com/Megghy/vtsuru.live/tree/master/src/client" target="_blank"
color="white" :style="{ borderRadius: borderRadius.medium }">
<NButton
ghost tag="a" href="https://github.com/Megghy/vtsuru.live/tree/master/src/client" target="_blank"
color="white" :style="{ borderRadius: borderRadius.medium }"
>
逻辑代码
</NButton>
</NSpace>
@@ -527,15 +566,17 @@ onMounted(async () => {
</NCard>
<!-- 使用本站的主播部分 -->
<NCard :style="{
background: cardBgLight,
backdropFilter: 'blur(10px)',
border: borderSystem.light,
width: '90vw',
maxWidth: '1400px',
borderRadius: borderRadius.xlarge,
boxShadow: 'none',
}">
<NCard
:style="{
background: cardBgLight,
backdropFilter: 'blur(10px)',
border: borderSystem.light,
width: '90vw',
maxWidth: '1400px',
borderRadius: borderRadius.xlarge,
boxShadow: 'none',
}"
>
<NFlex vertical>
<NFlex justify="center" align="center" style="margin-bottom: 30px;">
<div class="section-header">
@@ -559,8 +600,10 @@ onMounted(async () => {
<div v-if="indexData" class="streamers-section">
<!-- 主播卡片网格 -->
<div class="streamers-grid-modern">
<div v-for="streamer in indexData?.streamers" :key="streamer.name" class="streamer-card-modern"
@click="$router.push(`/@${streamer.name}`)">
<div
v-for="streamer in indexData?.streamers" :key="streamer.name" class="streamer-card-modern"
@click="$router.push(`/@${streamer.name}`)"
>
<div class="streamer-avatar-wrapper">
<img :src="`${streamer.avatar}@96w`" referrerpolicy="no-referrer" alt="主播头像">
</div>
@@ -580,32 +623,36 @@ onMounted(async () => {
<NFlex vertical align="center" :size="16" style="margin-top: 32px;">
<div class="more-indicator">
<div class="dots-container">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot" />
<div class="dot" />
<div class="dot" />
</div>
<NText :style="{ color: textColor, fontSize: '0.9rem', fontWeight: 500 }">
还有更多主播正在使用
</NText>
</div>
<NCard :style="{
background: 'rgba(255, 255, 255, 0.03)',
border: borderSystem.light,
borderRadius: borderRadius.medium,
padding: '12px 20px',
maxWidth: '400px'
}" size="small">
<NCard
:style="{
background: 'rgba(255, 255, 255, 0.03)',
border: borderSystem.light,
borderRadius: borderRadius.medium,
padding: '12px 20px',
maxWidth: '400px',
}" size="small"
>
<NFlex align="center" justify="center" :size="8">
<NIcon :component="Info24Filled" size="14" :color="textColorSecondary" />
<NText :style="{ color: textColorSecondary, fontSize: '0.8rem', textAlign: 'center' }">
不想被展示前往
<NButton text size="tiny" :style="{
color: textColor,
fontSize: '0.8rem',
padding: '0 4px',
textDecoration: 'underline'
}" @click="$router.push({ name: 'manage-index', query: { tab: 'setting', setting: 'index' } })">
<NButton
text size="tiny" :style="{
color: textColor,
fontSize: '0.8rem',
padding: '0 4px',
textDecoration: 'underline',
}" @click="$router.push({ name: 'manage-index', query: { tab: 'setting', setting: 'index' } })"
>
设置页面
</NButton>
关闭展示
@@ -621,10 +668,12 @@ onMounted(async () => {
<NFlex justify="center" class="footer">
<span :style="{ color: textColor }">
BY
<NButton tag="a" href="https://space.bilibili.com/10021741" target="_blank" text :style="{
color: isDarkMode ? 'rgb(200, 235, 220)' : 'rgb(215, 245, 230)',
borderRadius: borderRadius.small
}">
<NButton
tag="a" href="https://space.bilibili.com/10021741" target="_blank" text :style="{
color: isDarkMode ? 'rgb(200, 235, 220)' : 'rgb(215, 245, 230)',
borderRadius: borderRadius.small,
}"
>
Megghy
</NButton>
</span>
@@ -807,8 +856,6 @@ onMounted(async () => {
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
.stats-item
padding: 8px 16px;

File diff suppressed because it is too large Load Diff

View File

@@ -206,7 +206,7 @@ async function selectFolder() {
// @ts-ignore
const directoryHandle = await window.showDirectoryPicker({
mode: 'read'
mode: 'read',
})
message.info('正在扫描文件夹...')
@@ -254,7 +254,7 @@ async function selectFolder() {
async function scanDirectory(
directoryHandle: any,
audioFiles: { name: string, file: File, path: string }[],
currentPath: string
currentPath: string,
) {
for await (const entry of directoryHandle.values()) {
const entryPath = currentPath ? `${currentPath}/${entry.name}` : entry.name
@@ -266,8 +266,8 @@ async function scanDirectory(
const file = await entry.getFile()
audioFiles.push({
name: entry.name,
file: file,
path: entryPath
file,
path: entryPath,
})
}
} else if (entry.kind === 'directory') {
@@ -363,7 +363,7 @@ function parseAudioFileName(fileName: string, file: File, filePath: string): Son
// @ts-ignore
_originalFile: file,
// @ts-ignore
_filePath: filePath
_filePath: filePath,
} as SongsInfo
}
@@ -373,7 +373,7 @@ function parseAudioFileName(fileName: string, file: File, filePath: string): Son
function updateFolderSongsOptions(newlyAddedSongs: SongsInfo[] = []) {
folderSongsOptions.value = folderSongs.value.map(s => ({
label: `${s.name} - ${s.author?.join('/') || '未知'}`,
value: s.name + '_' + (s as any)._filePath, // 使用组合键避免重名
value: `${s.name}_${(s as any)._filePath}`, // 使用组合键避免重名
disabled:
songs.value.findIndex(exist => exist.name === s.name) > -1
|| newlyAddedSongs.findIndex(add => add.name === s.name) > -1,
@@ -393,7 +393,7 @@ async function addFolderSongs() {
try {
const songsToAdd = folderSongs.value.filter(s =>
selectedFolderSongs.value.find(select => select === (s.name + '_' + (s as any)._filePath))
selectedFolderSongs.value.find(select => select === (`${s.name}_${(s as any)._filePath}`)),
)
// 注意: 由于歌曲URL是本地Blob URL需要根据实际需求处理
@@ -403,7 +403,7 @@ async function addFolderSongs() {
const result = await addSongs(songsToAdd.map(s => ({
...s,
description: (s.description || '') + ' [注意: 链接为本地文件,刷新页面后可能失效]'
description: `${s.description || ''} [注意: 链接为本地文件,刷新页面后可能失效]`,
})), SongFrom.Custom)
if (result.code === 200) {
@@ -428,10 +428,10 @@ async function addFolderSongs() {
*/
function batchEditFolderSongs(field: 'author' | 'language' | 'tags', value: string[]) {
const selectedSongs = folderSongs.value.filter(s =>
selectedFolderSongs.value.find(select => select === (s.name + '_' + (s as any)._filePath))
selectedFolderSongs.value.find(select => select === (`${s.name}_${(s as any)._filePath}`)),
)
selectedSongs.forEach(song => {
selectedSongs.forEach((song) => {
if (field === 'author') {
song.author = value
} else if (field === 'language') {

View File

@@ -987,23 +987,23 @@ onMounted(() => { })
>
<NSelect
:value="currentGoodsModel.goods.setting?.guardFree?.year"
:options="allowedYearOptions"
placeholder="请选择年份"
@update:value="(v) => {
if (currentGoodsModel.goods.setting?.guardFree) {
currentGoodsModel.goods.setting.guardFree.year = v;
}
}"
:options="allowedYearOptions"
placeholder="请选择年份"
/>
<NSelect
:value="currentGoodsModel.goods.setting?.guardFree?.month"
:options="allowedMonthOptions"
placeholder="请选择月份"
@update:value="(v) => {
if (currentGoodsModel.goods.setting?.guardFree) {
currentGoodsModel.goods.setting.guardFree.month = v;
}
}"
:options="allowedMonthOptions"
placeholder="请选择月份"
/>
</NFlex>

View File

@@ -52,6 +52,12 @@ const defaultSettingPoint: Setting_Point = {
allowSelfCheckIn: false,
requireAuth: false,
shouldDiscontinueWhenSoldOut: false,
enableDailyFirstDanmaku: false,
dailyFirstDanmakuPoints: 5,
enableDailyFirstGift: false,
dailyFirstGiftPoints: 10,
useDailyFirstGiftPercent: false,
dailyFirstGiftPercent: 0.1,
}
// 响应式设置对象
@@ -407,6 +413,131 @@ async function SaveComboSetting() {
</NFlex>
</template>
<!-- 每日首次互动奖励设置 -->
<NDivider>每日首次互动奖励</NDivider>
<NFlex
vertical
:gap="12"
class="settings-section"
>
<NAlert
type="info"
closable
>
每日首次发送弹幕或礼物时可以给予额外积分每个用户每天只能获得一次
</NAlert>
<!-- 每日首次弹幕奖励 -->
<NFlex
align="center"
:gap="12"
>
<NCheckbox
v-model:checked="setting.enableDailyFirstDanmaku"
:disabled="!canEdit"
@update:checked="updateSettings"
>
启用每日首次弹幕奖励
</NCheckbox>
</NFlex>
<NInputGroup
v-if="setting.enableDailyFirstDanmaku"
class="input-group-wide"
:disabled="!canEdit"
>
<NInputGroupLabel> 每日首次弹幕积分 </NInputGroupLabel>
<NInputNumber
v-model:value="setting.dailyFirstDanmakuPoints"
:disabled="!canEdit"
min="0"
/>
<NButton
type="info"
:disabled="!canEdit"
@click="updateSettings"
>
确定
</NButton>
</NInputGroup>
<!-- 每日首次礼物奖励 -->
<NFlex
align="center"
:gap="12"
>
<NCheckbox
v-model:checked="setting.enableDailyFirstGift"
:disabled="!canEdit"
@update:checked="updateSettings"
>
启用每日首次礼物奖励
</NCheckbox>
</NFlex>
<template v-if="setting.enableDailyFirstGift">
<NRadioGroup
v-model:value="setting.useDailyFirstGiftPercent"
@update:value="updateSettings"
>
<NRadioButton :value="false">
固定积分
</NRadioButton>
<NRadioButton :value="true">
按礼物价值比例
</NRadioButton>
</NRadioGroup>
<NInputGroup
v-if="!setting.useDailyFirstGiftPercent"
class="input-group-wide"
:disabled="!canEdit"
>
<NInputGroupLabel> 固定积分数量 </NInputGroupLabel>
<NInputNumber
v-model:value="setting.dailyFirstGiftPoints"
:disabled="!canEdit"
min="0"
/>
<NButton
type="info"
:disabled="!canEdit"
@click="updateSettings"
>
确定
</NButton>
</NInputGroup>
<NInputGroup
v-else
class="input-group-wide"
:disabled="!canEdit"
>
<NInputGroupLabel> 礼物价值比例 </NInputGroupLabel>
<NInputNumber
v-model:value="setting.dailyFirstGiftPercent"
:disabled="!canEdit"
min="0"
step="0.01"
max="1"
/>
<NButton
type="info"
:disabled="!canEdit"
@click="updateSettings"
>
确定
<NTooltip>
<template #trigger>
<NIcon :component="Info24Filled" />
</template>
例如设置0.1送10元礼物获得1积分免费礼物不给予积分
</NTooltip>
</NButton>
</NInputGroup>
</template>
</NFlex>
<!-- 礼物设置区域 -->
<template v-if="setting.allowType.includes(EventDataTypes.Gift)">
<NDivider>礼物设置</NDivider>

File diff suppressed because it is too large Load Diff

View File

@@ -144,41 +144,47 @@ onUnmounted(() => {
:class="{ animating: isMoreThanContainer }"
:style="`width: ${width}px; --item-parent-width: ${width}px`"
>
<div
v-for="(song, index) in activeSongs"
:key="song.id"
class="live-request-list-item"
:from="song.from as number"
:status="song.status as number"
<TransitionGroup
name="live-request-transition"
tag="div"
class="live-request-transition-group"
>
<div
class="live-request-list-item-index"
:index="index + 1"
v-for="(song, index) in activeSongs"
:key="song.id"
class="live-request-list-item"
:from="song.from as number"
:status="song.status as number"
>
{{ index + 1 }}
</div>
<div class="live-request-list-item-scroll-view">
<div class="live-request-list-item-inner-scroll">
<div class="live-request-list-item-song-name">
{{ song.songName || '未知歌曲' }}
</div>
<div
v-if="settings.showUserName"
class="live-request-list-item-name"
:from="song.from as number"
>
{{ song.from == SongRequestFrom.Manual ? '主播添加' : song.user?.name || '未知用户' }}
</div>
<div
v-if="settings.showFanMadelInfo && (song.user?.fans_medal_level ?? 0) > 0"
class="live-request-list-item-level"
:has-level="(song.user?.fans_medal_level ?? 0) > 0"
>
{{ `${song.user?.fans_medal_name || ''} ${song.user?.fans_medal_level || ''}` }}
<div
class="live-request-list-item-index"
:index="index + 1"
>
{{ index + 1 }}
</div>
<div class="live-request-list-item-scroll-view">
<div class="live-request-list-item-inner-scroll">
<div class="live-request-list-item-song-name">
{{ song.songName || '未知歌曲' }}
</div>
<div
v-if="settings.showUserName"
class="live-request-list-item-name"
:from="song.from as number"
>
{{ song.from == SongRequestFrom.Manual ? '主播添加' : song.user?.name || '未知用户' }}
</div>
<div
v-if="settings.showFanMadelInfo && (song.user?.fans_medal_level ?? 0) > 0"
class="live-request-list-item-level"
:has-level="(song.user?.fans_medal_level ?? 0) > 0"
>
{{ `${song.user?.fans_medal_name || ''} ${song.user?.fans_medal_level || ''}` }}
</div>
</div>
</div>
</div>
</div>
</TransitionGroup>
</div>
</template>
<div
@@ -398,6 +404,33 @@ onUnmounted(() => {
position: relative;
}
.live-request-transition-group {
display: flex;
flex-direction: column;
gap: 5px;
position: relative;
}
.live-request-transition-enter-active,
.live-request-transition-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.live-request-transition-enter-from,
.live-request-transition-leave-to {
opacity: 0;
transform: translateY(10px);
}
.live-request-transition-leave-active {
position: absolute;
width: 100%;
}
.live-request-transition-move {
transition: transform 0.3s ease;
}
@keyframes vertical-ping-pong {
0% {
transform: translateY(0);
@@ -426,7 +459,7 @@ onUnmounted(() => {
position: relative;
align-items: center;
padding: 4px 6px;
margin-bottom: 5px;
margin-bottom: 0;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 6px;
min-height: 36px;

View File

@@ -156,47 +156,51 @@ onUnmounted(() => {
class="fresh-request-list-container"
>
<template v-if="activeSongs.length > 0">
<!-- Removed Vue3Marquee -->
<!-- Add a wrapper div for animation -->
<div
ref="songListInnerRef"
class="fresh-request-song-list-inner"
:class="{ animating: isMoreThanContainer }"
>
<div
v-for="(song, index) in activeSongs"
:key="song.id"
class="fresh-request-song-item"
<TransitionGroup
name="fresh-request-transition"
tag="div"
class="fresh-request-transition-group"
>
<div
class="fresh-request-song-rank"
:class="[`rank-${index + 1}`, { 'rank-top-3': index < 3 }]"
v-for="(song, index) in activeSongs"
:key="song.id"
class="fresh-request-song-item"
>
{{ index + 1 }}
</div>
<div class="fresh-request-song-content">
<div
class="fresh-request-song-name"
:title="song.songName"
class="fresh-request-song-rank"
:class="[`rank-${index + 1}`, { 'rank-top-3': index < 3 }]"
>
{{ song.songName }}
{{ index + 1 }}
</div>
<div class="fresh-request-song-footer">
<span
v-if="settings.showUserName"
class="fresh-request-song-requester"
<div class="fresh-request-song-content">
<div
class="fresh-request-song-name"
:title="song.songName"
>
<span class="requester-label">点歌:</span> {{ song.from === SongRequestFrom.Manual ? '主播' : song.user?.name }}
</span>
<span
v-if="settings.showFanMadelInfo && (song.user?.fans_medal_level ?? 0) > 0"
class="fresh-request-song-medal"
>
{{ song.user?.fans_medal_name }} {{ song.user?.fans_medal_level }}
</span>
{{ song.songName }}
</div>
<div class="fresh-request-song-footer">
<span
v-if="settings.showUserName"
class="fresh-request-song-requester"
>
<span class="requester-label">点歌:</span> {{ song.from === SongRequestFrom.Manual ? '主播' : song.user?.name }}
</span>
<span
v-if="settings.showFanMadelInfo && (song.user?.fans_medal_level ?? 0) > 0"
class="fresh-request-song-medal"
>
{{ song.user?.fans_medal_name }} {{ song.user?.fans_medal_level }}
</span>
</div>
</div>
</div>
</div>
</TransitionGroup>
</div>
<!-- End animation wrapper -->
</template>

View File

@@ -31,15 +31,16 @@ import {
import { computed, defineAsyncComponent, h, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { controllerBodies, controllerStructures, gamepadConfigs } from '@/data/gamepadConfigs'
import { useGamepadStore } from '@/store/useGamepadStore'
const props = withDefaults(defineProps<Props>(), {
viewBox: '',
})
const GamepadDisplay = defineAsyncComponent(() => import('./GamepadDisplay.vue'))
interface Props {
viewBox?: string
}
const props = withDefaults(defineProps<Props>(), {
viewBox: '',
})
// 基本设置
const selectedType = useStorage<GamepadType>('Setting.Gamepad.SelectedType', 'xbox')
const currentGamepadType = computed(() => selectedType.value)

View File

@@ -729,7 +729,7 @@ const columns = computed<DataTableColumns<ResponseQueueModel>>(() => [
{
title: '时间',
key: 'createAt', // 使用 createAt 作为 key 以便排序
sorter: 'default', // 使用 NDataTable 内置排序
sorter: true,
render: (data) => {
return h(NTime, { time: data.createAt, type: 'datetime' }) // 显示完整时间
},

View File

@@ -18,8 +18,8 @@ import {
NCollapseItem,
NDivider,
NEmpty,
NGrid,
NGi,
NGrid,
NIcon,
NInput,
NInputGroup,
@@ -41,13 +41,13 @@ import {
NTooltip,
useMessage,
} from 'naive-ui'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useAccount } from '@/api/account'
import { EventDataTypes } from '@/api/api-models'
import { useDanmakuClient } from '@/store/useDanmakuClient'
import { templateConstants, useSpeechService } from '@/store/useSpeechService'
import { copyToClipboard } from '@/Utils'
import { TTS_API_URL } from '@/data/constants';
import { TTS_API_URL } from '@/data/constants'
const props = defineProps<{
roomInfo?: any
@@ -70,11 +70,11 @@ const {
} = speechService
// Azure 语音列表
const azureVoices = ref<Array<{ label: string; value: string; locale: string }>>([])
const azureVoices = ref<Array<{ label: string, value: string, locale: string }>>([])
const azureVoicesLoading = ref(false)
// 音频输出设备列表
const audioOutputDevices = ref<Array<{ label: string; value: string }>>([])
const audioOutputDevices = ref<Array<{ label: string, value: string }>>([])
const audioOutputDevicesLoading = ref(false)
// 计算属性
@@ -423,7 +423,9 @@ onUnmounted(() => {
</template>
例如 Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland)各种营销号就用的这些配音
</NTooltip>
系列语音效果<NText strong>好很多</NText>
系列语音效果<NText strong>
好很多
</NText>
</NAlert>
<NAlert
@@ -433,7 +435,9 @@ onUnmounted(() => {
<template #icon>
<NIcon :component="Info24Filled" />
</template>
<NText strong>重要</NText> 当在后台运行时请关闭浏览器的页面休眠/内存节省功能
<NText strong>
重要
</NText> 当在后台运行时请关闭浏览器的页面休眠/内存节省功能
<NDivider vertical />
<NButton
tag="a"
@@ -774,7 +778,9 @@ onUnmounted(() => {
<!-- 输出设备选择 -->
<div>
<NSpace justify="space-between" align="center">
<NText strong>输出设备</NText>
<NText strong>
输出设备
</NText>
<NButton
v-if="audioOutputDevices.length === 0"
text
@@ -873,7 +879,9 @@ onUnmounted(() => {
:size="16"
>
<div>
<NText strong>选择语音</NText>
<NText strong>
选择语音
</NText>
<NSelect
v-model:value="settings.speechInfo.voice"
:options="voiceOptions"
@@ -962,7 +970,9 @@ onUnmounted(() => {
<div>
<NSpace justify="space-between" align="center">
<NText strong>语音选择</NText>
<NText strong>
语音选择
</NText>
<NButton
v-if="azureVoices.length === 0"
text
@@ -1125,7 +1135,9 @@ onUnmounted(() => {
</NAlert>
<div>
<NText strong>API 地址</NText>
<NText strong>
API 地址
</NText>
<NInputGroup style="margin-top: 8px">
<NSelect
v-model:value="settings.voiceAPISchemeType"

File diff suppressed because it is too large Load Diff

View File

@@ -288,10 +288,37 @@ async function buyGoods() {
message.success('兑换成功')
// 更新本地积分显示
currentPoint.value = Number((currentPoint.value - currentGoods.value!.price * buyCount.value).toFixed(1))
// 构建对话框内容
const isVirtualGoods = data.data.type === GoodsTypes.Virtual
const hasContent = data.data.goods.content
// 显示成功对话框
dialog.success({
title: '成功',
content: `兑换成功,订单号:${data.data.id}`,
content: () => {
const elements: any[] = [
h(NText, null, { default: () => `兑换成功,订单号:${data.data.id}` }),
]
// 如果是虚拟礼物且有内容,则显示礼物内容
if (isVirtualGoods && hasContent) {
elements.push(
h(NDivider, { style: 'margin: 16px 0;' }, { default: () => '礼物内容' }),
h(
NAlert,
{
type: 'success',
bordered: false,
style: 'white-space: pre-wrap; word-break: break-word;',
},
{ default: () => data.data.goods.content },
),
)
}
return h(NFlex, { vertical: true, size: 'small' }, { default: () => elements })
},
positiveText: '前往查看',
negativeText: '关闭',
onPositiveClick: () => {

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ResponsePointOrder2UserModel } from '@/api/api-models'
import { NButton, NCard, NDataTable, NEmpty, NFlex, NSelect, NSpin, NTag, useMessage } from 'naive-ui'
import { NButton, NCard, NEmpty, NFlex, NSelect, NSpin, useMessage } from 'naive-ui'
import { computed, onMounted, ref } from 'vue'
import { PointOrderStatus } from '@/api/api-models'
import PointOrderCard from '@/components/manage/PointOrderCard.vue'

View File

@@ -2,7 +2,7 @@
import type { ResponsePointHisrotyModel } from '@/api/api-models'
import { format } from 'date-fns'
import { saveAs } from 'file-saver'
import { NButton, NCard, NDatePicker, NEmpty, NFlex, NRadioButton, NRadioGroup, NSelect, NSpin, NStatistic, useMessage } from 'naive-ui'
import { NButton, NCard, NDatePicker, NEmpty, NFlex, NRadioButton, NRadioGroup, NSelect, NSpin, useMessage } from 'naive-ui'
import { computed, onMounted, ref } from 'vue'
import { PointFrom } from '@/api/api-models'
import PointHistoryCard from '@/components/manage/PointHistoryCard.vue'

File diff suppressed because it is too large Load Diff

View File

@@ -3,16 +3,16 @@
<script setup lang="ts">
import type { WritableComputedRef } from 'vue'
import type { ScheduleWeekInfo, UploadFileResponse } from '@/api/api-models';
import type { ScheduleWeekInfo, UploadFileResponse } from '@/api/api-models'
import type { ScheduleConfigTypeWithConfig } from '@/data/TemplateTypes' // Use base type
import type { ExtractConfigData, RGBAColor } from '@/data/VTsuruConfigTypes'
import { getWeek, getYear } from 'date-fns'
import { NDivider, NSelect, NSpace, useMessage } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { ScheduleDayInfo } from '@/api/api-models'
import SaveCompoent from '@/components/SaveCompoent.vue' // 引入截图组件
import { defineTemplateConfig, rgbaToString } from '@/data/VTsuruConfigTypes'
const props = defineProps<ScheduleConfigTypeWithConfig<KawaiiConfigType>>()
const Config = defineTemplateConfig([
{
name: '背景图', // Removed 'as const'
@@ -73,8 +73,6 @@ const Config = defineTemplateConfig([
])
type KawaiiConfigType = ExtractConfigData<typeof Config>
const props = defineProps<ScheduleConfigTypeWithConfig<KawaiiConfigType>>()
// Get message instance
const message = useMessage()