更新项目配置,删除不必要的文件,优化依赖项,修复类型定义,添加新歌单样式

This commit is contained in:
2025-03-31 16:32:57 +08:00
parent 891a922ab1
commit 73c738b42d
28 changed files with 2687 additions and 1072 deletions

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { QueryGetAPI } from '@/api/query'
import { VTSURU_API_URL } from '@/data/constants'
import vtb from '@/svgs/ic_vtuber.svg'
import {
BookCoins20Filled,
Info24Filled,
@@ -15,6 +14,7 @@ import { AnalyticsSharp, Calendar, Chatbox, ListCircle, MusicalNote } from '@vic
import { useWindowSize } from '@vueuse/core'
import { NButton, NDivider, NEllipsis, NFlex, NGradientText, NGrid, NGridItem, NIcon, NNumberAnimation, NSpace, NText, NTooltip } from 'naive-ui'
import { onMounted, ref } from 'vue'
import vtb from '@/svgs/ic_vtuber.svg'
const { width } = useWindowSize()

View File

@@ -189,15 +189,35 @@ onMounted(async () => {
</script>
<template>
<NLayoutContent v-if="!id" style="height: 100vh">
<NResult status="error" title="输入的uId无效" description="再检查检查" />
<NLayoutContent
v-if="!id"
style="height: 100vh"
>
<NResult
status="error"
title="输入的uId无效"
description="再检查检查"
/>
</NLayoutContent>
<NLayoutContent v-else-if="notfount" style="height: 100vh">
<NResult status="error" title="未找到指定 uId 的用户" description="或者是没有进行认证" />
<NLayoutContent
v-else-if="notfount"
style="height: 100vh"
>
<NResult
status="error"
title="未找到指定 uId 的用户"
description="或者是没有进行认证"
/>
</NLayoutContent>
<NLayout v-else style="height: 100vh">
<NLayout
v-else
style="height: 100vh"
>
<NLayoutHeader style="height: 50px; padding: 5px 15px 5px 15px">
<NPageHeader :subtitle="($route.meta.title as string) ?? ''" style="margin-top: 6px">
<NPageHeader
:subtitle="($route.meta.title as string) ?? ''"
style="margin-top: 6px"
>
<template #extra>
<NSpace align="center">
<NSwitch
@@ -233,8 +253,8 @@ onMounted(async () => {
<NButton
style="right: 0px; position: relative"
type="primary"
@click="$router.push({ name: 'manage-index' })"
size="small"
@click="$router.push({ name: 'manage-index' })"
>
<template #icon>
<NIcon :component="WindowWrench20Filled" />
@@ -255,13 +275,25 @@ onMounted(async () => {
</NSpace>
</template>
<template #title>
<NButton text tag="a" @click="$router.push({ name: 'index' })">
<NText strong style="font-size: 1.5rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)"> VTSURU </NText>
<NButton
text
tag="a"
@click="$router.push({ name: 'index' })"
>
<NText
strong
style="font-size: 1.5rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)"
>
VTSURU
</NText>
</NButton>
</template>
</NPageHeader>
</NLayoutHeader>
<NLayout has-sider style="height: calc(100vh - 50px)">
<NLayout
has-sider
style="height: calc(100vh - --vtsuru-header-height)"
>
<NLayoutSider
ref="sider"
show-trigger
@@ -270,11 +302,18 @@ onMounted(async () => {
:collapsed-width="64"
:width="180"
:native-scrollbar="false"
style="height: 100%"
style="height: calc(100vh - --vtsuru-header-height)"
>
<Transition>
<div v-if="userInfo?.streamerInfo" style="margin-top: 8px">
<NSpace vertical justify="center" align="center">
<div
v-if="userInfo?.streamerInfo"
style="margin-top: 8px"
>
<NSpace
vertical
justify="center"
align="center"
>
<NAvatar
:src="userInfo.streamerInfo.faceUrl"
:img-props="{ referrerpolicy: 'no-referrer' }"
@@ -284,7 +323,10 @@ onMounted(async () => {
boxShadow: isDarkMode ? 'rgb(195 192 192 / 35%) 0px 0px 8px' : '0 2px 3px rgba(0, 0, 0, 0.1)',
}"
/>
<NEllipsis v-if="width > 100" style="max-width: 100%">
<NEllipsis
v-if="width > 100"
style="max-width: 100%"
>
<NText strong>
{{ userInfo?.streamerInfo.name }}
</NText>
@@ -298,13 +340,34 @@ onMounted(async () => {
:collapsed-icon-size="22"
:options="menuOptions"
/>
<NSpace v-if="width > 150" justify="center" align="center" vertical>
<NSpace
v-if="width > 150"
justify="center"
align="center"
vertical
>
<NText depth="3">
有更多功能建议请
<NButton text type="info" tag="a" href="/feedback" target="_blank"> 反馈 </NButton>
<NButton
text
type="info"
tag="a"
href="/feedback"
target="_blank"
>
反馈
</NButton>
</NText>
<NText depth="3">
<NButton text type="info" tag="a" href="/about" target="_blank"> 关于本站 </NButton>
<NButton
text
type="info"
tag="a"
href="/about"
target="_blank"
>
关于本站
</NButton>
</NText>
</NSpace>
</NLayoutSider>
@@ -313,9 +376,16 @@ onMounted(async () => {
class="viewer-page-content"
:style="`box-shadow:${isDarkMode ? 'rgb(28 28 28 / 9%) 5px 5px 6px inset, rgba(139, 139, 139, 0.09) -5px -5px 6px inset' : 'inset 5px 5px 6px #8b8b8b17, inset -5px -5px 6px #8b8b8b17;'}`"
>
<RouterView v-if="userInfo" v-slot="{ Component }">
<RouterView
v-if="userInfo"
v-slot="{ Component }"
>
<KeepAlive>
<component :is="Component" :bili-info="biliUserInfo" :user-info="userInfo" />
<component
:is="Component"
:bili-info="biliUserInfo"
:user-info="userInfo"
/>
</KeepAlive>
</RouterView>
<template v-else>
@@ -326,7 +396,10 @@ onMounted(async () => {
</NLayout>
</NLayout>
</NLayout>
<NModal v-model:show="registerAndLoginModalVisiable" style="width: 500px; max-width: 90vw">
<NModal
v-model:show="registerAndLoginModalVisiable"
style="width: 500px; max-width: 90vw"
>
<div>
<RegisterAndLogin />
</div>
@@ -337,7 +410,8 @@ onMounted(async () => {
.viewer-page-content{
height: 100%;
border-radius: 18px;
padding: 15px;
padding: var(--vtsuru-content-padding);
height: calc(100vh - var(--vtsuru-header-height));
margin-right: 10px;
box-sizing: border-box;
overflow-y: auto;

View File

@@ -1,6 +1,30 @@
<script setup lang="ts">
import { useAccount } from '@/api/account';
import { useWebFetcher } from '@/store/useWebFetcher';
import {fetch} from "@tauri-apps/plugin-http";
import { NFlex } from 'naive-ui';
const webfetcher = useWebFetcher();
const accountInfo = useAccount();
function initWebfetcher() {
webfetcher.Start();
webfetcher.signalRClient?.send();
}
onMounted(() => {
if (accountInfo.value.id) {
initWebfetcher();
console.info('WebFetcher started')
}
})
</script>
<template>
1
<NFlex v-if="!accountInfo.id">
<RegisterAndLogin />
</NFlex>
<NFlex v-else>
{{ webfetcher }}
</NFlex>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,13 @@
<template>
<NSpin v-if="isLoading" show />
<NSpin
v-if="isLoading"
show
/>
<div v-else>
<NDivider style="margin: 16px 0 16px 0" title-placement="left">
<NDivider
style="margin: 16px 0 16px 0"
title-placement="left"
>
订阅链接
<NTooltip>
<template #trigger>
@@ -14,15 +20,26 @@
</NDivider>
<NFlex align="center">
<NInputGroup style="max-width: 400px;">
<NInput :value="`${SCHEDULE_API_URL}${userInfo?.id}.ics`" readonly />
<NButton secondary @click="copyToClipboard(`${SCHEDULE_API_URL}${userInfo?.id}.ics`)">
<NInput
:value="`${SCHEDULE_API_URL}${userInfo?.id}.ics`"
readonly
/>
<NButton
secondary
@click="copyToClipboard(`${SCHEDULE_API_URL}${userInfo?.id}.ics`)"
>
复制
</NButton>
</NInputGroup>
</NFlex>
<NDivider />
<component :is="ScheduleTemplateMap[componentType ?? ''].compoent" :bili-info="biliInfo"
:user-info="userInfo" :data="currentData" v-bind="$attrs" />
<component
:is="ScheduleTemplateMap[componentType ?? ''].component"
:bili-info="biliInfo"
:user-info="userInfo"
:data="currentData"
v-bind="$attrs"
/>
</div>
</template>

View File

@@ -4,10 +4,10 @@
show
/>
<component
:is="SongListTemplateMap[componentType ?? '']?.compoent"
:is="selectedTemplate?.component"
v-else
ref="dynamicConfigRef"
:config="selectedTemplateConfig?.name ? currentConfig : undefined"
:config="selectedTemplate?.settingName ? currentConfig : undefined"
:user-info="userInfo"
:bili-info="biliInfo"
:data="currentData"
@@ -16,164 +16,192 @@
v-bind="$attrs"
@request-song="requestSong"
/>
<NButton
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;"
@click="showSettingModal = true"
>
自定义
</NButton>
<NModal
v-model:show="showSettingModal"
style="max-width: 90vw; width: 800px;"
preset="card"
title="设置"
>
<DynamicForm
:name="selectedTemplate?.settingName"
:config-data="currentConfig"
:config="selectedTemplateConfig"
/>
</NModal>
</template>
<script lang="ts" setup>
import { DownloadConfig, downloadConfigDirect, useAccount } from '@/api/account'
import { Setting_LiveRequest, SongRequestInfo, SongsInfo, UserInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPIWithParams } from '@/api/query'
import { TemplateConfig } from '@/data/VTsuruTypes'
import { SONG_API_URL, SONG_REQUEST_API_URL, SongListTemplateMap, VTSURU_API_URL } from '@/data/constants'
import { useStorage } from '@vueuse/core'
import { addSeconds } from 'date-fns'
import { NSpin, useMessage } from 'naive-ui'
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
import { DownloadConfig, useAccount } from '@/api/account';
import { Setting_LiveRequest, SongRequestInfo, SongsInfo, UserInfo } from '@/api/api-models';
import { QueryGetAPI, QueryPostAPIWithParams } from '@/api/query';
import { SONG_API_URL, SONG_REQUEST_API_URL, SongListTemplateMap } from '@/data/constants';
import { ConfigItemDefinition } from '@/data/VTsuruTypes';
import { useStorage } from '@vueuse/core';
import { addSeconds } from 'date-fns';
import { NButton, NModal, NSpin, useMessage } from 'naive-ui';
import { computed, onMounted, ref, watch } from 'vue';
const accountInfo = useAccount()
const nextRequestTime = useStorage('SongList.NextRequestTime', new Date())
const accountInfo = useAccount();
const nextRequestTime = useStorage('SongList.NextRequestTime', new Date());
const minRequestTime = 30
const minRequestTime = 30;
const showSettingModal = ref(false);
const props = defineProps<{
biliInfo: any | undefined
userInfo: UserInfo | undefined
template?: string | undefined
fakeData?: SongsInfo[]
}>()
const props = defineProps<{
biliInfo: any | undefined;
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 selectedTemplateConfig = computed(() => {
if (dynamicConfigRef.value?.Config) {
return dynamicConfigRef.value?.Config as TemplateConfig<any>
}
return undefined
})
const currentConfig = ref()
watch(
() => dynamicConfigRef,
() => {
getConfig()
},
)
const isLoading = ref(true)
const message = useMessage()
const errMessage = ref('')
const songsActive = ref<SongRequestInfo[]>([])
const settings = ref<Setting_LiveRequest>({} as Setting_LiveRequest)
async function getSongRequestInfo() {
try {
const data = await QueryGetAPI<{ songs: SongRequestInfo[]; setting: Setting_LiveRequest }>(
SONG_REQUEST_API_URL + 'get-active-and-settings',
{
id: props.userInfo?.id,
},
)
if (data.code == 200) {
return data.data
const componentType = computed(() => {
return props.template ?? props.userInfo?.extra?.templateTypes['songlist']?.toLowerCase();
});
const currentData = ref<SongsInfo[]>();
const dynamicConfigRef = ref();
const selectedTemplateConfig = computed(() => {
if (dynamicConfigRef.value?.Config) {
return dynamicConfigRef.value?.Config as ConfigItemDefinition[];
}
} catch (err) { }
return {} as { songs: SongRequestInfo[]; setting: Setting_LiveRequest }
}
async function getSongs() {
isLoading.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.error('加载歌单失败: ' + data.message)
}
})
.catch((err) => {
message.error('加载失败: ' + err)
})
.finally(() => {
isLoading.value = false
})
}
async function getConfig() {
if(!selectedTemplateConfig.value) return
isLoading.value = true
await DownloadConfig(selectedTemplateConfig.value!.name)
.then((data) => {
if (data.msg) {
message.error('加载失败: ' + data.msg)
} else {
currentConfig.value = data.data
}
})
.catch((err) => {
message.error('加载失败: ' + err)
})
.finally(() => {
isLoading.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('主播不允许匿名点歌, 需要从网页点歌的话请注册登录, 点歌弹幕已复制到剪切板')
return undefined;
});
const selectedTemplate = computed(() => {
if (componentType.value) {
return SongListTemplateMap[componentType.value];
}
else if (!accountInfo.value.id) {
message.warning('要从网页点歌请先登录, 点歌弹幕已复制到剪切板')
} else {
message.success('复制成功')
}
} else {
if (props.userInfo) {
if (!accountInfo.value.id && nextRequestTime.value > new Date()) {
message.warning('距离点歌冷却还有' + (nextRequestTime.value.getTime() - new Date().getTime()) / 1000 + '秒')
return
}
try {
const data = await QueryPostAPIWithParams(SONG_REQUEST_API_URL + 'add-from-web', {
target: props.userInfo?.id,
song: song.key,
})
return undefined;
});
const currentConfig = ref();
watch(
() => dynamicConfigRef,
() => {
getConfig();
},
);
if (data.code == 200) {
message.success('点歌成功')
nextRequestTime.value = addSeconds(new Date(), minRequestTime)
} else {
message.error('点歌失败: ' + data.message)
}
} catch (err) {
message.error('点歌失败: ' + err)
}
}
}
}
const isLoading = ref(true);
const message = useMessage();
onMounted(async () => {
if (!props.fakeData) {
const errMessage = ref('');
const songsActive = ref<SongRequestInfo[]>([]);
const settings = ref<Setting_LiveRequest>({} as Setting_LiveRequest);
async function getSongRequestInfo() {
try {
await getSongs()
setTimeout(async () => {
const r = await getSongRequestInfo()
if (r) {
songsActive.value = r.songs
settings.value = r.setting
}
await getConfig()
}, 300)
} catch (err) {
message.error('加载失败: ' + err)
console.error(err)
}
} else {
currentData.value = props.fakeData
isLoading.value = false
const data = await QueryGetAPI<{ songs: SongRequestInfo[]; setting: Setting_LiveRequest; }>(
SONG_REQUEST_API_URL + 'get-active-and-settings',
{
id: props.userInfo?.id,
},
);
if (data.code == 200) {
return data.data;
}
} catch (err) { }
return {} as { songs: SongRequestInfo[]; setting: Setting_LiveRequest; };
}
})
async function getSongs() {
isLoading.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.error('加载歌单失败: ' + data.message);
}
})
.catch((err) => {
message.error('加载失败: ' + err);
})
.finally(() => {
isLoading.value = false;
});
}
async function getConfig() {
if (!selectedTemplateConfig.value || !selectedTemplate.value!.settingName) return;
isLoading.value = true;
await DownloadConfig(selectedTemplate.value!.settingName, props.userInfo?.id)
.then((data) => {
if (data.msg) {
message.error('加载失败: ' + data.msg);
} else {
currentConfig.value = data.data;
}
})
.catch((err) => {
message.error('加载失败: ' + err);
})
.finally(() => {
isLoading.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 {
message.success('复制成功');
}
} else {
if (props.userInfo) {
if (!accountInfo.value.id && nextRequestTime.value > new Date()) {
message.warning('距离点歌冷却还有' + (nextRequestTime.value.getTime() - new Date().getTime()) / 1000 + '秒');
return;
}
try {
const data = await QueryPostAPIWithParams(SONG_REQUEST_API_URL + 'add-from-web', {
target: props.userInfo?.id,
song: song.key,
});
if (data.code == 200) {
message.success('点歌成功');
nextRequestTime.value = addSeconds(new Date(), minRequestTime);
} else {
message.error('点歌失败: ' + data.message);
}
} catch (err) {
message.error('点歌失败: ' + err);
}
}
}
}
onMounted(async () => {
if (!props.fakeData) {
try {
await getSongs();
setTimeout(async () => {
const r = await getSongRequestInfo();
if (r) {
songsActive.value = r.songs;
settings.value = r.setting;
}
await getConfig();
}, 300);
} catch (err) {
message.error('加载失败: ' + err);
console.error(err);
}
} else {
currentData.value = props.fakeData;
isLoading.value = false;
}
});
</script>

View File

@@ -43,7 +43,7 @@ const buttons = (song: SongsInfo) => [
},
),
default: () =>
!props.liveRequestSettings.allowFromWeb || song.options
!props.liveRequestSettings?.allowFromWeb || song.options
? '点歌 | 用户不允许从网页点歌, 点击后将复制点歌内容到剪切板'
: !accountInfo
? '点歌 | 你需要登录后才能点歌'

File diff suppressed because it is too large Load Diff