index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import { Text, View, ScrollView } from "@tarojs/components";
  2. import { Heart, HeartFill, Plus } from "@nutui/icons-react-taro";
  3. import { Message } from "@nutui/icons-react-taro";
  4. import avatar from "../../asset/img/avatar.png";
  5. import {
  6. Grid,
  7. GridItem,
  8. Image,
  9. ImagePreview,
  10. Toast,
  11. } from "@nutui/nutui-react-taro";
  12. import { createPortal } from "react-dom";
  13. import MoreButton from "../fllower-button";
  14. import { circlePostListApi } from "../../api/post";
  15. import {
  16. useEffect,
  17. useState,
  18. useCallback,
  19. useImperativeHandle,
  20. forwardRef,
  21. } from "react";
  22. import { strSlice } from "../../utils/index";
  23. import { followApi, interactionApi, unfollowApi } from "../../api/interaction";
  24. import { Dialog } from "@nutui/nutui-react-taro";
  25. import { getStorageSync, navigateTo } from "@tarojs/taro";
  26. interface PostListProps {
  27. style?: any;
  28. }
  29. export interface PostListRef {
  30. loadMore: () => void;
  31. refresh: () => void;
  32. hasMore: boolean;
  33. loading: boolean;
  34. }
  35. const PostList = forwardRef<PostListRef, PostListProps>(({ style }, ref) => {
  36. const [postList, setPostList] = useState<any[]>([]);
  37. const [showPreview, setShowPreview] = useState(false);
  38. const [init, setInit] = useState(0);
  39. const [mediaUrl, setMediaUrl] = useState<any[]>([]);
  40. // 分页相关状态
  41. const [currentPage, setCurrentPage] = useState(1);
  42. const [hasMore, setHasMore] = useState(true);
  43. const [loading, setLoading] = useState(false);
  44. const [refreshing, setRefreshing] = useState(false);
  45. const pageSize = 10;
  46. const getPostList = useCallback(
  47. async (page = 1, isRefresh = false) => {
  48. if (isRefresh) {
  49. setRefreshing(true);
  50. } else {
  51. setLoading(true);
  52. }
  53. try {
  54. let circleInfo = getStorageSync("circleInfo");
  55. const res: any = await circlePostListApi({
  56. circleId: circleInfo.id,
  57. page,
  58. pageSize: pageSize,
  59. });
  60. if (res.code === 200) {
  61. const newList = res.data.list || [];
  62. if (isRefresh || page === 1) {
  63. setPostList(newList);
  64. } else {
  65. setPostList((prev) => [...prev, ...newList]);
  66. }
  67. // 判断是否还有更多数据
  68. setHasMore(res.data.has_more);
  69. setCurrentPage(page);
  70. }
  71. } catch (error) {
  72. Toast.show("load_error", {
  73. content: "加载失败,请重试",
  74. duration: 2,
  75. });
  76. } finally {
  77. setLoading(false);
  78. setRefreshing(false);
  79. }
  80. },
  81. [pageSize]
  82. );
  83. // 下拉刷新
  84. const handleRefresh = useCallback(async () => {
  85. await getPostList(1, true);
  86. }, [getPostList]);
  87. // 上拉加载更多
  88. const handleLoadMore = useCallback(async () => {
  89. if (hasMore && !loading && !refreshing) {
  90. await getPostList(currentPage + 1, false);
  91. }
  92. }, [hasMore, currentPage, getPostList, loading, refreshing]);
  93. // ScrollView 滚动到底部事件
  94. const handleScrollToLower = useCallback(() => {
  95. handleLoadMore();
  96. }, [handleLoadMore]);
  97. // ScrollView 下拉刷新事件
  98. const handleRefresherRefresh = useCallback(async () => {
  99. await handleRefresh();
  100. }, [handleRefresh]);
  101. const handleMoreClick = (postIndex: number) => {
  102. const data = [...postList];
  103. data.splice(postIndex, 1);
  104. setPostList(data);
  105. };
  106. // 点赞
  107. const handleInteraction = async (postId: number, type) => {
  108. const res: any = await interactionApi({
  109. target_type: "post",
  110. target_id: postId,
  111. interaction_type: type,
  112. });
  113. if (res.code === 200) {
  114. const data = [...postList];
  115. data.forEach((item: any) => {
  116. if (item.id === postId) {
  117. item.is_praised = type === "praise" ? true : false;
  118. item.like_count =
  119. type === "praise" ? item.like_count + 1 : item.like_count - 1;
  120. }
  121. });
  122. console.log(data);
  123. setPostList(data);
  124. }
  125. };
  126. // 关注/不关注
  127. const handleFollowClick = (followeeId: number, is_followed) => {
  128. if (is_followed) {
  129. unfollowApi({ followee_id: followeeId }).then(() => {
  130. Toast.show("follow_success", {
  131. content: "取消关注成功",
  132. duration: 2,
  133. lockScroll: true,
  134. });
  135. const data = [...postList];
  136. data.forEach((item: any) => {
  137. item.is_followed = false;
  138. });
  139. setPostList(data);
  140. Dialog.close("followed_dialog");
  141. });
  142. } else {
  143. followApi({ followee_id: followeeId }).then(() => {
  144. Toast.show("follow_success", {
  145. content: "关注成功",
  146. duration: 2,
  147. lockScroll: true,
  148. });
  149. const data = [...postList];
  150. data.forEach((item: any) => {
  151. item.is_followed = true;
  152. });
  153. setPostList(data);
  154. });
  155. }
  156. };
  157. // 暴露方法给父组件
  158. useImperativeHandle(ref, () => ({
  159. loadMore: handleLoadMore,
  160. refresh: handleRefresh,
  161. hasMore,
  162. loading,
  163. }));
  164. useEffect(() => {
  165. getPostList(1, true);
  166. }, [getPostList]);
  167. return (
  168. <>
  169. <ScrollView
  170. className="post-list bg-[#f8f8f8]"
  171. scrollY
  172. refresherEnabled
  173. refresherTriggered={refreshing}
  174. onRefresherRefresh={handleRefresherRefresh}
  175. onScrollToLower={handleScrollToLower}
  176. lowerThreshold={50}
  177. style={style}
  178. >
  179. {postList.map((item: any, index: number) => {
  180. return (
  181. <View
  182. key={item.id}
  183. className="post-list-item bg-[#fff] p-[10px] mb-[10px]"
  184. >
  185. <View
  186. className="flex items-center justify-between"
  187. onClick={() => {
  188. navigateTo({
  189. url: `/pages/self/index?id=${item.user_id}`,
  190. });
  191. }}
  192. >
  193. <View className="flex items-center">
  194. <View
  195. className="w-[40px] h-[40px] rounded-[50%] bg-[#000] overflow-hidden bg-cover bg-center"
  196. style={{
  197. backgroundImage: item.user.avatar_url
  198. ? `url('${item.user.avatar_url}')`
  199. : `url('${avatar}')`,
  200. }}
  201. ></View>
  202. <View className="ml-[5px]">
  203. <View className="text-[14px] font-[800] text-[#9c9dee]">
  204. {item.user.nickname || item.user.username || "微信用户"}
  205. </View>
  206. {item.user.is_official ? (
  207. <View className="text-[12px] text-[#949494]">
  208. 官方账号
  209. </View>
  210. ) : (
  211. <></>
  212. )}
  213. </View>
  214. </View>
  215. <View className="flex items-center">
  216. <View
  217. className="flex items-center justify-center mr-[12px] text-[12px] bg-[#f8f8f8] text-[#949494] rounded-[20px] h-[25px] w-[60px] text-center"
  218. onClick={(e) => {
  219. e.stopPropagation();
  220. handleFollowClick(item.user_id, item.is_followed);
  221. }}
  222. >
  223. {!item.is_followed && <Plus color="#1874d0" />}
  224. <Text>{item.is_followed ? "已关注" : "关注"}</Text>
  225. </View>
  226. {!item.user.is_official && (
  227. <MoreButton
  228. postIndex={index}
  229. followeeId={item.user_id}
  230. postId={item.id}
  231. postItem={item}
  232. onClick={handleMoreClick}
  233. />
  234. )}
  235. </View>
  236. </View>
  237. <View
  238. onClick={() =>
  239. navigateTo({ url: `/pages/detail/index?id=${item.id}` })
  240. }
  241. >
  242. <View className="text-[14px] text-[#333] pt-[20px]">
  243. {item.type === "1" ? (
  244. <>
  245. {strSlice(item.content, 100)}
  246. {item.content.length > 100 && (
  247. <Text className="text-[#1874d0]">全文</Text>
  248. )}
  249. </>
  250. ) : (
  251. <>
  252. <View
  253. className="text-[14px] text-[#333]"
  254. dangerouslySetInnerHTML={{
  255. __html: strSlice(item.content, 100),
  256. }}
  257. ></View>
  258. {item.content.length > 100 && (
  259. <Text className="text-[#1874d0]">全文</Text>
  260. )}
  261. </>
  262. )}
  263. </View>
  264. <View className="mt-[10px]">
  265. <Grid
  266. columns={item.media_url.length < 3 ? 2 : 3}
  267. gap={7}
  268. style={
  269. {
  270. "--nutui-grid-item-content-padding": "0px",
  271. "--nutui-grid-item-content-margin": "0px",
  272. "--nutui-grid-border-color": "transparent",
  273. } as any
  274. }
  275. >
  276. {item.media_url.map((ite: any, index: number) => {
  277. return (
  278. <GridItem>
  279. <Image
  280. src={ite.src}
  281. mode="scaleToFill"
  282. onClick={(e) => {
  283. e.preventDefault();
  284. e.stopPropagation();
  285. setMediaUrl(item.media_url);
  286. setInit(index + 1);
  287. setShowPreview(true);
  288. }}
  289. />
  290. </GridItem>
  291. );
  292. })}
  293. </Grid>
  294. </View>
  295. <View className="mt-[10px] pl-[20px] pr-[20px] flex items-center justify-between">
  296. <View className="flex items-center">
  297. <Message size={16} color="#949494" />
  298. <Text className="ml-[5px] text-[16px] text-[#949494]">
  299. {item.comment_count}
  300. </Text>
  301. </View>
  302. <View
  303. className="flex items-center"
  304. onClick={(e) => {
  305. e.stopPropagation();
  306. handleInteraction(
  307. item.id,
  308. item.is_praised ? "dispraise" : "praise"
  309. );
  310. }}
  311. >
  312. {item.is_praised ? (
  313. <HeartFill size={16} color="#ec4342" />
  314. ) : (
  315. <Heart size={16} color="#949494" />
  316. )}
  317. <Text className="ml-[5px] text-[16px] text-[#949494]">
  318. {item.like_count}
  319. </Text>
  320. </View>
  321. </View>
  322. </View>
  323. </View>
  324. );
  325. })}
  326. {/* 空状态提示 */}
  327. {postList.length === 0 && !loading && (
  328. <View className="flex flex-col items-center justify-center py-[50px]">
  329. <Text className="text-[#999] text-[14px]">暂无帖子</Text>
  330. </View>
  331. )}
  332. {/* 加载更多提示 */}
  333. {hasMore && (
  334. <View className="p-[20px] text-center">
  335. <Text className="text-[14px] text-[#999]">
  336. {loading ? "加载中..." : "上拉加载更多"}
  337. </Text>
  338. </View>
  339. )}
  340. {!hasMore && postList.length > 0 && (
  341. <View className="p-[20px] text-center">
  342. <Text className="text-[14px] text-[#999]">没有更多了</Text>
  343. </View>
  344. )}
  345. </ScrollView>
  346. <Toast id="follow_success" />
  347. <Toast id="load_error" />
  348. <Dialog id="followed_dialog" />
  349. {showPreview &&
  350. createPortal(
  351. <ImagePreview
  352. images={mediaUrl}
  353. visible={showPreview}
  354. defaultValue={init}
  355. onClose={() => setShowPreview(false)}
  356. indicator
  357. />,
  358. document.body
  359. )}
  360. </>
  361. );
  362. });
  363. PostList.displayName = "PostList";
  364. export default PostList;