This commit is contained in:
Phoenix 2025-05-22 15:26:49 +08:00
commit c7df773b97
10 changed files with 665 additions and 229 deletions

1
components.d.ts vendored
View File

@ -63,6 +63,7 @@ declare module 'vue' {
NotificationApi: typeof import('./src/components/common/NotificationApi.vue')['default']
NPopover: typeof import('naive-ui')['NPopover']
NRadio: typeof import('naive-ui')['NRadio']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpin: typeof import('naive-ui')['NSpin']
NTag: typeof import('naive-ui')['NTag']
NVirtualList: typeof import('naive-ui')['NVirtualList']

View File

@ -80,7 +80,7 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:show', 'cancel', 'confirm'])
const emit = defineEmits(['update:show', 'cancel', 'confirm', 'customCloseModal'])
const show = computed({
get: () => props.show,
@ -111,7 +111,7 @@ const state = reactive({
const handleCloseModal = () => {
if (props.customCloseEvent) {
emit('closeModal')
emit('customCloseModal')
} else {
show.value = false
}

View File

@ -663,7 +663,10 @@ const handleEditGroupNameConfirm = () => {
清空聊天记录
</n-button>
<n-button
v-if="isAdmin || isLeader"
v-if="
(isAdmin || isLeader) &&
(state.detail.group_type === 1 || state.detail.group_type === 3)
"
class="btn"
type="error"
ghost
@ -671,7 +674,13 @@ const handleEditGroupNameConfirm = () => {
>
解散该群
</n-button>
<n-button class="btn" type="error" ghost @click="showChatSettingOperateModal('quit')">
<n-button
class="btn"
type="error"
ghost
@click="showChatSettingOperateModal('quit')"
v-if="state.detail.group_type === 1 || state.detail.group_type === 3"
>
退出群聊
</n-button>
</div>
@ -731,7 +740,8 @@ const handleEditGroupNameConfirm = () => {
<UserCardModal
v-model:show="state.isShowUserCardModal"
v-model:uid="(state.userInfo as any).erp_user_id"
v-model:uid="(state.userInfo as any).user_id"
:euid="(state.userInfo as any).erp_user_id"
/>
</template>
<style lang="less" scoped>

View File

@ -4,7 +4,7 @@
:class="props?.conditionType ? 'search-item-condition' : ''"
v-if="resultName"
:style="{
'margin': props.searchResultKey === 'talk_record_infos_receiver' ? '12px 0 0' : '',
margin: props.searchResultKey === 'talk_record_infos_receiver' ? '12px 0 0' : '',
'background-color': props.isClickStay ? '#EEE9F8' : ''
}"
>
@ -70,6 +70,9 @@
:text="resultDetail"
:searchText="props.searchText"
/>
<div class="searchRecordDetail-fastLocal" v-if="searchRecordDetail">
<span>定位到聊天位置</span>
</div>
</div>
</div>
<div class="search-item-pointer" v-if="pointerIconSrc">
@ -320,10 +323,24 @@ const resultDetail = computed(() => {
}
}
.info-detail-searchRecordDetail {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
span {
color: #191919;
word-break: break-all;
}
.searchRecordDetail-fastLocal {
display: none;
line-height: 20px;
span {
color: #46299d;
font-size: 12px;
font-weight: 400;
line-height: 17px;
}
}
}
}
.search-item-pointer {
@ -355,5 +372,11 @@ const resultDetail = computed(() => {
}
.search-item:hover {
background-color: #f8f8f8;
.info-detail-searchRecordDetail {
.searchRecordDetail-fastLocal {
display: block;
}
}
}
</style>

View File

@ -1,5 +1,10 @@
<template>
<div class="search-list">
<n-infinite-scroll
:style="{ maxHeight: props.searchResultMaxHeight }"
:distance="47"
@load="doLoadMore"
>
<div class="search-result">
<div class="search-result-list">
<div
@ -15,12 +20,19 @@
searchResultKey !== 'group_infos' &&
searchResultKey !== 'group_member_infos'
"
:style="{ margin: props.useCustomTitle ? '0' : '' }"
>
<div class="result-title">
<!-- <div class="result-title" v-if="!props.useCustomTitle">
<span class="text-[14px] font-regular">
{{ getResultKeysValue(searchResultKey) }}
</span>
</div>
</div> -->
<slot
name="result-title"
:getResultKeysValue="getResultKeysValue"
:searchResultKey="searchResultKey"
:searchResultIndex="searchResultIndex"
></slot>
<div class="result-list">
<div
class="result-list-each"
@ -55,6 +67,7 @@
</div>
</div>
</div>
</n-infinite-scroll>
<!-- <ZPaging
ref="zPaging"
:show-scrollbar="false"
@ -131,6 +144,7 @@
// const zPaging = ref()
// useZPaging(zPaging)
import { NInfiniteScroll } from 'naive-ui'
import searchItem from './searchItem.vue'
import { ref, reactive, defineEmits, defineProps, onMounted, watch } from 'vue'
@ -139,7 +153,7 @@ const emits = defineEmits([
'lastIdChange',
'clickSearchItem',
'clickStayItemChange',
'doLoadMore'
'resultTotalCount'
])
const state = reactive({
@ -148,7 +162,9 @@ const state = reactive({
searchResult: null, //
pageNum: 1, //
uid: 12303, //id
clickStayItem: '' //item
clickStayItem: '', //item
hasMore: true, //
loading: false //
})
const props = defineProps({
@ -190,7 +206,15 @@ const props = defineProps({
useClickStay: {
type: Boolean,
default: false
} //使
}, //使
searchResultMaxHeight: {
type: String,
default: '677px'
}, //
useCustomTitle: {
type: Boolean,
default: false
} //使
})
onMounted(() => {
@ -212,27 +236,20 @@ watch(
watch(
() => props.searchText,
(newVal, oldVal) => {
queryAllSearch()
// state.searchText
state.searchText = newVal
//
state.searchResult = null
//
state.pageNum = 1
//
state.clickStayItem = ''
emits('clickStayItemChange', state.clickStayItem)
}
)
//
const inputSearchText = (e) => {
if (e.trim() != state.searchText.trim()) {
state.pageNum = 1
state.searchResult = null //
//
emits('lastIdChange', 0, 0, 0, '', '')
}
state.searchText = e.trim()
if (!e.trim()) {
state.searchResult = null //
emits('lastIdChange', 0, 0, 0, '', '')
}
// zPaging.value?.reload()
queryAllSearch()
}
)
// ES-
const queryAllSearch = (doClearSearchResult) => {
@ -354,6 +371,12 @@ const queryAllSearch = (doClearSearchResult) => {
total = data.group_record_count
}
}
if (total < props.searchResultPageSize) {
state.hasMore = false
} else {
state.hasMore = true
}
emits('resultTotalCount', total)
// zPaging.value?.completeByTotal([data], total)
} else {
state.searchResult = data
@ -383,6 +406,7 @@ const queryAllSearch = (doClearSearchResult) => {
// zPaging.value?.complete(state.searchResult ? [state.searchResult] : [])
}
})
return resp
}
//
@ -514,13 +538,14 @@ const clickSearchItem = (searchResultKey, searchItem) => {
//
const doLoadMore = (doClearSearchResult) => {
queryAllSearch(doClearSearchResult)
if (!state.hasMore || state.loading) {
return
}
// doLoadMore
defineExpose({
doLoadMore
state.loading = true
queryAllSearch(doClearSearchResult).finally(() => {
state.loading = false
})
}
</script>
<style lang="scss" scoped>
.search-list {
@ -555,7 +580,7 @@ defineExpose({
// padding: 0 10px;
.search-result-part {
margin: 18px 0 0;
// margin: 18px 0 0;
.result-title {
padding: 0 10px 5px;

View File

@ -11,6 +11,17 @@ interface Params {
limit: number
}
interface SpecialParams extends Params {
msg_id?: string
cursor?: number
direction?: 'up' | 'down'
}
interface LoadOptions {
specifiedMsg?: SpecialParams
middleMsgCreatedAt?: string
}
export const useTalkRecord = (uid: number) => {
const dialogueStore = useDialogueStore()
@ -25,9 +36,19 @@ export const useTalkRecord = (uid: number) => {
receiver_id: 0,
talk_type: 0,
status: 0,
cursor: 0
cursor: 0,
specialParams: undefined as SpecialParams | undefined
})
// 重置 loadConfig
const resetLoadConfig = () => {
loadConfig.receiver_id = 0
loadConfig.talk_type = 0
loadConfig.status = 0
loadConfig.cursor = 0
loadConfig.specialParams = undefined
}
const onJumpMessage = (msgid: string) => {
const element = document.getElementById(msgid)
if (!element) {
@ -135,8 +156,160 @@ export const useTalkRecord = (uid: number) => {
})
}
// 获取当前消息的最小 sequence
const getMinSequence = () => {
console.error('records.value', records.value)
if (!records.value.length) return 0
console.error(Math.min(...records.value.map(item => item.sequence)))
return Math.min(...records.value.map(item => item.sequence))
}
// 获取当前消息的最大 sequence
const getMaxSequence = () => {
if (!records.value.length) return 0
return Math.max(...records.value.map(item => item.sequence))
}
/**
*
* @param params
* @param options { specifiedMsg }
*/
const onLoad = (params: Params, options?: LoadOptions) => {
// 如果会话切换,重置所有状态
if (params.talk_type !== loadConfig.talk_type || params.receiver_id !== loadConfig.receiver_id) {
resetLoadConfig()
}
loadConfig.cursor = 0
loadConfig.receiver_id = params.receiver_id
loadConfig.talk_type = params.talk_type
console.error('onLoad', params, options)
// 新增:支持指定消息定位模式,参数以传入为准合并
if (options?.specifiedMsg?.cursor !== undefined) {
loadConfig.specialParams = { ...options.specifiedMsg } // 记录特殊参数,供分页加载用
console.error('options', options)
loadConfig.status = 0 // 复用主流程 loading 状态
// 以 params 为基础,合并 specifiedMsg 的所有字段(只要有就覆盖)
const contextParams = {
...params,
...options.specifiedMsg
}
ServeTalkRecords(contextParams).then(({ data, code }) => {
if (code !== 200) {
loadConfig.status = 2
return
}
dialogueStore.clearDialogueRecord()
const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item))
dialogueStore.unshiftDialogueRecord(items.reverse())
loadConfig.status = items.length >= contextParams.limit ? 1 : 2
loadConfig.cursor = data.cursor
nextTick(() => {
setTimeout(() => {
const el = document.getElementById('imChatPanel')
const target = document.getElementById(options.specifiedMsg?.msg_id || '')
if (el && target) {
const containerRect = el.getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const offset = targetRect.top - containerRect.top
// 居中
const scrollTo = el.scrollTop + offset - el.clientHeight / 2 + target.clientHeight / 2
el.scrollTo({ top: scrollTo, behavior: 'smooth' })
addClass(target, 'border')
setTimeout(() => removeClass(target, 'border'), 3000)
} else if (el) {
el.scrollTop = el.scrollHeight
}
}, 50)
})
})
return
}
loadConfig.specialParams = undefined // 普通模式清空
// 原有逻辑
load(params)
}
// 向上加载更多(兼容特殊参数模式)
const onRefreshLoad = () => {
console.error('loadConfig.status', loadConfig.status)
if (loadConfig.status == 1) {
console.log('specialParams', loadConfig.specialParams)
// 判断是否是特殊参数模式
if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') {
// 检查特殊参数是否与当前会话匹配
if (loadConfig.specialParams.talk_type === loadConfig.talk_type &&
loadConfig.specialParams.receiver_id === loadConfig.receiver_id) {
// 特殊参数模式下direction: 'up'cursor: 当前最小 sequence
onLoad(
{
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
},
{
specifiedMsg: {
...loadConfig.specialParams,
direction: 'up',
cursor: getMinSequence()
}
}
)
} else {
// 如果不匹配,重置为普通模式
resetLoadConfig()
load({
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
})
}
} else {
// 原有逻辑
load({
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
})
}
}
}
// 向下加载更多(兼容特殊参数模式)
const onLoadMoreDown = () => {
// 判断是否是特殊参数模式
if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') {
// 检查特殊参数是否与当前会话匹配
if (loadConfig.specialParams.talk_type === loadConfig.talk_type &&
loadConfig.specialParams.receiver_id === loadConfig.receiver_id) {
onLoad(
{
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
},
{
specifiedMsg: {
...loadConfig.specialParams,
direction: 'down',
cursor: getMaxSequence()
}
}
)
} else {
// 如果不匹配,重置为普通模式
resetLoadConfig()
load({
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
})
}
} else {
load({
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
@ -145,13 +318,5 @@ export const useTalkRecord = (uid: number) => {
}
}
const onLoad = (params: Params) => {
loadConfig.cursor = 0
loadConfig.receiver_id = params.receiver_id
loadConfig.talk_type = params.talk_type
load(params)
}
return { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage }
return { loadConfig, records, onLoad, onRefreshLoad, onLoadMoreDown, onJumpMessage, resetLoadConfig }
}

View File

@ -34,6 +34,12 @@ export const useDialogueStore = defineStore('dialogue', {
// 聊天记录
records: [],
// 查询指定消息上下文的消息信息
specifiedMsg: '',
// 是否是手动切换会话
isManualSwitch: false,
// 新消息提示
unreadBubble: 0,
@ -88,6 +94,12 @@ export const useDialogueStore = defineStore('dialogue', {
this.unreadBubble = 0
this.isShowEditor = data?.is_robot === 0
// 只在手动切换会话时清空 specifiedMsg
// if (this.isManualSwitch) {
// this.specifiedMsg = ''
// this.isManualSwitch = false
// }
this.members = []
if (data.talk_type == 2) {
this.updateGroupMembers()

View File

@ -31,7 +31,8 @@ const talkParams = reactive({
online: computed(() => dialogueStore.online),
keyboard: computed(() => dialogueStore.keyboard),
num: computed(() => dialogueStore.members.length),
avatar:computed(() => dialogueStore.talk.avatar)
avatar:computed(() => dialogueStore.talk.avatar),
specifiedMsg: computed(() => dialogueStore.specifiedMsg)
})
const state = reactive({
@ -394,6 +395,7 @@ const handleGroupNoticeModalShow = (isAdmin) => {
:talk_type="talkParams.type"
:receiver_id="talkParams.receiver_id"
:index_name="talkParams.index_name"
:specifiedMsg="talkParams.specifiedMsg"
/>
</main>
@ -544,7 +546,7 @@ const handleGroupNoticeModalShow = (isAdmin) => {
@confirm="handleGroupNoticeModalConfirm"
@cancel="handleGroupNoticeModalCancel"
:customCloseEvent="state.groupNoticeEditMode === 2 ? true : false"
@closeModal="handleGroupNoticeModalClose"
@customCloseModal="handleGroupNoticeModalClose"
>
<template #content>
<div class="group-notice-modal-content">

View File

@ -23,7 +23,7 @@ import {
NButton,
NPagination
} from 'naive-ui'
import { Search, Plus } from '@icon-park/vue-next'
import { Search, Plus, Right } from '@icon-park/vue-next'
import TalkItem from './TalkItem.vue'
import Skeleton from './Skeleton.vue'
import { ServeClearTalkUnreadNum } from '@/api/chat'
@ -39,7 +39,7 @@ import { processError, processSuccess } from '@/utils/helper/message.js'
import chatAppSearchList from '@/components/search/searchList.vue'
import { ServeSeachQueryAll, ServeQueryTalkRecord, ServeUserGroupChatList } from '@/api/search'
import { getUserInfoByERPUserId } from '@/api/user'
import HighlightText from '@/components/search/highLightText.vue'
import { useRouter } from 'vue-router'
const router = useRouter()
@ -66,21 +66,10 @@ const renderChatAppSearch = () => {
return h(
chatAppSearchList,
{
// searchResultKey: 'user_infos',
// searchItem: {
// avatar:
// 'https://e-cdn.fontree.cn/fonchain-main/prod/image/18248/avatar/a0b2bee7-947f-465a-986e-10a1b2b87032.png',
// created_at: '2025-03-27 14:44:23',
// erp_user_id: 18248,
// id: 44,
// mobile: '18994430450',
// nickname: '耀'
// },
// searchText: ''
searchResultPageSize: 3,
listLimit: true,
apiRequest: ServeSeachQueryAll,
searchText: '王',
searchText: searchKeyword.value,
onClickSearchItem: (searchText, searchResultKey, talk_type, receiver_id, res) => {
console.log(searchText, searchResultKey, talk_type, receiver_id)
const result = JSON.parse(decodeURIComponent(res))
@ -103,7 +92,29 @@ const renderChatAppSearch = () => {
console.log(searchResultKey, searchText)
}
},
{}
{
'result-title': ({ getResultKeysValue, searchResultKey, searchResultIndex }) => {
return h(
'div',
{
style: {
padding: searchResultIndex === 0 ? '6px 10px 5px' : '18px 10px 5px',
borderBottom: '1px solid #f8f8f8'
}
},
[
h(
'span',
{
class: 'text-[14px] font-regular',
style: 'line-height: 20px; color: #999999;'
},
getResultKeysValue(searchResultKey)
)
]
)
}
}
)
}
@ -254,7 +265,19 @@ const state = reactive({
searchRecordText: '', //
ServeQueryTalkRecordParams: '', //
ServeQueryTalkRecordDetailParams: '', //
isShowSearchRecordDetailInfo: false //
isShowSearchRecordDetailInfo: false, //
// searchList searchDetailList
searchList: {
searchText: '',
apiParams: '',
lastId: undefined as any
},
searchDetailList: {
searchText: '',
apiParams: '',
lastId: undefined as any,
total: 0
}
})
const items = computed((): ISession[] => {
@ -310,22 +333,31 @@ watch(
}
)
// watch(
// () => state.searchRecordText,
// (newValue, oldValue) => {
// console.log(newValue, 'newValue')
// state.ServeQueryTalkRecordParams = encodeURIComponent(
// JSON.stringify({
// talk_type: 0, //12
// receiver_id: 0, //
// last_group_id: 0, //id
// last_member_id: 0, //id
// last_receiver_user_name: '', //
// last_receiver_group_name: '' //
// })
// )
// }
// )
//
watch(
() => state.searchRecordText,
(newVal, oldVal) => {
//
state.searchList.searchText = newVal
state.searchList.apiParams = encodeURIComponent(
JSON.stringify({
talk_type: 0,
receiver_id: 0,
last_group_id: 0,
last_member_id: 0,
last_receiver_user_name: '',
last_receiver_group_name: ''
})
)
state.searchList.lastId = undefined
//
state.searchDetailList.searchText = newVal
state.searchDetailList.apiParams = ''
state.searchDetailList.lastId = undefined
//
state.isShowSearchRecordDetailInfo = false
}
)
//
const loadStatus = computed(() => talkStore.loadStatus)
@ -335,12 +367,14 @@ const indexName = computed(() => dialogueStore.index_name)
//
const onTabTalk = (item: ISession, follow = false) => {
console.log('onTabTalk');
console.log('onTabTalk')
if (item.index_name === indexName.value) return
searchKeyword.value = ''
dialogueStore.isManualSwitch = true
//
dialogueStore.setDialogue(item)
@ -565,19 +599,43 @@ const handleClickSearchItem = (searchText, searchResultKey, talk_type, receiver_
const result = JSON.parse(decodeURIComponent(res))
console.log(result)
if (searchResultKey === 'general_infos') {
state.ServeQueryTalkRecordDetailParams = encodeURIComponent(
//
state.isShowSearchRecordDetailInfo = false
state.searchDetailList.apiParams = encodeURIComponent(
JSON.stringify({
last_group_id: 0, //id
last_member_id: 0, //id
receiver_id: receiver_id, //
talk_type: talk_type //12
last_group_id: 0,
last_member_id: 0,
receiver_id: receiver_id,
talk_type: talk_type
})
)
state.searchDetailList.searchText = state.searchRecordText
state.searchDetailList.lastId = undefined
//
nextTick(() => {
searchDetailListRef.value?.doLoadMore(true)
state.isShowSearchRecordDetailInfo = true
})
}
}
//item
const handleClickSearchResultItem = (searchText, searchResultKey, talk_type, receiver_id, res) => {
const result = JSON.parse(decodeURIComponent(res))
console.error(result, 'result')
// , sequence
dialogueStore.specifiedMsg = encodeURIComponent(
JSON.stringify({
talk_type,
receiver_id,
msg_id: result.msg_id,
cursor: result.sequence - 15 > 0 ? result.sequence - 15 : 0,
direction: 'down',
sort_sequence: 'asc',
create_time: result.created_at
})
)
console.error(dialogueStore.specifiedMsg, 'dialogueStore.specifiedMsg')
talkStore.toTalk(talk_type, receiver_id, router)
}
//item
const handleClickStayItemChange = (item) => {
if (item) {
@ -593,52 +651,62 @@ const searchListRef = ref()
// ref
const searchDetailListRef = ref()
//
const loadMoreRecordList = () => {
searchListRef.value?.doLoadMore()
}
//
const loadMoreRecordDetail = () => {
searchDetailListRef.value?.doLoadMore()
}
const handleMoreRecordLastIdChange = (
// lastIdChange
const handleSearchListLastIdChange = (
last_id,
last_group_id,
last_member_id,
last_receiver_user_name,
last_receiver_group_name
) => {
let idChanges = {
state.searchList.lastId = {
last_id,
last_group_id,
last_member_id,
last_receiver_user_name,
last_receiver_group_name
}
state.ServeQueryTalkRecordParams = encodeURIComponent(
JSON.stringify(
Object.assign({}, JSON.parse(decodeURIComponent(state.ServeQueryTalkRecordParams)), idChanges)
)
state.searchList.apiParams = encodeURIComponent(
JSON.stringify({
...JSON.parse(decodeURIComponent(state.searchList.apiParams)),
last_id,
last_group_id,
last_member_id,
last_receiver_user_name,
last_receiver_group_name
})
)
}
const handleRecordDetailLastIdChange = (last_id, last_group_id, last_member_id) => {
let idChanges = {
const handleSearchDetailListLastIdChange = (last_id, last_group_id, last_member_id) => {
state.searchDetailList.lastId = { last_id, last_group_id, last_member_id }
state.searchDetailList.apiParams = encodeURIComponent(
JSON.stringify({
...JSON.parse(decodeURIComponent(state.searchDetailList.apiParams)),
last_id,
last_group_id,
last_member_id
})
)
}
state.ServeQueryTalkRecordDetailParams = encodeURIComponent(
JSON.stringify(
Object.assign(
{},
JSON.parse(decodeURIComponent(state.ServeQueryTalkRecordDetailParams)),
idChanges
)
)
)
//
const handleCloseSearchRecordModal = () => {
state.isShowSearchRecordModal = false
state.searchRecordText = ''
}
//
const getResultTotalCount = (total) => {
state.searchDetailList.total = total
}
//
const handleEnterSearchResultChat = () => {
const searchResult = JSON.parse(decodeURIComponent(state.searchDetailList.apiParams))
talkStore.toTalk(searchResult.talk_type, searchResult.receiver_id, router)
state.isShowSearchRecordModal = false
state.searchRecordText = ''
searchKeyword.value = ''
}
</script>
@ -660,7 +728,7 @@ const handleRecordDetailLastIdChange = (last_id, last_group_id, last_member_id)
<n-dropdown
trigger="click"
:options="state.chatSearchOptions"
style="width: 248px; height: 677px; overflow-y: scroll;"
style="width: 248px; height: 677px;"
>
<n-input
placeholder="搜索好友 / 群聊"
@ -854,6 +922,8 @@ const handleRecordDetailLastIdChange = (last_id, last_group_id, last_member_id)
:style="state.customSearchRecordModalStyle"
:customCloseBtn="true"
:closable="false"
:customCloseEvent="true"
@customCloseModal="handleCloseSearchRecordModal"
>
<template #content>
<div class="search-record-modal-content">
@ -872,34 +942,54 @@ const handleRecordDetailLastIdChange = (last_id, last_group_id, last_member_id)
</n-input>
</div>
<div class="search-record-card" v-if="state.searchRecordText">
<div class="search-record-list" v-loadmore="loadMoreRecordList">
<div class="search-record-list">
<chatAppSearchList
ref="searchListRef"
:searchResultPageSize="10"
:listLimit="false"
:apiRequest="ServeQueryTalkRecord"
:apiParams="state.ServeQueryTalkRecordParams"
:searchText="state.searchRecordText"
:apiParams="state.searchList.apiParams"
:searchText="state.searchList.searchText"
:isPagination="true"
searchResultKey="general_infos"
@clickSearchItem="handleClickSearchItem"
:useClickStay="true"
@clickStayItemChange="handleClickStayItemChange"
@lastIdChange="handleMoreRecordLastIdChange"
@lastIdChange="handleSearchListLastIdChange"
:searchResultMaxHeight="'517px'"
></chatAppSearchList>
</div>
<div class="search-record-detail" v-loadmore="loadMoreRecordDetail">
<div class="search-record-detail">
<div class="search-record-detail-header" v-if="state.isShowSearchRecordDetailInfo">
<HighlightText
class="text-[14px] text-[#B0B0B0] leading-[20px]"
:text="
state.searchDetailList.total +
'条与“' +
state.searchRecordText +
'”相关的搜索结果'
"
:searchText="state.searchRecordText"
/>
<div class="search-record-detail-header-btn" @click="handleEnterSearchResultChat">
<span>进入聊天</span>
<n-icon :component="Right" color="#46299D" size="14px" />
</div>
</div>
<chatAppSearchList
ref="searchDetailListRef"
v-if="state.isShowSearchRecordDetailInfo"
:searchResultPageSize="10"
:listLimit="false"
:apiRequest="ServeQueryTalkRecord"
:apiParams="state.ServeQueryTalkRecordDetailParams"
:searchText="state.searchRecordText"
:apiParams="state.searchDetailList.apiParams"
:searchText="state.searchDetailList.searchText"
:isPagination="true"
:searchRecordDetail="true"
@lastIdChange="handleRecordDetailLastIdChange"
@lastIdChange="handleSearchDetailListLastIdChange"
:searchResultMaxHeight="'469px'"
@resultTotalCount="getResultTotalCount"
@clickSearchItem="handleClickSearchResultItem"
></chatAppSearchList>
</div>
</div>
@ -1094,13 +1184,33 @@ html[theme-mode='dark'] {
width: 260px;
height: 517px;
border: 1px solid #efeff5;
overflow-y: scroll;
}
.search-record-detail {
width: 578px;
height: 517px;
border: 1px solid #efeff5;
overflow-y: scroll;
.search-record-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 4px 14px 10px;
box-sizing: border-box;
.search-record-detail-header-btn {
line-height: 20px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 8px;
cursor: pointer;
span {
line-height: 20px;
color: #46299d;
font-size: 14px;
font-weight: 400;
}
}
}
}
}
.search-record-empty {

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { watch, onMounted, ref } from 'vue'
import { watch, onMounted, ref, nextTick } from 'vue'
import { NDropdown, NCheckbox } from 'naive-ui'
import { Loading, MoreThree, ToTop } from '@icon-park/vue-next'
import { bus } from '@/utils/event-bus'
@ -13,7 +13,7 @@ import SkipBottom from './SkipBottom.vue'
import { ITalkRecord } from '@/types/chat'
import { EditorConst } from '@/constant/event-bus'
import { useInject, useTalkRecord, useUtil } from '@/hooks'
import { ExclamationCircleFilled } from '@ant-design/icons-vue';
import { ExclamationCircleFilled } from '@ant-design/icons-vue'
import { useUserStore } from '@/store'
import RevokeMessage from '@/components/talk/message/RevokeMessage.vue'
import { voiceToText } from '@/api/chat.js'
@ -33,6 +33,10 @@ const props = defineProps({
index_name: {
type: String,
default: ''
},
specifiedMsg: {
type: String,
default: ''
}
})
@ -277,18 +281,48 @@ const onRowClick = (item: ITalkRecord) => {
}
}
watch(props, () => {
onLoad({ ...props, limit: 30 })
})
const lastParams = ref('')
onMounted(() => {
onLoad({ ...props, limit: 30 })
})
// props
watch(
() => props,
async (newProps) => {
await nextTick()
let specialParams = undefined
console.error(newProps, 'newProps')
if (newProps.specifiedMsg) {
try {
const parsed = JSON.parse(decodeURIComponent(newProps.specifiedMsg))
// id
if (parsed.talk_type === newProps.talk_type && parsed.receiver_id === newProps.receiver_id) {
specialParams = parsed
}
} catch (e) {}
}
onLoad(
{
receiver_id: newProps.receiver_id,
talk_type: newProps.talk_type,
limit: 30
},
specialParams ? { specifiedMsg: specialParams } : undefined
)
},
{ immediate: true, deep: true }
)
// onMounted(() => {
// onLoad({ ...props, limit: 30 })
// })
</script>
<template>
<section class="section">
<div id="imChatPanel" class="me-scrollbar me-scrollbar-thumb talk-container" @scroll="onPanelScroll($event)">
<div
id="imChatPanel"
class="me-scrollbar me-scrollbar-thumb talk-container"
@scroll="onPanelScroll($event)"
>
<!-- 数据加载状态栏 -->
<div class="load-toolbar pointer">
<span v-if="loadConfig.status == 0"> 正在加载数据中 ... </span>
@ -296,23 +330,42 @@ onMounted(() => {
<span v-else class="no-more"> 没有更多消息了 </span>
</div>
<div class="message-item" v-for="(item, index) in records" :key="item.msg_id" :id="item.msg_id">
<div
class="message-item"
v-for="(item, index) in records"
:key="item.msg_id"
:id="item.msg_id"
>
<!-- 系统消息 -->
<div v-if="item.msg_type >= 1000" class="message-box">
<component :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra" :data="item" />
<component
:is="MessageComponents[item.msg_type] || 'unknown-message'"
:extra="item.extra"
:data="item"
/>
</div>
<!-- 撤回消息 -->
<div v-else-if="item.is_revoke == 1" class="message-box">
<revoke-message :login_uid="uid" :data="item" :user_id="item.user_id" :nickname="item.nickname" :talk_type="item.talk_type"
:datetime="item.created_at" />
<revoke-message
:login_uid="uid"
:data="item"
:user_id="item.user_id"
:nickname="item.nickname"
:talk_type="item.talk_type"
:datetime="item.created_at"
/>
</div>
<div v-else class="message-box record-box" :class="{
<div
v-else
class="message-box record-box"
:class="{
'direction-rt': item.float == 'right',
'multi-select': dialogueStore.isOpenMultiSelect,
'multi-select-check': item.isCheck
}">
}"
>
<!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0">
<n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" />
@ -320,29 +373,53 @@ onMounted(() => {
<!-- 头像信息 -->
<aside class="avatar-column">
<im-avatar class="pointer" :src="item.avatar" :size="42" :username="item.nickname"
@click="showUserInfoModal(item.erp_user_id,item.user_id)" />
<im-avatar
class="pointer"
:src="item.avatar"
:size="42"
:username="item.nickname"
@click="showUserInfoModal(item.erp_user_id, item.user_id)"
/>
</aside>
<!-- 主体信息 -->
<main class="main-column">
<div class="talk-title">
<span class="nickname pointer" v-show="talk_type == 2 && item.float == 'left'"
@click="onClickNickname(item)">
<span
class="nickname pointer"
v-show="talk_type == 2 && item.float == 'left'"
@click="onClickNickname(item)"
>
<span class="at">@</span>{{ item.nickname }}
</span>
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
</div>
<div class="talk-content" :class="{ pointer: dialogueStore.isOpenMultiSelect }" @click="onRowClick(item)">
<component :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra" :data="item"
:max-width="true" :source="'panel'" @contextmenu.prevent="onContextMenu($event, item)" />
<div v-if="item.float==='right'&&item.extra.percentage===-1&&item.extra.is_uploading" class="mr-10px"> <n-button text style="font-size: 20px">
<div
class="talk-content"
:class="{ pointer: dialogueStore.isOpenMultiSelect }"
@click="onRowClick(item)"
>
<component
:is="MessageComponents[item.msg_type] || 'unknown-message'"
:extra="item.extra"
:data="item"
:max-width="true"
:source="'panel'"
@contextmenu.prevent="onContextMenu($event, item)"
/>
<div
v-if="
item.float === 'right' && item.extra.percentage === -1 && item.extra.is_uploading
"
class="mr-10px"
>
<n-button text style="font-size: 20px;">
<n-icon color="#CF3050">
<ExclamationCircleFilled />
</n-icon>
</n-button></div>
</n-button>
</div>
<!-- <div class="talk-tools">
<template v-if="talk_type == 1 && item.float == 'right'">
<loading
@ -362,7 +439,11 @@ onMounted(() => {
</div> -->
</div>
<div v-if="item.extra.reply" class="talk-reply pointer" @click="onJumpMessage(item.extra?.reply?.msg_id)">
<div
v-if="item.extra.reply"
class="talk-reply pointer"
@click="onJumpMessage(item.extra?.reply?.msg_id)"
>
<n-icon :component="ToTop" size="14" class="icon-top" />
<span class="ellipsis">
回复 {{ item.extra?.reply?.nickname }}:
@ -383,8 +464,15 @@ onMounted(() => {
</section>
<!-- 右键菜单 -->
<n-dropdown :show="dropdown.show" :x="dropdown.x" :y="dropdown.y" style="width: 142px;" :options="dropdown.options"
@select="onContextMenuHandle" @clickoutside="closeDropdownMenu" />
<n-dropdown
:show="dropdown.show"
:x="dropdown.x"
:y="dropdown.y"
style="width: 142px;"
:options="dropdown.options"
@select="onContextMenuHandle"
@clickoutside="closeDropdownMenu"
/>
</template>
<style lang="less" scoped>