mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
particularly complete forum function, add point order export and user delete
This commit is contained in:
@@ -247,7 +247,7 @@ onUnmounted(() => {
|
||||
<NImage v-if="item.question.image" :src="item.question.image" height="100" lazy />
|
||||
</NCard>
|
||||
<template v-if="item.answer" #footer>
|
||||
<NSpace align="center" :size="6">
|
||||
<NSpace align="center" :size="6" :wrap="false">
|
||||
<NAvatar :src="biliInfo.face + '@64w'" circle :size="45" :img-props="{ referrerpolicy: 'no-referrer' }" />
|
||||
<NDivider vertical />
|
||||
<NText style="font-size: 16px">
|
||||
|
||||
104
src/views/view/forumViews/ForumCommentItem.vue
Normal file
104
src/views/view/forumViews/ForumCommentItem.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import { useAccount } from '@/api/account'
|
||||
import { ForumCommentModel, ForumTopicModel } from '@/api/models/forum'
|
||||
import { VTSURU_API_URL } from '@/data/constants'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import { ArrowReply16Filled } from '@vicons/fluent'
|
||||
import { Heart, HeartOutline } from '@vicons/ionicons5'
|
||||
import { NAvatar, NButton, NCard, NDivider, NFlex, NIcon, NText, NTime, NTooltip } from 'naive-ui'
|
||||
import ForumReplyItem from './ForumReplyItem.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: ForumCommentModel
|
||||
topic: ForumTopicModel
|
||||
}>()
|
||||
|
||||
const useForum = useForumStore()
|
||||
const accountInfo = useAccount()
|
||||
|
||||
const canOprate = computed(() => {
|
||||
return !props.topic.isLocked && accountInfo.value.id > 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex>
|
||||
<NAvatar
|
||||
:src="VTSURU_API_URL + 'user-face/' + item.user.id + '?size=64'"
|
||||
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||
/>
|
||||
<NFlex vertical style="flex: 1" :size="2">
|
||||
<NFlex>
|
||||
<NText>
|
||||
{{ item.user.name }}
|
||||
</NText>
|
||||
<NText depth="3">
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NTime :time="item.sendAt" type="relative" />
|
||||
</template>
|
||||
<NTime :time="item.sendAt" />
|
||||
</NTooltip>
|
||||
</NText>
|
||||
</NFlex>
|
||||
<div class="editor-content-view" v-html="item.content"></div>
|
||||
<NDivider style="margin: 0" />
|
||||
<NFlex>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
@click="
|
||||
useForum.LikeComment(item.id, !item.isLiked).then((success) => {
|
||||
if (success) {
|
||||
item.isLiked = !item.isLiked
|
||||
item.likeCount += item.isLiked ? 1 : -1
|
||||
}
|
||||
})
|
||||
"
|
||||
text
|
||||
:loading="useForum.isLikeLoading"
|
||||
:disabled="!canOprate"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="item.isLiked ? Heart : HeartOutline" :color="item.isLiked ? '#dd484f' : ''" />
|
||||
</template>
|
||||
{{ item.likeCount }}
|
||||
</NButton>
|
||||
</template>
|
||||
点赞
|
||||
</NTooltip>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
@click="useForum.SetReplyingComment(item)"
|
||||
text
|
||||
:disabled="!canOprate"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="ArrowReply16Filled" />
|
||||
</template>
|
||||
{{ item.replies.length }}
|
||||
</NButton>
|
||||
</template>
|
||||
回复
|
||||
</NTooltip>
|
||||
</NFlex>
|
||||
<NCard v-if="item.replies.length > 0" size="small">
|
||||
<NFlex vertical>
|
||||
<ForumReplyItem
|
||||
v-for="reply in item.replies"
|
||||
:key="reply.id"
|
||||
:item="reply"
|
||||
:comment="item"
|
||||
:topic="topic"
|
||||
showReplyButton
|
||||
:reply-to="reply.replyTo ? item.replies.find((r) => r.id === reply.replyTo) : undefined"
|
||||
/>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
101
src/views/view/forumViews/ForumPreviewItem.vue
Normal file
101
src/views/view/forumViews/ForumPreviewItem.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { ForumModel, ForumTopicBaseModel } from '@/api/models/forum'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import { ArrowReply24Filled, Chat24Regular, MoreVertical24Filled, Star24Filled } from '@vicons/fluent'
|
||||
import { NButton, NDropdown, NFlex, NIcon, NTag, NText, NTime, NTooltip, useDialog } from 'naive-ui'
|
||||
|
||||
const props = defineProps<{
|
||||
item: ForumTopicBaseModel
|
||||
forum: ForumModel
|
||||
}>()
|
||||
|
||||
const useForum = useForumStore()
|
||||
const dialog = useDialog()
|
||||
|
||||
function onDropdownSelect(key: string) {
|
||||
switch (key) {
|
||||
case 'delete':
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这条话题吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '再想想',
|
||||
onPositiveClick: () => {
|
||||
useForum.DelTopic(props.item.id).then((success) => {
|
||||
if (success) {
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
break
|
||||
case 'top':
|
||||
dialog.info({
|
||||
title: '问',
|
||||
content: `确定要${props.item.isPinned ? '取消' : ''}置顶这条话题吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '再想想',
|
||||
onPositiveClick: () => {
|
||||
useForum.SetTopicTop(props.item.id, !props.item.isPinned).then((success) => {
|
||||
if (success) {
|
||||
props.item.isPinned = !props.item.isPinned
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex align="center">
|
||||
<NFlex align="center" :wrap="false">
|
||||
<NTag v-if="item.isPinned" size="small" round>
|
||||
<NIcon :component="Star24Filled" color="#dba913" />
|
||||
</NTag>
|
||||
<NTag size="small" style="color: gray">
|
||||
<template #icon>
|
||||
<NIcon :component="Chat24Regular" />
|
||||
</template>
|
||||
{{ item.commentCount }}
|
||||
</NTag>
|
||||
<NText style="font-size: large">
|
||||
{{ item.title }}
|
||||
</NText>
|
||||
</NFlex>
|
||||
<NFlex style="flex: 1; color: gray; font-size: small" justify="end" align="center">
|
||||
<template v-if="item.latestRepliedBy">
|
||||
<span>
|
||||
<NIcon :component="ArrowReply24Filled" size="15" />
|
||||
@{{ item.latestRepliedBy.name }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else> @{{ item.user?.name }} 发布于 </template>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NTime :time="item.createAt" type="relative" />
|
||||
</template>
|
||||
<NTime :time="item.createAt" />
|
||||
</NTooltip>
|
||||
<NDropdown
|
||||
v-if="forum.isAdmin"
|
||||
:options="[
|
||||
{ label: '删除', key: 'delete' },
|
||||
{ label: item.isPinned ? '取消置顶' : '置顶', key: 'top' },
|
||||
]"
|
||||
trigger="hover"
|
||||
@select="onDropdownSelect"
|
||||
>
|
||||
<NButton text>
|
||||
<template #icon>
|
||||
<NIcon :component="MoreVertical24Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
82
src/views/view/forumViews/ForumReplyItem.vue
Normal file
82
src/views/view/forumViews/ForumReplyItem.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { getUserAvatarUrl } from '@/Utils'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { ForumCommentModel, ForumReplyModel, ForumTopicModel } from '@/api/models/forum'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import { ArrowReply16Filled } from '@vicons/fluent'
|
||||
import { NAvatar, NButton, NCard, NFlex, NIcon, NText, NTime, NTooltip } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: ForumReplyModel
|
||||
replyTo?: ForumReplyModel
|
||||
comment: ForumCommentModel
|
||||
topic: ForumTopicModel
|
||||
showReplyButton?: boolean
|
||||
}>()
|
||||
|
||||
const useForum = useForumStore()
|
||||
const accountInfo = useAccount()
|
||||
|
||||
const canOprate = computed(() => {
|
||||
return !props.topic.isLocked && accountInfo.value.id > 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex align="center" class="forum-reply-item">
|
||||
<NFlex :wrap="false" align="center">
|
||||
<NTooltip v-if="replyTo">
|
||||
<template #trigger>
|
||||
<NIcon :component="ArrowReply16Filled" />
|
||||
</template>
|
||||
<ForumReplyItem :item="replyTo" :comment="comment" :topic="topic" :show-reply-button="false" />
|
||||
</NTooltip>
|
||||
<NAvatar
|
||||
:src="getUserAvatarUrl(item.user.id)"
|
||||
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||
size="small"
|
||||
round
|
||||
style="margin-top: -3px; min-width: 28px; min-height: 28px"
|
||||
/>
|
||||
<NText strong depth="3" style="white-space: nowrap">
|
||||
{{ item.user.name }}
|
||||
</NText>
|
||||
</NFlex>
|
||||
{{ item.content }}
|
||||
<NFlex justify="end" align="center" :wrap="false" style="flex: 1">
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NText depth="3" style="font-size: small; min-width: 50px">
|
||||
<NTime :time="item.sendAt" type="relative" />
|
||||
</NText>
|
||||
</template>
|
||||
<NTime :time="item.sendAt" />
|
||||
</NTooltip>
|
||||
<NTooltip v-if="showReplyButton">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="tiny"
|
||||
@click="useForum.SetReplyingComment(comment, item)"
|
||||
round
|
||||
secondary
|
||||
:disabled="!canOprate"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="ArrowReply16Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
回复这条回复
|
||||
</NTooltip>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (min-width: 900px) {
|
||||
.forum-reply-item {
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
287
src/views/view/forumViews/ForumTopicDetail.vue
Normal file
287
src/views/view/forumViews/ForumTopicDetail.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<script setup lang="ts">
|
||||
import { getUserAvatarUrl } from '@/Utils'
|
||||
import { UserInfo } from '@/api/api-models'
|
||||
import { ForumCommentModel, ForumCommentSortTypes, ForumTopicModel } from '@/api/models/forum'
|
||||
import '@/assets/forumContentStyle.css'
|
||||
import TurnstileVerify from '@/components/TurnstileVerify.vue'
|
||||
import VEditor from '@/components/VEditor.vue'
|
||||
import { VTSURU_API_URL } from '@/data/constants'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import { ArrowCircleLeft12Filled, ArrowCircleLeft12Regular, Comment24Regular, Eye24Regular } from '@vicons/fluent'
|
||||
import { Heart, HeartOutline } from '@vicons/ionicons5'
|
||||
import {
|
||||
NAvatar,
|
||||
NAvatarGroup,
|
||||
NBackTop,
|
||||
NBadge,
|
||||
NButton,
|
||||
NCard,
|
||||
NDivider,
|
||||
NEllipsis,
|
||||
NEmpty,
|
||||
NFlex,
|
||||
NIcon,
|
||||
NInput,
|
||||
NList,
|
||||
NListItem,
|
||||
NModal,
|
||||
NText,
|
||||
NTime,
|
||||
NTooltip,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ForumCommentItem from './ForumCommentItem.vue'
|
||||
import ForumReplyItem from './ForumReplyItem.vue'
|
||||
import { useAccount } from '@/api/account'
|
||||
|
||||
type PostCommentModel = {
|
||||
content: string
|
||||
topic: number
|
||||
}
|
||||
type PostReplyModel = {
|
||||
content: string
|
||||
comment: number
|
||||
replyTo?: number
|
||||
}
|
||||
|
||||
const { biliInfo, userInfo } = defineProps<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
biliInfo: any | undefined
|
||||
userInfo: UserInfo | undefined
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
const accountInfo = useAccount()
|
||||
|
||||
const topicId = ref(-1)
|
||||
const useForum = useForumStore()
|
||||
|
||||
const token = ref('')
|
||||
const turnstile = ref()
|
||||
const editorRef = ref()
|
||||
|
||||
const showCommentModal = ref(false)
|
||||
const currentCommentContent = ref<PostCommentModel>({} as PostCommentModel)
|
||||
|
||||
const currentReplyContent = ref<PostReplyModel>({} as PostReplyModel)
|
||||
|
||||
const topic = ref<ForumTopicModel>({ id: -1 } as ForumTopicModel)
|
||||
const comments = ref<ForumCommentModel[]>([])
|
||||
const ps = ref(20)
|
||||
const pn = ref(0)
|
||||
const sort = ref(ForumCommentSortTypes.Time)
|
||||
|
||||
const canOprate = computed(() => {
|
||||
return !topic.value.isLocked && accountInfo.value.id > 0
|
||||
})
|
||||
|
||||
async function postComment() {
|
||||
if (!topic.value.id) return
|
||||
if (!currentCommentContent.value.content) {
|
||||
message.error('评论内容不能为空')
|
||||
return
|
||||
}
|
||||
currentCommentContent.value.topic = topic.value.id
|
||||
useForum
|
||||
.PostComment(currentCommentContent.value, token.value)
|
||||
.then(async (comment) => {
|
||||
if (comment) {
|
||||
comments.value = (await useForum.GetComments(topic.value.id, pn.value, ps.value, sort.value)) ?? []
|
||||
currentCommentContent.value = {} as PostCommentModel
|
||||
showCommentModal.value = false
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
turnstile.value?.reset()
|
||||
})
|
||||
}
|
||||
async function postReply() {
|
||||
if (!topic.value.id) return
|
||||
if (!currentReplyContent.value.content) {
|
||||
message.error('回复内容不能为空')
|
||||
return
|
||||
}
|
||||
currentReplyContent.value.comment = useForum.replyingComment?.id ?? -1
|
||||
currentReplyContent.value.replyTo = useForum.replyingReply?.id
|
||||
useForum
|
||||
.PostReply(currentReplyContent.value, token.value)
|
||||
.then(async (comment) => {
|
||||
if (comment) {
|
||||
comments.value = (await useForum.GetComments(topic.value.id, pn.value, ps.value, sort.value)) ?? []
|
||||
currentReplyContent.value = {} as PostReplyModel
|
||||
useForum.SetReplyingComment()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
turnstile.value?.reset()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.params.topicId) {
|
||||
topicId.value = route.params.topicId as unknown as number
|
||||
topic.value = (await useForum.GetTopicDetail(topicId.value)) ?? ({ id: -1 } as ForumTopicModel)
|
||||
comments.value = (await useForum.GetComments(topicId.value, pn.value, ps.value, sort.value)) ?? []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="!topic.id"> </template>
|
||||
<template v-else>
|
||||
<div size="small" embedded style="max-width: 1500px; margin: 0 auto">
|
||||
<NBackTop />
|
||||
<NBadge class="back-forum-badge" style="width: 100%; left: 0" type="info" :offset="[3, 3]">
|
||||
<NCard size="small">
|
||||
<NText style="font-size: large; font-weight: bold; text-align: center; width: 100%">
|
||||
<NEllipsis style="width: 100%">
|
||||
{{ topic.title }}
|
||||
</NEllipsis>
|
||||
</NText>
|
||||
</NCard>
|
||||
<template #value>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton text @click="() => $router.push({ name: 'user-forum', params: { id: userInfo?.name } })">
|
||||
<template #icon>
|
||||
<NIcon :component="ArrowCircleLeft12Regular" color="white" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
返回
|
||||
</NTooltip>
|
||||
</template>
|
||||
</NBadge>
|
||||
<NCard content-style="padding: 0 12px 0 12px;" embedded>
|
||||
<template #header>
|
||||
<NFlex align="center" :size="5">
|
||||
<NAvatar
|
||||
:src="VTSURU_API_URL + 'user-face/' + topic?.user?.id + '?size=64'"
|
||||
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||
/>
|
||||
<NDivider vertical />
|
||||
{{ topic.user?.name }}
|
||||
</NFlex>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NText depth="3">
|
||||
<NTime :time="topic.createAt" type="relative" />
|
||||
</NText>
|
||||
</template>
|
||||
<NTime :time="topic.createAt" />
|
||||
</NTooltip>
|
||||
</template>
|
||||
<template #footer>
|
||||
<NAvatarGroup
|
||||
:size="30"
|
||||
:options="topic.sampleLikedBy?.map((u) => ({ src: getUserAvatarUrl(u) })) ?? []"
|
||||
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||
/>
|
||||
<NDivider style="margin: 5px 0 10px 0" />
|
||||
<NFlex>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton size="small" :bordered="topic.isLiked" text>
|
||||
<template #icon>
|
||||
<NIcon :component="Eye24Regular" />
|
||||
</template>
|
||||
{{ topic.viewCount }}
|
||||
</NButton>
|
||||
</template>
|
||||
浏览
|
||||
</NTooltip>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
@click="
|
||||
useForum.LikeTopic(topic.id, !topic.isLiked).then((success) => {
|
||||
if (success) {
|
||||
topic.isLiked = !topic.isLiked
|
||||
topic.likeCount += topic.isLiked ? 1 : -1
|
||||
}
|
||||
})
|
||||
"
|
||||
:bordered="topic.isLiked"
|
||||
secondary
|
||||
:type="topic.isLiked ? 'primary' : 'default'"
|
||||
:loading="useForum.isLikeLoading"
|
||||
:disabled="!canOprate"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="topic.isLiked ? Heart : HeartOutline" :color="topic.isLiked ? '#dd484f' : ''" />
|
||||
</template>
|
||||
{{ topic.likeCount }}
|
||||
</NButton>
|
||||
</template>
|
||||
点赞
|
||||
</NTooltip>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton size="small" @click="showCommentModal = true" secondary :disabled="!canOprate">
|
||||
<template #icon>
|
||||
<NIcon :component="Comment24Regular" />
|
||||
</template>
|
||||
{{ topic.commentCount }}
|
||||
</NButton>
|
||||
</template>
|
||||
评论
|
||||
</NTooltip>
|
||||
</NFlex>
|
||||
</template>
|
||||
<div class="editor-content-view" v-html="topic.content"></div>
|
||||
</NCard>
|
||||
<NDivider>
|
||||
<NButton @click="showCommentModal = true" type="primary" :disabled="!canOprate">发送评论</NButton>
|
||||
</NDivider>
|
||||
<NEmpty v-if="comments.length === 0" description="暂无评论" />
|
||||
<NList v-else hoverable bordered size="small">
|
||||
<NListItem v-for="item in comments" :key="item.id">
|
||||
<ForumCommentItem :item="item" :topic="topic" />
|
||||
</NListItem>
|
||||
</NList>
|
||||
<NDivider />
|
||||
</div>
|
||||
</template>
|
||||
<NModal v-model:show="showCommentModal" preset="card" style="width: 1000px; max-width: 90vw; height: auto">
|
||||
<template #header> 发送评论 </template>
|
||||
<VEditor v-model:value="currentCommentContent.content" :max-length="1111" ref="editorRef" />
|
||||
<NButton type="primary" @click="postComment" :loading="!token || useForum.isLoading"> 发布 </NButton>
|
||||
</NModal>
|
||||
<NModal v-model:show="useForum.showReplyModal" preset="card" style="width: 1000px; max-width: 90vw; height: auto">
|
||||
<template #header> 发送回复 </template>
|
||||
<template v-if="useForum.replyingReply">
|
||||
<NCard size="small" title="正在回复" embedded>
|
||||
<ForumReplyItem
|
||||
v-if="useForum.replyingReply && useForum.replyingComment"
|
||||
:item="useForum.replyingReply"
|
||||
:comment="useForum.replyingComment"
|
||||
:topic="topic"
|
||||
:show-reply-button="false"
|
||||
/>
|
||||
</NCard>
|
||||
<NDivider />
|
||||
</template>
|
||||
<NInput
|
||||
v-model:value="currentReplyContent.content"
|
||||
type="textarea"
|
||||
placeholder="回复内容"
|
||||
maxlength="233"
|
||||
show-count
|
||||
/>
|
||||
<NDivider />
|
||||
<NButton type="primary" @click="postReply" :loading="!token || useForum.isLoading"> 发布 </NButton>
|
||||
</NModal>
|
||||
<TurnstileVerify ref="turnstile" v-model="token" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.n-badge-sup {
|
||||
left: 0 !important;
|
||||
}
|
||||
</style>
|
||||
163
src/views/view/forumViews/ForumView.vue
Normal file
163
src/views/view/forumViews/ForumView.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<script setup lang="ts">
|
||||
import { UserInfo } from '@/api/api-models'
|
||||
import { ForumPostTopicModel, ForumTopicBaseModel, ForumTopicSortTypes, ForumUserLevels } from '@/api/models/forum'
|
||||
import TurnstileVerify from '@/components/TurnstileVerify.vue'
|
||||
import VEditor from '@/components/VEditor.vue'
|
||||
import { TURNSTILE_KEY } from '@/data/constants'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NCard,
|
||||
NDivider,
|
||||
NFlex,
|
||||
NInput,
|
||||
NList,
|
||||
NListItem,
|
||||
NModal,
|
||||
NText,
|
||||
NTime,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import VueTurnstile from 'vue-turnstile'
|
||||
import ForumPreviewItem from './ForumPreviewItem.vue'
|
||||
import ForumCommentItem from './ForumCommentItem.vue'
|
||||
|
||||
const { biliInfo, userInfo } = defineProps<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
biliInfo: any | undefined
|
||||
userInfo: UserInfo | undefined
|
||||
}>()
|
||||
const token = ref('')
|
||||
const turnstile = ref()
|
||||
const editor = ref()
|
||||
|
||||
const postTopicBackup = useStorage<{ [key: number]: ForumPostTopicModel }>('Forum.PostTopic', {})
|
||||
const showPostTopicModal = ref(false)
|
||||
const currentPostTopicModel = ref<ForumPostTopicModel>({} as ForumPostTopicModel)
|
||||
const lastBackupTopic = ref(Date.now())
|
||||
|
||||
const useForum = useForumStore()
|
||||
const message = useMessage()
|
||||
const ps = ref(20)
|
||||
const pn = ref(0)
|
||||
const sort = ref(ForumTopicSortTypes.Time)
|
||||
|
||||
const forumInfo = ref(await useForum.GetForumInfo(userInfo?.id ?? -1))
|
||||
const topics = ref<{ data: ForumTopicBaseModel[]; total: number; more: boolean } | undefined>({
|
||||
data: [],
|
||||
total: 0,
|
||||
more: false,
|
||||
})
|
||||
|
||||
async function ApplyToForum() {
|
||||
if (!forumInfo.value) return
|
||||
if (await useForum.ApplyToForum(forumInfo.value.owner.id ?? -1)) {
|
||||
forumInfo.value.isApplied = true
|
||||
}
|
||||
}
|
||||
function backupTopic() {
|
||||
if (!showPostTopicModal.value) {
|
||||
return
|
||||
}
|
||||
postTopicBackup.value[forumInfo.value?.owner.id ?? -1] = currentPostTopicModel.value
|
||||
lastBackupTopic.value = Date.now()
|
||||
}
|
||||
function postTopic() {
|
||||
currentPostTopicModel.value.owner = forumInfo.value?.owner.id ?? -1
|
||||
useForum
|
||||
.PostTopic(currentPostTopicModel.value, token.value)
|
||||
.then(async (topic) => {
|
||||
if (topic) {
|
||||
currentPostTopicModel.value = {} as ForumPostTopicModel
|
||||
delete postTopicBackup.value[forumInfo.value?.owner.id ?? -1]
|
||||
showPostTopicModal.value = false
|
||||
topics.value = await useForum.GetTopics(forumInfo.value?.owner.id ?? -1, ps.value, pn.value, sort.value)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
turnstile.value?.reset()
|
||||
})
|
||||
}
|
||||
|
||||
let timer: any
|
||||
onMounted(async () => {
|
||||
if (forumInfo.value) {
|
||||
topics.value = await useForum.GetTopics(forumInfo.value.owner.id ?? -1, ps.value, pn.value, sort.value)
|
||||
if (postTopicBackup.value[forumInfo.value.owner.id ?? -1]) {
|
||||
currentPostTopicModel.value = postTopicBackup.value[forumInfo.value.owner.id ?? -1]
|
||||
}
|
||||
timer = setInterval(async () => {
|
||||
backupTopic()
|
||||
}, 10000)
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert v-if="!forumInfo" type="error"> 用户未创建粉丝讨论区 </NAlert>
|
||||
<NCard
|
||||
v-else-if="
|
||||
(forumInfo.level < ForumUserLevels.Member && forumInfo.settings.requireApply) ||
|
||||
forumInfo.settings.allowedViewerLevel > forumInfo.level
|
||||
"
|
||||
>
|
||||
<NAlert type="warning"> 你需要成为成员才能访问 </NAlert>
|
||||
<NAlert v-if="forumInfo.isApplied" type="success"> 已申请, 正在等待管理员审核 </NAlert>
|
||||
<NCard v-else title="加入">
|
||||
加入 {{ forumInfo.name }}
|
||||
<NButton type="primary" @click="ApplyToForum" :loading="useForum.isLoading">
|
||||
{{ forumInfo.settings.requireApply ? '申请' : '' }}加入
|
||||
</NButton>
|
||||
</NCard>
|
||||
</NCard>
|
||||
<template v-else>
|
||||
<NFlex vertical>
|
||||
<NCard size="small">
|
||||
<template #header>
|
||||
<NFlex justify="center">
|
||||
<NText style="font-size: large">{{ forumInfo.name }}</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NCard>
|
||||
<NFlex>
|
||||
<NCard style="max-width: 300px">
|
||||
<NFlex vertical>
|
||||
<NButton @click="showPostTopicModal = true"> 发布话题 </NButton>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
<NList bordered style="flex: 1" size="small" hoverable clickable>
|
||||
<NListItem v-for="item in topics?.data ?? []" :key="item.id">
|
||||
<a :href="`${$route.path}/topic/${item.id}`" target="_blank">
|
||||
<ForumPreviewItem :item="item" :forum="forumInfo" />
|
||||
</a>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
<NModal preset="card" v-model:show="showPostTopicModal" style="width: 800px; max-width: 95%">
|
||||
<template #header>
|
||||
发布话题
|
||||
<NDivider vertical />
|
||||
<NText depth="3" style="font-size: small"> 保存于 <NTime :time="lastBackupTopic" format="HH:mm:ss" /> </NText>
|
||||
</template>
|
||||
<NFlex vertical>
|
||||
<NInput v-model:value="currentPostTopicModel.title" placeholder="标题" />
|
||||
<VEditor v-model:value="currentPostTopicModel.content" :max-length="2333" ref="editor" />
|
||||
<NButton type="primary" @click="postTopic" :loading="!token || useForum.isLoading"> 发布 </NButton>
|
||||
</NFlex>
|
||||
</NModal>
|
||||
<TurnstileVerify ref="turnstile" v-model="token" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -60,7 +60,7 @@ export const Config: TemplateConfig<ConfigType> = {
|
||||
:img-props="{
|
||||
referrerpolicy: 'no-referrer',
|
||||
}"
|
||||
:style="{ boxShadow: isDarkMode() ? 'rgb(195 192 192 / 35%) 0px 5px 20px' : '0 5px 15px rgba(0, 0, 0, 0.2)' }"
|
||||
:style="{ boxShadow: isDarkMode ? 'rgb(195 192 192 / 35%) 0px 5px 20px' : '0 5px 15px rgba(0, 0, 0, 0.2)' }"
|
||||
/>
|
||||
<NSpace align="baseline" justify="center">
|
||||
<NText strong style="font-size: 32px"> {{ biliInfo?.name }} </NText>
|
||||
|
||||
Reference in New Issue
Block a user