Merge branch 'feat/liujie'

This commit is contained in:
2025-09-18 13:37:44 +08:00
8 changed files with 755 additions and 17 deletions

View File

@@ -0,0 +1,213 @@
.container {
.header {
padding: 20px 20px 0;
.commentCount {
border-bottom: 1px solid rgba(255, 255, 255, 0.10);
padding-bottom: 8px;
color: #FFF;
font-feature-settings: 'liga' off, 'clig' off;
text-overflow: ellipsis;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 24px;
}
.addComment {
height: 40px;
padding: 0 12px;
margin: 12px 0;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 6px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.20);
.addCommentImage {
width: 18px;
height: 18px;
}
.addCommentText {
color: rgba(255, 255, 255, 0.65);
font-feature-settings: 'liga' off, 'clig' off;
text-overflow: ellipsis;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px;
}
}
}
.list {
padding: 12px 20px;
& > .commentItem {
padding-bottom: 12px;
}
.commentItem {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
.avatar {
border-radius: 50%;
}
.contents {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 12px;
.main {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 4px;
.publisherInfo {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 4px;
.nickname {
color: rgba(255, 255, 255, 0.65);
font-feature-settings: 'liga' off, 'clig' off;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px;
}
.role {
padding: 0 4px;
height: 18px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.10);
color: rgba(255, 255, 255, 0.65);
font-feature-settings: 'liga' off, 'clig' off;
font-family: "PingFang SC";
font-size: 10px;
font-style: normal;
font-weight: 500;
line-height: 18px;
}
}
.content {
color: #FFF;
font-feature-settings: 'liga' off, 'clig' off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
.atPeople {
color: rgba(255, 255, 255, 0.45);
}
}
.addons {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 6px;
.time, .location, .reply, .delete {
font-feature-settings: 'liga' off, 'clig' off;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
line-height: 18px;
&.time, &.location {
color: rgba(255, 255, 255, 0.45);
font-weight: 400;
}
&.reply, &.delete {
color: rgba(255, 255, 255, 0.85);
font-weight: 600;
}
}
}
}
.viewMore {
color: #FFF;
font-feature-settings: 'liga' off, 'clig' off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
}
}
}
.inputContainer {
height: 36px;
padding: 12px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
.inputWrapper {
flex: 1;
display: flex;
justify-content: flex-start;
align-items: center;
& > .input {
width: 100%;
}
}
.sendIcon {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #000;
border: 0.5px solid rgba(0, 0, 0, 0.06);
.sendImage {
width: 36px;
height: 36px;
}
}
}
.empty {
height: 40vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
.emptyTip {
color: rgba(255, 255, 255, 0.85);
font-feature-settings: 'liga' off, 'clig' off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 24px;
}
}

View File

