Compare commits

...

3 Commits

Author SHA1 Message Date
4ac793f155 feat: 更新时间显示组件和相关设置
- 在多个组件中引入 NTime 和 NTooltip 以优化时间显示
- 修改 ActionHistoryViewer.vue 和 CheckInSettings.vue 中的时间渲染逻辑
- 在 CheckInRankingView.vue 中实现时间的相对显示和格式化
- 修复商品页加载问题
2025-05-02 06:37:18 +08:00
4bcb966bdc feat: 添加自定义测试上下文和更新模板设置
- 在 AutoActionEditor 和 TemplateSettings 组件中添加 customTestContext 属性
- 更新 CheckInSettings 组件以使用 customTestContext
- 移除 TemplateEditor 中的操作按钮部分
2025-05-02 02:34:56 +08:00
993107c24c feat: 替换认证存储逻辑为BiliAuth
- 将所有使用useAuthStore的地方替换为useBiliAuth
- 删除useAuthStore文件,整合认证逻辑
- 更新相关视图和组件以适应新的认证存储
2025-05-02 01:59:21 +08:00
24 changed files with 552 additions and 259 deletions

View File

@@ -36,11 +36,15 @@ const columns = [
width: 180, width: 180,
sorter: (a: HistoryItem, b: HistoryItem) => a.timestamp - b.timestamp, sorter: (a: HistoryItem, b: HistoryItem) => a.timestamp - b.timestamp,
render: (row: HistoryItem) => { render: (row: HistoryItem) => {
return h(NTime, { return h(NTooltip, {
time: new Date(row.timestamp), }, {
format: 'yyyy-MM-dd HH:mm:ss' trigger: () => h(NTime, {
time: row.timestamp,
type: 'relative'
}),
default: () => new Date(row.timestamp).toLocaleString()
}); });
} },
}, },
{ {
title: '操作名称', title: '操作名称',

View File

@@ -26,6 +26,10 @@ const props = defineProps({
hideEnabled: { hideEnabled: {
type: Boolean, type: Boolean,
default: false default: false
},
customTestContext: {
type: Object,
default: undefined
} }
}); });
@@ -58,7 +62,7 @@ const TriggerSettings = getTriggerSettings();
<div class="auto-action-editor"> <div class="auto-action-editor">
<NSpace vertical> <NSpace vertical>
<!-- 模板设置 - 移到最上面 --> <!-- 模板设置 - 移到最上面 -->
<TemplateSettings :action="action" /> <TemplateSettings :action="action" :custom-test-context="customTestContext" />
<!-- 基本设置 --> <!-- 基本设置 -->
<BasicSettings <BasicSettings

View File

