This commit is contained in:
oneao committed 2025-12-26 21:35:12 +08:00
1 parent 2f8a815c62
commit c76581d8cb
34 files changed
+539 -277

No files matched your search

@@ -5,17 +5,32 @@ import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor
public class AuthLoginVO { public class AuthLoginVO {
private Long companyId;
private String companyLogo;
private String companyName;
private Long userId; private Company company;
private Integer userRole; private User user;
private String userName;
private String userPosition;
private String token; private String token;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Company {
private Long id;
private String logo;
private String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class User {
private Long id;
private Integer role;
private String name;
private String position;
private String avatar;
}
} }
@@ -9,8 +9,8 @@ import cn.link56.auth.identity.model.vo.AuthLoginVO;
import cn.link56.auth.token.model.AuthToken; import cn.link56.auth.token.model.AuthToken;
import cn.link56.auth.token.model.TokenPayload; import cn.link56.auth.token.model.TokenPayload;
import cn.link56.auth.token.service.AuthTokenService; import cn.link56.auth.token.service.AuthTokenService;
import cn.link56.company.model.po.CompanyPO; import cn.link56.company.model.po.Company;
import cn.link56.company.model.po.CompanyUserPO; import cn.link56.company.model.po.CompanyUser;
import cn.link56.auth.identity.model.dto.AuthRegisterDTO; import cn.link56.auth.identity.model.dto.AuthRegisterDTO;
import cn.link56.company.service.CompanyService; import cn.link56.company.service.CompanyService;
import cn.link56.company.service.CompanyUserService; import cn.link56.company.service.CompanyUserService;
@@ -61,7 +61,7 @@ public class AuthIdentityService {
long companyId = IdGenerator.nextId(); long companyId = IdGenerator.nextId();
companyService.save( companyService.save(
new CompanyPO() new Company()
.setId(companyId) .setId(companyId)
.setName(company.getName()) .setName(company.getName())
.setIndustry(StringUtils.join(company.getIndustries(),",")) .setIndustry(StringUtils.join(company.getIndustries(),","))
@@ -72,7 +72,7 @@ public class AuthIdentityService {
); );
companyUserService.save( companyUserService.save(
new CompanyUserPO() new CompanyUser()
.setId(IdGenerator.nextId()) .setId(IdGenerator.nextId())
.setCompanyId(companyId) .setCompanyId(companyId)
.setName(admin.getName()) .setName(admin.getName())
@@ -85,7 +85,7 @@ public class AuthIdentityService {
} }
public AuthLoginVO loginByPassword(AuthLoginPasswordDTO dto) { public AuthLoginVO loginByPassword(AuthLoginPasswordDTO dto) {
CompanyUserPO user = companyUserService.loginByEmailAndPassword(dto.getEmail(), dto.getPassword()); CompanyUser user = companyUserService.loginByEmailAndPassword(dto.getEmail(), dto.getPassword());
if (user == null) { if (user == null) {
throw new BusinessException("邮箱或密码错误"); throw new BusinessException("邮箱或密码错误");
} }
@@ -107,7 +107,7 @@ public class AuthIdentityService {
throw new BusinessException("邮箱或验证码失效"); throw new BusinessException("邮箱或验证码失效");
} }
CompanyUserPO user = companyUserService.loginByEmail(email); CompanyUser user = companyUserService.loginByEmail(email);
if (user == null) { if (user == null) {
throw new BusinessException("邮箱不存在"); throw new BusinessException("邮箱不存在");
} }
@@ -118,8 +118,8 @@ public class AuthIdentityService {
/** /**
* 公共逻辑:根据用户生成 LoginVO * 公共逻辑:根据用户生成 LoginVO
*/ */
private AuthLoginVO buildLoginVO(CompanyUserPO user) { private AuthLoginVO buildLoginVO(CompanyUser user) {
CompanyPO company = companyService.getById(user.getCompanyId()); Company company = companyService.getById(user.getCompanyId());
TokenPayload tokenPayload = new TokenPayload(); TokenPayload tokenPayload = new TokenPayload();
tokenPayload.setCompanyId(user.getCompanyId()); tokenPayload.setCompanyId(user.getCompanyId());
@@ -127,18 +127,22 @@ public class AuthIdentityService {
AuthToken authToken = tokenService.generateToken(tokenPayload); AuthToken authToken = tokenService.generateToken(tokenPayload);
return new AuthLoginVO( AuthLoginVO.Company companyVO = new AuthLoginVO.Company(
company.getId(), company.getId(),
company.getLogo(), company.getLogo(),
company.getName(), company.getName()
);
AuthLoginVO.User userVO = new AuthLoginVO.User(
user.getId(), user.getId(),
user.getRole(), user.getRole(),
user.getName(), user.getName(),
user.getPosition(), user.getPosition(),
user.getAvatar()
authToken.getToken()
); );
return new AuthLoginVO(companyVO, userVO, authToken.getToken());
} }
} }
@@ -1,13 +1,13 @@
package cn.link56.company.mapper; package cn.link56.company.mapper;
import cn.link56.company.model.middle.RecommendCompanyRow; import cn.link56.company.model.middle.RecommendCompanyRow;
import cn.link56.company.model.po.CompanyPO; import cn.link56.company.model.po.Company;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.List; import java.util.List;
@Mapper @Mapper
public interface CompanyMapper extends BaseMapper<CompanyPO> { public interface CompanyMapper extends BaseMapper<Company> {
List<RecommendCompanyRow> listRecommend(); List<RecommendCompanyRow> listRecommend();
} }
@@ -1,9 +1,9 @@
package cn.link56.company.mapper; package cn.link56.company.mapper;
import cn.link56.company.model.po.CompanyUserPO; import cn.link56.company.model.po.CompanyUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@Mapper @Mapper
public interface CompanyUserMapper extends BaseMapper<CompanyUserPO> { public interface CompanyUserMapper extends BaseMapper<CompanyUser> {
} }
@@ -24,4 +24,6 @@ public class UpdateCompanyUserDTO {
private String name; private String name;
private String phone; private String phone;
private String canuse; private String canuse;
private String avatar;
} }
@@ -17,7 +17,7 @@ import java.util.Date;
@NoArgsConstructor @NoArgsConstructor
@TableName("b_company") @TableName("b_company")
@Accessors(chain = true) @Accessors(chain = true)
public class CompanyPO { public class Company {
@TableId(value = "b_id") @TableId(value = "b_id")
private Long id; private Long id;
@@ -17,7 +17,7 @@ import java.util.Date;
@NoArgsConstructor @NoArgsConstructor
@TableName("b_company_user") @TableName("b_company_user")
@Accessors(chain = true) @Accessors(chain = true)
public class CompanyUserPO { public class CompanyUser {
@TableId(value = "b_id") @TableId(value = "b_id")
private Long id; private Long id;
@@ -42,6 +42,9 @@ public class CompanyUserPO {
@TableField("b_position") @TableField("b_position")
private String position; private String position;
@TableField("b_avatar")
private String avatar;
@TableField("b_canuse") @TableField("b_canuse")
private String canuse; private String canuse;
@@ -26,7 +26,7 @@ public class CompanyDetailVO {
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public static class User{ public static class User {
private Long id; private Long id;
private Long companyId; private Long companyId;
private String email; private String email;
@@ -34,5 +34,6 @@ public class CompanyDetailVO {
private Integer role; private Integer role;
private String position; private String position;
private String name; private String name;
private String avatar;
} }
} }
@@ -17,5 +17,7 @@ public class QueryCompanyUserVO {
private String email; private String email;
private String phone; private String phone;
private String avatar;
private String canuse; private String canuse;
} }
@@ -7,8 +7,8 @@ import cn.link56.company.mapper.CompanyMapper;
import cn.link56.company.model.dto.QueryCompanyDTO; import cn.link56.company.model.dto.QueryCompanyDTO;
import cn.link56.company.model.dto.UpdateCompanyDTO; import cn.link56.company.model.dto.UpdateCompanyDTO;
import cn.link56.company.model.middle.RecommendCompanyRow; import cn.link56.company.model.middle.RecommendCompanyRow;
import cn.link56.company.model.po.CompanyPO; import cn.link56.company.model.po.Company;
import cn.link56.company.model.po.CompanyUserPO; import cn.link56.company.model.po.CompanyUser;
import cn.link56.company.model.vo.CompanyDetailVO; import cn.link56.company.model.vo.CompanyDetailVO;
import cn.link56.company.model.vo.QueryCompanyVO; import cn.link56.company.model.vo.QueryCompanyVO;
import cn.link56.company.model.vo.RecommendCompanyVO; import cn.link56.company.model.vo.RecommendCompanyVO;
@@ -29,7 +29,7 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
public class CompanyService extends ServiceImpl<CompanyMapper,CompanyPO> { public class CompanyService extends ServiceImpl<CompanyMapper, Company> {
@Resource @Resource
private CompanyMapper companyMapper; private CompanyMapper companyMapper;
@Resource @Resource
@@ -67,9 +67,9 @@ public class CompanyService extends ServiceImpl<CompanyMapper,CompanyPO> {
} }
public List<RecommendHotCompanyVO> listHot() { public List<RecommendHotCompanyVO> listHot() {
LambdaQueryWrapper<CompanyPO> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<Company> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CompanyPO::getHotRecommend, "1") queryWrapper.eq(Company::getHotRecommend, "1")
.select(CompanyPO::getId, CompanyPO::getName); .select(Company::getId, Company::getName);
return companyMapper.selectList(queryWrapper) return companyMapper.selectList(queryWrapper)
.stream() .stream()
@@ -86,8 +86,8 @@ public class CompanyService extends ServiceImpl<CompanyMapper,CompanyPO> {
*/ */
public boolean isNameExists(String name) { public boolean isNameExists(String name) {
Long count = companyMapper.selectCount( Long count = companyMapper.selectCount(
new LambdaQueryWrapper<CompanyPO>() new LambdaQueryWrapper<Company>()
.eq(CompanyPO::getName, name) .eq(Company::getName, name)
); );
return count > 0; return count > 0;
} }
@@ -97,8 +97,8 @@ public class CompanyService extends ServiceImpl<CompanyMapper,CompanyPO> {
*/ */
public boolean isPhoneExists(String phone) { public boolean isPhoneExists(String phone) {
Long count = companyMapper.selectCount( Long count = companyMapper.selectCount(
new LambdaQueryWrapper<CompanyPO>() new LambdaQueryWrapper<Company>()
.eq(CompanyPO::getPhone, phone) .eq(Company::getPhone, phone)
); );
return count > 0; return count > 0;
} }
@@ -108,8 +108,8 @@ public class CompanyService extends ServiceImpl<CompanyMapper,CompanyPO> {
*/ */
public boolean isEmailExists(String email) { public boolean isEmailExists(String email) {
Long count = companyMapper.selectCount( Long count = companyMapper.selectCount(
new LambdaQueryWrapper<CompanyPO>() new LambdaQueryWrapper<Company>()
.eq(CompanyPO::getEmail, email) .eq(Company::getEmail, email)
); );
return count > 0; return count > 0;
} }
@@ -123,19 +123,19 @@ public class CompanyService extends ServiceImpl<CompanyMapper,CompanyPO> {
List<String> industries = queryCompanyDTO.getIndustries(); List<String> industries = queryCompanyDTO.getIndustries();
// 分页 // 分页
Page<CompanyPO> companyPage = new Page<>(currentPage,pageSize); Page<Company> companyPage = new Page<>(currentPage,pageSize);
LambdaQueryWrapper<CompanyPO> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<Company> queryWrapper = new LambdaQueryWrapper<>();
// 名称 // 名称
if (StringUtils.isNotBlank(name)){ if (StringUtils.isNotBlank(name)){
queryWrapper.like(CompanyPO::getName,name); queryWrapper.like(Company::getName,name);
} }
// 地区 // 地区
if (areas != null && !areas.isEmpty()){ if (areas != null && !areas.isEmpty()){
queryWrapper.and(wrapper -> { queryWrapper.and(wrapper -> {
for (String area : areas) { for (String area : areas) {
wrapper.or(w -> w.like(CompanyPO::getArea, area)); wrapper.or(w -> w.like(Company::getArea, area));
} }
}); });
} }
@@ -144,12 +144,12 @@ public class CompanyService extends ServiceImpl<CompanyMapper,CompanyPO> {
if (industries != null && !industries.isEmpty()) { if (industries != null && !industries.isEmpty()) {
queryWrapper.and(wrapper -> { queryWrapper.and(wrapper -> {
for (String industry : industries) { for (String industry : industries) {
wrapper.or(w -> w.like(CompanyPO::getIndustry, industry)); wrapper.or(w -> w.like(Company::getIndustry, industry));
} }
}); });
} }
queryWrapper.eq(CompanyPO::getCanuse,"1"); queryWrapper.eq(Company::getCanuse,"1");
List<QueryCompanyVO> result = companyMapper.selectList(companyPage, queryWrapper).stream().map(item -> { List<QueryCompanyVO> result = companyMapper.selectList(companyPage, queryWrapper).stream().map(item -> {
QueryCompanyVO queryCompanyVO = new QueryCompanyVO(); QueryCompanyVO queryCompanyVO = new QueryCompanyVO();
@@ -173,18 +173,18 @@ public class CompanyService extends ServiceImpl<CompanyMapper,CompanyPO> {
} }
public CompanyDetailVO getCompanyDetail(Long companyId) { public CompanyDetailVO getCompanyDetail(Long companyId) {
CompanyPO companyPO = companyMapper.selectById(companyId); Company company = companyMapper.selectById(companyId);
List<CompanyUserPO> companyUserPOS = companyUserService.listByCompanyId(companyId); List<CompanyUser> companyUsers = companyUserService.listByCompanyId(companyId);
CompanyDetailVO companyDetailVO = new CompanyDetailVO(); CompanyDetailVO companyDetailVO = new CompanyDetailVO();
BeanUtils.copyProperties(companyPO,companyDetailVO); BeanUtils.copyProperties(company,companyDetailVO);
companyDetailVO.setAreas(Arrays.stream(companyPO.getArea().split(",")) companyDetailVO.setAreas(Arrays.stream(company.getArea().split(","))
.collect(Collectors.toList())); .collect(Collectors.toList()));
companyDetailVO.setIndustries(Arrays.stream(companyPO.getIndustry().split(",")) companyDetailVO.setIndustries(Arrays.stream(company.getIndustry().split(","))
.collect(Collectors.toList())); .collect(Collectors.toList()));
companyDetailVO.setUsers(companyUserPOS.stream() companyDetailVO.setUsers(companyUsers.stream()
.map(item -> { .map(item -> {
CompanyDetailVO.User user = new CompanyDetailVO.User(); CompanyDetailVO.User user = new CompanyDetailVO.User();
BeanUtils.copyProperties(item, user); BeanUtils.copyProperties(item, user);
@@ -206,43 +206,42 @@ public class CompanyService extends ServiceImpl<CompanyMapper,CompanyPO> {
String name = updateCompanyDTO.getName(); String name = updateCompanyDTO.getName();
String phone = updateCompanyDTO.getPhone(); String phone = updateCompanyDTO.getPhone();
String email = updateCompanyDTO.getEmail(); String email = updateCompanyDTO.getEmail();
Date establishmentDate = updateCompanyDTO.getEstablishmentDate();
String profile = updateCompanyDTO.getProfile();
String description = updateCompanyDTO.getDescription();
String logo = updateCompanyDTO.getLogo();
String area = StringUtils.join(updateCompanyDTO.getAreas(), ","); String area = StringUtils.join(updateCompanyDTO.getAreas(), ",");
String industry = StringUtils.join(updateCompanyDTO.getIndustries(), ","); String industry = StringUtils.join(updateCompanyDTO.getIndustries(), ",");
// 检查 // 检查
CompanyPO companyPO = companyMapper.selectById(id); Company company = companyMapper.selectById(id);
if (ObjectUtils.isEmpty(companyPO)){ if (ObjectUtils.isEmpty(company)){
throw new BusinessException("更新失败,该公司不存在"); throw new BusinessException("更新失败,该公司不存在");
} }
// 检查名称 // 检查名称
if (!companyPO.getName().equals(name) && isNameExists(name)){ if (!company.getName().equals(name) && isNameExists(name)){
throw new BusinessException("公司名称已存在"); throw new BusinessException("公司名称已存在");
} }
// 检查联系电话 // 检查联系电话
if (!StringUtils.equals(phone, companyPO.getPhone()) if (!StringUtils.equals(phone, company.getPhone())
&& isPhoneExists(phone)) { && isPhoneExists(phone)) {
throw new BusinessException("公司联系电话已存在"); throw new BusinessException("公司联系电话已存在");
} }
// 检查邮箱 // 检查邮箱
if (!StringUtils.equals(email, companyPO.getEmail()) if (!StringUtils.equals(email, company.getEmail())
&& isEmailExists(email)) { && isEmailExists(email)) {
throw new BusinessException("公司联系邮箱已存在"); throw new BusinessException("公司联系邮箱已存在");
} }
// 更新 // 更新
BeanUtils.copyProperties(updateCompanyDTO,companyPO); BeanUtils.copyProperties(updateCompanyDTO, company);
companyPO.setArea(area); company.setArea(area);
companyPO.setIndustry(industry); company.setIndustry(industry);
// 保存更新后的公司信息 // 保存更新后的公司信息
companyMapper.updateById(companyPO); companyMapper.updateById(company);
// 更新用户头像
companyUserService.updateCompanyAvatar(company.getId(),company.getLogo());
} }
} }
@@ -9,7 +9,7 @@ import cn.link56.company.model.dto.AddCompanyUserDTO;
import cn.link56.company.model.dto.DeleteCompanyUserDTO; import cn.link56.company.model.dto.DeleteCompanyUserDTO;
import cn.link56.company.model.dto.QueryCompanyUserDTO; import cn.link56.company.model.dto.QueryCompanyUserDTO;
import cn.link56.company.model.dto.UpdateCompanyUserDTO; import cn.link56.company.model.dto.UpdateCompanyUserDTO;
import cn.link56.company.model.po.CompanyUserPO; import cn.link56.company.model.po.CompanyUser;
import cn.link56.company.model.vo.QueryCompanyUserVO; import cn.link56.company.model.vo.QueryCompanyUserVO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -26,7 +26,7 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUserPO> { public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUser> {
@Resource @Resource
private CompanyUserMapper companyUserMapper; private CompanyUserMapper companyUserMapper;
@@ -41,8 +41,8 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
return false; return false;
} }
long count = companyUserMapper.selectCount( long count = companyUserMapper.selectCount(
new LambdaQueryWrapper<CompanyUserPO>() new LambdaQueryWrapper<CompanyUser>()
.eq(CompanyUserPO::getEmail, email) .eq(CompanyUser::getEmail, email)
); );
return count > 0; return count > 0;
} }
@@ -54,13 +54,29 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
*/ */
public boolean isPhoneExists(String phone) { public boolean isPhoneExists(String phone) {
long count = companyUserMapper.selectCount( long count = companyUserMapper.selectCount(
new LambdaQueryWrapper<CompanyUserPO>() new LambdaQueryWrapper<CompanyUser>()
.eq(CompanyUserPO::getPhone, phone) .eq(CompanyUser::getPhone, phone)
); );
return count > 0; return count > 0;
} }
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 * @param companyId 当前公司ID
@@ -69,10 +85,10 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
*/ */
public boolean hasPermission(Long companyId, Long userId) { public boolean hasPermission(Long companyId, Long userId) {
// 根据 userId 查询公司用户信息 // 根据 userId 查询公司用户信息
CompanyUserPO companyUserPO = companyUserMapper.selectById(userId); CompanyUser companyUser = companyUserMapper.selectById(userId);
// 如果 companyUserPO 为空,表示该用户不存在,直接返回无权限 // 如果 companyUserPO 为空,表示该用户不存在,直接返回无权限
if (ObjectUtils.isEmpty(companyUserPO)) { if (ObjectUtils.isEmpty(companyUser)) {
return false; return false;
} }
@@ -82,35 +98,35 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
} }
// 判断用户是否属于该公司 // 判断用户是否属于该公司
if (!companyUserPO.getCompanyId().equals(companyId)) { if (!companyUser.getCompanyId().equals(companyId)) {
return false; return false;
} }
// 检查用户角色是否为 0,只有角色为 0 才有权限 // 检查用户角色是否为 0,只有角色为 0 才有权限
return companyUserPO.getRole() == 0; return companyUser.getRole() == 0;
// 如果角色不是 0,表示没有权限 // 如果角色不是 0,表示没有权限
} }
public CompanyUserPO loginByEmailAndPassword(String email, String password){ public CompanyUser loginByEmailAndPassword(String email, String password){
LambdaQueryWrapper<CompanyUserPO> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<CompanyUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CompanyUserPO::getEmail,email) queryWrapper.eq(CompanyUser::getEmail,email)
.eq(CompanyUserPO::getPassword,password); .eq(CompanyUser::getPassword,password);
return companyUserMapper.selectOne(queryWrapper); return companyUserMapper.selectOne(queryWrapper);
} }
public CompanyUserPO loginByEmail(String email){ public CompanyUser loginByEmail(String email){
LambdaQueryWrapper<CompanyUserPO> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<CompanyUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CompanyUserPO::getEmail,email); queryWrapper.eq(CompanyUser::getEmail,email);
return companyUserMapper.selectOne(queryWrapper); return companyUserMapper.selectOne(queryWrapper);
} }
public List<CompanyUserPO> listByCompanyId(Long companyId){ public List<CompanyUser> listByCompanyId(Long companyId){
LambdaQueryWrapper<CompanyUserPO> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<CompanyUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CompanyUserPO::getCompanyId,companyId); queryWrapper.eq(CompanyUser::getCompanyId,companyId);
return companyUserMapper.selectList(queryWrapper); return companyUserMapper.selectList(queryWrapper);
} }
@@ -132,34 +148,34 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
String position = queryCompanyUserDTO.getPosition(); String position = queryCompanyUserDTO.getPosition();
String canuse = queryCompanyUserDTO.getCanuse(); String canuse = queryCompanyUserDTO.getCanuse();
Page<CompanyUserPO> queryPage = new Page<>(currentPage,pageSize); Page<CompanyUser> queryPage = new Page<>(currentPage,pageSize);
LambdaQueryWrapper<CompanyUserPO> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<CompanyUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CompanyUserPO::getCompanyId,companyId); queryWrapper.eq(CompanyUser::getCompanyId,companyId);
if (StringUtils.isNotBlank(name)){ if (StringUtils.isNotBlank(name)){
queryWrapper.like(CompanyUserPO::getName,name); queryWrapper.like(CompanyUser::getName,name);
} }
if (StringUtils.isNotBlank(email)){ if (StringUtils.isNotBlank(email)){
queryWrapper.like(CompanyUserPO::getEmail,email); queryWrapper.like(CompanyUser::getEmail,email);
} }
if (StringUtils.isNotBlank(phone)){ if (StringUtils.isNotBlank(phone)){
queryWrapper.like(CompanyUserPO::getPhone,phone); queryWrapper.like(CompanyUser::getPhone,phone);
} }
if (StringUtils.isNotBlank(position)){ if (StringUtils.isNotBlank(position)){
queryWrapper.like(CompanyUserPO::getPosition,position); queryWrapper.like(CompanyUser::getPosition,position);
} }
if (StringUtils.isNotBlank(canuse)){ if (StringUtils.isNotBlank(canuse)){
queryWrapper.eq(CompanyUserPO::getCanuse,canuse); queryWrapper.eq(CompanyUser::getCanuse,canuse);
} }
List<CompanyUserPO> companyUserPOS = companyUserMapper.selectList(queryPage, queryWrapper); List<CompanyUser> companyUsers = companyUserMapper.selectList(queryPage, queryWrapper);
List<QueryCompanyUserVO> result = companyUserPOS.stream().map(item -> { List<QueryCompanyUserVO> result = companyUsers.stream().map(item -> {
QueryCompanyUserVO queryCompanyUserVO = new QueryCompanyUserVO(); QueryCompanyUserVO queryCompanyUserVO = new QueryCompanyUserVO();
BeanUtils.copyProperties(item, queryCompanyUserVO); BeanUtils.copyProperties(item, queryCompanyUserVO);
return queryCompanyUserVO; return queryCompanyUserVO;
@@ -192,14 +208,14 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
canuse = "1"; canuse = "1";
} }
CompanyUserPO companyUserPO = new CompanyUserPO(); CompanyUser companyUser = new CompanyUser();
BeanUtils.copyProperties(companyUserDTO,companyUserPO); BeanUtils.copyProperties(companyUserDTO, companyUser);
companyUserPO.setId(IdGenerator.nextId()); companyUser.setId(IdGenerator.nextId());
companyUserPO.setRole(1); companyUser.setRole(1);
companyUserPO.setCanuse(canuse); companyUser.setCanuse(canuse);
companyUserMapper.insert(companyUserPO); companyUserMapper.insert(companyUser);
} }
@Transactional @Transactional
@@ -217,37 +233,38 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
throw new BusinessException("您没有权限访问该公司的信息,操作被拒绝"); throw new BusinessException("您没有权限访问该公司的信息,操作被拒绝");
} }
CompanyUserPO companyUserPO = companyUserMapper.selectById(id); CompanyUser companyUser = companyUserMapper.selectById(id);
String oldPassword = companyUserPO.getPassword(); String oldPassword = companyUser.getPassword();
if (ObjectUtils.isEmpty(companyUserPO)) { if (ObjectUtils.isEmpty(companyUser)) {
throw new BusinessException("更新失败,该用户不存在"); throw new BusinessException("更新失败,该用户不存在");
} }
// 检查邮箱是否发生变化 // 检查邮箱是否发生变化
boolean isEmailChanged = !StringUtils.equals(email, companyUserPO.getEmail()); boolean isEmailChanged = !StringUtils.equals(email, companyUser.getEmail());
if (isEmailChanged && isEmailExists(email)) { if (isEmailChanged && isEmailExists(email)) {
throw new BusinessException("邮箱已被注册"); throw new BusinessException("邮箱已被注册");
} }
// 检查手机号是否发生变化 // 检查手机号是否发生变化
if (!StringUtils.equals(phone, companyUserPO.getPhone()) && isPhoneExists(phone)) { if (!StringUtils.equals(phone, companyUser.getPhone()) && isPhoneExists(phone)) {
throw new BusinessException("手机号已被注册"); throw new BusinessException("手机号已被注册");
} }
if (StringUtils.isNotBlank(password)) { if (StringUtils.isNotBlank(password)) {
companyUserPO.setPassword(password); companyUser.setPassword(password);
} }
BeanUtils.copyProperties(companyUserDTO, companyUserPO); BeanUtils.copyProperties(companyUserDTO, companyUser);
if(StringUtils.isNotBlank(password)){
companyUserPO.setPassword(password); if(StringUtils.isNotBlank(password)) {
}else{ companyUser.setPassword(password);
companyUserPO.setPassword(oldPassword); } else {
companyUser.setPassword(oldPassword);
} }
// 更新用户信息 // 更新用户信息
companyUserMapper.updateById(companyUserPO); companyUserMapper.updateById(companyUser);
} }
@@ -261,10 +278,10 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
throw new BusinessException("您没有权限访问该公司的信息,操作被拒绝"); throw new BusinessException("您没有权限访问该公司的信息,操作被拒绝");
} }
LambdaUpdateWrapper<CompanyUserPO> updateWrapper = new LambdaUpdateWrapper<>(); LambdaUpdateWrapper<CompanyUser> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.in(CompanyUserPO::getId, ids) updateWrapper.in(CompanyUser::getId, ids)
.eq(CompanyUserPO::getCompanyId, companyId) .eq(CompanyUser::getCompanyId, companyId)
.ne(CompanyUserPO::getRole, 0); .ne(CompanyUser::getRole, 0);
// 执行删除操作 // 执行删除操作
companyUserMapper.delete(updateWrapper); companyUserMapper.delete(updateWrapper);
@@ -22,7 +22,7 @@ public class PostCommentVO {
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public static class User{ public static class User {
private Long id; private Long id;
private Long companyId; private Long companyId;
private String name; private String name;
@@ -4,6 +4,7 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List; import java.util.List;
@Data @Data
@@ -14,6 +15,7 @@ public class PostCompanyVO {
private String name; private String name;
private String logo; private String logo;
private String profile; private String profile;
private Date establishmentDate;
private List<String> areas; private List<String> areas;
private List<String> industries; private List<String> industries;
} }
@@ -41,61 +41,68 @@ public class PostCommentService extends ServiceImpl<PostCommentMapper, PostComme
return null; return null;
} }
public List<PostCommentVO> listPostComments(Long id) { public List<PostCommentVO> listPostComments(Long postId) {
// 1. 查询所有评论 // 1. 查询所有评论
List<PostCommentVO> list = postCommentMapper.selectComments(id); List<PostCommentVO> list = postCommentMapper.selectComments(postId);
// 2. 构建扁平化树形结构(所有子评论提升到根评论层级) // 2. 构建 id -> comment 映射
List<PostCommentVO> rootComments = new ArrayList<>(); Map<Long, PostCommentVO> commentMap = new HashMap<>(list.size());
Map<Long, PostCommentVO> commentMap = new HashMap<>();
// 2.1 初始化映射并收集根评论
for (PostCommentVO comment : list) { for (PostCommentVO comment : list) {
comment.setChildren(new ArrayList<>()); // 统一初始化
commentMap.put(comment.getId(), comment); commentMap.put(comment.getId(), comment);
if (ObjectUtils.isEmpty(comment.getFatherId())) {
rootComments.add(comment);
comment.setChildren(new ArrayList<>()); // 提前初始化children
}
} }
// 2.2 遍历所有评论,将子评论挂到根祖先的children中 // 3. 收集一级评论
List<PostCommentVO> rootComments = new ArrayList<>();
for (PostCommentVO comment : list) { for (PostCommentVO comment : list) {
if (ObjectUtils.isEmpty(comment.getFatherId())) { if (comment.getFatherId() == null) {
continue; // 跳过根评论 rootComments.add(comment);
}
// 查找当前评论的根祖先(追溯到没有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. 按原始顺序排序(可选) // 4. 将所有子评论挂到「根评论」的 children 中
rootComments.forEach(root -> { for (PostCommentVO comment : list) {
if (root.getChildren() != null) {
// 根据需求排序,例如按创建时间升序 // 跳过一级评论
root.getChildren().sort(Comparator.comparing(PostCommentVO::getCreateTime)); if (comment.getFatherId() == null) {
continue;
} }
});
PostCommentVO root = findRootComment(comment, commentMap);
if (root != null) {
root.getChildren().add(comment);
}
}
// 5. 子评论排序(可选)
rootComments.forEach(root ->
root.getChildren().sort(
Comparator.comparing(PostCommentVO::getCreateTime)
)
);
return rootComments; 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 @Transactional
public CreatePostCommentVO createPostComment(CreatePostCommentDTO dto) { public CreatePostCommentVO createPostComment(CreatePostCommentDTO dto) {
PostComment postComment = new PostComment(); PostComment postComment = new PostComment();
@@ -4,7 +4,7 @@ import cn.link56.common.BasePageVO;
import cn.link56.common.context.UserContext; import cn.link56.common.context.UserContext;
import cn.link56.common.exception.BusinessException; import cn.link56.common.exception.BusinessException;
import cn.link56.common.utils.snowflake.idgen.IdGenerator; import cn.link56.common.utils.snowflake.idgen.IdGenerator;
import cn.link56.company.model.po.CompanyPO; import cn.link56.company.model.po.Company;
import cn.link56.company.service.CompanyService; import cn.link56.company.service.CompanyService;
import cn.link56.post.mapper.PostMapper; import cn.link56.post.mapper.PostMapper;
import cn.link56.post.model.dto.*; import cn.link56.post.model.dto.*;
@@ -16,13 +16,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -258,7 +256,6 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
Long viewTotal = postViewService.getTotalByPostId(id); Long viewTotal = postViewService.getTotalByPostId(id);
Long commentTotal = postCommentService.getTotalByPostId(id); Long commentTotal = postCommentService.getTotalByPostId(id);
postVO.setViewCount(viewTotal); postVO.setViewCount(viewTotal);
postVO.setCommentCount(commentTotal); postVO.setCommentCount(commentTotal);
@@ -274,16 +271,16 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
} }
public PostCompanyVO getPostCompanyById(Long id) { public PostCompanyVO getPostCompanyById(Long id) {
CompanyPO companyPO = companyService.getById(id); Company company = companyService.getById(id);
if (companyPO == null){ if (company == null){
throw new BusinessException("该公司不存在或已注销"); throw new BusinessException("该公司不存在或已注销");
} }
PostCompanyVO postCompanyVO = new PostCompanyVO(); PostCompanyVO postCompanyVO = new PostCompanyVO();
BeanUtils.copyProperties(companyPO,postCompanyVO); BeanUtils.copyProperties(company,postCompanyVO);
postCompanyVO.setAreas(Arrays.stream(StringUtils.split(companyPO.getArea(),",")).collect(Collectors.toList())); postCompanyVO.setAreas(Arrays.stream(StringUtils.split(company.getArea(),",")).collect(Collectors.toList()));
postCompanyVO.setIndustries(Arrays.stream(StringUtils.split(companyPO.getIndustry(),",")).collect(Collectors.toList())); postCompanyVO.setIndustries(Arrays.stream(StringUtils.split(company.getIndustry(),",")).collect(Collectors.toList()));
return postCompanyVO; return postCompanyVO;
} }
@@ -28,8 +28,8 @@
<select id="selectComments" resultMap="CommentListMap"> <select id="selectComments" resultMap="CommentListMap">
SELECT c.*, SELECT c.*,
pc.b_id AS pub_user_id,pc.b_company_id AS pub_company_id,pc.b_name AS pub_user_name, pc.b_id AS pub_user_id,pc.b_company_id AS pub_company_id,pc.b_name AS pub_user_name,pc.b_avatar AS pub_user_avatar,
rc.b_id AS rep_user_id,rc.b_company_id AS rep_company_id,rc.b_name AS rep_company_id rc.b_id AS rep_user_id,rc.b_company_id AS rep_company_id,rc.b_name AS rep_user_name,rc.b_avatar AS rep_user_avatar
FROM b_post_comment c FROM b_post_comment c
LEFT JOIN b_company_user pc ON c.b_create_by = pc.b_id LEFT JOIN b_company_user pc ON c.b_create_by = pc.b_id
LEFT JOIN b_company_user rc ON ( LEFT JOIN b_company_user rc ON (
@@ -0,0 +1,74 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
defineOptions({ inheritAttrs: false })
const props = defineProps<{
url?: string
size?: number
width?: number
height?: number
round?: boolean
}>()
const fallback = '/no-avatar.png'
const currentSrc = ref(props.url?.trim() || fallback)
// 监听 url 变化
watch(
() => props.url,
(newUrl) => {
currentSrc.value = newUrl?.trim() || fallback
},
)
// 加载失败兜底
function onError() {
currentSrc.value = fallback
}
// 尺寸样式
const sizeStyle = computed(() => {
// width / height 优先
if (props.width || props.height) {
return {
width: props.width ? `${props.width}px` : undefined,
height: props.height ? `${props.height}px` : undefined,
}
}
// size(正方形)
if (typeof props.size === 'number') {
return {
width: `${props.size}px`,
height: `${props.size}px`,
}
}
return {}
})
</script>
<template>
<img
v-bind="$attrs"
:src="currentSrc"
alt="图片"
class="base-img"
:class="{ 'is-round': round }"
:style="sizeStyle"
@error="onError"
>
</template>
<style scoped>
.base-img {
display: block;
object-fit: cover;
}
/* 圆形 */
.is-round {
border-radius: 50%;
}
</style>
@@ -4,6 +4,8 @@ import {
User, User,
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import SafeImage from '@/components/SafeImage.vue'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
const route = useRoute() const route = useRoute()
@@ -63,7 +65,7 @@ function isActive(path: string) {
<el-dropdown> <el-dropdown>
<div class="user"> <div class="user">
<el-avatar :size="36" :src="userStore.userInfo.company.logo ? userStore.userInfo.company.logo : '/no-avatar.png'" /> <SafeImage :size="36" :url="userStore.userInfo.user.avatar || userStore.userInfo.company.logo" />
<div class="info"> <div class="info">
<span class="name">{{ userStore.userInfo.user.name }}</span> <span class="name">{{ userStore.userInfo.user.name }}</span>
<span class="company">{{ userStore.userInfo.company.name }}</span> <span class="company">{{ userStore.userInfo.company.name }}</span>
@@ -37,7 +37,7 @@ const router = createRouter({
}, },
{ {
path: '/company/detail', path: '/company/detail',
component: () => import('@/views/company/detail/index.vue'), component: () => import('@/views/company/detail.vue'),
}, },
{ {
path: '/settings', path: '/settings',
@@ -7,6 +7,7 @@ interface IUserInfo {
name: string name: string
position: string position: string
role: number role: number
avatar: string
} }
company: { company: {
id: number id: number
@@ -25,6 +26,7 @@ export const useUserStore = defineStore(
name: '', name: '',
position: '', position: '',
role: 0, role: 0,
avatar: '',
}, },
company: { company: {
id: 0, id: 0,
@@ -45,6 +47,7 @@ export const useUserStore = defineStore(
name: '', name: '',
position: '', position: '',
role: 0, role: 0,
avatar: '',
}, },
company: { company: {
id: 0, id: 0,
@@ -9,14 +9,18 @@ export interface LoginEmailCodeApiParams {
} }
export interface LoginApiResult { export interface LoginApiResult {
companyId: number company: {
companyLogo: string id: number
companyName: string logo: string
name: string
userId: number }
userRole: number user: {
userName: string id: number
userPosition: string role: number
name: string
position: string
avatar: string
}
token: string token: string
} }
@@ -18,6 +18,7 @@ export interface CompanyUser {
email: string email: string
phone: string phone: string
canuse: string canuse: string
avatar: string
} }
export interface CompanyUserForm extends CompanyUser { export interface CompanyUserForm extends CompanyUser {
@@ -34,6 +35,8 @@ export interface AddCompanyUserParams {
position: string position: string
phone: string phone: string
canuse: string canuse: string
avatar: string
} }
export interface UpdateCompanyUserParams { export interface UpdateCompanyUserParams {
id: number id: number
@@ -46,6 +49,8 @@ export interface UpdateCompanyUserParams {
position: string position: string
phone: string phone: string
canuse: string canuse: string
avatar: string
} }
export interface DeleteCompanyUserParams { export interface DeleteCompanyUserParams {
@@ -58,7 +58,6 @@ export interface PublicPostDetail {
activeTime: string activeTime: string
viewCount: number viewCount: number
commentCount: number
companyId: number companyId: number
} }
@@ -68,6 +67,7 @@ export interface PostCompany {
name: string name: string
logo: string logo: string
profile: string profile: string
establishmentDate: string
areas: string[] areas: string[]
industries: string[] industries: string[]
@@ -44,3 +44,69 @@ export function formatPostTime(input?: dayjs.ConfigType): string {
// 非本年 // 非本年
return time.format('YYYY年MM月DD日') return time.format('YYYY年MM月DD日')
} }
export function formatCommentTime(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 HH:mm')
}
// 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 < 7) {
return `${diffDays}天前`
}
// 本年
if (time.year() === now.year()) {
return time.format('MM-DD HH:mm')
}
// 非本年
return time.format('YYYY-MM-DD HH:mm')
}
export type DateFormat
= | 'YYYY'
| 'MM'
| 'DD'
| 'YYYY-MM-DD'
| 'YYYY-MM-DD HH:mm'
| 'YYYY-MM-DD HH:mm:ss'
| 'YYYY年MM月DD日'
export function formatDate(
input?: dayjs.ConfigType,
format: DateFormat = 'YYYY-MM-DD',
): string {
if (!input)
return ''
const date = dayjs(input)
if (!date.isValid())
return ''
return date.format(format)
}
@@ -133,20 +133,7 @@ async function submitForm(formEl: FormInstance | undefined) {
if (res.isSuccess) { if (res.isSuccess) {
ElMessage.success('登录成功') ElMessage.success('登录成功')
userStore.setUserInfo({ userStore.setUserInfo(res.data)
user: {
id: res.data.userId,
name: res.data.userName,
position: res.data.userPosition,
role: res.data.userRole,
},
company: {
id: res.data.companyId,
logo: res.data.companyLogo,
name: res.data.companyName,
},
token: res.data.token,
})
router.push('/') router.push('/')
} }
@@ -8,6 +8,7 @@ import AnchorMenu from '@/components/anchor-menu/index.vue'
import WangEditor from '@/components/wang-editor/index.vue' import WangEditor from '@/components/wang-editor/index.vue'
import { getCompanyDetailApi } from '@/request/api/company' import { getCompanyDetailApi } from '@/request/api/company'
import { formatDate } from '@/utils/time'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -110,14 +111,11 @@ function handleUpdateEditorDirectory(menus: AnchorMenuItem[]) {
{{ companyDetail.email || '-' }} {{ companyDetail.email || '-' }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="成立日期"> <el-descriptions-item label="成立日期">
- {{ formatDate(companyDetail.establishmentDate, 'YYYY-MM-DD') || '-' }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="区域"> <el-descriptions-item label="区域">
{{ companyDetail.areas.join('/') || '-' }} {{ companyDetail.areas.join('/') || '-' }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="详细地址" :span="2">
-
</el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
@@ -135,6 +133,13 @@ function handleUpdateEditorDirectory(menus: AnchorMenuItem[]) {
<span class="info-title">公司成员</span> <span class="info-title">公司成员</span>
</div> </div>
<el-table :data="companyDetail.users" style="width: 100%" stripe border> <el-table :data="companyDetail.users" style="width: 100%" stripe border>
<el-table-column prop="avatar" label="头像" align="center" width="120">
<template #default="scope">
<div style="display: flex;align-items: center;justify-content: center;">
<SafeImage :url="scope.row.avatar" :size="64" />
</div>
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="180" /> <el-table-column prop="name" label="姓名" width="180" />
<el-table-column prop="position" label="职务" width="180" /> <el-table-column prop="position" label="职务" width="180" />
<el-table-column prop="phone" label="手机号" width="180" /> <el-table-column prop="phone" label="手机号" width="180" />
@@ -4,6 +4,7 @@ import { ElMessage } from 'element-plus'
import { onMounted, reactive, ref } from 'vue' import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import QueryCompany from '@/components/QueryCompany.vue' import QueryCompany from '@/components/QueryCompany.vue'
import SafeImage from '@/components/SafeImage.vue'
import { queryCompanyApi } from '@/request/api/company' import { queryCompanyApi } from '@/request/api/company'
import { useCommonStore } from '@/store/common' import { useCommonStore } from '@/store/common'
@@ -77,21 +78,22 @@ function toDetail(data: Company) {
</div> </div>
<div class="company-list"> <div class="company-list">
<div v-for="item in companyList" :key="item.id" class="company-item"> <div v-for="item in companyList" :key="item.id" class="company-item">
<el-avatar shape="square" :src="item.logo || '/no-avatar.png'" :size="70" class="company-logo" /> <SafeImage :url="item.logo" :size="70" class="company-logo" />
<div class="company-info"> <div class="company-info">
<div class="company-header"> <div class="company-header">
<div class="company-name"> <div class="company-name" @click="toDetail(item)">
{{ item.name }} {{ item.name }}
</div> </div>
<div class="company-tags">
<el-tag v-for="tag in item.industries" :key="tag" type="primary" effect="dark">
{{ tag }}
</el-tag>
</div>
</div> </div>
<div class="company-tags">
<el-tag v-for="tag in item.industries" :key="tag" type="primary" effect="dark"> <p v-if="item.profile" class="company-profile">
{{ tag }} {{ item.profile }}
</el-tag>
</div>
<p class="company-profile">
{{ item.profile || '-' }}
</p> </p>
<div class="company-footer"> <div class="company-footer">
<div class="company-details"> <div class="company-details">
@@ -109,7 +111,7 @@ function toDetail(data: Company) {
</div> </div>
</div> </div>
<div class="company-actions"> <div class="company-actions">
<el-button @click="toDetail(item)"> <!-- <el-button @click="toDetail(item)">
<i-ri:information-2-line /> <i-ri:information-2-line />
<span class="button-text"> <span class="button-text">
详细 详细
@@ -121,7 +123,7 @@ function toDetail(data: Company) {
<span class="button-text"> <span class="button-text">
发起聊天 发起聊天
</span> </span>
</el-button> </el-button> -->
</div> </div>
</div> </div>
</div> </div>
@@ -202,7 +204,7 @@ function toDetail(data: Company) {
.company-info { .company-info {
flex: 1; flex: 1;
min-height: 70px;
.company-header { .company-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -219,7 +221,8 @@ function toDetail(data: Company) {
.company-tags { .company-tags {
margin-bottom: 6px; margin-bottom: 6px;
> * {
> *:not(:last-child) {
margin-right: 16px; margin-right: 16px;
} }
} }
@@ -2,9 +2,12 @@
import type { PostComment, PublicPostDetail } from '@/types/post' import type { PostComment, PublicPostDetail } from '@/types/post'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { nextTick, ref, watch } from 'vue' import { nextTick, ref, watch } from 'vue'
import SafeImage from '@/components/SafeImage.vue'
import SvgIcon from '@/components/SvgIcon.vue' import SvgIcon from '@/components/SvgIcon.vue'
import { createPostCommentApi } from '@/request/api/post' import { createPostCommentApi } from '@/request/api/post'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
import { formatCommentTime } from '@/utils/time'
import CommentInput from './comment-input.vue' import CommentInput from './comment-input.vue'
const props = defineProps<{ const props = defineProps<{
@@ -12,6 +15,11 @@ const props = defineProps<{
postDetail: PublicPostDetail postDetail: PublicPostDetail
}>() }>()
const emits = defineEmits<{
(e: 'update:modelValue', value: PostComment[]): void
(e: 'change'): void
}>()
const commentList = ref<PostComment[]>([]) const commentList = ref<PostComment[]>([])
const userStore = useUserStore() const userStore = useUserStore()
@@ -29,19 +37,36 @@ watch(
}, },
) )
function handleReplyComment(commentId: number) { function handleReplyComment(commentId: number, e: MouseEvent) {
if (replyCommentId.value === commentId) { const isOpen = replyCommentId.value !== commentId
replyCommentId.value = null replyCommentId.value = isOpen ? commentId : null
}
else { if (!isOpen)
replyCommentId.value = commentId return
}
nextTick(() => { nextTick(() => {
window.scrollTo({ const el = e.currentTarget as HTMLElement
top: document.documentElement.scrollHeight, if (!el)
behavior: 'smooth', return
})
const rect = el.getBoundingClientRect()
const viewportHeight = window.innerHeight
const scrollOffset = 70 // 👈 可调:40~70
// 点击点在视口上半部分 → 往上滚
if (rect.top < viewportHeight / 2) {
window.scrollBy({
top: -scrollOffset,
behavior: 'smooth',
})
}
// 点击点在视口下半部分 → 往下滚
else {
window.scrollBy({
top: scrollOffset,
behavior: 'smooth',
})
}
}) })
} }
@@ -129,6 +154,9 @@ function handleReplyCommentSubmit(content: string) {
replyUser: null, replyUser: null,
}) })
} }
emits('update:modelValue', commentList.value)
emits('change')
}) })
.finally(() => { .finally(() => {
@@ -141,7 +169,7 @@ function handleReplyCommentSubmit(content: string) {
<template> <template>
<div v-for="item in commentList" :key="item.id" class="custom-comment"> <div v-for="item in commentList" :key="item.id" class="custom-comment">
<div class="custom-comment-left"> <div class="custom-comment-left">
<el-avatar class="custom-comment-avatar" :src="item.publishUser.avatar || '/no-avatar.png'" :size="70" /> <SafeImage :url="item.publishUser.avatar" :size="32" round />
</div> </div>
<div class="custom-comment-right"> <div class="custom-comment-right">
<div class="custom-comment-meta"> <div class="custom-comment-meta">
@@ -149,33 +177,36 @@ function handleReplyCommentSubmit(content: string) {
<div class="custom-comment-author"> <div class="custom-comment-author">
{{ item.publishUser.name }} {{ item.publishUser.name }}
</div> </div>
<div v-if="item.publishUser.companyId === postDetail.companyId" class="custom-comment-author-tag"> <!-- <div v-if="item.publishUser.companyId === postDetail.companyId" class="custom-comment-author-tag">
作者 作者
</div> </div> -->
</div> </div>
<div class="custom-comment-time"> <div class="custom-comment-time">
{{ item.createTime }} {{ formatCommentTime(item.createTime) }}
</div> </div>
</div> </div>
<div class="custom-comment-content" v-html="item.content" /> <div class="custom-comment-content" v-html="item.content" />
<div class="custom-comment-actions"> <div class="custom-comment-actions">
<div class="custom-comment-action" :style="{ color: replyCommentId === item.id ? '#165dff' : '#1d2129' }" @click="handleReplyComment(item.id)"> <div
<SvgIcon icon="mingcute:message-3-line" /> 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>{{ replyCommentId === item.id ? "取消回复" : "回复" }}</div>
</div> </div>
<div class="custom-comment-action flex ml-auto"> <div class="custom-comment-action" style="margin-left: auto;">
<SvgIcon icon="akar-icons:more-horizontal-fill" /> <SvgIcon icon="mingcute:more-1-fill" :size="16" />
</div> </div>
</div> </div>
<!-- 父级评论输入框 --> <!-- 父级评论输入框 -->
<CommentInput v-if="replyCommentId === item.id" :loading="replyCommentLoading" @submit="handleReplyCommentSubmit" /> <CommentInput v-if="replyCommentId === item.id" :loading="replyCommentLoading" style="margin-top: 8px;" @submit="handleReplyCommentSubmit" />
<!-- 子评论 --> <!-- 子评论 -->
<div v-if="item.children && item.children.length > 0" class="custom-comment-children"> <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 v-for="child in item.children" :key="child.id" class="custom-comment">
<div class="custom-comment-left"> <div class="custom-comment-left">
<el-avatar class="custom-comment-avatar" :src="item.publishUser.avatar || '/no-avatar.png'" :size="70" /> <SafeImage :url="item.publishUser.avatar" :size="32" round />
</div> </div>
<div class="custom-comment-right"> <div class="custom-comment-right">
<div class="custom-comment-meta"> <div class="custom-comment-meta">
@@ -183,33 +214,36 @@ function handleReplyCommentSubmit(content: string) {
<div class="custom-comment-author"> <div class="custom-comment-author">
{{ child.publishUser.name }} {{ child.publishUser.name }}
</div> </div>
<div v-if="child.publishUser.companyId === postDetail.companyId" class="custom-comment-author-tag"> <!-- <div v-if="child.publishUser.companyId === postDetail.companyId" class="custom-comment-author-tag">
作者 作者
</div> </div> -->
<template v-if="child.replyUser && child.fatherId !== item.id"> <template v-if="child.replyUser && child.fatherId !== item.id">
<div>回复</div> <div style="margin: 0px 4px;">
回复
</div>
<div class="custom-comment-author"> <div class="custom-comment-author">
{{ child.replyUser.name }} {{ child.replyUser.name }}
</div> </div>
</template> </template>
</div> </div>
<div class="custom-comment-time"> <div class="custom-comment-time">
{{ child.createTime }} {{ formatCommentTime(child.createTime) }}
</div> </div>
</div> </div>
<div class="custom-comment-content"> <div class="custom-comment-content">
{{ child.content }} {{ child.content }}
</div> </div>
<div class="custom-comment-actions"> <div class="custom-comment-actions">
<div class="custom-comment-action" :style="{ color: replyCommentId === child.id ? '#165dff' : '#1d2129' }" @click="handleReplyComment(child.id)"> <div class="custom-comment-action" :style="{ color: replyCommentId === child.id ? '#165dff' : '#8A919F' }" @click="handleReplyComment(child.id, $event)">
<SvgIcon icon="mingcute:message-3-line" /> <SvgIcon icon="mingcute:message-3-line" :size="16" />
<div>{{ replyCommentId === child.id ? "取消回复" : "回复" }}</div> <div>{{ replyCommentId === child.id ? "取消回复" : "回复" }}</div>
</div> </div>
<div class="custom-comment-action flex ml-auto"> <div class="custom-comment-action" style="margin-left: auto;">
<SvgIcon icon="akar-icons:more-horizontal-fill" /> <SvgIcon icon="akar-icons:more-horizontal-fill" :size="16" />
</div> </div>
</div> </div>
<CommentInput v-if="replyCommentId === child.id" :loading="replyCommentLoading" @submit="handleReplyCommentSubmit" />
<CommentInput v-if="replyCommentId === child.id" :loading="replyCommentLoading" style="margin-top: 8px;" @submit="handleReplyCommentSubmit" />
</div> </div>
</div> </div>
</div> </div>
@@ -238,21 +272,17 @@ function handleReplyCommentSubmit(content: string) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
gap: 8px;
} }
.custom-comment-avatar { .custom-comment-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
} }
.custom-comment-meta { .custom-comment-meta {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 4px;
.custom-comment-title { .custom-comment-title {
font-size: 14px; font-size: 14px;
color: #4e5969; color: #515767;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
@@ -274,14 +304,15 @@ function handleReplyCommentSubmit(content: string) {
} }
} }
.custom-comment-content { .custom-comment-content {
font-size: 14px; font-size: 16px;
color: #1d2129; color: #252933;
word-break: break-all; word-break: break-all;
white-space: pre-wrap; white-space: pre-wrap;
margin-bottom: 6px;
} }
.custom-comment-actions { .custom-comment-actions {
display: flex; display: flex;
gap: 8px; gap: 12px;
.custom-comment-action { .custom-comment-action {
display: flex; display: flex;
gap: 4px; gap: 4px;
@@ -289,7 +320,7 @@ function handleReplyCommentSubmit(content: string) {
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
line-height: 14px; line-height: 14px;
color: #1d2129; color: #8a919f;
} }
.custom-comment-action:hover { .custom-comment-action:hover {
color: #165dff; color: #165dff;
@@ -2,14 +2,14 @@
import type { AnchorMenuItem } from '@/components/anchor-menu/type' import type { AnchorMenuItem } from '@/components/anchor-menu/type'
import type { PostComment, PostCompany, PublicPostDetail } from '@/types/post' import type { PostComment, PostCompany, PublicPostDetail } from '@/types/post'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import AnchorMenu from '@/components/anchor-menu/index.vue' import AnchorMenu from '@/components/anchor-menu/index.vue'
import CustomEmpty from '@/components/CustomEmpty.vue' import CustomEmpty from '@/components/CustomEmpty.vue'
import WandEditor from '@/components/wang-editor/index.vue' import WandEditor from '@/components/wang-editor/index.vue'
import { createPostCommentApi, getPostCompanyById, getPublicPostDetailApi, listPostCommentApi } from '@/request/api/post' import { createPostCommentApi, getPostCompanyById, getPublicPostDetailApi, listPostCommentApi } from '@/request/api/post'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
import { formatPostTime } from '@/utils/time' import { formatDate, formatPostTime } from '@/utils/time'
import CommentInput from './components/comment-input.vue' import CommentInput from './components/comment-input.vue'
import CommentList from './components/comment-list.vue' import CommentList from './components/comment-list.vue'
@@ -25,6 +25,7 @@ const postCompany = ref<PostCompany>({
name: '', name: '',
logo: '', logo: '',
profile: '', profile: '',
establishmentDate: '',
areas: [], areas: [],
industries: [], industries: [],
}) })
@@ -37,7 +38,6 @@ const postDetail = ref<PublicPostDetail>({
activeTime: '', activeTime: '',
viewCount: 0, viewCount: 0,
commentCount: 0,
companyId: 0, companyId: 0,
}) })
@@ -92,6 +92,22 @@ onMounted(async () => {
loading.value = false 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[]) { function handleUpdateEditorDirectory(menus: AnchorMenuItem[]) {
anchorMenu.value = anchorMenu.value.concat(menus) anchorMenu.value = anchorMenu.value.concat(menus)
@@ -161,7 +177,7 @@ function handleSubmitRootComment(content: string) {
</div> </div>
<div class="post-meta-item"> <div class="post-meta-item">
<SvgIcon icon="mdi:comment-outline" :size="16" /> <SvgIcon icon="mdi:comment-outline" :size="16" />
<span>{{ postDetail.commentCount }} </span> <span>{{ totalCommentCount }} </span>
</div> </div>
</div> </div>
</div> </div>
@@ -185,7 +201,10 @@ function handleSubmitRootComment(content: string) {
<div class="reply-comment"> <div class="reply-comment">
<div v-if="postComments.length > 0" class="reply-comment-list"> <div v-if="postComments.length > 0" class="reply-comment-list">
<CommentList v-model="postComments" :post-detail="postDetail" /> <CommentList
v-model="postComments"
:post-detail="postDetail"
/>
</div> </div>
<CustomEmpty v-else title="暂时没有评论 🤔" :in-container="true" /> <CustomEmpty v-else title="暂时没有评论 🤔" :in-container="true" />
</div> </div>
@@ -200,10 +219,11 @@ function handleSubmitRootComment(content: string) {
<h3 class="company-name"> <h3 class="company-name">
{{ postCompany.name }} {{ postCompany.name }}
</h3> </h3>
<div v-if="postCompany.areas.length > 0" class="company-meta"> <div v-if="postCompany.areas.length > 0" class="company-meta">
<!-- 成立于 2012 年 · 上海 --> <template v-if="postCompany.establishmentDate">
{{ postCompany.areas.join("/") }} 成立于 {{ formatDate(postCompany.establishmentDate, 'YYYY') }} 年 ·
</template>
{{ postCompany.areas.join('/') }}
</div> </div>
<div class="company-desc"> <div class="company-desc">
@@ -3,7 +3,6 @@ import type { DashboardData } from '@/types/post'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import CustomLoading from '@/components/CustomLoading.vue' import CustomLoading from '@/components/CustomLoading.vue'
import { getDashboardDataApi } from '@/request/api/post'
import PostNumberChart from './components/post-number-chart.vue' import PostNumberChart from './components/post-number-chart.vue'
import PostTypeChart from './components/post-type-chart.vue' import PostTypeChart from './components/post-type-chart.vue'
import PostViewChart from './components/post-view-chart.vue' import PostViewChart from './components/post-view-chart.vue'
@@ -25,22 +24,22 @@ const activeCollapseNames = ref(['1', '2'])
onMounted(() => { onMounted(() => {
loading.value = true loading.value = true
getDashboardDataApi().then((res) => { // getDashboardDataApi().then((res) => {
if (res.isSuccess) { // if (res.isSuccess) {
// dashboradData.value = res.data // // dashboradData.value = res.data
dashboradData.value.typeChart = [...res.data.typeChart] // dashboradData.value.typeChart = [...res.data.typeChart]
console.log(dashboradData.value) // console.log(dashboradData.value)
} // }
else { // else {
ElMessage.error(res.message || '加载失败,请稍后再试') // ElMessage.error(res.message || '加载失败,请稍后再试')
} // }
}).catch((err) => { // }).catch((err) => {
console.log(err) // console.log(err)
ElMessage.error('加载失败,请稍后再试') // ElMessage.error('加载失败,请稍后再试')
}).finally(() => { // }).finally(() => {
loading.value = false // loading.value = false
}) // })
}) })
</script> </script>
@@ -190,6 +190,7 @@ watch(searchKey, async (val) => {
} }
.right-view { .right-view {
padding:0px 120px;
flex: 1; flex: 1;
flex-shrink: 0; flex-shrink: 0;
border-radius: 0px; border-radius: 0px;
@@ -155,6 +155,7 @@ const activeCollapseNames = ref(['1', '2', '3'])
:rules="formRules" :rules="formRules"
label-width="auto" label-width="auto"
label-position="top" label-position="top"
style="width: 700px;"
> >
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="24"> <el-col :span="24">
@@ -214,6 +215,7 @@ const activeCollapseNames = ref(['1', '2', '3'])
:model="companyDetail" :model="companyDetail"
:rules="formRules" :rules="formRules"
label-width="auto" label-width="auto"
style="width: 700px;"
> >
<el-form-item prop="profile"> <el-form-item prop="profile">
<el-input v-model="companyDetail.profile" type="textarea" /> <el-input v-model="companyDetail.profile" type="textarea" />
@@ -284,9 +286,6 @@ const activeCollapseNames = ref(['1', '2', '3'])
} }
.form-footer { .form-footer {
width: 100%; width: 100%;
box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.08);
position: sticky;
bottom: 0;
align-self: flex-end; align-self: flex-end;
height: 56px; height: 56px;
display: flex; display: flex;
@@ -5,6 +5,7 @@ import type { CompanyUser, CompanyUserForm } from '@/types/company/user'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import EmailCodeInput from '@/components/EmailCodeInput.vue' import EmailCodeInput from '@/components/EmailCodeInput.vue'
import ImageUploadCard from '@/components/ImageUploadCard.vue'
import { authCodeVerifyApi } from '@/request/api/auth/code' import { authCodeVerifyApi } from '@/request/api/auth/code'
import { addCompanyUserApi, updateCompanyUserApi } from '@/request/api/company/user' import { addCompanyUserApi, updateCompanyUserApi } from '@/request/api/company/user'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
@@ -44,6 +45,7 @@ const form = ref<CompanyUserForm>({
canuse: '1', canuse: '1',
password: '', password: '',
emailCode: '', emailCode: '',
avatar: '',
}) })
watch( watch(
() => props.user, () => props.user,
@@ -172,6 +174,7 @@ async function handleSubmit(formEl: FormInstance | undefined) {
position: form.value.position, position: form.value.position,
phone: form.value.phone, phone: form.value.phone,
canuse: form.value.canuse, canuse: form.value.canuse,
avatar: form.value.avatar,
}) })
if (response.isSuccess) { if (response.isSuccess) {
@@ -220,6 +223,9 @@ async function handleSubmit(formEl: FormInstance | undefined) {
label-width="auto" label-width="auto"
label-position="right" label-position="right"
> >
<el-form-item label="头像" prop="name">
<ImageUploadCard v-model="form.avatar" :auto-upload-aliyun="true" />
</el-form-item>
<el-form-item label="名称" prop="name"> <el-form-item label="名称" prop="name">
<el-input v-model="form.name" /> <el-input v-model="form.name" />
</el-form-item> </el-form-item>
@@ -3,6 +3,7 @@ import type { CompanyUser, QueryCompanyUserParams } from '@/types/company/user'
import { Delete, Plus, Refresh, Search } from '@element-plus/icons-vue' import { Delete, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, reactive, ref } from 'vue' import { onMounted, reactive, ref } from 'vue'
import SafeImage from '@/components/SafeImage.vue'
import { deleteCompanyUserApi, queryCompanyUserApi } from '@/request/api/company/user' import { deleteCompanyUserApi, queryCompanyUserApi } from '@/request/api/company/user'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
import EditDrawer from './components/EditDrawer.vue' import EditDrawer from './components/EditDrawer.vue'
@@ -251,6 +252,13 @@ function handleDelteRows(val?: CompanyUser) {
align="center" align="center"
:selectable="selectable" :selectable="selectable"
/> />
<el-table-column prop="avatar" label="头像" align="center" width="120">
<template #default="scope">
<div style="display: flex;align-items: center;justify-content: center;">
<SafeImage :url="scope.row.avatar" :size="64" />
</div>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" align="center"> <el-table-column prop="name" label="名称" align="center">
<template #default="scope"> <template #default="scope">
<template v-if="scope.row.role === 1"> <template v-if="scope.row.role === 1">