cancel no payment

This commit is contained in:
2024-10-21 18:02:22 +08:00
parent 60b609ac26
commit c07257bb44
16 changed files with 337 additions and 227 deletions

View File

@@ -1,5 +1,7 @@
{ {
"tabWidth": 2, "tabWidth": 2,
"semi": false, "semi": false,
"singleQuote": true "singleQuote": true,
"trailingComma": "none",
"printWidth": 80
} }

View File

@@ -1,7 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { TemplateConfig } from '@/data/VTsuruTypes' import { getImageUploadModel } from '@/Utils';
import { QueryPostAPI } from '@/api/query';
import { TemplateConfig, TemplateConfigImageItem } from '@/data/VTsuruTypes'
import { FILE_BASE_URL, VTSURU_API_URL } from '@/data/constants';
import { NButton, NEmpty, NForm, NFormItem, NInput, NUpload, UploadFileInfo, useMessage } from 'naive-ui' import { NButton, NEmpty, NForm, NFormItem, NInput, NUpload, UploadFileInfo, useMessage } from 'naive-ui'
import { ref } from 'vue' import { onMounted, ref } from 'vue'
const message = useMessage() const message = useMessage()
@@ -10,23 +13,78 @@ const props = defineProps<{
config: TemplateConfig<any> | undefined config: TemplateConfig<any> | undefined
}>() }>()
const fileList = ref<UploadFileInfo[]>([]) const fileList = ref<{ [key: string]: UploadFileInfo[] }>({})
function OnFileListChange(files: UploadFileInfo[]) { const isUploading = ref(false)
console.log(props.configData)
function OnFileListChange(key: string, files: UploadFileInfo[]) {
if (files.length == 1) { if (files.length == 1) {
var file = files[0] var file = files[0]
if ((file.file?.size ?? 0) > 10 * 1024 * 1024) { if ((file.file?.size ?? 0) > 10 * 1024 * 1024) {
message.error('文件大小不能超过10MB') message.error('文件大小不能超过10MB')
fileList.value = [] fileList.value[key] = []
} }
} }
} }
function onSubmit() { async function onSubmit() {
console.log(props.configData) try {
isUploading.value = true
let images = {} as {
[key: string]: {
existImages: string[],
newImagesBase64: string[],
}
}
for (const item of props.config!.items) {
if (item.type == 'image') {
const key = (item as TemplateConfigImageItem<any>).key
images[key] = await getImageUploadModel(fileList.value[key])
}
}
const resp = await QueryPostAPI<any>(VTSURU_API_URL + 'set-config', {
name: props.config!.name,
json: JSON.stringify(props.configData),
images: images,
})
if (resp.code == 200) {
message.success('已保存至服务器')
props.config?.items.forEach(item => {
switch (item.type) {
case 'image':
item.onUploaded?.(resp.data[item.key], props.configData)
break
}
})
} else {
message.error('保存失败: ' + resp.message)
}
} catch (err) {
message.error('保存失败: ' + err)
}
finally {
isUploading.value = false
}
} }
function getItems() {} function getItems() { }
onMounted(() => {
props.config?.items.forEach(item => {
if (item.type == 'image') {
const configItem = props.configData[item.key]
if (configItem) {
fileList.value[item.key] = configItem.map((i: string) => ({
id: i,
thumbnailUrl: FILE_BASE_URL + i,
name: '',
status: 'finished',
}))
}
else {
fileList.value[item.key] = []
}
}
})
})
</script> </script>
<template> <template>
@@ -35,22 +93,17 @@ function getItems() {}
<NFormItem v-for="item in config.items" :key="item.name" :label="item.name"> <NFormItem v-for="item in config.items" :key="item.name" :label="item.name">
<component v-if="item.type == 'render'" :is="item.render(configData)"></component> <component v-if="item.type == 'render'" :is="item.render(configData)"></component>
<template v-else-if="item.type == 'string'"> <template v-else-if="item.type == 'string'">
<NInput :value="item.data.get(configData)" @update:value="item.data.set(configData, $event)" /> <NInput v-if="item.data" :value="configData[item.key]" @update:value="configData[item.key] = $event" />
<NInput v-else v-model:value="configData[item.key]" />
</template> </template>
<NUpload <NUpload v-else-if="item.type == 'image'" accept=".png,.jpg,.jpeg,.gif,.svg,.webp,.ico" list-type="image-card"
v-else-if="item.type == 'image'" :default-upload="false" v-model:file-list="fileList[item.key]"
accept=".png,.jpg,.jpeg,.gif,.svg,.webp,.ico" @update:file-list="file => OnFileListChange(item.key, file)" :max="item.imageLimit" im>
list-type="image-card"
:default-upload="false"
v-model:file-list="fileList"
@update:file-list="OnFileListChange"
:max="item.imageLimit"
>
上传图片 上传图片
</NUpload> </NUpload>
</NFormItem> </NFormItem>
<NFormItem> <NFormItem>
<NButton type="primary" @click="onSubmit">提交</NButton> <NButton type="primary" @click="onSubmit" :loading="isUploading" >提交</NButton>
</NFormItem> </NFormItem>
</NForm> </NForm>
</template> </template>

