Skip to content

Commit

Permalink
Merge pull request #113 from KIRAKIRA-DOUGA/feature-2023122301-VIdeoC…
Browse files Browse the repository at this point in the history
…omments-cfdxkk

✨ 视频评论,视频评论点赞/点踩功能
  • Loading branch information
otomad authored Dec 26, 2023
2 parents 9dbe8bd + f8d06d7 commit e65d7c0
Show file tree
Hide file tree
Showing 14 changed files with 566 additions and 58 deletions.
32 changes: 17 additions & 15 deletions components/Creation/CreationComments/CreationComments.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
/** 评论数目。 */
count?: number | string;
comments?: Comments200ResponseInner[];
videoId?: number;
count?: number;
comments?: GetVideoCommentByKvidResponseDto["videoCommentList"];
videoId: number;
}>(), {
count: 0,
comments: () => [],
videoId: undefined,
});
const downvote = ref(0), pinned = ref(false);
const pinned = ref(false);
const search = ref("");
const pageCount = computed(() => Math.floor(props.count / 20) + 1);
</script>

<template>
Expand All @@ -28,27 +28,29 @@
</div>
<div class="right">
<TextBox v-model="search" :placeholder="t.search" icon="search" />
<Pagination :current="1" :pages="99" :displayPageCount="7" />
<Pagination :current="1" :pages="pageCount" :displayPageCount="7" />
<SoftButton icon="deletion_history" />
</div>
</div>
<div class="items">
<CreationCommentsItem
v-for="comment in comments"
:key="comment.id"
:key="comment._id"
v-model:upvote="comment.upvoteCount"
v-model:downvote="downvote"
v-model:isUpvoted="comment.userHasUpvoted"
v-model:isDownvoted="comment.userHasDownvoted"
v-model:downvote="comment.downvoteCount"
v-model:isUpvoted="comment.isUpvote"
v-model:isDownvoted="comment.isDownvote"
v-model:pinned="pinned"
:index="comment.id"
:username="comment.fullname"
:avatar="comment.profilePictureUrl"
:date="new Date(comment.created!)"
:commentId="comment._id"
:videoId="videoId"
:index="comment.commentIndex"
:username="comment.userInfo?.username"
:avatar="comment.userInfo?.avatar"
:date="new Date(comment.editDateTime)"
:upvote_score="comment.upvoteCount"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="comment.content"></div>
<div v-html="comment.text"></div>
</CreationCommentsItem>
</div>
</Comp>
Expand Down
140 changes: 131 additions & 9 deletions components/Creation/CreationComments/CreationCommentsItem.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
/** 评论唯一 ID */
commentId: string;
/** 评论所在的视频的 ID */
videoId: number;
/** 评论发布者头像网址。 */
avatar?: string;
/** 评论发布者昵称。 */
Expand Down Expand Up @@ -32,29 +36,147 @@
const pinned = defineModel("pinned", { default: false });
const unpinnedCaption = computed(() => pinned.value ? "unpin" : "pin");
const voteLock = ref(false);
const userSelfInfoStore = useSelfUserInfoStore();
/**
* 点击加分、减分按钮事件。
* @param button - 点击的按钮是加分还是减分。
* @param [noNestingDolls=false] - 禁止套娃,防止递归调用。
*/
async function onClickVotes(button: "upvote" | "downvote", noNestingDolls: boolean = false) {
function onClickVotes(button: "upvote" | "downvote", noNestingDolls: boolean = false) {
// const states = { upvote, isUpvoted, downvote, isDownvoted };
// const value = states[button], clicked = states[`${button}Clicked`]; // 面向字符串编程。
// const another = button === "like" ? "dislike" : "like";
// const isActive = clicked.value = !clicked.value, gain = isActive ? 1 : -1;
// value.value += gain;
// if (isActive && states[`${another}Clicked`].value && !noNestingDolls) onClickUpvote(another, true);
if (!props.index) return;
const api = useApi();
const score = button === "upvote" ? 1 : -1;
await api.upvote(props.index, score);
upvote.value += score;
// TODO: We should separate upvote and downvote value!
const commentId = props.commentId; // 视频评论 ID
const videoId = props.videoId; // 视频 ID
if (!props.index || !commentId || videoId === undefined || videoId === null) { // 非空验证
useToast("出错啦!请刷新页面再试~", "error"); // TODO 使用多语言
return;
}
if (voteLock.value) { // 如果请求的"悲观锁"处于锁定状态,则弹出错误提示并停止
useToast("操作过于频繁,请稍后再试~", "error"); // TODO 使用多语言
return;
}
if (!userSelfInfoStore.isLogined) { // 如果用户未登录,则不允许点赞/点踩
useToast("请登录后再操作~", "error"); // TODO 使用多语言
return;
}
if (button === "upvote") // 判断是点赞还是点踩
if (isUpvoted.value) // 如果被点赞的视频评论此前已经被点赞,则取消点赞,否则点赞
// 取消点赞
cancelVideoCommentUpvote(commentId, videoId);
else
// 点赞
emitVideoCommentUpvote(commentId, videoId);
else
if (isDownvoted.value) // 如果被踩的视频评论此前已经被点踩,则取消点踩,否则点踩
// 取消点踩
cancelVideoCommentDownvote(commentId, videoId);
else
// 点踩
emitVideoCommentDownvote(commentId, videoId);
}
/**
* 点赞视频评论
* @param commentId 视频评论 ID
* @param videoId 视频 ID
*/
function emitVideoCommentUpvote(commentId: string, videoId: number) {
voteLock.value = true; // 请求锁:锁定
const emitVideoCommentUpvoteRequest: EmitVideoCommentUpvoteRequestDto = { id: commentId, videoId };
api.videoComment.emitVideoCommentUpvote(emitVideoCommentUpvoteRequest).catch(error => {
voteLock.value = false; // 请求锁:释放
useToast("点赞失败!", "error"); // TODO 使用多语言
console.error("ERROR", "点赞失败!", error);
}).finally(() => {
voteLock.value = false; // 请求锁:释放
});
isUpvoted.value = true; // 设置点赞 ICON 高亮
upvote.value++; // 赞数增加
if (isDownvoted.value) { // 如果用户在点赞操作前,已经有点踩,则取消点踩的高亮,并减少点踩数量
isDownvoted.value = false;
downvote.value--;
}
}
/**
* 取消视频评论点赞
* @param commentId 视频评论 ID
* @param videoId 视频 ID
*/
function cancelVideoCommentUpvote(commentId: string, videoId: number) {
voteLock.value = true; // 请求锁:锁定
const cancelVideoCommentUpvoteRequest: CancelVideoCommentUpvoteRequestDto = { id: commentId, videoId };
api.videoComment.cancelVideoCommentUpvote(cancelVideoCommentUpvoteRequest).catch(error => {
voteLock.value = false; // 请求锁:释放
useToast("取消点赞失败!", "error"); // TODO 使用多语言
console.error("ERROR", "取消点赞失败!", error);
}).finally(() => {
voteLock.value = false; // 请求锁:释放
});
isUpvoted.value = false; // 取消点赞 ICON 高亮
upvote.value--; // 赞数减少
}
/**
* 视频评论点踩
* @param commentId 视频评论 ID
* @param videoId 视频 ID
*/
function emitVideoCommentDownvote(commentId: string, videoId: number) {
voteLock.value = true; // 请求锁:锁定
const emitVideoCommentDownvoteRequest: EmitVideoCommentDownvoteRequestDto = { id: commentId, videoId };
api.videoComment.emitVideoCommentDownvote(emitVideoCommentDownvoteRequest).catch(error => {
voteLock.value = false; // 请求锁:释放
useToast("点踩失败!", "error"); // TODO 使用多语言
console.error("ERROR", "点踩失败!", error);
}).finally(() => {
voteLock.value = false; // 请求锁:释放
});
isDownvoted.value = true; // 设置点踩 ICON 高亮
downvote.value++; // 踩数增加
if (isUpvoted.value) { // 如果用户在点踩操作前,已经有点赞,则取消点赞的高亮,并减少点赞数量
upvote.value--;
isUpvoted.value = false;
}
}
/**
* 取消视频评论点踩
* @param commentId 视频评论 ID
* @param videoId 视频 ID
*/
function cancelVideoCommentDownvote(commentId: string, videoId: number) {
voteLock.value = true; // 请求锁:锁定
const cancelVideoCommentDownvoteRequest: CancelVideoCommentDownvoteRequestDto = { id: commentId, videoId };
api.videoComment.cancelVideoCommentDownvote(cancelVideoCommentDownvoteRequest).catch(error => {
voteLock.value = false; // 请求锁:释放
useToast("取消点踩失败!", "error"); // TODO 使用多语言
console.error("ERROR", "取消点踩失败!", error);
}).finally(() => {
voteLock.value = false; // 请求锁:释放
});
isDownvoted.value = false; // 取消点踩 ICON 高亮
downvote.value--; // 踩数减少
}
/**
* 删除评论。
* // TODO 删除评论。
* @param commentId - 评论 ID。
*/
async function deleteComment(commentId: number | undefined) {
Expand Down Expand Up @@ -181,7 +303,7 @@
cursor: pointer;
&.active .soft-button {
color: c(accent);
color: c(accent); // FIXME 看起来这些样式并没有被正确绑定到元素上
}
}
Expand Down
35 changes: 24 additions & 11 deletions components/TextEditor/TextEditorRtf.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
const props = defineProps<{
/** 视频 ID。 */
videoId?: number;
videoId: number;
}>();
type ActiveType = string | boolean;
Expand Down Expand Up @@ -87,16 +87,29 @@
/**
* sends comment to the backend.
*/
function sendComment() {
const api = useApi();
const content = editor.value?.getText() ?? ""; // Get plain text currently to avoid web attack.
const utf8Encode = new TextEncoder();
const encodedContent = utf8Encode.encode(content) as unknown as string;
// TODO: video ID
api.comment(0, encodedContent, props.videoId!).then(video => {
// TODO
}).catch(error => console.error(error));
async function sendComment() {
try {
// TODO // WARN 需要对用户输入的文字进行 Base64 编码
const content = editor.value?.getText() ?? ""; // Get plain text currently to avoid web attack.
const emitVideoCommentRequest: EmitVideoCommentRequestDto = {
videoId: props.videoId,
text: content,
};
// TODO 虽然我很想非阻塞地发送评论,但是楼层号必须在评论成功提交给后端后才会获得。emmmm...
const emitVideoCommentResult = await api.videoComment.emitVideoComment(emitVideoCommentRequest);
const videoComment = emitVideoCommentResult.videoComment;
if (emitVideoCommentResult?.success && videoComment) {
useEvent("videoComment:emitVideoComment", videoComment);
const messageDuration = 5000;
useToast("评论发出去咯~", "success", messageDuration); // TODO 使用多语言
} else {
useToast("发送评论失败!", "error"); // TODO 使用多语言
console.error("ERROR", "发送评论失败:请求未成功");
}
} catch (error) {
useToast("发送评论失败!", "error"); // TODO 使用多语言
console.error("ERROR", "发送评论失败!", error);
}
}
/**
Expand Down
3 changes: 2 additions & 1 deletion composables/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as videoComment from "api/Comment/VideoCommentController";
import * as danmaku from "api/Danmaku/DanmakuController";
import * as user from "api/User/UserController";
import * as video from "api/Video/VideoController";

export default {
user, video, danmaku,
user, video, danmaku, videoComment,
};
66 changes: 66 additions & 0 deletions composables/api/Comment/VideoCommentController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { DELETE, GET, POST } from "../Common";
import getCorrectUri from "../Common/getCorrectUri";
import type { CancelVideoCommentDownvoteRequestDto, CancelVideoCommentUpvoteRequestDto, EmitVideoCommentDownvoteRequestDto, EmitVideoCommentDownvoteResponseDto, EmitVideoCommentRequestDto, EmitVideoCommentResponseDto, EmitVideoCommentUpvoteRequestDto, EmitVideoCommentUpvoteResponseDto, GetVideoCommentByKvidRequestDto, GetVideoCommentByKvidResponseDto } from "./VideoCommentControllerDto";

const BACK_END_URL = getCorrectUri();
const USER_API_URI = `${BACK_END_URL}/video/comment`;

/**
* 用户发送视频评论
* @param emitVideoCommentRequest 用户发送的视频评论的请求载荷
* @returns 用户发送视频评论的结果
*/
export const emitVideoComment = async (emitVideoCommentRequest: EmitVideoCommentRequestDto): Promise<EmitVideoCommentResponseDto> => {
// TODO use { credentials: "include" } to allow save/read cookies from cross-origin domains. Maybe we should remove it before deployment to production env.
return await POST(`${USER_API_URI}/emit`, emitVideoCommentRequest, { credentials: "include" }) as EmitVideoCommentResponseDto;
};

/**
* 根据 kvid 获取视频评论列表
* @param getVideoCommentByKvidRequest 请求视频评论列表的查询参数
* @returns 视频的视频评论列表
*/
export const getVideoCommentByKvid = async (getVideoCommentByKvidRequest: GetVideoCommentByKvidRequestDto): Promise<GetVideoCommentByKvidResponseDto> => {
// TODO use { credentials: "include" } to allow save/read cookies from cross-origin domains. Maybe we should remove it before deployment to production env.
return await GET(`${USER_API_URI}?videoId=${getVideoCommentByKvidRequest.videoId}`, { credentials: "include" }) as GetVideoCommentByKvidResponseDto;
};

/**
* 用户为视频评论点赞
* @param emitVideoCommentUpvoteRequest 用户为视频评论点赞的请求载荷
* @returns 用户为视频评论点赞的结果
*/
export const emitVideoCommentUpvote = async (emitVideoCommentUpvoteRequest: EmitVideoCommentUpvoteRequestDto): Promise<EmitVideoCommentUpvoteResponseDto> => {
// TODO use { credentials: "include" } to allow save/read cookies from cross-origin domains. Maybe we should remove it before deployment to production env.
return await POST(`${USER_API_URI}/upvote`, emitVideoCommentUpvoteRequest, { credentials: "include" }) as EmitVideoCommentUpvoteResponseDto;
};

/**
* 用户为视频评论点踩
* @param emitVideoCommentDownvoteRequest 用户为视频评论点踩的请求载荷
* @returns 用户为视频评论点踩的结果
*/
export const emitVideoCommentDownvote = async (emitVideoCommentDownvoteRequest: EmitVideoCommentDownvoteRequestDto): Promise<EmitVideoCommentDownvoteResponseDto> => {
// TODO use { credentials: "include" } to allow save/read cookies from cross-origin domains. Maybe we should remove it before deployment to production env.
return await POST(`${USER_API_URI}/downvote`, emitVideoCommentDownvoteRequest, { credentials: "include" }) as EmitVideoCommentDownvoteResponseDto;
};

/**
* 用户取消一个视频评论的点赞
* @param cancelVideoCommentUpvoteRequest 用户取消一个视频评论的点赞的请求载荷
* @returns 用户取消一个视频评论的点赞的结果
*/
export const cancelVideoCommentUpvote = async (cancelVideoCommentUpvoteRequest: CancelVideoCommentUpvoteRequestDto): Promise<EmitVideoCommentUpvoteResponseDto> => {
// TODO use { credentials: "include" } to allow save/read cookies from cross-origin domains. Maybe we should remove it before deployment to production env.
return await DELETE(`${USER_API_URI}/upvote/cancel`, cancelVideoCommentUpvoteRequest, { credentials: "include" }) as EmitVideoCommentUpvoteResponseDto;
};

/**
* 用户取消一个视频评论的点踩
* @param cancelVideoCommentDownvoteRequest 用户取消一个视频评论的点踩的请求载荷
* @returns 用户取消一个视频评论的点踩的结果
*/
export const cancelVideoCommentDownvote = async (cancelVideoCommentDownvoteRequest: CancelVideoCommentDownvoteRequestDto): Promise<EmitVideoCommentDownvoteResponseDto> => {
// TODO use { credentials: "include" } to allow save/read cookies from cross-origin domains. Maybe we should remove it before deployment to production env.
return await DELETE(`${USER_API_URI}/downvote/cancel`, cancelVideoCommentDownvoteRequest, { credentials: "include" }) as EmitVideoCommentDownvoteResponseDto;
};
Loading

0 comments on commit e65d7c0

Please sign in to comment.