Compare commits

...

3 Commits

Author SHA1 Message Date
a5420e5914 feat: 更新API模型和组件以支持备注功能
- 在api-models.ts中为订单模型添加备注字段
- 在PointOrderCard.vue中新增备注列并调整显示逻辑
- 在PointOrderManage.vue中导出数据时包含备注信息
- 在PointGoodsView.vue中添加备注输入框以供用户填写
2025-05-06 08:50:21 +08:00
4ebfeaec69 feat: 在SongList组件中新增试听和链接开关的显示逻辑
- 添加计算属性以判断是否显示试听和链接开关
- 调整操作列的宽度计算逻辑
- 优化按钮的动态显示逻辑
2025-05-06 02:32:43 +08:00
8f734af8b3 feat: 更新API模型和组件以支持签到排行开关功能
- 在api-models.ts中将SongRequest更改为LiveRequest,并添加CheckInRanking
- 更新CheckInSettings.vue以支持签到排行的开关
- 在多个视图中调整签到排行的显示逻辑
- 移除不再使用的allowCheckInRanking字段
2025-05-06 02:19:23 +08:00
13 changed files with 131 additions and 85 deletions

View File

@@ -239,7 +239,6 @@ export interface Setting_Point {
maxBonusPoints: number // 最大奖励积分 maxBonusPoints: number // 最大奖励积分
allowSelfCheckIn: boolean // 是否允许自己签到 allowSelfCheckIn: boolean // 是否允许自己签到
requireAuth: boolean // 是否需要认证 requireAuth: boolean // 是否需要认证
allowCheckInRanking: boolean // 是否允许查询签到排行
} }
export interface Setting_QuestionDisplay { export interface Setting_QuestionDisplay {
font?: string // Optional string, with a maximum length of 30 characters font?: string // Optional string, with a maximum length of 30 characters
@@ -293,10 +292,11 @@ export enum FunctionTypes {
SongList, SongList,
QuestionBox, QuestionBox,
Schedule, Schedule,
SongRequest, LiveRequest,
Queue, Queue,
Point, Point,
VideoCollect VideoCollect,
CheckInRanking,
} }
export interface SongAuthorInfo { export interface SongAuthorInfo {
name: string name: string
@@ -777,7 +777,7 @@ export interface ResponsePointOrder2OwnerModel {
createAt: number createAt: number
updateAt: number updateAt: number
status: PointOrderStatus status: PointOrderStatus
remark?: string
trackingNumber?: string trackingNumber?: string
expressCompany?: string expressCompany?: string
} }
@@ -791,6 +791,7 @@ export interface ResponsePointOrder2UserModel {
goods: ResponsePointGoodModel goods: ResponsePointGoodModel
status: PointOrderStatus status: PointOrderStatus
createAt: number createAt: number
remark?: string
trackingNumber?: string trackingNumber?: string
expressCompany?: string expressCompany?: string

View File

@@ -148,8 +148,8 @@
<NFormItem label="允许查看签到排行"> <NFormItem label="允许查看签到排行">
<NSwitch <NSwitch
v-model:value="serverSetting.allowCheckInRanking" :value="accountInfo.settings.enableFunctions.includes(FunctionTypes.CheckInRanking)"
@update:value="updateServerSettings" @update:value="updateCheckInRanking"
/> />
<template #feedback> <template #feedback>
启用后用户可以查看签到排行榜 启用后用户可以查看签到排行榜
@@ -381,8 +381,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { SaveSetting, useAccount } from '@/api/account'; import { SaveEnableFunctions, SaveSetting, useAccount } from '@/api/account';
import { CheckInRankingInfo, CheckInResult } from '@/api/api-models'; import { CheckInRankingInfo, CheckInResult, FunctionTypes } from '@/api/api-models';
import { QueryGetAPI } from '@/api/query'; import { QueryGetAPI } from '@/api/query';
import { useAutoAction } from '@/client/store/useAutoAction'; import { useAutoAction } from '@/client/store/useAutoAction';
import { CHECKIN_API_URL } from '@/data/constants'; import { CHECKIN_API_URL } from '@/data/constants';
@@ -791,6 +791,10 @@ async function handleTestCheckIn() {
}); });
} }
} }
function updateCheckInRanking(value: boolean) {
accountInfo.value.settings.enableFunctions = value ? [...accountInfo.value.settings.enableFunctions, FunctionTypes.CheckInRanking] : accountInfo.value.settings.enableFunctions.filter(f => f !== FunctionTypes.CheckInRanking);
SaveEnableFunctions(accountInfo.value.settings.enableFunctions);
}
// 组件挂载时加载排行榜 // 组件挂载时加载排行榜
onMounted(() => { onMounted(() => {

View File

@@ -156,7 +156,6 @@ function getScoreColor(score: number | undefined): string {
lazy lazy
/> />
</NSpace> </NSpace>
<NDivider style="margin: 10px 0;" />
</template> </template>
<NText <NText

View File

@@ -119,20 +119,32 @@ defineExpose({
// --- 计算属性 --- // --- 计算属性 ---
// 新增:计算是否需要显示试听开关
const canShowListenSwitch = computed(() => {
const audioRegex = /\.(mp3|flac|ogg|wav|m4a)$/i;
return songsInternal.value.some(song => song.url && audioRegex.test(song.url));
});
// 新增:计算是否需要显示链接开关
const canShowLinkSwitch = computed(() => {
const linkSources = [SongFrom.Netease, SongFrom.FiveSing, SongFrom.Kugou]; // Corrected sources
return songsInternal.value.some(song => song.url || (song.from != null && linkSources.includes(song.from))); // Check url OR valid from source
});
// 计算操作列的预定义宽度 // 计算操作列的预定义宽度
const actionColumnWidth = computed(() => { const actionColumnWidth = computed(() => {
const baseSelfWidth = 85; // 基础宽度 (isSelf=true, 编辑+删除) const baseSelfWidth = 80; // 基础宽度 (isSelf=true, 编辑+删除)
const basePublicWidth = 40; // 基础宽度 (isSelf=false) const basePublicWidth = 40; // 基础宽度 (isSelf=false)
const listenButtonWidth = 40; const listenButtonWidth = 40;
const linkButtonWidth = 40; const linkButtonWidth = 50;
const extraButtonWidth = 40; // 假设的额外按钮宽度 const extraButtonWidth = 40; // 假设的额外按钮宽度
let width = props.isSelf ? baseSelfWidth : basePublicWidth; let width = props.isSelf ? baseSelfWidth : basePublicWidth;
if (showListenButton.value) { if (showListenButton.value && canShowListenSwitch.value) {
width += listenButtonWidth; width += listenButtonWidth;
} }
if (showLinkButton.value) { if (showLinkButton.value && canShowLinkSwitch.value) {
width += linkButtonWidth; width += linkButtonWidth;
} }
if (props.extraButton) { if (props.extraButton) {
@@ -351,7 +363,7 @@ function createColumns(): DataTableColumns<SongsInfo> {
{ {
title: '标签', title: '标签',
key: 'tags', key: 'tags',
width: 150, // 调整宽度 minWidth: 100,
resizable: true, resizable: true,
// 列筛选选项 // 列筛选选项
filterOptions: tagsSelectOption.value, filterOptions: tagsSelectOption.value,
@@ -452,17 +464,7 @@ function createColumns(): DataTableColumns<SongsInfo> {
// 使用 NSpace 渲染所有按钮 // 使用 NSpace 渲染所有按钮
return h(NSpace, { justify: 'end', size: 8, wrap: false }, () => buttons); // 增加间距,禁止换行 return h(NSpace, { justify: 'end', size: 8, wrap: false }, () => buttons); // 增加间距,禁止换行
}, },
// --- 动态计算宽度 --- START
/* width: (() => {
let calculatedWidth = 20; // 基础内边距
if (showLinkButton.value) calculatedWidth += 40; // 链接按钮宽度
if (showListenButton.value) calculatedWidth += 40; // 试听按钮宽度
if (props.isSelf) calculatedWidth += 80; // 编辑 + 删除按钮宽度
if (props.extraButton) calculatedWidth += 40; // 额外按钮预估宽度
return Math.max(calculatedWidth, props.isSelf ? 160 : 80); // 设置最小宽度防止太窄
})(), */
width: actionColumnWidth.value, // 使用计算属性 width: actionColumnWidth.value, // 使用计算属性
// --- 动态计算宽度 --- END
}, },
] ]
} }
@@ -763,6 +765,7 @@ onMounted(() => {
item-style="display: flex; align-items: center;" item-style="display: flex; align-items: center;"
size="small" size="small"
> >
<template v-if="canShowListenSwitch">
<NSwitch <NSwitch
v-model:value="showListenButton" v-model:value="showListenButton"
size="small" size="small"
@@ -770,6 +773,8 @@ onMounted(() => {
<NText style="font-size: 12px;"> <NText style="font-size: 12px;">
试听 试听
</NText> </NText>
</template>
<template v-if="canShowLinkSwitch">
<NSwitch <NSwitch
v-model:value="showLinkButton" v-model:value="showLinkButton"
size="small" size="small"
@@ -777,6 +782,7 @@ onMounted(() => {
<NText style="font-size: 12px;"> <NText style="font-size: 12px;">
链接 链接
</NText> </NText>
</template>
</NSpace> </NSpace>
</NSpace> </NSpace>
</NCard> </NCard>

View File

@@ -239,6 +239,17 @@ const orderColumn: DataTableColumns<OrderType> = [
}, () => row.type === GoodsTypes.Physical ? '实体礼物' : '虚拟礼物') }, () => row.type === GoodsTypes.Physical ? '实体礼物' : '虚拟礼物')
}, },
}, },
{
title: '备注',
key: 'remark',
minWidth: 100,
render: (row: OrderType) => {
if (!row.remark) {
return h(NText, { depth: 3, italic: true }, () => '无')
}
return h(NEllipsis, { style: { maxWidth: '100px' } }, () => row.remark)
},
},
{ {
title: '地址', title: '地址',
key: 'address', key: 'address',
@@ -281,6 +292,7 @@ const orderColumn: DataTableColumns<OrderType> = [
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
fixed: 'right',
render: (row: OrderType) => { render: (row: OrderType) => {
return h( return h(
NButton, NButton,
@@ -462,8 +474,6 @@ onMounted(() => {
trigger="none" trigger="none"
> >
<div class="order-detail-content"> <div class="order-detail-content">
<!-- 用户视图 -->
<template v-if="orderDetail.instanceOf === 'user'">
<NDivider style="margin-top: 0"> <NDivider style="margin-top: 0">
礼物快照 礼物快照
<NTooltip> <NTooltip>
@@ -482,6 +492,23 @@ onMounted(() => {
/> />
</NFlex> </NFlex>
<!-- 移动并修改备注信息 -->
<template v-if="orderDetail.remark">
<NAlert
title="备注信息"
type="info"
style="margin-top: 16px; margin-bottom: 16px;"
closable
>
<template #icon>
<NIcon :component="Info24Filled" />
</template>
<NText>{{ orderDetail.remark }}</NText>
</NAlert>
</template>
<!-- 用户视图 -->
<template v-if="orderDetail.instanceOf === 'user'">
<!-- 虚拟礼物内容 --> <!-- 虚拟礼物内容 -->
<template v-if="orderDetail.type === GoodsTypes.Virtual"> <template v-if="orderDetail.type === GoodsTypes.Virtual">
<NDivider>虚拟礼物内容</NDivider> <NDivider>虚拟礼物内容</NDivider>
@@ -531,14 +558,6 @@ onMounted(() => {
<!-- 主播视图 --> <!-- 主播视图 -->
<template v-else-if="orderDetail.instanceOf === 'owner'"> <template v-else-if="orderDetail.instanceOf === 'owner'">
<NFlex justify="center">
<PointGoodsItem
v-if="currentGoods"
class="goods-item"
:goods="currentGoods"
/>
</NFlex>
<NDivider>订单状态管理</NDivider> <NDivider>订单状态管理</NDivider>
<!-- 虚拟礼物提示 --> <!-- 虚拟礼物提示 -->

View File

@@ -121,7 +121,7 @@
{ {
label: () => h(RouterLink, { to: { name: 'user-checkin' } }, { default: () => '签到排行' }), label: () => h(RouterLink, { to: { name: 'user-checkin' } }, { default: () => '签到排行' }),
key: 'user-checkin', icon: renderIcon(CheckmarkCircle24Filled), key: 'user-checkin', icon: renderIcon(CheckmarkCircle24Filled),
show: userInfo.value?.extra?.allowCheckInRanking show: userInfo.value?.extra?.enableFunctions.includes(FunctionTypes.CheckInRanking)
}, },
].filter(option => option.show !== false) as MenuOption[]; // 过滤掉 show 为 false 的菜单项 ].filter(option => option.show !== false) as MenuOption[]; // 过滤掉 show 为 false 的菜单项
} }

View File

@@ -633,12 +633,15 @@
<NCheckbox :value="FunctionTypes.Schedule"> <NCheckbox :value="FunctionTypes.Schedule">
日程 日程
</NCheckbox> </NCheckbox>
<NCheckbox :value="FunctionTypes.SongRequest"> <NCheckbox :value="FunctionTypes.LiveRequest">
点歌 点歌
</NCheckbox> </NCheckbox>
<NCheckbox :value="FunctionTypes.Queue"> <NCheckbox :value="FunctionTypes.Queue">
排队 排队
</NCheckbox> </NCheckbox>
<NCheckbox :value="FunctionTypes.CheckInRanking">
签到排行
</NCheckbox>
</NCheckboxGroup> </NCheckboxGroup>
<NDivider> 通知 </NDivider> <NDivider> 通知 </NDivider>

View File

@@ -188,6 +188,7 @@ function exportData() {
礼物总价: s.point, 礼物总价: s.point,
快递公司: s.expressCompany, 快递公司: s.expressCompany,
快递单号: s.trackingNumber, 快递单号: s.trackingNumber,
备注: s.remark ?? '',
创建时间: format(s.createAt, 'yyyy-MM-dd HH:mm:ss'), 创建时间: format(s.createAt, 'yyyy-MM-dd HH:mm:ss'),
更新时间: s.updateAt ? format(s.updateAt, 'yyyy-MM-dd HH:mm:ss') : '未更新', 更新时间: s.updateAt ? format(s.updateAt, 'yyyy-MM-dd HH:mm:ss') : '未更新',
} }

View File

@@ -54,7 +54,6 @@ const defaultSettingPoint: Setting_Point = {
maxBonusPoints: 0, maxBonusPoints: 0,
allowSelfCheckIn: false, allowSelfCheckIn: false,
requireAuth: false, requireAuth: false,
allowCheckInRanking: false
} }
// 响应式设置对象 // 响应式设置对象

View File

@@ -76,12 +76,12 @@ const props = defineProps<{
async function onUpdateFunctionEnable() { async function onUpdateFunctionEnable() {
if (accountInfo.value.id) { if (accountInfo.value.id) {
const oldValue = JSON.parse(JSON.stringify(accountInfo.value.settings.enableFunctions)) const oldValue = JSON.parse(JSON.stringify(accountInfo.value.settings.enableFunctions))
if (accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest)) { if (accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.LiveRequest)) {
accountInfo.value.settings.enableFunctions = accountInfo.value.settings.enableFunctions.filter( accountInfo.value.settings.enableFunctions = accountInfo.value.settings.enableFunctions.filter(
(f) => f != FunctionTypes.SongRequest, (f) => f != FunctionTypes.LiveRequest,
) )
} else { } else {
accountInfo.value.settings.enableFunctions.push(FunctionTypes.SongRequest) accountInfo.value.settings.enableFunctions.push(FunctionTypes.LiveRequest)
} }
if (!accountInfo.value.settings.songRequest.orderPrefix) { if (!accountInfo.value.settings.songRequest.orderPrefix) {
accountInfo.value.settings.songRequest.orderPrefix = songRequest.defaultPrefix accountInfo.value.settings.songRequest.orderPrefix = songRequest.defaultPrefix
@@ -90,20 +90,20 @@ async function onUpdateFunctionEnable() {
.then((data) => { .then((data) => {
if (data.code == 200) { if (data.code == 200) {
message.success( message.success(
`${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}点播功能`, `${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.LiveRequest) ? '启用' : '禁用'}点播功能`,
) )
} else { } else {
if (accountInfo.value.id) { if (accountInfo.value.id) {
accountInfo.value.settings.enableFunctions = oldValue accountInfo.value.settings.enableFunctions = oldValue
} }
message.error( message.error(
`点播功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}失败: ${data.message}`, `点播功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.LiveRequest) ? '启用' : '禁用'}失败: ${data.message}`,
) )
} }
}) })
.catch((err) => { .catch((err) => {
message.error( message.error(
`点播功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}失败: ${err}`, `点播功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.LiveRequest) ? '启用' : '禁用'}失败: ${err}`,
) )
}) })
} }
@@ -158,11 +158,11 @@ onUnmounted(() => {
<template> <template>
<NAlert <NAlert
v-if="accountInfo.id" v-if="accountInfo.id"
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? 'success' : 'warning'" :type="accountInfo.settings.enableFunctions.includes(FunctionTypes.LiveRequest) ? 'success' : 'warning'"
> >
启用弹幕点播功能 启用弹幕点播功能
<NSwitch <NSwitch
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.SongRequest)" :value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.LiveRequest)"
@update:value="onUpdateFunctionEnable" @update:value="onUpdateFunctionEnable"
/> />
@@ -215,7 +215,7 @@ onUnmounted(() => {
<br> <br>
<NCard> <NCard>
<NTabs <NTabs
v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest)" v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.LiveRequest)"
animated animated
display-directive="show:lazy" display-directive="show:lazy"
> >

View File

@@ -24,7 +24,7 @@ const message = useMessage()
const enableSongRequest = computed({ const enableSongRequest = computed({
get: () => { get: () => {
return accountInfo.value?.settings?.enableFunctions?.includes(FunctionTypes.SongRequest) || false return accountInfo.value?.settings?.enableFunctions?.includes(FunctionTypes.LiveRequest) || false
}, },
set: async () => { set: async () => {
await updateEnableFunctions() await updateEnableFunctions()
@@ -35,12 +35,12 @@ const enableSongRequest = computed({
async function updateEnableFunctions() { async function updateEnableFunctions() {
if (accountInfo.value.id) { if (accountInfo.value.id) {
const oldValue = JSON.parse(JSON.stringify(accountInfo.value.settings.enableFunctions)) const oldValue = JSON.parse(JSON.stringify(accountInfo.value.settings.enableFunctions))
if (accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest)) { if (accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.LiveRequest)) {
accountInfo.value.settings.enableFunctions = accountInfo.value.settings.enableFunctions.filter( accountInfo.value.settings.enableFunctions = accountInfo.value.settings.enableFunctions.filter(
(f: number) => f != FunctionTypes.SongRequest, (f: number) => f != FunctionTypes.LiveRequest,
) )
} else { } else {
accountInfo.value.settings.enableFunctions.push(FunctionTypes.SongRequest) accountInfo.value.settings.enableFunctions.push(FunctionTypes.LiveRequest)
} }
if (!accountInfo.value.settings.songRequest.orderPrefix) { if (!accountInfo.value.settings.songRequest.orderPrefix) {
accountInfo.value.settings.songRequest.orderPrefix = liveRequest.defaultPrefix accountInfo.value.settings.songRequest.orderPrefix = liveRequest.defaultPrefix
@@ -49,20 +49,20 @@ async function updateEnableFunctions() {
.then((data) => { .then((data) => {
if (data.code == 200) { if (data.code == 200) {
message.success( message.success(
`${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}点播功能`, `${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.LiveRequest) ? '启用' : '禁用'}点播功能`,
) )
} else { } else {
if (accountInfo.value.id) { if (accountInfo.value.id) {
accountInfo.value.settings.enableFunctions = oldValue accountInfo.value.settings.enableFunctions = oldValue
} }
message.error( message.error(
`点播功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}失败: ${data.message}`, `点播功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.LiveRequest) ? '启用' : '禁用'}失败: ${data.message}`,
) )
} }
}) })
.catch((err) => { .catch((err) => {
message.error( message.error(
`点播功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}失败: ${err}`, `点播功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.LiveRequest) ? '启用' : '禁用'}失败: ${err}`,
) )
}) })
} }

View File

@@ -62,6 +62,7 @@ const showAddressSelect = ref(false)
const currentGoods = ref<ResponsePointGoodModel>() // 当前选中的礼物 const currentGoods = ref<ResponsePointGoodModel>() // 当前选中的礼物
const buyCount = ref(1) // 购买数量 const buyCount = ref(1) // 购买数量
const selectedAddress = ref<AddressInfo>() // 选中的地址 const selectedAddress = ref<AddressInfo>() // 选中的地址
const remark = ref('') // 新增:用于存储用户备注
// 筛选相关状态 // 筛选相关状态
const selectedTag = ref<string>() // 选中的标签 const selectedTag = ref<string>() // 选中的标签
@@ -217,6 +218,7 @@ function resetBuyModalState() {
selectedAddress.value = undefined selectedAddress.value = undefined
buyCount.value = 1 buyCount.value = 1
currentGoods.value = undefined currentGoods.value = undefined
remark.value = '' // 新增:重置备注
} }
// 处理模态框显示状态变化 // 处理模态框显示状态变化
@@ -260,6 +262,7 @@ async function buyGoods() {
goodsId: currentGoods.value?.id, goodsId: currentGoods.value?.id,
count: buyCount.value, count: buyCount.value,
addressId: selectedAddress.value?.id ?? null, // 如果地址未选择,则传 null addressId: selectedAddress.value?.id ?? null, // 如果地址未选择,则传 null
remark: remark.value, // 新增:将备注添加到请求中
}) })
if (data.code === 200) { if (data.code === 200) {
@@ -638,7 +641,7 @@ onMounted(async () => {
/> />
<!-- 兑换选项 (仅对实物或需要数量选择的礼物显示) --> <!-- 兑换选项 (仅对实物或需要数量选择的礼物显示) -->
<template v-if="currentGoods.type === GoodsTypes.Physical || (currentGoods.maxBuyCount ?? 1) > 1"> <template v-if="currentGoods.type === GoodsTypes.Physical || (currentGoods.maxBuyCount ?? 1) > 1 || true">
<NDivider style="margin-top: 12px; margin-bottom: 12px;"> <NDivider style="margin-top: 12px; margin-bottom: 12px;">
兑换选项 兑换选项
</NDivider> </NDivider>
@@ -689,6 +692,17 @@ onMounted(async () => {
管理地址 管理地址
</NButton> </NButton>
</NFormItem> </NFormItem>
<!-- 备注输入 -->
<NFormItem label="备注">
<NInput
v-model:value="remark"
type="textarea"
placeholder="可以在这里留下备注信息(可选)"
:autosize="{ minRows: 2, maxRows: 4 }"
maxlength="100"
show-count
/>
</NFormItem>
</NForm> </NForm>
</template> </template>

View File

@@ -153,7 +153,7 @@ function loadMore() {
clearable clearable
/> />
<NDivider /> <NDivider />
<LiveRequestOBS v-if="userInfo?.extra?.enableFunctions.includes(FunctionTypes.SongRequest)" /> <LiveRequestOBS v-if="userInfo?.extra?.enableFunctions.includes(FunctionTypes.LiveRequest)" />
</NSpace> </NSpace>
</NCard> </NCard>
<NEmpty <NEmpty