This commit is contained in:
oneao committed 2026-01-03 22:02:11 +08:00
1 parent c26d1b3c81
commit fac3206497
24 files changed
+364 -284

No files matched your search

@@ -7,9 +7,9 @@ import cn.link56.company.model.dto.DeleteCompanyUserDTO;
import cn.link56.company.model.dto.QueryCompanyUserDTO;
import cn.link56.company.model.dto.UpdateCompanyUserDTO;
import cn.link56.company.model.vo.ChatUserVO;
import cn.link56.company.model.vo.CompanyUserProfileVO;
import cn.link56.company.model.vo.QueryCompanyUserVO;
import cn.link56.company.service.CompanyUserService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -51,4 +51,10 @@ public class CompanyUserController {
List<ChatUserVO> data = companyUserService.listCompanyChatUsers(companyId);
return Result.success(data);
}
@GetMapping("/profile/{userId}")
public Result<CompanyUserProfileVO> getCompanyUserProfile(@PathVariable("userId") Long userId){
CompanyUserProfileVO companyUserProfileVO =companyUserService.getCompanyUserProfile(userId);
return Result.success(companyUserProfileVO);
}
}
@@ -4,10 +4,20 @@ import cn.link56.company.model.middle.RecommendCompanyRow;
import cn.link56.company.model.po.Company;
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;
@Mapper
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,40 @@
package cn.link56.company.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CompanyUserProfileVO {
private Company company;
private User user;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Company{
private Long id;
private String name;
private String logo;
private String profile;
private List<String> areas;
private List<String> industries;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class User{
private Long id;
private String name;
private String avatar;
private String position;
private String email;
private String phone;
private Boolean isOnline;
}
}
@@ -13,6 +13,7 @@ import cn.link56.company.model.dto.UpdateCompanyUserDTO;
import cn.link56.company.model.po.Company;
import cn.link56.company.model.po.CompanyUser;
import cn.link56.company.model.vo.ChatUserVO;
import cn.link56.company.model.vo.CompanyUserProfileVO;
import cn.link56.company.model.vo.QueryCompanyUserVO;
import cn.link56.im.storage.SessionManager;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -27,6 +28,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@@ -327,4 +329,43 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
return result;
}
public CompanyUserProfileVO getCompanyUserProfile(Long userId) {
CompanyUser companyUserPO = companyUserMapper.selectById(userId);
if (companyUserPO == null) {
return null;
}
CompanyUserProfileVO cardVO = new CompanyUserProfileVO();
// ===== 用户(联系人)=====
CompanyUserProfileVO.User userVO = new CompanyUserProfileVO.User();
BeanUtils.copyProperties(companyUserPO, userVO);
userVO.setIsOnline(SessionManager.isOnline(userId));
// ===== 公司(主体)=====
Company companyPO = companyMapper.selectCompanyByUserId(userId);
CompanyUserProfileVO.Company companyVO = new CompanyUserProfileVO.Company();
BeanUtils.copyProperties(companyPO, companyVO);
if (companyPO.getIndustry() != null) {
companyVO.setIndustries(
Arrays.stream(companyPO.getIndustry().split(","))
.map(String::trim)
.collect(Collectors.toList())
);
}
if (companyPO.getArea() != null) {
companyVO.setAreas(
Arrays.stream(companyPO.getArea().split(","))
.map(String::trim)
.collect(Collectors.toList())
);
}
cardVO.setUser(userVO);
cardVO.setCompany(companyVO);
return cardVO;
}
}
@@ -8,6 +8,7 @@ import cn.link56.post.model.vo.QueryPublicPostVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@@ -47,4 +48,7 @@ public interface PostMapper extends BaseMapper<Post> {
@Param("title") String title,
@Param("tag") String tag
);
@Update("UPDATE b_post SET b_active_time = GETDATE() WHERE b_id = #{postId}")
int updateActiveTime(@Param("postId") Long postId);
}
@@ -59,4 +59,9 @@ public class Post {
fill = FieldFill.INSERT_UPDATE
)
private Date updateTime;
@TableField(
value = "b_active_time"
)
private Date activeTime;
}
@@ -2,6 +2,7 @@ package cn.link56.post.service;
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
import cn.link56.post.mapper.PostCommentMapper;
import cn.link56.post.mapper.PostMapper;
import cn.link56.post.model.dto.CreatePostCommentDTO;
import cn.link56.post.model.po.PostComment;
import cn.link56.post.model.po.PostView;
@@ -30,6 +31,9 @@ public class PostCommentService extends ServiceImpl<PostCommentMapper, PostComme
@Resource
private PostStatService postStatService;
@Resource
private PostMapper postMapper;
/**
* 根据文章 id 获取浏览总数
*/
@@ -134,6 +138,8 @@ public class PostCommentService extends ServiceImpl<PostCommentMapper, PostComme
CreatePostCommentVO commentVO = new CreatePostCommentVO();
BeanUtils.copyProperties(postComment,commentVO);
// 更新活跃数量
postMapper.updateActiveTime(postComment.getPostId());
return commentVO;
}
@@ -13,6 +13,7 @@ import cn.link56.post.model.po.Post;
import cn.link56.post.model.po.PostComment;
import cn.link56.post.model.vo.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
@@ -47,6 +48,7 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
post.setId(IdGenerator.nextId());
post.setCompanyId(UserContext.getCompanyId());
post.setTag(joinTags(dto.getTags()));
post.setActiveTime(new Date());
handleUpdatePublishTime(post, dto.getStatus());
@@ -71,6 +73,10 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
handleUpdatePublishTime(post, dto.getStatus());
postMapper.updateById(post);
// 更新活跃时间
postMapper.updateActiveTime(post.getId());
return buildEditPostVO(post);
}
@@ -250,7 +256,6 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
List<QueryPublicPostVO> queryEditorPostVOS = postMapper.selectPublicPostPage(offset, limit, title, tag);
Long total = postMapper.selectPublicPostTotal(title, tag);
return new BasePageVO<QueryPublicPostVO>(total, queryEditorPostVOS);
}
@@ -275,14 +280,6 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
postVO.setViewCount(viewTotal);
postVO.setCommentCount(commentTotal);
PostComment latestCommentByPostId = postCommentService.getLatestCommentByPostId(id);
if (latestCommentByPostId == null){
postVO.setActiveTime(post.getUpdateTime());
} else {
postVO.setActiveTime(latestCommentByPostId.getCreateTime());
}
return postVO;
}
@@ -300,4 +297,5 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
return postCompanyVO;
}
}
@@ -163,41 +163,28 @@
t.commentCount,
t.activeTime
FROM (
SELECT
SELECT
p.b_id AS id,
p.b_title AS title,
p.b_tag AS tagsString,
p.b_company_id AS companyId,
bc.b_name AS companyName,
ISNULL(pv.viewCount, 0) AS viewCount,
ISNULL(pc.commentCount, 0) AS commentCount,
ISNULL(ps.b_view_count, 0) AS viewCount,
ISNULL(ps.b_comment_count, 0) AS commentCount,
-- 活跃时间:最后评论时间优先
ISNULL(pc.lastCommentTime, p.b_update_time) AS activeTime,
p.b_active_time AS activeTime,
ROW_NUMBER() OVER (
ORDER BY ISNULL(pc.lastCommentTime, p.b_update_time) DESC
) AS rn
ORDER BY p.b_update_time DESC
) AS rn
FROM b_post p
LEFT JOIN b_company bc ON bc.b_id = p.b_company_id
LEFT JOIN (
SELECT
b_post_id,
COUNT(1) AS viewCount
FROM b_post_view
GROUP BY b_post_id
) pv ON pv.b_post_id = p.b_id
LEFT JOIN (
SELECT
b_post_id,
COUNT(1) AS commentCount,
MAX(b_create_time) AS lastCommentTime
FROM b_post_comment
GROUP BY b_post_id
) pc ON pc.b_post_id = p.b_id
LEFT JOIN b_company bc
ON bc.b_id = p.b_company_id
LEFT JOIN b_post_stat ps
ON ps.b_post_id = p.b_id
<where>
AND p.b_status = 1
p.b_status = 1
<if test="title != null and title != ''">
AND p.b_title LIKE '%' + #{title} + '%'
</if>
@@ -205,10 +192,11 @@
AND p.b_tag LIKE '%' + #{tag} + '%'
</if>
</where>
) t
) t
WHERE t.rn BETWEEN #{offset} AND #{limit}
</select>
<select id="selectPublicPostTotal" resultType="java.lang.Long">
SELECT COUNT(1)
FROM b_post p
+2 -2
View File
@@ -1,2 +1,2 @@
VITE_SERVER_BASEURL = 'http://192.168.1.137:8084'
VITE_WEBSOCKET_BASEURL = 'ws://192.168.1.137:8090/ws'
VITE_SERVER_BASEURL = 'http://127.0.0.1:8084'
VITE_WEBSOCKET_BASEURL = 'ws://127.0.0.1:8090/ws'
@@ -1,10 +1,9 @@
<script lang="ts" setup>
import type { CompanyChatUser } from '@/types/company/user'
import { Search } from '@element-plus/icons-vue'
import type { ChatCompanyUser } from '@/types/company/user'
import { ElMessage } from 'element-plus'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { listCompanyChatUsersApi } from '@/request/api/company/user'
import { listChatCompanyUsersApi } from '@/request/api/company/user'
import { createFriendApi } from '@/request/api/im'
import { useUserStore } from '@/store/user'
@@ -19,12 +18,12 @@ const router = useRouter()
const userStore = useUserStore()
const loading = ref(true)
const users = ref<CompanyChatUser[]>([])
const users = ref<ChatCompanyUser[]>([])
function onPopoverShow() {
loading.value = true
listCompanyChatUsersApi(props.companyId).then((res) => {
listChatCompanyUsersApi(props.companyId).then((res) => {
if (res.isSuccess) {
users.value = res.data
}
@@ -44,7 +43,7 @@ function onPopoverHide() {
users.value = []
}
function selectUser(user: CompanyChatUser) {
function selectUser(user: ChatCompanyUser) {
console.log('选择用户:', user)
if (userStore.user.id === user.id) {
ElMessage.warning('无法与自己发起聊天')
@@ -0,0 +1,36 @@
<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>
@@ -67,9 +67,7 @@ function isActive(path: string) {
<el-dropdown>
<div class="user">
<el-badge v-if="websocket.connected.value" :value="12" is-dot>
<SafeImage :url="userStore.user.avatar || userStore.company.logo" :size="36" round />
</el-badge>
<SafeImage :url="userStore.user.avatar || userStore.company.logo" :size="36" round />
<div class="info">
<span class="name">{{ userStore.user.name }}</span>
<span class="company">{{ userStore.company.name }}</span>
@@ -1,4 +1,4 @@
import type { AddCompanyUserParams, CompanyChatUser, CompanyUser, DeleteCompanyUserParams, QueryCompanyUserParams, UpdateCompanyUserParams } from '@/types/company/user'
import type { AddCompanyUserParams, ChatCompanyUser, CompanyUser, DeleteCompanyUserParams, QueryCompanyUserParams, UpdateCompanyUserParams } from '@/types/company/user'
import { httpGet, httpPostJson } from '@/request/utils'
export function queryCompanyUserApi(params: QueryCompanyUserParams) {
@@ -14,6 +14,6 @@ export function deleteCompanyUserApi(params: DeleteCompanyUserParams) {
return httpPostJson<void>('/company/user/delete', params)
}
export function listCompanyChatUsersApi(companyId: number) {
return httpGet<CompanyChatUser[]>(`/company/user/chat/${companyId}`)
export function listChatCompanyUsersApi(companyId: number) {
return httpGet<ChatCompanyUser[]>(`/company/user/chat/${companyId}`)
}
+4 -1
View File
@@ -13,8 +13,11 @@ declare module 'vue' {
export interface GlobalComponents {
AnchorMenu: typeof import('./../components/anchor-menu/index.vue')['default']
AreaSelect: typeof import('./../components/AreaSelect.vue')['default']
ChatCompanyUserSelect: typeof import('../components/ChatUserSelect.vue')['default']
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']
CompanyUserPopover: typeof import('./../components/CompanyUserPopover.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']
@@ -58,10 +58,30 @@ export interface DeleteCompanyUserParams {
ids: number[]
}
export interface CompanyChatUser {
export interface ChatCompanyUser {
id: number
name: string
position: string
avatar: string
isOnline: boolean
}
export interface CompanyUserProfile {
company: {
id: number
name: string
logo: string
profile: string
areas: string[]
industries: string[]
}
user: {
id: number
name: string
position: string
avatar: string
email: string
phone: string
isOnline: boolean
}
}
@@ -54,11 +54,10 @@ export interface Post {
tags: string[]
status: number
updateTime: string
publishTime: string
viewCount: number
likeCount: number
favoriteCount: number
commentCount: number
activeTime: string
}
@@ -193,7 +193,7 @@ function handleToIm(user: CompanyDetailUser) {
<div class="anchor-title">
导航
</div>
<el-anchor :offset="70">
<el-anchor :offset="60">
<AnchorMenu :menu="anchorMenu" />
</el-anchor>
</el-affix>
@@ -14,7 +14,10 @@ import { generateUUID } from '@/utils/uuid'
const props = defineProps({
group: { type: Object as PropType<ImGroup>, required: true },
})
const emits = defineEmits<{
(e: 'avatarClick', id: number): void
(e: 'userClick', groupUser: ImGroupUser): void
}>()
// websocket
const websocket = useWebSocket()
@@ -270,6 +273,14 @@ async function sendMessage() {
await nextTick()
scrollToLatestMessage()
}
function handleClickAvatar(userId: number) {
emits('avatarClick', userId)
}
function handleClickGroupUser(groupUser: ImGroupUser) {
emits('userClick', groupUser)
}
</script>
<template>
@@ -293,7 +304,7 @@ async function sendMessage() {
? 'im__chat-message--from'
: 'im__chat-message--to'"
>
<div class="im__chat-message-avatar">
<div class="im__chat-message-avatar" @click="handleClickAvatar(item.senderId)">
<SafeImage :url="item.senderAvatar" :size="40" round />
</div>
<div class="im__chat-message-body">
@@ -363,7 +374,7 @@ async function sendMessage() {
</div>
<div class="im__group-section-body">
<div v-for="item in groupUsers" :key="item.userId" class="im__group-item">
<div v-for="item in groupUsers" :key="item.userId" class="im__group-item" @click="handleClickGroupUser(item)">
<el-badge
is-dot
:color="item.isOnline ? '#24e68a' : '#bfbfbf'"
@@ -15,6 +15,10 @@ const props = defineProps({
friend: { type: Object as PropType<ImFriend>, required: true },
})
const emits = defineEmits<{
(e: 'avatarClick', id: number): void
}>()
// websocket
const websocket = useWebSocket()
@@ -253,6 +257,10 @@ async function sendMessage() {
await nextTick()
scrollToLatestMessage()
}
function handleClickAvatar(userId: number) {
emits('avatarClick', userId)
}
</script>
<template>
@@ -276,8 +284,8 @@ async function sendMessage() {
? 'im__chat-message--from'
: 'im__chat-message--to'"
>
<div class="im__chat-message-avatar">
<SafeImage :url=" userStore.user.id === item.senderId ? userStore.user.avatar : props.friend.friendAvatar" :size="40" round />
<div class="im__chat-message-avatar" @click="handleClickAvatar(item.senderId)">
<SafeImage :url="userStore.user.id === item.senderId ? userStore.user.avatar : props.friend.friendAvatar" :size="40" round />
</div>
<div class="im__chat-message-body">
<div class="im__chat-message-name">
@@ -162,7 +162,7 @@ function getTotalCommentCount(list: PostComment[]): number {
<template v-else>
<div class="post-left">
<el-affix :offset="90">
<el-anchor>
<el-anchor :offset="60">
<AnchorMenu :menu="anchorMenu" />
</el-anchor>
</el-affix>
@@ -32,11 +32,11 @@ const menuList: NavMenuItem [] = [
index: '/settings/creator',
icon: 'hugeicons:edit-01',
children: [
{
label: '首页',
index: '/settings/creator/home',
icon: 'hugeicons:home-03',
},
// {
// label: '首页',
// index: '/settings/creator/home',
// icon: 'hugeicons:home-03',
// },
{
label: '写文章',
index: '/settings/creator/editor',
+125 -217
View File
@@ -1,246 +1,154 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>私聊界面示例</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
height: 100vh;
display: flex;
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC";
background: #f0f2f5;
}
/* 左侧聊天列表 */
.chat-list {
width: 280px;
background-color: #fff;
border-right: 1px solid #ddd;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.mask {
position: fixed;
inset: 0;
background: rgba(0,0,0,.45);
display: flex;
align-items: center;
justify-content: center;
}
.chat-list .user-card {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s;
border-bottom: 1px solid #f0f0f0;
}
.modal {
width: 480px;
background: #fff;
border-radius: 8px;
box-shadow: 0 12px 32px rgba(0,0,0,.18);
}
.chat-list .user-card:hover,
.chat-list .user-card.active {
background-color: #f0f8ff;
}
/* 用户区 */
.header {
display: flex;
gap: 14px;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
}
.chat-list .avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
object-fit: cover;
border: 2px solid #4caf50;
/* 默认在线状态绿色 */
}
.avatar {
width: 52px;
height: 52px;
border-radius: 50%;
background: #ddd;
}
.chat-list .user-info {
flex: 1;
}
.user-main {
flex: 1;
}
.chat-list .user-name {
margin: 0;
font-size: 14px;
font-weight: bold;
}
.user-line {
font-size: 16px;
font-weight: 600;
color: #262626;
}
.chat-list .user-position {
margin: 2px 0 0 0;
font-size: 12px;
color: #666;
}
.user-line .role {
font-weight: 400;
color: #595959;
}
.chat-list .status {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #4caf50;
}
.status {
font-size: 12px;
color: #52c41a;
margin-left: 6px;
}
.chat-list .offline {
background-color: #ccc;
}
.contact-line {
margin-top: 6px;
font-size: 13px;
color: #595959;
display: flex;
gap: 16px;
}
/* 右侧聊天详情 */
.chat-detail {
flex: 1;
display: flex;
flex-direction: column;
background-color: #f9f9f9;
}
.action {
margin-top: 10px;
}
.chat-header {
display: flex;
align-items: center;
padding: 12px 16px;
background-color: #fff;
border-bottom: 1px solid #ddd;
}
.btn {
padding: 6px 14px;
border-radius: 6px;
border: none;
background: #1677ff;
color: #fff;
font-size: 13px;
cursor: pointer;
}
.chat-header .avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 12px;
border: 2px solid #4caf50;
}
/* 公司区 */
.company {
padding: 14px 20px 18px;
}
.chat-header .info {
flex: 1;
}
.company-name {
font-size: 14px;
font-weight: 500;
color: #262626;
}
.chat-header .info .name {
font-size: 16px;
font-weight: bold;
margin: 0;
}
.company-meta {
margin-top: 4px;
font-size: 12px;
color: #8c8c8c;
}
.chat-header .info .position {
font-size: 12px;
color: #666;
margin: 2px 0;
}
.desc {
margin-top: 8px;
font-size: 13px;
color: #595959;
line-height: 1.6;
.chat-header .info .company-info {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
}
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</head>
.chat-header .info .company-logo {
width: 16px;
height: 16px;
margin-right: 4px;
border-radius: 2px;
object-fit: cover;
}
<body>
<div class="mask">
<div class="modal">
.messages {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.message {
margin-bottom: 12px;
max-width: 70%;
padding: 8px 12px;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.message.self {
background-color: #dcf8c6;
margin-left: auto;
}
.message.other {
background-color: #fff;
margin-right: auto;
}
.chat-input {
padding: 12px 16px;
border-top: 1px solid #ddd;
background-color: #fff;
display: flex;
gap: 8px;
}
.chat-input input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 20px;
outline: none;
}
.chat-input button {
padding: 8px 16px;
border: none;
background-color: #4caf50;
color: #fff;
border-radius: 20px;
cursor: pointer;
}
</style>
</head>
<body>
<!-- 左侧聊天列表 -->
<div class="chat-list">
<div class="user-card active">
<img class="avatar" src="https://i.pravatar.cc/40?img=1" alt="头像" />
<div class="user-info">
<p class="user-name">张三</p>
<p class="user-position">前端工程师</p>
<!-- 用户 -->
<div class="header">
<div class="avatar"></div>
<div class="user-main">
<div class="user-line">
张三 · <span class="role">产品经理</span>
<span class="status">● 在线</span>
</div>
<div class="status"></div>
</div>
<div class="user-card">
<img class="avatar" src="https://i.pravatar.cc/40?img=2" alt="头像" />
<div class="user-info">
<p class="user-name">李四</p>
<p class="user-position">产品经理</p>
<div class="contact-line">
<span>📧 zhangsan@company.com</span>
<span>📞 138****8888</span>
</div>
<div class="status offline"></div>
</div>
<div class="user-card">
<img class="avatar" src="https://i.pravatar.cc/40?img=3" alt="头像" />
<div class="user-info">
<p class="user-name">王五</p>
<p class="user-position">设计师</p>
<div class="action">
<button class="btn">发起聊天</button>
</div>
<div class="status"></div>
</div>
</div>
<!-- 右侧聊天详情 -->
<div class="chat-detail">
<!-- 聊天头部 -->
<div class="chat-header">
<img class="avatar" src="https://i.pravatar.cc/50?img=1" alt="头像" />
<div class="info">
<p class="name">张三</p>
<p class="position">前端工程师</p>
<div class="company-info">
<img class="company-logo" src="https://via.placeholder.com/16" alt="公司logo" />
<span>ABC科技公司</span>
</div>
</div>
</div>
<!-- 消息区域 -->
<div class="messages">
<div class="message other">你好!这是张三的消息。</div>
<div class="message self">你好,我是自己发的消息。</div>
<div class="message other">我们下午开会吗?</div>
<div class="message self">好的,下午三点。</div>
</div>
<!-- 输入框 -->
<div class="chat-input">
<input type="text" placeholder="输入消息..." />
<button>发送</button>
<!-- 公司 -->
<div class="company">
<div class="company-name">北京某某科技有限公司</div>
<div class="company-meta">互联网 / SaaS · 北京</div>
<div class="desc">
专注于为中大型企业提供数字化解决方案,覆盖供应链、协同办公与数据分析等多个领域,
服务客户超过 3000 家。
</div>
</div>
</body>
</div>
</div>
</body>
</html>
@@ -41,7 +41,7 @@ export default defineConfig((configEnv) => {
],
server: {
host: '0.0.0.0',
port: 8089,
port: 8080,
open: true,
// 用于配置开发服务器启动时预加载的一些客户端文件,以提高初次加载的速度。
warmup: {