mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
chore: remove unused steering docs and update point settings model
This commit is contained in:
@@ -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请求获取和更新。
|
||||
@@ -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构建配置
|
||||
@@ -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. 主播可以管理队列:开始演唱、标记完成、取消请求等
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
inclusion: always
|
||||
---
|
||||
@@ -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和组件
|
||||
@@ -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`组织不同功能区域
|
||||
- 使用状态颜色区分不同状态(如等待中、处理中、已完成)
|
||||
- 响应式设计适应不同屏幕尺寸
|
||||
@@ -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",
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
<ReadDanmaku />
|
||||
</template>
|
||||
@@ -211,8 +211,7 @@ const separatorOptions = [
|
||||
>
|
||||
<NGi>
|
||||
<NFormItem label="背景颜色">
|
||||
<NColorPicker
|
||||
/>
|
||||
<NColorPicker />
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
<NGi>
|
||||
|
||||
@@ -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: '目标',
|
||||
|
||||
@@ -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: '已认证',
|
||||
|
||||
@@ -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) {
|
||||
// 找到了表情
|
||||
|
||||
@@ -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]')
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
10
src/components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} 首歌曲`)
|
||||
|
||||
@@ -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 }, () => '兑换'),
|
||||
|
||||
@@ -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'
|
||||
// 导入事件模型和类型枚举
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
export default {
|
||||
path: '/client',
|
||||
name: 'client',
|
||||
component: RouterView,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
export default // 管理页面
|
||||
{
|
||||
path: '/manage',
|
||||
name: 'manage',
|
||||
component: RouterView,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
export default {
|
||||
path: '/obs',
|
||||
name: 'obs',
|
||||
component: RouterView,
|
||||
children: [
|
||||
{
|
||||
path: 'live-lottery',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
export default {
|
||||
path: '/obs-store',
|
||||
name: 'obs-store',
|
||||
component: RouterView,
|
||||
children: [
|
||||
{
|
||||
path: 'gamepad-manage',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
export default {
|
||||
path: '/open-live',
|
||||
name: 'open-live',
|
||||
component: RouterView,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
@@ -92,4 +94,4 @@ export default [
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
] satisfies RouteRecordRaw[]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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: '弹幕客户端已启动' }
|
||||
|
||||
@@ -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
@@ -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') {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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' }) // 显示完整时间
|
||||
},
|
||||
|
||||
@@ -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
@@ -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: () => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user