@@ -380,21 +380,6 @@ function insertExample(template: string) {
</transition> </transition>
</NFlex> </NFlex>
<!-- 操作按钮 -->
<NFlex
justify="end"
:size="12"
>
<NButton
type="default"
size="small"
class="btn-with-transition"
@click="convertPlaceholders"
>
占位符转表达式
</NButton>
</NFlex>
<!-- 模板示例 --> <!-- 模板示例 -->
<NCollapse <NCollapse
class="template-examples" class="template-examples"

View File

@@ -43,7 +43,10 @@
</NFormItem> </NFormItem>
<template v-if="serverSetting.enableCheckIn"> <template v-if="serverSetting.enableCheckIn">
<NFormItem label="签到命令"> <NFormItem
label="签到命令"
required
>
<NInputGroup> <NInputGroup>
<NInput <NInput
:value="serverSetting.checkInKeyword" :value="serverSetting.checkInKeyword"
@@ -188,23 +191,25 @@
</template> </template>
</NAlert> </NAlert>
</div> </div>
<NDivider title-placement="left">
<!-- 使用 AutoActionEditor 编辑 action 配置 --> 签到成功回复
<NFormItem label="签到成功回复"> </NDivider>
<AutoActionEditor <AutoActionEditor
:action="config.successAction" :action="config.successAction"
:hide-name="true" :hide-name="true"
:hide-enabled="true" :hide-enabled="true"
:custom-test-context="customTestContext"
/> />
</NFormItem>
<NFormItem label="签到冷却回复"> <NDivider title-placement="left">
签到冷却回复
</NDivider>
<AutoActionEditor <AutoActionEditor
:action="config.cooldownAction" :action="config.cooldownAction"
:hide-name="true" :hide-name="true"
:hide-enabled="true" :hide-enabled="true"
:custom-test-context="customTestContext"
/> />
</NFormItem>
</template> </template>
<NFormItem> <NFormItem>
@@ -384,7 +389,7 @@ import { CHECKIN_API_URL } from '@/data/constants';
import { GuidUtils } from '@/Utils'; import { GuidUtils } from '@/Utils';
import { Info24Filled } from '@vicons/fluent'; import { Info24Filled } from '@vicons/fluent';
import type { DataTableColumns } from 'naive-ui'; import type { DataTableColumns } from 'naive-ui';
import { NAlert, NButton, NCard, NDataTable, NDivider, NEmpty, NForm, NFormItem, NIcon, NInput, NInputGroup, NInputNumber, NPopconfirm, NSelect, NSpace, NSpin, NSwitch, NTabPane, NTabs, NText } from 'naive-ui'; import { NAlert, NButton, NCard, NDataTable, NDivider, NEmpty, NForm, NFormItem, NIcon, NInput, NInputGroup, NInputNumber, NPopconfirm, NSelect, NSpace, NSpin, NSwitch, NTabPane, NTabs, NText, NTime, NTooltip } from 'naive-ui';
import { computed, h, onMounted, ref, watch } from 'vue'; import { computed, h, onMounted, ref, watch } from 'vue';
import AutoActionEditor from '../AutoActionEditor.vue'; import AutoActionEditor from '../AutoActionEditor.vue';
import TemplateHelper from '../TemplateHelper.vue'; import TemplateHelper from '../TemplateHelper.vue';
@@ -398,6 +403,15 @@ const config = autoActionStore.checkInModule.checkInConfig;
const accountInfo = useAccount(); const accountInfo = useAccount();
const isLoading = ref(false); const isLoading = ref(false);
const customTestContext = ref({
checkin: {
points: 0,
consecutiveDays: 0,
todayRank: 0,
time: new Date()
}
});
// 签到模板的特定占位符 // 签到模板的特定占位符
const checkInPlaceholders = [ const checkInPlaceholders = [
{ name: '{{checkin.points}}', description: '获得的总积分' }, { name: '{{checkin.points}}', description: '获得的总积分' },
@@ -563,7 +577,14 @@ const rankingColumns: DataTableColumns<CheckInRankingInfo> = [
title: '最近签到时间', title: '最近签到时间',
key: 'lastCheckInTime', key: 'lastCheckInTime',
render(row: CheckInRankingInfo) { render(row: CheckInRankingInfo) {
return h('span', {}, new Date(row.lastCheckInTime).toLocaleString()); return h(NTooltip, {
}, {
trigger: () => h(NTime, {
time: row.lastCheckInTime,
type: 'relative'
}),
default: () => new Date(row.lastCheckInTime).toLocaleString()
});
}, },
sorter: 'default' sorter: 'default'
}, },

View File

@@ -8,6 +8,10 @@ const props = defineProps({
action: { action: {
type: Object as () => AutoActionItem, type: Object as () => AutoActionItem,
required: true required: true
},
customTestContext: {
type: Object,
default: undefined
} }
}); });
@@ -61,6 +65,7 @@ function handleTemplateUpdate(payload: { index: number, value: string }) {
:title="templateTitle" :title="templateTitle"
:description="templateDescription" :description="templateDescription"
:check-length="action.actionType === ActionType.SEND_DANMAKU" :check-length="action.actionType === ActionType.SEND_DANMAKU"
:custom-test-context="customTestContext"
class="template-editor" class="template-editor"
@update:template="handleTemplateUpdate" @update:template="handleTemplateUpdate"
/> />

2
src/components.d.ts vendored
View File

@@ -22,6 +22,7 @@ declare module 'vue' {
NAvatar: typeof import('naive-ui')['NAvatar'] NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton'] NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard'] NCard: typeof import('naive-ui')['NCard']
NDi: typeof import('naive-ui')['NDi']
NEmpty: typeof import('naive-ui')['NEmpty'] NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex'] NFlex: typeof import('naive-ui')['NFlex']
NFormItemGi: typeof import('naive-ui')['NFormItemGi'] NFormItemGi: typeof import('naive-ui')['NFormItemGi']
@@ -35,6 +36,7 @@ declare module 'vue' {
NSpace: typeof import('naive-ui')['NSpace'] NSpace: typeof import('naive-ui')['NSpace']
NTag: typeof import('naive-ui')['NTag'] NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText'] NText: typeof import('naive-ui')['NText']
NTime: typeof import('naive-ui')['NTime']
PointGoodsItem: typeof import('./components/manage/PointGoodsItem.vue')['default'] PointGoodsItem: typeof import('./components/manage/PointGoodsItem.vue')['default']
PointHistoryCard: typeof import('./components/manage/PointHistoryCard.vue')['default'] PointHistoryCard: typeof import('./components/manage/PointHistoryCard.vue')['default']
PointOrderCard: typeof import('./components/manage/PointOrderCard.vue')['default'] PointOrderCard: typeof import('./components/manage/PointOrderCard.vue')['default']

View File

@@ -1,6 +1,6 @@
import { GetSelfAccount, useAccount, UpdateAccountLoop } from "@/api/account"; import { GetSelfAccount, useAccount, UpdateAccountLoop } from "@/api/account";
import { QueryGetAPI } from "@/api/query"; import { QueryGetAPI } from "@/api/query";
import { useAuthStore } from "@/store/useAuthStore"; import { useBiliAuth } from "@/store/useBiliAuth";
import { useNotificationStore } from "@/store/useNotificationStore"; import { useNotificationStore } from "@/store/useNotificationStore";
import { createDiscreteApi, NText, NFlex, NButton } from "naive-ui"; import { createDiscreteApi, NText, NFlex, NButton } from "naive-ui";
import { BASE_API_URL, isTauri, apiFail } from "./constants"; import { BASE_API_URL, isTauri, apiFail } from "./constants";
@@ -64,7 +64,7 @@ async function InitOther() {
InitTTS() InitTTS()
await GetSelfAccount() await GetSelfAccount()
const account = useAccount() const account = useAccount()
const useAuth = useAuthStore() const useAuth = useBiliAuth()
if (account.value.id) { if (account.value.id) {
if (account.value.biliUserAuthInfo && !useAuth.currentToken) { if (account.value.biliUserAuthInfo && !useAuth.currentToken) {
useAuth.currentToken = account.value.biliUserAuthInfo.token useAuth.currentToken = account.value.biliUserAuthInfo.token

View File

@@ -6,7 +6,7 @@ import { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
export const useAuthStore = defineStore('BiliAuth', () => { export const useBiliAuth = defineStore('BiliAuth', () => {
const biliAuth = ref<BiliAuthModel>({} as BiliAuthModel) const biliAuth = ref<BiliAuthModel>({} as BiliAuthModel)
const biliTokens = useStorage< const biliTokens = useStorage<

View File

@@ -3,11 +3,11 @@ import { QueryGetAPI } from "@/api/query";
import { POINT_API_URL } from "@/data/constants"; import { POINT_API_URL } from "@/data/constants";
import { MessageApiInjection } from "naive-ui/es/message/src/MessageProvider"; import { MessageApiInjection } from "naive-ui/es/message/src/MessageProvider";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { useAuthStore } from "./useAuthStore"; import { useBiliAuth } from "./useBiliAuth";
import { GuidUtils } from "@/Utils"; import { GuidUtils } from "@/Utils";
export const usePointStore = defineStore('point', () => { export const usePointStore = defineStore('point', () => {
const useAuth = useAuthStore() const useAuth = useBiliAuth()
async function GetSpecificPoint(id: number) { async function GetSpecificPoint(id: number) {
try { try {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { QueryGetAPI } from '@/api/query' import { QueryGetAPI } from '@/api/query'
import { BILI_AUTH_API_URL, CURRENT_HOST } from '@/data/constants' import { BILI_AUTH_API_URL, CURRENT_HOST } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore' import { useBiliAuth } from '@/store/useBiliAuth'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { import {
NAlert, NAlert,
@@ -33,7 +33,7 @@ const message = useMessage()
const guidKey = useStorage('Bili.Auth.Key', uuidv4()) const guidKey = useStorage('Bili.Auth.Key', uuidv4())
const currentToken = useStorage<string>('Bili.Auth.Selected', null) const currentToken = useStorage<string>('Bili.Auth.Selected', null)
const useAuth = useAuthStore() const useAuth = useBiliAuth()
const startModel = ref<AuthStartModel>() const startModel = ref<AuthStartModel>()

View File

@@ -6,7 +6,7 @@ import { QueryGetAPI } from '@/api/query'
import RegisterAndLogin from '@/components/RegisterAndLogin.vue' import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
import { checkUpdateNote } from '@/data/UpdateNote'; import { checkUpdateNote } from '@/data/UpdateNote';
import { ACCOUNT_API_URL } from '@/data/constants' import { ACCOUNT_API_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore' import { useBiliAuth } from '@/store/useBiliAuth'
import { useMusicRequestProvider } from '@/store/useMusicRequest' import { useMusicRequestProvider } from '@/store/useMusicRequest'
import { import {
BookCoins20Filled, BookCoins20Filled,
@@ -369,7 +369,7 @@ function gotoAuthPage() {
message.error('你尚未进行 Bilibili 认证, 请前往面板进行认证和绑定') message.error('你尚未进行 Bilibili 认证, 请前往面板进行认证和绑定')
return return
} }
useAuthStore() useBiliAuth()
.setCurrentAuth(accountInfo.value?.biliUserAuthInfo.token) .setCurrentAuth(accountInfo.value?.biliUserAuthInfo.token)
.then(() => { .then(() => {
NavigateToNewTab('/bili-user') NavigateToNewTab('/bili-user')

View File

@@ -5,7 +5,7 @@
import { useUser } from '@/api/user'; import { useUser } from '@/api/user';
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'; import RegisterAndLogin from '@/components/RegisterAndLogin.vue';
import { FETCH_API } from '@/data/constants'; // 移除了未使用的 AVATAR_URL import { FETCH_API } from '@/data/constants'; // 移除了未使用的 AVATAR_URL
import { useAuthStore } from '@/store/useAuthStore'; import { useBiliAuth } from '@/store/useBiliAuth';
import { import {
BookCoins20Filled, BookCoins20Filled,
CalendarClock24Filled, CalendarClock24Filled,
@@ -47,7 +47,7 @@
const router = useRouter(); // 获取 router 实例 const router = useRouter(); // 获取 router 实例
const message = useMessage(); const message = useMessage();
const accountInfo = useAccount(); // 获取当前登录账户信息 const accountInfo = useAccount(); // 获取当前登录账户信息
const useAuth = useAuthStore(); // 获取认证状态 Store const useAuth = useBiliAuth(); // 获取认证状态 Store
// 路由参数 // 路由参数
const id = computed(() => route.params.id); const id = computed(() => route.params.id);

View File

@@ -6,7 +6,7 @@ import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue' import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
import PointGoodsItem from '@/components/manage/PointGoodsItem.vue' import PointGoodsItem from '@/components/manage/PointGoodsItem.vue'
import { CN_HOST, CURRENT_HOST, FILE_BASE_URL, POINT_API_URL } from '@/data/constants' import { CN_HOST, CURRENT_HOST, FILE_BASE_URL, POINT_API_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore' import { useBiliAuth } from '@/store/useBiliAuth'
import { Info24Filled } from '@vicons/fluent' import { Info24Filled } from '@vicons/fluent'
import { useRouteHash } from '@vueuse/router' import { useRouteHash } from '@vueuse/router'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
@@ -52,7 +52,7 @@ import PointUserManage from './PointUserManage.vue'
const message = useMessage() const message = useMessage()
const accountInfo = useAccount() const accountInfo = useAccount()
const dialog = useDialog() const dialog = useDialog()
const useBiliAuth = useAuthStore() const biliAuth = useBiliAuth()
const formRef = ref() const formRef = ref()
const isUpdating = ref(false) const isUpdating = ref(false)
const isAllowedPrivacyPolicy = ref(false) const isAllowedPrivacyPolicy = ref(false)
@@ -70,7 +70,7 @@ const hash = computed({
}) })
// 商品数据及模型 // 商品数据及模型
const goods = ref<ResponsePointGoodModel[]>(await useBiliAuth.GetGoods(accountInfo.value?.id, message)) const goods = ref<ResponsePointGoodModel[]>(await biliAuth.GetGoods(accountInfo.value?.id, message))
const defaultGoodsModel = { const defaultGoodsModel = {
goods: { goods: {
type: GoodsTypes.Virtual, type: GoodsTypes.Virtual,

View File

@@ -11,7 +11,7 @@ import {
import AddressDisplay from '@/components/manage/AddressDisplay.vue' import AddressDisplay from '@/components/manage/AddressDisplay.vue'
import PointGoodsItem from '@/components/manage/PointGoodsItem.vue' import PointGoodsItem from '@/components/manage/PointGoodsItem.vue'
import { POINT_API_URL } from '@/data/constants' import { POINT_API_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore' import { useBiliAuth } from '@/store/useBiliAuth'
import { import {
NAlert, NAlert,
NButton, NButton,
@@ -43,7 +43,7 @@ const props = defineProps<{
}>() }>()
const router = useRouter() const router = useRouter()
const useAuth = useAuthStore() const useAuth = useBiliAuth()
// 移除未使用的 accountInfo // 移除未使用的 accountInfo
const isLoading = ref(false) const isLoading = ref(false)
const message = useMessage() const message = useMessage()

View File

@@ -2,12 +2,12 @@
import { ResponsePointOrder2UserModel } from '@/api/api-models' import { ResponsePointOrder2UserModel } from '@/api/api-models'
import PointOrderCard from '@/components/manage/PointOrderCard.vue' import PointOrderCard from '@/components/manage/PointOrderCard.vue'
import { POINT_API_URL } from '@/data/constants' import { POINT_API_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore' import { useBiliAuth } from '@/store/useBiliAuth'
import { NButton, NEmpty, NFlex, NSpin, useMessage } from 'naive-ui' import { NButton, NEmpty, NFlex, NSpin, useMessage } from 'naive-ui'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
const message = useMessage() const message = useMessage()
const useAuth = useAuthStore() const useAuth = useBiliAuth()
const orders = ref<ResponsePointOrder2UserModel[]>([]) const orders = ref<ResponsePointOrder2UserModel[]>([])
const isLoading = ref(false) const isLoading = ref(false)

View File

@@ -2,12 +2,12 @@
import { ResponsePointHisrotyModel } from '@/api/api-models' import { ResponsePointHisrotyModel } from '@/api/api-models'
import PointHistoryCard from '@/components/manage/PointHistoryCard.vue' import PointHistoryCard from '@/components/manage/PointHistoryCard.vue'
import { POINT_API_URL } from '@/data/constants' import { POINT_API_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore' import { useBiliAuth } from '@/store/useBiliAuth'
import { NButton, NEmpty, NFlex, NSpin, useMessage } from 'naive-ui' import { NButton, NEmpty, NFlex, NSpin, useMessage } from 'naive-ui'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
const message = useMessage() const message = useMessage()
const useAuth = useAuthStore() const useAuth = useBiliAuth()
const isLoading = ref(false) const isLoading = ref(false)
const history = ref<ResponsePointHisrotyModel[]>([]) const history = ref<ResponsePointHisrotyModel[]>([])

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { UserInfo } from '@/api/api-models' import { UserInfo } from '@/api/api-models'
import { POINT_API_URL } from '@/data/constants' import { POINT_API_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore' import { useBiliAuth } from '@/store/useBiliAuth'
import { useRouteHash } from '@vueuse/router' import { useRouteHash } from '@vueuse/router'
import { import {
NAlert, NAlert,
@@ -48,7 +48,7 @@ interface SettingsViewInstance extends ComponentWithReset {
// 设置组件可能需要的方法 // 设置组件可能需要的方法
} }
const useAuth = useAuthStore() const useAuth = useBiliAuth()
const message = useMessage() const message = useMessage()
const realHash = useRouteHash('points', { const realHash = useRouteHash('points', {
mode: 'replace', mode: 'replace',

View File

@@ -2,7 +2,7 @@
import { AddressInfo } from '@/api/api-models' import { AddressInfo } from '@/api/api-models'
import AddressDisplay from '@/components/manage/AddressDisplay.vue' import AddressDisplay from '@/components/manage/AddressDisplay.vue'
import { CURRENT_HOST, POINT_API_URL, THINGS_URL } from '@/data/constants' import { CURRENT_HOST, POINT_API_URL, THINGS_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore' import { useBiliAuth } from '@/store/useBiliAuth'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { import {
FormRules, FormRules,
@@ -41,7 +41,7 @@ type AreaData = {
} }
} }
const useAuth = useAuthStore() const useAuth = useBiliAuth()
const message = useMessage() const message = useMessage()
const isLoading = ref(false) const isLoading = ref(false)
const userAgree = ref(false) const userAgree = ref(false)

View File

@@ -131,7 +131,19 @@
<!-- 签到时间列 --> <!-- 签到时间列 -->
<div class="col-time"> <div class="col-time">
{{ formatDate(item.lastCheckInTime) }} <NTooltip>
<template #trigger>
<NTime
:time="item.lastCheckInTime"
type="relative"
/>
</template>
<template #default>
<NTime
:time="item.lastCheckInTime"
/>
</template>
</NTooltip>
</div> </div>
</div> </div>
</div> </div>
@@ -180,6 +192,7 @@ import {
NSelect, NSelect,
NSpace, NSpace,
NSpin, NSpin,
NTooltip,
} from 'naive-ui'; } from 'naive-ui';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
@@ -341,15 +354,15 @@ onMounted(() => {
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
/* 使用官方阴影变量 */ /* 使用官方阴影变量 */
box-shadow: var(--n-boxShadow1, 0 1px 2px -2px rgba(0, 0, 0, .24), 0 3px 6px 0 rgba(0, 0, 0, .18), 0 5px 12px 4px rgba(0, 0, 0, .12)); box-shadow: var(--box-shadow-1);
margin-bottom: 16px; margin-bottom: 16px;
} }
.ranking-header { .ranking-header {
/* 使用官方背景色变量 */ /* 使用官方背景色变量 */
background-color: var(--n-tableHeaderColor, rgba(255, 255, 255, 0.06)); background-color: var(--table-header-color);
font-weight: var(--n-fontWeightStrong, 500); font-weight: var(--font-weight-strong);
color: var(--n-textColor2, rgba(255, 255, 255, 0.82)); color: var(--text-color-2);
} }
.ranking-row { .ranking-row {
@@ -357,13 +370,13 @@ onMounted(() => {
align-items: center; align-items: center;
padding: 12px 16px; padding: 12px 16px;
/* 使用官方分割线变量 */ /* 使用官方分割线变量 */
border-bottom: 1px solid var(--n-dividerColor, rgba(255, 255, 255, 0.09)); border-bottom: 1px solid var(--divider-color);
transition: background-color 0.3s var(--n-cubicBezierEaseInOut, cubic-bezier(.4, 0, .2, 1)); transition: background-color 0.3s var(--cubic-bezier-ease-in-out);
} }
.ranking-body .ranking-row:hover { .ranking-body .ranking-row:hover {
/* 使用官方悬停背景色变量 */ /* 使用官方悬停背景色变量 */
background-color: var(--n-hoverColor, rgba(255, 255, 255, 0.09)); background-color: var(--hover-color);
} }
.ranking-body .ranking-row:last-child { .ranking-body .ranking-row:last-child {
@@ -372,7 +385,7 @@ onMounted(() => {
.top-three { .top-three {
/* 使用官方条纹背景色变量 */ /* 使用官方条纹背景色变量 */
background-color: var(--n-tableColorStriped, rgba(255, 255, 255, 0.05)); background-color: var(--table-color-striped);
} }
.col-rank { .col-rank {
@@ -409,10 +422,10 @@ onMounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-weight: var(--n-fontWeightStrong, 500); font-weight: var(--font-weight-strong);
/* 使用官方文本和背景色变量 */ /* 使用官方文本和背景色变量 */
color: var(--n-textColor2, rgba(255, 255, 255, 0.82)); color: var(--text-color-2);
background-color: var(--n-actionColor, rgba(255, 255, 255, 0.06)); background-color: var(--action-color);
} }
/* 保持奖牌颜色在暗色模式下也清晰可见 */ /* 保持奖牌颜色在暗色模式下也清晰可见 */
@@ -432,30 +445,30 @@ onMounted(() => {
} }
.user-name { .user-name {
font-weight: var(--n-fontWeightStrong, 500); font-weight: var(--font-weight-strong);
margin-right: 8px; margin-right: 8px;
} }
.user-authed { .user-authed {
background-color: var(--n-successColor, #63e2b7); background-color: var(--success-color);
color: white; color: white;
padding: 2px 6px; padding: 2px 6px;
border-radius: 10px; border-radius: 10px;
font-size: var(--n-fontSizeTiny, 12px); font-size: var(--font-size-tiny);
} }
.days-count, .days-count,
.count-value { .count-value {
font-weight: var(--n-fontWeightStrong, 500); font-weight: var(--font-weight-strong);
font-size: var(--n-fontSizeLarge, 15px); font-size: var(--font-size-large);
color: var(--n-infoColor, #70c0e8); color: var(--info-color);
margin-right: 4px; margin-right: 4px;
} }
.days-text, .days-text,
.count-text { .count-text {
color: var(--n-textColor3, rgba(255, 255, 255, 0.52)); color: var(--text-color-3);
font-size: var(--n-fontSizeTiny, 12px); font-size: var(--font-size-tiny);
} }
.ranking-footer { .ranking-footer {
@@ -463,6 +476,6 @@ onMounted(() => {
display: flex; display: flex;
justify-content: center; justify-content: center;
/* 使用官方背景色变量 */ /* 使用官方背景色变量 */
background-color: var(--n-tableHeaderColor, rgba(255, 255, 255, 0.06)); background-color: var(--table-header-color);
} }
</style> </style>

View File

@@ -1,5 +1,6 @@
<template> <template>
<div> <div>
<!-- 加载中显示加载动画 -->
<NSpin :show="isLoading"> <NSpin :show="isLoading">
<component <component
:is="selectedTemplate?.component" :is="selectedTemplate?.component"
@@ -14,8 +15,10 @@
@request-song="requestSong" @request-song="requestSong"
/> />
</NSpin> </NSpin>
<!-- 主播自定义按钮 -->
<NButton <NButton
v-if="selectedTemplate?.settingName && userInfo?.id == accountInfo.id" v-if="selectedTemplate?.settingName && userInfo?.id === accountInfo.id"
type="info" type="info"
size="small" size="small"
style="position: absolute; right: 32px; top: 20px; z-index: 1000; border: solid 3px #dfdfdf;" style="position: absolute; right: 32px; top: 20px; z-index: 1000; border: solid 3px #dfdfdf;"
@@ -23,6 +26,8 @@
> >
自定义 自定义
</NButton> </NButton>
<!-- 设置弹窗 -->
<NModal <NModal
v-model:show="showSettingModal" v-model:show="showSettingModal"
style="max-width: 90vw; width: 800px;" style="max-width: 90vw; width: 800px;"
@@ -39,152 +44,244 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { DownloadConfig, useAccount } from '@/api/account'; import { DownloadConfig, useAccount } from '@/api/account';
import { Setting_LiveRequest, SongRequestInfo, SongsInfo, UserInfo } from '@/api/api-models'; import { Setting_LiveRequest, SongRequestInfo, SongsInfo, UserInfo } from '@/api/api-models';
import { QueryGetAPI, QueryPostAPIWithParams } from '@/api/query'; import { QueryGetAPI, QueryPostAPIWithParams } from '@/api/query';
import { SONG_API_URL, SONG_REQUEST_API_URL, SongListTemplateMap } from '@/data/constants'; import { SONG_API_URL, SONG_REQUEST_API_URL, SongListTemplateMap } from '@/data/constants';
import { ConfigItemDefinition } from '@/data/VTsuruTypes'; import { ConfigItemDefinition } from '@/data/VTsuruTypes';
import { useBiliAuth } from '@/store/useBiliAuth';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import { addSeconds } from 'date-fns'; import { addSeconds } from 'date-fns';
import { NButton, NModal, NSpin, useMessage, NFlex, NIcon, NInput, NInputGroup, NInputGroupLabel, NTag, NTooltip, NSelect, NSpace } from 'naive-ui'; import { NButton, NModal, NSpin, useMessage } from 'naive-ui';
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { GetGuardColor, getUserAvatarUrl, isDarkMode } from '@/Utils';
const accountInfo = useAccount(); // 用户账号信息
const nextRequestTime = useStorage('SongList.NextRequestTime', new Date()); const accountInfo = useAccount();
// 下次点歌时间
const nextRequestTime = useStorage('SongList.NextRequestTime', new Date());
// 点歌冷却时间(秒)
const minRequestTime = 30;
// 设置弹窗显示状态
const showSettingModal = ref(false);
const minRequestTime = 30; // 组件属性
const showSettingModal = ref(false); const props = defineProps<{
biliInfo: any | undefined; // B站信息
userInfo: UserInfo | undefined; // 用户信息
template?: string | undefined; // 模板名称
fakeData?: SongsInfo[]; // 测试数据
}>();
const props = defineProps<{ // 计算当前使用的模板类型
biliInfo: any | undefined; const componentType = computed(() => {
userInfo: UserInfo | undefined;
template?: string | undefined;
fakeData?: SongsInfo[];
}>();
const componentType = computed(() => {
return props.template ?? props.userInfo?.extra?.templateTypes['songlist']?.toLowerCase(); return props.template ?? props.userInfo?.extra?.templateTypes['songlist']?.toLowerCase();
}); });
const currentData = ref<SongsInfo[]>();
const dynamicConfigRef = ref(); // 数据状态
const selectedTemplateConfig = computed(() => { const currentData = ref<SongsInfo[]>([]); // 歌单数据
const dynamicConfigRef = ref(); // 动态配置引用
const songsActive = ref<SongRequestInfo[]>([]); // 当前点歌列表
const settings = ref<Setting_LiveRequest>({ // 点歌设置
allowFromWeb: false,
allowAnonymousFromWeb: false,
orderPrefix: '',
} as Setting_LiveRequest);
// 加载状态
const isDataLoading = ref(true);
const isConfigLoading = ref(true);
const isLoading = computed(() => isDataLoading.value || isConfigLoading.value);
// 计算属性
const selectedTemplateConfig = computed(() => {
if (dynamicConfigRef.value?.Config) { if (dynamicConfigRef.value?.Config) {
return dynamicConfigRef.value?.Config as ConfigItemDefinition[]; return dynamicConfigRef.value?.Config as ConfigItemDefinition[];
} }
return undefined; return undefined;
}); });
const selectedTemplate = computed(() => {
if (componentType.value) {
return SongListTemplateMap[componentType.value];
}
return SongListTemplateMap[''];
});
const currentConfig = ref();
const isDataLoading = ref(true); const selectedTemplate = computed(() => {
const isConfigLoading = ref(true); const type = componentType.value;
const isLoading = computed(() => isDataLoading.value || isConfigLoading.value); return type ? SongListTemplateMap[type] : SongListTemplateMap[''];
});
const message = useMessage(); const currentConfig = ref({}); // 当前配置
const message = useMessage(); // 消息提示
const biliAuth = useBiliAuth(); // B站授权
const errMessage = ref(''); /**
const songsActive = ref<SongRequestInfo[]>([]); * 获取点歌设置和当前点歌列表
const settings = ref<Setting_LiveRequest>({} as Setting_LiveRequest); */
async function getSongRequestInfo() {
if (!props.userInfo?.id) return { songs: [], setting: settings.value };
async function getSongRequestInfo() {
try { try {
const data = await QueryGetAPI<{ songs: SongRequestInfo[]; setting: Setting_LiveRequest; }>( const data = await QueryGetAPI<{ songs: SongRequestInfo[]; setting: Setting_LiveRequest; }>(
SONG_REQUEST_API_URL + 'get-active-and-settings', SONG_REQUEST_API_URL + 'get-active-and-settings',
{ {
id: props.userInfo?.id, id: props.userInfo.id,
}, },
); );
if (data.code == 200) {
if (data.code === 200) {
return data.data; return data.data;
}
} catch (err) { }
return {} as { songs: SongRequestInfo[]; setting: Setting_LiveRequest; };
}
async function getSongs() {
isDataLoading.value = true;
await QueryGetAPI<SongsInfo[]>(SONG_API_URL + 'get', {
id: props.userInfo?.id,
})
.then((data) => {
if (data.code == 200) {
currentData.value = data.data;
} else { } else {
errMessage.value = data.message; message.warning(`获取点歌设置失败: ${data.message}`);
}
} catch (err) {
console.error('获取点歌设置出错:', err);
message.error(`获取点歌设置出错: ${err instanceof Error ? err.message : String(err)}`);
}
return { songs: [], setting: settings.value };
}
/**
* 获取歌单数据
*/
async function getSongs() {
if (!props.userInfo?.id) {
isDataLoading.value = false;
return;
}
isDataLoading.value = true;
try {
const data = await QueryGetAPI<SongsInfo[]>(SONG_API_URL + 'get', {
id: props.userInfo.id,
});
if (data.code === 200) {
currentData.value = data.data || [];
} else {
message.error('加载歌单失败: ' + data.message); message.error('加载歌单失败: ' + data.message);
} }
}) } catch (err) {
.catch((err) => { console.error('加载歌单出错:', err);
message.error('加载失败: ' + err); message.error(`加载歌单失败: ${err instanceof Error ? err.message : String(err)}`);
}) } finally {
.finally(() => {
isDataLoading.value = false; isDataLoading.value = false;
});
} }
async function getConfig() { }
if (!selectedTemplateConfig.value || !selectedTemplate.value!.settingName) {
if (!selectedTemplate.value!.settingName) { /**
* 获取模板配置
*/
async function getConfig() {
if (!selectedTemplate.value?.settingName) {
isConfigLoading.value = false; isConfigLoading.value = false;
return;
} }
if (!selectedTemplateConfig.value) {
// 等待模板配置加载完成后再获取配置
setTimeout(() => getConfig(), 100);
return; return;
} }
isConfigLoading.value = true; isConfigLoading.value = true;
try { try {
const data = await DownloadConfig(selectedTemplate.value!.settingName, props.userInfo?.id); const data = await DownloadConfig(
selectedTemplate.value.settingName,
props.userInfo?.id
);
if (data.msg) { if (data.msg) {
currentConfig.value = dynamicConfigRef.value?.DefaultConfig ?? {}; currentConfig.value = dynamicConfigRef.value?.DefaultConfig ?? {};
} else { } else {
currentConfig.value = data.data; currentConfig.value = data.data || {};
} }
} catch (err) { } catch (err) {
message.error('加载配置失败: ' + err); console.error('加载配置出错:', err);
message.error(`加载配置失败: ${err instanceof Error ? err.message : String(err)}`);
currentConfig.value = dynamicConfigRef.value?.DefaultConfig ?? {};
} finally { } finally {
isConfigLoading.value = false; isConfigLoading.value = false;
} }
} }
async function requestSong(song: SongsInfo) {
if (song.options || !settings.value.allowFromWeb || (settings.value.allowFromWeb && !settings.value.allowAnonymousFromWeb)) { /**
navigator.clipboard.writeText(`${settings.value.orderPrefix} ${song.name}`); * 复制文本到剪贴板
if (!settings.value.allowAnonymousFromWeb) { */
message.warning('主播不允许匿名点歌, 需要从网页点歌的话请注册登录, 点歌弹幕已复制到剪切板'); function copyToClipboard(text: string, sendMessage: boolean = true) {
} navigator.clipboard.writeText(text)
else if (!accountInfo.value.id) { .then(() => {
message.warning('要从网页点歌请先登录, 点歌弹幕已复制到剪切板'); if (sendMessage) {
} else {
message.success('复制成功'); message.success('复制成功');
} }
} else { })
if (props.userInfo) { .catch(err => {
if (!accountInfo.value.id && nextRequestTime.value > new Date()) { console.error('复制失败:', err);
message.warning('距离点歌冷却还有' + (nextRequestTime.value.getTime() - new Date().getTime()) / 1000 + '秒'); message.error('复制失败,请重试');
});
}
/**
* 点歌处理
*/
async function requestSong(song: SongsInfo) {
if (!song) return;
const orderText = `${settings.value.orderPrefix || ''} ${song.name}`;
// 检查是否需要复制到剪贴板而不是直接点歌
const shouldCopyOnly = song.options ||
!settings.value.allowFromWeb ||
(settings.value.allowFromWeb && !settings.value.allowAnonymousFromWeb && !accountInfo.value.id && !biliAuth.isAuthed);
if (shouldCopyOnly) {
copyToClipboard(orderText, false);
if (song.options) {
message.info('此项目有特殊要求, 请在直播间内点歌, 点歌弹幕已复制到剪切板');
} else if (!settings.value.allowAnonymousFromWeb && !accountInfo.value.id && !biliAuth.isAuthed) {
message.info('主播不允许匿名点歌, 需要从网页点歌的话请注册登录, 点歌弹幕已复制到剪切板');
} else if (!settings.value.allowFromWeb) {
message.info('主播不允许从网页点歌, 点歌弹幕已复制到剪切板');
}
return; return;
} }
// 执行网页点歌
if (!props.userInfo?.id) {
message.error('无法获取主播信息,无法完成点歌');
return;
}
// 检查点歌冷却时间
if (!accountInfo.value.id && nextRequestTime.value > new Date()) {
const remainingSeconds = Math.ceil((nextRequestTime.value.getTime() - new Date().getTime()) / 1000);
message.warning(`距离点歌冷却还有${remainingSeconds}`);
return;
}
try { try {
const data = await QueryPostAPIWithParams(SONG_REQUEST_API_URL + 'add-from-web', { const data = await QueryPostAPIWithParams(SONG_REQUEST_API_URL + 'add-from-web', {
target: props.userInfo?.id, target: props.userInfo.id,
song: song.key, song: song.key,
}); });
if (data.code == 200) { if (data.code === 200) {
message.success('点歌成功'); message.success('点歌成功');
nextRequestTime.value = addSeconds(new Date(), minRequestTime); nextRequestTime.value = addSeconds(new Date(), minRequestTime);
// 重新获取当前点歌列表,更新界面
const songRequestInfo = await getSongRequestInfo();
if (songRequestInfo) {
songsActive.value = songRequestInfo.songs;
}
} else { } else {
message.error('点歌失败: ' + data.message); message.error('点歌失败: ' + data.message);
} }
} catch (err) { } catch (err) {
message.error('点歌失败: ' + err); console.error('点歌出错:', err);
} message.error(`点歌失败: ${err instanceof Error ? err.message : String(err)}`);
}
}
} }
}
watch( // 监听动态配置变化,重新获取配置
watch(
() => dynamicConfigRef.value, () => dynamicConfigRef.value,
(newValue) => { (newValue) => {
if (newValue?.Config) { if (newValue?.Config) {
@@ -192,33 +289,52 @@ import { GetGuardColor, getUserAvatarUrl, isDarkMode } from '@/Utils';
} }
}, },
{ immediate: false } { immediate: false }
); );
onMounted(async () => { // 监听用户ID变化重新加载数据
isDataLoading.value = true; watch(
() => props.userInfo?.id,
() => {
if (!props.fakeData) {
getSongs();
getSongRequestInfo().then(info => {
if (info) {
songsActive.value = info.songs || [];
settings.value = info.setting || settings.value;
}
});
}
}
);
// 组件挂载时初始化
onMounted(async () => {
if (!props.fakeData) { if (!props.fakeData) {
try { try {
await getSongs(); // 并行加载歌单和点歌设置
const r = await getSongRequestInfo(); await Promise.all([
if (r) { getSongs(),
songsActive.value = r.songs; getSongRequestInfo().then(info => {
settings.value = r.setting; if (info) {
songsActive.value = info.songs || [];
settings.value = info.setting || settings.value;
} }
})
]);
} catch (err) { } catch (err) {
message.error('加载失败: ' + err); console.error('初始化失败:', err);
console.error(err); message.error(`初始化失败: ${err instanceof Error ? err.message : String(err)}`);
} finally {
isDataLoading.value = false; isDataLoading.value = false;
isConfigLoading.value = false;
} }
} else { } else {
currentData.value = props.fakeData; // 测试模式使用假数据
currentData.value = props.fakeData || [];
isDataLoading.value = false; isDataLoading.value = false;
isConfigLoading.value = false;
} }
if (!selectedTemplate.value?.settingName) { if (!selectedTemplate.value?.settingName) {
isConfigLoading.value = false; isConfigLoading.value = false;
} }
}); });
</script> </script>

View File

@@ -4,11 +4,14 @@ import { SongsInfo } from '@/api/api-models'
import SongList from '@/components/SongList.vue' import SongList from '@/components/SongList.vue'
import { SongListConfigType } from '@/data/TemplateTypes' import { SongListConfigType } from '@/data/TemplateTypes'
import LiveRequestOBS from '@/views/obs/LiveRequestOBS.vue' import LiveRequestOBS from '@/views/obs/LiveRequestOBS.vue'
import { getSongRequestTooltip } from './utils/songRequestUtils'
import { CloudAdd20Filled, ChevronLeft24Filled, ChevronRight24Filled } from '@vicons/fluent' import { CloudAdd20Filled, ChevronLeft24Filled, ChevronRight24Filled } from '@vicons/fluent'
import { NButton, NCard, NCollapse, NCollapseItem, NDivider, NIcon, NTooltip, useMessage } from 'naive-ui' import { NButton, NCard, NCollapse, NCollapseItem, NDivider, NIcon, NTooltip, useMessage } from 'naive-ui'
import { h, ref, onMounted, onUnmounted } from 'vue' import { h, ref, onMounted, onUnmounted } from 'vue'
import { useBiliAuth } from '@/store/useBiliAuth'
const accountInfo = useAccount() const accountInfo = useAccount()
const biliAuth = useBiliAuth()
//所有模板都应该有这些 //所有模板都应该有这些
const props = defineProps<SongListConfigType>() const props = defineProps<SongListConfigType>()
@@ -75,7 +78,7 @@ const buttons = (song: SongsInfo) => [
size: 'small', size: 'small',
circle: true, circle: true,
loading: isLoading.value == song.key, loading: isLoading.value == song.key,
disabled: !accountInfo, disabled: !accountInfo.value,
onClick: () => { onClick: () => {
isLoading.value = song.key isLoading.value = song.key
emits('requestSong', song) emits('requestSong', song)
@@ -86,12 +89,7 @@ const buttons = (song: SongsInfo) => [
icon: () => h(NIcon, { component: CloudAdd20Filled }), icon: () => h(NIcon, { component: CloudAdd20Filled }),
}, },
), ),
default: () => default: () => getSongRequestTooltip(song, props.liveRequestSettings)
!props.liveRequestSettings?.allowFromWeb || song.options
? '点歌 | 用户不允许从网页点歌, 点击后将复制点歌内容到剪切板'
: !accountInfo
? '点歌 | 你需要登录后才能点歌'
: '点歌',
}, },
) )
: undefined, : undefined,

View File

@@ -5,9 +5,11 @@ import { FunctionTypes, SongsInfo } from '@/api/api-models'
import SongPlayer from '@/components/SongPlayer.vue' import SongPlayer from '@/components/SongPlayer.vue'
import { SongListConfigType } from '@/data/TemplateTypes' import { SongListConfigType } from '@/data/TemplateTypes'
import LiveRequestOBS from '@/views/obs/LiveRequestOBS.vue' import LiveRequestOBS from '@/views/obs/LiveRequestOBS.vue'
import { getSongRequestTooltip, getSongRequestButtonType } from './utils/songRequestUtils'
import { CloudAdd20Filled, Play24Filled } from '@vicons/fluent' import { CloudAdd20Filled, Play24Filled } from '@vicons/fluent'
import { useWindowSize } from '@vueuse/core' import { useWindowSize } from '@vueuse/core'
import { throttle } from 'lodash' import { throttle } from 'lodash'
import { useBiliAuth } from '@/store/useBiliAuth'
import { import {
NButton, NButton,
NCard, NCard,
@@ -36,6 +38,7 @@ const container = ref()
const index = ref(20) const index = ref(20)
const accountInfo = useAccount() const accountInfo = useAccount()
const biliAuth = useBiliAuth()
const selectedTag = ref('') const selectedTag = ref('')
const selectedSong = ref<SongsInfo>() const selectedSong = ref<SongsInfo>()
@@ -296,7 +299,7 @@ function loadMore() {
<template #trigger> <template #trigger>
<NButton <NButton
size="small" size="small"
:type="liveRequestSettings?.allowFromWeb == false || item.options ? 'warning' : 'info'" :type="getSongRequestButtonType(item, liveRequestSettings, !!accountInfo, biliAuth.isAuthed)"
:loading="isLoading == item.key" :loading="isLoading == item.key"
@click="() => { @click="() => {
isLoading = item.key isLoading = item.key
@@ -310,13 +313,7 @@ function loadMore() {
</template> </template>
</NButton> </NButton>
</template> </template>
{{ {{ getSongRequestTooltip(item, liveRequestSettings) }}
liveRequestSettings?.allowFromWeb == false || item.options
? '点歌 | 用户或此歌曲不允许从网页点歌, 点击后将复制点歌内容到剪切板'
: !accountInfo
? '点歌 | 你需要登录后才能点歌'
: '点歌'
}}
</NTooltip> </NTooltip>
<NPopover <NPopover

View File

@@ -13,6 +13,9 @@ import { SongFrom, SongsInfo, SongRequestOption } from '@/api/api-models';
import FiveSingIcon from '@/svgs/fivesing.svg'; import FiveSingIcon from '@/svgs/fivesing.svg';
import { SquareArrowForward24Filled, ArrowCounterclockwise20Filled, ArrowSortDown20Filled, ArrowSortUp20Filled } from '@vicons/fluent'; import { SquareArrowForward24Filled, ArrowCounterclockwise20Filled, ArrowSortDown20Filled, ArrowSortUp20Filled } from '@vicons/fluent';
import { List } from 'linqts'; import { List } from 'linqts';
import { useAccount } from '@/api/account';
import { getSongRequestTooltip, getSongRequestConfirmText } from './utils/songRequestUtils';
import { useBiliAuth } from '@/store/useBiliAuth';
// Interface Tab - can be reused for both language and tag buttons // Interface Tab - can be reused for both language and tag buttons
interface FilterButton { interface FilterButton {
@@ -326,6 +329,9 @@ watch(allArtists, (newArtists) => {
} }
}); });
const accountInfo = useAccount();
const biliAuth = useBiliAuth();
const randomOrder = () => { const randomOrder = () => {
const songsToChooseFrom = filteredAndSortedSongs.value.length > 0 ? filteredAndSortedSongs.value : props.data ?? []; const songsToChooseFrom = filteredAndSortedSongs.value.length > 0 ? filteredAndSortedSongs.value : props.data ?? [];
if (songsToChooseFrom.length === 0) { if (songsToChooseFrom.length === 0) {
@@ -347,10 +353,12 @@ const randomOrder = () => {
}; };
function onSongClick(song: SongsInfo) { function onSongClick(song: SongsInfo) {
const tooltip = getSongRequestTooltip(song, props.liveRequestSettings);
const confirmText = getSongRequestConfirmText(song);
window.$modal.create({ window.$modal.create({
preset: 'dialog', preset: 'dialog',
title: '点歌', title: '点歌',
content: `确定要点 ${song.name}`, content: `${confirmText}${tooltip !== '点歌' ? '\n' + tooltip : ''}`,
positiveText: '点歌', positiveText: '点歌',
negativeText: '算了', negativeText: '算了',
onPositiveClick: () => { onPositiveClick: () => {
@@ -928,12 +936,17 @@ export const Config = defineTemplateConfig([
<td> <td>
<span class="song-name"> <span class="song-name">
<component :is="GetPlayButton(song)" /> <component :is="GetPlayButton(song)" />
<NTooltip>
<template #trigger>
<span <span
style="cursor: pointer;" style="cursor: pointer;"
@click="onSongClick(song)" @click="onSongClick(song)"
> >
{{ song.name }} {{ song.name }}
</span> </span>
</template>
{{ getSongRequestTooltip(song, props.liveRequestSettings) }}
</NTooltip>
</span> </span>
</td> </td>
<td> <td>

View File

@@ -0,0 +1,135 @@
import { useAccount } from '@/api/account'
import { Setting_LiveRequest, SongsInfo, UserInfo } from '@/api/api-models'
import { useBiliAuth } from '@/store/useBiliAuth';
/**
* 获取点歌按钮的tooltip文本
* @param song 歌曲信息
* @param liveRequestSettings 直播点歌设置
* @param isLoggedIn 用户是否已登录
* @param isBiliAuthed B站是否已授权
* @returns tooltip文本
*/
export function getSongRequestTooltip(
song: SongsInfo,
liveRequestSettings: Setting_LiveRequest | undefined
): string {
const accountInfo = useAccount();
const biliAuth = useBiliAuth();
// 歌曲有特殊要求
if (song.options) {
return '点歌 | 此项目有特殊要求, 请在直播间内点歌, 点击后将复制点歌内容到剪切板'
}
// 主播不允许从网页点歌
if (liveRequestSettings?.allowFromWeb === false) {
return '点歌 | 主播不允许从网页点歌, 点击后将复制点歌内容到剪切板'
}
// 主播不允许匿名点歌且用户未登录
if (liveRequestSettings?.allowFromWeb &&
!liveRequestSettings.allowAnonymousFromWeb &&
!accountInfo.value.id &&
!biliAuth.isAuthed) {
return '点歌 | 主播不允许匿名点歌, 需要从网页点歌的话请注册登录, 点击后将复制点歌内容到剪切板'
}
// 用户未登录
if (!accountInfo.value.id && !biliAuth.isAuthed) {
return '点歌 | 根据主播设置, 需要登录后才能点歌'
}
return '点歌'
}
/**
* 获取点歌按钮的类型
* @param song 歌曲信息
* @param liveRequestSettings 直播点歌设置
* @param isLoggedIn 用户是否已登录
* @param isBiliAuthed B站是否已授权
* @returns 按钮类型
*/
export function getSongRequestButtonType(
song: SongsInfo,
liveRequestSettings: Setting_LiveRequest | undefined,
isLoggedIn: boolean = true,
isBiliAuthed: boolean = false
): 'warning' | 'info' {
if (song.options ||
liveRequestSettings?.allowFromWeb === false ||
(liveRequestSettings?.allowFromWeb &&
!liveRequestSettings.allowAnonymousFromWeb &&
!isLoggedIn &&
!isBiliAuthed)) {
return 'warning'
}
return 'info'
}
/**
* 判断用户是否可以点歌
* @param song 歌曲信息
* @param userInfo 主播信息
* @param liveRequestSettings 直播点歌设置
* @param isLoggedIn 用户是否已登录
* @param isBiliAuthed B站是否已授权
* @param nextRequestTime 下次点歌时间
* @returns 是否可以点歌和原因
*/
export function canRequestSong(
song: SongsInfo,
userInfo: UserInfo | undefined,
liveRequestSettings: Setting_LiveRequest | undefined,
isLoggedIn: boolean,
isBiliAuthed: boolean = false,
nextRequestTime?: Date
): { canRequest: boolean; reason?: string; shouldCopyOnly?: boolean } {
// 检查主播信息
if (!userInfo?.id) {
return { canRequest: false, reason: '无法获取主播信息,无法完成点歌' }
}
// 判断是否应该只复制到剪贴板
const shouldCopyOnly = song.options ||
!liveRequestSettings?.allowFromWeb ||
(liveRequestSettings?.allowFromWeb &&
!liveRequestSettings.allowAnonymousFromWeb &&
!isLoggedIn &&
!isBiliAuthed)
if (shouldCopyOnly) {
let reason = ''
if (song.options) {
reason = '此项目有特殊要求, 请在直播间内点歌, 点歌弹幕已复制到剪切板'
} else if (!liveRequestSettings?.allowAnonymousFromWeb && !isLoggedIn && !isBiliAuthed) {
reason = '主播不允许匿名点歌, 需要从网页点歌的话请注册登录, 点歌弹幕已复制到剪切板'
} else if (!liveRequestSettings?.allowFromWeb) {
reason = '主播不允许从网页点歌, 点歌弹幕已复制到剪切板'
}
return { canRequest: false, reason, shouldCopyOnly: true }
}
// 检查点歌冷却时间
if (!isLoggedIn && nextRequestTime && nextRequestTime > new Date()) {
const remainingSeconds = Math.ceil((nextRequestTime.getTime() - new Date().getTime()) / 1000)
return {
canRequest: false,
reason: `距离点歌冷却还有${remainingSeconds}`
}
}
return { canRequest: true }
}
/**
* 生成点歌的提示文本
* @param song 歌曲信息
* @returns 点歌提示文本
*/
export function getSongRequestConfirmText(song: SongsInfo): string {
return `确定要点 ${song.name}`
}