Merge branch 'feat/liujie'
This commit is contained in:
213
src/components/Comments/index.module.scss
Normal file
213
src/components/Comments/index.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
415
src/components/Comments/index.tsx
Normal file
415
src/components/Comments/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,10 +1089,16 @@ function Index() {
|
|||||||
|
|
||||||
const fetchDetail = async () => {
|
const fetchDetail = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
const res = await DetailService.getDetail(Number(id));
|
try {
|
||||||
if (res.code === 0) {
|
const res = await DetailService.getDetail(Number(id));
|
||||||
setDetail(res.data);
|
if (res.code === 0) {
|
||||||
fetchUserInfoById(res.data.publisher_id);
|
setDetail(res.data);
|
||||||
|
fetchUserInfoById(res.data.publisher_id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '球局不存在') {
|
||||||
|
handleBack()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
85
src/services/commentServices.ts
Normal file
85
src/services/commentServices.ts
Normal 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();
|
||||||
4
src/static/detail/icon-sendup.svg
Normal file
4
src/static/detail/icon-sendup.svg
Normal 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 |
4
src/static/detail/icon-write.svg
Normal file
4
src/static/detail/icon-write.svg
Normal 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 |
Reference in New Issue
Block a user