View File

@@ -50,7 +50,7 @@ const props = defineProps<{
songs: SongsInfo[] songs: SongsInfo[]
canEdit?: boolean canEdit?: boolean
isSelf: boolean isSelf: boolean
extraButtom?: (song: SongsInfo) => VNodeChild[] extraButton?: (song: SongsInfo) => VNodeChild[]
}>() }>()
watch( watch(
() => props.songs, () => props.songs,
@@ -380,7 +380,7 @@ function createColumns(): DataTableColumns<SongsInfo> {
}), }),
] ]
: null, : null,
props.extraButtom?.(data), props.extraButton?.(data),
], ],
) )
}, },

23
src/data/TemplateTypes.ts Normal file
View File

@@ -0,0 +1,23 @@
import {
ScheduleWeekInfo,
Setting_LiveRequest,
SongRequestInfo,
SongsInfo,
UserInfo
} from '@/api/api-models'
export interface SongListConfigType {
userInfo: UserInfo | undefined
biliInfo: any | undefined
songRequestSettings: Setting_LiveRequest
songRequestActive: SongRequestInfo[]
data: SongsInfo[] | undefined
}
export interface SongListConfigTypeWithConfig<T> extends SongListConfigType {
config?: T
}
export interface ScheduleConfigType {
userInfo: UserInfo | undefined
biliInfo: any | undefined
data: ScheduleWeekInfo[] | undefined
}

View File

