mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
553 lines
16 KiB
Vue
553 lines
16 KiB
Vue
<script setup lang="ts">
|
||
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 router from '@/router'
|
||
import { Heart, HeartOutline } from '@vicons/ionicons5'
|
||
import { saveAs } from 'file-saver'
|
||
import html2canvas from 'html2canvas'
|
||
import { List } from 'linqts'
|
||
import {
|
||
NButton,
|
||
NCard,
|
||
NCheckbox,
|
||
NDivider,
|
||
NIcon,
|
||
NImage,
|
||
NInput,
|
||
NInputGroup,
|
||
NList,
|
||
NListItem,
|
||
NModal,
|
||
NSpace,
|
||
NSpin,
|
||
NSwitch,
|
||
NTabPane,
|
||
NTabs,
|
||
NTag,
|
||
NText,
|
||
NTime,
|
||
NTooltip,
|
||
useMessage,
|
||
} from 'naive-ui'
|
||
import QrcodeVue from 'qrcode.vue'
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
|
||
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 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 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()
|
||
isRevieveGetted = true
|
||
} else if (value == '1' && !isSendGetted) {
|
||
await GetSendQAInfo()
|
||
isSendGetted = true
|
||
}
|
||
}
|
||
function onOpenModal(question: QAInfo) {
|
||
currentQuestion.value = question
|
||
replyMessage.value = question.answer?.message
|
||
replyModalVisiable.value = true
|
||
}
|
||
function refresh() {
|
||
isSendGetted = false
|
||
isRevieveGetted = false
|
||
onTabChange(selectedTabItem.value)
|
||
}
|
||
function saveShareImage() {
|
||
html2canvas(shareCardRef.value, {
|
||
width: shareCardRef.value.clientWidth, //dom 原始宽度
|
||
height: shareCardRef.value.clientHeight,
|
||
backgroundColor: null,
|
||
scrollY: 0, // html2canvas默认绘制视图内的页面,需要把scrollY,scrollX设置为0
|
||
scrollX: 0,
|
||
useCORS: true, //支持跨域,但好像没什么用
|
||
allowTaint: true, //允许跨域(默认false)
|
||
scale: 1,
|
||
}).then((canvas) => {
|
||
// 生成的ba64图片
|
||
canvas.toBlob(
|
||
(data) => {
|
||
saveAs(data, `vtsuru-提问箱-${accountInfo.value?.name}.png`)
|
||
},
|
||
'image/png',
|
||
1,
|
||
)
|
||
})
|
||
}
|
||
function saveQRCode() {
|
||
downloadImage(`https://api.qrserver.com/v1/create-qr-code/?data=${shareUrl.value}`, 'vtsuru-提问箱二维码.png')
|
||
}
|
||
async function saveSettings() {
|
||
try {
|
||
isLoading.value = true
|
||
const data = await SaveAccountSettings()
|
||
if (data.code == 200) {
|
||
message.success('保存成功')
|
||
} else {
|
||
message.error('保存失败: ' + data.message)
|
||
}
|
||
} catch (error) {
|
||
message.error('保存失败:' + error)
|
||
}
|
||
isLoading.value = false
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (selectedTabItem.value == '0') {
|
||
GetRecieveQAInfo()
|
||
} else {
|
||
GetSendQAInfo()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<NSpace>
|
||
<NButton type="primary" @click="refresh"> 刷新 </NButton>
|
||
<NButton type="primary" @click="shareModalVisiable = true" secondary> 分享 </NButton>
|
||
</NSpace>
|
||
<NDivider style="margin: 10px 0 10px 0" />
|
||
<NSpin v-if="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)">
|
||
<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="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>
|
||
</NTabPane>
|
||
<NTabPane tab="我发送的" name="1">
|
||
<NList>
|
||
<NListItem v-for="item in sendQuestions" :key="item.id">
|
||
<NCard hoverable size="small">
|
||
<template #header>
|
||
<NSpace :size="0" align="center">
|
||
发给
|
||
<NDivider vertical />
|
||
<NButton text type="info" @click="router.push('/user/' + item.target.id)">
|
||
{{ item.target.name }}
|
||
</NButton>
|
||
<NDivider vertical />
|
||
<NText depth="3" style="font-size: small">
|
||
<NTooltip>
|
||
<template #trigger>
|
||
<NTime :time="item.sendAt" :to="Date.now()" type="relative" />
|
||
</template>
|
||
<NTime />
|
||
</NTooltip>
|
||
</NText>
|
||
</NSpace>
|
||
</template>
|
||
<template v-if="item.answer" #footer>
|
||
<NDivider style="margin: 0" />
|
||
<NCard :bordered="false" size="small">
|
||
<template #header>
|
||
<NSpace align="center">
|
||
<NText depth="3"> 回复 </NText>
|
||
</NSpace>
|
||
</template>
|
||
{{ item.answer.message }}
|
||
</NCard>
|
||
</template>
|
||
<template v-if="item.question?.image">
|
||
<NImage :src="item.question.image" height="100" lazy />
|
||
<br />
|
||
</template>
|
||
{{ item.question?.message }}
|
||
</NCard>
|
||
</NListItem>
|
||
</NList>
|
||
</NTabPane>
|
||
<NTabPane v-if="accountInfo" tab="设置" name="2">
|
||
<NSpin :show="isLoading">
|
||
<NCheckbox
|
||
v-model:checked="accountInfo.settings.questionBox.allowUnregistedUser"
|
||
@update:checked="saveSettings"
|
||
>
|
||
允许未注册用户进行提问
|
||
</NCheckbox>
|
||
<NDivider> 通知 </NDivider>
|
||
<NCheckbox v-model:checked="accountInfo.settings.sendEmail.recieveQA" @update:checked="saveSettings">
|
||
收到新提问时发送邮件
|
||
</NCheckbox>
|
||
<NCheckbox v-model:checked="accountInfo.settings.sendEmail.recieveQAReply" @update:checked="saveSettings">
|
||
提问后收到回复时发送邮件
|
||
</NCheckbox>
|
||
</NSpin>
|
||
</NTabPane>
|
||
</NTabs>
|
||
<NModal preset="card" v-model:show="replyModalVisiable" style="max-width: 90vw; width: 500px">
|
||
<template #header> 回复 </template>
|
||
<NSpace vertical>
|
||
<NInput
|
||
placeholder="请输入回复"
|
||
type="textarea"
|
||
v-model:value="replyMessage"
|
||
maxlength="1000"
|
||
show-count
|
||
clearable
|
||
/>
|
||
<NSpin :show="isChangingPublic">
|
||
<NCheckbox @update:checked="(v) => setPublic(v)" :default-checked="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>
|
||
</NModal>
|
||
<NModal v-model:show="shareModalVisiable" preset="card" title="分享" style="width: 600px">
|
||
<div ref="shareCardRef" class="share-card container">
|
||
<NText class="share-card title"> 向我提问 </NText>
|
||
<NText class="share-card type"> 提 问 箱 </NText>
|
||
<NText class="share-card name">
|
||
{{ accountInfo?.name }}
|
||
</NText>
|
||
<NDivider class="share-card divider-1" />
|
||
<NText class="share-card site"> VTSURU.LIVE </NText>
|
||
<QrcodeVue
|
||
class="share-card qrcode"
|
||
:value="shareUrl"
|
||
level="Q"
|
||
:size="100"
|
||
background="#00000000"
|
||
foreground="#ffffff"
|
||
:margin="1"
|
||
/>
|
||
</div>
|
||
<NDivider style="margin: 10px" />
|
||
<NInputGroup>
|
||
<NInput :value="shareUrl" />
|
||
<NButton secondary @click="copyToClipboard(shareUrl)"> 复制 </NButton>
|
||
</NInputGroup>
|
||
<br /><br />
|
||
<NSpace justify="center">
|
||
<NButton type="primary" @click="saveShareImage"> 保存卡片 </NButton>
|
||
<NButton type="primary" @click="saveQRCode"> 保存二维码 </NButton>
|
||
</NSpace>
|
||
</NModal>
|
||
</template>
|
||
|
||
<style>
|
||
.n-list {
|
||
background-color: transparent;
|
||
}
|
||
|
||
.share-card.container {
|
||
position: relative;
|
||
height: 200px;
|
||
width: 550px;
|
||
border-radius: 10px;
|
||
background: linear-gradient(to right, #66bea3, #9179be);
|
||
}
|
||
.share-card.qrcode {
|
||
position: absolute;
|
||
right: 10px;
|
||
top: 10px;
|
||
border-radius: 4px;
|
||
background: linear-gradient(to right, #3d554e, #503e74);
|
||
}
|
||
.share-card.title {
|
||
position: absolute;
|
||
font-size: 80px;
|
||
bottom: -17px;
|
||
left: 5px;
|
||
color: #e6e6e662;
|
||
font-weight: 550;
|
||
}
|
||
/* .share-card.type {
|
||
position: absolute;
|
||
font-size: 20px;
|
||
transform:rotate(90deg);
|
||
left: 300px;
|
||
bottom: 20px;
|
||
color: #e6e6e6b6;
|
||
} */
|
||
.share-card.type {
|
||
position: absolute;
|
||
font-size: 20px;
|
||
left: 332px;
|
||
bottom: 55px;
|
||
font-weight: 550;
|
||
color: #e6e6e662;
|
||
}
|
||
.share-card.name {
|
||
position: absolute;
|
||
font-size: 30px;
|
||
left: 10px;
|
||
bottom: 95px;
|
||
max-width: 300px;
|
||
word-wrap: break-word;
|
||
line-height: 1.3;
|
||
color: #e6e6e6;
|
||
font-weight: 550;
|
||
}
|
||
.share-card.site {
|
||
position: absolute;
|
||
font-size: 12px;
|
||
right: 20px;
|
||
top: 110px;
|
||
color: #e6e6e6a4;
|
||
font-weight: 550;
|
||
}
|
||
.share-card.divider-1 {
|
||
position: absolute;
|
||
width: 400px;
|
||
left: 5px;
|
||
bottom: 66px;
|
||
background-color: #c0c0c057;
|
||
}
|
||
</style>
|