This commit is contained in:
oneao committed 2026-01-04 17:31:04 +08:00
1 parent fac3206497
commit dc885119a0
43 files changed
+927 -364

No files matched your search

@@ -1,22 +0,0 @@
package cn.link56.base.controller;
import cn.link56.base.model.vo.BaseDataVO;
import cn.link56.base.service.BaseDataService;
import cn.link56.common.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/base")
public class BaseDataController {
@Resource
private BaseDataService baseDataService;
@GetMapping("/init")
public Result<BaseDataVO> initBaseData() {
return Result.success(baseDataService.getBaseData());
}
}
@@ -1,35 +0,0 @@
package cn.link56.base.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BaseDataVO {
private List<Industry> industries;
private List<Area> areas;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Industry {
private Long id;
private Integer xh;
private String name;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Area {
private String id;
private Integer xh;
private String name;
private String fatherId;
private List<Area> children;
}
}
@@ -1,108 +0,0 @@
package cn.link56.base.service;
import cn.link56.base.mapper.AreaMapper;
import cn.link56.base.mapper.IndustryMapper;
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;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class BaseDataService {
@Resource
private IndustryMapper IndustryMapper;
@Resource
private AreaMapper AreaMapper;
public BaseDataVO getBaseData() {
BaseDataVO baseDataVO = new BaseDataVO();
// 查询可用行业
List<BaseDataVO.Industry> industries = IndustryMapper.selectList(
new LambdaQueryWrapper<Industry>()
.eq(Industry::getCanuse, "1")
).stream()
.map(industry -> new BaseDataVO.Industry(
industry.getId(),
industry.getXh(),
industry.getName()
))
.collect(Collectors.toList());
baseDataVO.setIndustries(industries);
// 地区
List<Area> allAreas = AreaMapper.listAllArea();
List<BaseDataVO.Area> areaTree = buildAreaTree(allAreas);
baseDataVO.setAreas(areaTree);
return baseDataVO;
}
/**
* BArea → BaseDataVO.Area 树
*/
private List<BaseDataVO.Area> buildAreaTree(List<Area> areas) {
if (areas == null || areas.isEmpty()) {
return Collections.emptyList();
}
// id -> VO
Map<String, BaseDataVO.Area> map = new HashMap<>(areas.size());
// 1️⃣ 先全部转 VO
for (Area a : areas) {
BaseDataVO.Area vo = new BaseDataVO.Area(
a.getId(),
a.getXh(),
a.getName(),
a.getFatherId(),
new ArrayList<>()
);
map.put(vo.getId(), vo);
}
// 2️⃣ 组装父子关系
List<BaseDataVO.Area> roots = new ArrayList<>();
for (BaseDataVO.Area area : map.values()) {
String parentId = area.getFatherId();
if (parentId == null || parentId.isEmpty()) {
roots.add(area);
} else {
BaseDataVO.Area parent = map.get(parentId);
if (parent != null) {
parent.getChildren().add(area);
}
}
}
// 3️⃣ 可选排序
sortTree(roots);
return roots;
}
/**
* 按 xh 排序(如果你用 order,也可以换)
*/
private void sortTree(List<BaseDataVO.Area> nodes) {
if (nodes == null || nodes.isEmpty()) {
return;
}
nodes.sort(Comparator.comparing(
BaseDataVO.Area::getXh,
Comparator.nullsLast(Integer::compareTo)
));
for (BaseDataVO.Area node : nodes) {
sortTree(node.getChildren());
}
}
}
@@ -4,10 +4,7 @@ import cn.link56.common.BasePageVO;
import cn.link56.common.Result;
import cn.link56.company.model.dto.QueryCompanyDTO;
import cn.link56.company.model.dto.UpdateCompanyDTO;
import cn.link56.company.model.vo.CompanyDetailVO;
import cn.link56.company.model.vo.QueryCompanyVO;
import cn.link56.company.model.vo.RecommendCompanyVO;
import cn.link56.company.model.vo.RecommendHotCompanyVO;
import cn.link56.company.model.vo.*;
import cn.link56.company.service.CompanyService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -61,4 +58,13 @@ public class CompanyController {
return Result.success();
}
@GetMapping("/area")
public Result<List<AreaVO>> listAreas(){
return Result.success(companyService.listAreas());
}
@GetMapping("/industry")
public Result<List<IndustryVO>> listIndustries(){
return Result.success(companyService.listIndustries());
}
}
@@ -2,6 +2,7 @@ package cn.link56.company.controller;
import cn.link56.common.BasePageVO;
import cn.link56.common.Result;
import cn.link56.common.context.UserContext;
import cn.link56.company.model.dto.AddCompanyUserDTO;
import cn.link56.company.model.dto.DeleteCompanyUserDTO;
import cn.link56.company.model.dto.QueryCompanyUserDTO;
@@ -29,32 +30,33 @@ public class CompanyUserController {
}
@PostMapping("/add")
public Result<Void> addCompanyUser(@RequestBody @Validated AddCompanyUserDTO companyUserDTO){
public Result<Void> addCompanyUser(@RequestBody @Validated AddCompanyUserDTO companyUserDTO) {
companyUserService.addCompanyUser(companyUserDTO);
return Result.success();
}
@PostMapping("/update")
public Result<Void> updateCompanyUser(@RequestBody @Validated UpdateCompanyUserDTO companyUserDTO){
public Result<Void> updateCompanyUser(@RequestBody @Validated UpdateCompanyUserDTO companyUserDTO) {
companyUserService.updateCompanyUser(companyUserDTO);
return Result.success();
}
@PostMapping("/delete")
public Result<Void> deleteCompanyUser(@RequestBody @Validated DeleteCompanyUserDTO deleteCompanyUserDTO){
public Result<Void> deleteCompanyUser(@RequestBody @Validated DeleteCompanyUserDTO deleteCompanyUserDTO) {
companyUserService.deleteCompanyUser(deleteCompanyUserDTO);
return Result.success();
}
@GetMapping("/chat/{companyId}")
public Result<List<ChatUserVO>> listCompanyChatUsers(@PathVariable("companyId") Long companyId){
public Result<List<ChatUserVO>> listCompanyChatUsers(@PathVariable("companyId") Long companyId) {
List<ChatUserVO> data = companyUserService.listCompanyChatUsers(companyId);
return Result.success(data);
}
@GetMapping("/profile/{userId}")
public Result<CompanyUserProfileVO> getCompanyUserProfile(@PathVariable("userId") Long userId){
public Result<CompanyUserProfileVO> getCompanyUserProfile(@PathVariable("userId") Long userId) {
CompanyUserProfileVO companyUserProfileVO =companyUserService.getCompanyUserProfile(userId);
return Result.success(companyUserProfileVO);
}
}
@@ -13,11 +13,5 @@ import java.util.List;
public interface CompanyMapper extends BaseMapper<Company> {
List<RecommendCompanyRow> listRecommend();
@Select(
"SELECT c.* " +
"FROM b_company c " +
"INNER JOIN b_company_user cu ON cu.company_id = c.id " +
"WHERE cu.user_id = #{userId} "
)
Company selectCompanyByUserId(@Param("userId") Long userId);
}
@@ -0,0 +1,17 @@
package cn.link56.company.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AreaVO {
private String id;
private Integer xh;
private String name;
private String fatherId;
private List<AreaVO> children;
}
@@ -0,0 +1,14 @@
package cn.link56.company.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class IndustryVO {
private Long id;
private Integer xh;
private String name;
}
@@ -1,5 +1,9 @@
package cn.link56.company.service;
import cn.link56.base.mapper.AreaMapper;
import cn.link56.base.mapper.IndustryMapper;
import cn.link56.base.model.po.Area;
import cn.link56.base.model.po.Industry;
import cn.link56.common.BasePageVO;
import cn.link56.common.context.UserContext;
import cn.link56.common.exception.BusinessException;
@@ -9,10 +13,7 @@ import cn.link56.company.model.dto.UpdateCompanyDTO;
import cn.link56.company.model.middle.RecommendCompanyRow;
import cn.link56.company.model.po.Company;
import cn.link56.company.model.po.CompanyUser;
import cn.link56.company.model.vo.CompanyDetailVO;
import cn.link56.company.model.vo.QueryCompanyVO;
import cn.link56.company.model.vo.RecommendCompanyVO;
import cn.link56.company.model.vo.RecommendHotCompanyVO;
import cn.link56.company.model.vo.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -23,8 +24,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
@Service
@@ -33,6 +33,10 @@ public class CompanyService extends ServiceImpl<CompanyMapper, Company> {
private CompanyMapper companyMapper;
@Resource
private CompanyUserService companyUserService;
@Resource
private IndustryMapper industryMapper;
@Resource
private AreaMapper areaMapper;
public List<RecommendCompanyVO> listRecommend() {
List<RecommendCompanyRow> rows = companyMapper.listRecommend();
@@ -242,4 +246,86 @@ public class CompanyService extends ServiceImpl<CompanyMapper, Company> {
companyUserService.updateCompanyAvatar(company.getId(),company.getLogo());
}
public List<IndustryVO> listIndustries() {
LambdaQueryWrapper<Industry> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Industry::getCanuse,"1");
List<Industry> industries = industryMapper.selectList(queryWrapper);
return industries.stream()
.map(industry -> new IndustryVO(
industry.getId(),
industry.getXh(),
industry.getName()
))
.collect(Collectors.toList());
}
public List<AreaVO> listAreas() {
List<Area> allAreas = areaMapper.listAllArea();
return buildAreaTree(allAreas);
}
/**
* BArea → Area 树
*/
private List<AreaVO> buildAreaTree(List<Area> areas) {
if (areas == null || areas.isEmpty()) {
return Collections.emptyList();
}
// id -> VO
Map<String, AreaVO> map = new HashMap<>(areas.size());
// 1️⃣ 先全部转 VO
for (Area a : areas) {
AreaVO vo = new AreaVO(
a.getId(),
a.getXh(),
a.getName(),
a.getFatherId(),
new ArrayList<>()
);
map.put(vo.getId(), vo);
}
// 2️⃣ 组装父子关系
List<AreaVO> roots = new ArrayList<>();
for (AreaVO area : map.values()) {
String parentId = area.getFatherId();
if (parentId == null || parentId.isEmpty()) {
roots.add(area);
} else {
AreaVO parent = map.get(parentId);
if (parent != null) {
parent.getChildren().add(area);
}
}
}
// 3️⃣ 可选排序
sortTree(roots);
return roots;
}
/**
* 按 xh 排序(如果你用 order,也可以换)
*/
private void sortTree(List<AreaVO> nodes) {
if (nodes == null || nodes.isEmpty()) {
return;
}
nodes.sort(Comparator.comparing(
AreaVO::getXh,
Comparator.nullsLast(Integer::compareTo)
));
for (AreaVO node : nodes) {
sortTree(node.getChildren());
}
}
}
@@ -15,6 +15,9 @@ import cn.link56.company.model.po.CompanyUser;
import cn.link56.company.model.vo.ChatUserVO;
import cn.link56.company.model.vo.CompanyUserProfileVO;
import cn.link56.company.model.vo.QueryCompanyUserVO;
import cn.link56.im.mapper.ImFriendMapper;
import cn.link56.im.mapper.ImGroupMapper;
import cn.link56.im.mapper.ImGroupUserMapper;
import cn.link56.im.storage.SessionManager;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -330,13 +333,12 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
}
public CompanyUserProfileVO getCompanyUserProfile(Long userId) {
CompanyUser companyUserPO = companyUserMapper.selectById(userId);
if (companyUserPO == null) {
return null;
}
CompanyUserProfileVO cardVO = new CompanyUserProfileVO();
CompanyUserProfileVO profileVO = new CompanyUserProfileVO();
// ===== 用户(联系人)=====
CompanyUserProfileVO.User userVO = new CompanyUserProfileVO.User();
@@ -364,8 +366,9 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
);
}
cardVO.setUser(userVO);
cardVO.setCompany(companyVO);
return cardVO;
profileVO.setUser(userVO);
profileVO.setCompany(companyVO);
return profileVO;
}
}
@@ -71,7 +71,7 @@ public class ImController {
@PostMapping("/private/update/unread")
public Result<Void> updatePrivateUnread(@RequestBody UpdatePrivateUnreadDTO dto){
Long userId = dto.getUserId();
Long userId = UserContext.getUserId();
Long friendId = dto.getFriendId();
imFriendService.decreaseUnreadCount(userId,friendId);
@@ -81,7 +81,7 @@ public class ImController {
@PostMapping("/group/update/unread")
public Result<Void> updateGroup(@RequestBody UpdateGroupUnreadDTO dto){
Long userId = dto.getUserId();
Long userId = UserContext.getUserId();
Long groupId = dto.getGroupId();
imGroupUserService.decreaseUnreadCount(groupId,userId);
@@ -103,4 +103,14 @@ public class ImController {
return Result.success(groupProfileVO);
}
@GetMapping("/unreadCount")
public Result<Long> getAllUnreadCount(){
Long userId = UserContext.getUserId();
Long friendUnread = imFriendService.getAllUnreadCount(userId);
Long groupUnread = imGroupUserService.getAllUnreadCount(userId);
return Result.success(friendUnread + groupUnread);
}
}
@@ -6,6 +6,7 @@ import cn.link56.im.model.vo.QueryFriendVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@@ -41,4 +42,8 @@ public interface ImFriendMapper extends BaseMapper<ImFriend> {
@Param("userId") Long userId,
@Param("friendId") Long friendId
);
@Select("SELECT SUM(b_unread_count) FROM b_im_friend WHERE b_user_id = #{userId}")
Long selectAllUnreadCount(@Param("userId") Long userId);
}
@@ -6,6 +6,7 @@ import cn.link56.im.model.vo.QueryGroupVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@@ -19,10 +20,14 @@ public interface ImGroupMapper extends BaseMapper<ImGroup> {
@Param("id") Long id
);
Long selectGroupTotal(@Param("userId") Long userId,
@Param("name") String name);
Long selectGroupTotal(
@Param("userId") Long userId,
@Param("name") String name
);
GroupProfileVO selectGroupProfile(
@Param("groupId") Long groupId,
@Param("userId") Long userId);
@Param("userId") Long userId
);
}
@@ -5,6 +5,7 @@ import cn.link56.im.model.vo.QueryGroupUserVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@@ -14,4 +15,8 @@ public interface ImGroupUserMapper extends BaseMapper<ImGroupUser> {
@Param("groupId") Long groupId,
@Param("userName") String userName
);
@Select("SELECT SUM(b_unread_count) FROM b_im_group_user WHERE b_user_id = #{userId}")
Long selectAllUnreadCount(@Param("userId") Long userId);
}
@@ -9,5 +9,4 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
public class UpdateGroupUnreadDTO {
private Long groupId;
private Long userId;
}
@@ -8,6 +8,5 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
public class UpdatePrivateUnreadDTO {
private Long userId;
private Long friendId;
}
@@ -4,6 +4,8 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@@ -11,5 +13,12 @@ public class FriendProfileVO {
private Long friendId;
private String friendName;
private String friendAvatar;
private Date lastMessageTime;
private Integer lastMessageType;
private String lastMessageContent;
private Integer unreadCount;
private Boolean isOnline;
}
@@ -25,7 +25,7 @@ public class ImFriendService extends ServiceImpl<ImFriendMapper, ImFriend> {
private ImFriendMapper imFriendMapper;
public void createFriend(Long userId,Long friendId){
// 先检查是否存在
// 先检查是否存在好友关系
LambdaQueryWrapper<ImFriend> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ImFriend::getUserId,userId)
.eq(ImFriend::getFriendId,friendId);
@@ -108,6 +108,14 @@ public class ImFriendService extends ServiceImpl<ImFriendMapper, ImFriend> {
public FriendProfileVO getFriendProfile(Long friendId) {
Long userId = UserContext.getUserId();
return imFriendMapper.selectFriendProfile(userId, friendId);
FriendProfileVO friendProfileVO = imFriendMapper.selectFriendProfile(userId, friendId);
friendProfileVO.setIsOnline(SessionManager.isOnline(friendId));
return friendProfileVO;
}
public Long getAllUnreadCount(Long userId){
return imFriendMapper.selectAllUnreadCount(userId);
}
}
@@ -50,4 +50,6 @@ public class ImGroupService extends ServiceImpl<ImGroupMapper, ImGroup>{
return imGroupMapper.selectGroupProfile(groupId,userId);
}
}
@@ -94,4 +94,7 @@ public class ImGroupUserService extends ServiceImpl<ImGroupUserMapper, ImGroupUs
imGroupUserMapper.update(updateWrapper);
}
public Long getAllUnreadCount(Long userId){
return imGroupUserMapper.selectAllUnreadCount(userId);
}
}
@@ -30,5 +30,21 @@
sheng.b_order ASC,
shi.b_order ASC
</select>
<select id="selectCompanyByUserId" resultType="cn.link56.company.model.po.Company">
SELECT
c.b_id as id,
c.b_name as name,
c.b_logo as logo,
c.b_profile as profile,
c.b_area as area,
c.b_industry as industry
FROM
b_company c
INNER JOIN b_company_user cu ON cu.b_company_id = c.b_id
WHERE
cu.b_id = #{userId}
</select>
</mapper>
@@ -13,7 +13,7 @@
u.b_name AS friendName,
u.b_avatar AS friendAvatar,
f.b_unread_count AS unreadCount,
m.b_send_time AS lastMessageTime,
m.b_send_time AS lastMessageTime,
m.b_type AS lastMessageType,
m.b_content AS lastMessageContent,
ROW_NUMBER() OVER (
@@ -64,12 +64,17 @@
<select id="selectFriendProfile" resultType="cn.link56.im.model.vo.FriendProfileVO">
SELECT
f.b_friend_id AS friendId,
u.b_name AS friendName,
u.b_avatar AS friendAvatar,
f.b_unread_count AS unreadCount
f.b_friend_id AS friendId,
u.b_name AS friendName,
u.b_avatar AS friendAvatar,
f.b_unread_count AS unreadCount,
m.b_send_time AS lastMessageTime,
m.b_type AS lastMessageType,
m.b_content AS lastMessageContent
FROM b_im_friend f
INNER JOIN b_company_user u ON f.b_friend_id = u.b_id
INNER JOIN b_company_user u ON f.b_friend_id = u.b_id
LEFT JOIN b_im_private_message m ON f.b_last_message_id = m.b_id
WHERE f.b_user_id = #{userId} AND f.b_friend_id = #{friendId}
</select>
</mapper>
+34 -25
View File
@@ -1,9 +1,9 @@
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { onBeforeUnmount, onMounted } from 'vue'
import { initBaseApi } from '@/request/api/base'
import { useCommonStore } from '@/store/common'
import { getPostTagApi } from './request/api/post'
import { listAreasApi, listIndustriesApi } from './request/api/company'
import { listPostTagApi } from './request/api/post'
import { useWebSocket } from './store/websocket'
const websocket = useWebSocket()
@@ -20,34 +20,43 @@ onBeforeUnmount(() => {
websocket.close()
})
async function handleInitData(): Promise<void> {
try {
// 并行执行三个 API 请求
const [areaRes, industryRes, postTagRes] = await Promise.all([
listAreasApi(),
listIndustriesApi(),
listPostTagApi(),
])
function handleInitData() {
initBaseApi().then((res) => {
if (res.isSuccess) {
commonOptions.areaOptions.splice(0, commonOptions.areaOptions.length, ...res.data.areas || [])
commonOptions.industryOptions.splice(0, commonOptions.industryOptions.length, ...res.data.industries || [])
// 处理区域数据
if (areaRes.isSuccess) {
commonOptions.setAreaOptions(areaRes.data)
}
else {
ElMessage.error(res.message || '初始化失败,请刷新页面后重试')
ElMessage.error(areaRes.message || '初始化失败,请刷新页面后重试')
}
}).catch((err) => {
console.log(err)
ElMessage.error('初始化失败,请刷新页面后重试')
})
getPostTagApi()
.then((res) => {
if (res.isSuccess) {
commonOptions.postTagOptions.splice(0, commonOptions.postTagOptions.length, ...res.data || [])
}
else {
ElMessage.error(res.message || '初始化失败,请刷新页面后重试')
}
})
.catch((err) => {
console.error(err)
ElMessage.error('初始化失败,请刷新页面后重试')
})
// 处理行业数据
if (industryRes.isSuccess) {
commonOptions.setIndustryOptions(industryRes.data)
}
else {
ElMessage.error(industryRes.message || '初始化失败,请刷新页面后重试')
}
// 处理岗位标签数据
if (postTagRes.isSuccess) {
commonOptions.setPostTagOptions(postTagRes.data)
}
else {
ElMessage.error(postTagRes.message || '初始化失败,请刷新页面后重试')
}
}
catch (err) {
console.error(err)
ElMessage.error('初始化失败,请刷新页面后重试')
}
}
</script>
@@ -1,36 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue'
const props = defineProps({
userId: {
type: Number,
required: true,
},
})
</script>
<template>
<el-dialog
title="Tips"
width="500"
>
<span>This is a message</span>
</el-dialog>
</template>
<style lang="scss" scoped>
.loading-wrapper,
.empty-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
}
.empty-wrapper {
flex-direction: column;
.empty-tip {
margin-top: 8px;
}
}
</style>
@@ -0,0 +1,267 @@
<script lang="ts" setup>
import type { CompanyUserProfile } from '@/types/company/user'
import { ElMessage } from 'element-plus'
import { ref } from 'vue'
import { getCompanyUserProfileApi } from '@/request/api/company/user'
import { createFriendApi } from '@/request/api/im'
import { useUserStore } from '@/store/user'
import SafeImage from './SafeImage.vue'
import SvgIcon from './SvgIcon.vue'
const props = defineProps({
userId: {
type: [Number, null],
required: true,
},
})
const emits = defineEmits<{
(e: 'startChat'): void
}>()
const userStore = useUserStore()
const loading = ref(false)
const profile = ref<CompanyUserProfile>({
company: {
id: 0,
name: '',
logo: '',
profile: '',
areas: [],
industries: [],
},
user: {
id: 0,
name: '',
position: '',
avatar: '',
email: '',
phone: '',
isOnline: false,
},
})
function onOpened() {
loading.value = true
if (!props.userId) {
return
}
getCompanyUserProfileApi(props.userId).then((res) => {
if (res.isSuccess) {
profile.value = res.data
loading.value = false
}
else {
ElMessage.error(res.message || '加载用户信息失败,请稍后再试')
}
}).catch((err) => {
console.log(err)
ElMessage.error('加载用户信息失败,请稍后再试')
})
}
function onClosed() {
loading.value = true
profile.value = {
company: {
id: 0,
name: '',
logo: '',
profile: '',
areas: [],
industries: [],
},
user: {
id: 0,
name: '',
position: '',
avatar: '',
email: '',
phone: '',
isOnline: false,
},
}
}
function handleChat() {
if (props.userId === null) {
return
}
if (props.userId === userStore.user.id) {
ElMessage.warning('无法与自己发起聊天')
return
}
// 创建好友关系
createFriendApi({
friendId: props.userId,
}).then((res) => {
if (res.isSuccess) {
emits('startChat')
}
else {
ElMessage.error(res.message || '发起聊天失败,请稍后再试')
}
}).catch((err) => {
console.log(err)
ElMessage.error('发起聊天失败,请稍后再试')
})
}
</script>
<template>
<el-dialog
width="700"
class="dlalog-wrapper"
@opened="onOpened"
@closed="onClosed"
>
<div v-if="loading" class="loading-wrapper">
<SvgIcon icon="svg-spinners:ring-resize" :size="20" />
</div>
<div v-else>
<div class="user">
<el-badge
is-dot
:color="profile.user.isOnline ? '#24e68a' : '#bfbfbf'"
:badge-style="{
width: '12px',
height: '12px',
}"
:offset="[-4, 42]"
>
<SafeImage class="user-logo" :size="52" round :url="profile.user.avatar" />
</el-badge>
<div class="user-info">
<div class="user-line">
<span>{{ profile.user.name }}</span>
<span v-if="profile.user.position" class="position"> · {{ profile.user.position }}</span>
</div>
<div class="user-contract">
<div v-if="profile.user.phone" class="user-contract__item">
<SvgIcon icon="material-symbols:phone-android-outline" />
<span>{{ profile.user.phone }}</span>
</div>
<div v-if="profile.user.email" class="user-contract__item">
<SvgIcon icon="material-symbols:mail-outline" />
<span>{{ profile.user.email }}</span>
</div>
</div>
<div class="user-action">
<el-button size="small" type="primary" @click="handleChat">
发起聊天
</el-button>
</div>
</div>
</div>
<el-divider />
<div class="company">
<div class="company-name">
{{ profile.company.name }}
</div>
<div class="company-meta">
<span>{{ profile.company.industries.join(" / ") }}</span>
<span v-if="profile.company.areas.length > 0"> · {{ profile.company.areas[0] }} </span>
</div>
<div v-if="profile.company.profile" class="company-profile">
{{ profile.company.profile }}
</div>
</div>
</div>
</el-dialog>
</template>
<style lang="scss" scoped>
.dlalog-wrapper {
.el-divider {
margin: 16px 0px;
}
}
.loading-wrapper,
.empty-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
}
.empty-wrapper {
flex-direction: column;
.empty-tip {
margin-top: 8px;
}
}
.user {
display: flex;
.user-logo {
}
.user-info {
margin-left: 16px;
.user-line {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #262626;
.position {
font-weight: 400;
color: #595959;
font-size: 14px;
}
}
.user-contract {
margin-top: 6px;
font-size: 13px;
color: #595959;
gap: 16px;
.user-contract__item {
display: flex;
align-items: center;
gap: 4px;
margin-top: 4px;
}
}
.user-action {
margin-top: 6px;
}
}
}
.company {
.company-name {
font-size: 14px;
font-weight: 500;
color: #262626;
}
.company-meta {
margin-top: 4px;
font-size: 12px;
color: #8c8c8c;
}
.company-profile {
margin-top: 8px;
font-size: 13px;
color: #595959;
line-height: 1.6;
}
}
</style>
@@ -1,13 +1,16 @@
<script lang="ts" setup>
import {
CircleClose,
User,
} from '@element-plus/icons-vue'
import { CircleClose, User } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import SafeImage from '@/components/SafeImage.vue'
import { getUnreadCountApi } from '@/request/api/im'
import { useCountStore } from '@/store/count'
import { useUserStore } from '@/store/user'
import { useWebSocket } from '@/store/websocket'
const countStore = useCountStore()
const websocket = useWebSocket()
const route = useRoute()
const router = useRouter()
@@ -18,9 +21,27 @@ const menuList = [
{ name: '企业名录', path: '/company' },
{ name: '论坛中心', path: '/post' },
{ name: '关于我们', path: '/aboutus' },
{ name: '联系我们', path: '/forum' },
// { name: '联系我们', path: '/forum' },
]
watch(() => userStore.isLogined, (newVal) => {
if (newVal) {
getUnreadCountApi().then((res) => {
if (res.isSuccess) {
countStore.setImCount(res.data || 0)
}
else {
ElMessage.error(res.message || '获取IM未读消息数量失败,请稍后再试')
}
}).catch((err) => {
console.log(err)
ElMessage.error('获取IM未读消息数量失败,请稍后再试')
})
}
}, {
immediate: true,
})
function navigateTo(path: string) {
router.push(path)
}
@@ -58,9 +79,15 @@ function isActive(path: string) {
</span>
</div>
<div class="action">
<i-tdesign:search class="action-item" />
<SvgIcon icon="solar:chat-round-line-linear" class="action-item" @click="navigateTo('/im')" />
<!-- <SvgIcon icon="tdesign:search" class="action-item"/> -->
<el-badge
:value="countStore.imCount"
:max="99"
:offset="[-22, 0]"
:show-zero="false"
>
<SvgIcon icon="solar:chat-round-line-linear" class="action-item" @click="navigateTo('/im')" />
</el-badge>
<template v-if="userStore.isLogined">
<el-divider direction="vertical" />
@@ -169,6 +196,10 @@ function isActive(path: string) {
flex-shrink: 0;
min-width: 0;
:deep(.el-badge) {
display: flex;
}
.action-item {
font-size: 20px;
margin-right: 24px;
@@ -1,10 +1,6 @@
import type { AddFileParams, GetAliyunKeyResult, InitBaseDataResult } from '@/types/base'
import type { AddFileParams, GetAliyunKeyResult } from '@/types/base'
import { httpGet, httpPostJson } from '@/request/utils'
export function initBaseApi() {
return httpGet<InitBaseDataResult>(`/base/init`)
}
export function getAliyunKeyApi() {
return httpGet<GetAliyunKeyResult>(`/base/file/getAliyunKey`)
}
@@ -1,4 +1,4 @@
import type { Company, CompanyDetail, QueryCompanyParams, RecommendCompany, RecommendHotCompany, UpdateCompanyParams } from '@/types/company'
import type { Area, Company, CompanyDetail, Industry, QueryCompanyParams, RecommendCompany, RecommendHotCompany, UpdateCompanyParams } from '@/types/company'
import { httpGet, httpPostJson } from '@/request/utils'
export function getRecommendCompanyApi() {
@@ -20,3 +20,11 @@ export function getCompanyDetailApi(id: number) {
export function updateCompanyApi(params: UpdateCompanyParams) {
return httpPostJson<void>(`/company/update`, params)
}
export function listAreasApi() {
return httpGet<Area[]>('/company/area')
}
export function listIndustriesApi() {
return httpGet<Industry[]>('/company/industry')
}
@@ -1,4 +1,4 @@
import type { AddCompanyUserParams, ChatCompanyUser, CompanyUser, DeleteCompanyUserParams, QueryCompanyUserParams, UpdateCompanyUserParams } from '@/types/company/user'
import type { AddCompanyUserParams, ChatCompanyUser, CompanyUser, CompanyUserProfile, DeleteCompanyUserParams, QueryCompanyUserParams, UpdateCompanyUserParams } from '@/types/company/user'
import { httpGet, httpPostJson } from '@/request/utils'
export function queryCompanyUserApi(params: QueryCompanyUserParams) {
@@ -17,3 +17,7 @@ export function deleteCompanyUserApi(params: DeleteCompanyUserParams) {
export function listChatCompanyUsersApi(companyId: number) {
return httpGet<ChatCompanyUser[]>(`/company/user/chat/${companyId}`)
}
export function getCompanyUserProfileApi(userId: number) {
return httpGet<CompanyUserProfile>(`/company/user/profile/${userId}`)
}
@@ -39,3 +39,7 @@ export function getFriendProfileApi(friendId: number) {
export function getGroupProfileApi(groupId: number) {
return httpGet<ImGroupProfile>(`/im/group/${groupId}`)
}
export function getUnreadCountApi() {
return httpGet<number>('/im/unreadCount')
}
@@ -2,7 +2,7 @@ import type { CreatePostCommentParams, CreatePostCommentResult, PostComment, Pos
import { httpGet, httpPostJson } from '@/request/utils'
// ===== 公共接口 =====
export function getPostTagApi() {
export function listPostTagApi() {
return httpGet<PostTag[]>('/post/tag')
}
@@ -1,4 +1,6 @@
import type { Area, CompanyQuery, Industry, PostTag } from '@/types/base'
import type { CompanyQuery } from '@/types/base'
import type { Area, Industry } from '@/types/company'
import type { PostTag } from '@/types/post'
import { defineStore } from 'pinia'
import { computed, reactive, ref } from 'vue'
@@ -19,11 +21,26 @@ export const useCommonStore = defineStore('common', () => {
...postTagOptions,
])
function setAreaOptions(options: Area[]) {
areaOptions.splice(0, areaOptions.length, ...options)
}
function setIndustryOptions(options: Industry[]) {
industryOptions.splice(0, industryOptions.length, ...options)
}
function setPostTagOptions(options: PostTag[]) {
postTagOptions.splice(0, postTagOptions.length, ...options)
}
return {
areaOptions,
industryOptions,
companyQuery,
postTagOptions, // 原始数据
postTagOptionsWithAll, // 带“全部”,响应式
setAreaOptions,
setIndustryOptions,
setPostTagOptions,
}
})
@@ -0,0 +1,25 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCountStore = defineStore('count', () => {
const imCount = ref(0)
function incrementImCount(amount: number = 1) {
imCount.value += amount
}
function decrementImCount(amount: number = 1) {
imCount.value = Math.max(imCount.value - amount, 0)
}
function setImCount(value: number) {
imCount.value = value
}
return {
imCount,
incrementImCount,
decrementImCount,
setImCount,
}
})
+2 -1
View File
@@ -16,8 +16,9 @@ declare module 'vue' {
ChatCompanyUserCard: typeof import('./../components/ChatCompanyUserCard.vue')['default']
ChatCompanyUserSelect: typeof i../components/ChatCompanyUserCard.vuet.vue')['default']
ChatUserSelect: typeof import('./../components/ChatUserSelect.vue')['default']
CompanyUserCard: typeof import('./../components/CompanyUserCard.vue')['default']
CompanyUserCard: typeof import('../components/CompanyUserProfileDialog.vue')['default']
CompanyUserPopover: typeof import('./../components/CompanyUserPopover.vue')['default']
CompanyUserProfileDialog: typeof import('./../components/CompanyUserProfileDialog.vue')['default']
CustomEmpty: typeof import('./../components/CustomEmpty.vue')['default']
CustomLoading: typeof import('./../components/CustomLoading.vue')['default']
Directory: typeof import('./../components/wang-editor/modules/directory.vue')['default']
@@ -1,27 +1,3 @@
export interface Area {
id: string
xh: number
name: string
fatherId: string
children: Area[]
}
export interface Industry {
id: number
xh: number
name: string
}
export interface PostTag {
id: number
name: string
icon: string
}
export interface InitBaseDataResult {
industries: Industry[]
areas: Area[]
}
export interface CompanyQuery {
industries: string[]
@@ -1,3 +1,17 @@
export interface Area {
id: string
xh: number
name: string
fatherId: string
children: Area[]
}
export interface Industry {
id: number
xh: number
name: string
}
export interface RecommendCompany {
shengName: string
shiName: string
@@ -126,12 +126,10 @@ export interface ImGroupMessage {
}
export interface UpdatePrivateUnreadParams {
userId: number
friendId: number
}
export interface UpdateGroupUnreadParams {
groupId: number
userId: number
}
export interface QueryGroupUserParams {
@@ -152,6 +150,12 @@ export interface ImFriendProfile {
friendName: string
friendAvatar: string
unreadCount: number
lastMessageTime: number
lastMessageType: number
lastMessageContent: string
isOnline: boolean
}
export interface ImGroupProfile {
@@ -3,7 +3,7 @@ import type { Company } from '@/types/company'
import { ElMessage } from 'element-plus'
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import ChatUserSelect from '@/components/ChatUserSelect.vue'
import ChatCompanyUserCard from '@/components/ChatCompanyUserCard.vue'
import QueryCompany from '@/components/QueryCompany.vue'
import { queryCompanyApi } from '@/request/api/company'
import { useCommonStore } from '@/store/common'
@@ -111,7 +111,7 @@ function toDetail(data: Company) {
</div>
</div>
<div class="company-actions">
<ChatUserSelect :company-id="item.id">
<ChatCompanyUserCard :company-id="item.id">
<el-button
type="primary"
>
@@ -120,7 +120,7 @@ function toDetail(data: Company) {
发起聊天
</span>
</el-button>
</ChatUserSelect>
</ChatCompanyUserCard>
</div>
</div>
</div>
@@ -368,7 +368,7 @@ function handleClickGroupUser(groupUser: ImGroupUser) {
</div>
<div class="im__group-section-header-extra">
<div class="im__group-section-header--search">
<SvgIcon icon="lucide:search" :size="16" />
<!-- <SvgIcon icon="lucide:search" :size="16" /> -->
</div>
</div>
</div>
@@ -439,6 +439,7 @@ function handleClickGroupUser(groupUser: ImGroupUser) {
width: 100%;
.im__chat-message-avatar {
flex-shrink: 0;
cursor: pointer;
}
.im__chat-message-body {
width: 100%;
@@ -384,6 +384,7 @@ function handleClickAvatar(userId: number) {
width: 100%;
.im__chat-message-avatar {
flex-shrink: 0;
cursor: pointer;
}
.im__chat-message-body {
width: 100%;
@@ -1,20 +1,31 @@
<script lang="ts" setup>
import type { ImFriend, ImGroup } from '@/types/im'
import type { ImFriend, ImGroup, ImGroupUser } from '@/types/im'
import { Search } from '@element-plus/icons-vue'
import { computed, nextTick, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import CompanyUserProfileDialog from '@/components/CompanyUserProfileDialog.vue'
import { getFriendProfileApi, getGroupProfileApi, queryFriendApi, queryGroupApi, updateGroupUnreadApi, updatePrivateUnreadApi } from '@/request/api/im'
import { useCountStore } from '@/store/count'
import { useUserStore } from '@/store/user'
import { useWebSocket } from '@/store/websocket'
import { MessageType } from '@/types/im'
import { formatImTime } from '@/utils/time'
import GroupChatWindows from './components/GroupChatWindows.vue'
import PrivateChatWindow from './components/PrivateChatWindow.vue'
const countStore = useCountStore()
const userStore = useUserStore()
const route = useRoute()
const websocket = useWebSocket()
// 是否第一次加载
const isFirstLoad = ref(true)
// 公司用户信息
const showUserProfile = ref(false)
const userProfileId = ref<number | null>(null)
// 当前聊天对象
const searchName = ref('')
@@ -65,7 +76,9 @@ const groupState = ref<ImListState<ImGroup>>({
})
// 初始化
onMounted(() => {
onMounted(async () => {
isFirstLoad.value = true
const userId = userStore.user.id
const type = route.params.type as ChatType | undefined
@@ -75,10 +88,12 @@ onMounted(() => {
selectedChat.value = { type, id }
}
nextTick(() => {
loadGroups()
loadFriends()
})
await Promise.all([
loadGroups(),
loadFriends(),
])
isFirstLoad.value = false
websocket.onMessage(async (msg) => {
const { type, from, data } = msg
@@ -146,6 +161,8 @@ onMounted(() => {
// 未读数处理
if (userId !== senderId && targetFriendId !== chatFriendId) {
friend.unreadCount = (friend.unreadCount || 0) + 1
countStore.incrementImCount(1)
}
// 当前聊天窗口
@@ -157,7 +174,6 @@ onMounted(() => {
if (userId !== senderId) {
currentFriendChat.value.unreadCount = 0
updatePrivateUnreadApi({
userId,
friendId: chatFriendId!,
})
}
@@ -227,6 +243,7 @@ onMounted(() => {
// 4 未读处理(不是自己 & 当前群未打开)
if (senderId !== userId && groupId !== currentGroupId) {
group.unreadCount = (group.unreadCount || 0) + 1
countStore.incrementImCount(1)
}
// 5 当前群窗口处理
@@ -240,7 +257,6 @@ onMounted(() => {
currentGroupChat.value.unreadCount = 0
updateGroupUnreadApi({
userId,
groupId,
})
}
@@ -270,7 +286,7 @@ async function loadFriends() {
currentPage: state.page,
pageSize: state.pageSize,
name: searchName.value,
id: isPrivateChat.value ? (selectedChat.value?.id ?? null) : null,
id: (isFirstLoad.value && isPrivateChat.value) ? selectedChat.value.id : null,
})
if (!res.isSuccess)
@@ -278,15 +294,21 @@ async function loadFriends() {
const { records, total } = res.data
state.list
= state.page === 1
? records
: [...state.list, ...records]
// 去重:合并新记录并避免重复 friendId
const newRecords = records.filter(
newFriend => !state.list.some(existingFriend => existingFriend.friendId === newFriend.friendId),
)
// 合并新好友记录到当前列表
state.list = state.page === 1
? newRecords
: [...state.list, ...newRecords]
state.total = total
state.hasMore = state.list.length < total
state.page++
// 如果是私聊,选中的好友移到首位
if (isPrivateChat.value && selectedChat.value.id) {
const index = state.list.findIndex(
item => item.friendId === selectedChat.value.id,
@@ -295,7 +317,7 @@ async function loadFriends() {
if (index !== -1) {
const [target] = state.list.splice(index, 1)
// 移动到第一位
// 将选中的好友移到第一位
state.list.unshift(target)
currentFriendChat.value = target
@@ -322,7 +344,7 @@ async function loadGroups() {
currentPage: state.page,
pageSize: state.pageSize,
name: searchName.value,
id: isGroupChat.value ? (selectedChat.value?.id ?? null) : null,
id: (isFirstLoad.value && isGroupChat.value) ? selectedChat.value?.id : null,
})
if (!res.isSuccess)
@@ -330,14 +352,35 @@ async function loadGroups() {
const { records, total } = res.data
state.list
= state.page === 1
? records
: [...state.list, ...records]
// 去重:合并新记录并避免重复 groupId
const newRecords = records.filter(
newGroup => !state.list.some(existingGroup => existingGroup.groupId === newGroup.groupId),
)
// 合并新群组记录到当前列表
state.list = state.page === 1
? newRecords
: [...state.list, ...newRecords]
state.total = total
state.hasMore = state.list.length < total
state.page++
// 如果是群组聊天,选中的群组移到首位
if (isGroupChat.value && selectedChat.value.id) {
const index = state.list.findIndex(
item => item.groupId === selectedChat.value.id,
)
if (index !== -1) {
const [target] = state.list.splice(index, 1)
// 将选中的群组移到第一位
state.list.unshift(target)
currentGroupChat.value = target
}
}
}
catch (e) {
console.error(e)
@@ -346,7 +389,49 @@ async function loadGroups() {
state.loading = false
}
}
// 好友列表滚动
function onFriendListScroll(e: Event) {
const target = e.target as HTMLElement
// 确保 target 是一个有效的 DOM 元素
if (!target || !(target instanceof HTMLElement)) {
return
}
const totalPages = Math.ceil(friendState.value.total / friendState.value.pageSize)
// 滚动到底部,加载下一页
if (target.scrollHeight - target.scrollTop - target.clientHeight <= 5) {
// 确保没有正在加载且页数小于总页数
if (friendState.value.page < totalPages && !friendState.value.loading) {
friendState.value.page += 1
loadFriends()
}
}
}
// 群组列表滚动
function onGroupListScroll(e: Event) {
const target = e.target as HTMLElement
// 确保 target 是一个有效的 DOM 元素
if (!target || !(target instanceof HTMLElement)) {
return
}
const totalPages = Math.ceil(groupState.value.total / groupState.value.pageSize)
// 滚动到底部,加载下一页
if (target.scrollHeight - target.scrollTop - target.clientHeight <= 5) {
// 确保没有正在加载且页数小于总页数
if (groupState.value.page < totalPages && !groupState.value.loading) {
groupState.value.page += 1
loadGroups()
}
}
}
// 重置
function resetList<T>(state: ImListState<T>) {
state.page = 1
state.total = 0
@@ -355,6 +440,9 @@ function resetList<T>(state: ImListState<T>) {
}
function handleSearch() {
selectedChat.value.id = null
selectedChat.value.type = ''
resetList(friendState.value)
resetList(groupState.value)
@@ -365,11 +453,6 @@ function handleSearch() {
function onPrivateChat(friend: ImFriend) {
currentGroupChat.value = null
const userId = userStore.user.id
// 将 friend 的未读置0
const hadUnread = friend.unreadCount > 0
friend.unreadCount = 0
// 设置当前聊天
selectedChat.value = {
type: 'private',
@@ -378,9 +461,13 @@ function onPrivateChat(friend: ImFriend) {
currentFriendChat.value = friend
if (hadUnread) {
// 更新未读数量
if (friend.unreadCount > 0) {
countStore.decrementImCount(friend.unreadCount)
friend.unreadCount = 0
updatePrivateUnreadApi({
userId,
friendId: friend.friendId,
})
}
@@ -389,12 +476,6 @@ function onPrivateChat(friend: ImFriend) {
function onGroupChat(group: ImGroup) {
currentFriendChat.value = null
const userId = userStore.user.id
// 将 group 的未读置0
const hadUnread = group.unreadCount > 0
group.unreadCount = 0
// 设置当前聊天
selectedChat.value = {
type: 'group',
@@ -402,13 +483,106 @@ function onGroupChat(group: ImGroup) {
}
currentGroupChat.value = group
if (hadUnread) {
// 更新未读数量
if (group.unreadCount > 0) {
countStore.decrementImCount(group.unreadCount)
group.unreadCount = 0
updateGroupUnreadApi({
groupId: group.groupId,
userId,
})
}
}
function handlePrivateAvatarClick(id: number) {
userProfileId.value = id
showUserProfile.value = true
}
function handleGroupAvatarClick(id: number) {
userProfileId.value = id
showUserProfile.value = true
}
function handleGroupUserClick(groupUser: ImGroupUser) {
userProfileId.value = groupUser.userId
showUserProfile.value = true
}
async function handleStartChat() {
const friendId = userProfileId.value
if (friendId === null) {
return
}
// 如果当前聊天已是目标好友,关闭用户资料界面
if (selectedChat.value.type === 'private' && selectedChat.value.id === friendId) {
userProfileId.value = null
showUserProfile.value = false
return
}
// 查找是否存在该好友
let friend = friendState.value.list.find(item => item.friendId === friendId)
// 如果好友不存在,拉取好友数据并添加
if (!friend) {
try {
const res = await getFriendProfileApi(friendId)
friend = {
friendId: res.data.friendId,
friendAvatar: res.data.friendAvatar,
friendName: res.data.friendName,
unreadCount: 0,
lastMessageTime: res.data.lastMessageTime,
lastMessageContent: res.data.lastMessageContent,
lastMessageType: res.data.lastMessageType,
isOnline: res.data.isOnline,
}
// 更新未读数量
if (res.data.unreadCount > 0) {
updatePrivateUnreadApi({ friendId })
}
// 将新好友添加到列表首位
friendState.value.list.unshift(friend)
}
catch (e) {
console.error('getFriendProfileApi failed', e)
return // 出现错误时提前返回
}
}
else {
// 如果好友已存在,更新未读消息并将其移到首位
if (friend.unreadCount > 0) {
friend.unreadCount = 0
updatePrivateUnreadApi({ friendId })
}
// 将该好友移到列表首位
const index = friendState.value.list.findIndex(item => item.friendId === friendId)
if (index !== -1) {
friendState.value.list.splice(index, 1)
friendState.value.list.unshift(friend)
}
}
// 更新选中的聊天为当前好友
selectedChat.value = { type: 'private', id: friend.friendId }
currentGroupChat.value = null
currentFriendChat.value = friend
// 清除用户资料显示
userProfileId.value = null
showUserProfile.value = false
}
</script>
<template>
@@ -459,7 +633,7 @@ function onGroupChat(group: ImGroup) {
单聊
</span>
</div>
<div class="im__list-body">
<div class="im__list-body" @scroll="onFriendListScroll">
<div
v-for="item in friendState.list" :key="item.friendId" class="im__chat-item"
:class="{
@@ -506,7 +680,7 @@ function onGroupChat(group: ImGroup) {
群聊
</span>
</div>
<div class="im__list-body">
<div class="im__list-body" @scroll="onGroupListScroll">
<div
v-for="item in groupState.list"
:key="item.groupId"
@@ -562,7 +736,7 @@ function onGroupChat(group: ImGroup) {
<span> ]</span>
</div>
<span>
公司描述等等等...
<!-- 公司描述等等等... -->
</span>
</div>
</div>
@@ -571,13 +745,15 @@ function onGroupChat(group: ImGroup) {
</div>
<div class="im__window-body">
<PrivateChatWindow v-if="isPrivateChat && currentFriendChat != null" :friend="currentFriendChat" />
<GroupChatWindows v-if="isGroupChat && currentGroupChat != null" :group="currentGroupChat" />
<PrivateChatWindow v-if="isPrivateChat && currentFriendChat != null" :friend="currentFriendChat" @avatar-click="handlePrivateAvatarClick" />
<GroupChatWindows v-if="isGroupChat && currentGroupChat != null" :group="currentGroupChat" @avatar-click="handleGroupAvatarClick" @user-click="handleGroupUserClick" />
</div>
</div>
</div>
</div>
</div>
<CompanyUserProfileDialog v-model="showUserProfile" :user-id="userProfileId" @start-chat="handleStartChat" />
</template>
<style lang="scss" scoped>
@@ -2,7 +2,10 @@
import type { PostComment, PublicPostDetail } from '@/types/post'
import { ElMessage, ElMessageBox } from 'element-plus'
import { nextTick, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import CompanyUserProfileDialog from '@/components/CompanyUserProfileDialog.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { createFriendApi } from '@/request/api/im'
import { createPostCommentApi, deletePostCommentApi } from '@/request/api/post'
import { useUserStore } from '@/store/user'
import { formatCommentTime } from '@/utils/time'
@@ -17,6 +20,14 @@ const emits = defineEmits<{
(e: 'update:modelValue', val: PostComment[]): void
}>()
// 路由
const router = useRouter()
// 公司用户信息
const showUserProfile = ref(false)
const userProfileId = ref<number | null>(null)
// 列表信息
const commentList = ref<PostComment[]>([])
const userStore = useUserStore()
@@ -200,17 +211,36 @@ function handleDeleteSelfComment(data: PostComment) {
// 用户取消删除
})
}
// 显示用户信息
function handleShowUserProfile(id: number) {
userProfileId.value = id
showUserProfile.value = true
}
// 点击发起聊天
async function handleStartChat() {
const friendId = userProfileId.value
if (friendId === null) {
return
}
userProfileId.value = null
showUserProfile.value = false
router.push(`/im/private/${friendId}`)
}
</script>
<template>
<div v-for="item in commentList" :key="item.id" class="custom-comment">
<div class="custom-comment-left">
<div class="custom-comment-left" @click="handleShowUserProfile(item.publishUser.id)">
<SafeImage :url="item.publishUser.avatar" :size="32" round />
</div>
<div class="custom-comment-right">
<div class="custom-comment-meta">
<div class="custom-comment-title">
<div class="custom-comment-author">
<div class="custom-comment-author" @click="handleShowUserProfile(item.publishUser.id)">
{{ item.publishUser.name }}
</div>
<!-- <div v-if="item.publishUser.companyId === postDetail.companyId" class="custom-comment-author-tag">
@@ -247,13 +277,13 @@ function handleDeleteSelfComment(data: PostComment) {
<!-- 子评论 -->
<div v-if="item.children && item.children.length > 0" class="custom-comment-children">
<div v-for="child in item.children" :key="child.id" class="custom-comment">
<div class="custom-comment-left">
<div class="custom-comment-left" @click="handleShowUserProfile(child.publishUser.id)">
<SafeImage :url="item.publishUser.avatar" :size="32" round />
</div>
<div class="custom-comment-right">
<div class="custom-comment-meta">
<div class="custom-comment-title">
<div class="custom-comment-author">
<div class="custom-comment-author" @click="handleShowUserProfile(child.publishUser.id)">
{{ child.publishUser.name }}
</div>
<!-- <div v-if="child.publishUser.companyId === postDetail.companyId" class="custom-comment-author-tag">
@@ -263,7 +293,7 @@ function handleDeleteSelfComment(data: PostComment) {
<div style="margin: 0px 8px;">
回复
</div>
<div class="custom-comment-author">
<div class="custom-comment-author" @click="handleShowUserProfile(child.replyUser.id)">
{{ child.replyUser.name }}
</div>
</template>
@@ -299,6 +329,8 @@ function handleDeleteSelfComment(data: PostComment) {
</div>
</div>
</div>
<CompanyUserProfileDialog v-model="showUserProfile" :user-id="userProfileId" @start-chat="handleStartChat" />
</template>
<style lang="scss" scoped>
@@ -317,6 +349,7 @@ function handleDeleteSelfComment(data: PostComment) {
.custom-comment-left {
display: flex;
flex-direction: column;
cursor: pointer;
}
.custom-comment-right {
display: flex;
@@ -45,7 +45,18 @@ const postDetail = ref<PublicPostDetail>({
const postComments = ref<PostComment[]>([])
const anchorMenu = ref<AnchorMenuItem[]>([])
const anchorMenu = ref<AnchorMenuItem[]>([
{
label: '目录',
href: 'anchro',
children: [],
},
{
label: '评论',
href: 'comment',
children: [],
},
])
const rootComment = ref<string>('')
const rootCommentLoading = ref<boolean>(false)
@@ -69,7 +80,6 @@ onMounted(async () => {
throw new Error(postRes.message || '加载帖子失败,请稍后再试')
}
postDetail.value = postRes.data
console.log(postDetail.value)
// 获取公司信息
const companyRes = await getPostCompanyById(postDetail.value.companyId)
@@ -78,7 +88,6 @@ onMounted(async () => {
}
postCompany.value = companyRes.data
console.log(postCompany.value)
// 获取评论信息
const commentRes = await listPostCommentApi(postDetail.value.id)
@@ -99,7 +108,7 @@ onMounted(async () => {
})
function handleUpdateEditorDirectory(menus: AnchorMenuItem[]) {
anchorMenu.value = anchorMenu.value.concat(menus)
anchorMenu.value[0].children = anchorMenu.value[0].children.concat(menus)
}
function handleSubmitRootComment(content: string) {