@@ -0,0 +1,415 @@
import React, {
useState,
useEffect,
forwardRef,
useRef,
useImperativeHandle,
} from "react";
import { View, Text, Image, Input } from "@tarojs/components";
import Taro from "@tarojs/taro";
import dayjs from "dayjs";
import CommentServices from "@/services/commentServices";
import type {
BaseComment,
Comment,
ReplyComment,
} from "@/services/commentServices";
import { useUserInfo } from "@/store/userStore";
import sendImg from "@/static/detail/icon-sendup.svg";
import addComment from "@/static/detail/icon-write.svg";
import emptyComment from "@/static/emptyStatus/comment-empty.png";
import CommonPopup from "../CommonPopup";
import styles from "./index.module.scss";
const PAGESIZE = 4;
// const PAGESIZE = 1000;
function toast(msg) {
Taro.showToast({ title: msg, icon: "none" });
}
interface CommentInputProps {
onConfirm?: (
value: { content: string } & Partial<CommentInputReplyParamsType>
) => void;
}
// 2⃣ 定义通过 ref 暴露出去的方法类型
interface CommentInputRef {
show: (params?: CommentInputReplyParamsType) => void;
}
interface CommentInputReplyParamsType {
parent_id: number;
reply_to_user_id: number;
nickname: string;
}
const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(function (
props,
ref
) {
const { onConfirm } = props;
const [visible, setVisible] = useState(false);
const [value, setValue] = useState("");
const [params, setParams] = useState<
CommentInputReplyParamsType | undefined
>();
const inputDomRef = useRef(null);
useImperativeHandle(ref, () => ({
show: (_params: CommentInputReplyParamsType | undefined) => {
setVisible(true);
setTimeout(() => {
inputDomRef.current && inputDomRef.current?.focus();
}, 100);
setParams(_params);
},
}));
function handleSend() {
if (!value) {
toast("评论内容不得为空");
return;
}
onConfirm?.({ content: value, ...params });
onClose();
}
function onClose() {
setVisible(false);
setValue("");
inputDomRef.current && inputDomRef.current?.blur();
}
return (
<CommonPopup
visible={visible}
showHeader={false}
hideFooter
zIndex={1002}
onClose={onClose}
style={{ height: "60px!important", minHeight: "unset" }}
enableDragToClose={false}
>
<View className={styles.inputContainer}>
<View className={styles.inputWrapper}>
<Input
ref={inputDomRef}
className={styles.input}
value={value}
onInput={(e) => setValue(e.detail.value)}
placeholder={
params?.reply_to_user_id ? `回复 @${params.nickname}` : "写评论"
}
focus
maxlength={100}
/>
</View>
<View className={styles.sendIcon} onClick={handleSend}>
<Image className={styles.sendImage} src={sendImg} />
</View>
</View>
</CommonPopup>
);
});
function isReplyComment(item: BaseComment<any>): item is ReplyComment {
return "reply_to_user" in item;
}
function getRelativeDay(time) {
const theTime = dayjs(time);
const isThisYear = dayjs().isSame(theTime, "year");
const diffDay = dayjs().startOf("day").diff(theTime, "day");
return diffDay <= 3
? diffDay >= 1
? `${diffDay}天前`
: theTime.format("HH:mm:ss")
: theTime.format(isThisYear ? "MM-DD HH:mm:ss" : "YYYY-MM-DD HH:mm:ss");
}
function CommentItem(props: {
level: number;
publisher_id: number;
comment: Comment | ReplyComment;
loadMore: (c: Comment) => void;
handleReply: (options: CommentInputReplyParamsType) => void;
handleDelete: (options: { parent_id: number | null; id: number }) => void;
}) {
const {
level,
publisher_id,
comment,
loadMore: handleLoadMore,
handleReply,
handleDelete,
} = props;
const currentUserInfo = useUserInfo();
const isGamePublisher = publisher_id === comment.user.id;
const isCommentPublisher = currentUserInfo.id === comment.user.id;
return (
<View className={styles.commentItem} key={comment.id}>
<View style={{ width: level === 1 ? "36px" : "28px" }}>
<Image
className={styles.avatar}
src={comment.user.avatar_url}
mode="aspectFill"
style={
level === 1
? { width: "36px", height: "36px" }
: { width: "28px", height: "28px" }
}
/>
</View>
<View className={styles.contents}>
<View className={styles.main}>
<View className={styles.publisherInfo}>
<View className={styles.nickname}>
<Text>{comment.user.nickname}</Text>
</View>
{isGamePublisher && (
<View className={styles.role}>
<Text></Text>
</View>
)}
</View>
<View className={styles.content}>
<Text className={styles.atPeople}>
{isReplyComment(comment) && comment.reply_to_user
? `@${comment.reply_to_user.nickname} `
: ""}
</Text>
<Text>{comment.content}</Text>
</View>
<View className={styles.addons}>
<View className={styles.time}>
<Text>{getRelativeDay(comment.create_time)}</Text>
</View>
<View className={styles.location}>
<Text></Text>
</View>
<View
className={styles.reply}
onClick={() =>
handleReply({
parent_id: comment.parent_id || comment.id,
reply_to_user_id: comment.user.id,
nickname: comment.user.nickname,
})
}
>
<Text></Text>
</View>
{isGamePublisher || isCommentPublisher}
<View
className={styles.delete}
onClick={() =>
handleDelete({
parent_id: comment.parent_id,
id: comment.id,
})
}
>
<Text></Text>
</View>
</View>
</View>
{!isReplyComment(comment) &&
comment.replies.map((item: ReplyComment) => (
<CommentItem
key={comment.id}
publisher_id={publisher_id}
comment={item}
level={2}
loadMore={handleLoadMore}
handleReply={handleReply}
handleDelete={handleDelete}
/>
))}
{!isReplyComment(comment) &&
comment.replies.length !== comment.reply_count && (
<View
className={styles.viewMore}
onClick={() => handleLoadMore(comment)}
>
</View>
)}
</View>
</View>
);
}
export default forwardRef(function Comments(
props: { game_id: number; publisher_id: number },
ref
) {
const { game_id, publisher_id } = props;
const [comments, setComments] = useState<Comment[]>([]);
const inputRef = useRef<CommentInputRef>(null);
useEffect(() => {
getComments(1);
}, [game_id]);
useImperativeHandle(ref, () => ({
addComment: handleReply,
getCommentCount: () => comments.length,
}));
async function getComments(page) {
if (!game_id) return;
const res = await CommentServices.getComments({
page,
pageSize: PAGESIZE,
game_id,
});
if (res.code === 0) {
const newComments: Comment[] = res.data.rows;
setComments((prev) => {
const res = [...prev];
res.splice(page * PAGESIZE - 1, newComments.length, ...newComments);
return res;
});
}
}
async function getReplies(c: Comment) {
const { replies, id: comment_id } = c;
const page = replies.length < PAGESIZE ? 1 : replies.length / PAGESIZE + 1;
const res = await CommentServices.getReplies({
comment_id,
page,
pageSize: PAGESIZE,
});
if (res.code === 0) {
const newReplies = res.data.rows;
setComments((prev) => {
const newComments = [...prev];
newComments.forEach((item) => {
if (item.id === comment_id) {
item.replies.splice(
page === 1 ? 0 : page * PAGESIZE - 1,
newReplies.length,
...newReplies
);
item.reply_count = res.data.count;
}
});
return newComments;
});
}
}
function handleReply(options?: CommentInputReplyParamsType) {
inputRef.current?.show(options);
}
function onSend({ content, parent_id, reply_to_user_id }) {
if (!parent_id) {
createComment(content);
return;
}
replyComment({ content, parent_id, reply_to_user_id });
}
async function createComment(val: string) {
const res = await CommentServices.createComment({ game_id, content: val });
if (res.code === 0) {
setComments((prev) => {
return [{ ...res.data, replies: [] }, ...prev];
});
toast("发布成功");
}
}
async function replyComment({ parent_id, reply_to_user_id, content }) {
const res = await CommentServices.replyComment({
parent_id,
reply_to_user_id,
content,
});
if (res.code === 0) {
setComments((prev) => {
return prev.map((item) => {
if (item.id === parent_id) {
return {
...item,
replies: [res.data, ...item.replies],
reply_count: item.reply_count + 1,
};
}
return item;
});
});
toast("回复成功");
}
}
async function deleteComment({ parent_id, id }) {
const res = await CommentServices.deleteComment({ comment_id: id });
if (res.code === 0) {
if (parent_id) {
setComments((prev) => {
return prev.map((item) => {
if (item.id === parent_id) {
return {
...item,
replies: item.replies.filter(
(replyItem) => replyItem.id !== id
),
reply_count: item.reply_count - 1,
};
}
return item;
});
});
} else {
setComments((prev) => {
console.log(prev, parent_id, id);
return prev.filter((item) => item.id !== id);
});
}
toast("评论已删除");
}
}
return (
<View className={styles.container}>
<View className={styles.header}>
<View className={styles.commentCount}>
{comments.length > 0 ? `${comments.length}` : ""}
</View>
<View className={styles.addComment} onClick={() => handleReply()}>
<Image className={styles.addCommentImage} src={addComment} />
<Text className={styles.addCommentText}></Text>
</View>
</View>
{comments.length > 0 ? (
<View className={styles.list}>
{comments.map((comment) => {
return (
<CommentItem
key={comment.id}
publisher_id={publisher_id}
level={1}
comment={comment}
loadMore={getReplies}
handleReply={handleReply}
handleDelete={deleteComment}
/>
);
})}
</View>
) : (
<View className={styles.empty}>
<Image className={styles.emptyImage} src={emptyComment} />
<Text className={styles.emptyTip}></Text>
</View>
)}
<CommentInput ref={inputRef} onConfirm={onSend} />
</View>
);
});