@@ -23,10 +23,11 @@ type DataAccessor<T, V> = {
} }
// 扩展 CommonProps 以包含额外的共有属性 // 扩展 CommonProps 以包含额外的共有属性
export type TemplateConfigItemWithType<T, V> = CommonProps & { data: DataAccessor<T, V> } export type TemplateConfigItemWithType<T, V> = CommonProps & { data?: DataAccessor<T, V> }
export type TemplateConfigStringItem<T> = TemplateConfigItemWithType<T, string> & { export type TemplateConfigStringItem<T> = TemplateConfigItemWithType<T, string> & {
type: 'string' type: 'string'
key: string //将被保存到指定key中
} }
export type TemplateConfigStringArrayItem<T> = TemplateConfigItemWithType<T, string[]> & { export type TemplateConfigStringArrayItem<T> = TemplateConfigItemWithType<T, string[]> & {
type: 'stringArray' type: 'stringArray'
@@ -42,8 +43,19 @@ export type TemplateConfigRenderItem<T> = TemplateConfigBase & {
type: 'render' type: 'render'
render: (arg0: T) => VNode render: (arg0: T) => VNode
} }
/**
*
* @template T - The type of the associated data model.
*/
export type TemplateConfigImageItem<T> = TemplateConfigBase & { export type TemplateConfigImageItem<T> = TemplateConfigBase & {
type: 'image' type: 'image' // Specifies the type of configuration item as 'image'.
imageLimit: number imageLimit: number // The maximum number of images allowed.
onUploaded: (arg0: string | string[], arg1: T) => void key: string //图片将被保存到指定key中, 类型为字符串数组
/**
* Callback function triggered upon image upload.
* @param {string[]} uploadedImages - The uploaded image or array of images.
* @param {T} config - The configuration data model.
*/
onUploaded?: (uploadedImages: string[], config: T) => void
} }

View File

@@ -70,6 +70,10 @@ export const SongListTemplateMap = {
name: '简单', name: '简单',
compoent: defineAsyncComponent(() => import('@/views/view/songListTemplate/SimpleSongListTemplate.vue')), compoent: defineAsyncComponent(() => import('@/views/view/songListTemplate/SimpleSongListTemplate.vue')),
}, },
traditional: {
name: '传统',
compoent: defineAsyncComponent(() => import('@/views/view/songListTemplate/TraditionalSongListTemplate.vue')),
},
} as { [key: string]: { name: string; compoent: any } } } as { [key: string]: { name: string; compoent: any } }
export const IndexTemplateMap = { export const IndexTemplateMap = {
'': { name: '默认', compoent: DefaultIndexTemplateVue }, '': { name: '默认', compoent: DefaultIndexTemplateVue },

View File

@@ -86,11 +86,6 @@ const functions = [
desc: '绑定账号后查看粉丝 舰长 观看数 等数据的历史记录', desc: '绑定账号后查看粉丝 舰长 观看数 等数据的历史记录',
icon: AnalyticsSharp, icon: AnalyticsSharp,
}, },
{
name: '绝不收米!',
desc: '为爱发电, 所有功能都免费并提供技术支持 (当然你想赞助的话也可以捏',
icon: MoneyOff24Filled,
},
{ {
name: '还有更多', name: '还有更多',
desc: '更多功能仍在开发中. 有其他合理需求或者建议, 或者有想要添加的样式? 向我提出!', desc: '更多功能仍在开发中. 有其他合理需求或者建议, 或者有想要添加的样式? 向我提出!',

View File

@@ -55,7 +55,7 @@ import {
SelectOption, SelectOption,
useMessage, useMessage,
} from 'naive-ui' } from 'naive-ui'
import { computed, h, nextTick, onActivated, onMounted, ref } from 'vue' import { computed, h, nextTick, onActivated, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
interface TemplateDefineTypes { interface TemplateDefineTypes {
@@ -247,7 +247,6 @@ const selectedTemplateConfig = computed(() => {
} }
return undefined return undefined
}) })
const biliUserInfo = ref() const biliUserInfo = ref()
const settingModalVisiable = ref(false) const settingModalVisiable = ref(false)
const showAddVideoModal = ref(false) const showAddVideoModal = ref(false)
@@ -547,19 +546,15 @@ onMounted(async () => {
<NCheckbox v-model:checked="accountInfo.settings.sendEmail.recieveQA" @update:checked="SaveComboSetting"> <NCheckbox v-model:checked="accountInfo.settings.sendEmail.recieveQA" @update:checked="SaveComboSetting">
收到新提问时发送邮件 收到新提问时发送邮件
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-model:checked="accountInfo.settings.sendEmail.recieveQAReply"
v-model:checked="accountInfo.settings.sendEmail.recieveQAReply" @update:checked="SaveComboSetting">
@update:checked="SaveComboSetting"
>
提问收到回复时发送邮件 提问收到回复时发送邮件
</NCheckbox> </NCheckbox>
</NSpace> </NSpace>
<NDivider> 提问箱 </NDivider> <NDivider> 提问箱 </NDivider>
<NSpace> <NSpace>
<NCheckbox <NCheckbox v-model:checked="accountInfo.settings.questionBox.allowUnregistedUser"
v-model:checked="accountInfo.settings.questionBox.allowUnregistedUser" @update:checked="SaveComboSetting">
@update:checked="SaveComboSetting"
>
允许未注册用户提问 允许未注册用户提问
</NCheckbox> </NCheckbox>
</NSpace> </NSpace>
@@ -649,28 +644,22 @@ onMounted(async () => {
<br /> <br />
<NSpace vertical> <NSpace vertical>
<NSpace align="center"> <NSpace align="center">
页面 <NSelect :options="templateOptions" v-model:value="selectedOption" style="width: 150px" /> 页面
<NSelect :options="templateOptions" v-model:value="selectedOption" style="width: 150px" />
</NSpace> </NSpace>
<NDivider style="margin: 5px 0 5px 0" title-placement="left"> 模板 </NDivider> <NDivider style="margin: 5px 0 5px 0" title-placement="left"> 模板 </NDivider>
<div> <div>
<NSpace> <NSpace>
<NSelect <NSelect style="width: 150px" :options="selectedTemplateData.Options"
style="width: 150px" v-model:value="selectedTemplateData.Selected" />
:options="selectedTemplateData.Options"
v-model:value="selectedTemplateData.Selected"
/>
<component :is="buttonGroup" /> <component :is="buttonGroup" />
</NSpace> </NSpace>
<NDivider /> <NDivider />
<Transition name="fade" mode="out-in"> <Transition name="fade" mode="out-in">
<div v-if="selectedComponent" :key="selectedTemplateData.Selected"> <div v-if="selectedComponent" :key="selectedTemplateData.Selected">
<component <component ref="dynamicConfigRef" @vue:mounted="getTemplateConfig" :is="selectedComponent"
ref="dynamicConfigRef" :user-info="accountInfo" :bili-info="biliUserInfo" :current-data="selectedTemplateData.Data"
:is="selectedComponent" :config="selectedTemplateData.Config" />
:user-info="accountInfo"
:bili-info="biliUserInfo"
:current-data="selectedTemplateData.Data"
/>
</div> </div>
</Transition> </Transition>
</div> </div>
@@ -679,29 +668,13 @@ onMounted(async () => {
</NTabs> </NTabs>
</NSpin> </NSpin>
</NCard> </NCard>
<NModal <NModal preset="card" v-model:show="settingModalVisiable" closable style="width: 600px; max-width: 90vw" title="模板设置">
preset="card"
v-model:show="settingModalVisiable"
closable
style="width: 600px; max-width: 90vw"
title="模板设置"
>
只是测试, 没用 只是测试, 没用
<NSpin v-if="!selectedTemplateData.Config" show /> <NSpin v-if="!selectedTemplateData.Config" show />
<DynamicForm <DynamicForm v-else :key="selectedTemplateData.Selected" :configData="selectedTemplateData.Config"
v-else :config="selectedTemplateConfig" />
:key="selectedTemplateData.Selected"
:configData="selectedTemplateData.Config"
:config="selectedTemplateConfig"
/>
</NModal> </NModal>
<NModal <NModal preset="card" v-model:show="showAddVideoModal" closable style="width: 600px; max-width: 90vw" title="添加视频">
preset="card"
v-model:show="showAddVideoModal"
closable
style="width: 600px; max-width: 90vw"
title="添加视频"
>
<NInput v-model:value="addVideoUrl" placeholder="请输入视频链接" /> <NInput v-model:value="addVideoUrl" placeholder="请输入视频链接" />
<NDivider /> <NDivider />
<NButton type="primary" @click="addVideo" :loading="isLoading"> 添加视频 </NButton> <NButton type="primary" @click="addVideo" :loading="isLoading"> 添加视频 </NButton>

View File

@@ -2,35 +2,36 @@
import { NavigateToNewTab } from '@/Utils' import { NavigateToNewTab } from '@/Utils'
import { useAccount } from '@/api/account' import { useAccount } from '@/api/account'
import { import {
AddressInfo, AddressInfo,
GoodsTypes, GoodsTypes,
ResponsePointGoodModel, ResponsePointGoodModel,
ResponsePointOrder2UserModel, ResponsePointOrder2UserModel,
UserInfo, UserInfo,
} from '@/api/api-models' } from '@/api/api-models'
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 { useAuthStore } from '@/store/useAuthStore'
import { import {
NAlert, NAlert,
NButton, NButton,
NCard, NCard,
NDivider, NCheckbox,
NEmpty, NDivider,
NFlex, NEmpty,
NForm, NFlex,
NFormItem, NForm,
NInputNumber, NFormItem,
NModal, NInputNumber,
NSelect, NModal,
NSpin, NSelect,
NTag, NSpin,
NText, NTag,
NTooltip, NText,
SelectOption, NTooltip,
useDialog, SelectOption,
useMessage useDialog,
useMessage
} from 'naive-ui' } from 'naive-ui'
import { computed, h, onMounted, ref } from 'vue' import { computed, h, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -65,11 +66,12 @@ const tags = computed(() => {
}) })
const selectedTag = ref<string>() const selectedTag = ref<string>()
const selectedItems = computed(() => { const selectedItems = computed(() => {
return selectedTag.value return goods.value.filter((item) => selectedTag.value ? item.tags.includes(selectedTag.value) : true).filter((item) => !onlyCanBuy.value || getTooltip(item) == '开始兑换').filter((item) => !ignoreGuard.value || item.allowGuardLevel == 0)
? goods.value.filter((g) => g.tags.includes(selectedTag.value))
: goods.value
}) })
const onlyCanBuy = ref(false)
const ignoreGuard = ref(false)
const addressOptions = computed(() => { const addressOptions = computed(() => {
if (!biliAuth.value.id) return [] if (!biliAuth.value.id) return []
return ( return (
@@ -86,7 +88,7 @@ const canBuy = computed(() => {
if (!biliAuth.value.id) return false if (!biliAuth.value.id) return false
return true return true
}) })
function getTooltip(goods: ResponsePointGoodModel) { function getTooltip(goods: ResponsePointGoodModel): '开始兑换' | '当前积分不足' | '请先进行账号认证' | '库存不足' {
if ((currentPoint.value ?? 0) < goods.price) { if ((currentPoint.value ?? 0) < goods.price) {
return '当前积分不足' return '当前积分不足'
} else if (!biliAuth.value.id) return '请先进行账号认证' } else if (!biliAuth.value.id) return '请先进行账号认证'
@@ -215,34 +217,29 @@ onMounted(async () => {
<NText> 你在 {{ userInfo.extra?.streamerInfo?.name ?? userInfo.name }} 的直播间的积分为 {{ currentPoint }} </NText> <NText> 你在 {{ userInfo.extra?.streamerInfo?.name ?? userInfo.name }} 的直播间的积分为 {{ currentPoint }} </NText>
</NCard> </NCard>
<NDivider /> <NDivider />
<NCard size="small" title="标签"> <NCard v-if="tags.length > 0" size="small" title="标签筛选">
<NFlex align="center" justify="center"> <NFlex align="center" justify="center">
<NButton v-for="tag in tags" <NButton v-for="tag in tags" :type="tag == selectedTag ? 'success' : 'default'"
:type="tag == selectedTag ? 'success' : 'default'" @click="selectedTag = selectedTag == tag ? undefined : tag" :borderd="false" style="margin: 4px" size="small">
@click="selectedTag = selectedTag == tag ? undefined : tag" {{ tag }}
:borderd="false" </NButton>
style="margin: 4px"
size="small">
{{ tag }}
</NButton>
</NFlex> </NFlex>
<NDivider />
<NCheckbox v-model:checked="onlyCanBuy"> 只显示可兑换的礼物 </NCheckbox>
<NCheckbox v-model:checked="ignoreGuard"> 忽略需要舰长的礼物 </NCheckbox>
</NCard> </NCard>
<NDivider /> <NDivider />
<NSpin :show="isLoading"> <NSpin :show="isLoading">
<NEmpty v-if="selectedItems.length == 0"> 暂无礼物 </NEmpty> <NEmpty v-if="selectedItems.length == 0"> 暂无礼物 </NEmpty>
<NFlex justify="center"> <NFlex justify="center">
<PointGoodsItem v-for="item in selectedItems" :key="item.id" :goods="item" content-style="max-width: 300px;height: 365px"> <PointGoodsItem v-for="item in selectedItems" :key="item.id" :goods="item"
content-style="max-width: 300px;height: 365px">
<template #footer> <template #footer>
<NFlex justify="space-between" align="center"> <NFlex justify="space-between" align="center">
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton <NButton :disabled="getTooltip(item) != '开始兑换'" size="small" type="primary" @click="onBuyClick(item)">兑换
:disabled="getTooltip(item) != '开始兑换'" </NButton>
size="small"
type="primary"
@click="onBuyClick(item)"
>兑换</NButton
>
</template> </template>
{{ getTooltip(item) }} {{ getTooltip(item) }}
</NTooltip> </NTooltip>
@@ -262,13 +259,8 @@ onMounted(async () => {
</PointGoodsItem> </PointGoodsItem>
</NFlex> </NFlex>
</NSpin> </NSpin>
<NModal <NModal v-model:show="showBuyModal" v-if="currentGoods" preset="card" title="确认兑换"
v-model:show="showBuyModal" style="width: 500px; max-width: 90vw; height: auto">
v-if="currentGoods"
preset="card"
title="确认兑换"
style="width: 500px; max-width: 90vw; height: auto"
>
<template #header> <template #header>
<NFlex align="baseline"> <NFlex align="baseline">
<NTag :type="currentGoods.type == GoodsTypes.Physical ? 'info' : 'default'" :bordered="false"> <NTag :type="currentGoods.type == GoodsTypes.Physical ? 'info' : 'default'" :bordered="false">
@@ -281,32 +273,16 @@ onMounted(async () => {
<template v-if="currentGoods.type == GoodsTypes.Physical"> <template v-if="currentGoods.type == GoodsTypes.Physical">
<NDivider> 选项 </NDivider> <NDivider> 选项 </NDivider>
<NForm> <NForm>
<NFormItem label="兑换数量" required <NFormItem label="兑换数量" required>
><NInputNumber <NInputNumber v-model:value="buyCount" :min="1" :max="currentGoods.maxBuyCount ?? 100000"
v-model:value="buyCount" style="max-width: 120px" step="1" :precision="0" />
:min="1"
:max="currentGoods.maxBuyCount ?? 100000"
style="max-width: 120px"
step="1"
:precision="0"
/>
</NFormItem> </NFormItem>
<NFormItem <NFormItem v-if="
v-if=" currentGoods.type == GoodsTypes.Physical &&
currentGoods.type == GoodsTypes.Physical && (currentGoods.collectUrl == null || currentGoods.collectUrl == undefined)
(currentGoods.collectUrl == null || currentGoods.collectUrl == undefined) " label="收货地址" required>
" <NSelect v-model:show="showAddressSelect" :value="selectedAddress?.id" :options="addressOptions"
label="收货地址" :render-label="renderLabel" :render-option="renderOption" placeholder="请选择地址" />
required
>
<NSelect
v-model:show="showAddressSelect"
:value="selectedAddress?.id"
:options="addressOptions"
:render-label="renderLabel"
:render-option="renderOption"
placeholder="请选择地址"
/>
&nbsp; &nbsp;
<NButton size="small" type="info" tag="a" href="/bili-user#settings" target="_blank"> 管理收货地址 </NButton> <NButton size="small" type="info" tag="a" href="/bili-user#settings" target="_blank"> 管理收货地址 </NButton>
</NFormItem> </NFormItem>

View File

@@ -1,25 +1,19 @@
<template> <template>
<NSpin v-if="isLoading" show /> <NSpin v-if="isLoading" show />
<component <component v-else ref="dynamicConfigRef" :config="currentConfig"
v-else :is="SongListTemplateMap[componentType ?? '']?.compoent" :user-info="userInfo" :bili-info="biliInfo"
:is="SongListTemplateMap[componentType ?? '']?.compoent" :currentData="currentData" :live-request-settings="settings" :live-request-active="songsActive"
:user-info="userInfo" @request-song="requestSong" v-bind="$attrs" />
:bili-info="biliInfo"
:currentData="currentData"
:live-request-settings="settings"
:live-request-active="songsActive"
@request-song="requestSong"
v-bind="$attrs"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useAccount } from '@/api/account' import { DownloadConfig, downloadConfigDirect, 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 { TemplateConfig } from '@/data/VTsuruTypes'
import { SONG_API_URL, SONG_REQUEST_API_URL, SongListTemplateMap, VTSURU_API_URL } from '@/data/constants'
import { NSpin, useMessage } from 'naive-ui' import { NSpin, useMessage } from 'naive-ui'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref, watch, watchEffect } from 'vue'
const accountInfo = useAccount() const accountInfo = useAccount()
@@ -35,6 +29,21 @@ 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 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 isLoading = ref(true)
const message = useMessage() const message = useMessage()
@@ -53,7 +62,7 @@ async function getSongRequestInfo() {
if (data.code == 200) { if (data.code == 200) {
return data.data return data.data
} }
} catch (err) {} } catch (err) { }
return {} as { songs: SongRequestInfo[]; setting: Setting_LiveRequest } return {} as { songs: SongRequestInfo[]; setting: Setting_LiveRequest }
} }
async function getSongs() { async function getSongs() {
@@ -76,6 +85,23 @@ async function getSongs() {
isLoading.value = false isLoading.value = false
}) })
} }
async function getConfig() {
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('加载失败')
})
.finally(() => {
isLoading.value = false
})
}
async function requestSong(song: SongsInfo) { async function requestSong(song: SongsInfo) {
if (song.options || !settings.value.allowFromWeb) { if (song.options || !settings.value.allowFromWeb) {
navigator.clipboard.writeText(`${settings.value.orderPrefix} ${song.name}`) navigator.clipboard.writeText(`${settings.value.orderPrefix} ${song.name}`)

View File

@@ -47,7 +47,7 @@ function navigate(url: string) {
<script lang="ts"> <script lang="ts">
export type ConfigType = { export type ConfigType = {
cover: string test: string
} }
export const DefaultConfig = {} as ConfigType export const DefaultConfig = {} as ConfigType
export const Config: TemplateConfig<ConfigType> = { export const Config: TemplateConfig<ConfigType> = {
@@ -57,19 +57,12 @@ export const Config: TemplateConfig<ConfigType> = {
name: '封面', name: '封面',
type: 'image', type: 'image',
imageLimit: 1, imageLimit: 1,
onUploaded: (url, config) => { key: 'cover',
config.cover = url instanceof String ? (url as string) : url[0]
},
}, },
{ {
name: 'test', name: '测试',
type: 'string', type: 'string',
data: { key: 'test',
get: (d) => d.cover,
set: (d, v) => {
d.cover = v
},
},
}, },
], ],
} }

View File

@@ -1,20 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAccount } from '@/api/account' import { useAccount } from '@/api/account';
import { ScheduleWeekInfo, UserInfo } from '@/api/api-models' import ScheduleList from '@/components/ScheduleList.vue';
import ScheduleList from '@/components/ScheduleList.vue' import { ScheduleConfigType } from '@/data/TemplateTypes';
import { NDivider } from 'naive-ui' import { NDivider } from 'naive-ui';
const accountInfo = useAccount() const accountInfo = useAccount()
defineProps<{ defineProps<ScheduleConfigType>()
userInfo: UserInfo | undefined
biliInfo: any | undefined
currentData: ScheduleWeekInfo[] | undefined
}>()
</script> </script>
<template> <template>
<NDivider style="margin-top: 10px" /> <NDivider style="margin-top: 10px" />
<ScheduleList v-if="currentData" :schedules="currentData ?? []" :is-self="false" v-bind="$attrs" /> <ScheduleList v-if="data" :schedules="data ?? []" :is-self="false" v-bind="$attrs" />
<NDivider /> <NDivider />
</template> </template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ScheduleWeekInfo, UserInfo } from '@/api/api-models'
import SaveCompoent from '@/components/SaveCompoent.vue' import SaveCompoent from '@/components/SaveCompoent.vue'
import { ScheduleConfigType } from '@/data/TemplateTypes'
import { getWeek, getYear } from 'date-fns' import { getWeek, getYear } from 'date-fns'
import { NDivider, NSelect, NSpace } from 'naive-ui' import { NDivider, NSelect, NSpace } from 'naive-ui'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
@@ -8,17 +8,13 @@ import { computed, onMounted, ref } from 'vue'
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
const table = ref() const table = ref()
const props = defineProps<{ const props = defineProps<ScheduleConfigType>()
userInfo: UserInfo | undefined
biliInfo: any | undefined
currentData: ScheduleWeekInfo[] | undefined
}>()
const currentWeek = computed(() => { const currentWeek = computed(() => {
if (props.currentData?.length == 1) { if (props.data?.length == 1) {
return props.currentData[0] return props.data[0]
} }
return props.currentData?.find((item) => { return props.data?.find((item) => {
if (selectedDate.value) { if (selectedDate.value) {
return item.year + '-' + item.week === selectedDate.value return item.year + '-' + item.week === selectedDate.value
} }
@@ -26,7 +22,7 @@ const currentWeek = computed(() => {
}) })
}) })
const options = computed(() => { const options = computed(() => {
return props.currentData?.map((item) => { return props.data?.map((item) => {
return { return {
label: item.year + '年' + item.week + '周', label: item.year + '年' + item.week + '周',
value: item.year + '-' + item.week, value: item.year + '-' + item.week,

View File

@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAccount } from '@/api/account' import { useAccount } from '@/api/account'
import { Setting_LiveRequest, SongRequestInfo, SongsInfo, UserInfo } from '@/api/api-models' import { SongsInfo } from '@/api/api-models'
import SongList from '@/components/SongList.vue' import SongList from '@/components/SongList.vue'
import { SongListConfigType } from '@/data/TemplateTypes'
import LiveRequestOBS from '@/views/obs/LiveRequestOBS.vue' import LiveRequestOBS from '@/views/obs/LiveRequestOBS.vue'
import { CloudAdd20Filled } from '@vicons/fluent' import { CloudAdd20Filled } 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'
@@ -10,19 +11,13 @@ import { h, ref } from 'vue'
const accountInfo = useAccount() const accountInfo = useAccount()
//所有模板都应该有这些 //所有模板都应该有这些
const props = defineProps<{ const props = defineProps<SongListConfigType>()
userInfo: UserInfo | undefined
biliInfo: any | undefined
songRequestSettings: Setting_LiveRequest
songRequestActive: SongRequestInfo[]
currentData?: SongsInfo[] | undefined
}>()
const emits = defineEmits(['requestSong']) const emits = defineEmits(['requestSong'])
const isLoading = ref('') const isLoading = ref('')
const message = useMessage() const message = useMessage()
const buttoms = (song: SongsInfo) => [ const buttons = (song: SongsInfo) => [
accountInfo.value?.id != props.userInfo?.id accountInfo.value?.id != props.userInfo?.id
? h( ? h(
NTooltip, NTooltip,
@@ -62,10 +57,10 @@ const buttoms = (song: SongsInfo) => [
<template> <template>
<NDivider style="margin-top: 10px" /> <NDivider style="margin-top: 10px" />
<SongList <SongList
v-if="currentData" v-if="data"
:songs="currentData ?? []" :songs="data ?? []"
:is-self="accountInfo?.id == userInfo?.id" :is-self="accountInfo?.id == userInfo?.id"
:extra-buttom="buttoms" :extraButton="buttons"
v-bind="$attrs" v-bind="$attrs"
/> />
<NCollapse v-if="userInfo?.canRequestSong"> <NCollapse v-if="userInfo?.canRequestSong">

View File

@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { GetGuardColor } from '@/Utils' import { GetGuardColor } from '@/Utils'
import { useAccount } from '@/api/account' import { useAccount } from '@/api/account'
import { FunctionTypes, Setting_LiveRequest, SongRequestInfo, SongsInfo, UserInfo } from '@/api/api-models' 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 LiveRequestOBS from '@/views/obs/LiveRequestOBS.vue' import LiveRequestOBS from '@/views/obs/LiveRequestOBS.vue'
import { CloudAdd20Filled, Play24Filled } from '@vicons/fluent' import { CloudAdd20Filled, Play24Filled } from '@vicons/fluent'
import { useWindowSize } from '@vueuse/core' import { useWindowSize } from '@vueuse/core'
@@ -28,13 +29,7 @@ import {
} from 'naive-ui' } from 'naive-ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
const props = defineProps<{ const props = defineProps<SongListConfigType>()
userInfo: UserInfo | undefined
biliInfo: any | undefined
songRequestSettings: Setting_LiveRequest
songRequestActive: SongRequestInfo[]
currentData: SongsInfo[] | undefined
}>()
const emits = defineEmits(['requestSong']) const emits = defineEmits(['requestSong'])
const windowSize = useWindowSize() const windowSize = useWindowSize()
const container = ref() const container = ref()
@@ -51,10 +46,10 @@ const isLrcLoading = ref('')
const isLoading = ref('') const isLoading = ref('')
const tags = computed(() => { const tags = computed(() => {
if (props.currentData) { if (props.data) {
return [ return [
...new Set( ...new Set(
props.currentData props.data
.map((item) => { .map((item) => {
return item.tags ?? [] return item.tags ?? []
}) })
@@ -65,10 +60,10 @@ const tags = computed(() => {
return [] return []
}) })
const authors = computed(() => { const authors = computed(() => {
if (props.currentData) { if (props.data) {
return [ return [
...new Set( ...new Set(
props.currentData props.data
.map((item) => { .map((item) => {
return item.author ?? [] return item.author ?? []
}) })
@@ -79,8 +74,8 @@ const authors = computed(() => {
return [] return []
}) })
const songs = computed(() => { const songs = computed(() => {
if (props.currentData) { if (props.data) {
return props.currentData return props.data
.filter((item) => { .filter((item) => {
return ( return (
(!selectedTag.value || item.tags?.includes(selectedTag.value)) && (!selectedTag.value || item.tags?.includes(selectedTag.value)) &&
@@ -99,8 +94,8 @@ const onScroll = throttle((e: Event) => {
} }
}, 100) }, 100)
function loadMore() { function loadMore() {
if (props.currentData) { if (props.data) {
index.value += props.currentData.length > 20 + index.value ? 20 : props.currentData.length - index.value index.value += props.data.length > 20 + index.value ? 20 : props.data.length - index.value
} }
} }
</script> </script>
@@ -151,7 +146,7 @@ function loadMore() {
/> />
</NSpace> </NSpace>
</NCard> </NCard>
<NEmpty v-if="!currentData || songs?.length == 0" description="暂无曲目" style="max-width: 0 auto" /> <NEmpty v-if="!data || songs?.length == 0" description="暂无曲目" style="max-width: 0 auto" />
<NScrollbar <NScrollbar
v-else v-else
ref="container" ref="container"
@@ -301,7 +296,7 @@ function loadMore() {
</NGrid> </NGrid>
<NDivider /> <NDivider />
<NSpace justify="center"> <NSpace justify="center">
<NButton v-if="currentData.length > index" @click="loadMore"> 加载更多 </NButton> <NButton v-if="data.length > index" @click="loadMore"> 加载更多 </NButton>
</NSpace> </NSpace>
</NScrollbar> </NScrollbar>
</div> </div>

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import { Setting_LiveRequest, SongRequestInfo, SongsInfo, UserInfo } from '@/api/api-models';
import { SongListConfigType, SongListConfigTypeWithConfig } from '@/data/TemplateTypes';
import { TemplateConfig } from '@/data/VTsuruTypes';
import { FILE_BASE_URL } from '@/data/constants';
const props = defineProps<SongListConfigTypeWithConfig<ConfigType>>()
defineExpose({ Config, DefaultConfig })
</script>
<script lang="ts">
export type ConfigType = {
background: string[],
notice: string,
}
export const DefaultConfig = {} as ConfigType
export const Config: TemplateConfig<ConfigType> = {
name: 'Template.SongList.Traditional',
items: [
{
name: '背景',
type: 'image',
imageLimit: 1,
key: 'background',
onUploaded: (url, config) => {
config.background = url
},
},
{
name: '公告',
type: 'string',
key: 'notice',
},
],
}
</script>
<template>
<div :style="{
backgroundImage: `${props.config?.background ? 'url(' + FILE_BASE_URL + props.config?.background[0] + ')' : ''}`,
height: '100%', 'max-width': '100%',
minHeight: '400px',
backgroundSize: 'cover',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
}">
<div style="{
height: 100%;
width: 100%;
min-height: 400px;
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}">
<div style="{
border-radius: 20px;
border: 3px solid var(--pinky-border-color-dark);
height: 50px;
width: 400px;
}">
<div v-for="song in props.data" :key="song.id">
{{song.name}}
</div>
</div>
</div>
</div>
</template>