新增:IM 弹框

feature/im
安浩浩 2024-04-24 20:54:13 +08:00
parent ffc81621d7
commit de27cfa8f6
23 changed files with 1012 additions and 3 deletions

View File

@ -93,7 +93,7 @@
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": false,
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.sourceLanguage": "zh-CN",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"],
"cSpell.words": [

View File

@ -39,6 +39,7 @@
"benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.10.0",
"camunda-bpmn-moddle": "^7.0.1",
"components": "link:@/components",
"cropperjs": "^1.6.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
@ -83,8 +84,8 @@
"@types/qs": "^6.9.12",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@unocss/transformer-variant-group": "^0.58.5",
"@unocss/eslint-config": "^0.57.4",
"@unocss/transformer-variant-group": "^0.58.5",
"@vitejs/plugin-legacy": "^5.3.1",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View File

@ -0,0 +1,173 @@
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
//value
const inputValue = ref('')
//
const isShowResultContent = ref(false)
//
const querySearch = () => {
console.log('>>>>>>>>触发搜索')
}
</script>
<template>
<div class="search_box" ref="searchBox">
<div>
<el-input
v-model.trim="inputValue"
placeholder="搜索"
@focus="isShowResultContent = true"
@clear="isShowResultContent = false"
@input="querySearch"
:prefix-icon="Search"
clearable
/>
</div>
</div>
</template>
<style scoped lang="scss">
::v-deep .el-input__wrapper {
box-shadow: none;
}
.search_box {
width: 100%;
height: 60px;
background: #f8f8f8;
padding: 14px 20px;
box-sizing: border-box;
}
.resultContent {
position: absolute;
top: 58px;
left: 0;
width: 100%;
height: calc(100% - 60px);
background-color: #ededed;
z-index: 888;
overflow-y: auto;
.search_history {
.search_history_item {
width: 100%;
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 17px;
letter-spacing: 0.669643px;
color: #000000;
li {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
padding: 0 15px;
background: #fff;
margin: 1px 0;
height: 32px;
transition: all 0.5s;
cursor: pointer;
&:hover {
background: #e5e5e5;
}
}
}
}
.title {
height: 32px;
line-height: 32px;
padding: 0 15px;
background: #f2f2f2;
font-weight: 400;
font-size: 12px;
letter-spacing: 0.342857px;
color: #333333;
}
.search_history_title {
display: flex;
flex-direction: row;
justify-content: space-between;
.clear_search_history:hover {
color: #00a0fb;
cursor: pointer;
}
}
.search_result_item {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
height: 66px;
background: #fff;
// padding: 0 14px;
cursor: pointer;
.item_left {
padding: 0;
margin-right: 11px;
margin-left: 14px;
}
.item_main {
// width: 25%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-around;
height: 40px;
font-family: 'PingFang SC';
font-style: normal;
font-weight: 500;
font-size: 14px;
.name {
max-width: 100px;
height: 17px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.last_msg_body {
max-width: 100px;
height: 17px;
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 17px;
letter-spacing: 0.3px;
color: #a3a3a3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.time {
position: absolute;
right: 15px;
top: 13px;
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 10px;
line-height: 14px;
letter-spacing: 0.25px;
color: #a3a3a3;
}
}
}
</style>

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import tang from '@/assets/imgs/welcome/Group 78@3x.png'
import maskGroup from '@/assets/imgs/welcome/Mask_group2.png'
</script>
<template>
<div class="app_contanier">
<div class="welcome_box">
<img class="tang" :src="tang" alt="" />
<h1 class="welcome_box_title">欢迎体验 芋道 即时通讯</h1>
<p class="welcome_box_text"
>包含单聊群聊添加好友创建群组等功能更多其他功能等你发现快去试试吧
</p>
</div>
<img class="maskGroup" :src="maskGroup" alt="" />
</div>
</template>
<style scoped>
.app_contanier {
position: absolute;
right: 0;
bottom: 0;
z-index: -1;
width: 100%;
height: 100%;
background-size: 100% 100%;
border-radius: 0 5px 5px 0;
overflow: hidden;
}
.welcome_box {
position: relative;
margin-top: 140px;
margin-left: 100px;
width: 637px;
height: 400px;
}
.tang {
position: absolute;
left: -30px;
top: 0;
width: 32px;
height: 32px;
}
.welcome_box_title {
font-family: 'PingFang SC', serif;
font-style: normal;
font-weight: 500;
font-size: 24px;
line-height: 34px;
letter-spacing: 1px;
color: #333333;
}
.welcome_box_text {
margin-top: 12px;
font-family: 'PingFang SC', serif;
font-style: normal;
font-weight: 300;
font-size: 16px;
line-height: 22px;
letter-spacing: 1.5px;
color: #a3a3a3;
}
.maskGroup {
position: absolute;
right: 0;
top: 25%;
}
</style>

View File

@ -0,0 +1,3 @@
import Chat from './src/Chat.vue'
export { Chat }

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { Dialog } from '@/components/Dialog'
import { ref } from 'vue'
import IM from '@/views/im/index.vue'
import { useRouter } from 'vue-router' // useRouter
defineOptions({ name: 'Chat' })
const dialogVisible = ref(false)
const router = useRouter() // router
//
function handleClick() {
dialogVisible.value = !dialogVisible.value
router.push('/im/conversation') // 设置路由为 /im/conversation
}
</script>
<template>
<div class="custom-hover" v-bind="$attrs">
<ElBadge>
<Icon :size="18" class="cursor-pointer" icon="ep:chat-round" @click="handleClick" />
</ElBadge>
</div>
<Dialog v-model="dialogVisible" width="90%" top="10vh">
<IM />
</Dialog>
</template>

View File

@ -1,6 +1,7 @@
<script lang="tsx">
import { defineComponent, computed } from 'vue'
import { Message } from '@/layout/components//Message'
import { Chat } from '@/layout/components/Chat'
import { Collapse } from '@/layout/components/Collapse'
import { UserInfo } from '@/layout/components/UserInfo'
import { Screenfull } from '@/layout/components/Screenfull'
@ -78,6 +79,7 @@ export default defineComponent({
{message.value ? (
<Message class="custom-hover" color="var(--top-header-text-color)"></Message>
) : undefined}
<Chat class="custom-hover" color="var(--top-header-text-color)"></Chat>
<UserInfo></UserInfo>
</div>
</div>

View File

@ -573,6 +573,63 @@ const remainingRouter: AppRouteRecordRaw[] = [
component: () => import('@/views/crm/product/detail/index.vue')
}
]
},
{
path: '/im',
component: Layout,
name: 'IM',
meta: { hidden: true },
children: [
{
path: 'conversation',
name: 'Conversation',
meta: {
title: '会话',
noCache: true,
hidden: true,
noTagsView: true
},
component: () => import('@/views/im/Conversation/index.vue'),
children: [
{
// 会话详情
path: 'informdetails',
name: 'InformDetails',
meta: {
title: '通知详情',
noCache: true,
hidden: true,
noTagsView: true
},
component: () => import('@/views/im/InformDetails/index.vue')
},
{
//聊天对话框
path: 'message',
name: 'Message',
meta: {
title: '聊天对话框',
noCache: true,
hidden: true,
noTagsView: true
},
component: () => import('@/views/im/Message/index.vue')
}
]
},
{
path: 'contacts',
name: 'Contacts',
meta: {
title: '联系人',
noCache: true,
hidden: true,
noTagsView: true
},
component: () => import('@/views/im/Contacts/index.vue'),
children: []
}
]
}
]

View File

@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template></template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,263 @@
<script setup lang="ts">
import { formatDate } from '@/utils/formatTime'
import { reactive } from 'vue'
//(使)
const friendList = reactive([
{
friendKey: {
avatarurl: '',
nickName: ''
}
}
])
//
const conversationList = reactive({
conversationKey: {
conversationInfo: {
avatarUrl: '',
name: '',
conversationType: 0
},
latestMessage: {
msg: ''
},
latestSendTime: 0,
unreadMessageNum: 0,
isMention: false
}
})
//name
const handleConversationName = computed(() => {
return ''
})
//lastmsgfrom
const handleLastMsgNickName = computed(() => {
return ''
})
//
const checkedConverItemIndex = ref(null)
const toChatMessage = (item, itemKey, index) => {
checkedConverItemIndex.value = index
}
//
const deleteConversation = (itemKey) => {}
</script>
<template>
<!-- 普通会话 -->
<template v-if="Object.keys(conversationList).length > 0">
<li
v-for="(item, itemKey, index) in conversationList"
:key="itemKey"
@click="toChatMessage(item, itemKey, index)"
:style="{
background: checkedConverItemIndex === index ? '#E5E5E5' : ''
}"
>
<el-popover
popper-class="conversation_popover"
placement="right-end"
trigger="contextmenu"
:show-arrow="false"
:offset="-10"
>
<template #reference>
<div class="session_list_item">
<div class="item_body item_left">
<div class="session_other_avatar">
<el-avatar
:size="34"
:src="
friendList[item.conversationKey] && friendList[item.conversationKey].avatarurl
? friendList[item.conversationKey].avatarurl
: item.conversationInfo.avatarUrl
"
/>
</div>
</div>
<div class="item_body item_main">
<div class="name"> 好友 </div>
<div class="last_msg_body">
<span class="last_msg_body_mention" v-if="item.isMention">[@]</span>
<span v-show="item.conversationType === 2"></span>
{{ item.latestMessage.msg }}
</div>
</div>
<div class="item_body item_right">
<span class="time">{{ formatDate(item.latestSendTime, 'MM/DD/HH:mm') }}</span>
<span class="unReadNum_box" v-if="item.unreadMessageNum >= 1">
<sup
class="unReadNum_count"
v-text="item.unreadMessageNum >= 99 ? '99+' : item.unreadMessageNum"
></sup>
</span>
</div>
</div>
</template>
<template #default>
<div class="session_list_delete" @click="deleteConversation(itemKey)"> </div>
</template>
</el-popover>
</li>
</template>
<template v-else>
<el-empty description="暂无最近会话" />
</template>
</template>
<style scoped lang="scss">
.session_list {
position: relative;
height: 100%;
padding: 0;
margin: 0;
}
.offline_hint {
width: 100%;
height: 30px;
text-align: center;
line-height: 30px;
color: #f35f81;
background: #fce7e8;
font-size: 7px;
.plaint_icon {
display: inline-block;
width: 15px;
height: 15px;
color: #e5e5e5;
text-align: center;
line-height: 15px;
font-size: 7px;
font-weight: bold;
background: #e6686e;
border-radius: 50%;
}
}
.session_list .session_list_item {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 66px;
background: #f0f0f0;
color: var(--el-color-primary);
border-bottom: 1px solid var(--el-border-color);
cursor: pointer;
&:hover {
background: #e5e5e5;
}
.item_body {
display: flex;
height: 100%;
}
.item_left {
flex-direction: row;
align-items: center;
justify-content: center;
margin-left: 14px;
margin-right: 10px;
}
.item_main {
width: 225px;
max-width: 225px;
height: 34px;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
.name {
min-width: 56px;
max-width: 180px;
height: 17px;
font-weight: 400;
font-size: 14px;
/* identical to box height */
color: #333333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.last_msg_body {
max-width: 185px;
height: 17px;
font-weight: 400;
font-size: 12px;
line-height: 17px;
letter-spacing: 0.3px;
color: #a3a3a3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.last_msg_body_mention {
font-size: 12px;
line-height: 17px;
font-weight: bold;
color: red;
}
}
.item_right {
width: 25%;
height: 34px;
flex-direction: column;
align-items: flex-end;
margin-right: 10px;
.time {
font-size: 10px;
font-weight: 400;
font-size: 10px;
line-height: 14px;
letter-spacing: 0.25px;
color: #a3a3a3;
}
.unReadNum_box {
margin-top: 10px;
vertical-align: middle;
.unReadNum_count {
display: inline-block;
min-width: 20px;
height: 20px;
padding: 0 6px;
color: #fff;
font-weight: normal;
font-size: 12px;
line-height: 20px;
white-space: nowrap;
text-align: center;
background: #f5222d;
border-radius: 10px;
box-sizing: border-box;
}
}
}
}
.session_list_item_active {
background: #d2d2d2;
}
.session_list .session_list_item + .list_item {
margin-top: 10px;
}
.session_list_delete {
cursor: pointer;
transition: all 0.5s;
&:hover {
background: #e1e1e1;
}
}
</style>

View File

@ -0,0 +1,42 @@
<script setup lang="ts">
/* 搜索框组件 */
import SearchInput from '@/components/SearchInput/index.vue'
/* 欢迎页 */
import Welcome from '@/components/Welcome/index.vue'
import ConversationList from '../Conversation/components/ConversationList.vue'
</script>
<template>
<el-container style="height: 100%">
<el-aside class="chat_conversation_box">
<!-- 搜索组件 -->
<SearchInput :searchType="'conversation'" />
<div class="chat_conversation_list">
<ConversationList />
</div>
</el-aside>
<el-main class="chat_conversation_main_box">
<router-view />
<Welcome />
</el-main>
</el-container>
</template>
<style lang="scss" scoped>
.chat_conversation_box {
position: relative;
background: #cfdbf171;
overflow: hidden;
min-width: 324px;
.chat_conversation_list {
height: calc(100% - 60px);
}
}
.chat_conversation_main_box {
position: relative;
width: 100%;
height: 100%;
padding: 0;
}
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,287 @@
<script setup lang="ts">
/* route */
import { useRoute } from 'vue-router'
/* 取用户头像 */
import router from '@/router'
import highlightConversation from '@/assets/imgs/tabbar/highlightconversation.png'
import grayConversation from '@/assets/imgs/tabbar/grayconversation.png'
import highlightContacts from '@/assets/imgs/tabbar/higtlightcontacts.png'
import grayContacts from '@/assets/imgs/tabbar/graycontacts.png'
import avatarImg from '@/assets/imgs/avatar.gif'
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
/* tabular icon 路由跳转 */
const skipRouterName = ref('conversation')
const changeSkipRouterName = (routerName: string) => {
router.push(`/im/${routerName}`)
}
const route = useRoute()
//
watch(
() => route.path,
(newPath) => {
console.log('>>>>>newPath', newPath)
if (newPath.includes('/im/conversation')) {
skipRouterName.value = 'conversation'
}
if (newPath.includes('/im/contacts')) {
console.log('>>>>>存在赋值为联系人样式')
skipRouterName.value = 'contacts'
}
}
)
</script>
<template>
<!-- 头像 -->
<div class="chat_avatar">
<ElAvatar :src="avatar" alt="" class="w-[calc(var(--logo-height)-25px)] rounded-[50%]" />
</div>
<!-- 去往会话 -->
<div class="chat_conversation chat_icon_box" @click="changeSkipRouterName('conversation')">
<div class="img_box">
<img
:src="skipRouterName === 'conversation' ? highlightConversation : grayConversation"
alt=""
/>
</div>
</div>
<!-- 去往联系人 -->
<div class="chat_contacts chat_icon_box" @click="changeSkipRouterName('contacts')">
<img
class="chat_contacts_icon"
:src="skipRouterName === 'contacts' ? highlightContacts : grayContacts"
alt=""
/>
</div>
</template>
<style lang="scss" scoped>
.chat_avatar {
margin-top: 43px;
position: relative;
width: 44px;
height: 44px;
transition: all 0.3s;
&:hover {
transform: scale(1.3);
}
span {
display: inline-block;
width: 100%;
height: 100%;
}
.online_status {
position: absolute;
right: 2px;
bottom: 2px;
display: inline-block;
width: 6px;
height: 6px;
border: 2px solid #fff;
background: #fff;
border-radius: 50%;
transition: all 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.2);
}
}
}
.chat_icon_box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 67px;
text-align: center;
line-height: 67px;
margin: 4px 0;
}
.chat_conversation {
.img_box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
img {
display: inline-block;
width: 27px;
height: 27px;
transition: all 0.5s;
&:hover {
transform: scale(1.3);
}
}
.badge {
position: absolute;
right: 0;
top: 8px;
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: red;
}
}
}
.chat_contacts {
img {
display: inline-block;
width: 27px;
height: 27px;
transition: all 0.5s;
&:hover {
transform: scale(1.3);
}
}
}
.chat_settings {
position: absolute;
bottom: 92px;
font-size: 30px;
color: #8e8e8e;
cursor: pointer;
transition: all 0.5s;
&:hover {
color: #1b83f9;
transform: scale(1.3);
}
.chat_setting_item {
width: 100%;
height: 30px;
}
}
.more_settings {
position: absolute;
bottom: 46px;
color: #8e8e8e;
cursor: pointer;
transition: all 0.5s;
&:hover {
transform: scale(1.3);
}
}
.setting_fun_list {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.func_item {
display: flex;
flex-direction: row;
align-items: center;
// justify-content: space-around;
width: 101px;
height: 40px;
border-radius: 3px;
&:hover {
background-color: #f2f2f2;
}
.settting_fun_icon {
display: flex;
align-items: center;
justify-content: center;
margin-left: 5px;
img {
width: 20px;
height: 20px;
}
}
.setting_fun_text {
display: inline-block;
text-align: center;
margin-left: 12px;
height: 20px;
width: 58px;
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.4px;
color: #333333;
cursor: pointer;
}
.apply_groups {
display: flex;
flex-direction: column;
}
}
}
.settting_fun_icon {
font-size: 20px;
}
.line {
display: inline-block;
width: 69px;
height: 1px;
border: 1px solid rgba(0, 0, 0, 0.0462467);
}
.components {
::v-deep .edit_userinfo_diglog {
border-radius: 4px;
overflow: hidden;
}
::v-deep .setting_func_diglog > .el-dialog__body {
padding: 28px 24px 24px 24px;
}
::v-deep .setting_func_diglog > .el-dialog__header {
background: #f2f2f2;
margin: 0;
}
::v-deep .edit_userinfo_diglog > .el-dialog__header {
padding: 0;
margin-right: 0;
}
::v-deep .edit_userinfo_diglog > .el-dialog__body {
padding: 0;
border-radius: 4px;
}
::v-deep .login_diglog > .el-dialog__header {
background: #f2f2f2;
margin: 0;
}
::v-deep .personal_setting_card > .el-dialog__header {
background: #f2f2f2;
margin: 0;
}
}
</style>

53
src/views/im/index.vue Normal file
View File

@ -0,0 +1,53 @@
<script lang="ts" setup>
import NavBar from './NavBar/index.vue'
defineOptions({ name: 'IM' })
</script>
<template>
<div class="app-container">
<el-container class="chat_container">
<el-aside class="chat_nav_bar" width="72px">
<NavBar />
</el-aside>
<el-main class="chat_main_box">
<router-view />
</el-main>
</el-container>
</div>
</template>
<style lang="scss" scoped>
.app-container {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background-size: cover;
backdrop-filter: blur(5px);
.chat_container {
width: 85%;
height: 95%;
background: #fff;
position: relative;
top: 50%;
transform: translateY(-50%);
margin: auto auto;
border-radius: 5px;
.chat_nav_bar {
display: flex;
flex-direction: column;
align-items: center;
border-radius: 5px 0 0 5px;
width: 80px;
background: #262626;
overflow: hidden;
}
.chat_main_box {
padding: 0;
}
}
}
</style>

View File

@ -1,4 +1,4 @@
import { resolve } from 'path'
import { resolve } from 'path'
import { loadEnv } from 'vite'
import type { UserConfig, ConfigEnv } from 'vite'
import { createVitePlugins } from './build/vite'