ith5 4 ماه پیش
والد
کامیت
f974992acb

+ 0 - 7
src/app.scss

@@ -9,11 +9,4 @@
   box-sizing: border-box;
 }
 
-page {
-  margin: 0;
-  padding: 0;
-  height: 100vh;
-  overflow: hidden;
-}
-
 // 您的自定义样式可以在这里添加

+ 20 - 0
src/components/custom-image/index.scss

@@ -0,0 +1,20 @@
+.custom-image-wrapper {
+  width: 100%;
+  height: 100%;
+
+  // 确保 NutUI Image 组件不会应用 webkit-mask 样式
+  .nut-image {
+    -webkit-mask: none !important;
+    mask: none !important;
+    -webkit-mask-image: none !important;
+    mask-image: none !important;
+  }
+
+  // 确保图片正常显示
+  .nut-image img {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+}

+ 39 - 0
src/components/custom-image/index.tsx

@@ -0,0 +1,39 @@
+import { Image as NutImage } from "@nutui/nutui-react-taro";
+import { View } from "@tarojs/components";
+import { useEffect, useRef } from "react";
+import "./index.scss";
+
+interface CustomImageProps {
+  src: string;
+  width?: string | number;
+  height?: string | number;
+  mode?: string;
+  onClick?: (e: any) => void;
+  style?: any;
+  className?: string;
+}
+
+const CustomImage = (props: CustomImageProps) => {
+  const imageRef = useRef<any>(null);
+
+  useEffect(() => {
+    // 在组件挂载后,移除可能存在的 webkit-mask 样式
+    if (imageRef.current) {
+      const element = imageRef.current;
+      if (element.style) {
+        element.style.webkitMask = "none";
+        element.style.mask = "none";
+        element.style.webkitMaskImage = "none";
+        element.style.maskImage = "none";
+      }
+    }
+  }, []);
+
+  return (
+    <View ref={imageRef} className="custom-image-wrapper">
+      <NutImage {...props} />
+    </View>
+  );
+};
+
+export default CustomImage;

+ 354 - 0
src/components/post-list/index copy.tsx

@@ -0,0 +1,354 @@
+import { Text, View, ScrollView } from "@tarojs/components";
+import { Heart, HeartFill, Plus } from "@nutui/icons-react-taro";
+import { Message } from "@nutui/icons-react-taro";
+import {
+  Grid,
+  GridItem,
+  Image,
+  ImagePreview,
+  Toast,
+  PullToRefresh,
+  Sticky,
+} from "@nutui/nutui-react-taro";
+import MoreButton from "../fllower-button";
+import { circlePostListApi } from "../../api/post";
+import { useEffect, useState, useCallback, useRef } from "react";
+import { strSlice } from "../../utils/index";
+import { followApi, interactionApi, unfollowApi } from "../../api/interaction";
+import { Dialog } from "@nutui/nutui-react-taro";
+import { navigateTo } from "@tarojs/taro";
+
+const PostList = () => {
+  const [postList, setPostList] = useState<any[]>([]);
+  const [showPreview, setShowPreview] = useState(false);
+  const [init, setInit] = useState(0);
+  const [mediaUrl, setMediaUrl] = useState<any[]>([]);
+  const containerTopRef = useRef(null);
+  // 分页相关状态
+  const [currentPage, setCurrentPage] = useState(1);
+  const [hasMore, setHasMore] = useState(true);
+  const [loading, setLoading] = useState(false);
+  const pageSize = 10;
+
+  const getPostList = useCallback(
+    async (page = 1, isRefresh = false) => {
+      setLoading(true);
+      try {
+        const res: any = await circlePostListApi({
+          circleId: 1,
+          page,
+          pageSize: pageSize,
+        });
+
+        if (res.code === 200) {
+          const newList = res.data.list || [];
+          console.log("API Response:", {
+            page,
+            newListLength: newList.length,
+            hasMore: res.data.has_more,
+            isRefresh,
+          });
+
+          if (isRefresh || page === 1) {
+            setPostList(newList);
+          } else {
+            setPostList((prev) => [...prev, ...newList]);
+          }
+
+          // 判断是否还有更多数据
+          setHasMore(res.data.has_more);
+          setCurrentPage(page);
+        }
+      } catch (error) {
+        console.error("获取帖子列表失败:", error);
+        Toast.show("load_error", {
+          content: "加载失败,请重试",
+          duration: 2,
+        });
+      } finally {
+        setLoading(false);
+      }
+    },
+    [pageSize]
+  );
+
+  // 下拉刷新
+  const handleRefresh = useCallback(async () => {
+    await getPostList(1, true);
+  }, [getPostList]);
+
+  // 上拉加载更多
+  const handleLoadMore = useCallback(async () => {
+    console.log("handleLoadMore called", { hasMore, currentPage, loading });
+    if (hasMore && !loading) {
+      console.log("Loading next page:", currentPage + 1);
+      await getPostList(currentPage + 1, false);
+    }
+  }, [hasMore, currentPage, getPostList, loading]);
+
+  const handleMoreClick = (postIndex: number) => {
+    const data = [...postList];
+    data.splice(postIndex, 1);
+    setPostList(data);
+  };
+
+  // 点赞
+  const handleInteraction = async (postId: number, type) => {
+    const res: any = await interactionApi({
+      target_type: "post",
+      target_id: postId,
+      interaction_type: type,
+    });
+    if (res.code === 200) {
+      const data = [...postList];
+      data.forEach((item: any) => {
+        if (item.id === postId) {
+          item.interaction_type = type;
+          item.like_count =
+            type === "praise" ? item.like_count + 1 : item.like_count - 1;
+        }
+      });
+      setPostList(data);
+    }
+  };
+
+  // 关注/不关注
+  const handleFollowClick = (followeeId: number, is_followed) => {
+    if (is_followed) {
+      Dialog.open("followed_dialog", {
+        title: "提示",
+        content: "确定取消关注吗?",
+        onConfirm: () => {
+          unfollowApi({ followee_id: followeeId }).then(() => {
+            Toast.show("follow_success", {
+              content: "取消关注成功",
+              duration: 2,
+              lockScroll: true,
+            });
+            const data = [...postList];
+            data.forEach((item: any) => {
+              item.is_followed = false;
+            });
+            setPostList(data);
+            Dialog.close("followed_dialog");
+          });
+        },
+        onCancel: () => {
+          Dialog.close("followed_dialog");
+        },
+        onClose: () => {
+          Dialog.close("followed_dialog");
+        },
+      });
+    } else {
+      followApi({ followee_id: followeeId }).then(() => {
+        Toast.show("follow_success", {
+          content: "关注成功",
+          duration: 2,
+          lockScroll: true,
+        });
+        const data = [...postList];
+        data.forEach((item: any) => {
+          item.is_followed = true;
+        });
+
+        setPostList(data);
+      });
+    }
+  };
+  useEffect(() => {
+    getPostList(1, true);
+  }, []);
+
+  return (
+    // <ScrollView
+    //   scrollY
+    //   className="post-list-scroll"
+    //   style={{ height: "calc(100vh - 100px)" }}
+    //   onScrollToLower={handleLoadMore}
+    //   lowerThreshold={200}
+    // >
+    <View>
+      {/* <PullToRefresh onRefresh={handleRefresh}> */}
+      <View className="post-list bg-[#f8f8f8]">
+        {postList.map((item: any, index: number) => {
+          return (
+            <View
+              key={item.id}
+              className="post-list-item  bg-[#fff] p-[10px] mb-[10px]"
+            >
+              <View
+                className="flex items-center justify-between"
+                onClick={() => {
+                  navigateTo({
+                    url: `/pages/self/index?id=${item.user_id}`,
+                  });
+                }}
+              >
+                <View className="flex items-center">
+                  <View
+                    className="w-[40px] h-[40px] rounded-[50%] bg-[#000] overflow-hidden bg-cover bg-center"
+                    style={{
+                      backgroundImage: item.user.avatar_url
+                        ? `url('${item.user.avatar_url}')`
+                        : "none",
+                    }}
+                  ></View>
+                  <View className="ml-[5px]">
+                    <View className="text-[14px] font-[800] text-[#9c9dee]">
+                      {item.user.nickname || item.user.username || "微信用户"}
+                    </View>
+                    {item.user.is_official ? (
+                      <View className="text-[12px] text-[#949494]">
+                        《心动女友》官方账号
+                      </View>
+                    ) : (
+                      <></>
+                    )}
+                  </View>
+                </View>
+                <View className="flex items-center">
+                  <View
+                    className="flex items-center justify-center mr-[12px] text-[12px] bg-[#f8f8f8] text-[#949494] rounded-[20px] h-[25px] w-[60px] text-center"
+                    onClick={(e) => {
+                      e.stopPropagation();
+                      handleFollowClick(item.user_id, item.is_followed);
+                    }}
+                  >
+                    {item.is_followed ? <></> : <Plus color="#1874d0" />}
+                    <Text>{item.is_followed ? "已关注" : "关注"}</Text>
+                  </View>
+                  {item.user.is_official ? (
+                    <></>
+                  ) : (
+                    <MoreButton
+                      postIndex={index}
+                      followeeId={item.user_id}
+                      postId={item.id}
+                      postItem={item}
+                      onClick={handleMoreClick}
+                    />
+                  )}
+                </View>
+              </View>
+              <View
+                onClick={() =>
+                  navigateTo({ url: `/pages/detail/index?id=${item.id}` })
+                }
+              >
+                <View className="text-[14px] text-[#333] pt-[20px]">
+                  {strSlice(item.content, 100)}
+                  {item.content.length > 100 && (
+                    <Text className="text-[#1874d0]">全文</Text>
+                  )}
+                </View>
+                <View className="mt-[10px]">
+                  <Grid
+                    columns={item.media_url.length < 3 ? 2 : 3} // 这里,如果图片小于3张,则columns为2,如果图片大于等于3张,则columns为3
+                    gap={7}
+                    style={
+                      {
+                        "--nutui-grid-item-content-padding": "0px",
+                        "--nutui-grid-item-content-margin": "0px",
+                        "--nutui-grid-border-color": "transparent",
+                      } as any
+                    }
+                  >
+                    {item.media_url.map((ite: any, index: number) => {
+                      return (
+                        <GridItem>
+                          <Image
+                            src={ite.src}
+                            mode={
+                              item.media_url.length === 1
+                                ? "scaleToFill"
+                                : "scaleToFill"
+                            }
+                            onClick={(e) => {
+                              e.preventDefault();
+                              e.stopPropagation();
+                              setMediaUrl(item.media_url);
+                              setInit(index + 1);
+
+                              setShowPreview(true);
+                            }}
+                          />
+                        </GridItem>
+                      );
+                    })}
+                  </Grid>
+                </View>
+                <View className="mt-[10px] pl-[20px] pr-[20px] flex items-center justify-between">
+                  <View className="flex items-center">
+                    <Message size={16} color="#949494" />
+                    <Text className="ml-[5px] text-[16px] text-[#949494]">
+                      {item.comment_count}
+                    </Text>
+                  </View>
+                  <View
+                    className="flex items-center"
+                    onClick={(e) => {
+                      e.stopPropagation();
+                      handleInteraction(
+                        item.id,
+                        item.interaction_type === "praise"
+                          ? "dispraise"
+                          : "praise"
+                      );
+                    }}
+                  >
+                    {item.interaction_type === "praise" ? (
+                      <HeartFill size={16} color="#ec4342" />
+                    ) : (
+                      <Heart size={16} color="#949494" />
+                    )}
+                    <Text className="ml-[5px] text-[16px] text-[#949494]">
+                      {item.like_count}
+                    </Text>
+                  </View>
+                </View>
+              </View>
+            </View>
+          );
+        })}
+
+        {/* 空状态提示 */}
+        {postList.length === 0 && !loading && (
+          <View className="flex flex-col items-center justify-center py-[50px]">
+            <Text className="text-[#999] text-[14px]">暂无帖子</Text>
+          </View>
+        )}
+
+        {/* 加载更多提示 */}
+        {hasMore && (
+          <View className="p-[20px] text-center">
+            <Text className="text-[14px] text-[#999]">
+              {loading ? "加载中..." : "上拉加载更多"}
+            </Text>
+          </View>
+        )}
+
+        {!hasMore && postList.length > 0 && (
+          <View className="p-[20px] text-center">
+            <Text className="text-[14px] text-[#999]">没有更多了</Text>
+          </View>
+        )}
+
+        <ImagePreview
+          images={mediaUrl}
+          visible={showPreview}
+          defaultValue={init}
+          onClose={() => setShowPreview(false)}
+          indicator
+        />
+        <Toast id="follow_success" />
+        <Toast id="load_error" />
+        <Dialog id="followed_dialog" />
+      </View>
+      {/* </PullToRefresh> */}
+    </View>
+    // </ScrollView>
+  );
+};
+
+export default PostList;

+ 34 - 17
src/components/post-list/index.tsx

@@ -1,4 +1,4 @@
-import { Text, View, ScrollView } from "@tarojs/components";
+import { Text, View } from "@tarojs/components";
 import { Heart, HeartFill, Plus } from "@nutui/icons-react-taro";
 import { Message } from "@nutui/icons-react-taro";
 import {
@@ -7,23 +7,38 @@ import {
   Image,
   ImagePreview,
   Toast,
-  PullToRefresh,
-  Sticky,
 } from "@nutui/nutui-react-taro";
 import MoreButton from "../fllower-button";
 import { circlePostListApi } from "../../api/post";
-import { useEffect, useState, useCallback, useRef } from "react";
+import {
+  useEffect,
+  useState,
+  useCallback,
+  useImperativeHandle,
+  forwardRef,
+} from "react";
 import { strSlice } from "../../utils/index";
 import { followApi, interactionApi, unfollowApi } from "../../api/interaction";
 import { Dialog } from "@nutui/nutui-react-taro";
 import { navigateTo } from "@tarojs/taro";
 
-const PostList = () => {
+interface PostListProps {
+  onLoadMore?: () => void;
+  onRefresh?: () => void;
+}
+
+export interface PostListRef {
+  loadMore: () => void;
+  refresh: () => void;
+  hasMore: boolean;
+  loading: boolean;
+}
+
+const PostList = forwardRef<PostListRef, PostListProps>((props, ref) => {
   const [postList, setPostList] = useState<any[]>([]);
   const [showPreview, setShowPreview] = useState(false);
   const [init, setInit] = useState(0);
   const [mediaUrl, setMediaUrl] = useState<any[]>([]);
-  const containerTopRef = useRef(null);
   // 分页相关状态
   const [currentPage, setCurrentPage] = useState(1);
   const [hasMore, setHasMore] = useState(true);
@@ -156,19 +171,20 @@ const PostList = () => {
       });
     }
   };
+  // 暴露方法给父组件
+  useImperativeHandle(ref, () => ({
+    loadMore: handleLoadMore,
+    refresh: handleRefresh,
+    hasMore,
+    loading,
+  }));
+
   useEffect(() => {
     getPostList(1, true);
   }, []);
 
   return (
-    <ScrollView
-      scrollY
-      className="post-list-scroll"
-      style={{ height: "calc(100vh - 50px)" }}
-      onScrollToLower={handleLoadMore}
-      lowerThreshold={200}
-    >
-      {/* <PullToRefresh onRefresh={handleRefresh}> */}
+    <>
       <View className="post-list bg-[#f8f8f8]">
         {postList.map((item: any, index: number) => {
           return (
@@ -344,9 +360,10 @@ const PostList = () => {
         <Toast id="load_error" />
         <Dialog id="followed_dialog" />
       </View>
-      {/* </PullToRefresh> */}
-    </ScrollView>
+    </>
   );
-};
+});
+
+PostList.displayName = "PostList";
 
 export default PostList;

+ 1 - 0
src/pages/index/index.scss

@@ -2,6 +2,7 @@
   font-size: 24rpx;
   margin-bottom: 30px;
   background-color: #f8f8f8;
+  overflow-y: scroll;
   .header {
     width: 100%;
     height: 100px;

+ 84 - 28
src/pages/index/index.tsx

@@ -1,29 +1,65 @@
-import { View, Text, ScrollView } from "@tarojs/components";
-import { Plus, User } from "@nutui/icons-react-taro";
-import { getStorageSync, navigateTo } from "@tarojs/taro";
-import PostList from "../../components/post-list";
+import { View, ScrollView } from "@tarojs/components";
+import { Gift, Plus, User } from "@nutui/icons-react-taro";
+import { navigateTo } from "@tarojs/taro";
+import PostList, { PostListRef } from "../../components/post-list";
 
 import "./index.scss";
-import { Image, Sticky, Tabs } from "@nutui/nutui-react-taro";
-import { useEffect, useState } from "react";
+import { Image, Sticky, TabPane, Tabs } from "@nutui/nutui-react-taro";
+import { useEffect, useRef, useState } from "react";
 import { getCircleInfoApi } from "../../api/circle";
 
 function Index() {
   const [circleData, setCircleData] = useState<any>(null);
   const [tab1value, setTab1value] = useState<string | number>("0");
-  const [userInfo, setUserInfo] = useState<any>(null);
-  const getUserInfo = () => {
-    const userInfo = getStorageSync("User");
-    setUserInfo(userInfo);
-  };
+  const [stickyNum, setStickyNum] = useState<number>(50);
+  const containerTopRef = useRef<HTMLDivElement>(null);
+  const postListRef = useRef<PostListRef>(null);
+  // const [userInfo, setUserInfo] = useState<any>(null);
+
+  // const getUserInfo = () => {
+  //   const userInfo = getStorageSync("User");
+  //   setUserInfo(userInfo);
+  // };
   useEffect(() => {
     getCircleInfoApi({ id: 1 }).then((res: any) => {
       setCircleData(res.data);
     });
-    getUserInfo();
+    // getUserInfo();
   }, []);
+
+  const handleUpperThreshold = () => {
+    console.log("handleUpperThreshold");
+    setStickyNum(50);
+  };
+  const handleScroll = (e: any) => {
+    console.log("handleScroll", e);
+    if (e.detail.scrollTop > 50) {
+      setStickyNum(0);
+    } else {
+      setStickyNum(50);
+      containerTopRef.current?.scrollTo({
+        top: 50,
+        behavior: "smooth",
+      });
+    }
+  };
+
+  const handleScrollToLower = () => {
+    console.log("handleScrollToLower - 触发加载更多");
+    if (
+      postListRef.current &&
+      postListRef.current.hasMore &&
+      !postListRef.current.loading
+    ) {
+      postListRef.current.loadMore();
+    }
+  };
   return (
-    <View className="index">
+    <View
+      style={{ height: "calc(100vh - 50px)" }}
+      ref={containerTopRef}
+      className="index"
+    >
       {/* 头部导航栏 - 移出page-content容器 */}
       <View className="index-header bg-[#6069d9] fixed top-0 left-0 right-0 z-20 h-[50px] w-full flex items-center justify-between px-[10px]">
         <View className="flex items-center relative z-40">
@@ -44,12 +80,14 @@ function Index() {
             </View>
           </View>
         </View>
-        <View
-          onClick={() => {
-            navigateTo({ url: "/pages/self/index" });
-          }}
-        >
-          <User size={20} />
+        <View className="flex items-center">
+          <View
+            onClick={() => {
+              navigateTo({ url: "/pages/self/index" });
+            }}
+          >
+            <User size={20} />
+          </View>
         </View>
       </View>
 
@@ -57,7 +95,6 @@ function Index() {
       <View
         className="page-scroll"
         // scrollY
-        // style={{ height: "calc(100vh - 50px)" }}
       >
         <View className="page-content">
           <View className="scrollable-banner h-[100px] pb-[10px] bg-[#6069d9] pt-[60px] flex items-center justify-around">
@@ -70,34 +107,53 @@ function Index() {
               积分中心
             </View>
             <View className="text-[#fff] w-[100px] h-[30px] flex items-center justify-center text-center text-[14px] font-[800] px-[10px] bg-[#5258acf0]">
+              <Gift size={16} />
               礼包中心
             </View>
+            <View
+              className="text-[#fff] w-[100px] h-[30px] flex items-center justify-center text-center text-[14px] font-[800] px-[10px] bg-[#5258acf0]"
+              onClick={() => {
+                navigateTo({ url: "/pages/publish/index" });
+              }}
+            >
+              <Plus size={20} />
+              发布帖子
+            </View>
           </View>
-
-          <Sticky threshold={50}>
+          <Sticky threshold={50} className="relative">
             <Tabs
-              value={tab1value}
               align="left"
+              value={tab1value}
               onChange={(value) => {
                 setTab1value(value);
               }}
             >
-              <Tabs.TabPane title="最新">
-                <PostList />
-              </Tabs.TabPane>
+              <TabPane title="最新">
+                <ScrollView
+                  upperThreshold={50}
+                  lowerThreshold={50}
+                  onScrollToUpper={handleUpperThreshold}
+                  onScrollToLower={handleScrollToLower}
+                  onScroll={handleScroll}
+                  scrollY
+                  style={{ height: "calc(100vh - 50px)" }}
+                >
+                  <PostList ref={postListRef} />
+                </ScrollView>
+              </TabPane>
             </Tabs>
           </Sticky>
         </View>
       </View>
 
-      <View
+      {/* <View
         className="fixed bottom-[50px] right-[20px] w-[50px] h-[50px] bg-[#6069d9] rounded-[50%] z-10 justify-center items-center flex "
         onClick={() => {
           navigateTo({ url: "/pages/publish/index" });
         }}
       >
         <Plus color="#fff" size={50} style={{ marginLeft: "-2px" }} />
-      </View>
+      </View> */}
     </View>
   );
 }