This commit is contained in:
oneao committed 2025-12-27 20:07:15 +08:00
commit e4ea14506f
117 files changed
+4247 -1055

No files matched your search

+9
View File
@@ -24,6 +24,8 @@
<druid.version>1.2.23</druid.version>
<mybatisplus.version>3.5.10.1</mybatisplus.version>
<apachecommons.version>3.12.0</apachecommons.version>
<netty.version>4.1.86.Final</netty.version>
</properties>
<dependencies>
@@ -100,6 +102,13 @@
<artifactId>commons-lang3</artifactId>
<version>${apachecommons.version}</version>
</dependency>
<!-- Netty -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty.version}</version>
</dependency>
</dependencies>
@@ -1,6 +1,6 @@
package cn.link56.base.mapper;
import cn.link56.base.model.po.AreaPO;
import cn.link56.base.model.po.Area;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@@ -11,5 +11,5 @@ public interface AreaMapper {
* 获取所有区域信息
* @return 区域集合
*/
List<AreaPO> listAllArea();
List<Area> listAllArea();
}
@@ -1,9 +1,9 @@
package cn.link56.base.mapper;
import cn.link56.base.model.po.FilePO;
import cn.link56.base.model.po.File;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FileMapper extends BaseMapper<FilePO> {
public interface FileMapper extends BaseMapper<File> {
}
@@ -1,9 +1,9 @@
package cn.link56.base.mapper;
import cn.link56.base.model.po.IndustryPO;
import cn.link56.base.model.po.Industry;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface IndustryMapper extends BaseMapper<IndustryPO> {
public interface IndustryMapper extends BaseMapper<Industry> {
}
@@ -7,7 +7,7 @@ import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AreaPO {
public class Area {
private String id;
private Integer xh;
private String name;
@@ -11,7 +11,7 @@ import lombok.experimental.Accessors;
@NoArgsConstructor
@TableName("b_file")
@Accessors(chain = true)
public class FilePO {
public class File {
@TableId(value = "b_id")
private Long id;
@@ -34,7 +34,7 @@ public class FilePO {
private String createTime;
@TableField(
value = "b_create_user_id",
value = "b_create_by",
fill = FieldFill.INSERT
)
private String createBy;
@@ -13,7 +13,7 @@ import lombok.experimental.Accessors;
@NoArgsConstructor
@TableName("b_industry")
@Accessors(chain = true)
public class IndustryPO {
public class Industry {
@TableId(value = "b_id")
private Long id;
@TableField("b_xh")
@@ -2,8 +2,8 @@ package cn.link56.base.service;
import cn.link56.base.mapper.AreaMapper;
import cn.link56.base.mapper.IndustryMapper;
import cn.link56.base.model.po.AreaPO;
import cn.link56.base.model.po.IndustryPO;
import cn.link56.base.model.po.Area;
import cn.link56.base.model.po.Industry;
import cn.link56.base.model.vo.BaseDataVO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Service;
@@ -24,8 +24,8 @@ public class BaseDataService {
// 查询可用行业
List<BaseDataVO.Industry> industries = IndustryMapper.selectList(
new LambdaQueryWrapper<IndustryPO>()
.eq(IndustryPO::getCanuse, "1")
new LambdaQueryWrapper<Industry>()
.eq(Industry::getCanuse, "1")
).stream()
.map(industry -> new BaseDataVO.Industry(
industry.getId(),
@@ -37,7 +37,7 @@ public class BaseDataService {
baseDataVO.setIndustries(industries);
// 地区
List<AreaPO> allAreas = AreaMapper.listAllArea();
List<Area> allAreas = AreaMapper.listAllArea();
List<BaseDataVO.Area> areaTree = buildAreaTree(allAreas);
baseDataVO.setAreas(areaTree);
@@ -47,7 +47,7 @@ public class BaseDataService {
/**
* BArea → BaseDataVO.Area 树
*/
private List<BaseDataVO.Area> buildAreaTree(List<AreaPO> areas) {
private List<BaseDataVO.Area> buildAreaTree(List<Area> areas) {
if (areas == null || areas.isEmpty()) {
return Collections.emptyList();
}
@@ -56,7 +56,7 @@ public class BaseDataService {
Map<String, BaseDataVO.Area> map = new HashMap<>(areas.size());
// 1️⃣ 先全部转 VO
for (AreaPO a : areas) {
for (Area a : areas) {
BaseDataVO.Area vo = new BaseDataVO.Area(
a.getId(),
a.getXh(),
@@ -2,7 +2,7 @@ package cn.link56.base.service;
import cn.link56.base.mapper.FileMapper;
import cn.link56.base.model.dto.AddFileDTO;
import cn.link56.base.model.po.FilePO;
import cn.link56.base.model.po.File;
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
@@ -12,7 +12,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class FileService extends ServiceImpl<FileMapper, FilePO> {
public class FileService extends ServiceImpl<FileMapper, File> {
@Resource
private FileMapper fileMapper;
@@ -30,13 +30,13 @@ public class FileService extends ServiceImpl<FileMapper, FilePO> {
}
}
FilePO filePO = new FilePO();
filePO.setId(IdGenerator.nextId())
File file = new File();
file.setId(IdGenerator.nextId())
.setExt(suffix)
.setName(fileName)
.setMapName(fileMapName)
.setSize(fileSize);
fileMapper.insert(filePO);
fileMapper.insert(file);
}
}
@@ -24,6 +24,8 @@ public class AutoMetaObjectHandler implements MetaObjectHandler {
this.strictInsertFill(metaObject, "createTime", Date.class, date);
this.strictInsertFill(metaObject, "updateTime", Date.class, date);
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
}
@Override
@@ -33,6 +35,7 @@ public class AutoMetaObjectHandler implements MetaObjectHandler {
if (ObjectUtils.isNotEmpty(userId) && userId > 0){
this.strictUpdateFill(metaObject, "updateBy", Long.class, userId);
}
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}
@@ -0,0 +1,13 @@
package cn.link56.common.utils;
import java.time.Instant;
public class TimeUtils {
public static long getCurrentTimestampMillis() {
return Instant.now().toEpochMilli();
}
public static long getCurrentTimestampSeconds() {
return Instant.now().getEpochSecond();
}
}
@@ -48,11 +48,12 @@ public class CompanyUser {
@TableField("b_canuse")
private String canuse;
@TableField("b_avatar")
private String avatar;
@TableField(value = "b_create_time",fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@TableField(value = "b_update_time",fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -18,6 +18,5 @@ public class QueryCompanyUserVO {
private String phone;
private String avatar;
private String canuse;
}
@@ -24,7 +24,6 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@@ -241,7 +240,6 @@ public class CompanyService extends ServiceImpl<CompanyMapper, Company> {
// 保存更新后的公司信息
companyMapper.updateById(company);
// 更新用户头像
companyUserService.updateCompanyAvatar(company.getId(),company.getLogo());
}
}
@@ -4,11 +4,13 @@ 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.mapper.CompanyMapper;
import cn.link56.company.mapper.CompanyUserMapper;
import cn.link56.company.model.dto.AddCompanyUserDTO;
import cn.link56.company.model.dto.DeleteCompanyUserDTO;
import cn.link56.company.model.dto.QueryCompanyUserDTO;
import cn.link56.company.model.dto.UpdateCompanyUserDTO;
import cn.link56.company.model.po.Company;
import cn.link56.company.model.po.CompanyUser;
import cn.link56.company.model.vo.QueryCompanyUserVO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -31,6 +33,9 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
@Resource
private CompanyUserMapper companyUserMapper;
@Resource
private CompanyMapper companyMapper;
/**
* 判断邮箱是否重复
* @param email 用户邮箱
@@ -61,22 +66,6 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
}
public void updateCompanyAvatar(Long companyId, String avatar) {
if (StringUtils.isBlank(avatar)) {
return;
}
LambdaUpdateWrapper<CompanyUser> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(CompanyUser::getCompanyId, companyId)
.and(w -> w.isNull(CompanyUser::getAvatar)
.or()
.eq(CompanyUser::getAvatar, ""))
.set(CompanyUser::getAvatar, avatar);
companyUserMapper.update(wrapper);
}
/**
* 校验当前用户是否有权限操作指定的公司
* @param companyId 当前公司ID
@@ -131,6 +120,22 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
return companyUserMapper.selectList(queryWrapper);
}
public void updateCompanyAvatar(Long companyId, String avatar) {
if (StringUtils.isBlank(avatar)) {
return;
}
LambdaUpdateWrapper<CompanyUser> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(CompanyUser::getCompanyId, companyId)
.and(w -> w.isNull(CompanyUser::getAvatar)
.or()
.eq(CompanyUser::getAvatar, ""))
.set(CompanyUser::getAvatar, avatar);
companyUserMapper.update(wrapper);
}
public BasePageVO<QueryCompanyUserVO> getUsersByCompanyId(QueryCompanyUserDTO queryCompanyUserDTO) {
boolean hasPermission = hasPermission(queryCompanyUserDTO.getCompanyId(), UserContext.getUserId());
@@ -256,13 +261,24 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
}
BeanUtils.copyProperties(companyUserDTO, companyUser);
if(StringUtils.isNotBlank(password)) {
if(StringUtils.isNotBlank(password)){
companyUser.setPassword(password);
} else {
companyUser.setPassword(oldPassword);
}
if (StringUtils.isBlank(companyUser.getAvatar())) {
LambdaQueryWrapper<Company> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Company::getId, companyUser.getCompanyId())
.select(Company::getLogo);
Company company = companyMapper.selectOne(wrapper);
if (company != null && StringUtils.isNotBlank(company.getLogo())) {
companyUser.setAvatar(company.getLogo());
}
}
// 更新用户信息
companyUserMapper.updateById(companyUser);
}
@@ -0,0 +1,78 @@
package cn.link56.im;
import cn.link56.im.config.NettyConfig;
import cn.link56.im.handler.ServerHandler;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.cors.CorsConfig;
import io.netty.handler.codec.http.cors.CorsHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
@Component
public class NettyServer {
@Resource
private NettyConfig nettyConfig;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
@PostConstruct
public void start() {
new Thread(this::runServer, "Netty-ImServer-Thread").start();
}
private void runServer() {
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new CorsHandler(CorsConfig.withAnyOrigin().build()));
pipeline.addLast(new WebSocketServerProtocolHandler(nettyConfig.getPath(), null, true, 65536));
pipeline.addLast(new ServerHandler());
}
});
ChannelFuture future = bootstrap.bind(nettyConfig.getPort()).sync();
System.out.println("🚀 Netty 已启动,端口:" + nettyConfig.getPort() + ",路径:" + nettyConfig.getPath());
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
e.printStackTrace();
} finally {
stop();
}
}
@PreDestroy
public void stop() {
if (bossGroup != null) bossGroup.shutdownGracefully();
if (workerGroup != null) workerGroup.shutdownGracefully();
System.out.println("🛑 Netty 已关闭");
}
}
@@ -0,0 +1,21 @@
package cn.link56.im.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "netty")
@Data
public class NettyConfig {
/**
* Netty 服务端端口,默认 9000
*/
private Integer port = 9000;
/**
* WebSocket 路径,默认 /ws
*/
private String path = "/ws";
}
@@ -0,0 +1,18 @@
package cn.link56.im.constants;
public class MessageResponseStatusConstants {
/**
* 操作成功
*/
public static final Integer SUCCESS = 200;
/**
* 系统错误
*/
public static final Integer ERROR = 500;
/**
* 登录失效或已过期
*/
public static final Integer UNAUTHORIZED = 401;
}
@@ -0,0 +1,7 @@
package cn.link56.im.enums;
import lombok.Getter;
public enum MessageTypeEnums {
PING,PONG,ONLINE,OFFLINE,PRIVATE,GROUP
}
@@ -0,0 +1,62 @@
package cn.link56.im.handler;
import cn.link56.im.enums.MessageTypeEnums;
import cn.link56.im.message.MessageRequest;
import cn.link56.im.message.MessageResponse;
import cn.link56.im.utils.MessageWriter;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import org.springframework.stereotype.Component;
@Component
public class MessageHandler {
// 处理消息
public void handleMessage(Channel channel, MessageRequest request) {
switch (MessageTypeEnums.valueOf(request.getType())) {
case PING:
handlePing(channel, request);
break;
case ONLINE:
handleOnline(channel, request);
break;
case OFFLINE:
handleOffline(channel, request);
break;
case PRIVATE:
handlePrivate(channel, request);
break;
case GROUP:
handleGroup(channel, request);
break;
default:
System.out.println("未知消息类型: " + request.getType());
}
}
// 心跳处理
private void handlePing(Channel channel, MessageRequest request) {
MessageWriter.writeToChannel(channel, MessageResponse.success(MessageTypeEnums.PONG));
}
// 上线
private void handleOnline(Channel channel, MessageRequest request) {
// 验证
Long from = request.getFrom();
Object data = request.getData(); // companyId userId
// 账号 Token
}
private void handleOffline(Channel channel, MessageRequest request) {
// 下线逻辑
}
private void handlePrivate(Channel channel, MessageRequest request) {
// 私聊逻辑
}
private void handleGroup(Channel channel, MessageRequest request) {
// 群聊逻辑
}
}
@@ -0,0 +1,50 @@
package cn.link56.im.handler;
import cn.link56.im.message.MessageRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.timeout.IdleStateEvent;
@ChannelHandler.Sharable
public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final MessageHandler messageHandler = new MessageHandler();
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
System.out.println("客户端连接: " + ctx.channel().id().asShortText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
System.out.println("客户端断开: " + ctx.channel().id().asShortText());
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
MessageRequest request = MAPPER.readValue(textWebSocketFrame.text(), MessageRequest.class);
messageHandler.handleMessage(channelHandlerContext.channel(),request);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
System.out.println("连接超时,关闭: " + ctx.channel().id().asShortText());
ctx.close();
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println("Channel 异常, 关闭连接:: " + ctx.channel().id().asShortText());
cause.printStackTrace();
ctx.close();
}
}
@@ -0,0 +1,16 @@
package cn.link56.im.message;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MessageRequest {
private String type;
private Long from;
private Object to;
private Long timestamp;
private Object data;
}
@@ -0,0 +1,50 @@
package cn.link56.im.message;
import cn.link56.common.utils.TimeUtils;
import cn.link56.im.constants.MessageResponseStatusConstants;
import cn.link56.im.enums.MessageTypeEnums;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MessageResponse {
private String type;
private Long from;
private Object data;
private Long timestamp;
private Integer code;
private String message;
public static MessageResponse success() {
return buildResponse(null, MessageResponseStatusConstants.SUCCESS, null, null);
}
public static MessageResponse success(MessageTypeEnums messageTypeEnums) {
return buildResponse(messageTypeEnums.name(), MessageResponseStatusConstants.SUCCESS, null, null);
}
public static MessageResponse error() {
return buildResponse(null, MessageResponseStatusConstants.ERROR, null, "系统异常");
}
public static MessageResponse error(String msg) {
return buildResponse(null, MessageResponseStatusConstants.ERROR, null, msg);
}
private static MessageResponse buildResponse(String type, Integer code, Object data, String message) {
MessageResponse response = new MessageResponse();
response.setType(type);
response.setCode(code);
response.setData(data);
response.setMessage(message);
response.setTimestamp(TimeUtils.getCurrentTimestampMillis());
return response;
}
}
@@ -0,0 +1,41 @@
package cn.link56.im.storage;
import io.netty.channel.Channel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Data
public class Session {
private Long companyId;
private Long userId;
private final Set<Channel> channels;
// 默认构造器,初始化 channels
public Session() {
this.channels = Collections.newSetFromMap(new ConcurrentHashMap<>());
}
// 带参数构造器(不需要传 channels,因为总是新建空 Set)
public Session(Long companyId, Long userId) {
this.companyId = companyId;
this.userId = userId;
this.channels = Collections.newSetFromMap(new ConcurrentHashMap<>());
}
public void addChannel(Channel channel) {
channels.add(channel);
}
public void removeChannel(Channel channel) {
channels.remove(channel);
}
public boolean isOnline() {
return !channels.isEmpty();
}
}
@@ -0,0 +1,51 @@
package cn.link56.im.storage;
import io.netty.channel.Channel;
import java.util.concurrent.ConcurrentHashMap;
public class SessionManager {
private static final ConcurrentHashMap<Long, Session> sessions = new ConcurrentHashMap<>();
/** 添加会话,支持同一个用户多端登录 */
public static void addSession(Long companyId, Long userId, Channel channel) {
if (companyId == null || userId == null || channel == null) return;
// 通过 compute 方法保证线程安全
sessions.compute(userId, (key, session) -> {
if (session == null) {
// 新建 Session
session = new Session(companyId, userId);
}
// 添加新的 Channel 到 Session
session.addChannel(channel);
return session;
});
}
/** 移除某个 Channel */
public static void removeChannel(Long userId, Channel channel) {
if (userId == null || channel == null) return;
Session session = sessions.get(userId);
if (session != null) {
session.removeChannel(channel);
// 如果用户所有连接都断开,删除 Session
if (!session.isOnline()) {
sessions.remove(userId);
}
}
}
/** 获取用户 Session */
public static Session getSession(Long userId) {
return sessions.get(userId);
}
/** 检查用户是否在线 */
public static boolean isOnline(Long userId) {
Session session = sessions.get(userId);
return session != null && session.isOnline();
}
}
@@ -0,0 +1,58 @@
package cn.link56.im.utils;
import cn.link56.im.storage.Session;
import cn.link56.im.storage.SessionManager;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.apache.commons.lang3.ObjectUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MessageWriter {
private static final ObjectMapper MAPPER = new ObjectMapper();
public static void writeToUser(Long userId,Object message){
if (ObjectUtils.isEmpty(userId) || ObjectUtils.isEmpty(message)){
return;
}
Session session = SessionManager.getSession(userId);
Set<Channel> channels = session.getChannels();
if (channels.isEmpty()) return;
for (Channel ch : channels) {
writeToChannel(ch, message);
}
}
public static void writeToChannel(Channel channel, Object message) {
if (channel == null || !channel.isActive() || message == null) {
return;
}
try {
// 转为 JSON 字符串
String payload = MAPPER.writeValueAsString(message);
// 发送给客户端
channel.writeAndFlush(new TextWebSocketFrame(payload));
} catch (Exception e) {
System.out.println("===发送消息失败===");
e.printStackTrace();
}
}
public static void broadcastToUsers(List<Long> userIds, Object message) {
if (message == null || userIds == null || userIds.isEmpty()) return;
for (Long userId : userIds) {
writeToUser(userId, message);
}
}
}
@@ -0,0 +1,9 @@
package cn.link56.post.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/post/view")
public class PostViewController {
}
@@ -1,9 +0,0 @@
package cn.link56.post.mapper;
import cn.link56.post.model.po.PostFavorite;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PostFavoriteMapper extends BaseMapper<PostFavorite> {
}
@@ -1,9 +0,0 @@
package cn.link56.post.mapper;
import cn.link56.post.model.po.PostLike;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PostLikeMapper extends BaseMapper<PostLike> {
}
@@ -0,0 +1,15 @@
package cn.link56.post.mapper;
import cn.link56.post.model.po.PostStat;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface PostStatMapper extends BaseMapper<PostStat> {
int increaseViewCount(@Param("postId") Long postId,@Param("count") Integer count);
int increaseCommentCount(@Param("postId") Long postId,@Param("count") Integer count);
int decreaseCommentCount(@Param("postId") Long postId,@Param("count") Integer count);
}
@@ -10,6 +10,7 @@ import lombok.NoArgsConstructor;
public class CreatePostCommentDTO {
private Long fatherId;
private Long postId;
private Long rootId;
private String content;
}
@@ -15,8 +15,6 @@ public class CreatePostDTO {
private String title;
@NotBlank(message = "帖子内容不能为空")
private String content;
@NotBlank(message = "帖子摘要不能为空")
private String summary;
private String cover;
private List<String> tags;
@@ -1,9 +1,6 @@
package cn.link56.post.model.po;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -38,4 +35,14 @@ public class PostComment {
fill = FieldFill.INSERT
)
private Date createTime;
@TableField("b_root_id")
private Long rootId;
@TableLogic
@TableField(
value = "b_deleted",
fill = FieldFill.INSERT
)
private Integer deleted;
}
@@ -1,30 +0,0 @@
package cn.link56.post.model.po;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("b_post_favorite")
public class PostFavorite {
@TableId(value = "b_id")
private Long id;
@TableField("b_post_id")
private Long postId;
@TableField("b_user_id")
private Long userId;
@TableField(
value = "b_create_time",
fill = FieldFill.INSERT
)
private Long createTime;
}
@@ -11,20 +11,20 @@ import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("b_post_like")
public class PostLike {
@TableId(value = "b_id")
private Long id;
@TableField("b_post_id")
@TableName("b_post_stat")
public class PostStat {
@TableId(value = "b_post_id")
private Long postId;
@TableField("b_user_id")
private Long userId;
@TableField("b_view_count")
private Integer viewCount;
@TableField("b_comment_count")
private Integer commentCount;
@TableField(
value = "b_create_time",
fill = FieldFill.INSERT
value = "b_update_time",
fill = FieldFill.UPDATE
)
private Long createTime;
}
@@ -21,8 +21,6 @@ public class EditorDashboardVO {
public static class Count {
private Long postCount;
private Long viewCount;
private Long likeCount;
private Long favoriteCount;
}
@Data
@@ -13,6 +13,7 @@ import java.util.List;
public class PostCommentVO {
private Long id;
private Long fatherId;
private Long rootId;
private String content;
private Date createTime;
private User publishUser;
@@ -7,7 +7,9 @@ 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.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
@@ -25,6 +27,9 @@ public class PostCommentService extends ServiceImpl<PostCommentMapper, PostComme
@Resource
private PostCommentMapper postCommentMapper;
@Resource
private PostStatService postStatService;
/**
* 根据文章 id 获取浏览总数
*/
@@ -34,11 +39,33 @@ public class PostCommentService extends ServiceImpl<PostCommentMapper, PostComme
.count();
}
/**
* 根据文章 ID 删除所有浏览记录
*/
public void deleteByPostId(Long postId) {
this.lambdaUpdate()
.eq(PostComment::getPostId, postId)
.remove();
}
/**
* 根据文章 id 获取最新一条评论
*/
public PostComment getLatestCommentByPostId(Long postId) {
return null;
if (postId == null) {
return null;
}
Page<PostComment> page = new Page<>(1, 1);
List<PostComment> list = postCommentMapper.selectPage(
page,
new LambdaQueryWrapper<PostComment>()
.eq(PostComment::getPostId, postId)
.orderByDesc(PostComment::getCreateTime)
).getRecords();
return list.isEmpty() ? null : list.get(0);
}
public List<PostCommentVO> listPostComments(Long postId) {
@@ -46,36 +73,38 @@ public class PostCommentService extends ServiceImpl<PostCommentMapper, PostComme
// 1. 查询所有评论
List<PostCommentVO> list = postCommentMapper.selectComments(postId);
// 2. 构建 id -> comment 映射
Map<Long, PostCommentVO> commentMap = new HashMap<>(list.size());
// 2. 构建 rootId -> 根评论 映射
Map<Long, PostCommentVO> rootMap = new HashMap<>();
for (PostCommentVO comment : list) {
comment.setChildren(new ArrayList<>()); // 统一初始化
commentMap.put(comment.getId(), comment);
// 如果是根评论,直接加入 rootMap
if (comment.getRootId() == null || comment.getRootId().equals(comment.getId())) {
rootMap.put(comment.getId(), comment);
}
}
// 3. 收集一级评论
// 3. 挂载子评论到对应根评论的 children
List<PostCommentVO> rootComments = new ArrayList<>();
for (PostCommentVO comment : list) {
if (comment.getFatherId() == null) {
// 根评论已经在 rootMap 中
if (comment.getRootId() == null || comment.getRootId().equals(comment.getId())) {
rootComments.add(comment);
continue;
}
// 查找对应的根评论
PostCommentVO root = rootMap.get(comment.getRootId());
if (root != null) {
root.getChildren().add(comment);
} else {
// rootId 不存在或异常,提升为根评论
rootComments.add(comment);
}
}
// 4. 将所有子评论挂到「根评论」的 children 中
for (PostCommentVO comment : list) {
// 跳过一级评论
if (comment.getFatherId() == null) {
continue;
}
PostCommentVO root = findRootComment(comment, commentMap);
if (root != null) {
root.getChildren().add(comment);
}
}
// 5. 子评论排序(可选)
// 4. 对子评论排序(可选)
rootComments.forEach(root ->
root.getChildren().sort(
Comparator.comparing(PostCommentVO::getCreateTime)
@@ -85,24 +114,6 @@ public class PostCommentService extends ServiceImpl<PostCommentMapper, PostComme
return rootComments;
}
private PostCommentVO findRootComment(
PostCommentVO comment,
Map<Long, PostCommentVO> commentMap) {
PostCommentVO current = comment;
while (current.getFatherId() != null) {
PostCommentVO parent = commentMap.get(current.getFatherId());
if (parent == null) {
return null; // 数据异常,防御
}
current = parent;
}
return current; // fatherId == null 的就是根评论
}
@Transactional
public CreatePostCommentVO createPostComment(CreatePostCommentDTO dto) {
PostComment postComment = new PostComment();
@@ -110,7 +121,15 @@ public class PostCommentService extends ServiceImpl<PostCommentMapper, PostComme
BeanUtils.copyProperties(dto,postComment);
postComment.setId(IdGenerator.nextId());
if (ObjectUtils.isEmpty(postComment.getFatherId())){
postComment.setRootId(postComment.getId());
}
// 插入评论
postCommentMapper.insert(postComment);
// 评论数量 + 1
postStatService.increaseCommentCount(dto.getPostId());
CreatePostCommentVO commentVO = new CreatePostCommentVO();
BeanUtils.copyProperties(postComment,commentVO);
@@ -119,22 +138,35 @@ public class PostCommentService extends ServiceImpl<PostCommentMapper, PostComme
}
@Transactional
public void deletePostComment(@NotBlank(message = "评论ID不能为空") Long postCommentId) {
public void deletePostComment(Long postCommentId) {
PostComment postComment = postCommentMapper.selectById(postCommentId);
if (ObjectUtils.isEmpty(postComment)){
if (postComment == null) {
return;
}
Long postId = postComment.getPostId();
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);
int deletedCount = 0;
// 1. 删除根评论(连同子评论)
if (fatherId == null) {
LambdaQueryWrapper<PostComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PostComment::getId, postCommentId)
.or()
.eq(PostComment::getRootId, postCommentId);
deletedCount = postCommentMapper.delete(wrapper);
}
// 2. 删除子评论
else {
deletedCount = postCommentMapper.deleteById(postCommentId);
}
// 3. 按实际删除条数,减少评论数
if (deletedCount > 0) {
postStatService.decreaseCommentCount(postId, deletedCount);
}
}
}
@@ -1,14 +0,0 @@
package cn.link56.post.service;
import cn.link56.post.mapper.PostFavoriteMapper;
import cn.link56.post.model.po.PostFavorite;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class PostFavoriteService extends ServiceImpl<PostFavoriteMapper, PostFavorite> {
@Resource
private PostFavoriteMapper postFavoriteMapper;
}
@@ -1,14 +0,0 @@
package cn.link56.post.service;
import cn.link56.post.mapper.PostLikeMapper;
import cn.link56.post.model.po.PostLike;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class PostLikeService extends ServiceImpl<PostLikeMapper, PostLike> {
@Resource
private PostLikeMapper postLikeMapper;
}
@@ -36,6 +36,8 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
private PostCommentService postCommentService;
@Resource
private CompanyService companyService;
@Resource
private PostStatService postStatService;
@Transactional
public EditorPostVO createPost(CreatePostDTO dto) {
@@ -48,7 +50,12 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
handleUpdatePublishTime(post, dto.getStatus());
// 新建帖子
postMapper.insert(post);
// 新建帖子数据综合
postStatService.createPostStat(post.getId());
return buildEditPostVO(post);
}
@@ -133,7 +140,16 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
@Transactional
public void deletePost(DeletePostDTO deletePostDTO) {
postMapper.deleteById(deletePostDTO.getId());
Long postId = deletePostDTO.getId();
// 删除帖子
postMapper.deleteById(postId);
// 删除帖子统计
postStatService.removeById(postId);
// 删除帖子浏览
postViewService.deleteByPostId(postId);
// 删除帖子评论
postCommentService.deleteByPostId(postId);
}
@Transactional
@@ -0,0 +1,42 @@
package cn.link56.post.service;
import cn.link56.post.mapper.PostStatMapper;
import cn.link56.post.model.po.PostStat;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class PostStatService extends ServiceImpl<PostStatMapper, PostStat> {
@Resource
private PostStatMapper postStatMapper;
public void increaseViewCount(Long postId) {
postStatMapper.increaseViewCount(postId,1);
}
public void increaseCommentCount(Long postId) {
postStatMapper.increaseCommentCount(postId,1);
}
public void decreaseCommentCount(Long postId) {
postStatMapper.decreaseCommentCount(postId,1);
}
public void decreaseCommentCount(Long postId,Integer count) {
postStatMapper.decreaseCommentCount(postId,count);
}
public void createPostStat(Long postId) {
PostStat stat = new PostStat();
stat.setPostId(postId);
stat.setViewCount(0);
stat.setCommentCount(0);
this.save(stat);
}
}
@@ -21,4 +21,13 @@ public class PostViewService extends ServiceImpl<PostViewMapper, PostView> {
.eq(PostView::getPostId, postId)
.count();
}
/**
* 根据文章 ID 删除所有浏览记录
*/
public void deleteByPostId(Long postId) {
this.lambdaUpdate()
.eq(PostView::getPostId, postId)
.remove();
}
}
@@ -1,5 +1,8 @@
server:
port: 8084
netty:
port: 8090
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
@@ -36,4 +36,9 @@ mybatis-plus:
use-generated-keys: true
default-executor-type: simple
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
map-underscore-to-camel-case: true
global-config:
db-config:
logic-delete-field: deleted
logic-not-delete-value: 0
logic-delete-value: 1
@@ -15,7 +15,7 @@
b_order AS [order]
</sql>
<select id="listAllArea" resultType="cn.link56.base.model.po.AreaPO">
<select id="listAllArea" resultType="cn.link56.base.model.po.Area">
SELECT <include refid="area_columns"/> FROM b_area_sheng WHERE b_canuse = '1'
UNION ALL
SELECT <include refid="area_columns"/> FROM b_area_shi WHERE b_canuse = '1'
@@ -5,6 +5,7 @@
<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="rootId" column="b_root_id"/>
<result property="fatherId" column="b_father_id"/>
<result property="content" column="b_content"/>
<result property="createTime" column="b_create_time"/>
@@ -35,7 +36,7 @@
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}
WHERE c.b_post_id = #{postId} AND c.b_deleted = 0
ORDER BY c.b_create_time
</select>
@@ -56,13 +56,12 @@
p.b_publish_time AS publishTime,
p.b_status AS status,
(SELECT COUNT(1) FROM b_post_view v WHERE v.b_post_id = p.b_id) AS viewCount,
(SELECT COUNT(1) FROM b_post_like l WHERE l.b_post_id = p.b_id) AS likeCount,
(SELECT COUNT(1) FROM b_post_favorite f WHERE f.b_post_id = p.b_id) AS favoriteCount,
(SELECT COUNT(1) FROM b_post_comment c WHERE c.b_post_id = p.b_id) AS commentCount,
ISNULL(ps.b_view_count, 0) AS viewCount,
ISNULL(ps.b_comment_count, 0) AS commentCount,
ROW_NUMBER() OVER (ORDER BY p.b_update_time DESC) AS rn
FROM b_post p
LEFT JOIN b_post_stat ps ON ps.b_post_id = p.b_id
<where>
<if test="userId != null">
AND p.b_create_by = #{userId}
@@ -73,7 +72,7 @@
</if>
<if test="title != null and title != ''">
AND p.b_title LIKE '%' + #{title} + '%'
AND p.b_title LIKE CONCAT('%', #{title}, '%')
</if>
<if test="year != null and year != ''">
@@ -88,6 +87,7 @@
WHERE t.rn BETWEEN #{offset} AND #{limit}
</select>
<select id="selectEditorPostTotal" resultType="java.lang.Long">
SELECT COUNT(*)
FROM b_post p
@@ -114,14 +114,18 @@
</where>
</select>
<select id="selectDashboardOverview" resultType="cn.link56.post.model.vo.EditorDashboardVO$Count">
<select id="selectDashboardOverview"
resultType="cn.link56.post.model.vo.EditorDashboardVO$Count">
SELECT
(SELECT COUNT(*) FROM b_post WHERE b_create_by = #{userId}) AS postCount,
(SELECT COUNT(*) FROM b_post_view WHERE b_post_id IN (SELECT b_id FROM b_post WHERE b_create_by = #{userId})) AS viewCount,
(SELECT COUNT(*) FROM b_post_like WHERE b_post_id IN (SELECT b_id FROM b_post WHERE b_create_by = #{userId})) AS likeCount,
(SELECT COUNT(*) FROM b_post_favorite WHERE b_post_id IN (SELECT b_id FROM b_post WHERE b_create_by = #{userId})) AS favoriteCount
COUNT(p.b_id) AS postCount,
ISNULL(SUM(ps.b_view_count), 0) AS viewCount,
ISNULL(SUM(ps.b_like_count), 0) AS likeCount
FROM b_post p
LEFT JOIN b_post_stat ps ON ps.b_post_id = p.b_id
WHERE p.b_create_by = #{userId}
</select>
<select id="selectDashboardViewStats" resultType="cn.link56.post.model.vo.EditorDashboardVO$Chart">
SELECT
CONVERT(VARCHAR(10), v.b_create_time, 120) AS label,
@@ -0,0 +1,33 @@
<?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.PostStatMapper">
<update id="increaseViewCount">
UPDATE b_post_stat
SET
b_view_count = b_view_count + #{count},
b_update_time = GETDATE()
WHERE b_post_id = #{postId}
</update>
<update id="increaseCommentCount">
UPDATE b_post_stat
SET
b_comment_count = b_comment_count + #{count},
b_update_time = GETDATE()
WHERE b_post_id = #{postId}
</update>
<update id="decreaseCommentCount">
UPDATE b_post_stat
SET
b_comment_count = b_comment_count - #{count},
b_update_time = GETDATE()
WHERE b_post_id = #{postId}
AND b_comment_count >= #{count}
</update>
</mapper>
+1 -12
View File
@@ -31,18 +31,7 @@ function handleInitData() {
.then((res) => {
console.log(res)
if (res.isSuccess) {
const list: PostTag[] = res.data || []
commonOptions.postTagOptions.splice(
0,
commonOptions.postTagOptions.length,
{
id: 0,
name: '全部',
icon: 'fluent-color:apps-28',
},
...list,
)
commonOptions.postTagOptions.splice(0, commonOptions.postTagOptions.length, ...res.data || [])
}
else {
ElMessage.error(res.message || '初始化失败,请刷新页面后重试')
@@ -44,7 +44,7 @@ function handleClickEmoji(emoji: string) {
</script>
<template>
<el-popover trigger="click" position="tl">
<el-popover trigger="click" placement="top-start">
<SvgIcon icon="majesticons:emoji-happy-line" class="chat-window-bottom-icon" />
<template #content>
<div class="emoji-container">
@@ -56,7 +56,7 @@ function onSearch() {
0 4px 12px rgba(0, 0, 0, 0.08);
.query-item {
flex: 1;
border-right: 1px solid #ddd;
// border-right: 1px solid #ddd;
:deep(.el-select__wrapper) {
height: 60px;
border-radius: 0px;
@@ -5,7 +5,6 @@ import {
} from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router'
import SafeImage from '@/components/SafeImage.vue'
import { useUserStore } from '@/store/user'
const route = useRoute()
@@ -25,7 +24,7 @@ function navigateTo(path: string) {
}
function exitLogin() {
userStore.clearUserInfo()
userStore.clear()
router.push('/auth/login')
}
@@ -65,10 +64,10 @@ function isActive(path: string) {
<el-dropdown>
<div class="user">
<SafeImage :size="36" :url="userStore.userInfo.user.avatar || userStore.userInfo.company.logo" />
<SafeImage :url="userStore.user.avatar || userStore.company.logo" :size="36" round />
<div class="info">
<span class="name">{{ userStore.userInfo.user.name }}</span>
<span class="company">{{ userStore.userInfo.company.name }}</span>
<span class="name">{{ userStore.user.name }}</span>
<span class="company">{{ userStore.company.name }}</span>
</div>
</div>
@@ -27,5 +27,5 @@ export function listPostCommentApi(id: number) {
}
export function deletePostCommentApi(id: number) {
return httpGet<void>(`/post/comment/delete/${id}`)
return httpPostJson<void>(`/post/comment/delete/${id}`)
}
@@ -50,15 +50,15 @@ axiosInstance.interceptors.request.use(
if (needAuth) {
if (!userStore.isLogined) {
userStore.clearUserInfo()
userStore.clear()
router.push('/login')
return Promise.reject(new Error('尚未登录,请先登录!'))
}
}
config.headers.Authorization = `Bearer ${userStore.userInfo.token}`
config.headers['X-Company-Id'] = userStore.userInfo.company.id
config.headers['X-User-Id'] = userStore.userInfo.user.id
config.headers.Authorization = `Bearer ${userStore.token}`
config.headers['X-Company-Id'] = userStore.company.id
config.headers['X-User-Id'] = userStore.user.id
return config
},
@@ -1,26 +1,29 @@
import type { Area, CompanyQuery, Industry, PostTag } from '@/types/base'
import { defineStore } from 'pinia'
import { reactive, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
export const useCommonStore = defineStore(
'common',
() => {
const areaOptions = reactive<Area[]>([])
const industryOptions = reactive<Industry[]>([])
const postTagOptions = reactive<PostTag[]>([])
export const useCommonStore = defineStore('common', () => {
const areaOptions = reactive<Area[]>([])
const industryOptions = reactive<Industry[]>([])
const postTagOptions = reactive<PostTag[]>([]) // 原始列表,不含“全部”
const companyQuery = ref<CompanyQuery>({
industries: [],
areas: [],
name: '',
})
const companyQuery = ref<CompanyQuery>({
industries: [],
areas: [],
name: '',
})
return {
areaOptions,
industryOptions,
companyQuery,
postTagOptions,
}
},
)
// 计算属性:带“全部”选项
const postTagOptionsWithAll = computed(() => [
{ id: 0, name: '全部', icon: 'fluent-color:apps-28' },
...postTagOptions,
])
return {
areaOptions,
industryOptions,
companyQuery,
postTagOptions, // 原始数据
postTagOptionsWithAll, // 带“全部”,响应式
}
})
@@ -1,72 +1,93 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
interface IUserInfo {
user: {
id: number
name: string
position: string
role: number
avatar: string
}
company: {
id: number
logo: string
name: string
}
token: string
interface IUser {
id: number
role: number
name: string
position: string
avatar: string
}
interface ICompany {
id: number
logo: string
name: string
}
export const useUserStore = defineStore(
'user',
() => {
const userInfo = ref<IUserInfo>({
user: {
const user = ref<IUser>({
id: 0,
role: 0,
name: '',
position: '',
avatar: '',
})
const company = ref<ICompany>({
id: 0,
logo: '',
name: '',
})
const token = ref('')
// ===== setters =====
const setUser = (val: IUser) => {
user.value = val
}
const setCompany = (val: ICompany) => {
company.value = val
}
const setToken = (val: string) => {
token.value = val
}
// 一次性设置(登录时用)
const setLoginInfo = (data: {
user: IUser
company: ICompany
token: string
}) => {
user.value = data.user
company.value = data.company
token.value = data.token
}
const clear = () => {
user.value = {
id: 0,
role: 0,
name: '',
position: '',
role: 0,
avatar: '',
},
company: {
}
company.value = {
id: 0,
logo: '',
name: '',
},
token: '',
})
const setUserInfo = (val: IUserInfo) => {
userInfo.value = val
}
const clearUserInfo = () => {
userInfo.value = {
user: {
id: 0,
name: '',
position: '',
role: 0,
avatar: '',
},
company: {
id: 0,
logo: '',
name: '',
},
token: '',
}
token.value = ''
}
// ✅ 判断是否登录
// ===== computed =====
const isLogined = computed(() => {
return !!(userInfo.value.token && userInfo.value.user.id && userInfo.value.company.id)
return !!(token.value && user.value.id && company.value.id)
})
return {
userInfo,
setUserInfo,
clearUserInfo,
user,
company,
token,
setUser,
setCompany,
setToken,
setLoginInfo,
clear,
isLogined,
}
},
@@ -21,7 +21,6 @@ export interface LoginApiResult {
position: string
avatar: string
}
token: string
}
@@ -58,6 +58,7 @@ export interface PublicPostDetail {
activeTime: string
viewCount: number
commentCount: number
companyId: number
}
@@ -68,15 +69,14 @@ export interface PostCompany {
logo: string
profile: string
establishmentDate: string
areas: string[]
industries: string[]
}
export interface CreatePostCommentParams {
fatherId: number | null
postId: number
rootId: number | null
fatherId: number | null
content: string
}
export interface CreatePostCommentResult {
@@ -90,6 +90,7 @@ export interface CreatePostCommentResult {
export interface PostComment {
id: number
rootId: number
fatherId: number | null
content: string
createTime: string
@@ -44,6 +44,7 @@ export function formatPostTime(input?: dayjs.ConfigType): string {
// 非本年
return time.format('YYYY年MM月DD日')
}
export function formatCommentTime(input?: dayjs.ConfigType): string {
if (!input)
return ''
@@ -88,10 +89,12 @@ export function formatCommentTime(input?: dayjs.ConfigType): string {
// 非本年
return time.format('YYYY-MM-DD HH:mm')
}
export type DateFormat
type DateFormat
= | 'YYYY'
| 'MM'
| 'DD'
| 'YYYY-MM'
| 'YYYY-MM-DD'
| 'YYYY-MM-DD HH:mm'
| 'YYYY-MM-DD HH:mm:ss'
@@ -133,7 +133,7 @@ async function submitForm(formEl: FormInstance | undefined) {
if (res.isSuccess) {
ElMessage.success('登录成功')
userStore.setUserInfo(res.data)
userStore.setLoginInfo(res.data)
router.push('/')
}
@@ -4,7 +4,6 @@ import { ElMessage } from 'element-plus'
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import QueryCompany from '@/components/QueryCompany.vue'
import SafeImage from '@/components/SafeImage.vue'
import { queryCompanyApi } from '@/request/api/company'
import { useCommonStore } from '@/store/common'
@@ -79,12 +78,12 @@ function toDetail(data: Company) {
<div class="company-list">
<div v-for="item in companyList" :key="item.id" class="company-item">
<SafeImage :url="item.logo" :size="70" class="company-logo" />
<div class="company-info">
<div class="company-header">
<div class="company-name" @click="toDetail(item)">
{{ item.name }}
</div>
<div class="company-tags">
<el-tag v-for="tag in item.industries" :key="tag" type="primary" effect="dark">
{{ tag }}
@@ -92,8 +91,8 @@ function toDetail(data: Company) {
</div>
</div>
<p v-if="item.profile" class="company-profile">
{{ item.profile }}
<p class="company-profile">
{{ item.profile || '-' }}
</p>
<div class="company-footer">
<div class="company-details">
@@ -111,19 +110,16 @@ function toDetail(data: Company) {
</div>
</div>
<div class="company-actions">
<!-- <el-button @click="toDetail(item)">
<i-ri:information-2-line />
<span class="button-text">
详细
</span>
</el-button>
<el-button type="primary">
<el-button
type="primary"
text
bg
>
<i-ri:message-3-fill />
<span class="button-text">
发起聊天
</span>
</el-button> -->
</el-button>
</div>
</div>
</div>
@@ -204,7 +200,7 @@ function toDetail(data: Company) {
.company-info {
flex: 1;
min-height: 70px;
.company-header {
display: flex;
justify-content: space-between;
@@ -221,7 +217,6 @@ function toDetail(data: Company) {
.company-tags {
margin-bottom: 6px;
> *:not(:last-child) {
margin-right: 16px;
}
@@ -1,11 +1,9 @@
<script lang="ts" setup>
import type { PostComment, PublicPostDetail } from '@/types/post'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { nextTick, ref, watch } from 'vue'
import SafeImage from '@/components/SafeImage.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { createPostCommentApi } from '@/request/api/post'
import { createPostCommentApi, deletePostCommentApi } from '@/request/api/post'
import { useUserStore } from '@/store/user'
import { formatCommentTime } from '@/utils/time'
import CommentInput from './comment-input.vue'
@@ -16,14 +14,14 @@ const props = defineProps<{
}>()
const emits = defineEmits<{
(e: 'update:modelValue', value: PostComment[]): void
(e: 'change'): void
(e: 'update:modelValue', val: PostComment[]): void
}>()
const commentList = ref<PostComment[]>([])
const userStore = useUserStore()
const replyCommentId = ref<number | null>(0)
const replyComment = ref<PostComment | null>(null)
const replyCommentLoading = ref<boolean>(false)
watch(
@@ -37,12 +35,13 @@ watch(
},
)
function handleReplyComment(commentId: number, e: MouseEvent) {
const isOpen = replyCommentId.value !== commentId
replyCommentId.value = isOpen ? commentId : null
if (!isOpen)
return
function handleReplyComment(comment: PostComment, e: MouseEvent) {
if (replyComment.value?.id === comment.id) {
replyComment.value = null
}
else {
replyComment.value = comment
}
nextTick(() => {
const el = e.currentTarget as HTMLElement
@@ -51,7 +50,7 @@ function handleReplyComment(commentId: number, e: MouseEvent) {
const rect = el.getBoundingClientRect()
const viewportHeight = window.innerHeight
const scrollOffset = 70 // 👈 可调:40~70
const scrollOffset = 70
// 点击点在视口上半部分 → 往上滚
if (rect.top < viewportHeight / 2) {
@@ -70,28 +69,31 @@ function handleReplyComment(commentId: number, e: MouseEvent) {
})
}
function findRootComment(commentList: PostComment[], commentId: number | null): PostComment | undefined {
// 使用 for 循环遍历一级评论列表
function findRootCommentByRootId(commentList: PostComment[], rootId: number | undefined): PostComment | undefined {
if (!rootId)
return undefined
// 遍历根评论列表
for (const item of commentList) {
// 如果找到目标评论,直接返回该评论
if (item.id === commentId) {
// 根评论的 id 或 rootId 与目标匹配
if (item.id === rootId || item.rootId === rootId) {
return item
}
// 如果该评论有子评论,检查子评论
if (item.children) {
const childComment = item.children.find(child => child.id === commentId)
if (childComment) {
return item
}
// 子评论中如果存在目标 rootId,则返回对应根评论
if (item.children && item.children.some(child => child.rootId === rootId)) {
return item
}
}
// 如果没有找到对应评论,返回 undefined
return undefined
}
function handleReplyCommentSubmit(content: string) {
if (replyComment.value === null) {
ElMessage.error('请选中需要回复的评论')
return
}
if (content === '') {
ElMessage.error('评论内容不能为空!')
return
@@ -100,16 +102,17 @@ function handleReplyCommentSubmit(content: string) {
replyCommentLoading.value = true
createPostCommentApi({
fatherId: replyCommentId.value,
rootId: replyComment.value.rootId,
fatherId: replyComment.value.id,
postId: props.postDetail.id,
content,
})
.then((res) => {
// 显示成功消息
ElMessage.success(res.message)
ElMessage.success('评论成功')
// 查找根评论
const rootComment = findRootComment(commentList.value, replyCommentId.value)
const rootComment = findRootCommentByRootId(commentList.value, replyComment.value?.rootId)
// 如果根评论存在,并且根评论没有子评论,初始化子评论数组
if (rootComment) {
@@ -119,16 +122,17 @@ function handleReplyCommentSubmit(content: string) {
// 将新回复添加到子评论中
rootComment.children.push({
rootId: rootComment.id,
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,
id: userStore.user.id,
companyId: userStore.company.id,
name: userStore.user.name,
avatar: userStore.company.logo,
},
replyUser: {
id: rootComment.publishUser.id,
@@ -140,30 +144,62 @@ function handleReplyCommentSubmit(content: string) {
}
else {
commentList.value.push({
rootId: res.data.id,
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,
id: userStore.user.id,
companyId: userStore.company.id,
name: userStore.user.name,
avatar: userStore.company.logo,
},
replyUser: null,
})
}
emits('update:modelValue', commentList.value)
emits('change')
})
.finally(() => {
replyCommentId.value = null
replyComment.value = null
replyCommentLoading.value = false
})
}
function removeCommentById(list: PostComment[], id: number): PostComment[] {
return list
.filter(item => item.id !== id)
.map(item => ({
...item,
children: item.children ? removeCommentById(item.children, id) : [],
}))
}
function handleDeleteSelfComment(data: PostComment) {
ElMessageBox.confirm('确定删除该条评论么?', '提示')
.then(() => {
deletePostCommentApi(data.id)
.then((res) => {
if (res.isSuccess) {
commentList.value = removeCommentById(commentList.value, data.id)
ElMessage.success('删除成功')
}
else {
ElMessage.error(res.message || '删除失败,请稍后再试')
}
})
.catch((err) => {
console.log(err)
ElMessage.error('删除失败,请稍后再试')
})
})
.catch(() => {
// 用户取消删除
})
}
</script>
<template>
@@ -187,20 +223,26 @@ function handleReplyCommentSubmit(content: string) {
</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' : '#8A919F' }"
@click="handleReplyComment(item.id, $event)"
>
<SvgIcon icon="mingcute:message-3-line" :size="16" />
<div>{{ replyCommentId === item.id ? "取消回复" : "回复" }}</div>
<div class="custom-comment-action" :style="{ color: replyComment?.id === item.id ? '#165dff' : '#8A919F' }" @click="handleReplyComment(item, $event)">
<SvgIcon icon="mingcute:message-3-line" />
<div>{{ replyComment?.id === item.id ? "取消回复" : "回复" }}</div>
</div>
<div class="custom-comment-action" style="margin-left: auto;">
<SvgIcon icon="mingcute:more-1-fill" :size="16" />
<el-dropdown>
<SvgIcon icon="akar-icons:more-horizontal-fill" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="userStore.user.id === item.publishUser.id" @click="handleDeleteSelfComment(item)">
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<!-- 父级评论输入框 -->
<CommentInput v-if="replyCommentId === item.id" :loading="replyCommentLoading" style="margin-top: 8px;" @submit="handleReplyCommentSubmit" />
<CommentInput v-if="replyComment?.id === item.id" :loading="replyCommentLoading" @submit="handleReplyCommentSubmit" />
<!-- 子评论 -->
<div v-if="item.children && item.children.length > 0" class="custom-comment-children">
@@ -218,7 +260,7 @@ function handleReplyCommentSubmit(content: string) {
作者
</div> -->
<template v-if="child.replyUser && child.fatherId !== item.id">
<div style="margin: 0px 4px;">
<div style="margin: 0px 8px;">
回复
</div>
<div class="custom-comment-author">
@@ -227,23 +269,31 @@ function handleReplyCommentSubmit(content: string) {
</template>
</div>
<div class="custom-comment-time">
{{ formatCommentTime(child.createTime) }}
{{ formatCommentTime(item.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' : '#8A919F' }" @click="handleReplyComment(child.id, $event)">
<SvgIcon icon="mingcute:message-3-line" :size="16" />
<div>{{ replyCommentId === child.id ? "取消回复" : "回复" }}</div>
<div class="custom-comment-action" :style="{ color: replyComment?.id === child.id ? '#165dff' : '#8A919F' }" @click="handleReplyComment(child, $event)">
<SvgIcon icon="mingcute:message-3-line" />
<div>{{ replyComment?.id === child.id ? "取消回复" : "回复" }}</div>
</div>
<div class="custom-comment-action" style="margin-left: auto;">
<SvgIcon icon="akar-icons:more-horizontal-fill" :size="16" />
<el-dropdown>
<SvgIcon icon="akar-icons:more-horizontal-fill" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="userStore.user.id === child.publishUser.id" @click="handleDeleteSelfComment(child)">
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<CommentInput v-if="replyCommentId === child.id" :loading="replyCommentLoading" style="margin-top: 8px;" @submit="handleReplyCommentSubmit" />
<CommentInput v-if="replyComment?.id === child.id" :loading="replyCommentLoading" @submit="handleReplyCommentSubmit" />
</div>
</div>
</div>
@@ -272,24 +322,28 @@ function handleReplyCommentSubmit(content: string) {
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;
margin-bottom: 4px;
.custom-comment-title {
font-size: 14px;
color: #515767;
color: #4e5969;
display: flex;
align-items: center;
gap: 4px;
}
.custom-comment-author {
cursor: pointer;
color: #515767;
}
.custom-comment-author-tag {
background-color: #165dff;
@@ -304,15 +358,15 @@ function handleReplyCommentSubmit(content: string) {
}
}
.custom-comment-content {
font-size: 16px;
font-size: 14px;
color: #252933;
word-break: break-all;
white-space: pre-wrap;
margin-bottom: 6px;
}
.custom-comment-actions {
display: flex;
gap: 12px;
gap: 8px;
color: #8a919f;
.custom-comment-action {
display: flex;
gap: 4px;
@@ -320,7 +374,10 @@ function handleReplyCommentSubmit(content: string) {
cursor: pointer;
font-size: 14px;
line-height: 14px;
color: #8a919f;
color: #1d2129;
svg {
outline: 0;
}
}
.custom-comment-action:hover {
color: #165dff;
@@ -38,6 +38,7 @@ const postDetail = ref<PublicPostDetail>({
activeTime: '',
viewCount: 0,
commentCount: 0,
companyId: 0,
})
@@ -49,6 +50,10 @@ const anchorMenu = ref<AnchorMenuItem[]>([])
const rootComment = ref<string>('')
const rootCommentLoading = ref<boolean>(false)
const totalCommentCount = computed(() => {
return getTotalCommentCount(postComments.value)
})
onMounted(async () => {
if (!id) {
router.push('/post')
@@ -92,22 +97,6 @@ onMounted(async () => {
loading.value = false
}
})
function getTotalCommentCount(list: PostComment[]): number {
let count = 0
for (const comment of list) {
count += 1 // 一级评论
if (comment.children && comment.children.length > 0) {
count += comment.children.length // 所有子评论(已扁平)
}
}
return count
}
const totalCommentCount = computed(() => {
return getTotalCommentCount(postComments.value)
})
function handleUpdateEditorDirectory(menus: AnchorMenuItem[]) {
anchorMenu.value = anchorMenu.value.concat(menus)
@@ -118,6 +107,7 @@ function handleSubmitRootComment(content: string) {
createPostCommentApi({
fatherId: null,
rootId: null,
postId: postDetail.value.id,
content,
})
@@ -127,16 +117,17 @@ function handleSubmitRootComment(content: string) {
ElMessage.success(res.message)
postComments.value.push({
rootId: res.data.id,
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,
id: userStore.user.id,
companyId: userStore.company.id,
name: userStore.user.name,
avatar: userStore.company.logo,
},
replyUser: null,
})
@@ -149,94 +140,109 @@ function handleSubmitRootComment(content: string) {
rootCommentLoading.value = false
})
}
function getTotalCommentCount(list: PostComment[]): number {
let count = 0
for (const comment of list) {
count += 1 // 一级评论
if (comment.children && comment.children.length > 0) {
count += comment.children.length // 所有子评论(已扁平)
}
}
return count
}
</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>
<CustomLoading v-if="loading" />
<div class="post-center">
<div class="post-main">
<div class="post-title">
{{ postDetail.title }}
<template v-else>
<div class="post-left">
<el-affix :offset="90">
<el-anchor>
<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>{{ totalCommentCount }} </span>
</div>
</div>
</div>
<div class="post-meta">
<div class="post-meta-item">
<SvgIcon icon="mdi:alarm" :size="18" />
<span>{{ formatPostTime(postDetail.activeTime) }} </span>
<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="post-meta-item">
<SvgIcon icon="mdi:eye-outline" :size="18" />
<span>{{ postDetail.viewCount }} </span>
<div class="primary-comment">
<el-avatar class="post-comment-avatar" :src="userStore.company.logo || '/no-avatar.png'" :size="46" />
<CommentInput v-model="rootComment" :loading="rootCommentLoading" class="post-comment-input" @submit="handleSubmitRootComment" />
</div>
<div class="post-meta-item">
<SvgIcon icon="mdi:comment-outline" :size="16" />
<span>{{ totalCommentCount }} </span>
<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-content">
<WandEditor v-model="postDetail.content" mode="view" @update:directory="handleUpdateEditorDirectory" />
</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 class="post-comment">
<div class="divider" />
<div v-if="postCompany.areas.length > 0" class="company-meta">
<template v-if="postCompany.establishmentDate">
成立于 {{ formatDate(postCompany.establishmentDate, 'YYYY') }} 年 ·
</template>
{{ postCompany.areas.join('/') }}
</div>
<div class="post-comment-title">
评论
</div>
<div class="company-desc">
{{ postCompany.profile }}
</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">
<template v-if="postCompany.establishmentDate">
成立于 {{ formatDate(postCompany.establishmentDate, 'YYYY') }} 年 ·
</template>
{{ 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 class="company-tags">
<span v-for="industry in postCompany.industries" :key="industry" class="tag">{{ industry }}</span>
</div>
</div>
</div>
</div>
</el-affix>
</div>
</el-affix>
</div>
</template>
</div>
</template>
@@ -244,7 +250,6 @@ function handleSubmitRootComment(content: string) {
.post {
padding: 24px 100px;
display: flex;
background-color: #fff;
.post-left {
width: 240px;
flex-shrink: 0;
@@ -258,13 +263,16 @@ function handleSubmitRootComment(content: string) {
}
.post-center {
background-color: #fff;
min-width: 600px;
flex: 1;
margin: 0px 32px;
.post-main {
display: flex;
flex-direction: column;
background-color: #ffffff;
padding: 0px 16px 12px 16px;
padding: 12px 16px 12px 16px;
width: 100%;
.post-title {
@@ -349,12 +357,14 @@ function handleSubmitRootComment(content: string) {
flex-shrink: 0;
.post-company {
background-color: #fff;
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;
@@ -13,7 +13,7 @@ import { useCommonStore } from '@/store/common'
import { formatPostTime } from '@/utils/time'
const router = useRouter()
const { postTagOptions } = useCommonStore()
const { postTagOptionsWithAll } = useCommonStore()
const activeNames = ref(['1'])
const selectedTag = ref<PostTag | null>(null)
@@ -39,7 +39,7 @@ onMounted(() => {
})
watch(
postTagOptions,
postTagOptionsWithAll,
(list) => {
if (list.length && !selectedTag.value) {
selectedTag.value = list[0]
@@ -107,7 +107,7 @@ function toDetail(data: PublicPost) {
<el-collapse-item title="标签" name="1">
<ul class="tag-list">
<li
v-for="item in postTagOptions"
v-for="item in postTagOptionsWithAll"
:key="item.id"
class="tag-item"
:class="{ active: selectedTag && (selectedTag.id === item.id) }"
@@ -332,9 +332,9 @@ function handleToEdit(row?: Post) {
<div>{{ item.updateTime }}</div>
<div class="post-meta-divider" />
<div>{{ item.viewCount }} 浏览</div>
<div>{{ item.likeCount }} 点赞</div>
<!-- <div>{{ item.likeCount }} 点赞</div> -->
<div>{{ item.commentCount }} 评论</div>
<div>{{ item.favoriteCount }} 收藏</div>
<!-- <div>{{ item.favoriteCount }} 收藏</div> -->
</div>
<div v-if="item.tags && item.tags.length > 0" class="post-tags">
<div v-for="tag in item.tags" :key="tag" class="post-tag">
@@ -1,6 +1,5 @@
<script lang="ts" setup>
import type { NavMenuItem } from '@/components/nav-menu/type'
import { Search } from '@element-plus/icons-vue'
import { computed, nextTick, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import NavMenu from '@/components/nav-menu/index.vue'
@@ -196,6 +195,7 @@ watch(searchKey, async (val) => {
border-radius: 0px;
width: 100%;
background-color: #fff;
padding: 0px 160px;
// border-right: 1px solid rgb(232.9, 232.9, 232.9);
}
}
@@ -83,7 +83,7 @@ onMounted(() => {
})
function getCompanyDetail() {
getCompanyDetailApi(userStore.userInfo.company.id).then((res) => {
getCompanyDetailApi(userStore.company.id).then((res) => {
if (res.isSuccess) {
companyDetail.value = res.data
}
@@ -124,8 +124,8 @@ async function handleSubmit(formEl: FormInstance | undefined) {
logo: companyDetail.value.logo,
}).then((res) => {
if (res.isSuccess) {
userStore.userInfo.company.name = companyDetail.value.name
userStore.userInfo.company.logo = companyDetail.value.logo
userStore.company.name = companyDetail.value.name
userStore.company.logo = companyDetail.value.logo
ElMessage.success('更新成功')
}
@@ -2,14 +2,15 @@
import type { FormInstance } from 'element-plus'
import type { PropType } from 'vue'
import type { CompanyUser, CompanyUserForm } from '@/types/company/user'
import type { UploadImageResponse } from '@/utils/file'
import { ElMessage } from 'element-plus'
import { computed, ref, watch } from 'vue'
import EmailCodeInput from '@/components/EmailCodeInput.vue'
import ImageUploadCard from '@/components/ImageUploadCard.vue'
import { authCodeVerifyApi } from '@/request/api/auth/code'
import { addCompanyUserApi, updateCompanyUserApi } from '@/request/api/company/user'
import { useUserStore } from '@/store/user'
import { AuthCodeChannel, AuthCodePurpose } from '@/types/auth/code'
import { uploadToAliyun } from '@/utils/file'
import { validatePhone } from '@/utils/validate'
const props = defineProps({
@@ -21,6 +22,8 @@ const props = defineProps({
const emits = defineEmits(['customClose', 'dataChange'])
const imageUpload = ref<UploadImageResponse | null>(null)
const userStore = useUserStore()
const roleOptions = [
{
@@ -42,16 +45,28 @@ const form = ref<CompanyUserForm>({
position: '',
email: '',
phone: '',
avatar: userStore.company.logo,
canuse: '1',
password: '',
emailCode: '',
avatar: '',
})
watch(
() => props.user,
(user) => {
if (!user)
imageUpload.value = null
if (!user) {
form.value.id = 0
form.value.role = 1
form.value.name = ''
form.value.position = ''
form.value.email = ''
form.value.phone = ''
form.value.avatar = userStore.company.logo
form.value.canuse = '1'
return
}
form.value = { ...user, password: '', emailCode: '' }
originEmail.value = user.email
@@ -163,18 +178,31 @@ async function handleSubmit(formEl: FormInstance | undefined) {
}
}
// 检查图片上传
if (imageUpload.value) {
uploadToAliyun(imageUpload.value.blob, imageUpload.value.name, imageUpload.value.size).then((res) => {
form.value.avatar = res.url
})
}
// 提交用户数据
const userApi = isUpdate.value ? updateCompanyUserApi : addCompanyUserApi
let avatar = form.value.avatar
if (!avatar) {
avatar = userStore.company.logo || ''
}
const response = await userApi({
id: form.value.id,
companyId: userStore.userInfo.company.id,
companyId: userStore.company.id,
email: form.value.email,
password: form.value.password,
name: form.value.name,
position: form.value.position,
phone: form.value.phone,
canuse: form.value.canuse,
avatar: form.value.avatar,
avatar,
})
if (response.isSuccess) {
@@ -203,7 +231,7 @@ async function handleSubmit(formEl: FormInstance | undefined) {
</script>
<template>
<el-drawer v-bind="$attrs" title="成员编辑" resizable>
<el-drawer v-bind="$attrs" title="成员编辑" resizable destroy-on-close>
<el-alert
title="提示"
style="margin-bottom: 24px;"
@@ -223,8 +251,15 @@ async function handleSubmit(formEl: FormInstance | undefined) {
label-width="auto"
label-position="right"
>
<el-form-item label="头像" prop="name">
<ImageUploadCard v-model="form.avatar" :auto-upload-aliyun="true" />
<el-form-item label="头像" prop="avatar">
<ImageUploadCard
v-model="form.avatar"
@change="(val) => {
imageUpload = val
form.avatar = val?.url || ''
}"
/>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" />
@@ -3,7 +3,6 @@ import type { CompanyUser, QueryCompanyUserParams } from '@/types/company/user'
import { Delete, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, reactive, ref } from 'vue'
import SafeImage from '@/components/SafeImage.vue'
import { deleteCompanyUserApi, queryCompanyUserApi } from '@/request/api/company/user'
import { useUserStore } from '@/store/user'
import EditDrawer from './components/EditDrawer.vue'
@@ -25,7 +24,7 @@ const queryForm = reactive<QueryCompanyUserParams>({
currentPage: page.current,
pageSize: page.pageSize,
companyId: userStore.userInfo.company.id,
companyId: userStore.company.id,
name: '',
phone: '',
position: '',
@@ -143,7 +142,7 @@ function handleDelteRows(val?: CompanyUser) {
},
).then(() => {
deleteCompanyUserApi({
companyId: userStore.userInfo.company.id,
companyId: userStore.company.id,
ids: deleteIds,
}).then((res) => {
if (res.isSuccess) {
@@ -252,10 +251,10 @@ function handleDelteRows(val?: CompanyUser) {
align="center"
:selectable="selectable"
/>
<el-table-column prop="avatar" label="头像" align="center" width="120">
<el-table-column prop="avatar" label="头像" align="center" width="100">
<template #default="scope">
<div style="display: flex;align-items: center;justify-content: center;">
<SafeImage :url="scope.row.avatar" :size="64" />
<SafeImage :url="scope.row.avatar" :size="48" />
</div>
</template>
</el-table-column>