u
This commit is contained in:
1 parent
4f5218c892
commit
2f8a815c62
32 files changed
+1915
-1738
No files matched your search
+1
@@ -71,6 +71,7 @@ public class ForumPostCommentServiceImpl extends ServiceImpl<ForumPostCommentMap
|
||||
|
||||
return Result.success(forumPostCommentVO,"评论成功");
|
||||
}
|
||||
|
||||
public List<ForumPostCommentVO> getPostComments(String forumPostId) {
|
||||
// 1. 查询所有评论
|
||||
List<ForumPostCommentVO> list = forumPostCommentMapper.getPostComments(forumPostId);
|
||||
|
||||
-1
@@ -35,5 +35,4 @@ public class CompanyDetailVO {
|
||||
private String position;
|
||||
private String name;
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package cn.link56.post.controller;
|
||||
|
||||
import cn.link56.common.Result;
|
||||
import cn.link56.post.model.dto.CreatePostCommentDTO;
|
||||
import cn.link56.post.model.vo.CreatePostCommentVO;
|
||||
import cn.link56.post.model.vo.PostCommentVO;
|
||||
import cn.link56.post.service.PostCommentService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/post/comment")
|
||||
public class PostCommentController {
|
||||
@Resource
|
||||
private PostCommentService postCommentService;
|
||||
|
||||
@PostMapping("/create")
|
||||
public Result<CreatePostCommentVO> createPostComment(@RequestBody CreatePostCommentDTO dto){
|
||||
CreatePostCommentVO commentVO = postCommentService.createPostComment(dto);
|
||||
return Result.success(commentVO);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Result<List<PostCommentVO>> listPostComments(@PathVariable("id") @NotNull(message = "文章ID不能为空") Long id){
|
||||
List<PostCommentVO> postComments = postCommentService.listPostComments(id);
|
||||
|
||||
return Result.success(postComments);
|
||||
}
|
||||
|
||||
@PostMapping("/delete/{id}")
|
||||
public Result<Void> delete(@PathVariable("id") @NotNull(message = "评论ID不能为空") Long postCommentId) {
|
||||
postCommentService.deletePostComment(postCommentId);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
+17
-2
@@ -3,13 +3,13 @@ package cn.link56.post.controller;
|
||||
import cn.link56.common.BasePageVO;
|
||||
import cn.link56.common.Result;
|
||||
import cn.link56.post.model.dto.QueryPublicPostDTO;
|
||||
import cn.link56.post.model.vo.PostTagVO;
|
||||
import cn.link56.post.model.vo.QueryPublicPostVO;
|
||||
import cn.link56.post.model.vo.*;
|
||||
import cn.link56.post.service.PostService;
|
||||
import cn.link56.post.service.PostTagService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@@ -32,4 +32,19 @@ public class PostController {
|
||||
BasePageVO<QueryPublicPostVO> res = postService.queryPublicPosts(dto);
|
||||
return Result.success(res);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Result<PublicPostVO> getPublicDetail(@PathVariable("id") @NotNull(message = "文章ID不能为空") Long id){
|
||||
PublicPostVO publicPostVO = postService.getPublicDetail(id);
|
||||
|
||||
return Result.success(publicPostVO);
|
||||
}
|
||||
|
||||
@GetMapping("/company/{id}")
|
||||
public Result<PostCompanyVO> getForumCompany(@PathVariable("id") @NotNull(message = "公司ID不能为空") Long companyId){
|
||||
PostCompanyVO postCompanyVO = postService.getPostCompanyById(companyId);
|
||||
|
||||
return Result.success(postCompanyVO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package cn.link56.post.mapper;
|
||||
|
||||
import cn.link56.post.model.po.PostComment;
|
||||
import cn.link56.post.model.vo.PostCommentVO;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface PostCommentMapper extends BaseMapper<PostComment> {
|
||||
List<PostCommentVO> selectComments(@Param("postId") Long postId);
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package cn.link56.post.mapper;
|
||||
|
||||
import cn.link56.post.model.po.Post;
|
||||
import cn.link56.post.model.vo.EditorDashboardVO;
|
||||
import cn.link56.post.model.vo.PublicPostVO;
|
||||
import cn.link56.post.model.vo.QueryEditorPostVO;
|
||||
import cn.link56.post.model.vo.QueryPublicPostVO;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
@@ -33,4 +35,16 @@ public interface PostMapper extends BaseMapper<Post> {
|
||||
|
||||
List<EditorDashboardVO.Chart> selectDashboardViewStats(@Param("userId") Long userId);
|
||||
List<EditorDashboardVO.Chart> selectDashboardNumberStats(@Param("userId") Long userId);
|
||||
|
||||
List<QueryPublicPostVO> selectPublicPostPage(
|
||||
@Param("offset") int offset,
|
||||
@Param("limit") int limit,
|
||||
@Param("title") String title,
|
||||
@Param("tag") String tag
|
||||
);
|
||||
|
||||
Long selectPublicPostTotal(
|
||||
@Param("title") String title,
|
||||
@Param("tag") String tag
|
||||
);
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package cn.link56.post.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class CreatePostCommentDTO {
|
||||
private Long fatherId;
|
||||
private Long postId;
|
||||
|
||||
private String content;
|
||||
}
|
||||
+3
@@ -8,6 +8,9 @@ import lombok.NoArgsConstructor;
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class QueryPublicPostDTO {
|
||||
private Integer currentPage;
|
||||
private Integer pageSize;
|
||||
|
||||
private String title;
|
||||
private String tag;
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@@ -26,14 +28,14 @@ public class PostComment {
|
||||
private String content;
|
||||
|
||||
@TableField(
|
||||
value = "b_create_user_id",
|
||||
value = "b_create_by",
|
||||
fill = FieldFill.INSERT
|
||||
)
|
||||
private Long createUserId;
|
||||
private Long createBy;
|
||||
|
||||
@TableField(
|
||||
value = "b_create_time",
|
||||
fill = FieldFill.INSERT
|
||||
)
|
||||
private Long createTime;
|
||||
private Date createTime;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package cn.link56.post.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class CreatePostCommentVO {
|
||||
private Long id;
|
||||
private Long fatherId;
|
||||
private String content;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.link56.post.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class PostCommentVO {
|
||||
private Long id;
|
||||
private Long fatherId;
|
||||
private String content;
|
||||
private Date createTime;
|
||||
private User publishUser;
|
||||
private User replyUser;
|
||||
private List<PostCommentVO> children;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class User{
|
||||
private Long id;
|
||||
private Long companyId;
|
||||
private String name;
|
||||
private String avatar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.link56.post.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class PostCompanyVO {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String logo;
|
||||
private String profile;
|
||||
private List<String> areas;
|
||||
private List<String> industries;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.link56.post.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class PublicPostVO {
|
||||
private Long id;
|
||||
private String title;
|
||||
private String content;
|
||||
private List<String> tags;
|
||||
private Date activeTime;
|
||||
|
||||
private Long viewCount;
|
||||
private Long commentCount;
|
||||
|
||||
private Long companyId;
|
||||
}
|
||||
+119
@@ -1,14 +1,133 @@
|
||||
package cn.link56.post.service;
|
||||
|
||||
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
|
||||
import cn.link56.post.mapper.PostCommentMapper;
|
||||
import cn.link56.post.model.dto.CreatePostCommentDTO;
|
||||
import cn.link56.post.model.po.PostComment;
|
||||
import cn.link56.post.model.po.PostView;
|
||||
import cn.link56.post.model.vo.CreatePostCommentVO;
|
||||
import cn.link56.post.model.vo.PostCommentVO;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class PostCommentService extends ServiceImpl<PostCommentMapper, PostComment> {
|
||||
@Resource
|
||||
private PostCommentMapper postCommentMapper;
|
||||
|
||||
/**
|
||||
* 根据文章 id 获取浏览总数
|
||||
*/
|
||||
public Long getTotalByPostId(Long postId) {
|
||||
return lambdaQuery()
|
||||
.eq(PostComment::getPostId, postId)
|
||||
.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文章 id 获取最新一条评论
|
||||
*/
|
||||
public PostComment getLatestCommentByPostId(Long postId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<PostCommentVO> listPostComments(Long id) {
|
||||
// 1. 查询所有评论
|
||||
List<PostCommentVO> list = postCommentMapper.selectComments(id);
|
||||
|
||||
// 2. 构建扁平化树形结构(所有子评论提升到根评论层级)
|
||||
List<PostCommentVO> rootComments = new ArrayList<>();
|
||||
Map<Long, PostCommentVO> commentMap = new HashMap<>();
|
||||
|
||||
// 2.1 初始化映射并收集根评论
|
||||
for (PostCommentVO comment : list) {
|
||||
commentMap.put(comment.getId(), comment);
|
||||
if (ObjectUtils.isEmpty(comment.getFatherId())) {
|
||||
rootComments.add(comment);
|
||||
comment.setChildren(new ArrayList<>()); // 提前初始化children
|
||||
}
|
||||
}
|
||||
|
||||
// 2.2 遍历所有评论,将子评论挂到根祖先的children中
|
||||
for (PostCommentVO comment : list) {
|
||||
if (ObjectUtils.isEmpty(comment.getFatherId())) {
|
||||
continue; // 跳过根评论
|
||||
}
|
||||
|
||||
// 查找当前评论的根祖先(追溯到没有fatherId的祖先)
|
||||
PostCommentVO rootAncestor = null;
|
||||
Long currentFatherId = comment.getFatherId();
|
||||
while (true) {
|
||||
PostCommentVO parent = commentMap.get(currentFatherId);
|
||||
if (parent == null) break; // 父节点不存在,终止循环
|
||||
|
||||
if (ObjectUtils.isEmpty(comment.getFatherId())) {
|
||||
rootAncestor = parent; // 找到根祖先
|
||||
break;
|
||||
} else {
|
||||
currentFatherId = parent.getFatherId(); // 继续向上查找
|
||||
}
|
||||
}
|
||||
|
||||
// 挂载到根祖先的children中
|
||||
if (rootAncestor != null && rootAncestor.getChildren() != null) {
|
||||
rootAncestor.getChildren().add(comment);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 按原始顺序排序(可选)
|
||||
rootComments.forEach(root -> {
|
||||
if (root.getChildren() != null) {
|
||||
// 根据需求排序,例如按创建时间升序
|
||||
root.getChildren().sort(Comparator.comparing(PostCommentVO::getCreateTime));
|
||||
}
|
||||
});
|
||||
|
||||
return rootComments;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CreatePostCommentVO createPostComment(CreatePostCommentDTO dto) {
|
||||
PostComment postComment = new PostComment();
|
||||
|
||||
BeanUtils.copyProperties(dto,postComment);
|
||||
|
||||
postComment.setId(IdGenerator.nextId());
|
||||
postCommentMapper.insert(postComment);
|
||||
|
||||
CreatePostCommentVO commentVO = new CreatePostCommentVO();
|
||||
BeanUtils.copyProperties(postComment,commentVO);
|
||||
|
||||
return commentVO;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deletePostComment(@NotBlank(message = "评论ID不能为空") Long postCommentId) {
|
||||
PostComment postComment = postCommentMapper.selectById(postCommentId);
|
||||
if (ObjectUtils.isEmpty(postComment)){
|
||||
return;
|
||||
}
|
||||
|
||||
Long fatherId = postComment.getFatherId();
|
||||
if(ObjectUtils.isEmpty(fatherId)){
|
||||
postCommentMapper.deleteById(postCommentId);
|
||||
}else{
|
||||
LambdaUpdateWrapper<PostComment> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(PostComment::getId,postCommentId)
|
||||
.or()
|
||||
.eq(PostComment::getFatherId,postCommentId);
|
||||
|
||||
postCommentMapper.delete(updateWrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,27 @@ package cn.link56.post.service;
|
||||
|
||||
import cn.link56.common.BasePageVO;
|
||||
import cn.link56.common.context.UserContext;
|
||||
import cn.link56.common.exception.BusinessException;
|
||||
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
|
||||
import cn.link56.company.model.po.CompanyPO;
|
||||
import cn.link56.company.service.CompanyService;
|
||||
import cn.link56.post.mapper.PostMapper;
|
||||
import cn.link56.post.model.dto.*;
|
||||
import cn.link56.post.model.enums.PostStatusEnum;
|
||||
import cn.link56.post.model.po.Post;
|
||||
import cn.link56.post.model.vo.EditorDashboardVO;
|
||||
import cn.link56.post.model.vo.EditorPostVO;
|
||||
import cn.link56.post.model.vo.QueryEditorPostVO;
|
||||
import cn.link56.post.model.vo.QueryPublicPostVO;
|
||||
import cn.link56.post.model.po.PostComment;
|
||||
import cn.link56.post.model.vo.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -29,6 +32,12 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
|
||||
|
||||
@Resource
|
||||
private PostMapper postMapper;
|
||||
@Resource
|
||||
private PostViewService postViewService;
|
||||
@Resource
|
||||
private PostCommentService postCommentService;
|
||||
@Resource
|
||||
private CompanyService companyService;
|
||||
|
||||
@Transactional
|
||||
public EditorPostVO createPost(CreatePostDTO dto) {
|
||||
@@ -219,10 +228,63 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
|
||||
public BasePageVO<QueryPublicPostVO> queryPublicPosts(QueryPublicPostDTO dto) {
|
||||
String title = dto.getTitle();
|
||||
String tag = dto.getTag();
|
||||
|
||||
int offset = (dto.getCurrentPage() - 1) * dto.getPageSize() + 1;
|
||||
int limit = dto.getCurrentPage() * dto.getPageSize();
|
||||
|
||||
BasePageVO<QueryPublicPostVO> result = new BasePageVO<>();
|
||||
|
||||
return result;
|
||||
List<QueryPublicPostVO> queryEditorPostVOS = postMapper.selectPublicPostPage(offset, limit, title, tag);
|
||||
Long total = postMapper.selectPublicPostTotal(title, tag);
|
||||
|
||||
|
||||
return new BasePageVO<QueryPublicPostVO>(total, queryEditorPostVOS);
|
||||
}
|
||||
|
||||
public PublicPostVO getPublicDetail(Long id) {
|
||||
Post post = postMapper.selectById(id);
|
||||
|
||||
if (post == null) {
|
||||
throw new BusinessException("帖子不存在或已被删除");
|
||||
}
|
||||
|
||||
if (PostStatusEnum.DRAFT.getCode() == post.getStatus()) {
|
||||
throw new BusinessException("该帖子尚未发布");
|
||||
}
|
||||
|
||||
PublicPostVO postVO = new PublicPostVO();
|
||||
|
||||
BeanUtils.copyProperties(post,postVO);
|
||||
postVO.setTags(Arrays.stream(StringUtils.split(post.getTag(),",")).collect(Collectors.toList()));
|
||||
|
||||
Long viewTotal = postViewService.getTotalByPostId(id);
|
||||
Long commentTotal = postCommentService.getTotalByPostId(id);
|
||||
|
||||
postVO.setViewCount(viewTotal);
|
||||
postVO.setCommentCount(commentTotal);
|
||||
|
||||
PostComment latestCommentByPostId = postCommentService.getLatestCommentByPostId(id);
|
||||
|
||||
if (latestCommentByPostId == null){
|
||||
postVO.setActiveTime(post.getUpdateTime());
|
||||
} else {
|
||||
postVO.setActiveTime(latestCommentByPostId.getCreateTime());
|
||||
}
|
||||
|
||||
return postVO;
|
||||
}
|
||||
|
||||
public PostCompanyVO getPostCompanyById(Long id) {
|
||||
CompanyPO companyPO = companyService.getById(id);
|
||||
if (companyPO == null){
|
||||
throw new BusinessException("该公司不存在或已注销");
|
||||
}
|
||||
|
||||
PostCompanyVO postCompanyVO = new PostCompanyVO();
|
||||
BeanUtils.copyProperties(companyPO,postCompanyVO);
|
||||
|
||||
postCompanyVO.setAreas(Arrays.stream(StringUtils.split(companyPO.getArea(),",")).collect(Collectors.toList()));
|
||||
postCompanyVO.setIndustries(Arrays.stream(StringUtils.split(companyPO.getIndustry(),",")).collect(Collectors.toList()));
|
||||
|
||||
return postCompanyVO;
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,14 @@ import javax.annotation.Resource;
|
||||
public class PostViewService extends ServiceImpl<PostViewMapper, PostView> {
|
||||
@Resource
|
||||
private PostViewMapper postViewMapper;
|
||||
|
||||
|
||||
/**
|
||||
* 根据文章 id 获取浏览总数
|
||||
*/
|
||||
public Long getTotalByPostId(Long postId) {
|
||||
return lambdaQuery()
|
||||
.eq(PostView::getPostId, postId)
|
||||
.count();
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.link56.post.mapper.PostCommentMapper">
|
||||
<resultMap id="CommentListMap" type="cn.link56.post.model.vo.PostCommentVO">
|
||||
<id property="id" column="b_id"/>
|
||||
<result property="fatherId" column="b_father_id"/>
|
||||
<result property="content" column="b_content"/>
|
||||
<result property="createTime" column="b_create_time"/>
|
||||
|
||||
<!-- 映射发布公司 -->
|
||||
<association property="publishUser" javaType="cn.link56.post.model.vo.PostCommentVO$User">
|
||||
<result property="id" column="pub_user_id"/>
|
||||
<result property="companyId" column="pub_company_id"/>
|
||||
<result property="name" column="pub_user_name"/>
|
||||
<result property="avatar" column="pub_user_avatar"/>
|
||||
</association>
|
||||
|
||||
<!-- 映射回复公司 -->
|
||||
<association property="replyUser" javaType="cn.link56.post.model.vo.PostCommentVO$User">
|
||||
<result property="id" column="rep_user_id"/>
|
||||
<result property="companyId" column="rep_company_id"/>
|
||||
<result property="name" column="rep_user_name"/>
|
||||
<result property="avatar" column="rep_user_avatar"/>
|
||||
</association>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectComments" resultMap="CommentListMap">
|
||||
SELECT c.*,
|
||||
pc.b_id AS pub_user_id,pc.b_company_id AS pub_company_id,pc.b_name AS pub_user_name,
|
||||
rc.b_id AS rep_user_id,rc.b_company_id AS rep_company_id,rc.b_name AS rep_company_id
|
||||
FROM b_post_comment c
|
||||
LEFT JOIN b_company_user pc ON c.b_create_by = pc.b_id
|
||||
LEFT JOIN b_company_user rc ON (
|
||||
SELECT b_create_by FROM b_post_comment WHERE b_id = c.b_father_id
|
||||
) = rc.b_id
|
||||
WHERE c.b_post_id = #{postId}
|
||||
ORDER BY c.b_create_time
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -163,7 +163,7 @@
|
||||
p.b_id AS id,
|
||||
p.b_title AS title,
|
||||
p.b_tag AS tagsString,
|
||||
p.company_id AS companyId,
|
||||
p.b_company_id AS companyId,
|
||||
bc.b_name AS companyName,
|
||||
|
||||
ISNULL(pv.viewCount, 0) AS viewCount,
|
||||
@@ -173,10 +173,10 @@
|
||||
ISNULL(pc.lastCommentTime, p.b_update_time) AS activeTime,
|
||||
|
||||
ROW_NUMBER() OVER (
|
||||
ORDER BY ISNULL(pc.lastCommentTime, p.b_update_time) DESC
|
||||
ORDER BY ISNULL(pc.lastCommentTime, p.b_update_time) DESC
|
||||
) AS rn
|
||||
FROM b_post p
|
||||
LEFT JOIN b_company bc ON bc.b_id = p.company_id
|
||||
LEFT JOIN b_company bc ON bc.b_id = p.b_company_id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
b_post_id,
|
||||
@@ -193,10 +193,11 @@
|
||||
GROUP BY b_post_id
|
||||
) pc ON pc.b_post_id = p.b_id
|
||||
<where>
|
||||
AND p.b_status = 1
|
||||
<if test="title != null and title != ''">
|
||||
AND p.b_title LIKE '%' + #{title} + '%'
|
||||
</if>
|
||||
<if test="tag != null and tag != -1">
|
||||
<if test="tag != null and tag != ''">
|
||||
AND p.b_tag LIKE '%' + #{tag} + '%'
|
||||
</if>
|
||||
</where>
|
||||
@@ -204,4 +205,19 @@
|
||||
WHERE t.rn BETWEEN #{offset} AND #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectPublicPostTotal" resultType="java.lang.Long">
|
||||
SELECT COUNT(1)
|
||||
FROM b_post p
|
||||
<where>
|
||||
AND p.b_status = 1
|
||||
|
||||
<if test="title != null and title != ''">
|
||||
AND p.b_title LIKE '%' + #{title} + '%'
|
||||
</if>
|
||||
<if test="tag != null and tag != ''">
|
||||
AND p.b_tag LIKE '%' + #{tag} + '%'
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -22,7 +22,8 @@
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.3"
|
||||
"vue-router": "^4.6.3",
|
||||
"vue3-puzzle-vcode": "^1.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^6.6.1",
|
||||
|
||||
+25
@@ -44,6 +44,9 @@ importers:
|
||||
vue-router:
|
||||
specifier: ^4.6.3
|
||||
version: 4.6.3(vue@3.5.25(typescript@5.9.3))
|
||||
vue3-puzzle-vcode:
|
||||
specifier: ^1.1.7
|
||||
version: 1.1.7
|
||||
devDependencies:
|
||||
'@antfu/eslint-config':
|
||||
specifier: ^6.6.1
|
||||
@@ -502,36 +505,42 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
@@ -596,56 +605,67 @@ packages:
|
||||
resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.53.3':
|
||||
resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.53.3':
|
||||
resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.53.3':
|
||||
resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.53.3':
|
||||
resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.53.3':
|
||||
resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
|
||||
@@ -2445,6 +2465,9 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
|
||||
vue3-puzzle-vcode@1.1.7:
|
||||
resolution: {integrity: sha512-mW780dz7HKjrElnE60CeYSeHGidKBKHoMjTDYfqF21330rTkFOsfDK1FQKZ22MktgMtTEoS/imfpEDlM1cxY/g==}
|
||||
|
||||
vue@3.5.25:
|
||||
resolution: {integrity: sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==}
|
||||
peerDependencies:
|
||||
@@ -4980,6 +5003,8 @@ snapshots:
|
||||
'@vue/language-core': 3.1.8(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
|
||||
vue3-puzzle-vcode@1.1.7: {}
|
||||
|
||||
vue@3.5.25(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.25
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import SvgIcon from './SvgIcon.vue'
|
||||
|
||||
const emits = defineEmits(['select'])
|
||||
|
||||
// 示例 emoji
|
||||
const emojis = ref<string[]>([
|
||||
'😊',
|
||||
'😂',
|
||||
'😍',
|
||||
'😎',
|
||||
'😜',
|
||||
'🤔',
|
||||
'😏',
|
||||
'😢',
|
||||
'🥺',
|
||||
'😇',
|
||||
'😈',
|
||||
'🤩',
|
||||
'🥳',
|
||||
'😱',
|
||||
'😅',
|
||||
'😴',
|
||||
'😷',
|
||||
'🤐',
|
||||
'😻',
|
||||
'🤡',
|
||||
'💩',
|
||||
'👻',
|
||||
'🤖',
|
||||
'💀',
|
||||
'👀',
|
||||
'👍',
|
||||
'👊',
|
||||
'✌️',
|
||||
'💪',
|
||||
'👏',
|
||||
])
|
||||
|
||||
function handleClickEmoji(emoji: string) {
|
||||
emits('select', emoji)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-popover trigger="click" position="tl">
|
||||
<SvgIcon icon="majesticons:emoji-happy-line" class="chat-window-bottom-icon" />
|
||||
<template #content>
|
||||
<div class="emoji-container">
|
||||
<div v-for="(emoji, index) in emojis" :key="index" class="emoji-item" @click="handleClickEmoji(emoji)">
|
||||
{{ emoji }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-window-bottom-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.emoji-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
.emoji-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.emoji-item:hover {
|
||||
transform: scale(1.2);
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,31 @@
|
||||
import type { PostTag } from '@/types/post'
|
||||
import type { CreatePostCommentParams, CreatePostCommentResult, PostComment, PostCompany, PostTag, PublicPost, PublicPostDetail, QueryPublicPostParams } from '@/types/post'
|
||||
import { httpGet, httpPostJson } from '@/request/utils'
|
||||
|
||||
// ===== 公共接口 =====
|
||||
export function getPostTagApi() {
|
||||
return httpGet<PostTag[]>('/post/tag')
|
||||
}
|
||||
|
||||
export function queryPublicPostApi(params: QueryPublicPostParams) {
|
||||
return httpPostJson<BasePageResult<PublicPost>>('/post/query', params)
|
||||
}
|
||||
|
||||
export function getPublicPostDetailApi(id: number) {
|
||||
return httpGet<PublicPostDetail>(`/post/${id}`)
|
||||
}
|
||||
|
||||
export function getPostCompanyById(id: number) {
|
||||
return httpGet<PostCompany>(`/post/company/${id}`)
|
||||
}
|
||||
|
||||
export function createPostCommentApi(params: CreatePostCommentParams) {
|
||||
return httpPostJson<CreatePostCommentResult>(`/post/comment/create`, params)
|
||||
}
|
||||
|
||||
export function listPostCommentApi(id: number) {
|
||||
return httpGet<PostComment[]>(`/post/comment/${id}`)
|
||||
}
|
||||
|
||||
export function deletePostCommentApi(id: number) {
|
||||
return httpGet<void>(`/post/comment/delete/${id}`)
|
||||
}
|
||||
@@ -25,6 +25,10 @@ const router = createRouter({
|
||||
path: '',
|
||||
component: () => import('@/views/post/home/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/post/detail/:id',
|
||||
component: () => import('@/views/post/detail/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ declare module 'vue' {
|
||||
CustomLoading: typeof import('./../components/CustomLoading.vue')['default']
|
||||
Directory: typeof import('./../components/wang-editor/modules/directory.vue')['default']
|
||||
EmailCodeInput: typeof import('./../components/EmailCodeInput.vue')['default']
|
||||
EmojiPopover: typeof import('./../components/EmojiPopover.vue')['default']
|
||||
HelloWorld: typeof import('./../components/HelloWorld.vue')['default']
|
||||
ICarbonAccessibility: typeof import('~icons/carbon/accessibility')['default']
|
||||
'IFa7Solid:userShield': typeof import('~icons/fa7-solid/user-shield')['default']
|
||||
|
||||
@@ -27,3 +27,84 @@ export interface PostCategory {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface PublicPost {
|
||||
id: number
|
||||
title: string
|
||||
tags: string[]
|
||||
|
||||
companyId: string
|
||||
companyName: string
|
||||
|
||||
viewCount: number
|
||||
commentCount: number
|
||||
|
||||
activeTime: string
|
||||
}
|
||||
|
||||
export interface QueryPublicPostParams {
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
|
||||
title: string
|
||||
tag: string
|
||||
}
|
||||
|
||||
export interface PublicPostDetail {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
tags: string[]
|
||||
activeTime: string
|
||||
|
||||
viewCount: number
|
||||
commentCount: number
|
||||
|
||||
companyId: number
|
||||
}
|
||||
|
||||
export interface PostCompany {
|
||||
id: number
|
||||
name: string
|
||||
logo: string
|
||||
profile: string
|
||||
|
||||
areas: string[]
|
||||
industries: string[]
|
||||
}
|
||||
|
||||
export interface CreatePostCommentParams {
|
||||
fatherId: number | null
|
||||
postId: number
|
||||
|
||||
content: string
|
||||
}
|
||||
export interface CreatePostCommentResult {
|
||||
id: number
|
||||
fatherId: number | null
|
||||
|
||||
content: string
|
||||
createId: number
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export interface PostComment {
|
||||
id: number
|
||||
fatherId: number | null
|
||||
content: string
|
||||
createTime: string
|
||||
children: PostComment[]
|
||||
|
||||
publishUser: {
|
||||
id: number
|
||||
companyId: number
|
||||
name: string
|
||||
avatar: string
|
||||
}
|
||||
replyUser: {
|
||||
id: number
|
||||
companyId: number
|
||||
name: string
|
||||
avatar: string
|
||||
} | null
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export function formatPostTime(input?: dayjs.ConfigType): string {
|
||||
if (!input)
|
||||
return ''
|
||||
|
||||
const time = dayjs(input)
|
||||
if (!time.isValid())
|
||||
return ''
|
||||
|
||||
const now = dayjs()
|
||||
|
||||
const diffSeconds = now.diff(time, 'second')
|
||||
if (diffSeconds < 0) {
|
||||
// 未来时间兜底
|
||||
return time.format('YYYY年MM月DD日')
|
||||
}
|
||||
|
||||
// 1 分钟内
|
||||
if (diffSeconds < 60) {
|
||||
return '刚刚'
|
||||
}
|
||||
|
||||
const diffMinutes = now.diff(time, 'minute')
|
||||
if (diffMinutes < 60) {
|
||||
return `${diffMinutes}分钟前`
|
||||
}
|
||||
|
||||
const diffHours = now.diff(time, 'hour')
|
||||
if (diffHours < 24) {
|
||||
return `${diffHours}小时前`
|
||||
}
|
||||
|
||||
const diffDays = now.diff(time, 'day')
|
||||
if (diffDays < 14) {
|
||||
return `${diffDays}天前`
|
||||
}
|
||||
|
||||
// 本年
|
||||
if (time.year() === now.year()) {
|
||||
return time.format('MM月DD日')
|
||||
}
|
||||
|
||||
// 非本年
|
||||
return time.format('YYYY年MM月DD日')
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<script lang="ts" setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import Vcode from 'vue3-puzzle-vcode'
|
||||
import { useRouter } from 'vue-router'
|
||||
import EmojiPopover from '@/components/EmojiPopover.vue'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '请输入内容...' },
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
const emits = defineEmits(['update:modelValue', 'submit'])
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const showVcode = ref<boolean>(false)
|
||||
|
||||
const inputRef = ref<HTMLElement | null>(null)
|
||||
const inputValue = ref('')
|
||||
|
||||
const isInputFocused = ref(false) // 是否获得焦点
|
||||
|
||||
const isEmpty = computed(() => {
|
||||
if (inputValue.value.trim() === '') {
|
||||
return true
|
||||
}
|
||||
return inputRef.value?.textContent?.trim() === ''
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
inputValue.value = newVal
|
||||
if (inputRef.value && inputRef.value.innerHTML !== newVal) {
|
||||
inputRef.value.innerHTML = newVal
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 焦点事件处理
|
||||
function handleFocus() {
|
||||
isInputFocused.value = true
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
isInputFocused.value = false
|
||||
}
|
||||
// 输入内容变化处理
|
||||
function handleInputChange(event: any) {
|
||||
let html = event.target.innerHTML || ''
|
||||
|
||||
if (html.trim() === '<br>' || html.trim() === '<div><br></div>') {
|
||||
html = ''
|
||||
event.target.innerHTML = ''
|
||||
}
|
||||
|
||||
inputValue.value = html
|
||||
emits('update:modelValue', inputValue.value)
|
||||
}
|
||||
|
||||
function handleInputPaste(event: any) {
|
||||
event.preventDefault()
|
||||
|
||||
const text = event.clipboardData.getData('text/plain')
|
||||
|
||||
// 获取当前光标位置
|
||||
const selection = window.getSelection()
|
||||
if (selection !== null && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0)
|
||||
|
||||
// 创建一个新的文本节点
|
||||
const textNode = document.createTextNode(text)
|
||||
|
||||
// 将文本节点插入到当前光标位置
|
||||
range.deleteContents()
|
||||
range.insertNode(textNode)
|
||||
|
||||
// 重新设置光标位置到插入的文本节点后面
|
||||
range.setStartAfter(textNode)
|
||||
range.setEndAfter(textNode)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
if (inputRef.value !== null) {
|
||||
inputValue.value = inputRef.value.innerHTML
|
||||
emits('update:modelValue', inputValue.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleInputKeyDown(event: any) {
|
||||
// 判断是否是回车键
|
||||
if (event.key === 'Enter') {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
let lastCaretPosition: { node: Node, offset: number } | null = null
|
||||
|
||||
// 插入内容函数(兼容现代浏览器)
|
||||
function insertContent(content: string) {
|
||||
if (!inputRef.value)
|
||||
return
|
||||
|
||||
const selection = window.getSelection()
|
||||
if (!selection || !lastCaretPosition)
|
||||
return
|
||||
|
||||
const range = document.createRange()
|
||||
range.setStart(lastCaretPosition.node, lastCaretPosition.offset)
|
||||
range.collapse(true)
|
||||
|
||||
const textNode = document.createTextNode(content)
|
||||
range.insertNode(textNode)
|
||||
|
||||
range.setStartAfter(textNode)
|
||||
range.collapse(true)
|
||||
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
|
||||
inputRef.value.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
|
||||
// 保存光标位置
|
||||
function saveCaretPosition() {
|
||||
const selection = window.getSelection()
|
||||
if (!selection?.rangeCount)
|
||||
return
|
||||
|
||||
const range = selection.getRangeAt(0)
|
||||
lastCaretPosition = {
|
||||
node: range.startContainer,
|
||||
offset: range.startOffset,
|
||||
}
|
||||
}
|
||||
|
||||
// 选择emoji
|
||||
function handleEmojiSelect(emoji: string) {
|
||||
insertContent(emoji || '')
|
||||
}
|
||||
// 发送按钮点击
|
||||
function handleSend() {
|
||||
// 判断是否为空内容
|
||||
if (inputRef.value?.textContent?.trim() === '') {
|
||||
ElMessage.warning('发送内容不能为空!')
|
||||
return
|
||||
}
|
||||
showVcode.value = true
|
||||
}
|
||||
|
||||
function handleValiteVcodeSuccess() {
|
||||
showVcode.value = false
|
||||
|
||||
// 发送内容给父组件
|
||||
emits('submit', inputValue.value)
|
||||
|
||||
nextTick(() => {
|
||||
inputValue.value = ''
|
||||
if (inputRef.value) {
|
||||
inputRef.value.textContent = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
function handleToLogin() {
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="userStore.isLogined" class="custom-input-container" :class="{ 'is-focused': isInputFocused }" v-bind="$attrs">
|
||||
<div
|
||||
ref="inputRef"
|
||||
contenteditable="true"
|
||||
class="custom-input"
|
||||
:data-placeholder="placeholder"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInputChange"
|
||||
@paste="handleInputPaste"
|
||||
@keydown="handleInputKeyDown"
|
||||
@click="saveCaretPosition"
|
||||
@keyup="saveCaretPosition"
|
||||
/>
|
||||
<div class="custom-input-footer">
|
||||
<EmojiPopover @select="handleEmojiSelect" />
|
||||
<div>
|
||||
<el-button type="primary" size="small" class="send-button" :disabled="isEmpty" :loading="loading" @click.stop="handleSend">
|
||||
发送
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="nologin-container">
|
||||
<div class="nologin-title">
|
||||
尚未登陆,请先登录后发表评论!
|
||||
</div>
|
||||
<el-button type="primary" size="small" @click="handleToLogin">
|
||||
登录/注册
|
||||
</el-button>
|
||||
</div>
|
||||
<Vcode :show="showVcode" @success="handleValiteVcodeSuccess" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid transparent;
|
||||
background-color: #f2f3f5;
|
||||
min-height: 100px;
|
||||
max-height: 160px;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nologin-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f2f3f5;
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
|
||||
gap: 8px;
|
||||
.nologin-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-input-container.is-focused {
|
||||
border-color: rgb(22, 93, 255);
|
||||
background-color: #fff !important;
|
||||
}
|
||||
.custom-input-container:hover {
|
||||
background-color: #e5e6eb;
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
border: none;
|
||||
resize: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
|
||||
&[data-placeholder]:empty:before {
|
||||
content: attr(data-placeholder);
|
||||
color: #999;
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:focus:empty:before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-input-footer {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.send-button {
|
||||
width: 60px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,303 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PostComment, PublicPostDetail } from '@/types/post'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { createPostCommentApi } from '@/request/api/post'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import CommentInput from './comment-input.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: PostComment[]
|
||||
postDetail: PublicPostDetail
|
||||
}>()
|
||||
|
||||
const commentList = ref<PostComment[]>([])
|
||||
const userStore = useUserStore()
|
||||
|
||||
const replyCommentId = ref<number | null>(0)
|
||||
const replyCommentLoading = ref<boolean>(false)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
commentList.value = newVal
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
|
||||
function handleReplyComment(commentId: number) {
|
||||
if (replyCommentId.value === commentId) {
|
||||
replyCommentId.value = null
|
||||
}
|
||||
else {
|
||||
replyCommentId.value = commentId
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
window.scrollTo({
|
||||
top: document.documentElement.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function findRootComment(commentList: PostComment[], commentId: number | null): PostComment | undefined {
|
||||
// 使用 for 循环遍历一级评论列表
|
||||
for (const item of commentList) {
|
||||
// 如果找到目标评论,直接返回该评论
|
||||
if (item.id === commentId) {
|
||||
return item
|
||||
}
|
||||
|
||||
// 如果该评论有子评论,检查子评论
|
||||
if (item.children) {
|
||||
const childComment = item.children.find(child => child.id === commentId)
|
||||
if (childComment) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到对应评论,返回 undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
function handleReplyCommentSubmit(content: string) {
|
||||
if (content === '') {
|
||||
ElMessage.error('评论内容不能为空!')
|
||||
return
|
||||
}
|
||||
|
||||
replyCommentLoading.value = true
|
||||
|
||||
createPostCommentApi({
|
||||
fatherId: replyCommentId.value,
|
||||
postId: props.postDetail.id,
|
||||
content,
|
||||
})
|
||||
.then((res) => {
|
||||
// 显示成功消息
|
||||
ElMessage.success(res.message)
|
||||
|
||||
// 查找根评论
|
||||
const rootComment = findRootComment(commentList.value, replyCommentId.value)
|
||||
|
||||
// 如果根评论存在,并且根评论没有子评论,初始化子评论数组
|
||||
if (rootComment) {
|
||||
if (!rootComment.children) {
|
||||
rootComment.children = [] // 初始化子评论数组
|
||||
}
|
||||
|
||||
// 将新回复添加到子评论中
|
||||
rootComment.children.push({
|
||||
id: res.data.id,
|
||||
fatherId: res.data.fatherId,
|
||||
content: res.data.content,
|
||||
createTime: res.data.createTime,
|
||||
children: [],
|
||||
publishUser: {
|
||||
id: userStore.userInfo.user.id,
|
||||
companyId: userStore.userInfo.company.id,
|
||||
name: userStore.userInfo.user.name,
|
||||
avatar: userStore.userInfo.company.logo,
|
||||
},
|
||||
replyUser: {
|
||||
id: rootComment.publishUser.id,
|
||||
companyId: rootComment.publishUser.companyId,
|
||||
name: rootComment.publishUser.name,
|
||||
avatar: rootComment.publishUser.avatar,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
commentList.value.push({
|
||||
id: res.data.id,
|
||||
fatherId: res.data.fatherId,
|
||||
content: res.data.content,
|
||||
createTime: res.data.createTime,
|
||||
children: [],
|
||||
publishUser: {
|
||||
id: userStore.userInfo.user.id,
|
||||
companyId: userStore.userInfo.company.id,
|
||||
name: userStore.userInfo.user.name,
|
||||
avatar: userStore.userInfo.company.logo,
|
||||
},
|
||||
replyUser: null,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.finally(() => {
|
||||
replyCommentId.value = null
|
||||
replyCommentLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="item in commentList" :key="item.id" class="custom-comment">
|
||||
<div class="custom-comment-left">
|
||||
<el-avatar class="custom-comment-avatar" :src="item.publishUser.avatar || '/no-avatar.png'" :size="70" />
|
||||
</div>
|
||||
<div class="custom-comment-right">
|
||||
<div class="custom-comment-meta">
|
||||
<div class="custom-comment-title">
|
||||
<div class="custom-comment-author">
|
||||
{{ item.publishUser.name }}
|
||||
</div>
|
||||
<div v-if="item.publishUser.companyId === postDetail.companyId" class="custom-comment-author-tag">
|
||||
作者
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-comment-time">
|
||||
{{ item.createTime }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-comment-content" v-html="item.content" />
|
||||
<div class="custom-comment-actions">
|
||||
<div class="custom-comment-action" :style="{ color: replyCommentId === item.id ? '#165dff' : '#1d2129' }" @click="handleReplyComment(item.id)">
|
||||
<SvgIcon icon="mingcute:message-3-line" />
|
||||
<div>{{ replyCommentId === item.id ? "取消回复" : "回复" }}</div>
|
||||
</div>
|
||||
<div class="custom-comment-action flex ml-auto">
|
||||
<SvgIcon icon="akar-icons:more-horizontal-fill" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 父级评论输入框 -->
|
||||
<CommentInput v-if="replyCommentId === item.id" :loading="replyCommentLoading" @submit="handleReplyCommentSubmit" />
|
||||
|
||||
<!-- 子评论 -->
|
||||
<div v-if="item.children && item.children.length > 0" class="custom-comment-children">
|
||||
<div v-for="child in item.children" :key="child.id" class="custom-comment">
|
||||
<div class="custom-comment-left">
|
||||
<el-avatar class="custom-comment-avatar" :src="item.publishUser.avatar || '/no-avatar.png'" :size="70" />
|
||||
</div>
|
||||
<div class="custom-comment-right">
|
||||
<div class="custom-comment-meta">
|
||||
<div class="custom-comment-title">
|
||||
<div class="custom-comment-author">
|
||||
{{ child.publishUser.name }}
|
||||
</div>
|
||||
<div v-if="child.publishUser.companyId === postDetail.companyId" class="custom-comment-author-tag">
|
||||
作者
|
||||
</div>
|
||||
<template v-if="child.replyUser && child.fatherId !== item.id">
|
||||
<div>回复</div>
|
||||
<div class="custom-comment-author">
|
||||
{{ child.replyUser.name }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="custom-comment-time">
|
||||
{{ child.createTime }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-comment-content">
|
||||
{{ child.content }}
|
||||
</div>
|
||||
<div class="custom-comment-actions">
|
||||
<div class="custom-comment-action" :style="{ color: replyCommentId === child.id ? '#165dff' : '#1d2129' }" @click="handleReplyComment(child.id)">
|
||||
<SvgIcon icon="mingcute:message-3-line" />
|
||||
<div>{{ replyCommentId === child.id ? "取消回复" : "回复" }}</div>
|
||||
</div>
|
||||
<div class="custom-comment-action flex ml-auto">
|
||||
<SvgIcon icon="akar-icons:more-horizontal-fill" />
|
||||
</div>
|
||||
</div>
|
||||
<CommentInput v-if="replyCommentId === child.id" :loading="replyCommentLoading" @submit="handleReplyCommentSubmit" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-comment {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.custom-comment:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.custom-comment {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.custom-comment-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.custom-comment-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
}
|
||||
.custom-comment-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.custom-comment-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.custom-comment-title {
|
||||
font-size: 14px;
|
||||
color: #4e5969;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.custom-comment-author {
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-comment-author-tag {
|
||||
background-color: #165dff;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
padding: 1px 8px;
|
||||
}
|
||||
.custom-comment-time {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
.custom-comment-content {
|
||||
font-size: 14px;
|
||||
color: #1d2129;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.custom-comment-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
.custom-comment-action {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
color: #1d2129;
|
||||
}
|
||||
.custom-comment-action:hover {
|
||||
color: #165dff;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-comment-children {
|
||||
margin-left: 20px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,380 @@
|
||||
<script setup lang="ts">
|
||||
import type { AnchorMenuItem } from '@/components/anchor-menu/type'
|
||||
import type { PostComment, PostCompany, PublicPostDetail } from '@/types/post'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import AnchorMenu from '@/components/anchor-menu/index.vue'
|
||||
import CustomEmpty from '@/components/CustomEmpty.vue'
|
||||
import WandEditor from '@/components/wang-editor/index.vue'
|
||||
import { createPostCommentApi, getPostCompanyById, getPublicPostDetailApi, listPostCommentApi } from '@/request/api/post'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { formatPostTime } from '@/utils/time'
|
||||
import CommentInput from './components/comment-input.vue'
|
||||
import CommentList from './components/comment-list.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const id = Number(route.params.id)
|
||||
|
||||
const loading = ref<boolean>(true)
|
||||
const postCompany = ref<PostCompany>({
|
||||
id: 0,
|
||||
name: '',
|
||||
logo: '',
|
||||
profile: '',
|
||||
areas: [],
|
||||
industries: [],
|
||||
})
|
||||
|
||||
const postDetail = ref<PublicPostDetail>({
|
||||
id: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
tags: [],
|
||||
activeTime: '',
|
||||
|
||||
viewCount: 0,
|
||||
commentCount: 0,
|
||||
|
||||
companyId: 0,
|
||||
})
|
||||
|
||||
const postComments = ref<PostComment[]>([])
|
||||
|
||||
const anchorMenu = ref<AnchorMenuItem[]>([])
|
||||
|
||||
const rootComment = ref<string>('')
|
||||
const rootCommentLoading = ref<boolean>(false)
|
||||
|
||||
onMounted(async () => {
|
||||
if (!id) {
|
||||
router.push('/post')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 获取帖子详情
|
||||
const postRes = await getPublicPostDetailApi(id)
|
||||
if (!postRes.isSuccess) {
|
||||
throw new Error(postRes.message || '加载帖子失败,请稍后再试')
|
||||
}
|
||||
postDetail.value = postRes.data
|
||||
console.log(postDetail.value)
|
||||
|
||||
// 获取公司信息
|
||||
const companyRes = await getPostCompanyById(postDetail.value.companyId)
|
||||
if (!companyRes.isSuccess) {
|
||||
throw new Error(companyRes.message || '加载公司信息失败,请稍后再试')
|
||||
}
|
||||
|
||||
postCompany.value = companyRes.data
|
||||
console.log(postCompany.value)
|
||||
|
||||
// 获取评论信息
|
||||
const commentRes = await listPostCommentApi(postDetail.value.id)
|
||||
if (!commentRes.isSuccess) {
|
||||
throw new Error(commentRes.message || '加载评论信息失败,请稍后再试')
|
||||
}
|
||||
|
||||
postComments.value = commentRes.data
|
||||
}
|
||||
catch (err: any) {
|
||||
console.log(err)
|
||||
ElMessage.error(err.message || '加载数据失败,请稍后再试')
|
||||
router.push('/post')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function handleUpdateEditorDirectory(menus: AnchorMenuItem[]) {
|
||||
anchorMenu.value = anchorMenu.value.concat(menus)
|
||||
}
|
||||
|
||||
function handleSubmitRootComment(content: string) {
|
||||
rootCommentLoading.value = true
|
||||
|
||||
createPostCommentApi({
|
||||
fatherId: null,
|
||||
postId: postDetail.value.id,
|
||||
content,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.isSuccess) {
|
||||
rootComment.value = ''
|
||||
ElMessage.success(res.message)
|
||||
|
||||
postComments.value.push({
|
||||
id: res.data.id,
|
||||
fatherId: res.data.fatherId,
|
||||
content: res.data.content,
|
||||
createTime: res.data.createTime,
|
||||
children: [],
|
||||
publishUser: {
|
||||
id: userStore.userInfo.user.id,
|
||||
companyId: userStore.userInfo.company.id,
|
||||
name: userStore.userInfo.user.name,
|
||||
avatar: userStore.userInfo.company.logo,
|
||||
},
|
||||
replyUser: null,
|
||||
})
|
||||
}
|
||||
else {
|
||||
ElMessage.error(res.message || '评论失败,请稍后再试')
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
rootCommentLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="post">
|
||||
<div class="post-left">
|
||||
<el-affix :offset="90">
|
||||
<el-anchor type="underline">
|
||||
<AnchorMenu :menu="anchorMenu" />
|
||||
</el-anchor>
|
||||
</el-affix>
|
||||
</div>
|
||||
|
||||
<div class="post-center">
|
||||
<div class="post-main">
|
||||
<div class="post-title">
|
||||
{{ postDetail.title }}
|
||||
</div>
|
||||
<div class="post-meta">
|
||||
<div class="post-meta-item">
|
||||
<SvgIcon icon="mdi:alarm" :size="18" />
|
||||
<span>{{ formatPostTime(postDetail.activeTime) }} </span>
|
||||
</div>
|
||||
<div class="post-meta-item">
|
||||
<SvgIcon icon="mdi:eye-outline" :size="18" />
|
||||
<span>{{ postDetail.viewCount }} </span>
|
||||
</div>
|
||||
<div class="post-meta-item">
|
||||
<SvgIcon icon="mdi:comment-outline" :size="16" />
|
||||
<span>{{ postDetail.commentCount }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post-content">
|
||||
<WandEditor v-model="postDetail.content" mode="view" @update:directory="handleUpdateEditorDirectory" />
|
||||
</div>
|
||||
|
||||
<div class="post-comment">
|
||||
<div class="divider" />
|
||||
|
||||
<div class="post-comment-title">
|
||||
评论
|
||||
</div>
|
||||
|
||||
<div class="primary-comment">
|
||||
<el-avatar class="post-comment-avatar" :src="userStore.userInfo.company.logo || '/no-avatar.png'" :size="46" />
|
||||
|
||||
<CommentInput v-model="rootComment" :loading="rootCommentLoading" class="post-comment-input" @submit="handleSubmitRootComment" />
|
||||
</div>
|
||||
|
||||
<div class="reply-comment">
|
||||
<div v-if="postComments.length > 0" class="reply-comment-list">
|
||||
<CommentList v-model="postComments" :post-detail="postDetail" />
|
||||
</div>
|
||||
<CustomEmpty v-else title="暂时没有评论 🤔" :in-container="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post-right">
|
||||
<el-affix :offset="70">
|
||||
<div class="post-company">
|
||||
<el-avatar :src="postCompany.logo || '/no-avatar.png'" :size="70" class="company-logo" />
|
||||
<div class="company-info">
|
||||
<h3 class="company-name">
|
||||
{{ postCompany.name }}
|
||||
</h3>
|
||||
|
||||
<div v-if="postCompany.areas.length > 0" class="company-meta">
|
||||
<!-- 成立于 2012 年 · 上海 -->
|
||||
{{ postCompany.areas.join("/") }}
|
||||
</div>
|
||||
|
||||
<div class="company-desc">
|
||||
{{ postCompany.profile }}
|
||||
</div>
|
||||
|
||||
<div class="company-tags">
|
||||
<span v-for="industry in postCompany.industries" :key="industry" class="tag">{{ industry }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-affix>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.post {
|
||||
padding: 24px 100px;
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
.post-left {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.anchor-title {
|
||||
background-color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-center {
|
||||
flex: 1;
|
||||
margin: 0px 32px;
|
||||
.post-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #ffffff;
|
||||
padding: 0px 16px 12px 16px;
|
||||
width: 100%;
|
||||
|
||||
.post-title {
|
||||
font-weight: bold;
|
||||
font-size: 28px;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
.post-meta-item {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #8a919f;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& + .post-meta-item {
|
||||
margin-left: 12px;
|
||||
}
|
||||
span {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.post-comment {
|
||||
padding: 0px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: #e0e0e0;
|
||||
margin: 18px 0px;
|
||||
}
|
||||
|
||||
.post-comment-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.primary-comment {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
.primary-comment-avatar {
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.post-comment-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.reply-comment {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 32px;
|
||||
.reply-comment-list {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-right {
|
||||
width: 360px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.post-company {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
margin: 0 auto;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
.company-info {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.company-meta {
|
||||
margin: 6px 0 10px;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.company-desc {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #555;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.company-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
background-color: #eef5ff;
|
||||
color: #3a7afe;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,102 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PostTag } from '@/types/base'
|
||||
import type { PublicPost, QueryPublicPostParams } from '@/types/post'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CustomEmpty from '@/components/CustomEmpty.vue'
|
||||
import CustomLoading from '@/components/CustomLoading.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { queryPublicPostApi } from '@/request/api/post'
|
||||
import { useCommonStore } from '@/store/common'
|
||||
import { formatPostTime } from '@/utils/time'
|
||||
|
||||
const router = useRouter()
|
||||
const { postTagOptions } = useCommonStore()
|
||||
|
||||
const activeNames = ref(['1'])
|
||||
const selectedTag = ref<PostTag>(postTagOptions[0])
|
||||
const selectedTag = ref<PostTag | null>(null)
|
||||
|
||||
const tableData = ref<PublicPost[]>([])
|
||||
|
||||
const page = reactive<Pagination>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
const queryForm = reactive<QueryPublicPostParams>({
|
||||
currentPage: page.current,
|
||||
pageSize: page.pageSize,
|
||||
|
||||
title: '',
|
||||
tag: '',
|
||||
})
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
onMounted(() => {
|
||||
queryPost()
|
||||
})
|
||||
|
||||
watch(
|
||||
postTagOptions,
|
||||
(list) => {
|
||||
if (list.length && !selectedTag.value) {
|
||||
selectedTag.value = list[0]
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function handleChangeSelectedTag(tag: PostTag) {
|
||||
selectedTag.value = tag
|
||||
|
||||
if (selectedTag.value.id === 0) {
|
||||
queryForm.tag = ''
|
||||
}
|
||||
else {
|
||||
queryForm.tag = selectedTag.value.name
|
||||
}
|
||||
|
||||
queryForm.title = ''
|
||||
|
||||
page.current = 1
|
||||
|
||||
queryPost()
|
||||
}
|
||||
|
||||
function queryPost() {
|
||||
loading.value = true
|
||||
queryPublicPostApi({
|
||||
...queryForm,
|
||||
currentPage: page.current,
|
||||
pageSize: page.pageSize,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
tableData.value = res.data.records
|
||||
page.total = res.data.total
|
||||
}
|
||||
else {
|
||||
ElMessage.error(res.message || '查询失败,请稍后再试')
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
ElMessage.error('查询失败,请稍后再试')
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.current = 1
|
||||
|
||||
queryPost()
|
||||
}
|
||||
|
||||
function toDetail(data: PublicPost) {
|
||||
router.push({
|
||||
path: `/post/detail/${data.id}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -26,7 +110,7 @@ function handleChangeSelectedTag(tag: PostTag) {
|
||||
v-for="item in postTagOptions"
|
||||
:key="item.id"
|
||||
class="tag-item"
|
||||
:class="{ active: selectedTag.id === item.id }"
|
||||
:class="{ active: selectedTag && (selectedTag.id === item.id) }"
|
||||
@click="handleChangeSelectedTag(item)"
|
||||
>
|
||||
<SvgIcon v-if="item.icon" :icon="item.icon" />
|
||||
@@ -40,55 +124,70 @@ function handleChangeSelectedTag(tag: PostTag) {
|
||||
<div class="post-main">
|
||||
<div class="post-main-header">
|
||||
<el-input
|
||||
v-model="queryForm.title"
|
||||
:prefix-icon="Search"
|
||||
placeholder="搜索" size="large"
|
||||
placeholder="搜索"
|
||||
size="large"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<div class="post-pagination">
|
||||
<el-pagination layout="prev, pager, next,jumper" background :total="1000" />
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
:page-size="page.pageSize"
|
||||
:total="page.total"
|
||||
layout="prev, pager, next,jumper"
|
||||
size="small"
|
||||
background
|
||||
@change="queryPost"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post-content">
|
||||
<!-- <CustomEmpty :in-container="true" /> -->
|
||||
<CustomLoading v-if="loading" :in-container="true" />
|
||||
|
||||
<div class="post-list">
|
||||
<div v-for="i in 20" :key="i" class="post-item">
|
||||
<div v-else-if="tableData.length > 0" class="post-list">
|
||||
<div v-for="item in tableData" :key="item.id" class="post-item">
|
||||
<div class="post-title-row">
|
||||
<div class="post-title">
|
||||
新项目为什么推荐WebFlux,而非SpringMVC?
|
||||
<div class="post-title" @click="toDetail(item)">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-meta">
|
||||
<div class="post-meta-actions">
|
||||
<div class="post-meta-item">
|
||||
<div>刘大华</div>
|
||||
<div class="post-meta-company">
|
||||
{{ item.companyName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post-meta-divider" />
|
||||
|
||||
<div class="post-meta-item">
|
||||
<SvgIcon icon="mdi:eye-outline" />
|
||||
<span>1.7k</span>
|
||||
<span>{{ item.viewCount }}</span>
|
||||
</div>
|
||||
<div class="post-meta-item">
|
||||
<SvgIcon icon="iconamoon:comment" />
|
||||
<span>1.7k</span>
|
||||
<span>{{ item.commentCount }}</span>
|
||||
</div>
|
||||
|
||||
<div class="post-meta-divider" />
|
||||
|
||||
<div class="post-meta-item">
|
||||
<div>20分钟前</div>
|
||||
<div>{{ formatPostTime(item.activeTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-meta-tags">
|
||||
<div class="post-tag">
|
||||
后端
|
||||
<div v-for="tag in item.tags" :key="tag" class="post-tag">
|
||||
{{ tag }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CustomEmpty v-else :in-container="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,6 +195,7 @@ function handleChangeSelectedTag(tag: PostTag) {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.post-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 0px 240px;
|
||||
background-color: #fff;
|
||||
@@ -153,8 +253,10 @@ function handleChangeSelectedTag(tag: PostTag) {
|
||||
}
|
||||
|
||||
.post-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
.post-main-header {
|
||||
position: sticky;
|
||||
@@ -169,6 +271,7 @@ function handleChangeSelectedTag(tag: PostTag) {
|
||||
}
|
||||
|
||||
.post-content {
|
||||
flex: 1;
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
@@ -177,19 +280,24 @@ function handleChangeSelectedTag(tag: PostTag) {
|
||||
flex-direction: column;
|
||||
|
||||
.post-item {
|
||||
padding: 12px 16px;
|
||||
padding: 6px 16px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
|
||||
.post-title-row {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.post-title {
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #252933;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #ff6600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,12 +308,13 @@ function handleChangeSelectedTag(tag: PostTag) {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.post-meta-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.post-meta-item {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: #8a919f;
|
||||
flex-shrink: 0;
|
||||
@@ -215,13 +324,17 @@ function handleChangeSelectedTag(tag: PostTag) {
|
||||
margin-left: 12px;
|
||||
}
|
||||
span {
|
||||
margin-left: 4px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.post-meta-company {
|
||||
color: #0053a1;
|
||||
}
|
||||
}
|
||||
|
||||
.post-meta-divider {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
height: 16px;
|
||||
background-color: #e4e6eb;
|
||||
margin: 0 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+2
@@ -93,9 +93,11 @@ const props = defineProps<{
|
||||
modelValue: ForumCommentReuslt[];
|
||||
postDetail: ForumPostDetailResult;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", data: ForumCommentReuslt[]): void;
|
||||
}>();
|
||||
|
||||
const commentList = ref<ForumCommentReuslt[]>([]);
|
||||
|
||||
watch(
|
||||
|
||||
Reference in new issue
Block a user