mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
add question display page
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
51
src/components/QuestionItem.vue
Normal file
51
src/components/QuestionItem.vue
Normal 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>
|
||||
24
src/components/QuestionItems.vue
Normal file
24
src/components/QuestionItems.vue
Normal 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>
|
||||
@@ -9,9 +9,7 @@ declare type MittType<T = any> = {
|
||||
onMusicRequestPlayerEnded: {
|
||||
music: Music
|
||||
}
|
||||
onMusicRequestPlayNextWaitingMusic: {
|
||||
|
||||
}
|
||||
onMusicRequestPlayNextWaitingMusic: never
|
||||
};
|
||||
// 类型
|
||||
const emitter: Emitter<MittType> = mitt<MittType>()
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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
10
src/router/singlePage.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export default [
|
||||
{
|
||||
path: '/question-display',
|
||||
name: 'question-display',
|
||||
component: () => import('@/views/single/QuestionDisplay.vue'),
|
||||
meta: {
|
||||
title: '棉花糖展示页',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -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
245
src/store/useQuestionBox.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
127
src/views/manage/QuestionDisplayCard.vue
Normal file
127
src/views/manage/QuestionDisplayCard.vue
Normal 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>
|
||||
56
src/views/obs/QuestionDisplayOBS.vue
Normal file
56
src/views/obs/QuestionDisplayOBS.vue
Normal 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>
|
||||
462
src/views/single/QuestionDisplay.vue
Normal file
462
src/views/single/QuestionDisplay.vue
Normal 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>
|
||||
Reference in New Issue
Block a user