mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 更新配置和文件上传逻辑, 迁移数据库结构(前端也得改
- 移除不再使用的 vite-plugin-monaco-editor - 更新 package.json 和 vite.config.mts 文件 - 修改用户配置 API 逻辑,支持上传和下载配置 - 添加对文件上传的支持,优化文件处理逻辑 - 更新多个组件以支持新文件上传功能 - 删除不必要的 VTsuruTypes.ts 文件,整合到 VTsuruConfigTypes.ts 中
This commit is contained in:
@@ -634,14 +634,21 @@ watch(() => accountInfo.value?.settings?.questionBox?.saftyLevel, (newLevel) =>
|
||||
</NFlex>
|
||||
</template>
|
||||
<!-- 问题内容 -->
|
||||
<template v-if="item.question?.image">
|
||||
<NImage
|
||||
:src="item.question.image"
|
||||
width="100"
|
||||
object-fit="cover"
|
||||
lazy
|
||||
style="border-radius: 4px; margin-bottom: 5px;"
|
||||
/>
|
||||
<template v-if="item.questionImages && item.questionImages.length > 0">
|
||||
<NSpace
|
||||
vertical
|
||||
size="small"
|
||||
>
|
||||
<NImage
|
||||
v-for="(img, index) in item.questionImages"
|
||||
:key="index"
|
||||
:src="img.path"
|
||||
width="100"
|
||||
object-fit="cover"
|
||||
lazy
|
||||
style="border-radius: 4px; margin-bottom: 5px;"
|
||||
/>
|
||||
</NSpace>
|
||||
<br>
|
||||
</template>
|
||||
<NText>{{ item.question?.message }}</NText>
|
||||
@@ -797,7 +804,13 @@ watch(() => accountInfo.value?.settings?.questionBox?.saftyLevel, (newLevel) =>
|
||||
>
|
||||
允许未注册/匿名用户进行提问
|
||||
</NCheckbox>
|
||||
|
||||
<NCheckbox
|
||||
v-model:checked="accountInfo.settings.questionBox.allowImageUpload"
|
||||
:disabled="useQB.isLoading"
|
||||
@update:checked="saveQuestionBoxSettings"
|
||||
>
|
||||
允许上传图片
|
||||
</NCheckbox>
|
||||
<!-- 内容审查 -->
|
||||
<NDivider title-placement="left">
|
||||
内容审查等级
|
||||
|
||||
@@ -128,11 +128,14 @@ onUnmounted(() => {
|
||||
<div class="question-display-text">
|
||||
{{ question?.question.message }}
|
||||
</div>
|
||||
<img
|
||||
v-if="setting.showImage && question?.question.image"
|
||||
class="question-display-image"
|
||||
:src="question?.question.image"
|
||||
>
|
||||
<div v-if="setting.showImage && question?.questionImages && question.questionImages.length > 0" class="question-display-images">
|
||||
<img
|
||||
v-for="(img, index) in question.questionImages"
|
||||
:key="index"
|
||||
class="question-display-image"
|
||||
:src="img.path"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
@@ -186,10 +189,18 @@ onUnmounted(() => {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.question-display-images {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.question-display-image {
|
||||
max-width: 40%;
|
||||
max-height: 40%;
|
||||
margin: 0 auto;
|
||||
max-height: 150px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
TemplateMapType,
|
||||
USER_INDEX_API_URL,
|
||||
} from '@/data/constants';
|
||||
import { ConfigItemDefinition } from '@/data/VTsuruTypes';
|
||||
import { ConfigItemDefinition } from '@/data/VTsuruConfigTypes';
|
||||
import { Delete24Regular } from '@vicons/fluent';
|
||||
import {
|
||||
NAlert,
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { copyToClipboard, getImageUploadModel } from '@/Utils'
|
||||
import { copyToClipboard } from '@/Utils'
|
||||
import { DisableFunction, EnableFunction, useAccount } from '@/api/account'
|
||||
import { FunctionTypes, GoodsStatus, GoodsTypes, UploadPointGoodsModel, ResponsePointGoodModel, KeySelectionMode } from '@/api/api-models'
|
||||
import {
|
||||
FunctionTypes,
|
||||
GoodsStatus,
|
||||
GoodsTypes,
|
||||
KeySelectionMode,
|
||||
ResponsePointGoodModel,
|
||||
UploadPointGoodsModel,
|
||||
UserFileLocation
|
||||
} from '@/api/api-models'
|
||||
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 { CURRENT_HOST, POINT_API_URL } from '@/data/constants'
|
||||
import { uploadFiles, UploadStage } from '@/data/fileUpload'
|
||||
import { useBiliAuth } from '@/store/useBiliAuth'
|
||||
import { Info24Filled } from '@vicons/fluent'
|
||||
import { useRouteHash } from '@vueuse/router'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import {
|
||||
FormItemRule,
|
||||
NAlert,
|
||||
NButton,
|
||||
NCheckbox,
|
||||
NDivider,
|
||||
NDynamicTags,
|
||||
NEmpty,
|
||||
NFlex,
|
||||
NForm,
|
||||
@@ -23,12 +32,12 @@ import {
|
||||
NGrid,
|
||||
NGridItem,
|
||||
NIcon,
|
||||
NImage,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NInputGroup,
|
||||
NInputNumber,
|
||||
NModal,
|
||||
NPopconfirm,
|
||||
NProgress,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
NScrollbar,
|
||||
@@ -41,10 +50,9 @@ import {
|
||||
NUpload,
|
||||
UploadFileInfo,
|
||||
useDialog,
|
||||
useMessage,
|
||||
NDynamicTags,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import PointOrderManage from './PointOrderManage.vue'
|
||||
import PointSettings from './PointSettings.vue'
|
||||
import PointUserManage from './PointUserManage.vue'
|
||||
@@ -57,6 +65,8 @@ const formRef = ref()
|
||||
const isUpdating = ref(false)
|
||||
const isAllowedPrivacyPolicy = ref(false)
|
||||
const showAddGoodsModal = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const isUploadingCover = ref(false)
|
||||
|
||||
// 路由哈希处理
|
||||
const realHash = useRouteHash('goods', { mode: 'replace' })
|
||||
@@ -71,7 +81,7 @@ const hash = computed({
|
||||
|
||||
// 商品数据及模型
|
||||
const goods = ref<ResponsePointGoodModel[]>(await biliAuth.GetGoods(accountInfo.value?.id, message))
|
||||
const defaultGoodsModel = {
|
||||
const defaultGoodsModel = (): { goods: UploadPointGoodsModel; fileList: UploadFileInfo[] } => ({
|
||||
goods: {
|
||||
type: GoodsTypes.Virtual,
|
||||
status: GoodsStatus.Normal,
|
||||
@@ -87,14 +97,24 @@ const defaultGoodsModel = {
|
||||
name: '',
|
||||
price: 0,
|
||||
tags: [],
|
||||
description: ''
|
||||
description: '',
|
||||
cover: undefined,
|
||||
} as UploadPointGoodsModel,
|
||||
fileList: [],
|
||||
} as { goods: UploadPointGoodsModel; fileList: UploadFileInfo[] }
|
||||
})
|
||||
const currentGoodsModel = ref<{ goods: UploadPointGoodsModel; fileList: UploadFileInfo[] }>(
|
||||
JSON.parse(JSON.stringify(defaultGoodsModel))
|
||||
defaultGoodsModel()
|
||||
)
|
||||
|
||||
// 监听 fileList 变化,确保 cover 和 fileList 同步
|
||||
watch(() => currentGoodsModel.value.fileList, (newFileList, oldFileList) => {
|
||||
if (oldFileList && oldFileList.length > 0 && newFileList.length === 0) {
|
||||
if (currentGoodsModel.value.goods.id && currentGoodsModel.value.goods.cover) {
|
||||
currentGoodsModel.value.goods.cover = undefined
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 计算属性
|
||||
const allowedYearOptions = computed(() => {
|
||||
return Array.from({ length: new Date().getFullYear() - 2024 + 1 }, (_, i) => 2024 + i).map((item) => ({
|
||||
@@ -202,12 +222,45 @@ async function updateGoods(e: MouseEvent) {
|
||||
if (isUpdating.value || !formRef.value) return
|
||||
e.preventDefault()
|
||||
isUpdating.value = true
|
||||
isUploadingCover.value = false
|
||||
uploadProgress.value = 0
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (currentGoodsModel.value.fileList.length > 0) {
|
||||
currentGoodsModel.value.goods.cover = await getImageUploadModel(currentGoodsModel.value.fileList)
|
||||
const newFilesToUpload = currentGoodsModel.value.fileList.filter(f => f.file && f.status !== 'finished')
|
||||
if (newFilesToUpload.length > 0 && newFilesToUpload[0].file) {
|
||||
isUploadingCover.value = true
|
||||
message.info('正在上传封面...')
|
||||
const uploadResults = await uploadFiles(
|
||||
[newFilesToUpload[0].file],
|
||||
undefined,
|
||||
UserFileLocation.Local,
|
||||
(stage: string) => {
|
||||
if (stage === UploadStage.Uploading) {
|
||||
uploadProgress.value = 0
|
||||
}
|
||||
}
|
||||
)
|
||||
isUploadingCover.value = false
|
||||
if (uploadResults && uploadResults.length > 0) {
|
||||
currentGoodsModel.value.goods.cover = uploadResults[0]
|
||||
message.success('封面上传成功')
|
||||
const uploadedFileIndex = currentGoodsModel.value.fileList.findIndex(f => f.id === newFilesToUpload[0].id)
|
||||
if (uploadedFileIndex > -1) {
|
||||
currentGoodsModel.value.fileList[uploadedFileIndex] = {
|
||||
...currentGoodsModel.value.fileList[uploadedFileIndex],
|
||||
id: uploadResults[0].id.toString(),
|
||||
status: 'finished',
|
||||
thumbnailUrl: uploadResults[0].path,
|
||||
url: uploadResults[0].path
|
||||
};
|
||||
}
|
||||
} else {
|
||||
throw new Error('封面上传失败')
|
||||
}
|
||||
} else if (currentGoodsModel.value.fileList.length === 0 && currentGoodsModel.value.goods.id) {
|
||||
currentGoodsModel.value.goods.cover = undefined
|
||||
}
|
||||
|
||||
const { code, data, message: errMsg } = await QueryPostAPI<ResponsePointGoodModel>(
|
||||
@@ -216,9 +269,9 @@ async function updateGoods(e: MouseEvent) {
|
||||
)
|
||||
|
||||
if (code === 200) {
|
||||
message.success('成功')
|
||||
message.success('商品信息保存成功')
|
||||
showAddGoodsModal.value = false
|
||||
currentGoodsModel.value = JSON.parse(JSON.stringify(defaultGoodsModel))
|
||||
currentGoodsModel.value = defaultGoodsModel()
|
||||
|
||||
const index = goods.value.findIndex(g => g.id === data.id)
|
||||
if (index >= 0) {
|
||||
@@ -227,13 +280,15 @@ async function updateGoods(e: MouseEvent) {
|
||||
goods.value.push(data)
|
||||
}
|
||||
} else {
|
||||
message.error('失败: ' + errMsg)
|
||||
message.error('商品信息保存失败: ' + errMsg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
message.error(typeof err === 'string' ? `失败: ${err}` : '表单验证失败')
|
||||
} catch (err: any) {
|
||||
console.error(currentGoodsModel.value, err)
|
||||
const errorMsg = err instanceof Error ? err.message : typeof err === 'string' ? err : '表单验证失败或上传出错'
|
||||
message.error(`失败: ${errorMsg}`)
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
isUploadingCover.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,23 +296,24 @@ function OnFileListChange(files: UploadFileInfo[]) {
|
||||
if (files.length === 1 && (files[0].file?.size ?? 0) > 10 * 1024 * 1024) {
|
||||
message.error('文件大小不能超过10MB')
|
||||
currentGoodsModel.value.fileList = []
|
||||
} else {
|
||||
currentGoodsModel.value.fileList = files
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateClick(item: ResponsePointGoodModel) {
|
||||
currentGoodsModel.value = {
|
||||
goods: {
|
||||
goods: JSON.parse(JSON.stringify({
|
||||
...item,
|
||||
count: item.count,
|
||||
cover: undefined,
|
||||
},
|
||||
})),
|
||||
fileList: item.cover
|
||||
? [
|
||||
{
|
||||
id: item.cover ?? 'cover',
|
||||
thumbnailUrl: FILE_BASE_URL + item.cover,
|
||||
name: '封面',
|
||||
id: item.cover.id.toString(),
|
||||
name: item.cover.name || '封面',
|
||||
status: 'finished',
|
||||
url: item.cover.path,
|
||||
thumbnailUrl: item.cover.path,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
@@ -336,14 +392,15 @@ function onDeleteClick(item: ResponsePointGoodModel) {
|
||||
}
|
||||
|
||||
function onModalOpen() {
|
||||
if (currentGoodsModel.value.goods.id) {
|
||||
if (!currentGoodsModel.value.goods.id) {
|
||||
resetGoods()
|
||||
}
|
||||
showAddGoodsModal.value = true
|
||||
}
|
||||
|
||||
function resetGoods() {
|
||||
currentGoodsModel.value = JSON.parse(JSON.stringify(defaultGoodsModel))
|
||||
currentGoodsModel.value = defaultGoodsModel()
|
||||
isAllowedPrivacyPolicy.value = false
|
||||
}
|
||||
|
||||
onMounted(() => { })
|
||||
@@ -613,6 +670,8 @@ onMounted(() => { })
|
||||
style="width: 600px; max-width: 90%"
|
||||
title="添加/修改礼物信息"
|
||||
class="goods-modal"
|
||||
:mask-closable="!isUpdating && !isUploadingCover"
|
||||
:close-on-esc="!isUpdating && !isUploadingCover"
|
||||
>
|
||||
<template #header-extra>
|
||||
<NPopconfirm
|
||||
@@ -637,7 +696,7 @@ onMounted(() => { })
|
||||
>
|
||||
<NForm
|
||||
ref="formRef"
|
||||
:model="currentGoodsModel"
|
||||
:model="currentGoodsModel.goods"
|
||||
:rules="rules"
|
||||
style="width: 100%"
|
||||
>
|
||||
@@ -746,28 +805,36 @@ onMounted(() => { })
|
||||
vertical
|
||||
:gap="8"
|
||||
>
|
||||
<NFlex
|
||||
v-if="currentGoodsModel.goods.cover"
|
||||
:gap="8"
|
||||
align="center"
|
||||
>
|
||||
<NText>当前封面: </NText>
|
||||
<NImage
|
||||
:src="FILE_BASE_URL + currentGoodsModel.goods.cover"
|
||||
height="50"
|
||||
object-fit="cover"
|
||||
/>
|
||||
</NFlex>
|
||||
<NUpload
|
||||
v-model:file-list="currentGoodsModel.fileList"
|
||||
:max="1"
|
||||
accept=".png,.jpg,.jpeg,.gif,.svg,.webp,.ico,.bmp,.tif,.tiff,.jfif,.jpe,.jp,.psd,."
|
||||
list-type="image-card"
|
||||
:default-upload="false"
|
||||
:disabled="isUploadingCover"
|
||||
@update:file-list="OnFileListChange"
|
||||
>
|
||||
+ {{ currentGoodsModel.goods.cover ? '更换' : '上传' }}封面
|
||||
<NFlex
|
||||
vertical
|
||||
align="center"
|
||||
justify="center"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
<NIcon
|
||||
size="24"
|
||||
:depth="3"
|
||||
/>
|
||||
<span>{{ currentGoodsModel.goods.cover ? '更换' : '上传' }}封面</span>
|
||||
<span style="font-size: 12px; color: grey">(小于10MB)</span>
|
||||
</NFlex>
|
||||
</NUpload>
|
||||
<NProgress
|
||||
v-if="isUploadingCover"
|
||||
type="line"
|
||||
:percentage="uploadProgress"
|
||||
:indicator-placement="'inside'"
|
||||
processing
|
||||
/>
|
||||
</NFlex>
|
||||
</NFormItem>
|
||||
|
||||
@@ -779,7 +846,7 @@ onMounted(() => { })
|
||||
兑换规则
|
||||
</NDivider>
|
||||
<NFormItem
|
||||
path="goods.type"
|
||||
path="type"
|
||||
label="礼物类型"
|
||||
>
|
||||
<NRadioGroup v-model:value="currentGoodsModel.goods.type">
|
||||
@@ -1066,10 +1133,12 @@ onMounted(() => { })
|
||||
<NButton
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="isUpdating"
|
||||
:loading="isUpdating || isUploadingCover"
|
||||
:disabled="isUploadingCover"
|
||||
@click="updateGoods"
|
||||
>
|
||||
{{ currentGoodsModel.goods.id ? '修改' : '创建' }}
|
||||
<span v-if="isUploadingCover">正在上传封面...</span>
|
||||
<span v-else>{{ currentGoodsModel.goods.id ? '修改' : '创建' }}</span>
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</template>
|
||||
@@ -1157,4 +1226,12 @@ onMounted(() => { })
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
.goods-modal :deep(.n-upload-trigger.n-upload-trigger--image-card) {
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
<template>
|
||||
<yt-live-chat-author-badge-renderer :type="authorTypeText">
|
||||
<NTooltip :content="readableAuthorTypeText" placement="top">
|
||||
<NTooltip
|
||||
:content="readableAuthorTypeText"
|
||||
placement="top"
|
||||
>
|
||||
<template #trigger>
|
||||
<div id="image" class="style-scope yt-live-chat-author-badge-renderer">
|
||||
<yt-icon v-if="isAdmin" class="style-scope yt-live-chat-author-badge-renderer">
|
||||
<svg viewBox="0 0 16 16" class="style-scope yt-icon" preserveAspectRatio="xMidYMid meet" focusable="false"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%;">
|
||||
<div
|
||||
id="image"
|
||||
class="style-scope yt-live-chat-author-badge-renderer"
|
||||
>
|
||||
<yt-icon
|
||||
v-if="isAdmin"
|
||||
class="style-scope yt-live-chat-author-badge-renderer"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
class="style-scope yt-icon"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%;"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path class="style-scope yt-icon"
|
||||
d="M9.64589146,7.05569719 C9.83346524,6.562372 9.93617022,6.02722257 9.93617022,5.46808511 C9.93617022,3.00042984 7.93574038,1 5.46808511,1 C4.90894765,1 4.37379823,1.10270499 3.88047304,1.29027875 L6.95744681,4.36725249 L4.36725255,6.95744681 L1.29027875,3.88047305 C1.10270498,4.37379824 1,4.90894766 1,5.46808511 C1,7.93574038 3.00042984,9.93617022 5.46808511,9.93617022 C6.02722256,9.93617022 6.56237198,9.83346524 7.05569716,9.64589147 L12.4098057,15 L15,12.4098057 L9.64589146,7.05569719 Z">
|
||||
</path>
|
||||
<path
|
||||
class="style-scope yt-icon"
|
||||
d="M9.64589146,7.05569719 C9.83346524,6.562372 9.93617022,6.02722257 9.93617022,5.46808511 C9.93617022,3.00042984 7.93574038,1 5.46808511,1 C4.90894765,1 4.37379823,1.10270499 3.88047304,1.29027875 L6.95744681,4.36725249 L4.36725255,6.95744681 L1.29027875,3.88047305 C1.10270498,4.37379824 1,4.90894766 1,5.46808511 C1,7.93574038 3.00042984,9.93617022 5.46808511,9.93617022 C6.02722256,9.93617022 6.56237198,9.83346524 7.05569716,9.64589147 L12.4098057,15 L15,12.4098057 L9.64589146,7.05569719 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</yt-icon>
|
||||
<img v-else :src="`${fileServerUrl}/blivechat/icons/guard-level-${privilegeType}.png`"
|
||||
class="style-scope yt-live-chat-author-badge-renderer" :alt="readableAuthorTypeText">
|
||||
<img
|
||||
v-else
|
||||
:src="`${fileServerUrl}/blivechat/icons/guard-level-${privilegeType}.png`"
|
||||
class="style-scope yt-live-chat-author-badge-renderer"
|
||||
:alt="readableAuthorTypeText"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
{{ readableAuthorTypeText }}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { copyToClipboard } from '@/Utils'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { DownloadConfig, UploadConfig, useAccount } from '@/api/account'
|
||||
import { EventDataTypes, EventModel, OpenLiveInfo } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import { FETCH_API, VTSURU_API_URL } from '@/data/constants'
|
||||
import { FETCH_API } from '@/data/constants'
|
||||
import { useDanmakuClient } from '@/store/useDanmakuClient'
|
||||
import { Info24Filled, Mic24Filled } from '@vicons/fluent'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
@@ -456,38 +455,23 @@ function stopSpeech() {
|
||||
message.success('已停止监听')
|
||||
}
|
||||
async function uploadConfig() {
|
||||
await QueryPostAPI(VTSURU_API_URL + 'set-config', {
|
||||
name: 'Speech',
|
||||
json: JSON.stringify(settings.value),
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
message.success('已保存至服务器')
|
||||
} else {
|
||||
message.error('保存失败: ' + data.message)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('保存失败')
|
||||
})
|
||||
const result = await UploadConfig('Speech', settings.value)
|
||||
if (result) {
|
||||
message.success('已保存至服务器')
|
||||
} else {
|
||||
message.error('保存失败')
|
||||
}
|
||||
}
|
||||
async function downloadConfig() {
|
||||
await QueryGetAPI<string>(VTSURU_API_URL + 'get-config', {
|
||||
name: 'Speech',
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
settings.value = JSON.parse(data.data)
|
||||
message.success('已获取配置文件')
|
||||
} else if (data.code == 404) {
|
||||
message.error('未上传配置文件')
|
||||
} else {
|
||||
message.error('获取失败: ' + data.message)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('获取失败')
|
||||
})
|
||||
const result = await DownloadConfig<SpeechSettings>('Speech')
|
||||
if (result.status === 'success' && result.data) {
|
||||
settings.value = result.data
|
||||
message.success('已获取配置文件')
|
||||
} else if (result.status === 'notfound') {
|
||||
message.error('未上传配置文件')
|
||||
} else {
|
||||
message.error('获取失败: ' + result.msg)
|
||||
}
|
||||
}
|
||||
function test(type: EventDataTypes) {
|
||||
switch (type) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -48,7 +48,7 @@ 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 { ConfigItemDefinition } from '@/data/VTsuruConfigTypes';
|
||||
import { useBiliAuth } from '@/store/useBiliAuth';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { addSeconds } from 'date-fns';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAccount } from '@/api/account';
|
||||
import { ResponseUserIndexModel, UserInfo } from '@/api/api-models';
|
||||
import { QueryGetAPI } from '@/api/query';
|
||||
import SimpleVideoCard from '@/components/SimpleVideoCard.vue';
|
||||
import { defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruTypes';
|
||||
import { defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruConfigTypes';
|
||||
import { USER_INDEX_API_URL } from '@/data/constants';
|
||||
import { NAvatar, NButton, NCard, NDivider, NFlex, NSpace, NText, useMessage } from 'naive-ui';
|
||||
import { ref } from 'vue';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { UserInfo } from '@/api/api-models'
|
||||
import { TemplateConfig } from '@/data/VTsuruTypes'
|
||||
import { TemplateConfig } from '@/data/VTsuruConfigTypes'
|
||||
import { h } from 'vue'
|
||||
|
||||
const width = window.innerWidth
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<script lang="ts">
|
||||
// --- Define Config First ---
|
||||
// NOTE: Define ConfigDefinition *before* types that depend on it.
|
||||
// Use 'any' for config param in render/onUploaded to break circular dependency for now.
|
||||
export const Config = defineTemplateConfig([
|
||||
{
|
||||
name: '背景图', // Removed 'as const'
|
||||
type: 'image',
|
||||
key: 'backgroundImage', // Removed 'as const'
|
||||
imageLimit: 1,
|
||||
default: [] as string[],
|
||||
onUploaded: (urls: string[], config: any) => {
|
||||
config.backgroundImage = urls;
|
||||
type: 'file',
|
||||
key: 'backgroundFile', // Removed 'as const'
|
||||
fileLimit: 1,
|
||||
onUploaded: (files: UploadFileResponse[], config: any) => {
|
||||
config.backgroundFile = files;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -58,26 +54,24 @@ export const Config = defineTemplateConfig([
|
||||
{
|
||||
name: '装饰图片',
|
||||
type: 'decorativeImages',
|
||||
key: 'decorativeImages',
|
||||
default: [] as DecorativeImageProperties[],
|
||||
key: 'decorativeFile',
|
||||
},
|
||||
]);
|
||||
export type KawaiiConfigType = ExtractConfigData<typeof Config>;
|
||||
export const DefaultConfig = {
|
||||
|
||||
|
||||
} as KawaiiConfigType;
|
||||
</script>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ScheduleDayInfo, ScheduleWeekInfo } from '@/api/api-models';
|
||||
import { ScheduleDayInfo, ScheduleWeekInfo, UploadFileResponse } from '@/api/api-models';
|
||||
import SaveCompoent from '@/components/SaveCompoent.vue'; // 引入截图组件
|
||||
import { ScheduleConfigTypeWithConfig } from '@/data/TemplateTypes'; // Use base type
|
||||
import { DecorativeImageProperties, defineTemplateConfig, ExtractConfigData, RGBAColor, rgbaToString } from '@/data/VTsuruTypes';
|
||||
import { FILE_BASE_URL } from '@/data/constants';
|
||||
import { defineTemplateConfig, ExtractConfigData, RGBAColor, rgbaToString } from '@/data/VTsuruConfigTypes';
|
||||
import { getWeek, getYear } from 'date-fns';
|
||||
import { NButton, NDivider, NEmpty, NFlex, NSelect, NSpace, useMessage } from 'naive-ui';
|
||||
import { computed, h, ref, watch, WritableComputedRef } from 'vue';
|
||||
import { NDivider, NSelect, NSpace, useMessage } from 'naive-ui';
|
||||
import { computed, ref, watch, WritableComputedRef } from 'vue';
|
||||
|
||||
// Get message instance
|
||||
const message = useMessage();
|
||||
@@ -87,14 +81,14 @@ const props = defineProps<ScheduleConfigTypeWithConfig<KawaiiConfigType>>();
|
||||
// --- 默认配置 --- Define DefaultConfig using KawaiiConfigType
|
||||
// No export needed here
|
||||
const DefaultConfig: KawaiiConfigType = {
|
||||
backgroundImage: [],
|
||||
backgroundFile: [],
|
||||
containerColor: { r: 255, g: 255, b: 255, a: 0.8 },
|
||||
dayLabelColor: { r: 126, g: 136, b: 184, a: 1 },
|
||||
dayContentBgColor: { r: 255, g: 255, b: 255, a: 1 },
|
||||
dayContentTextColor: { r: 100, g: 100, b: 100, a: 1 },
|
||||
timeLabelBgColor: { r: 245, g: 189, b: 189, a: 1 },
|
||||
timeLabelTextColor: { r: 255, g: 255, b: 255, a: 1 },
|
||||
decorativeImages: [],
|
||||
decorativeFile: [],
|
||||
};
|
||||
|
||||
// --- 状态 ---
|
||||
@@ -225,12 +219,12 @@ defineExpose({ Config, DefaultConfig });
|
||||
'--day-content-text-color': rgbaToString(effectiveConfig.dayContentTextColor),
|
||||
'--time-label-bg-color': rgbaToString(effectiveConfig.timeLabelBgColor),
|
||||
'--time-label-text-color': rgbaToString(effectiveConfig.timeLabelTextColor),
|
||||
backgroundImage: effectiveConfig.backgroundImage && effectiveConfig.backgroundImage.length > 0 ? `url(${FILE_BASE_URL + effectiveConfig.backgroundImage[0]})` : 'none',
|
||||
backgroundImage: effectiveConfig.backgroundFile && effectiveConfig.backgroundFile.length > 0 ? `url(${effectiveConfig.backgroundFile[0].path})` : 'none',
|
||||
}"
|
||||
>
|
||||
<!-- 装饰图片渲染 -->
|
||||
<div
|
||||
v-for="img in effectiveConfig.decorativeImages"
|
||||
v-for="img in effectiveConfig.decorativeFile"
|
||||
:key="img.id"
|
||||
class="decorative-image"
|
||||
:style="{
|
||||
@@ -247,7 +241,7 @@ defineExpose({ Config, DefaultConfig });
|
||||
}"
|
||||
>
|
||||
<img
|
||||
:src="FILE_BASE_URL + img.src"
|
||||
:src="img.path"
|
||||
alt="decoration"
|
||||
style="display: block; width: 100%; height: auto;"
|
||||
>
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, ref, watch, VNode } from 'vue';
|
||||
import { getUserAvatarUrl, isDarkMode } from '@/Utils';
|
||||
import { isDarkMode } from '@/Utils';
|
||||
import { useAccount } from '@/api/account';
|
||||
import { SongFrom, SongRequestOption, SongsInfo } from '@/api/api-models';
|
||||
import { SongListConfigTypeWithConfig } from '@/data/TemplateTypes';
|
||||
import { defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruTypes';
|
||||
import { FILE_BASE_URL } from '@/data/constants';
|
||||
import { NButton, NFlex, NIcon, NInput, NInputGroup, NInputGroupLabel, NTag, NTooltip, NSelect } from 'naive-ui';
|
||||
import { defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruConfigTypes';
|
||||
import { useBiliAuth } from '@/store/useBiliAuth';
|
||||
import bilibili from '@/svgs/bilibili.svg';
|
||||
import douyin from '@/svgs/douyin.svg';
|
||||
import FiveSingIcon from '@/svgs/fivesing.svg';
|
||||
import neteaseMusic from '@/svgs/neteaseMusic.svg';
|
||||
import qqMusic from '@/svgs/qqMusic.svg';
|
||||
import douyin from '@/svgs/douyin.svg';
|
||||
import { SongFrom, SongsInfo, SongRequestOption } from '@/api/api-models';
|
||||
import FiveSingIcon from '@/svgs/fivesing.svg';
|
||||
import { SquareArrowForward24Filled, ArrowCounterclockwise20Filled, ArrowSortDown20Filled, ArrowSortUp20Filled } from '@vicons/fluent';
|
||||
import { ArrowCounterclockwise20Filled, ArrowSortDown20Filled, ArrowSortUp20Filled, SquareArrowForward24Filled } from '@vicons/fluent';
|
||||
import { List } from 'linqts';
|
||||
import { useAccount } from '@/api/account';
|
||||
import { getSongRequestTooltip, getSongRequestConfirmText } from './utils/songRequestUtils';
|
||||
import { useBiliAuth } from '@/store/useBiliAuth';
|
||||
import { NButton, NFlex, NIcon, NInput, NInputGroup, NInputGroupLabel, NSelect, NTag, NTooltip } from 'naive-ui';
|
||||
import { computed, h, ref, VNode, watch } from 'vue';
|
||||
import { getSongRequestConfirmText, getSongRequestTooltip } from './utils/songRequestUtils';
|
||||
|
||||
// Interface Tab - can be reused for both language and tag buttons
|
||||
interface FilterButton {
|
||||
@@ -487,11 +486,12 @@ export const DefaultConfig = {} as TraditionalConfigType;
|
||||
export const Config = defineTemplateConfig([
|
||||
{
|
||||
name: '背景',
|
||||
type: 'image',
|
||||
imageLimit: 1,
|
||||
key: 'background',
|
||||
onUploaded: (url, config) => {
|
||||
config.background = url;
|
||||
type: 'file',
|
||||
fileLimit: 1,
|
||||
key: 'backgroundFile',
|
||||
onUploaded: (file, config) => {
|
||||
console.log(file, config);
|
||||
config.backgroundFile = file;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -645,7 +645,7 @@ export const Config = defineTemplateConfig([
|
||||
<div
|
||||
class="song-list-background-wrapper"
|
||||
:style="{
|
||||
backgroundImage: props.config?.background ? `url(${FILE_BASE_URL + props.config.background})` : 'none',
|
||||
backgroundImage: props.config?.backgroundFile && props.config.backgroundFile.length > 0 ? `url(${props.config.backgroundFile[0].path})` : 'none',
|
||||
}"
|
||||
>
|
||||
<!-- 原始: 滚动和内容容器 -->
|
||||
|
||||
Reference in New Issue
Block a user