Files
lilishop-ui/im/src/views/message/index.vue
pikachu1995@126.com 97e611c072 refactor(im): 重构聊天面板UI组件并修复多处交互问题
- 将头像组件从 dicebear 外链改为本地首字+底色生成,新增 avatarColor 工具函数
- 重构订单卡片(footPrint.vue)布局,优化商品与订单信息展示结构
- 重构店铺详情卡片(storeDetail.vue),统一信息行与评分行样式
- 移除消息气泡中的昵称时间行,调整气泡背景色与头像内边距
- 修复 face 变量名与全局组件 <face> 冲突导致头像URL被当作标签渲染
- 将 unload 事件监听替换为 pagehide,兼容 Chrome 弃用策略
- 修复搜索关键词未转义正则元字符导致输入特殊字符时抛异常
- 修复 WebSocket onMessage 对无 text 字段的推送消息误调用 JSON.parse
- 将 setup 中同步调用全局 mixin 方法移至 onMounted,避免代理未就绪
- 商品/订单图片统一添加 onerror 默认图兜底
- 聊天列表消息预览支持 emoji 图片渲染(textReplaceEmoji)
- 搜索框增加 clearable 属性及"没有找到相关好友"空态提示
- 锁定 element-plus 版本为 2.14.1 防止自动升级引入兼容问题
- 日期格式化分隔符由斜杠改为横杠
- 提现设置页新增提现规则说明字段,满足微信审核公示要求
2026-10-29 10:56:28 +08:00

