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,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import ReadDanmaku from '@/views/open_live/ReadDanmaku.vue';
|
||||
|
||||
import ReadDanmaku from '@/views/open_live/ReadDanmaku.vue'
|
||||
</script>
|
||||
|
||||
<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 ref="containerRef" :style="`height: ${height}px; width: 100%;`"></div>
|
||||
</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 为空则不渲染
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
@@ -44,7 +45,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
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('MESSAGE', (data) => {
|
||||
switch (data.data.cmd) {
|
||||
case 'INTERACT_WORD_V2':
|
||||
this.onEnter(data.data)
|
||||
|
||||
@@ -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)',
|
||||
}))
|
||||
|
||||
// 功能图标颜色映射 - 优化为统一的色系,与背景渐变协调
|
||||
@@ -203,7 +203,7 @@ const iconColors = computed(() => {
|
||||
mint: '#20C997', // 薄荷绿
|
||||
lavender: '#B794F6', // 薰衣草紫
|
||||
coral: '#FF6B6B', // 珊瑚色
|
||||
sage: '#8FBC8F' // 鼠尾草绿
|
||||
sage: '#8FBC8F', // 鼠尾草绿
|
||||
} : {
|
||||
// 亮色模式:更鲜艳的色调,保持活力
|
||||
teal: '#2EBFA5', // 青绿色 - 与背景起始色呼应
|
||||
@@ -217,7 +217,7 @@ const iconColors = computed(() => {
|
||||
mint: '#14B8A6', // 薄荷绿
|
||||
lavender: '#A855F7', // 薰衣草紫
|
||||
coral: '#EF4444', // 珊瑚色
|
||||
sage: '#22C55E' // 鼠尾草绿
|
||||
sage: '#22C55E', // 鼠尾草绿
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -238,7 +238,7 @@ const iconColors = computed(() => {
|
||||
})
|
||||
|
||||
// 处理功能卡片点击
|
||||
const handleFunctionClick = (item: typeof functions[0]) => {
|
||||
function handleFunctionClick(item: typeof functions[0]) {
|
||||
if (item.route) {
|
||||
// 跳转到对应的管理页面
|
||||
$router.push({ name: item.route })
|
||||
@@ -257,38 +257,47 @@ onMounted(async () => {
|
||||
<div class="index-background">
|
||||
<NSpace vertical justify="center" align="center" class="main-container">
|
||||
<!-- 顶部标题部分 -->
|
||||
<NCard :style="{
|
||||
<NCard
|
||||
:style="{
|
||||
background: cardBgLight,
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: 'none',
|
||||
width: '90vw',
|
||||
maxWidth: '1400px',
|
||||
borderRadius: borderRadius.xlarge,
|
||||
}" class="hero-card">
|
||||
}" 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="{
|
||||
<NGradientText
|
||||
:size="width > 700 ? '3rem' : '2.5rem'" :gradient="{
|
||||
deg: 180,
|
||||
...gradientColors,
|
||||
}" style="font-weight: 700">
|
||||
}" style="font-weight: 700"
|
||||
>
|
||||
VTSURU.LIVE
|
||||
</NGradientText>
|
||||
<NText :style="{
|
||||
<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="{
|
||||
<NCard
|
||||
hoverable :style="{
|
||||
width: width > 700 ? '240px' : '100%',
|
||||
minWidth: '200px',
|
||||
background: cardBgMedium,
|
||||
@@ -296,7 +305,8 @@ onMounted(async () => {
|
||||
border: 'none',
|
||||
borderRadius: borderRadius.large,
|
||||
transition: 'all 0.3s ease',
|
||||
}" class="entry-card" @click="$router.push({ name: 'manage-index' })">
|
||||
}" 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,7 +324,8 @@ onMounted(async () => {
|
||||
<!-- 观众入口 -->
|
||||
<NTooltip placement="bottom">
|
||||
<template #trigger>
|
||||
<NCard hoverable :style="{
|
||||
<NCard
|
||||
hoverable :style="{
|
||||
width: width > 700 ? '240px' : '100%',
|
||||
minWidth: '200px',
|
||||
background: cardBgMedium,
|
||||
@@ -322,7 +333,8 @@ onMounted(async () => {
|
||||
border: 'none',
|
||||
borderRadius: borderRadius.large,
|
||||
transition: 'all 0.3s ease',
|
||||
}" class="entry-card" @click="$router.push({ name: 'bili-user' })">
|
||||
}" 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="{
|
||||
<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">
|
||||
}" 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,7 +401,8 @@ onMounted(async () => {
|
||||
</NCard>
|
||||
|
||||
<!-- 功能列表部分 -->
|
||||
<NCard :style="{
|
||||
<NCard
|
||||
:style="{
|
||||
background: cardBgLight,
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: 'none',
|
||||
@@ -388,7 +410,8 @@ onMounted(async () => {
|
||||
maxWidth: '1400px',
|
||||
marginBottom: '20px',
|
||||
borderRadius: borderRadius.xlarge,
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<NFlex vertical>
|
||||
<NFlex justify="center" align="center" style="margin-bottom: 30px;">
|
||||
<div class="section-header">
|
||||
@@ -404,7 +427,8 @@ onMounted(async () => {
|
||||
</NFlex>
|
||||
|
||||
<NFlex :wrap="true" justify="center" style="gap: 15px;">
|
||||
<NCard v-for="item in functions" :key="item.name" :style="{
|
||||
<NCard
|
||||
v-for="item in functions" :key="item.name" :style="{
|
||||
width: '300px',
|
||||
maxWidth: '100%',
|
||||
background: cardBgMedium,
|
||||
@@ -412,12 +436,15 @@ onMounted(async () => {
|
||||
borderRadius: borderRadius.large,
|
||||
boxShadow: 'none',
|
||||
cursor: item.route ? 'pointer' : 'default',
|
||||
}" hoverable class="feature-card" @click="handleFunctionClick(item)">
|
||||
}" 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,7 +460,8 @@ onMounted(async () => {
|
||||
</NCard>
|
||||
|
||||
<!-- 客户端专属功能部分 -->
|
||||
<NCard :style="{
|
||||
<NCard
|
||||
:style="{
|
||||
background: cardBgLight,
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: 'none',
|
||||
@@ -441,7 +469,8 @@ onMounted(async () => {
|
||||
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="{
|
||||
<NCard
|
||||
:style="{
|
||||
width: '380px',
|
||||
maxWidth: '100%',
|
||||
background: cardBgMedium,
|
||||
border: borderSystem.light,
|
||||
borderRadius: borderRadius.large,
|
||||
boxShadow: 'none',
|
||||
}" hoverable class="feature-card">
|
||||
}" 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="{
|
||||
<NCard
|
||||
:style="{
|
||||
width: '380px',
|
||||
maxWidth: '100%',
|
||||
background: cardBgMedium,
|
||||
border: borderSystem.light,
|
||||
borderRadius: borderRadius.large,
|
||||
boxShadow: 'none',
|
||||
}" hoverable class="feature-card">
|
||||
}" 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,7 +566,8 @@ onMounted(async () => {
|
||||
</NCard>
|
||||
|
||||
<!-- 使用本站的主播部分 -->
|
||||
<NCard :style="{
|
||||
<NCard
|
||||
:style="{
|
||||
background: cardBgLight,
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: borderSystem.light,
|
||||
@@ -535,7 +575,8 @@ onMounted(async () => {
|
||||
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="{
|
||||
<NCard
|
||||
:style="{
|
||||
background: 'rgba(255, 255, 255, 0.03)',
|
||||
border: borderSystem.light,
|
||||
borderRadius: borderRadius.medium,
|
||||
padding: '12px 20px',
|
||||
maxWidth: '400px'
|
||||
}" size="small">
|
||||
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="{
|
||||
<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' } })">
|
||||
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="{
|
||||
<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
|
||||
}">
|
||||
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;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import {
|
||||
BookCoins20Filled,
|
||||
CalendarClock24Filled,
|
||||
Chat24Filled,
|
||||
Info24Filled,
|
||||
Live24Filled,
|
||||
Lottery24Filled,
|
||||
@@ -13,9 +12,9 @@
|
||||
TabletSpeaker24Filled,
|
||||
VehicleShip24Filled,
|
||||
VideoAdd20Filled,
|
||||
} from '@vicons/fluent';
|
||||
import { AnalyticsSharp, Bookmark, BookmarkOutline, BrowsersOutline, Chatbox, ChevronDown, ChevronUp, Eye, Moon, MusicalNote, Pause, Play, PlayBack, PlayForward, Sunny, TrashBin, VolumeHigh } from '@vicons/ionicons5';
|
||||
import { useElementSize, useStorage } from '@vueuse/core';
|
||||
} from '@vicons/fluent'
|
||||
import { AnalyticsSharp, Bookmark, BookmarkOutline, BrowsersOutline, Chatbox, ChevronDown, ChevronUp, Eye, Moon, MusicalNote, Pause, Play, PlayBack, PlayForward, Sunny, TrashBin, VolumeHigh } from '@vicons/ionicons5'
|
||||
import { useElementSize, useStorage } from '@vueuse/core'
|
||||
import {
|
||||
NAlert,
|
||||
NBackTop,
|
||||
@@ -43,42 +42,42 @@
|
||||
NText,
|
||||
NTooltip,
|
||||
useMessage,
|
||||
} from 'naive-ui';
|
||||
import { computed, h, onMounted, ref, watch } from 'vue';
|
||||
} from 'naive-ui'
|
||||
import { computed, h, onMounted, ref, watch } from 'vue'
|
||||
// @ts-ignore
|
||||
import APlayer from 'vue3-aplayer';
|
||||
import { RouterLink, useRoute } from 'vue-router';
|
||||
import { cookie, isLoadingAccount, useAccount } from '@/api/account';
|
||||
import { ThemeType } from '@/api/api-models';
|
||||
import { QueryGetAPI } from '@/api/query';
|
||||
import RegisterAndLogin from '@/components/RegisterAndLogin.vue';
|
||||
import { ACCOUNT_API_URL } from '@/data/constants';
|
||||
import { useBiliAuth } from '@/store/useBiliAuth';
|
||||
import { useMusicRequestProvider } from '@/store/useMusicRequest';
|
||||
import { isDarkMode, NavigateToNewTab } from '@/Utils';
|
||||
import APlayer from 'vue3-aplayer'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { cookie, isLoadingAccount, useAccount } from '@/api/account'
|
||||
import { ThemeType } from '@/api/api-models'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
|
||||
import { ACCOUNT_API_URL } from '@/data/constants'
|
||||
import { useBiliAuth } from '@/store/useBiliAuth'
|
||||
import { useMusicRequestProvider } from '@/store/useMusicRequest'
|
||||
import { isDarkMode, NavigateToNewTab } from '@/Utils'
|
||||
|
||||
// 全局状态和工具
|
||||
const accountInfo = useAccount();
|
||||
const message = useMessage();
|
||||
const route = useRoute();
|
||||
const windowWidth = window.innerWidth;
|
||||
const themeType = useStorage('Settings.Theme', ThemeType.Auto);
|
||||
const accountInfo = useAccount()
|
||||
const message = useMessage()
|
||||
const route = useRoute()
|
||||
const windowWidth = window.innerWidth
|
||||
const themeType = useStorage('Settings.Theme', ThemeType.Auto)
|
||||
|
||||
// 收藏功能相关
|
||||
const favoriteMenuItems = useStorage<string[]>('Settings.FavoriteMenuItems', []);
|
||||
const isFavorite = (key: string) => favoriteMenuItems.value?.includes(key);
|
||||
const favoriteMenuItems = useStorage<string[]>('Settings.FavoriteMenuItems', [])
|
||||
const isFavorite = (key: string) => favoriteMenuItems.value?.includes(key)
|
||||
function toggleFavorite(key: string) {
|
||||
const list = favoriteMenuItems.value ?? [];
|
||||
const idx = list.indexOf(key);
|
||||
if (idx === -1) list.unshift(key);
|
||||
else list.splice(idx, 1);
|
||||
favoriteMenuItems.value = [...list];
|
||||
const list = favoriteMenuItems.value ?? []
|
||||
const idx = list.indexOf(key)
|
||||
if (idx === -1) list.unshift(key)
|
||||
else list.splice(idx, 1)
|
||||
favoriteMenuItems.value = [...list]
|
||||
}
|
||||
function renderFavoriteExtra(key: string) {
|
||||
return () => {
|
||||
// 侧边栏收起时不显示收藏按钮
|
||||
if (width.value < 150) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return h(
|
||||
'span',
|
||||
@@ -96,8 +95,8 @@
|
||||
size: 'tiny',
|
||||
circle: true,
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(key);
|
||||
e.stopPropagation()
|
||||
toggleFavorite(key)
|
||||
},
|
||||
style: 'padding: 0; height: 18px; width: 18px;',
|
||||
},
|
||||
@@ -114,52 +113,52 @@
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏和布局相关
|
||||
const sider = ref();
|
||||
const { width } = useElementSize(sider);
|
||||
const musicPlayerCardRef = ref(null);
|
||||
const { height: musicPlayerCardHeight } = useElementSize(musicPlayerCardRef);
|
||||
const sider = ref()
|
||||
const { width } = useElementSize(sider)
|
||||
const musicPlayerCardRef = ref(null)
|
||||
const { height: musicPlayerCardHeight } = useElementSize(musicPlayerCardRef)
|
||||
|
||||
// 菜单组展开状态
|
||||
const expandedKeys = useStorage<string[]>('Settings.MenuExpandedKeys', [
|
||||
'manage-danmaku',
|
||||
]);
|
||||
])
|
||||
|
||||
// 页面类型计算
|
||||
const type = computed(() => route.meta.danmaku ? 'danmaku' : '');
|
||||
const type = computed(() => route.meta.danmaku ? 'danmaku' : '')
|
||||
|
||||
// 音乐请求服务相关
|
||||
const musicRquestStore = useMusicRequestProvider();
|
||||
const musicRquestStore = useMusicRequestProvider()
|
||||
|
||||
// 优化音乐播放器高度计算逻辑
|
||||
const aplayerHeight = computed(() => {
|
||||
if (!isPlayerVisible.value) {
|
||||
return '0';
|
||||
return '0'
|
||||
}
|
||||
// Add 16px for NCard's top/bottom margin.
|
||||
return `${musicPlayerCardHeight.value + 16}`;
|
||||
});
|
||||
return `${musicPlayerCardHeight.value + 16}`
|
||||
})
|
||||
|
||||
// 播放器是否可见
|
||||
const isPlayerVisible = computed(
|
||||
() => musicRquestStore.originMusics.length > 0 || musicRquestStore.waitingMusics.length > 0,
|
||||
);
|
||||
)
|
||||
|
||||
// 音乐播放器相关状态
|
||||
const isPlayerMinimized = useStorage('Settings.MusicPlayer.Minimized', false);
|
||||
const isPlayerMinimized = useStorage('Settings.MusicPlayer.Minimized', false)
|
||||
const playerVolume = computed({
|
||||
get: () => musicRquestStore.settings.volume,
|
||||
set: value => musicRquestStore.settings.volume = value,
|
||||
});
|
||||
})
|
||||
|
||||
const aplayer = ref();
|
||||
const aplayer = ref()
|
||||
watch(aplayer, () => {
|
||||
musicRquestStore.aplayerRef = aplayer.value;
|
||||
});
|
||||
musicRquestStore.aplayerRef = aplayer.value
|
||||
})
|
||||
|
||||
// 当前播放信息
|
||||
const currentPlayingInfo = computed(() => {
|
||||
@@ -167,22 +166,22 @@
|
||||
return {
|
||||
type: 'request',
|
||||
info: `正在播放 ${musicRquestStore.currentOriginMusic.from.name} 点的歌`,
|
||||
};
|
||||
}
|
||||
} else if (musicRquestStore.currentMusic && musicRquestStore.currentMusic.title) {
|
||||
return {
|
||||
type: 'normal',
|
||||
info: '正在播放背景音乐',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// 邮箱验证相关
|
||||
const canResendEmail = ref(false);
|
||||
const isBiliVerified = computed(() => accountInfo.value?.isBiliVerified);
|
||||
const canResendEmail = ref(false)
|
||||
const isBiliVerified = computed(() => accountInfo.value?.isBiliVerified)
|
||||
|
||||
// 图标渲染函数 - 用于菜单项
|
||||
const renderIcon = (icon: any) => () => h(NIcon, null, { default: () => h(icon) });
|
||||
const renderIcon = (icon: any) => () => h(NIcon, null, { default: () => h(icon) })
|
||||
|
||||
// 菜单配置(支持分组与收藏置顶)
|
||||
const menuOptions = computed(() => {
|
||||
@@ -192,13 +191,13 @@
|
||||
return {
|
||||
...item,
|
||||
children: item.children.map(withFavoriteExtra),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
extra: width.value >= 180 ? renderFavoriteExtra(item.key) : undefined,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 定义基础菜单项 - 参照 Naive UI 官方格式
|
||||
const baseMenuItems = [
|
||||
@@ -207,69 +206,69 @@
|
||||
key: 'manage-history',
|
||||
disabled: accountInfo.value?.isEmailVerified === false,
|
||||
icon: renderIcon(AnalyticsSharp),
|
||||
group: 'common'
|
||||
group: 'common',
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'manage-live' } }, { default: () => '直播记录' }),
|
||||
key: 'manage-live',
|
||||
disabled: accountInfo.value?.isEmailVerified === false,
|
||||
icon: renderIcon(Live24Filled),
|
||||
group: 'common'
|
||||
group: 'common',
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'manage-analyze' } }, { default: () => '直播数据' }),
|
||||
key: 'manage-analyze',
|
||||
disabled: accountInfo.value?.isEmailVerified === false,
|
||||
icon: renderIcon(Eye),
|
||||
group: 'common'
|
||||
group: 'common',
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'manage-event' } }, { default: () => '舰长和SC' }),
|
||||
key: 'manage-event',
|
||||
disabled: accountInfo.value?.isEmailVerified === false,
|
||||
icon: renderIcon(VehicleShip24Filled),
|
||||
group: 'data'
|
||||
group: 'data',
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'manage-point' } }, { default: () => '积分和礼物' }),
|
||||
key: 'manage-point',
|
||||
disabled: accountInfo.value?.isEmailVerified === false,
|
||||
icon: renderIcon(BookCoins20Filled),
|
||||
group: 'data'
|
||||
group: 'data',
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'manage-schedule' } }, { default: () => '日程' }),
|
||||
key: 'manage-schedule',
|
||||
icon: renderIcon(CalendarClock24Filled),
|
||||
disabled: accountInfo.value?.isEmailVerified === false,
|
||||
group: 'tools'
|
||||
group: 'tools',
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'manage-songList' } }, { default: () => '歌单' }),
|
||||
key: 'manage-songList',
|
||||
icon: renderIcon(MusicalNote),
|
||||
disabled: accountInfo.value?.isEmailVerified === false,
|
||||
group: 'tools'
|
||||
group: 'tools',
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'manage-questionBox' } }, { default: () => '棉花糖 (提问箱' }),
|
||||
key: 'manage-questionBox',
|
||||
icon: renderIcon(Chatbox),
|
||||
disabled: accountInfo.value?.isEmailVerified === false,
|
||||
group: 'tools'
|
||||
group: 'tools',
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'manage-videoCollect' } }, { default: () => '视频征集' }),
|
||||
key: 'manage-videoCollect',
|
||||
icon: renderIcon(VideoAdd20Filled),
|
||||
disabled: accountInfo.value?.isEmailVerified === false,
|
||||
group: 'tools'
|
||||
group: 'tools',
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'manage-lottery' } }, { default: () => '动态抽奖' }),
|
||||
key: 'manage-lottery',
|
||||
icon: renderIcon(Lottery24Filled),
|
||||
group: 'tools'
|
||||
group: 'tools',
|
||||
},
|
||||
{
|
||||
label: () => !isBiliVerified.value
|
||||
@@ -285,7 +284,7 @@
|
||||
key: 'manage-danmuji',
|
||||
disabled: !isBiliVerified.value,
|
||||
icon: renderIcon(Lottery24Filled),
|
||||
group: 'danmaku'
|
||||
group: 'danmaku',
|
||||
},
|
||||
{
|
||||
label: () => !isBiliVerified.value
|
||||
@@ -305,7 +304,7 @@
|
||||
key: 'manage-liveRequest',
|
||||
icon: renderIcon(MusicalNote),
|
||||
disabled: !isBiliVerified.value,
|
||||
group: 'danmaku'
|
||||
group: 'danmaku',
|
||||
},
|
||||
{
|
||||
label: () => !isBiliVerified.value
|
||||
@@ -318,7 +317,7 @@
|
||||
key: 'manage-liveLottery',
|
||||
icon: renderIcon(Lottery24Filled),
|
||||
disabled: !isBiliVerified.value,
|
||||
group: 'danmaku'
|
||||
group: 'danmaku',
|
||||
},
|
||||
{
|
||||
label: () => !isBiliVerified.value
|
||||
@@ -338,7 +337,7 @@
|
||||
key: 'manage-musicRequest',
|
||||
icon: renderIcon(MusicalNote),
|
||||
disabled: !isBiliVerified.value,
|
||||
group: 'danmaku'
|
||||
group: 'danmaku',
|
||||
},
|
||||
{
|
||||
label: () => !isBiliVerified.value
|
||||
@@ -351,7 +350,7 @@
|
||||
key: 'manage-liveQueue',
|
||||
icon: renderIcon(PeopleQueue24Filled),
|
||||
disabled: !isBiliVerified.value,
|
||||
group: 'danmaku'
|
||||
group: 'danmaku',
|
||||
},
|
||||
{
|
||||
label: () => !isBiliVerified.value
|
||||
@@ -364,24 +363,24 @@
|
||||
key: 'manage-speech',
|
||||
icon: renderIcon(TabletSpeaker24Filled),
|
||||
disabled: !isBiliVerified.value,
|
||||
group: 'danmaku'
|
||||
group: 'danmaku',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
// 应用收藏功能到所有菜单项
|
||||
const allMenuItems = baseMenuItems.map(withFavoriteExtra);
|
||||
const itemMap = new Map(allMenuItems.map(i => [i.key, i]));
|
||||
const allMenuItems = baseMenuItems.map(withFavoriteExtra)
|
||||
const itemMap = new Map(allMenuItems.map(i => [i.key, i]))
|
||||
|
||||
// 获取收藏列表
|
||||
const favorites = (favoriteMenuItems.value ?? [])
|
||||
.map(k => itemMap.get(k))
|
||||
.filter(Boolean) as any[];
|
||||
.filter(Boolean) as any[]
|
||||
|
||||
// 过滤掉已收藏的项
|
||||
const notFav = (i: any) => !isFavorite(i.key);
|
||||
const notFav = (i: any) => !isFavorite(i.key)
|
||||
|
||||
// 构建分组
|
||||
const groups: any[] = [];
|
||||
const groups: any[] = []
|
||||
|
||||
// 我的收藏分组
|
||||
if (favorites.length > 0) {
|
||||
@@ -389,45 +388,45 @@
|
||||
type: 'group',
|
||||
key: 'group-favorites',
|
||||
label: '我的收藏',
|
||||
children: favorites
|
||||
});
|
||||
children: favorites,
|
||||
})
|
||||
}
|
||||
|
||||
// 常用分组
|
||||
const commonItems = allMenuItems.filter(i => i.group === 'common' && notFav(i));
|
||||
const commonItems = allMenuItems.filter(i => i.group === 'common' && notFav(i))
|
||||
if (commonItems.length > 0) {
|
||||
groups.push({
|
||||
type: 'group',
|
||||
key: 'group-common',
|
||||
label: '常用',
|
||||
children: commonItems
|
||||
});
|
||||
children: commonItems,
|
||||
})
|
||||
}
|
||||
|
||||
// 数据分组
|
||||
const dataItems = allMenuItems.filter(i => i.group === 'data' && notFav(i));
|
||||
const dataItems = allMenuItems.filter(i => i.group === 'data' && notFav(i))
|
||||
if (dataItems.length > 0) {
|
||||
groups.push({
|
||||
type: 'group',
|
||||
key: 'group-data',
|
||||
label: '数据',
|
||||
children: dataItems
|
||||
});
|
||||
children: dataItems,
|
||||
})
|
||||
}
|
||||
|
||||
// 互动与工具分组
|
||||
const toolsItems = allMenuItems.filter(i => i.group === 'tools' && notFav(i));
|
||||
const toolsItems = allMenuItems.filter(i => i.group === 'tools' && notFav(i))
|
||||
if (toolsItems.length > 0) {
|
||||
groups.push({
|
||||
type: 'group',
|
||||
key: 'group-tools',
|
||||
label: '互动与工具',
|
||||
children: toolsItems
|
||||
});
|
||||
children: toolsItems,
|
||||
})
|
||||
}
|
||||
|
||||
// 弹幕相关分组
|
||||
const danmakuItems = allMenuItems.filter(i => i.group === 'danmaku' && notFav(i));
|
||||
const danmakuItems = allMenuItems.filter(i => i.group === 'danmaku' && notFav(i))
|
||||
if (danmakuItems.length > 0) {
|
||||
groups.push({
|
||||
type: 'group',
|
||||
@@ -520,50 +519,50 @@
|
||||
: '你尚未进行 Bilibili 认证, 请前往面板进行绑定'),
|
||||
},
|
||||
),
|
||||
children: danmakuItems
|
||||
});
|
||||
children: danmakuItems,
|
||||
})
|
||||
}
|
||||
|
||||
return groups;
|
||||
});
|
||||
return groups
|
||||
})
|
||||
|
||||
// 重发验证邮件
|
||||
async function resendEmail() {
|
||||
try {
|
||||
const data = await QueryGetAPI(`${ACCOUNT_API_URL}send-verify-email`);
|
||||
const data = await QueryGetAPI(`${ACCOUNT_API_URL}send-verify-email`)
|
||||
if (data.code === 200) {
|
||||
canResendEmail.value = false;
|
||||
message.success('发送成功, 请检查你的邮箱. 如果没有收到, 请检查垃圾邮件');
|
||||
canResendEmail.value = false
|
||||
message.success('发送成功, 请检查你的邮箱. 如果没有收到, 请检查垃圾邮件')
|
||||
if (accountInfo.value && accountInfo.value.nextSendEmailTime) {
|
||||
accountInfo.value.nextSendEmailTime += 1000 * 60;
|
||||
accountInfo.value.nextSendEmailTime += 1000 * 60
|
||||
}
|
||||
} else {
|
||||
message.error(`发送失败: ${data.message}`);
|
||||
message.error(`发送失败: ${data.message}`)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('发送失败');
|
||||
message.error('发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 登出操作
|
||||
function logout() {
|
||||
cookie.value = undefined;
|
||||
window.location.reload();
|
||||
cookie.value = undefined
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
// 播放下一首音乐
|
||||
function onNextMusic() {
|
||||
musicRquestStore.nextMusic();
|
||||
musicRquestStore.nextMusic()
|
||||
}
|
||||
|
||||
// 音乐播放器控制功能
|
||||
function togglePlay() {
|
||||
if (aplayer.value) {
|
||||
const audio = aplayer.value.audio;
|
||||
const audio = aplayer.value.audio
|
||||
if (audio.paused) {
|
||||
aplayer.value.play();
|
||||
aplayer.value.play()
|
||||
} else {
|
||||
aplayer.value.pause();
|
||||
aplayer.value.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,50 +571,50 @@
|
||||
if (aplayer.value) {
|
||||
// 如果当前播放时间大于3秒,则重新开始播放当前歌曲
|
||||
if (aplayer.value.audio.currentTime > 3) {
|
||||
aplayer.value.audio.currentTime = 0;
|
||||
aplayer.value.audio.currentTime = 0
|
||||
} else {
|
||||
// 否则播放上一首
|
||||
const currentIndex = musicRquestStore.aplayerMusics.findIndex(
|
||||
music => music.id === musicRquestStore.currentMusic.id,
|
||||
);
|
||||
)
|
||||
if (currentIndex > 0) {
|
||||
musicRquestStore.currentMusic = musicRquestStore.aplayerMusics[currentIndex - 1];
|
||||
aplayer.value.thenPlay();
|
||||
musicRquestStore.currentMusic = musicRquestStore.aplayerMusics[currentIndex - 1]
|
||||
aplayer.value.thenPlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearWaitingQueue() {
|
||||
musicRquestStore.waitingMusics.splice(0);
|
||||
message.success('已清空等待队列');
|
||||
musicRquestStore.waitingMusics.splice(0)
|
||||
message.success('已清空等待队列')
|
||||
}
|
||||
|
||||
function togglePlayerMinimize() {
|
||||
isPlayerMinimized.value = !isPlayerMinimized.value;
|
||||
isPlayerMinimized.value = !isPlayerMinimized.value
|
||||
}
|
||||
|
||||
// 跳转到认证页面
|
||||
function gotoAuthPage() {
|
||||
if (!accountInfo.value?.biliUserAuthInfo) {
|
||||
message.error('你尚未进行 Bilibili 认证, 请前往面板进行认证和绑定');
|
||||
return;
|
||||
message.error('你尚未进行 Bilibili 认证, 请前往面板进行认证和绑定')
|
||||
return
|
||||
}
|
||||
useBiliAuth()
|
||||
.setCurrentAuth(accountInfo.value?.biliUserAuthInfo.token)
|
||||
.then(() => {
|
||||
NavigateToNewTab('/bili-user');
|
||||
});
|
||||
NavigateToNewTab('/bili-user')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查邮箱验证状态
|
||||
if (accountInfo.value?.isEmailVerified === false) {
|
||||
if ((accountInfo.value?.nextSendEmailTime ?? -1) <= 0) {
|
||||
canResendEmail.value = true;
|
||||
canResendEmail.value = true
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -631,8 +630,10 @@
|
||||
<template #extra>
|
||||
<NSpace align="center" justify="center">
|
||||
<!-- 主题切换开关 -->
|
||||
<NSwitch :default-value="!isDarkMode"
|
||||
@update:value="(value) => (themeType = value ? ThemeType.Light : ThemeType.Dark)">
|
||||
<NSwitch
|
||||
:default-value="!isDarkMode"
|
||||
@update:value="(value) => (themeType = value ? ThemeType.Light : ThemeType.Dark)"
|
||||
>
|
||||
<template #checked>
|
||||
<NIcon :component="Sunny" />
|
||||
</template>
|
||||
@@ -640,8 +641,10 @@
|
||||
<NIcon :component="Moon" />
|
||||
</template>
|
||||
</NSwitch>
|
||||
<NButton size="small" type="primary"
|
||||
@click="$router.push({ name: 'user-index', params: { id: accountInfo?.name } })">
|
||||
<NButton
|
||||
size="small" type="primary"
|
||||
@click="$router.push({ name: 'user-index', params: { id: accountInfo?.name } })"
|
||||
>
|
||||
回到展示页
|
||||
</NButton>
|
||||
</NSpace>
|
||||
@@ -652,9 +655,11 @@
|
||||
<!-- 主布局部分 -->
|
||||
<NLayout has-sider style="height: calc(100vh - 50px)">
|
||||
<!-- 侧边导航栏 -->
|
||||
<NLayoutSider v-if="accountInfo?.isEmailVerified" ref="sider" bordered show-trigger collapse-mode="width"
|
||||
<NLayoutSider
|
||||
v-if="accountInfo?.isEmailVerified" ref="sider" bordered show-trigger collapse-mode="width"
|
||||
:default-collapsed="windowWidth < 750" :collapsed-width="64" :width="180" :native-scrollbar="false"
|
||||
:scrollbar-props="{ trigger: 'none', style: {} }" :class="{ 'sider-collapsed': width < 150 }">
|
||||
:scrollbar-props="{ trigger: 'none', style: {} }" :class="{ 'sider-collapsed': width < 150 }"
|
||||
>
|
||||
<!-- 顶部功能按钮区 -->
|
||||
<NSpace vertical style="margin-top: 16px" align="center">
|
||||
<NSpace justify="center">
|
||||
@@ -689,12 +694,14 @@
|
||||
</NSpace>
|
||||
|
||||
<!-- 主导航菜单 -->
|
||||
<NMenu v-model:expanded-keys="expandedKeys" class="manage-sider-menu" style="margin-top: 12px"
|
||||
<NMenu
|
||||
v-model:expanded-keys="expandedKeys" class="manage-sider-menu" style="margin-top: 12px"
|
||||
:disabled="accountInfo?.isEmailVerified !== true"
|
||||
:default-value="($route.meta.parent as string) ?? $route.name?.toString()"
|
||||
:default-expanded-keys="['group-common', 'group-data', 'group-tools', 'group-danmaku', 'group-favorites']"
|
||||
:collapsed-width="64" :collapsed-icon-size="22" :icon-size="16" :root-indent="10" :indent="12"
|
||||
:options="menuOptions" />
|
||||
:options="menuOptions"
|
||||
/>
|
||||
|
||||
<!-- 底部信息区 -->
|
||||
<NSpace v-if="width > 150" justify="center" align="center" vertical>
|
||||
@@ -713,7 +720,8 @@
|
||||
<NDivider style="margin-bottom: 8px;" />
|
||||
<NFlex justify="center" align="center">
|
||||
<NText
|
||||
:style="`font-size: 12px; text-align: center;color: ${isDarkMode ? '#555' : '#c0c0c0'};visibility: ${width < 180 ? 'hidden' : 'visible'}`">
|
||||
:style="`font-size: 12px; text-align: center;color: ${isDarkMode ? '#555' : '#c0c0c0'};visibility: ${width < 180 ? 'hidden' : 'visible'}`"
|
||||
>
|
||||
By Megghy
|
||||
</NText>
|
||||
</NFlex>
|
||||
@@ -724,7 +732,8 @@
|
||||
<!-- 主内容区域 -->
|
||||
<NScrollbar :style="`height: calc(100vh - var(--vtsuru-header-height) - ${aplayerHeight}px)`" :x-scrollable="true">
|
||||
<NLayoutContent
|
||||
content-style="margin: var(--vtsuru-content-padding); margin-right: calc(var(--vtsuru-content-padding) + 4px); padding-bottom: 32px;min-width: 370px">
|
||||
content-style="margin: var(--vtsuru-content-padding); margin-right: calc(var(--vtsuru-content-padding) + 4px); padding-bottom: 32px;min-width: 370px"
|
||||
>
|
||||
<NElement>
|
||||
<!-- 已验证邮箱的用户显示内容 -->
|
||||
<RouterView v-if="accountInfo?.isEmailVerified" v-slot="{ Component }">
|
||||
@@ -765,8 +774,10 @@
|
||||
</NAlert>
|
||||
|
||||
<NSpace>
|
||||
<NButton type="primary" :disabled="!canResendEmail" style="min-width: 140px;"
|
||||
@click="resendEmail">
|
||||
<NButton
|
||||
type="primary" :disabled="!canResendEmail" style="min-width: 140px;"
|
||||
@click="resendEmail"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<Mail24Filled />
|
||||
@@ -775,8 +786,10 @@
|
||||
重新发送验证邮件
|
||||
</NButton>
|
||||
<NTag v-if="!canResendEmail" type="warning" round>
|
||||
<NCountdown :duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()"
|
||||
@finish="canResendEmail = true" />
|
||||
<NCountdown
|
||||
:duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()"
|
||||
@finish="canResendEmail = true"
|
||||
/>
|
||||
后可重新发送
|
||||
</NTag>
|
||||
</NSpace>
|
||||
@@ -805,15 +818,19 @@
|
||||
</NScrollbar>
|
||||
|
||||
<!-- 音乐播放器区域 -->
|
||||
<NLayoutFooter v-if="isPlayerVisible"
|
||||
<NLayoutFooter
|
||||
v-if="isPlayerVisible"
|
||||
:style="`height: ${aplayerHeight}px; overflow: hidden; transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);`"
|
||||
class="music-player-footer">
|
||||
<NCard ref="musicPlayerCardRef" :bordered="false" embedded
|
||||
class="music-player-footer"
|
||||
>
|
||||
<NCard
|
||||
ref="musicPlayerCardRef" :bordered="false" embedded
|
||||
:content-style="isPlayerMinimized ? 'padding: 0' : undefined" size="small" class="music-player-card" style="
|
||||
margin: 8px;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
">
|
||||
"
|
||||
>
|
||||
<!-- 播放器头部控制栏 -->
|
||||
<template #header>
|
||||
<NFlex justify="space-between" align="center" style="padding: 0;">
|
||||
@@ -822,15 +839,19 @@
|
||||
<NText :depth="2" style="font-size: 13px; font-weight: 500;">
|
||||
音乐播放器
|
||||
</NText>
|
||||
<NTag v-if="currentPlayingInfo && !isPlayerMinimized"
|
||||
<NTag
|
||||
v-if="currentPlayingInfo && !isPlayerMinimized"
|
||||
:type="currentPlayingInfo.type === 'request' ? 'success' : 'info'" size="small" round
|
||||
:bordered="false" style="font-size: 11px; padding: 2px 8px;">
|
||||
:bordered="false" style="font-size: 11px; padding: 2px 8px;"
|
||||
>
|
||||
{{ currentPlayingInfo.info }}
|
||||
</NTag>
|
||||
|
||||
<template v-if="isPlayerMinimized">
|
||||
<NText v-if="musicRquestStore.currentMusic.title"
|
||||
style="font-size: 13px; max-width: 250px; margin-left: 12px" :ellipsis="{ tooltip: true }">
|
||||
<NText
|
||||
v-if="musicRquestStore.currentMusic.title"
|
||||
style="font-size: 13px; max-width: 250px; margin-left: 12px" :ellipsis="{ tooltip: true }"
|
||||
>
|
||||
{{ musicRquestStore.currentMusic.title }} - {{ musicRquestStore.currentMusic.artist }}
|
||||
</NText>
|
||||
<NText v-else depth="3" style="font-size: 13px; margin-left: 12px">
|
||||
@@ -841,21 +862,27 @@
|
||||
|
||||
<NFlex align="center" size="small">
|
||||
<template v-if="isPlayerMinimized">
|
||||
<NTag v-if="musicRquestStore.waitingMusics.length > 0" type="warning" size="small" round
|
||||
:bordered="false">
|
||||
<NTag
|
||||
v-if="musicRquestStore.waitingMusics.length > 0" type="warning" size="small" round
|
||||
:bordered="false"
|
||||
>
|
||||
{{ musicRquestStore.waitingMusics.length }}
|
||||
</NTag>
|
||||
|
||||
<NButton circle size="tiny" tertiary :disabled="musicRquestStore.aplayerMusics.length === 0"
|
||||
@click.stop="togglePlay">
|
||||
<NButton
|
||||
circle size="tiny" tertiary :disabled="musicRquestStore.aplayerMusics.length === 0"
|
||||
@click.stop="togglePlay"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="aplayer?.audio?.paused !== false ? Play : Pause" size="14" />
|
||||
</template>
|
||||
</NButton>
|
||||
|
||||
<NButton circle size="tiny" tertiary
|
||||
<NButton
|
||||
circle size="tiny" tertiary
|
||||
:disabled="musicRquestStore.waitingMusics.length === 0 && musicRquestStore.aplayerMusics.length <= 1"
|
||||
@click.stop="onNextMusic">
|
||||
@click.stop="onNextMusic"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="PlayForward" size="14" />
|
||||
</template>
|
||||
@@ -864,8 +891,10 @@
|
||||
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton :type="isPlayerMinimized ? 'primary' : 'default'" tertiary size="small" circle
|
||||
@click="togglePlayerMinimize">
|
||||
<NButton
|
||||
:type="isPlayerMinimized ? 'primary' : 'default'" tertiary size="small" circle
|
||||
@click="togglePlayerMinimize"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="isPlayerMinimized ? ChevronUp : ChevronDown" />
|
||||
</template>
|
||||
@@ -882,11 +911,13 @@
|
||||
<NFlex align="center" :wrap="false" style="gap: 12px;">
|
||||
<!-- APlayer组件 -->
|
||||
<div style="flex: 1; min-width: 280px;">
|
||||
<APlayer ref="aplayer" v-model:music="musicRquestStore.currentMusic" v-model:volume="playerVolume"
|
||||
<APlayer
|
||||
ref="aplayer" v-model:music="musicRquestStore.currentMusic" v-model:volume="playerVolume"
|
||||
v-model:shuffle="musicRquestStore.settings.shuffle"
|
||||
v-model:repeat="musicRquestStore.settings.repeat" :list="musicRquestStore.aplayerMusics"
|
||||
list-max-height="200" mutex list-folded style="border-radius: 8px;"
|
||||
@ended="musicRquestStore.onMusicEnd" @play="musicRquestStore.onMusicPlay" />
|
||||
@ended="musicRquestStore.onMusicEnd" @play="musicRquestStore.onMusicPlay"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 右侧控制面板 -->
|
||||
@@ -899,8 +930,10 @@
|
||||
<NFlex size="small" justify="center">
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton circle secondary size="small" :disabled="musicRquestStore.aplayerMusics.length === 0"
|
||||
@click="onPreviousMusic">
|
||||
<NButton
|
||||
circle secondary size="small" :disabled="musicRquestStore.aplayerMusics.length === 0"
|
||||
@click="onPreviousMusic"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="PlayBack" />
|
||||
</template>
|
||||
@@ -911,8 +944,10 @@
|
||||
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton circle type="primary" size="small"
|
||||
:disabled="musicRquestStore.aplayerMusics.length === 0" @click="togglePlay">
|
||||
<NButton
|
||||
circle type="primary" size="small"
|
||||
:disabled="musicRquestStore.aplayerMusics.length === 0" @click="togglePlay"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="aplayer?.audio?.paused !== false ? Play : Pause" />
|
||||
</template>
|
||||
@@ -923,9 +958,11 @@
|
||||
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton circle secondary size="small"
|
||||
<NButton
|
||||
circle secondary size="small"
|
||||
:disabled="musicRquestStore.waitingMusics.length === 0 && musicRquestStore.aplayerMusics.length <= 1"
|
||||
@click="onNextMusic">
|
||||
@click="onNextMusic"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="PlayForward" />
|
||||
</template>
|
||||
@@ -942,13 +979,17 @@
|
||||
队列管理
|
||||
</NText>
|
||||
<NFlex vertical size="small" align="center">
|
||||
<NTag :bordered="false" :type="musicRquestStore.waitingMusics.length > 0 ? 'warning' : 'info'"
|
||||
size="small" round style="min-width: 80px; text-align: center;">
|
||||
<NTag
|
||||
:bordered="false" :type="musicRquestStore.waitingMusics.length > 0 ? 'warning' : 'info'"
|
||||
size="small" round style="min-width: 80px; text-align: center;"
|
||||
>
|
||||
等待: {{ musicRquestStore.waitingMusics.length }}
|
||||
</NTag>
|
||||
|
||||
<NTag :bordered="false" type="success" size="small" round
|
||||
style="min-width: 80px; text-align: center;">
|
||||
<NTag
|
||||
:bordered="false" type="success" size="small" round
|
||||
style="min-width: 80px; text-align: center;"
|
||||
>
|
||||
歌单: {{ musicRquestStore.originMusics.length }}
|
||||
</NTag>
|
||||
|
||||
@@ -974,8 +1015,10 @@
|
||||
音量
|
||||
</NText>
|
||||
</NFlex>
|
||||
<NSlider v-model:value="playerVolume" :min="0" :max="1" :step="0.01" style="width: 80px;"
|
||||
:tooltip="false" size="small" />
|
||||
<NSlider
|
||||
v-model:value="playerVolume" :min="0" :max="1" :step="0.01" style="width: 80px;"
|
||||
:tooltip="false" size="small"
|
||||
/>
|
||||
<NText depth="3" style="font-size: 11px;">
|
||||
{{ Math.round(playerVolume * 100) }}%
|
||||
</NText>
|
||||
@@ -996,7 +1039,8 @@
|
||||
|
||||
<!-- 未登录时显示的登录/注册界面 -->
|
||||
<template v-else>
|
||||
<NLayoutContent style="
|
||||
<NLayoutContent
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -1010,13 +1054,16 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
" :class="isDarkMode ? 'login-dark-bg' : ''">
|
||||
" :class="isDarkMode ? 'login-dark-bg' : ''"
|
||||
>
|
||||
<template v-if="!isLoadingAccount">
|
||||
<NCard class="login-card" :bordered="false">
|
||||
<template #header>
|
||||
<NFlex justify="center" align="center" style="padding: 12px 0;">
|
||||
<NText strong
|
||||
style="font-size: 1.8rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); background-image: linear-gradient(to right, #36d1dc, #5b86e5); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
|
||||
<NText
|
||||
strong
|
||||
style="font-size: 1.8rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); background-image: linear-gradient(to right, #36d1dc, #5b86e5); -webkit-background-clip: text; -webkit-text-fill-color: transparent;"
|
||||
>
|
||||
VTSURU CENTER
|
||||
</NText>
|
||||
</NFlex>
|
||||
|
||||
@@ -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
@@ -143,6 +143,11 @@ onUnmounted(() => {
|
||||
class="live-request-list"
|
||||
:class="{ animating: isMoreThanContainer }"
|
||||
:style="`width: ${width}px; --item-parent-width: ${width}px`"
|
||||
>
|
||||
<TransitionGroup
|
||||
name="live-request-transition"
|
||||
tag="div"
|
||||
class="live-request-transition-group"
|
||||
>
|
||||
<div
|
||||
v-for="(song, index) in activeSongs"
|
||||
@@ -179,6 +184,7 @@ onUnmounted(() => {
|
||||
</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,12 +156,15 @@ 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 }"
|
||||
>
|
||||
<TransitionGroup
|
||||
name="fresh-request-transition"
|
||||
tag="div"
|
||||
class="fresh-request-transition-group"
|
||||
>
|
||||
<div
|
||||
v-for="(song, index) in activeSongs"
|
||||
@@ -197,6 +200,7 @@ onUnmounted(() => {
|
||||
</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'
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
NButton,
|
||||
NCard,
|
||||
NCheckbox,
|
||||
NCollapse,
|
||||
NCollapseItem,
|
||||
NDivider,
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
@@ -109,7 +107,7 @@ function countGraphemes(value: string) {
|
||||
|
||||
function isValidEmail(email: string): boolean {
|
||||
if (!email) return true // 空邮箱是允许的
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
@@ -985,7 +983,9 @@ onUnmounted(() => {
|
||||
<NDrawerContent closable>
|
||||
<template #header>
|
||||
<NSpace justify="space-between" align="center" style="width: 100%;">
|
||||
<NText strong style="font-size: 16px;">本地提问记录</NText>
|
||||
<NText strong style="font-size: 16px;">
|
||||
本地提问记录
|
||||
</NText>
|
||||
<NButton
|
||||
v-if="localQuestions.length > 0"
|
||||
text
|
||||
@@ -1019,7 +1019,9 @@ onUnmounted(() => {
|
||||
<template #header>
|
||||
<NSpace :size="8" align="center" justify="space-between">
|
||||
<NSpace :size="8" align="center">
|
||||
<NText strong>提给:{{ item.targetUserName }}</NText>
|
||||
<NText strong>
|
||||
提给:{{ item.targetUserName }}
|
||||
</NText>
|
||||
<NTag v-if="item.tag" size="small" type="info">
|
||||
{{ item.tag }}
|
||||
</NTag>
|
||||
|
||||
@@ -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