feat: like topic

This commit is contained in:
KUN1007 2023-09-30 21:49:37 +08:00
parent efef4bc6f9
commit cc7fcb678a
12 changed files with 241 additions and 48 deletions

View file

@ -7,6 +7,7 @@ export * from './edit/types/edit'
export * from './home/types/home' export * from './home/types/home'
export * from './kungalgamer/types/kungalgamer' export * from './kungalgamer/types/kungalgamer'
export * from './login/types/login' export * from './login/types/login'
export * from './topic/types/action'
export * from './topic/types/topic' export * from './topic/types/topic'
export * from './topic/types/aside' export * from './topic/types/aside'
export * from './update-log/types/updateLog' export * from './update-log/types/updateLog'

View file

@ -1,6 +1,7 @@
import { fetchGet, fetchPost } from '@/utils/request' import { fetchGet, fetchPost, fetchPut } from '@/utils/request'
// 将对象转为请求参数的函数 // 将对象转为请求参数的函数
import objectToQueryParams from '@/utils/objectToQueryParams' import objectToQueryParams from '@/utils/objectToQueryParams'
import * as Action from './types/action'
import * as Aside from './types/aside' import * as Aside from './types/aside'
import * as Topic from './types/topic' import * as Topic from './types/topic'
@ -54,6 +55,18 @@ export async function getTopicByTidApi(
} }
} }
// 点赞话题
export async function updateTopicLikeApi(
request: Action.TopicLikeTopicRequestData
): Promise<Action.TopicLikeTopicResponseData> {
const queryParams = objectToQueryParams(request, 'tid')
const url = `/topics/${request.tid}/like?${queryParams}`
const response = fetchPut<Action.TopicLikeTopicResponseData>(url)
return response
}
// 根据话题 tid 获取话题回复 // 根据话题 tid 获取话题回复
export async function getRepliesByPidApi( export async function getRepliesByPidApi(
request: Topic.TopicReplyRequestData request: Topic.TopicReplyRequestData

View file

@ -1,3 +1,13 @@
/** /**
* *
*/ */
// 点赞
export interface TopicLikeTopicRequestData {
tid: number
to_uid: number
isPush: boolean
}
// 点赞话题响应数据的格式
export type TopicLikeTopicResponseData = KUNGalgameResponseData<{}>

View file

@ -16,6 +16,9 @@ export default (
type: MessageType, type: MessageType,
duration?: number duration?: number
) => { ) => {
// 初始化的时候先将上一次创建的内容销毁
render(null, document.body)
const messageNode = h(Message, { const messageNode = h(Message, {
messageCN, messageCN,
messageEN, messageEN,

View file

@ -200,6 +200,10 @@ const handleTextChange = async () => {
/* 头部下方阴影 */ /* 头部下方阴影 */
box-shadow: 0 2px 4px 0 var(--kungalgame-trans-blue-1); box-shadow: 0 2px 4px 0 var(--kungalgame-trans-blue-1);
display: v-bind(isShowEditorToolbar); display: v-bind(isShowEditorToolbar);
/* 不显示视频插入,这个功能 BUG 太多了 */
.ql-video {
display: none;
}
} }
/* 编辑器体的样式 */ /* 编辑器体的样式 */

View file

@ -10,6 +10,14 @@ import {
getPopularTopicsByUserUidApi, getPopularTopicsByUserUidApi,
TopicAsideResponseData, TopicAsideResponseData,
} from '@/api' } from '@/api'
// 点赞等动作
import {
updateTopicLikeApi,
TopicLikeTopicRequestData,
TopicLikeTopicResponseData,
} from '@/api'
// 回复 // 回复
import { import {
getRepliesByPidApi, getRepliesByPidApi,
@ -19,6 +27,7 @@ import {
TopicCreateReplyRequestData, TopicCreateReplyRequestData,
TopicCreateReplyResponseData, TopicCreateReplyResponseData,
} from '@/api' } from '@/api'
// 评论 // 评论
import { import {
getCommentsByReplyRidApi, getCommentsByReplyRidApi,
@ -98,6 +107,20 @@ export const useKUNGalgameTopicStore = defineStore({
return await getTopicByTidApi(tid) return await getTopicByTidApi(tid)
}, },
// 点赞话题
async updateTopicLike(
tid: number,
toUid: number,
isPush: boolean
): Promise<TopicLikeTopicResponseData> {
const requestData: TopicLikeTopicRequestData = {
tid: tid,
to_uid: toUid,
isPush: isPush,
}
return await updateTopicLikeApi(requestData)
},
// 获取回复 // 获取回复
async getReplies(tid: number): Promise<TopicReplyResponseData> { async getReplies(tid: number): Promise<TopicReplyResponseData> {
// 这里的默认值用于初始化 // 这里的默认值用于初始化

View file

@ -1,8 +1,8 @@
export function getPlainText(html: string): string { export function getPlainText(html: string): string {
// 使用正则表达式匹配所有HTML标签并删除 // 使用正则表达式匹配所有 HTML 标签并删除
const plainText = html.replace(/<[^>]*>/g, '') const plainText = html.replace(/<[^>]*>/g, '')
// 使用实体编码映射表将HTML实体编码还原 // 使用实体编码映射表将 HTML 实体编码还原
const entityMap: Record<string, string> = { const entityMap: Record<string, string> = {
lt: '<', lt: '<',
gt: '>', gt: '>',

View file

@ -58,19 +58,19 @@ const loliFaceLeft = loli.face.left + 'px'
const loliFaceTop = loli.face.top + 'px' const loliFaceTop = loli.face.top + 'px'
// 身体的图片资源链接 // 身体的图片资源链接
let lass = getAssetsFile(loli.lass.layer_id) const lass = getAssetsFile(loli.lass.layer_id)
// 眼睛的图片资源链接 // 眼睛的图片资源链接
let eye = getAssetsFile(loli.eye.layer_id) const eye = getAssetsFile(loli.eye.layer_id)
// 眉毛的图片资源链接 // 眉毛的图片资源链接
let brow = getAssetsFile(loli.brow.layer_id) const brow = getAssetsFile(loli.brow.layer_id)
// 嘴巴的图片资源链接 // 嘴巴的图片资源链接
let mouth = getAssetsFile(loli.mouth.layer_id) const mouth = getAssetsFile(loli.mouth.layer_id)
// 腮红的图片资源链接 // 腮红的图片资源链接
let face = getAssetsFile(loli.face.layer_id) const face = getAssetsFile(loli.face.layer_id)
// 导出模块 // 导出模块
export { export {

36
src/utils/throttle.ts Normal file
View file

@ -0,0 +1,36 @@
/**
* Throttle
*
*/
/**
* @param {T} executeCallback - throttle
* @param {number} delay - throttle
* @param {T | undefined} delayedCallback - throttle execute
*/
export function throttle<T extends (...args: any[]) => void>(
executeCallback: T,
delay: number,
delayedCallback?: T | undefined
) {
let lastExecution = 0
let timeout: NodeJS.Timeout | null = null
const throttled = (...args: Parameters<T>) => {
const now = Date.now()
if (!lastExecution || now - lastExecution >= delay) {
executeCallback(...args)
lastExecution = now
} else {
if (!timeout && delayedCallback) {
delayedCallback(...args)
timeout = setTimeout(() => {
timeout = null
}, delay)
}
}
}
return throttled as (...args: Parameters<T>) => void
}

View file

@ -36,7 +36,7 @@ const {
user, user,
// rid, // rid,
status, status,
// share, share,
category, category,
} = topicData.topicData } = topicData.topicData
@ -94,11 +94,19 @@ const loliStatus = computed(() => {
<Rewrite v-if="edited" :time="edited" /> <Rewrite v-if="edited" :time="edited" />
</div> </div>
</div> </div>
<!-- 话题的点赞数等信息楼主的 floor 就是 0 --> <!-- 话题的点赞数等信息楼主的 floor 就是 0被作用人就是该话题的发布人 -->
<MasterFooter <MasterFooter
:info="{ tid, views, likes, dislikes, upvotes, to_floor: 0 }" :info="{
tid,
views,
likes,
dislikes,
upvotes,
share,
to_floor: 0,
}"
:topic="{ tid, title, content, tags, category }" :topic="{ tid, title, content, tags, category }"
:r-user="user" :master="user"
/> />
</div> </div>
</div> </div>

View file

@ -1,7 +1,12 @@
<!-- 话题的底部区域推话题回复点赞等 --> <!-- 话题的底部区域推话题回复点赞等 -->
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
//
import message from '@/components/alert/Message'
// throttle
import { throttle } from '@/utils/throttle'
import { TopicUserInfo } from '@/api' import { TopicUserInfo } from '@/api'
@ -28,6 +33,7 @@ const props = defineProps<{
likes: number[] likes: number[]
dislikes: number[] dislikes: number[]
upvotes: number[] upvotes: number[]
share: number[]
// floor // floor
to_floor: number to_floor: number
} }
@ -38,23 +44,87 @@ const props = defineProps<{
tags: string[] tags: string[]
category: string[] category: string[]
} }
rUser: TopicUserInfo master: TopicUserInfo
}>() }>()
//
/** /**
* 这里只是简单起见不显示重新编辑 * 这里只是简单起见不显示重新编辑
* 实际上如果用户自己修改了 localStorage 中保存的信息这个验证就失效了 * 实际上如果用户自己修改了 localStorage 中保存的信息这个验证就失效了
* 但是修改了也没有用验证逻辑位于后端 * 但是修改了也没有用验证逻辑位于后端
*/ */
const isShowRewrite = useKUNGalgameUserStore().uid === props.rUser.uid // uid
const currUserUid = useKUNGalgameUserStore().uid
//
const isShowRewrite = currUserUid === props.master.uid
// footer
const actions = reactive({
upvotes: props.info.upvotes,
likes: props.info.likes,
dislikes: props.info.dislikes,
share: props.info.share,
})
//
const isActive = reactive({
isUpvote: false,
isLiked: false,
isDisliked: false,
isShared: false,
})
// throttle 1007
const handleClickLikeThrottled = throttle(
async () => {
//
if (currUserUid === props.master.uid) {
message(
'You cannot like your own topic',
'您不可以给自己的话题点赞',
'warn'
)
return
}
//
isActive.isLiked = !isActive.isLiked
const res = await useKUNGalgameTopicStore().updateTopicLike(
props.info.tid,
props.master.uid,
isActive.isLiked
)
if (res.code === 200) {
//
actions.likes.length = isActive.isLiked
? actions.likes.length + 1
: actions.likes.length - 1
} else {
message('Like topic failed!', '点赞话题失败', 'error')
}
},
1007,
() => {
message(
'You can only perform one operation within 1007 milliseconds',
'您在 1007 毫秒内只能进行一次操作',
'warn'
)
}
)
// 使
const handleClickLike = () => {
handleClickLikeThrottled()
}
// //
const handelReply = async () => { const handelReply = async () => {
// 便 // 便
replyDraft.value.tid = props.info.tid replyDraft.value.tid = props.info.tid
// uid // uid
replyDraft.value.to_uid = props.rUser.uid replyDraft.value.to_uid = props.master.uid
replyDraft.value.to_floor = props.info.to_floor replyDraft.value.to_floor = props.info.to_floor
isEdit.value = true isEdit.value = true
@ -75,6 +145,29 @@ const handleClickEdit = () => {
// //
router.push({ name: 'Edit' }) router.push({ name: 'Edit' })
} }
//
onMounted(() => {
//
if (props.info.upvotes.includes(currUserUid)) {
isActive.isUpvote = true
}
//
if (props.info.likes.includes(currUserUid)) {
isActive.isLiked = true
}
//
if (props.info.dislikes.includes(currUserUid)) {
isActive.isDisliked = true
}
//
if (props.info.share.includes(currUserUid)) {
isActive.isShared = true
}
})
</script> </script>
<template> <template>
@ -86,7 +179,7 @@ const handleClickEdit = () => {
<!-- 推话题 --> <!-- 推话题 -->
<li> <li>
<span class="icon"><Icon icon="bi:rocket" /></span> <span class="icon"><Icon icon="bi:rocket" /></span>
{{ info.upvotes.length }} {{ actions.upvotes.length }}
</li> </li>
<!-- 查看数量 --> <!-- 查看数量 -->
<li> <li>
@ -95,39 +188,38 @@ const handleClickEdit = () => {
</li> </li>
<!-- 点赞 --> <!-- 点赞 -->
<li> <li>
<span class="icon"><Icon icon="line-md:thumbs-up-twotone" /></span> <span
{{ info.likes.length }} class="icon"
:class="isActive.isLiked ? 'active' : ''"
@click="handleClickLike"
><Icon icon="line-md:thumbs-up-twotone"
/></span>
{{ actions.likes.length }}
</li> </li>
<!-- --> <!-- -->
<li> <li>
<span class="icon"><Icon icon="line-md:thumbs-down-twotone" /></span> <span class="icon"><Icon icon="line-md:thumbs-down-twotone" /></span>
{{ info.dislikes.length }} {{ actions.dislikes.length }}
</li> </li>
</ul> </ul>
</div> </div>
<!-- 底部右侧部分回复评论只看编辑 --> <!-- 底部右侧部分回复评论只看编辑 -->
<div class="right"> <div class="right">
<ul> <div @click="handelReply" class="reply">
<li @click="handelReply"> {{ $tm('topic.content.reply') }}
<div class="reply"> </div>
{{ $tm('topic.content.reply') }}
</div> <!-- 分享 -->
</li> <span class="icon"><Icon icon="majesticons:share-line" /></span>
<!-- 分享 -->
<li> <!-- 只看 -->
<span class="icon"><Icon icon="majesticons:share-line" /></span> <span class="icon"><Icon icon="ph:user-focus-duotone" /></span>
</li>
<!-- 只看 --> <!-- 编辑 -->
<li> <span v-if="isShowRewrite" @click="handleClickEdit" class="icon">
<span class="icon"><Icon icon="ph:user-focus-duotone" /></span> <Icon icon="line-md:pencil-twotone-alt" />
</li> </span>
<!-- 编辑 -->
<li v-if="isShowRewrite">
<span @click="handleClickEdit" class="icon">
<Icon icon="line-md:pencil-twotone-alt" />
</span>
</li>
</ul>
</div> </div>
</div> </div>
</template> </template>
@ -170,17 +262,20 @@ const handleClickEdit = () => {
color: var(--kungalgame-font-color-2); color: var(--kungalgame-font-color-2);
cursor: pointer; cursor: pointer;
} }
/* 激活后的样式 */
.active {
color: var(--kungalgame-blue-4);
}
/* 底部右侧部分(回复、评论、只看、编辑) */ /* 底部右侧部分(回复、评论、只看、编辑) */
.right ul { .right {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-left: 10px; span {
li { display: flex;
margin-right: 17px; margin-right: 17px;
span {
display: flex;
}
} }
} }

View file

@ -45,7 +45,7 @@ const handelClosePanel = () => {
<template> <template>
<Teleport to="body" :disabled="isEdit"> <Teleport to="body" :disabled="isEdit">
<Transition <Transition
enter-active-class="animate__animated animate__bounceInUp animate__faster" enter-active-class="animate__animated animate__fadeInUp animate__faster"
leave-active-class="animate__animated animate__fadeOutDown animate__faster" leave-active-class="animate__animated animate__fadeOutDown animate__faster"
> >
<div class="root" v-if="isEdit"> <div class="root" v-if="isEdit">