add question display page

This commit is contained in:
2024-02-20 22:03:49 +08:00
parent e4c8839491
commit 6bf003d18b
21 changed files with 1427 additions and 573 deletions

View File

@@ -5,9 +5,11 @@ import { isSameDay } from 'date-fns'
import { createDiscreteApi } from 'naive-ui'
import { ref } from 'vue'
import { APIRoot, AccountInfo, FunctionTypes } from './api-models'
import { useRoute } from 'vue-router'
export const ACCOUNT = ref<AccountInfo>()
export const isLoadingAccount = ref(true)
const route = useRoute()
const { message } = createDiscreteApi(['message'])
const cookie = useLocalStorage('JWT_Token', '')
@@ -40,7 +42,8 @@ export async function GetSelfAccount() {
}
export function UpdateAccountLoop() {
setInterval(() => {
if (ACCOUNT.value) {
if (ACCOUNT.value && route?.name != 'question-display') {
// 防止在问题详情页刷新
GetSelfAccount()
}
}, 60 * 1000)
@@ -93,7 +96,7 @@ export async function DelBiliBlackList(id: number): Promise<APIRoot<unknown>> {
id: id,
})
}
export function downloadConfigDirect<T>(name: string) {
export function downloadConfigDirect(name: string) {
return QueryGetAPI<string>(VTSURU_API_URL + 'get-config', {
name: name,
})

View File

@@ -91,6 +91,7 @@ export interface UserSetting {
songRequest: Setting_SongRequest
queue: Setting_Queue
point: Setting_Point
questionDisplay: Setting_QuestionDisplay
enableFunctions: FunctionTypes[]
@@ -169,6 +170,32 @@ export interface Setting_Point {
giftPointPercent: number // double maps to number in TypeScript
giftAllowType: SettingPointGiftAllowType
}
export interface Setting_QuestionDisplay {
font?: string // Optional string, with a maximum length of 30 characters
nameFont: string // Optional string, with a maximum length of 30 characters
fontSize: number // Default is 20
fontWeight: number
nameFontSize: number // Default is 20
lineSpacing: number // Default is 0, 行间距
fontColor?: string // Optional string, must exactly be 6 characters long
nameFontColor?: string // Optional string, must exactly be 6 characters long
nameFontWeight?: number
backgroundColor?: string // Optional string, must exactly be 6 characters long
showUserName: boolean // Default is true
align: QuestionDisplayAlign // Default is QuestionDisplayAlign.Left, 对齐
showImage: boolean // Default is false
borderColor?: string
borderWidth?: number
currentQuestion?: number
}
export enum QuestionDisplayAlign {
Left,
Right,
Center,
}
export enum SettingPointGiftAllowType {
All,
WhiteList,

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import { QAInfo } from '@/api/api-models'
import { NCard, NDivider, NFlex, NImage, NTag, NText, NTime, NTooltip } from 'naive-ui'
const props = defineProps<{
item: QAInfo
}>()
</script>
<template>
<NCard v-if="item" :embedded="!item.isReaded" hoverable size="small" :bordered="false">
<template #header>
<NFlex :size="0" align="center" >
<template v-if="!item.isReaded">
<NTag type="warning" size="tiny"> 未读 </NTag>
<NDivider vertical />
</template>
<NText :depth="item.isAnonymous ? 3 : 1" style="margin-top: 3px">
{{ item.isAnonymous ? '匿名用户' : item.sender?.name }}
</NText>
<NTag v-if="item.isSenderRegisted" size="small" type="info" :bordered="false" style="margin-left: 5px">
已注册
</NTag>
<NTag v-if="item.isPublic" size="small" type="success" :bordered="false" style="margin-left: 5px"> 公开 </NTag>
<NDivider vertical />
<NText depth="3" style="font-size: small">
<NTooltip>
<template #trigger>
<NTime :time="item.sendAt" :to="Date.now()" type="relative" />
</template>
<NTime :time="item.sendAt" />
</NTooltip>
</NText>
</NFlex>
</template>
<template #footer>
<slot name="footer" :item="item"></slot>
</template>
<template #header-extra>
<slot name="header-extra" :item="item"></slot>
</template>
<template v-if="item.question?.image">
<NImage v-if="item.question?.image" :src="item.question.image" height="100" lazy />
<br />
</template>
<NText style="">
{{ item.question?.message }}
</NText>
</NCard>
</template>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { QAInfo } from '@/api/api-models'
import { NCard, NDivider, NFlex, NImage, NList, NListItem, NTag, NText, NTime, NTooltip } from 'naive-ui'
import QuestionItem from './QuestionItem.vue'
const props = defineProps<{
questions: QAInfo[]
}>()
</script>
<template>
<NList bordered>
<NListItem v-for="item in questions" :key="item?.id">
<QuestionItem :item="item">
<template #footer>
<slot name="footer" :item="item"></slot>
</template>
<template #header-extra>
<slot name="header-extra" :item="item"></slot>
</template>
</QuestionItem>
</NListItem>
</NList>
</template>

View File

@@ -9,9 +9,7 @@ declare type MittType<T = any> = {
onMusicRequestPlayerEnded: {
music: Music
}
onMusicRequestPlayNextWaitingMusic: {
}
onMusicRequestPlayNextWaitingMusic: never
};
// 类型
const emitter: Emitter<MittType> = mitt<MittType>()

View File

@@ -5,6 +5,7 @@ import manage from './manage'
import user from './user'
import obs from './obs'
import open_live from './open_live'
import singlePage from './singlePage'
const routes: Array<RouteRecordRaw> = [
{
@@ -96,6 +97,7 @@ const routes: Array<RouteRecordRaw> = [
title: '页面不存在',
},
},
...singlePage,
]
const router = createRouter({
@@ -106,7 +108,7 @@ router.beforeEach((to, from, next) => {
useLoadingBarStore().loadingBar?.start()
next()
})
router.afterEach((to, from) => {
router.afterEach(() => {
const loadingBar = useLoadingBarStore().loadingBar
loadingBar?.finish()
})

View File

@@ -1,38 +1,46 @@
export default {
path: '/obs',
name: 'obs',
children: [
{
path: 'live-lottery',
name: 'obs-live-lottery',
component: () => import('@/views/obs/LiveLotteryOBS.vue'),
meta: {
title: '直播抽奖',
},
path: '/obs',
name: 'obs',
children: [
{
path: 'live-lottery',
name: 'obs-live-lottery',
component: () => import('@/views/obs/LiveLotteryOBS.vue'),
meta: {
title: '直播抽奖',
},
{
path: 'song-request',
name: 'obs-song-request',
component: () => import('@/views/obs/SongRequestOBS.vue'),
meta: {
title: '弹幕点歌 (歌势',
},
},
{
path: 'song-request',
name: 'obs-song-request',
component: () => import('@/views/obs/SongRequestOBS.vue'),
meta: {
title: '弹幕点歌 (歌势',
},
{
path: 'queue',
name: 'obs-queue',
component: () => import('@/views/obs/QueueOBS.vue'),
meta: {
title: '弹幕排队',
},
},
{
path: 'queue',
name: 'obs-queue',
component: () => import('@/views/obs/QueueOBS.vue'),
meta: {
title: '弹幕排队',
},
{
path: 'music-request',
name: 'obs-music-request',
component: () => import('@/views/obs/MusicRequestOBS.vue'),
meta: {
title: '弹幕排队 (播放',
},
},
{
path: 'music-request',
name: 'obs-music-request',
component: () => import('@/views/obs/MusicRequestOBS.vue'),
meta: {
title: '弹幕排队 (播放',
},
],
}
},
{
path: 'question-display',
name: 'obs-question-display',
component: () => import('@/views/obs/QuestionDisplayOBS.vue'),
meta: {
title: '棉花糖展示',
},
},
],
}

10
src/router/singlePage.ts Normal file
View File

@@ -0,0 +1,10 @@
export default [
{
path: '/question-display',
name: 'question-display',
component: () => import('@/views/single/QuestionDisplay.vue'),
meta: {
title: '棉花糖展示页',
},
},
]

View File

@@ -106,7 +106,7 @@ export const useAuthStore = defineStore('BiliAuth', () => {
return []
}
try {
var resp = await QueryGetAPI<ResponsePointGoodModel[]>(POINT_API_URL + 'get-goods', {
const resp = await QueryGetAPI<ResponsePointGoodModel[]>(POINT_API_URL + 'get-goods', {
id: id,
})
if (resp.code == 200) {

245
src/store/useQuestionBox.ts Normal file
View File

@@ -0,0 +1,245 @@
import { useAccount } from '@/api/account'
import { QAInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import { ACCOUNT_API_URL, QUESTION_API_URL } from '@/data/constants'
import { List } from 'linqts'
import { useMessage } from 'naive-ui'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useQuestionBox = defineStore('QuestionBox', () => {
const isLoading = ref(false)
const isRepling = ref(false)
const isChangingPublic = ref(false)
const accountInfo = useAccount()
const recieveQuestions = ref<QAInfo[]>([])
const sendQuestions = ref<QAInfo[]>([])
const onlyFavorite = ref(false)
const onlyPublic = ref(false)
const onlyUnread = ref(false)
const recieveQuestionsFiltered = computed(() => {
const result = recieveQuestions.value.filter((q) => {
/*if (q.id == displayQuestion.value?.id) {
return false
}*/
return (
(q.isFavorite || !onlyFavorite.value) && (q.isPublic || !onlyPublic.value) && (!q.isReaded || !onlyUnread.value)
)
})
return result
//displayQuestion排在最前面
//return displayQuestion.value ? [displayQuestion.value, ...result] : result
})
const currentQuestion = ref<QAInfo>()
const displayQuestion = ref<QAInfo>()
let isRevieveGetted = false
let isSendGetted = false
const message = useMessage()
async function GetRecieveQAInfo() {
isLoading.value = true
await QueryGetAPI<QAInfo[]>(QUESTION_API_URL + 'get-recieve')
.then((data) => {
if (data.code == 200) {
if (data.data.length > 0) {
recieveQuestions.value = new List(data.data)
.OrderBy((d) => d.isReaded)
//.ThenByDescending(d => d.isFavorite)
.ThenByDescending((d) => d.sendAt)
.ToArray()
const displayId = accountInfo.value?.settings.questionDisplay.currentQuestion
if (displayId && displayQuestion.value?.id != displayId) {
displayQuestion.value = recieveQuestions.value.find((q) => q.id == displayId)
}
}
message.success('共收取 ' + data.data.length + ' 条提问')
isRevieveGetted = true
} else {
message.error(data.message)
}
})
.catch((err) => {
message.error('发生错误: ' + err)
})
.finally(() => {
isLoading.value = false
})
}
async function GetSendQAInfo() {
isLoading.value = true
await QueryGetAPI<QAInfo[]>(QUESTION_API_URL + 'get-send')
.then((data) => {
if (data.code == 200) {
sendQuestions.value = data.data
message.success('共发送 ' + data.data.length + ' 条提问')
} else {
message.error(data.message)
}
})
.catch((err) => {
message.error('发生错误')
})
.finally(() => {
isLoading.value = false
})
}
async function reply(id: number, msg: string) {
isRepling.value = true
await QueryPostAPI<QAInfo>(QUESTION_API_URL + 'reply', {
Id: id,
Message: msg,
})
.then((data) => {
if (data.code == 200) {
var index = recieveQuestions.value.findIndex((q) => q.id == id)
if (index > -1) {
recieveQuestions.value[index] = data.data
}
message.success('回复成功')
currentQuestion.value = undefined
//replyModalVisiable.value = false
} else {
message.error('发送失败: ' + data.message)
}
})
.catch((err) => {
message.error('发送失败')
})
.finally(() => {
isRepling.value = false
})
}
async function read(question: QAInfo, read: boolean) {
await QueryGetAPI(QUESTION_API_URL + 'read', {
id: question.id,
read: read ? 'true' : 'false',
})
.then((data) => {
if (data.code == 200) {
question.isReaded = read
if (read && displayQuestion.value?.id == question.id) {
setCurrentQuestion(question) //取消设为当前展示的问题
}
} else {
message.error('修改失败: ' + data.message)
}
})
.catch((err) => {
message.error('修改失败')
})
}
async function favorite(question: QAInfo, fav: boolean) {
await QueryGetAPI(QUESTION_API_URL + 'favorite', {
id: question.id,
favorite: fav,
})
.then((data) => {
if (data.code == 200) {
question.isFavorite = fav
} else {
message.error('修改失败: ' + data.message)
}
})
.catch((err) => {
message.error('修改失败')
})
}
async function setPublic(pub: boolean) {
isChangingPublic.value = true
await QueryGetAPI(QUESTION_API_URL + 'public', {
id: currentQuestion.value?.id,
public: pub,
})
.then((data) => {
if (data.code == 200) {
if (currentQuestion.value) currentQuestion.value.isPublic = pub
message.success('已修改公开状态')
} else {
message.error('修改失败: ' + data.message)
}
})
.catch((err) => {
message.error('修改失败')
})
.finally(() => {
isChangingPublic.value = false
})
}
async function blacklist(question: QAInfo) {
await QueryGetAPI(ACCOUNT_API_URL + 'black-list/add', {
id: question.sender.id,
})
.then(async (data) => {
if (data.code == 200) {
await QueryGetAPI(QUESTION_API_URL + 'del', {
id: question.id,
}).then((data) => {
if (data.code == 200) {
message.success('已拉黑 ' + question.sender.name)
} else {
message.error('修改失败: ' + data.message)
}
})
} else {
message.error('拉黑失败: ' + data.message)
}
})
.catch((err) => {
message.error('拉黑失败')
})
}
async function setCurrentQuestion(item: QAInfo) {
const isCurrent = displayQuestion.value?.id == item.id
if (!isCurrent) {
displayQuestion.value = item
} else {
displayQuestion.value = undefined
}
try {
const data = await QueryGetAPI(
QUESTION_API_URL + 'set-current',
isCurrent
? null
: {
id: item.id,
},
)
if (data.code == 200) {
//message.success('设置成功')
} else {
message.error('设置失败: ' + data.message)
}
} catch (err) {
message.error('设置失败:' + err)
}
}
return {
currentQuestion,
isLoading,
isRevieveGetted,
isRepling,
isChangingPublic,
recieveQuestions,
recieveQuestionsFiltered,
sendQuestions,
onlyFavorite,
onlyPublic,
onlyUnread,
displayQuestion,
GetRecieveQAInfo,
GetSendQAInfo,
reply,
read,
favorite,
setPublic,
blacklist,
setCurrentQuestion,
}
})

View File

@@ -31,6 +31,7 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
</NSpace>
<NDivider title-placement="left"> 更新日志 </NDivider>
<NTimeline>
<NTimelineItem type="info" title="功能更新" content="棉花糖添加展示页面" time="2024-2-20" />
<NTimelineItem type="info" title="功能更新" content="歌单新增从文件导入" time="2024-2-10" />
<NTimelineItem type="info" title="功能更新" content="排队的OBS组件添加设置项" time="2024-1-27" />
<NTimelineItem type="warning" title="Bug修复" content="修复点歌会直接跳到下一首的问题 (怎么没人跟我说" time="2024-1-22" />

View File

@@ -544,7 +544,7 @@ onMounted(() => {
</NLayoutSider>
<NLayout>
<NScrollbar :style="`height: calc(100vh - 50px - ${aplayerHeight}px)`">
<NLayoutContent style="box-sizing: border-box; padding: 20px; min-width: 300px">
<NLayoutContent style="box-sizing: border-box; padding: 20px; min-width: 300px; height: 100%">
<RouterView v-if="accountInfo?.isEmailVerified" v-slot="{ Component, route }">
<KeepAlive>
<DanmakuLayout v-if="route.meta.danmaku" :component="Component" />

View File

@@ -59,7 +59,7 @@ const menuOptions = ref<MenuOption[]>()
async function RequestBiliUserData() {
await fetch(FETCH_API + `https://account.bilibili.com/api/member/getCardByMid?mid=${userInfo.value?.biliId}`).then(
async (respone) => {
let data = await respone.json()
const data = await respone.json()
if (data.code == 0) {
biliUserInfo.value = data.card
} else {

View File

@@ -83,7 +83,7 @@ const showModal = ref(false)
const inputDynamic = ref<string>()
const inputDynamicId = computed(() => {
try {
var id = BigInt(inputDynamic.value ?? '')
const id = BigInt(inputDynamic.value ?? '')
return id
} catch {
try {

View File

@@ -2,18 +2,19 @@
import { copyToClipboard, downloadImage } from '@/Utils'
import { SaveAccountSettings, useAccount } from '@/api/account'
import { QAInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import { ACCOUNT_API_URL, QUESTION_API_URL } from '@/data/constants'
import { QueryGetAPI } from '@/api/query'
import { QUESTION_API_URL } from '@/data/constants'
import router from '@/router'
import { Heart, HeartOutline } from '@vicons/ionicons5'
import { Heart, HeartOutline, SwapHorizontal } from '@vicons/ionicons5'
import { saveAs } from 'file-saver'
import html2canvas from 'html2canvas'
import { List } from 'linqts'
import {
NAffix,
NButton,
NCard,
NCheckbox,
NDivider,
NFlex,
NIcon,
NImage,
NInput,
@@ -21,8 +22,10 @@ import {
NList,
NListItem,
NModal,
NScrollbar,
NSpace,
NSpin,
NSplit,
NSwitch,
NTabPane,
NTabs,
@@ -35,195 +38,42 @@ import {
import QrcodeVue from 'qrcode.vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import QuestionDisplay from './QuestionDisplaySettings.vue'
import { useElementSize } from '@vueuse/core'
import QuestionItem from '@/components/QuestionItems.vue'
import { ArrowCircleRight12Filled } from '@vicons/fluent'
import { useQuestionBox } from '@/store/useQuestionBox'
const accountInfo = useAccount()
const route = useRoute()
const recieveQuestions = ref<QAInfo[]>([])
const recieveQuestionsFiltered = computed(() => {
return recieveQuestions.value.filter((q) => {
return (
(q.isFavorite || !onlyFavorite.value) && (q.isPublic || !onlyPublic.value) && (!q.isReaded || !onlyUnread.value)
)
})
})
const sendQuestions = ref<QAInfo[]>([])
const message = useMessage()
const useQB = useQuestionBox()
const selectedTabItem = ref(route.query.send ? '1' : '0')
const isRepling = ref(false)
const onlyFavorite = ref(false)
const onlyPublic = ref(false)
const onlyUnread = ref(false)
const isLoading = ref(true)
const isChangingPublic = ref(false)
const replyModalVisiable = ref(false)
const shareModalVisiable = ref(false)
const currentQuestion = ref<QAInfo>()
const replyMessage = ref()
const showSettingCard = ref(true)
const shareCardRef = ref()
const shareUrl = computed(() => 'https://vtsuru.live/user/' + accountInfo.value?.name + '/question-box')
async function GetRecieveQAInfo() {
isLoading.value = true
await QueryGetAPI<QAInfo[]>(QUESTION_API_URL + 'get-recieve')
.then((data) => {
if (data.code == 200) {
if (data.data.length > 0) {
recieveQuestions.value = new List(data.data)
.OrderBy((d) => d.isReaded)
//.ThenByDescending(d => d.isFavorite)
.ThenByDescending((d) => d.sendAt)
.ToArray()
}
message.success('共收取 ' + data.data.length + ' 条提问')
isRevieveGetted = true
} else {
message.error(data.message)
}
})
.catch((err) => {
message.error('发生错误')
})
.finally(() => {
isLoading.value = false
})
}
async function GetSendQAInfo() {
isLoading.value = true
await QueryGetAPI<QAInfo[]>(QUESTION_API_URL + 'get-send')
.then((data) => {
if (data.code == 200) {
sendQuestions.value = data.data
message.success('共发送 ' + data.data.length + ' 条提问')
} else {
message.error(data.message)
}
})
.catch((err) => {
message.error('发生错误')
})
.finally(() => {
isLoading.value = false
})
}
async function reply() {
isRepling.value = true
await QueryPostAPI<QAInfo>(QUESTION_API_URL + 'reply', {
Id: currentQuestion.value?.id,
Message: replyMessage.value,
})
.then((data) => {
if (data.code == 200) {
var index = recieveQuestions.value.findIndex((q) => q.id == currentQuestion.value?.id)
if (index > -1) {
recieveQuestions.value[index] = data.data
}
message.success('回复成功')
currentQuestion.value = undefined
replyModalVisiable.value = false
} else {
message.error('发送失败: ' + data.message)
}
})
.catch((err) => {
message.error('发送失败')
})
.finally(() => {
isRepling.value = false
})
}
async function read(question: QAInfo, read: boolean) {
await QueryGetAPI(QUESTION_API_URL + 'read', {
id: question.id,
read: read ? 'true' : 'false',
})
.then((data) => {
if (data.code == 200) {
question.isReaded = read
} else {
message.error('修改失败: ' + data.message)
}
})
.catch((err) => {
message.error('修改失败')
})
}
async function favorite(question: QAInfo, fav: boolean) {
await QueryGetAPI(QUESTION_API_URL + 'favorite', {
id: question.id,
favorite: fav,
})
.then((data) => {
if (data.code == 200) {
question.isFavorite = fav
} else {
message.error('修改失败: ' + data.message)
}
})
.catch((err) => {
message.error('修改失败')
})
}
async function setPublic(pub: boolean) {
isChangingPublic.value = true
await QueryGetAPI(QUESTION_API_URL + 'public', {
id: currentQuestion.value?.id,
public: pub,
})
.then((data) => {
if (data.code == 200) {
if (currentQuestion.value) currentQuestion.value.isPublic = pub
message.success('已修改公开状态')
} else {
message.error('修改失败: ' + data.message)
}
})
.catch((err) => {
message.error('修改失败')
})
.finally(() => {
isChangingPublic.value = false
})
}
async function blacklist(question: QAInfo) {
await QueryGetAPI(ACCOUNT_API_URL + 'black-list/add', {
id: question.sender.id,
})
.then(async (data) => {
if (data.code == 200) {
await QueryGetAPI(QUESTION_API_URL + 'del', {
id: question.id,
}).then((data) => {
if (data.code == 200) {
message.success('已拉黑 ' + question.sender.name)
} else {
message.error('修改失败: ' + data.message)
}
})
} else {
message.error('拉黑失败: ' + data.message)
}
})
.catch((err) => {
message.error('拉黑失败')
})
}
let isRevieveGetted = false
let isSendGetted = false
async function onTabChange(value: string) {
if (value == '0' && !isRevieveGetted) {
await GetRecieveQAInfo()
await useQB.GetRecieveQAInfo()
isRevieveGetted = true
} else if (value == '1' && !isSendGetted) {
await GetSendQAInfo()
await useQB.GetSendQAInfo()
isSendGetted = true
}
}
function onOpenModal(question: QAInfo) {
currentQuestion.value = question
useQB.currentQuestion = question
replyMessage.value = question.answer?.message
replyModalVisiable.value = true
}
@@ -258,7 +108,7 @@ function saveQRCode() {
}
async function saveSettings() {
try {
isLoading.value = true
useQB.isLoading = true
const data = await SaveAccountSettings()
if (data.code == 200) {
message.success('保存成功')
@@ -268,15 +118,21 @@ async function saveSettings() {
} catch (error) {
message.error('保存失败:' + error)
}
isLoading.value = false
useQB.isLoading = false
}
const parentRef = ref<HTMLElement | null>(null)
onMounted(() => {
if (selectedTabItem.value == '0') {
GetRecieveQAInfo()
useQB.GetRecieveQAInfo()
} else {
GetSendQAInfo()
useQB.GetSendQAInfo()
}
useQB.displayQuestion = useQB.recieveQuestions.find(
(s) => s.id == accountInfo.value?.settings.questionDisplay.currentQuestion,
)
})
</script>
@@ -286,89 +142,63 @@ onMounted(() => {
<NButton type="primary" @click="shareModalVisiable = true" secondary> 分享 </NButton>
</NSpace>
<NDivider style="margin: 10px 0 10px 0" />
<NSpin v-if="isLoading" show />
<NSpin v-if="useQB.isLoading" show />
<NTabs v-else animated @update:value="onTabChange" v-model:value="selectedTabItem">
<NTabPane tab="我收到的" name="0">
<NCheckbox v-model:checked="onlyFavorite"> 只显示收藏 </NCheckbox>
<NCheckbox v-model:checked="onlyPublic"> 只显示公开 </NCheckbox>
<NCheckbox v-model:checked="onlyUnread"> 只显示未读 </NCheckbox>
<NList :bordered="false">
<NListItem v-for="item in recieveQuestionsFiltered" :key="item.id">
<NCard :embedded="!item.isReaded" hoverable size="small">
<template #header>
<NSpace :size="0" align="center">
<template v-if="!item.isReaded">
<NTag type="warning" size="tiny"> 未读 </NTag>
<NDivider vertical />
</template>
<NText :depth="item.isAnonymous ? 3 : 1" style="margin-top: 3px">
{{ item.isAnonymous ? '匿名用户' : item.sender?.name }}
</NText>
<NTag v-if="item.isSenderRegisted" size="small" type="info" :bordered="false" style="margin-left: 5px">
已注册
</NTag>
<NTag v-if="item.isPublic" size="small" type="success" :bordered="false" style="margin-left: 5px">
公开
</NTag>
<NDivider vertical />
<NText depth="3" style="font-size: small">
<NTooltip>
<template #trigger>
<NTime :time="item.sendAt" :to="Date.now()" type="relative" />
</template>
<NTime :time="item.sendAt" />
</NTooltip>
</NText>
</NSpace>
</template>
<template #footer>
<NSpace>
<NButton v-if="!item.isReaded" size="small" @click="read(item, true)" type="success">
设为已读
</NButton>
<NButton size="small" @click="favorite(item, !item.isFavorite)">
<NButton @click="$router.push({ name: 'question-display' })" type="primary">
打开展示页
</NButton>
<NDivider vertical />
<NCheckbox v-model:checked="useQB.onlyFavorite"> 只显示收藏 </NCheckbox>
<NCheckbox v-model:checked="useQB.onlyPublic"> 只显示公开 </NCheckbox>
<NCheckbox v-model:checked="useQB.onlyUnread"> 只显示未读 </NCheckbox>
<NDivider style="margin: 10px 0 10px 0" />
<QuestionItem :questions="useQB.recieveQuestionsFiltered">
<template #footer="{ item }">
<NSpace>
<NTooltip>
<template #trigger>
<NButton
@click="useQB.setCurrentQuestion(item)"
size="small"
:type="useQB.displayQuestion?.id == item.id ? 'primary' : 'default'"
>
<template #icon>
<NIcon
:component="item.isFavorite ? Heart : HeartOutline"
:color="item.isFavorite ? '#dd484f' : ''"
/>
<NIcon :component="ArrowCircleRight12Filled" />
</template>
收藏
</NButton>
<NTooltip>
<template #trigger>
<NButton size="small"> 举报 </NButton>
</template>
暂时还没写
</NTooltip>
<NButton size="small" @click="blacklist(item)"> 拉黑 </NButton>
</NSpace>
</template>
<template #header-extra>
<NButton
@click="onOpenModal(item)"
:type="item.isReaded ? 'default' : 'primary'"
:secondary="item.isReaded"
>
{{ item.answer ? '查看回复' : '回复' }}
</NButton>
</template>
<template v-if="item.question?.image">
<NImage v-if="item.question?.image" :src="item.question.image" height="100" lazy />
<br />
</template>
<NText style="">
{{ item.question?.message }}
</NText>
<NButton text @click="onOpenModal(item)" style="max-width: 100%; word-wrap: break-word"> </NButton>
</NCard>
</NListItem>
</NList>
</template>
设为当前展示的提问
</NTooltip>
<NButton v-if="!item.isReaded" size="small" @click="useQB.read(item, true)" type="success">
设为已读
</NButton>
<NButton v-else size="small" @click="useQB.read(item, false)" type="warning">重设为未读</NButton>
<NButton size="small" @click="useQB.favorite(item, !item.isFavorite)">
<template #icon>
<NIcon :component="item.isFavorite ? Heart : HeartOutline" :color="item.isFavorite ? '#dd484f' : ''" />
</template>
收藏
</NButton>
<!-- <NTooltip>
<template #trigger>
<NButton size="small"> 举报 </NButton>
</template>
暂时还没写
</NTooltip> -->
<NButton size="small" @click="useQB.blacklist(item)"> 拉黑 </NButton>
</NSpace>
</template>
<template #header-extra="{ item }">
<NButton @click="onOpenModal(item)" :type="item.isReaded ? 'default' : 'info'" :secondary="item.isReaded">
{{ item.answer ? '查看回复' : '回复' }}
</NButton>
</template>
</QuestionItem>
</NTabPane>
<NTabPane tab="我发送的" name="1">
<NTabPane ref="parentRef" tab="我发送的" name="1">
<NList>
<NListItem v-for="item in sendQuestions" :key="item.id">
<NListItem v-for="item in useQB.sendQuestions" :key="item.id">
<NCard hoverable size="small">
<template #header>
<NSpace :size="0" align="center">
@@ -409,7 +239,7 @@ onMounted(() => {
</NList>
</NTabPane>
<NTabPane v-if="accountInfo" tab="设置" name="2">
<NSpin :show="isLoading">
<NSpin :show="useQB.isLoading">
<NCheckbox
v-model:checked="accountInfo.settings.questionBox.allowUnregistedUser"
@update:checked="saveSettings"
@@ -437,15 +267,20 @@ onMounted(() => {
show-count
clearable
/>
<NSpin :show="isChangingPublic">
<NCheckbox @update:checked="(v) => setPublic(v)" :default-checked="currentQuestion?.isPublic">
<NSpin :show="useQB.isChangingPublic">
<NCheckbox @update:checked="(v) => useQB.setPublic(v)" :default-checked="useQB.currentQuestion?.isPublic">
公开可见
</NCheckbox>
</NSpin>
</NSpace>
<NDivider style="margin: 10px 0 10px 0" />
<NButton :loading="isRepling" @click="reply" type="primary" :secondary="currentQuestion?.answer ? true : false">
{{ currentQuestion?.answer ? '修改' : '发送' }}
<NButton
:loading="useQB.isRepling"
@click="useQB.reply(useQB.currentQuestion?.id ?? -1, replyMessage)"
type="primary"
:secondary="useQB.currentQuestion?.answer ? true : false"
>
{{ useQB.currentQuestion?.answer ? '修改' : '发送' }}
</NButton>
</NModal>
<NModal v-model:show="shareModalVisiable" preset="card" title="分享" style="width: 600px">

View File

@@ -0,0 +1,127 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, watch } from 'vue'
import { QAInfo, QuestionDisplayAlign, Setting_QuestionDisplay } from '@/api/api-models'
import { useDebounceFn, useStorage } from '@vueuse/core'
const props = defineProps<{
question: QAInfo | undefined
setting: Setting_QuestionDisplay
showGreenBorder?: boolean
css?: string
}>()
let styleElement: HTMLStyleElement
const cssDebounce = useDebounceFn(() => {
if (styleElement) {
styleElement.textContent = props.css ?? ''
console.log('已更新CSS')
}
}, 1000)
watch(() => props.css, cssDebounce)
const align = computed(() => {
switch (props.setting.align) {
case QuestionDisplayAlign.Left:
return 'left'
case QuestionDisplayAlign.Right:
return 'right'
case QuestionDisplayAlign.Center:
return 'center'
}
return 'left'
})
onMounted(() => {
// 创建<style>元素并添加到<head>中
styleElement = document.createElement('style')
// 可能需要对 userStyleString 做安全处理以避免XSS攻击
styleElement.textContent = props.css ?? ''
document.head.appendChild(styleElement)
})
onUnmounted(() => {
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement)
}
})
</script>
<template>
<div
class="question-display-root"
:style="{
backgroundColor: '#' + setting.borderColor,
borderColor: setting.borderColor ? '#' + setting.borderColor : undefined,
borderWidth: setting.borderWidth ? setting.borderWidth + 'px' : undefined,
borderTopWidth: 0,
}"
:display="question ? 1 : 0"
>
<div
v-if="setting.showUserName"
class="question-display-user-name"
:style="{
color: '#' + setting.nameFontColor,
fontSize: setting.nameFontSize + 'px',
fontWeight: setting.nameFontWeight ? setting.nameFontWeight : undefined,
fontFamily: setting.nameFont,
}"
>
{{ question?.sender?.name ?? '匿名用户' }}
</div>
<div
class="question-display-content"
:style="{
color: '#' + setting.fontColor,
backgroundColor: '#' + setting.backgroundColor,
fontSize: setting.fontSize + 'px',
fontWeight: setting.fontWeight ? setting.fontWeight : undefined,
textAlign: align,
fontFamily: setting.font,
}"
>
<div class="question-display-text">
{{ question?.question.message }}
</div>
<img
class="question-display-image"
v-if="setting.showImage && question?.question.image"
:src="question?.question.image"
/>
</div>
</div>
</template>
<style scoped>
.question-display-root {
height: 100%;
width: 100%;
border-radius: 16px;
border: 2 solid rgb(255, 255, 255);
display: flex;
flex-direction: column;
transition: all 0.3s ease;
border-style: solid;
box-sizing: border-box;
}
.question-display-content {
border-radius: 10px;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-evenly;
padding: 24px;
}
.question-display-user-name {
text-align: center;
margin: 5px;
}
.question-display-text {
min-height: 50px;
transition: all 0.3s ease;
}
.question-display-image {
max-width: 40%;
max-height: 40%;
margin: 0 auto;
}
</style>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import { QAInfo, Setting_QuestionDisplay } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query'
import { QUESTION_API_URL } from '@/data/constants'
import { useRouteQuery } from '@vueuse/router'
import { onMounted, ref } from 'vue'
import QuestionDisplayCard from '../manage/QuestionDisplayCard.vue'
const hash = ref('')
const token = useRouteQuery('token')
const question = ref<QAInfo>()
const setting = ref<Setting_QuestionDisplay>({} as Setting_QuestionDisplay)
async function checkIfChanged() {
try {
const data = await QueryGetAPI<string>(QUESTION_API_URL + 'get-hash', {
token: token.value,
})
if (data.code == 200) {
if (data.data != hash.value) {
getQuestionAndSetting()
}
hash.value = data.data
}
} catch (err) {
console.log(err)
}
}
async function getQuestionAndSetting() {
try {
const data = await QueryGetAPI<{
question: QAInfo
setting: Setting_QuestionDisplay
}>(QUESTION_API_URL + 'get-current-and-settings', {
token: token.value,
})
if (data.code == 200) {
question.value = data.data.question
setting.value = data.data.setting
}
} catch (err) {
console.log(err)
}
}
onMounted(() => {
setInterval(() => {
checkIfChanged()
}, 1000)
})
</script>
<template>
<QuestionDisplayCard :question="question" :setting="setting" />
</template>

View File

@@ -0,0 +1,462 @@
<!-- eslint-disable @typescript-eslint/no-explicit-any -->
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import {
NButton,
NCard,
NCheckbox,
NColorPicker,
NDivider,
NEmpty,
NFlex,
NIcon,
NInput,
NInputGroup,
NInputGroupLabel,
NInputNumber,
NRadioButton,
NRadioGroup,
NSelect,
NSpin,
NTooltip,
NDrawer,
useMessage,
NScrollbar,
NDrawerContent,
NModal,
} from 'naive-ui'
import { QAInfo, QuestionDisplayAlign, Setting_QuestionDisplay } from '@/api/api-models'
import QuestionDisplayCard from '@/views/manage/QuestionDisplayCard.vue'
import { useAccount } from '@/api/account'
import { QUESTION_API_URL } from '@/data/constants'
import {
ArrowCircleLeft12Filled,
ArrowCircleRight12Filled,
Info24Filled,
TextAlignCenter16Filled,
TextAlignLeft16Filled,
TextAlignRight16Filled,
} from '@vicons/fluent'
import { useDebounceFn, useElementSize, useStorage } from '@vueuse/core'
import QuestionItems from '@/components/QuestionItems.vue'
import QuestionItem from '@/components/QuestionItem.vue'
import { useQuestionBox } from '@/store/useQuestionBox'
import { Heart, HeartOutline } from '@vicons/ionicons5'
const message = useMessage()
const accountInfo = useAccount()
const defaultSettings = {} as Setting_QuestionDisplay
const useQB = useQuestionBox()
const showSettingDrawer = ref(false)
const showGreenBorder = ref(false)
const showOBSModal = ref(false)
const isLoading = ref(false)
const cardRef = ref<HTMLElement>()
const cardSize = useElementSize(cardRef)
const savedCardSize = useStorage<{ width: number; height: number }>('Settings.QuestionDisplay.CardSize', {
width: 400,
height: 400,
})
const customCss = useStorage<string>('Settings.QuestionDisplay.CustomCss', '')
const debouncedSize = useDebounceFn(() => {
savedCardSize.value = { width: cardSize.width.value, height: cardSize.height.value }
}, 500)
watch([cardSize.width, cardSize.height], () => {
if (cardSize.width.value > 300 && cardSize.height.value > 300) {
debouncedSize()
}
})
const setting = computed({
get: () => {
if (accountInfo.value) {
return accountInfo.value.settings.questionDisplay
}
return defaultSettings
},
set: (value) => {
if (accountInfo.value) {
accountInfo.value.settings.questionDisplay = value
}
},
})
async function updateSettings() {
if (accountInfo.value) {
isLoading.value = true
await QueryPostAPI(QUESTION_API_URL + 'update-setting', setting.value)
.then((data) => {
if (data.code == 200) {
//message.success('已保存')
} else {
message.error('保存失败: ' + data.message)
}
})
.catch((err) => {
message.error('保存失败')
})
.finally(() => {
isLoading.value = false
})
} else {
message.success('完成')
}
}
const fontsOptions = useStorage<{ label: string; value: string }[]>('Settings.Fonts', [])
async function loadFonts() {
if ('queryLocalFonts' in window) {
//@ts-expect-error 不知道为啥不存在
const status = await navigator.permissions.query({ name: 'local-fonts' })
if (status.state === 'granted') {
console.log('Permission was granted 👍')
} else if (status.state === 'prompt') {
console.log('Permission will be requested')
} else {
console.log('Permission was denied 👎')
message.error('你没有授予本地字体权限, 无法读取本地字体')
}
//@ts-expect-error 不知道为啥不存在
const fonts = await window.queryLocalFonts()
fontsOptions.value = fonts.map((f: any) => {
return { label: f.fullName, value: f.fullName }
})
message.success('已获取字体列表, 共' + fontsOptions.value.length + '个字体')
} else {
message.error('你的浏览器不支持获取字体列表')
}
}
onMounted(() => {
useQB.GetRecieveQAInfo()
useQB.displayQuestion = useQB.recieveQuestions.find(
(s) => s.id == accountInfo.value?.settings.questionDisplay.currentQuestion,
)
})
</script>
<template>
<NFlex :wrap="false" style="margin: 20px">
<NFlex style="height: calc(100vh - 40px)" :wrap="false" vertical>
<NCard size="small" title="内容设置">
<NFlex align="center">
<NButton @click="useQB.GetRecieveQAInfo" type="primary"> 刷新 </NButton>
<NCheckbox v-model:checked="useQB.onlyFavorite"> 只显示收藏 </NCheckbox>
<NCheckbox v-model:checked="useQB.onlyUnread"> 只显示未读 </NCheckbox>
</NFlex>
</NCard>
<template v-if="useQB.displayQuestion">
<NDivider style="margin: 10px 0 10px 0" />
<NCard size="small" title="当前展示" embedded>
<QuestionItem :item="useQB.displayQuestion" />
</NCard>
<NDivider style="margin: 10px 0 10px 0" />
</template>
<NScrollbar :style="{ flex: 1, minWidth: '400px', overflow: 'auto' }">
<QuestionItems :questions="useQB.recieveQuestionsFiltered">
<template #footer="{ item }">
<NFlex>
<NTooltip>
<template #trigger>
<NButton
@click="useQB.setCurrentQuestion(item)"
size="small"
:type="item.id != useQB.displayQuestion?.id ? 'default' : 'primary'"
:secondary="item.id != useQB.displayQuestion?.id"
>
<template #icon>
<NIcon
:component="
item.id != useQB.displayQuestion?.id ? ArrowCircleRight12Filled : ArrowCircleLeft12Filled
"
/>
</template>
</NButton>
</template>
{{ item.id == useQB.displayQuestion?.id ? '取消' : '' }}设为当前展示的提问
</NTooltip>
<NButton v-if="!item.isReaded" size="small" @click="useQB.read(item, true)" type="success" secondary>
设为已读
</NButton>
<NButton v-else size="small" @click="useQB.read(item, false)" type="warning" secondary
>重设为未读</NButton
>
<NButton size="small" @click="useQB.favorite(item, !item.isFavorite)">
<template #icon>
<NIcon
:component="item.isFavorite ? Heart : HeartOutline"
:color="item.isFavorite ? '#dd484f' : ''"
/>
</template>
收藏
</NButton>
</NFlex>
</template>
</QuestionItems>
</NScrollbar>
</NFlex>
<NCard style="min-height: 600px">
<NFlex vertical :size="0" style="height: 100%">
<NFlex align="center">
<NButton @click="showSettingDrawer = true" type="primary"> 打开设置 </NButton>
<NButton @click="showOBSModal = true" type="primary" secondary> 预览OBS组件 </NButton>
<NCheckbox v-model:checked="showGreenBorder">
显示边框
<NTooltip>
<template #trigger>
<NIcon :component="Info24Filled" />
</template>
用于使用 OBS 直接捕获浏览器窗口时消除背景
</NTooltip>
</NCheckbox>
<template v-if="useQB.displayQuestion">
<NDivider vertical />
<NButton @click="useQB.read(useQB.displayQuestion, true)" type="success"> 将当前问题设为已读 </NButton>
</template>
</NFlex>
<NDivider style="margin-top: 10px">
{{ cardSize.width.value.toFixed(0) }} x {{ cardSize.height.value.toFixed(0) }}
<NTooltip>
<template #trigger>
<NIcon :component="Info24Filled" />
</template>
宽x高, 展示框右下角可以调整尺寸. 如果用的 OBS 组件的话尺寸不会同步到obs, 得你自己调
</NTooltip>
</NDivider>
<NFlex justify="center" align="center" style="height: 100%">
<div
ref="cardRef"
class="resize-box"
:style="{
border: showGreenBorder ? '24px solid green' : '',
background: showGreenBorder ? 'green' : '',
padding: '10px',
width: savedCardSize.width + 'px',
height: savedCardSize.height + 'px',
}"
>
<QuestionDisplayCard :question="useQB.displayQuestion" :setting="setting" :css="customCss" />
</div>
</NFlex>
</NFlex>
</NCard>
</NFlex>
<NDrawer v-model:show="showSettingDrawer" :title="`设置`" :width="400" placement="left">
<NDrawerContent title="设置" closable>
<NFlex>
<NTooltip>
<template #trigger>
<NRadioGroup v-model:value="setting.align" @update:value="updateSettings">
<NRadioButton :value="QuestionDisplayAlign.Left">
<NIcon :component="TextAlignLeft16Filled" />
</NRadioButton>
<NRadioButton :value="QuestionDisplayAlign.Center">
<NIcon :component="TextAlignCenter16Filled" />
</NRadioButton>
<NRadioButton :value="QuestionDisplayAlign.Right">
<NIcon :component="TextAlignRight16Filled" />
</NRadioButton>
</NRadioGroup>
</template>
文字对齐
</NTooltip>
<NFlex>
<NCheckbox v-model:checked="setting.showImage" @update:checked="updateSettings"> 显示图片 </NCheckbox>
<NCheckbox v-model:checked="setting.showUserName" @update:checked="updateSettings">
显示投稿用户名
</NCheckbox>
</NFlex>
<NCard size="small" title="内容设置">
<NFlex>
<NInputGroup style="max-width: 230px">
<NInputGroupLabel>字体大小</NInputGroupLabel>
<NInputNumber v-model:value="setting.fontSize" :min="1" :max="1000" />
<NButton @click="updateSettings" type="info">保存</NButton>
</NInputGroup>
<NInputGroup style="max-width: 230px">
<NInputGroupLabel>边框宽度</NInputGroupLabel>
<NInputNumber v-model:value="setting.borderWidth" :min="1" :max="1000" />
<NButton @click="updateSettings" type="info">保存</NButton>
</NInputGroup>
<NInputGroup style="max-width: 300px">
<NInputGroupLabel>字重</NInputGroupLabel>
<NInputNumber
v-model:value="setting.fontWeight"
:min="1"
:max="10000"
step="100"
placeholder="只有部分字体支持"
/>
<NButton @click="updateSettings" type="info">保存</NButton>
</NInputGroup>
<NFlex>
<NSelect
v-model:value="setting.font"
:options="fontsOptions"
filterable
@update:value="updateSettings"
placeholder="选择内容字体"
/>
<NTooltip>
<template #trigger>
<NButton @click="loadFonts" type="info" secondary> 获取字体列表 </NButton>
</template>
如果选用了本地字体且使用了obs组件的话请确保运行obs的电脑上也有这个字体
</NTooltip>
</NFlex>
<NFlex>
字体颜色
<NColorPicker
:value="setting.fontColor ? '#' + setting.fontColor : undefined"
show-preview
:modes="['hex']"
:actions="['clear', 'confirm']"
:show-alpha="false"
@update:value="
(c: string | null | undefined) => {
setting.fontColor = c?.replace('#', '')
}
"
@confirm="updateSettings"
/>
</NFlex>
<NFlex>
背景颜色
<NColorPicker
:value="setting.backgroundColor ? '#' + setting.backgroundColor : undefined"
show-preview
:modes="['hex']"
:actions="['clear', 'confirm']"
:show-alpha="false"
@update:value="
(c: string | null | undefined) => {
setting.backgroundColor = c?.replace('#', '')
}
"
@confirm="updateSettings"
/>
</NFlex>
<NFlex>
边框颜色
<NColorPicker
:value="setting.borderColor ? '#' + setting.borderColor : undefined"
show-preview
:modes="['hex']"
:actions="['clear', 'confirm']"
:show-alpha="false"
@update:value="
(c: string | null | undefined) => {
setting.borderColor = c?.replace('#', '')
}
"
@confirm="updateSettings"
/>
</NFlex>
</NFlex>
</NCard>
<NCard size="small" title="用户名设置">
<NFlex>
<NInputGroup style="max-width: 230px">
<NInputGroupLabel>字体大小</NInputGroupLabel>
<NInputNumber v-model:value="setting.nameFontSize" :min="1" :max="1000" />
<NButton @click="updateSettings" type="info">保存</NButton>
</NInputGroup>
<NInputGroup style="max-width: 300px">
<NInputGroupLabel>字重</NInputGroupLabel>
<NInputNumber
v-model:value="setting.nameFontWeight"
:min="1"
:max="10000"
step="100"
placeholder="只有部分字体支持"
/>
<NButton @click="updateSettings" type="info">保存</NButton>
</NInputGroup>
<NFlex>
<NSelect
v-model:value="setting.nameFont"
:options="fontsOptions"
filterable
@update:value="updateSettings"
placeholder="选择用户名字体"
/>
<NTooltip>
<template #trigger>
<NButton @click="loadFonts" type="info" secondary> 获取字体列表 </NButton>
</template>
如果选用了本地字体且使用了obs组件的话请确保运行obs的电脑上也有这个字体
</NTooltip>
</NFlex>
<NFlex>
字体颜色
<NColorPicker
:value="setting.nameFontColor ? '#' + setting.nameFontColor : undefined"
show-preview
:modes="['hex']"
:actions="['clear', 'confirm']"
:show-alpha="false"
@update:value="
(c: string | null | undefined) => {
setting.nameFontColor = c?.replace('#', '')
}
"
@confirm="updateSettings"
/>
</NFlex>
</NFlex>
</NCard>
<NDivider style="margin: 10px 0 10px 0">
自定义样式 (CSS)
<NTooltip>
<template #trigger>
<NIcon :component="Info24Filled" />
</template>
只会在当前页面生效, 要想在OBS中也生效的话需要自己粘贴到创建浏览器源时的css栏中
</NTooltip>
</NDivider>
<NInput type="textarea" v-model:value="customCss" placeholder="写上css" style="max-height: 500px" />
</NFlex>
</NDrawerContent>
</NDrawer>
<NModal
preset="card"
v-model:show="showOBSModal"
closable
style="max-width: 90vw"
title="OBS组件"
content-style="display: flex; align-items: center; justify-content: center; flex-direction: column"
>
<div
:style="{
width: savedCardSize.width + 'px',
height: savedCardSize.height + 'px',
}"
>
<QuestionDisplayCard :question="useQB.displayQuestion" :setting="setting" />
</div>
<NInput readonly :value="'https://vtsuru.live/obs/question-display?token=' + accountInfo?.token" />
</NModal>
</template>
<style>
.resize-box {
display: flex;
justify-content: center;
overflow-y: visible;
min-width: 300px;
min-height: 100px;
resize: both;
overflow: auto;
overflow-y: hidden;
padding: 10px;
}
.n-drawer-mask {
background-color: rgba(0, 0, 0, 0);
}
</style>