1097 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="wrapper">
<MainLayout :idx="0" ref="mainLayout">
<template #container>
<el-container class="full-height">
<!-- 左侧侧边栏 -->
<el-aside width="320px" class="aside-box">
<el-container class="full-height" direction="vertical">
<!-- 搜索栏 -->
<el-header height="60px" class="header">
<div class="user-login">
<div class="user-box">
<face :text="userFace" :name="name" class="user-face"></face>
</div>
</div>
<p class="user-status">
<span v-if="name">{{ name }}</span>
<span v-else>用户名称</span>
</p>
</el-header>
<el-header height="60px" class="header">
<div class="from-search">
<el-input v-model="input" :prefix-icon="$legacyIcon('el-icon-search')" placeholder="搜索好友" size="small" clearable />
</div>
</el-header>
<!-- <el-header height="118px" class="logo-header">
</el-header> -->
<!-- 置顶栏 -->
<el-header v-show="loadStatus == 1 && topItems.length > 0" class="subheader"
:class="{ shadow: subHeaderShadow }" :height="subHeaderPx">
<div v-for="item in topItems" :key="item.index_name" class="top-item" @click="clickTab(item.index_name)"
@contextmenu.prevent="topItemsMenu(item, $event)">
<el-tooltip effect="dark" placement="top-start" :content="item.name ? item.name : item.name">
<div class="avatar">
<span v-show="!item.face">
{{ (item.name ? item.name : item.name).substr(0, 1) }}
</span>
<img v-show="item.face" :src="item.face" :onerror="defaultAvatar" v-if="item.face" />
<face-null :text="item.name" v-else></face-null>
</div>
</el-tooltip>
<div class="name" :class="{ active: index_name === item.id }">
{{ item.name ? item.name : item.name }}
</div>
</div>
</el-header>
<!-- 对话列表栏 -->
<el-scrollbar tag="section" ref="menusScrollbar" class="full-height" :native="false" @scroll="onMenusScroll">
<el-main class="main">
<p v-show="loadStatus === 0 && userTalkItem.length === 0" class="empty-data">
<legacy-el-icon name="el-icon-loading" /> 数据加载中...
</p>
<p v-show="loadStatus === 1 && talkNum === 0" class="empty-data">
暂无聊天消息
</p>
<p v-show="loadStatus === 1 && talkNum > 0 && userTalkItem.length === 0" class="empty-data">
没有找到相关好友
</p>
<p v-show="loadStatus === 1 && talkNum > 0" class="main-menu">
<span class="title">对话记录({{ talkNum }})</span>
<!-- <el-tag size="small" type="danger" v-if="myUnreadNum"
>未读:{{ myUnreadNum }}</el-tag
> -->
</p>
<!-- 对话列表 -->
<RecycleScroller
:item-size="64"
:items="userTalkItem"
:prerender="10"
v-slot="{ item, index }"
>
<div v-bind:key="item.id" class="talk-item pointer" :class="{ active: activeIndex == index }" @click="clickTab(item.userId, item, index)">
<div class="avatar-box">
<face :text="item.face" v-if="item.face"></face>
<face-null :text="item.name" v-else></face-null>
<div v-show="item.is_top == 0" class="top-mask" @click.stop="topChatItem(item)">
<legacy-el-icon name="el-icon-top" />
</div>
</div>
<div class="card-box">
<div class="title">
<div class="card-name">
<p class="nickname">
{{ item.remark_name ? item.remark_name : item.name }}
</p>
<div v-show="item.unread" class="larkc-tag">
{{ item.unread }}条未读
</div>
<div v-show="item.is_top" class="larkc-tag top">
TOP
</div>
<div v-show="item.is_robot" class="larkc-tag top">
BOT
</div>
<div v-show="item.talk_type == 2" class="larkc-tag group">
群组
</div>
<div v-show="item.is_disturb" class="larkc-tag disturb">
<i class="iconfont icon-xiaoximiandarao" />
</div>
</div>
<div class="card-time">
{{ beautifyTime(item.lastTalkTime) }}
</div>
</div>
<div class="talk-message">
<span v-if="item.lastMessageType === 'MESSAGE'" v-html="textReplaceEmoji(item.lastTalkMessage)"></span>
<span v-if="item.lastMessageType === 'GOODS'">[商品链接]</span>
<span v-if="item.lastMessageType === 'ORDER'">[订单链接]</span>
</div>
<div class="content">
<template v-if="index_name != item.index_name && item.draft_text">
<span class="draft-color">[草稿]</span>
<span>{{ item.draft_text }}</span>
</template>
<template v-else>
<template v-if="item.is_robot == 0">
<span v-if="item.talk_type == 1" :class="{ 'online-color': item.is_online == 1 }">
[{{ item.is_online == 1 ? "在线" : "离线" }}]
</span>
<span v-else>[群消息]</span>
</template>
<span>{{ item.msg_text }}</span>
</template>
</div>
</div>
</div>
</RecycleScroller>
</el-main>
</el-scrollbar>
</el-container>
</el-aside>
<!-- 聊天面板容器 -->
<el-main class="main-box ov-hidden full-height no-padding flex">
<WelcomeModule class="flex-8" v-if="index_name == null" />
<TalkPanel v-else class="full-height flex-8" :params="params" :goodsParams="goodsParams"
:is-online="isFriendOnline" @change-talk="changeTalk" @close-talk="closeTalk" />
<!-- <OtherLink :toUser="toUser" :goodsParams="goodsParams" :id="id" class="flex-4"/> -->
</el-main>
</el-container>
</template>
</MainLayout>
<!-- 用户查询组件 -->
<UserSearch ref="searchUsers" />
</div>
</template>
<script setup>
import {
computed,
getCurrentInstance,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch,
} from "vue";
import { onBeforeRouteUpdate, useRoute } from "vue-router";
import MainLayout from "@/views/layout/MainLayout";
import WelcomeModule from "@/components/layout/WelcomeModule";
import TalkPanel from "@/components/chat/panel/TalkPanel";
import UserSearch from "@/components/user/UserSearch";
import OtherLink from "@/components/chat/panel/OtherLink.vue";
import {
ServeGetTalkList,
ServeDeleteTalkList,
ServeTopTalkList,
ServeSetNotDisturb,
ServeCreateTalkList,
} from "@/api/chat";
import { ServeDeleteContact, ServeEditContactRemark } from "@/api/contacts";
import { beautifyTime } from "@/utils/functions";
import { textReplaceEmoji } from "@/utils/emojis";
import { formatTalkItem, findTalkIndex, getCacheIndexName } from "@/utils/talk";
import { setToken } from "@/utils/auth";
import { syncImTokenFromRoute } from "@/utils/im-mode";
import { ElMessageBox, ElNotification } from "@/utils/message";
import { useAppStore } from "@/stores/app";
import { useUserStore } from "@/stores/user";
import { useTalkStore } from "@/stores/talk";
import { useNotifyStore } from "@/stores/notify";
import { useDialogueStore } from "@/stores/dialogue";
const title = document.title;
defineOptions({ name: "MessagePage" });
const route = useRoute();
// 全局属性($user/$contextmenu)与全局 mixin 方法(initialize/loadUserSetting)经实例代理访问
const instance = getCurrentInstance();
const { proxy } = instance;
const appStore = useAppStore();
const userStore = useUserStore();
const talkStore = useTalkStore();
const notifyStore = useNotifyStore();
const dialogueStore = useDialogueStore();
const searchUsers = ref(null);
const activeIndex = ref(9999999); //默认样式索引
const subHeaderShadow = ref(false);
const launchGroupShow = ref(false);
// 对话面板的传递参数
const params = ref({
talk_type: 0,
receiver_id: 0,
nickname: "",
clickFlag: true,
});
const goodsParams = ref({
goodsId: '',
skuId: '',
});
// 查询关键词
const input = ref("");
// header 工具菜单
const subMenu = ref(false);
// 对话消息列表加载状态[0:加载中;1:加载完成;2:加载失败;]
// 注意:不能命名为 loadStatus——全局 main-mixin 的 loadStoreChatList 会以
// Options API 方式(this.loadStatus = ...)写入它,而 <script setup> 的同名绑定
// 会被 Vue 公共代理拦截(dev 下直接拒绝写入)。故改用内部 ref,
// 并以 loadStatus 为名桥接到 ctx,供模板(_ctx.loadStatus)与 mixin 读写。
const loadStatusRef = ref(0);
// 消息未读数计时器
let interval = null;
const userTalkItem = ref([]);
// 原 vuex mapGetters(["topItems", "talkItems", "unreadNum", "talkNum"])
const topItems = computed(() => talkStore.topItems);
const talkItems = computed(() => talkStore.talkItems);
const unreadNum = computed(() => talkStore.unreadNum);
const talkNum = computed(() => talkStore.talkNum);
// 原 vuex mapState
// 注意:不能命名为 face——会与全局注册的 <face> 组件绑定冲突,
// 模板里的 <face> 标签会被解析成这个 computed 的值(头像 base64 字符串)当作标签名渲染
const userFace = computed(() => userStore.face);
const name = computed(() => userStore.name);
const talks = computed(() => talkStore.items);
const id = computed(() => userStore.id);
const index_name = computed(() => dialogueStore.index_name);
const monitorFriendsStatus = computed(() => notifyStore.friendStatus);
const myUnreadNum = computed(() => notifyStore.unreadNum);
const toUser = computed(() => userStore.toUser);
// 原模板中的 $store.state.defaultAvatar(根级 state)
const defaultAvatar = computed(() => appStore.defaultAvatar);
// 计算置顶栏目的高度
const subHeaderPx = computed(() => {
const n = 7; // 一排能显示的用户数
const num = topItems.value.length;
let len = 60;
if (num > n) {
len = (Math.floor(num / n) + (num % n > 0 ? 1 : 0)) * len;
}
return `${len}px`;
});
// 当前对话好友在线状态
const isFriendOnline = computed(() => {
let index = findTalkIndex(index_name.value);
return index >= 0 && talks.value[index].is_online == 1;
});
// 全局 main-mixin 的方法(loadUserSetting/loadStoreChatList)以组件公共实例代理为
// this 回调本组件:createTalk/loadChatList/clickTab、读 goodsParams/talkNum、
// 读写 loadStatus。<script setup> 绑定对公共代理不可见,故桥接到 instance.ctx;
// 同时 defineExpose 供外部通过 ref 访问。
defineExpose({
createTalk,
loadChatList,
clickTab,
goodsParams,
talkNum,
loadStatus: loadStatusRef,
});
Object.defineProperties(instance.ctx, {
createTalk: { configurable: true, value: createTalk },
loadChatList: { configurable: true, value: loadChatList },
clickTab: { configurable: true, value: clickTab },
goodsParams: { configurable: true, get: () => goodsParams.value },
talkNum: { configurable: true, get: () => talkNum.value },
loadStatus: {
configurable: true,
get: () => loadStatusRef.value,
set: (v) => {
loadStatusRef.value = v;
},
},
});
// 初始化商品参数
function initGoodsParams(query) {
goodsParams.value = {
goodsId: query.goodsId || '',
skuId: query.skuId || '',
};
console.log('初始化商品参数:', goodsParams.value);
}
//创建会话
async function createTalk(id) {
await ServeCreateTalkList(id);
await loadChatList();
}
// header 功能栏隐藏事件
function closeSubMenu() {
subMenu.value = false;
}
// 清除当前对话
function clearTalk() {
params.value = {
talk_type: 0,
receiver_id: 0,
nickname: "",
clickFlag: true
};
dialogueStore.UPDATE_DIALOGUE_MESSAGE({
talk_type: 0,
receiver_id: 0,
is_robot: 0,
});
}
// 工具栏事件
function triggerSubMenu(type) {
closeSubMenu();
if (type == 1) {
launchGroupShow.value = true;
} else {
searchUsers.value.open();
}
}
// 左侧会话列表滚动时,控制置顶栏阴影
function onMenusScroll({ scrollTop }) {
subHeaderShadow.value = scrollTop > 0;
}
// 获取用户对话列表
function loadChatList() {
loadStatusRef.value = talkNum.value == 0 ? 0 : 1;
return ServeGetTalkList()
.then(({ code, result }) => {
if (code !== 200 || !Array.isArray(result)) return false;
notifyStore.SET_UNREAD_NUM(0);
talkStore.SET_TALK_ITEMS({
items: result.map((item) => formatTalkItem(item)),
});
if (route.query.id) {
const targetId = route.query.id;
const takeIndex = talkItems.value.findIndex(
(take) => take.userId == targetId
);
const takeData = takeIndex >= 0 ? talkItems.value[takeIndex] : null;
if (takeData) {
clickTab(targetId, takeData, takeIndex);
}
}
})
.catch(() => {})
.finally(() => {
loadStatusRef.value = 1;
});
}
// 切换聊天栏目
function clickTab(id, val, index) {
if (!id) return;
activeIndex.value = index;
let item =
talks.value.find((item) => {
return item.userId == id;
}) || val || {};
// 点击当前栏目存储当前用户的信息
userStore.toUser = val || item;
let nickname = item.name;
const talkId = item.id || val?.id;
if (!talkId) return;
params.value = {
talk_type: 1,
receiver_id: item.userId || id,
nickname,
is_robot: item.is_robot,
talkId,
clickFlag: true
};
// 更新信息
dialogueStore.UPDATE_DIALOGUE_MESSAGE({
talk_type: 1,
receiver_id: item.userId,
is_robot: item.is_robot,
});
nextTick(() => {
if (id == index_name.value.split("_")[1]) {
talkStore.UPDATE_TALK_ITEM({
userId: id,
index_name: "MESSAGE_" + index_name.value.split("_")[1],
unread: 0,
});
}
});
}
// 修改当前对话
function changeTalk(index_name) {
console.log("修改当前对话", index_name);
sessionStorage.setItem("send_message_index_name", index_name);
loadChatList();
}
// 关闭当前对话及刷新对话列表
function closeTalk() {
dialogueStore.UPDATE_DIALOGUE_MESSAGE({
talk_type: 0,
receiver_id: 0,
is_robot: 0,
});
loadChatList();
}
// 对话列表的右键自定义菜单
function talkItemsMenu(item, event) {
let items = {
items: [
{
label: "好友信息",
icon: "el-icon-user",
disabled: item.talk_type == 2 || item.is_robot == 1,
onClick: () => {
proxy.$user(item.receiver_id);
},
},
{
label: "修改备注",
icon: "el-icon-edit-outline",
disabled: item.talk_type == 2 || item.is_robot == 1,
onClick: () => {
editFriendRemarks(item);
},
},
{
label: item.is_top == 0 ? "会话置顶" : "取消置顶",
icon: "el-icon-top",
onClick: () => {
topChatItem(item);
},
},
{
label: item.is_disturb == 0 ? "消息免打扰" : "开启消息提示",
icon:
item.is_disturb == 0
? "el-icon-close-notification"
: "el-icon-bell",
disabled: item.is_robot == 1,
onClick: () => {
setNotDisturb(item);
},
},
{
label: "移除会话",
icon: "el-icon-remove-outline",
divided: true,
onClick: () => {
delChatItem(item);
},
},
{
label: item.talk_type == 1 ? "删除好友" : "退出群聊",
icon: "el-icon-delete",
disabled: item.is_robot == 1,
onClick: () => {
let title = item.talk_type == 1 ? "删除好友" : "退出群聊";
ElMessageBox.confirm(
`此操作将 <span style="color:red;font-size:16px;">${title}</span>, 是否继续?`,
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
center: true,
dangerouslyUseHTMLString: true,
}
).then(() => {
if (item.talk_type == 1) {
removeFriend(item);
}
});
},
},
],
event: event,
zIndex: 3,
};
proxy.$contextmenu(items);
return false;
}
// 置顶栏右键菜单栏
function topItemsMenu(item, event) {
proxy.$contextmenu({
items: [
{
label: item.is_top == 0 ? "会话置顶" : "取消置顶",
icon: "el-icon-top",
onClick: () => {
topChatItem(item);
},
},
],
event: event,
zIndex: 3,
});
return false;
}
// 会话列表置顶
function topChatItem(item) {
ServeTopTalkList({
list_id: item.id,
type: item.is_top == 0 ? 1 : 2,
}).then(({ code }) => {
if (code == 200) {
talkStore.UPDATE_TALK_ITEM({
index_name: item.index_name,
is_top: item.is_top == 0 ? 1 : 0,
});
}
});
}
// 设置消息免打扰
function setNotDisturb(item) {
ServeSetNotDisturb({
talk_type: item.talk_type,
receiver_id: item.receiver_id,
is_disturb: item.is_disturb == 0 ? 1 : 0,
}).then(({ code }) => {
if (code == 200) {
talkStore.UPDATE_TALK_ITEM({
index_name: item.index_name,
is_disturb: item.is_disturb == 0 ? 1 : 0,
});
}
});
}
// 移除会话列表
function delChatItem(item) {
ServeDeleteTalkList({
list_id: item.id,
}).then(({ code }) => {
if (code == 200) {
clearTalk();
talkStore.REMOVE_TALK_ITEM(item.index_name);
}
});
}
// 解除好友关系
function removeFriend(item) {
ServeDeleteContact({
friend_id: item.receiver_id,
}).then(({ code }) => {
if (code == 200) {
if (index_name.value == item.index_name) {
clearTalk();
}
talkStore.REMOVE_TALK_ITEM(item.index_name);
}
});
}
// 修改好友备注信息
function editFriendRemarks(item) {
let title = `您正在设置【${item.name}】好友的备注信息`;
if (item.remark_name) {
title += `,当前备注为【${item.remark_name}】`;
}
ElMessageBox.prompt(title, "修改备注", {
confirmButtonText: "确定",
cancelButtonText: "取消",
customClass: "border-radius0",
inputPlaceholder: "请设置好友备注信息",
inputValue: item.remark_name ? item.remark_name : item.name,
inputValidator (val) {
return val == null || val == "" ? "好友备注不能为空" : true;
},
})
.then(({ value }) => {
if (value == item.remark_name) {
return false;
}
ServeEditContactRemark({
friend_id: item.receiver_id,
remarks: value,
}).then((res) => {
if (res.code == 200) {
talkStore.UPDATE_TALK_ITEM({
index_name: item.index_name,
remark_name: value,
});
ElNotification({
title: "成功",
message: "好友备注修改成功...",
type: "success",
});
} else {
ElNotification({
title: "消息",
message: "好友备注修改失败,请稍后再试...",
type: "warning",
});
}
});
})
.catch(() => { });
}
watch(talkItems, (val) => {
if (val) userTalkItem.value = val;
});
// 搜索用户的时候 根据当前用户表进行模糊搜索
watch(input, (val) => {
if (val) {
// 转义正则元字符,避免输入 [ 等字符时构造 RegExp 抛异常
const keyword = val.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
let str = ["", ...keyword, ""].join(".*");
let reg = new RegExp(str);
userTalkItem.value = talkItems.value.filter((item) =>
reg.test(item.name)
);
} else {
userTalkItem.value = talkItems.value;
}
});
watch(unreadNum, (value) => {
clearInterval(interval);
// console.log("%c 更新未读消息", "color:#32ccbc");
notifyStore.SET_UNREAD_NUM(value);
if (value > 0) {
interval = setInterval(() => {
document.title =
document.title == title ? `【新消息】${title}` : title;
}, 2000);
} else {
document.title = title;
}
});
// 监听好友在线状态
watch(monitorFriendsStatus, (value) => {
talkStore.UPDATE_TALK_ITEM({
index_name: `1_${value.friend_id}`,
is_online: value.status,
});
});
onBeforeRouteUpdate((to, from, next) => {
syncImTokenFromRoute(to, setToken);
let index_name = getCacheIndexName();
if (index_name) clickTab(index_name);
// 更新商品参数
initGoodsParams(to.query);
next();
});
// 原 beforeCreate
syncImTokenFromRoute(route, setToken);
// 原 async created。不能在 setup 同步执行:全局 mixin 的方法(initialize/
// loadUserSetting)在 finishComponentSetup 的 applyOptions 阶段才挂到公共代理上,
// 即 setup() 返回之后。故放到 onMounted 里执行。
onMounted(async () => {
await proxy.initialize();
await proxy.loadUserSetting();
// 初始化商品参数
initGoodsParams(route.query);
/**
* 如果说有id 说明是用户点击 “联系客服” 进入的该页面
* 所以创建会话 并请求用户列表
* 如果没有id说明当前商家登录 直接请求用户列表
*/
});
onBeforeUnmount(() => {
document.title = title;
clearInterval(interval);
clearTalk();
});
</script>
<style lang="less" scoped>
:deep(.el-scrollbar__wrap ) {
overflow-x: hidden;
}
.user-status{
margin-left: 10px;
}
.aside-box {
position: relative;
border-right: 1px solid rgb(245, 245, 245);
overflow: hidden;
padding: 0;
transition: width 0.3s;
.header {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 15px;
.from-search {
flex: 1 1;
flex-shrink: 0;
height: 40px;
:deep(.el-input__wrapper) {
border-radius: 16px;
background-color: #f5f6f7;
box-shadow: none;
transition: box-shadow 0.2s;
&.is-focus {
background-color: #fff;
box-shadow: 0 0 0 1px #409eff inset;
}
}
:deep(.el-input__inner) {
font-size: 12px;
}
}
.tools {
flex-basis: 32px;
flex-shrink: 0;
height: 32px;
margin-bottom: 8px;
margin-left: 15px;
cursor: pointer;
line-height: 32px;
text-align: center;
position: relative;
user-select: none;
.tools-menu {
position: absolute;
right: 0;
top: 38px;
width: 100px;
min-height: 80px;
box-sizing: border-box;
background-color: rgba(31, 35, 41, 0.9);
border-radius: 5px;
z-index: 1;
padding: 3px 0;
.menu-item {
height: 40px;
line-height: 40px;
color: white;
font-size: 14px;
&:hover {
background-color: rgba(70, 72, 73, 0.9);
}
}
}
}
}
.subheader {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 3px 8px 10px;
overflow: hidden;
flex-shrink: 0;
.top-item {
flex-basis: 41px;
flex-shrink: 0;
height: 50px;
margin: 0 1px 6px 1px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
cursor: pointer;
.avatar {
flex-basis: 32px;
width: 32px;
height: 32px;
background-color: #508afe;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 10px;
color: white;
flex-shrink: 0;
overflow: hidden;
user-select: none;
img {
width: 100%;
height: 100%;
background-color: white;
}
}
.name {
font-size: 12px;
text-align: center;
color: #8f959e;
transform: scale(0.84);
text-align: center;
line-height: 20px;
word-break: break-all;
overflow: hidden;
&.active {
color: #508afe;
}
}
}
&.shadow {
box-shadow: 0 2px 6px 0 rgba(31, 35, 41, 0.05);
}
}
}
.aside-box .main {
overflow: hidden;
padding: 0;
.empty-data {
text-align: center;
padding-top: 40px;
color: #ccc;
}
.main-menu {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 3px 10px 3px 10px;
align-items: center;
user-select: none;
.title {
font-size: 12px;
font-weight: 600;
color: #1f2329;
}
.icon {
cursor: pointer;
}
}
.talk-item {
padding: 8px 10px;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
border-left: 3px solid transparent;
transition: 0.2s ease-in;
.avatar-box {
height: 35px;
width: 35px;
flex-shrink: 0;
background-color: #508afe;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: white;
user-select: none;
transition: ease 1s;
position: relative;
overflow: hidden;
img {
width: 100%;
height: 100%;
background-color: white;
border-radius: 3px;
}
.top-mask {
width: 100%;
height: 100%;
background-color: rgba(22, 25, 29, 0.6);
position: absolute;
top: 0;
left: 0;
color: white;
display: none;
align-items: center;
justify-content: center;
font-weight: bold;
}
&:hover .top-mask {
display: flex;
}
}
.card-box {
height: 40px;
display: flex;
align-content: center;
flex-direction: column;
flex: 1 1;
margin-left: 10px;
overflow: hidden;
.title {
width: 100%;
height: 20px;
display: flex;
align-items: center;
.card-name {
color: #1f2329;
font-size: 14px;
line-height: 20px;
flex: 1 1;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
align-items: center;
overflow: hidden;
.nickname {
font-weight: 400;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 3px;
}
.top {
color: #dc9b04 !important;
background-color: #faf1d1 !important;
}
.group {
color: #3370ff !important;
background-color: #e1eaff !important;
font-size: 13px;
}
.disturb {
color: #98999c !important;
background-color: #ecedf1 !important;
i {
font-size: 12px;
}
}
}
}
.card-time {
color: #8f959e;
font-size: 12px;
margin-left: 10px;
user-select: none;
}
.content {
font-size: 13px;
line-height: 18px;
color: #8f959e;
margin-top: 3px;
font-weight: 300;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
span:first-child {
margin-right: 5px;
}
.online-color {
color: #4aa71c;
font-weight: 300;
}
.draft-color {
color: red;
font-weight: 300;
}
}
}
&:hover {
background-color: #eff0f1;
}
&.active {
border-color: #3370ff;
background-color: #eff0f1;
}
}
}
// iphone
@media screen and (max-width: 767px) {
.side-edge,
.from-search,
.card-box {
display: none !important;
}
.aside-box {
width: 66px !important;
.subheader {
display: none;
}
.card-box .larkc-tag {
display: none;
}
}
}
.talk-message {
font-size: 12px;
color: #8f959e;
:deep(img) {
width: 16px;
height: 16px;
vertical-align: middle;
}
}
.user-face {
height: 100%;
width: 100%;
}
</style>