更新ContactModal.vue组件,添加NRadio和NVirtualList支持,优化联系人选择逻辑,调整搜索过滤器,改进用户界面和交互体验。

This commit is contained in:
Phoenix 2025-05-15 18:32:34 +08:00
parent fad84e5bf3
commit 94cf0f9f63
2 changed files with 141 additions and 188 deletions

2
components.d.ts vendored
View File

@ -59,7 +59,9 @@ declare module 'vue' {
NoticeTab: typeof import('./src/components/group/manage/NoticeTab.vue')['default']
NotificationApi: typeof import('./src/components/common/NotificationApi.vue')['default']
NProgress: typeof import('naive-ui')['NProgress']
NRadio: typeof import('naive-ui')['NRadio']
NTag: typeof import('naive-ui')['NTag']
NVirtualList: typeof import('naive-ui')['NVirtualList']
RevokeMessage: typeof import('./src/components/talk/message/RevokeMessage.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@ -1,12 +1,12 @@
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { NModal, NInput, NScrollbar, NCheckbox, NTabs, NTab } from 'naive-ui'
import { ref, computed, onMounted, watch } from 'vue'
import { NModal, NInput, NScrollbar, NCheckbox, NTabs, NTab, NButton, NIcon, NImage, NRadio, NVirtualList, NEmpty } from 'naive-ui'
import { Search, Delete } from '@icon-park/vue-next'
import { ServeGetContacts } from '@/api/contact'
import { ServeGetGroups } from '@/api/group'
import XNModal from '@/components/x-naive-ui/x-n-modal/index.vue'
const emit = defineEmits(['close', 'on-submit'])
import { CloseCircle } from '@vicons/ionicons5'
interface Item {
id: number
type: number
@ -17,16 +17,16 @@ interface Item {
keyword: string
}
const tabsIndex = ref<number>(1)
const isShowBox = ref(true)
const loading = ref(true)
const items = ref<Item[]>([])
const keywords = ref('')
const loadGroupStatus = ref(false)
//
const searchFilter = computed(() => {
return items.value.filter((item: Item) => {
return tabsIndex.value == item.type && item.keyword.match(keywords.value) != null
return item.keyword.toLowerCase().includes(keywords.value.toLowerCase())
})
})
@ -40,6 +40,7 @@ const isCanSubmit = computed(() => {
const onLoad = () => {
onLoadContact()
onLoadGroup()
}
const onLoadContact = () => {
@ -55,7 +56,7 @@ const onLoadContact = () => {
avatar: item.avatar,
type: 1,
name: item.remark || item.nickname,
keyword: item.remark + item.nickname,
keyword: (item.remark || '') + item.nickname,
remark: item.remark,
checked: false
}
@ -75,6 +76,7 @@ const onLoadGroup = async () => {
loading.value = true
let { code, data } = await ServeGetGroups()
if (code != 200) {
loading.value = false
return
}
@ -101,6 +103,13 @@ const onMaskClick = () => {
}
const onTriggerContact = (item: any) => {
//
if (selectType.value === 1) {
items.value.forEach(contact => {
contact.checked = false
})
}
let data = items.value.find((val: any) => val.id === item.id)
if (data) {
@ -108,6 +117,18 @@ const onTriggerContact = (item: any) => {
}
}
const onRemoveContact = (item: any) => {
let data = items.value.find((val: any) => val.id === item.id)
if (data) {
data.checked = false
}
}
const onCancel = () => {
isShowBox.value = false
}
const onSubmit = () => {
let data = checkedFilter.value.map((item: any) => {
return {
@ -119,136 +140,113 @@ const onSubmit = () => {
emit('on-submit', data)
}
const onTabs = (value: number) => {
tabsIndex.value = value
if (value == 2) {
onLoadGroup()
}
// 1 2
const selectType = ref(1)
const changeSelectType = () => {
selectType.value = selectType.value == 1 ? 2 : 1
//
items.value.forEach(item => {
item.checked = false
})
}
onLoad()
</script>
<template>
<x-n-modal
v-model:show="isShowBox"
title="选择联系人"
class="modal-radius"
style="width: 997px; height: 740px;background-color: #F9F9FD"
:on-after-leave="onMaskClick"
>
<section class="el-container launch-box">
<aside class="el-aside bdr-r" style="width: 240px">
<section class="el-container is-vertical height100">
<header class="el-header tabs">
<n-tabs type="line" justify-content="space-around" @update:value="onTabs">
<n-tab name="1"> 好友 </n-tab>
<n-tab name="2"> 群聊 </n-tab>
<!-- <n-tab name="企业"> 企业 </n-tab> -->
</n-tabs>
</header>
<header class="el-header sub-header">
<n-input placeholder="搜索" v-model:value="keywords" clearable size="small">
<x-n-modal v-model:show="isShowBox" title="合并转发/逐条转发" style="width: 997px; height: 740px;background-color: #F9F9FD"
:on-after-leave="onMaskClick" content-style="display: flex; justify-content: center; align-items: center;">
<div class="w-927px h-627px bg-#fff rounded-3px px-35px py-20px">
<div class="flex items-center justify-between mb-28px">
<div class="text-#333639">搜索</div>
<div class="w-779px h-34px">
<n-input v-model:value="keywords" type="text" clearable placeholder="请输入">
<template #prefix>
<n-icon :component="Search" />
</template>
</n-input>
</header>
<main class="el-main" v-loading="loading" loading-text="加载中...">
<n-scrollbar>
<div class="friend-items">
<div
class="friend-item pointer"
v-for="item in searchFilter"
:key="item.id"
@click="onTriggerContact(item)"
>
<div class="avatar">
<im-avatar
class="pointer"
:src="item.avatar"
:size="25"
:username="item.remark || item.name"
/>
</div>
<div class="content">
<span class="text-ellipsis">{{ item.remark || item.name }}</span>
</div>
<div class="checkbox">
<n-checkbox size="small" :checked="item.checked" />
<div class="flex justify-between">
<div class="w-260px h-517px rounded-4px border-1px border-solid border-#E5E5E5 px-12px">
<div class="border-b-2px border-b-solid border-b-#FBFBFB h-35px flex items-center justify-end">
<n-button text color="#46299D" class="text-14px" @click="changeSelectType">
{{ selectType === 1 ? '多选' : '单选' }}
</n-button>
</div>
<div>
<n-virtual-list v-if="!loading" style="max-height: 470px" :item-size="65" :items="searchFilter">
<template #default="{ item }">
<div class="flex items-center border-b-2px border-b-solid h-65px border-b-#FBFBFB"
@click="onTriggerContact(item)">
<div class="mr-22px">
<n-radio v-if="selectType === 1" :checked="item.checked" />
<n-checkbox v-else :checked="item.checked" />
</div>
<div class="mr-10px">
<n-image class="w-42px h-42px rounded-full" :src="item.avatar" />
</div>
<div class="flex items-center">
<span class="text-ellipsis">{{ item.name }}</span>
<span v-if="item.type == 2" class="badge group ml-2"></span>
</div>
</div>
</template>
</n-virtual-list>
<div v-else class="flex-center h-470px">
<span>加载中...</span>
</div>
</div>
</div>
</n-scrollbar>
</main>
</section>
</aside>
<main class="el-main">
<section class="el-container is-vertical height100">
<main class="el-main o-hidden">
<n-scrollbar class="friend-items">
<div class="friend-items">
<div v-show="!checkedFilter.length" style="padding-top: 100px">
<n-empty size="200" description="暂无数据">
<div class="w-578px h-517px rounded-4px border-1px border-solid border-#E5E5E5 px-12px">
<div class="border-b-2px border-b-solid border-b-#FBFBFB h-35px flex items-center text-14px text-#000">
发送给
</div>
<div class="h-350px border-b-2px border-b-solid border-b-#FBFBFB">
<div v-if="checkedFilter.length > 0">
<n-virtual-list style="max-height: 350px" :item-size="65" :items="checkedFilter">
<template #default="{ item }">
<div class="flex items-center border-b-2px border-b-solid h-65px border-b-#FBFBFB pr-20px">
<div class="mr-10px">
<n-image class="w-42px h-42px rounded-full" :src="item.avatar" />
</div>
<div class="flex items-center">
<span class="text-ellipsis">{{ item.name }}</span>
<span v-if="item.type == 2" class="badge group ml-2"></span>
</div>
<n-button class="ml-auto" text color="#C7C7C9" @click="onRemoveContact(item)">
<n-icon :component="CloseCircle" size="18" />
</n-button>
</div>
</template>
</n-virtual-list>
</div>
<div v-else class="flex-center h-350px">
<n-empty size="medium" description="暂无选择联系人">
<template #icon>
<img src="@/assets/image/no-data.svg" alt="" />
</template>
</n-empty>
</div>
<div
class="friend-item pointer"
v-for="item in checkedFilter"
:key="item.id"
@click="onTriggerContact(item)"
>
<div class="avatar">
<im-avatar
class="pointer"
:src="item.avatar"
:size="25"
:username="item.remark || item.name"
/>
</div>
<div class="content">
<span class="text-ellipsis">
{{ item.remark || item.name }}
<div class="flex flex-col items-center justify-center h-120px">
<div class="text-14px text-#999999 mb-23px">
<span>[{{ selectType === 1 ? '合并转发' : '逐条转发' }}]</span>
<span v-if="checkedFilter.length > 0">
{{ checkedFilter.map(item => item.name).join('、') }}的会话记录
</span>
<span v-if="item.type == 2" class="badge group"></span>
<span v-else>请选择联系人</span>
</div>
<div class="checkbox">
<n-icon :size="16" :component="Delete" />
<div class="flex justify-center items-center">
<n-button color="#C7C7C9" class="w-250px h-34px text-14px text-#fff mr-10px" @click="onCancel">取消</n-button>
<n-button color="#46299D" class="w-250px h-34px text-14px text-#fff"
@click="onSubmit" :disabled="isCanSubmit">发送</n-button>
</div>
</div>
</div>
</n-scrollbar>
</main>
</section>
</main>
</section>
<template #footer>
<div class="footer">
<div>
<span>已选择({{ checkedFilter.length }})</span>
</div>
<div>
<n-button type="tertiary" @click="isShowBox = false"> 取消 </n-button>
<n-button type="primary" class="mt-l15" @click="onSubmit" :disabled="isCanSubmit">
确定
</n-button>
</div>
</div>
</template>
</x-n-modal>
</template>
@ -257,74 +255,27 @@ onLoad()
font-weight: unset;
}
.launch-box {
height: 410px;
width: 100%;
overflow: hidden;
.sub-header {
height: 50px;
padding: 10px 15px;
.flex-center {
display: flex;
justify-content: space-between;
align-items: center;
}
.friend-items {
height: 100%;
overflow-y: auto;
padding: 0 15px;
.friend-item {
height: 40px;
box-sizing: border-box;
display: flex;
flex-direction: row;
margin: 5px 0;
> div {
display: flex;
align-items: center;
justify-content: center;
align-items: center;
}
.avatar {
width: 30px;
justify-content: flex-start;
}
.content {
flex: 1 auto;
padding-left: 8px;
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
font-size: 14px;
font-weight: 400;
justify-content: flex-start;
&:hover {
color: #409eff;
}
}
.checkbox {
flex-shrink: 0;
width: 30px;
justify-content: flex-end;
}
}
}
text-overflow: ellipsis;
max-width: 180px;
}
.badge {
&.group {
color: #3370ff !important;
background-color: #e1eaff !important;
font-size: 12px;
padding: 0 5px;
border-radius: 4px;
}
margin: 0 3px;
}
.footer {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>