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,
sorter: (a: HistoryItem, b: HistoryItem) => a.timestamp - b.timestamp,
render: (row: HistoryItem) => {
return h(NTime, {
time: new Date(row.timestamp),
format: 'yyyy-MM-dd HH:mm:ss'
return h(NTooltip, {
}, {
trigger: () => h(NTime, {
time: row.timestamp,
type: 'relative'
}),
default: () => new Date(row.timestamp).toLocaleString()
});
}
},
},
{
title: '操作名称',

View File

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

View File

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

View File

@@ -43,7 +43,10 @@
</NFormItem>
<template v-if="serverSetting.enableCheckIn">
<NFormItem label="签到命令">
<NFormItem
label="签到命令"
required
>
<NInputGroup>
<NInput
:value="serverSetting.checkInKeyword"
@@ -188,23 +191,25 @@
</template>
</NAlert>
</div>
<!-- 使用 AutoActionEditor 编辑 action 配置 -->
<NFormItem label="签到成功回复">
<NDivider title-placement="left">
签到成功回复
</NDivider>
<AutoActionEditor
:action="config.successAction"
:hide-name="true"
:hide-enabled="true"
:custom-test-context="customTestContext"
/>
</NFormItem>
<NFormItem label="签到冷却回复">
<NDivider title-placement="left">
签到冷却回复
</NDivider>
<AutoActionEditor
:action="config.cooldownAction"
:hide-name="true"
:hide-enabled="true"
:custom-test-context="customTestContext"
/>
</NFormItem>
</template>
<NFormItem>
@@ -384,7 +389,7 @@ import { CHECKIN_API_URL } from '@/data/constants';
import { GuidUtils } from '@/Utils';
import { Info24Filled } from '@vicons/fluent';
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 AutoActionEditor from '../AutoActionEditor.vue';
import TemplateHelper from '../TemplateHelper.vue';
@@ -398,6 +403,15 @@ const config = autoActionStore.checkInModule.checkInConfig;
const accountInfo = useAccount();
const isLoading = ref(false);
const customTestContext = ref({
checkin: {
points: 0,
consecutiveDays: 0,
todayRank: 0,
time: new Date()
}
});
// 签到模板的特定占位符
const checkInPlaceholders = [
{ name: '{{checkin.points}}', description: '获得的总积分' },
@@ -563,7 +577,14 @@ const rankingColumns: DataTableColumns<CheckInRankingInfo> = [
title: '最近签到时间',
key: 'lastCheckInTime',
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'
},

View File

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

2
src/components.d.ts vendored
View File

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

View File

@@ -1,6 +1,6 @@
import { GetSelfAccount, useAccount, UpdateAccountLoop } from "@/api/account";
import { QueryGetAPI } from "@/api/query";
import { useAuthStore } from "@/store/useAuthStore";
import { useBiliAuth } from "@/store/useBiliAuth";
import { useNotificationStore } from "@/store/useNotificationStore";
import { createDiscreteApi, NText, NFlex, NButton } from "naive-ui";
import { BASE_API_URL, isTauri, apiFail } from "./constants";
@@ -64,7 +64,7 @@ async function InitOther() {
InitTTS()
await GetSelfAccount()
const account = useAccount()
const useAuth = useAuthStore()
const useAuth = useBiliAuth()
if (account.value.id) {
if (account.value.biliUserAuthInfo && !useAuth.currentToken) {
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 { computed, ref } from 'vue'
export const useAuthStore = defineStore('BiliAuth', () => {
export const useBiliAuth = defineStore('BiliAuth', () => {
const biliAuth = ref<BiliAuthModel>({} as BiliAuthModel)
const biliTokens = useStorage<

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
import { useUser } from '@/api/user';
import RegisterAndLogin from '@/components/RegisterAndLogin.vue';
import { FETCH_API } from '@/data/constants'; // 移除了未使用的 AVATAR_URL
import { useAuthStore } from '@/store/useAuthStore';
import { useBiliAuth } from '@/store/useBiliAuth';
import {
BookCoins20Filled,
CalendarClock24Filled,
@@ -47,7 +47,7 @@
const router = useRouter(); // 获取 router 实例
const message = useMessage();
const accountInfo = useAccount(); // 获取当前登录账户信息
const useAuth = useAuthStore(); // 获取认证状态 Store
const useAuth = useBiliAuth(); // 获取认证状态 Store
// 路由参数
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 PointGoodsItem from '@/components/manage/PointGoodsItem.vue'
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 { useRouteHash } from '@vueuse/router'
import { useStorage } from '@vueuse/core'
@@ -52,7 +52,7 @@ import PointUserManage from './PointUserManage.vue'
const message = useMessage()
const accountInfo = useAccount()
const dialog = useDialog()
const useBiliAuth = useAuthStore()
const biliAuth = useBiliAuth()
const formRef = ref()
const isUpdating = 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 = {
goods: {
type: GoodsTypes.Virtual,

View File

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

View File

@@ -2,12 +2,12 @@
import { ResponsePointOrder2UserModel } from '@/api/api-models'
import PointOrderCard from '@/components/manage/PointOrderCard.vue'
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 { onMounted, ref } from 'vue'
const message = useMessage()
const useAuth = useAuthStore()
const useAuth = useBiliAuth()
const orders = ref<ResponsePointOrder2UserModel[]>([])
const isLoading = ref(false)

View File

@@ -2,12 +2,12 @@
import { ResponsePointHisrotyModel } from '@/api/api-models'
import PointHistoryCard from '@/components/manage/PointHistoryCard.vue'
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 { onMounted, ref } from 'vue'
const message = useMessage()
const useAuth = useAuthStore()
const useAuth = useBiliAuth()
const isLoading = ref(false)
const history = ref<ResponsePointHisrotyModel[]>([])

View File

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

View File

@@ -2,7 +2,7 @@
import { AddressInfo } from '@/api/api-models'
import AddressDisplay from '@/components/manage/AddressDisplay.vue'
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 {
FormRules,
@@ -41,7 +41,7 @@ type AreaData = {
}
}
const useAuth = useAuthStore()
const useAuth = useBiliAuth()
const message = useMessage()
const isLoading = ref(false)
const userAgree = ref(false)

View File

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

View File

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

View File

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

View File

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

View File

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