View File

@@ -32,7 +32,7 @@ const CancelPopup = forwardRef((props, ref) => {
onFinish.current = onAct; onFinish.current = onAct;
setVisible(true); setVisible(true);
setTimeout(() => { setTimeout(() => {
inputRef.current.focus(); inputRef.current && inputRef.current.focus();
}, 0); }, 0);
}, },
})); }));
@@ -87,6 +87,7 @@ const CancelPopup = forwardRef((props, ref) => {
focus focus
value={cancelReason} value={cancelReason}
onInput={(e) => setCancelReason(e.detail.value)} onInput={(e) => setCancelReason(e.detail.value)}
maxlength={100}
/> />
</View> </View>
)} )}

View File

@@ -19,6 +19,7 @@ import NTRPEvaluatePopup from "./NTRPEvaluatePopup";
import RefundPopup from "./refundPopup"; import RefundPopup from "./refundPopup";
import GameManagePopup from './GameManagePopup'; import GameManagePopup from './GameManagePopup';
import FollowUserCard from './FollowUserCard/index'; import FollowUserCard from './FollowUserCard/index';
import Comments from "./Comments";
export { export {
ActivityTypeSwitch, ActivityTypeSwitch,
@@ -43,4 +44,5 @@ export {
RefundPopup, RefundPopup,
GameManagePopup, GameManagePopup,
FollowUserCard, FollowUserCard,
Comments,
}; };

View File

@@ -6,7 +6,7 @@ import React, {
forwardRef, forwardRef,
} from "react"; } from "react";
import { View, Text, Image, Map, ScrollView } from "@tarojs/components"; import { View, Text, Image, Map, ScrollView } from "@tarojs/components";
import { Avatar } from "@nutui/nutui-react-taro"; // import { Avatar } from "@nutui/nutui-react-taro";
import Taro, { import Taro, {
useRouter, useRouter,
useShareAppMessage, useShareAppMessage,
@@ -22,6 +22,7 @@ import {
withAuth, withAuth,
NTRPEvaluatePopup, NTRPEvaluatePopup,
GameManagePopup, GameManagePopup,
Comments,
} from "@/components"; } from "@/components";
import { import {
EvaluateType, EvaluateType,
@@ -40,6 +41,7 @@ dayjs.locale("zh-cn");
// 将·作为连接符插入到标签文本之间 // 将·作为连接符插入到标签文本之间
function insertDotInTags(tags: string[]) { function insertDotInTags(tags: string[]) {
if (!tags) return []
return tags.join("-·-").split("-"); return tags.join("-·-").split("-");
} }
@@ -104,7 +106,6 @@ function Coursel(props) {
async function getImagesMsg(imageList) { async function getImagesMsg(imageList) {
const latest_list: CourselItemType[] = []; const latest_list: CourselItemType[] = [];
const sys_info = await Taro.getSystemInfo(); const sys_info = await Taro.getSystemInfo();
console.log(sys_info, "info");
const max_width = sys_info.screenWidth - 30; const max_width = sys_info.screenWidth - 30;
const max_height = 240; const max_height = 240;
const current_aspect_ratio = max_width / max_height; const current_aspect_ratio = max_width / max_height;
@@ -239,7 +240,7 @@ function isFull (counts) {
// 底部操作栏 // 底部操作栏
function StickyButton(props) { function StickyButton(props) {
const { handleShare, handleJoinGame, detail, onStatusChange } = props; const { handleShare, handleJoinGame, detail, onStatusChange, handleAddComment, getCommentCount } = props;
const ntrpRef = useRef(null); const ntrpRef = useRef(null);
const { id, price, user_action_status, match_status, start_time, end_time, is_organizer } = const { id, price, user_action_status, match_status, start_time, end_time, is_organizer } =
detail || {}; detail || {};
@@ -363,6 +364,8 @@ function StickyButton(props) {
}; };
} }
const commentCount = getCommentCount()
return ( return (
<> <>
<View className="sticky-bottom-bar"> <View className="sticky-bottom-bar">
@@ -378,14 +381,15 @@ function StickyButton(props) {
<View <View
className="sticky-bottom-bar-comment" className="sticky-bottom-bar-comment"
onClick={() => { onClick={() => {
Taro.showToast({ title: "To be continued", icon: "none" }); // Taro.showToast({ title: "To be continued", icon: "none" });
handleAddComment()
}} }}
> >
<Image <Image
className="sticky-bottom-bar-comment-icon" className="sticky-bottom-bar-comment-icon"
src={img.ICON_DETAIL_COMMENT_DARK} src={img.ICON_DETAIL_COMMENT_DARK}
/> />
<Text className="sticky-bottom-bar-comment-text">32</Text> <Text className="sticky-bottom-bar-comment-text">{commentCount > 0 ? commentCount : '评论'}</Text>
</View> </View>
</View> </View>
<View className={classnames("detail-main-action", available ? '' : 'disabled')}> <View className={classnames("detail-main-action", available ? '' : 'disabled')}>
@@ -580,7 +584,7 @@ function VenueInfo(props) {
function previewImage(current_url) { function previewImage(current_url) {
Taro.previewImage({ Taro.previewImage({
current: current_url, current: current_url,
urls: venue_image_list.map((c) => c.url), urls: venue_image_list?.length > 0 ? venue_image_list.map((c) => c.url) : [],
}); });
} }
return ( return (
@@ -629,7 +633,7 @@ function VenueInfo(props) {
<View className="venue-screenshot-title"></View> <View className="venue-screenshot-title"></View>
<ScrollView scrollY className="venue-screenshot-scroll-view"> <ScrollView scrollY className="venue-screenshot-scroll-view">
<View className="venue-screenshot-image-list"> <View className="venue-screenshot-image-list">
{venue_image_list.map((item) => { {venue_image_list?.length > 0 && venue_image_list.map((item) => {
return ( return (
<View <View
className="venue-screenshot-image-item" className="venue-screenshot-image-item"
@@ -651,7 +655,6 @@ function VenueInfo(props) {
} }
function genNTRPRequirementText(min, max) { function genNTRPRequirementText(min, max) {
console.log(min, max, "ntrp");
if (min && max && min !== max) { if (min && max && min !== max) {
return `${min} - ${max} 之间`; return `${min} - ${max} 之间`;
} else if (max === "1") { } else if (max === "1") {
@@ -798,7 +801,7 @@ function Participants(props) {
function SupplementalNotes(props) { function SupplementalNotes(props) {
const { const {
detail: { description, description_tag = [] }, detail: { description, description_tag },
} = props; } = props;
return ( return (
<View className="detail-page-content-supplemental-notes"> <View className="detail-page-content-supplemental-notes">
@@ -808,7 +811,7 @@ function SupplementalNotes(props) {
<View className="supplemental-notes-content"> <View className="supplemental-notes-content">
{/* supplemental notes tags */} {/* supplemental notes tags */}
<View className="supplemental-notes-content-tags"> <View className="supplemental-notes-content-tags">
{insertDotInTags(description_tag).map((tag, index) => ( {insertDotInTags(description_tag || []).map((tag, index) => (
<View key={index} className="supplemental-notes-content-tags-tag"> <View key={index} className="supplemental-notes-content-tags-tag">
<Text>{tag}</Text> <Text>{tag}</Text>
</View> </View>
@@ -873,6 +876,7 @@ function OrganizerInfo(props) {
currentLocation: location, currentLocation: location,
onUpdateUserInfo = () => {}, onUpdateUserInfo = () => {},
handleViewUserInfo, handleViewUserInfo,
handleAddComment,
} = props; } = props;
const { const {
id, id,
@@ -956,7 +960,7 @@ function OrganizerInfo(props) {
)} )}
</View> </View>
)} )}
<View className="organizer-actions-comment"> <View className="organizer-actions-comment" onClick={() => handleAddComment()}>
<Image <Image
className="organizer-actions-comment-icon" className="organizer-actions-comment-icon"
src={img.ICON_DETAIL_COMMENT} src={img.ICON_DETAIL_COMMENT}
@@ -1056,6 +1060,7 @@ function Index() {
const isMyOwn = userInfo.id === myInfo.id; const isMyOwn = userInfo.id === myInfo.id;
const sharePopupRef = useRef<any>(null); const sharePopupRef = useRef<any>(null);
const commentRef = useRef();
useDidShow(async () => { useDidShow(async () => {
await updateLocation(); await updateLocation();
@@ -1084,11 +1089,17 @@ function Index() {
const fetchDetail = async () => { const fetchDetail = async () => {
if (!id) return; if (!id) return;
try {
const res = await DetailService.getDetail(Number(id)); const res = await DetailService.getDetail(Number(id));
if (res.code === 0) { if (res.code === 0) {
setDetail(res.data); setDetail(res.data);
fetchUserInfoById(res.data.publisher_id); fetchUserInfoById(res.data.publisher_id);
} }
} catch (e) {
if (e.message === '球局不存在') {
handleBack()
}
}
}; };
const onUpdateUserInfo = () => { const onUpdateUserInfo = () => {
@@ -1139,7 +1150,6 @@ function Index() {
navto(`/user_pages/other/index?userid=${userId}`); navto(`/user_pages/other/index?userid=${userId}`);
} }
console.log("detail", detail);
const backgroundImage = detail?.image_list?.[0] const backgroundImage = detail?.image_list?.[0]
? { backgroundImage: `url(${detail?.image_list?.[0]})` } ? { backgroundImage: `url(${detail?.image_list?.[0]})` }
: {}; : {};
@@ -1199,13 +1209,17 @@ function Index() {
currentLocation={currentLocation} currentLocation={currentLocation}
onUpdateUserInfo={onUpdateUserInfo} onUpdateUserInfo={onUpdateUserInfo}
handleViewUserInfo={handleViewUserInfo} handleViewUserInfo={handleViewUserInfo}
handleAddComment={() => { commentRef.current && commentRef.current.addComment() }}
/> />
<Comments ref={commentRef} game_id={Number(id)} publisher_id={Number(detail.publisher_id)} />
{/* sticky bottom action bar */} {/* sticky bottom action bar */}
<StickyButton <StickyButton
handleShare={handleShare} handleShare={handleShare}
handleJoinGame={handleJoinGame} handleJoinGame={handleJoinGame}
detail={detail} detail={detail}
onStatusChange={onStatusChange} onStatusChange={onStatusChange}
handleAddComment={() => { commentRef.current && commentRef.current.addComment() }}
getCommentCount={() => commentRef.current && commentRef.current.getCommentCount()}
/> />
{/* share popup */} {/* share popup */}
<SharePopup <SharePopup

View File

@@ -0,0 +1,85 @@
import httpService from "./httpService";
import type { ApiResponse } from "./httpService";
export interface CommentResponse {
rows: Comment[]
count: number
}
export interface UserInfo {
id: number
nickname: string
avatar_url: string
}
export type BaseComment<T = {}> = {
create_time: string
last_modify_time: string
id: number
game_id: number
user_id: number
parent_id: number | null
reply_to_user_id: number | null
content: string
like_count: number
reply_count: number
user: UserInfo
is_liked?: boolean
} & T
export type ReplyComment = BaseComment<{
parent_id: number
reply_to_user_id: number
reply_to_user: UserInfo
}>
export type Comment = BaseComment<{
replies: ReplyComment[]
}>
// 接口响应
export interface ReplyCommentResponse {
count: number
rows: ReplyComment[]
}
export interface ToggleLikeType {
is_liked: boolean,
like_count: number,
message: string
}
// 评论管理类
class CommentService {
// 查询评论列表
async getComments(req: { game_id: number, page: number, pageSize: number }): Promise<ApiResponse<CommentResponse>> {
return httpService.post("/comments/list", req, { showLoading: true });
}
// 发表评论
async createComment(req: { game_id: number, content: string }): Promise<ApiResponse<BaseComment>> {
return httpService.post("/comments/create", req, { showLoading: true });
}
// 回复评论
async replyComment(req: { parent_id: number, reply_to_user_id: number, content: string }): Promise<ApiResponse<ReplyComment>> {
return httpService.post("/comments/reply", req, { showLoading: true });
}
// 点赞取消点赞评论
async toggleLike(req: { comment_id: number }): Promise<ApiResponse<ToggleLikeType>> {
return httpService.post("/comments/like", req, { showLoading: true });
}
// 删除评论
async deleteComment(req: { comment_id: number }): Promise<ApiResponse<any>> {
return httpService.post("/comments/delete", req, { showLoading: true });
}
// 获取评论的所有回复
async getReplies(req: { comment_id: number, page: number, pageSize: number }): Promise<ApiResponse<ReplyCommentResponse>> {
return httpService.post("/comments/replies", req, { showLoading: true });
}
}
export default new CommentService();

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0042 6.05029V18" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 12L12 6L18 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.625 15.75H16.125" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.125 10.02V12.75H6.86895L14.625 4.99054L11.8857 2.25L4.125 10.02Z" stroke="white" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B