消息
This commit is contained in:
5
TUIKit/components/TUISearch/index.ts
Normal file
5
TUIKit/components/TUISearch/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import TUISearch from './index.vue';
|
||||
import Server from './server';
|
||||
new Server();
|
||||
|
||||
export default TUISearch;
|
||||
207
TUIKit/components/TUISearch/index.vue
Normal file
207
TUIKit/components/TUISearch/index.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="
|
||||
searchType === 'global' ||
|
||||
((searchType === 'conversation' || (!searchType && isUniFrameWork)) &&
|
||||
isShowInConversationSearch)
|
||||
"
|
||||
:class="[
|
||||
'tui-search',
|
||||
!isPC && 'tui-search-h5',
|
||||
`tui-search-main-${searchType ? searchType : 'conversation'}`,
|
||||
isFullScreen && 'tui-search-h5-full-screen',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="searchType === 'global'"
|
||||
ref="globalSearchRef"
|
||||
:class="['tui-search-global', !isPC && 'tui-search-h5-global']"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'tui-search-global-header',
|
||||
!isPC && 'tui-search-h5-global-header',
|
||||
]"
|
||||
>
|
||||
<SearchInput
|
||||
class="search-input"
|
||||
:searchType="searchType"
|
||||
/>
|
||||
<SearchMore
|
||||
v-if="isPC || !searchingStatus"
|
||||
class="search-more"
|
||||
:searchType="searchType"
|
||||
/>
|
||||
</div>
|
||||
<SearchContainer
|
||||
v-if="searchingStatus"
|
||||
class="search-container"
|
||||
popupPosition="bottom"
|
||||
:searchType="searchType"
|
||||
>
|
||||
<template #result>
|
||||
<SearchResult
|
||||
class="search-result"
|
||||
:searchType="searchType"
|
||||
/>
|
||||
</template>
|
||||
</SearchContainer>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(searchType === 'conversation' && isShowInConversationSearch) ||
|
||||
isUniFrameWork
|
||||
"
|
||||
:class="[
|
||||
'tui-search-conversation',
|
||||
!isPC && 'tui-search-h5-conversation',
|
||||
]"
|
||||
>
|
||||
<SearchContainer
|
||||
class="search-container"
|
||||
popupPosition="aside"
|
||||
:searchType="searchType ? searchType : 'conversation'"
|
||||
@closeInConversationSearch="closeInConversationSearch"
|
||||
>
|
||||
<template #input>
|
||||
<SearchInput
|
||||
:searchType="searchType ? searchType : 'conversation'"
|
||||
/>
|
||||
</template>
|
||||
<template #result>
|
||||
<SearchResult
|
||||
class="search-result"
|
||||
:searchType="searchType ? searchType : 'conversation'"
|
||||
/>
|
||||
</template>
|
||||
</SearchContainer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
computed,
|
||||
withDefaults,
|
||||
onUnmounted,
|
||||
} from '../../adapter-vue';
|
||||
import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
|
||||
import { TUIGlobal, outsideClick } from '@tencentcloud/universal-api';
|
||||
import SearchInput from './search-input/index.vue';
|
||||
import SearchContainer from './search-container/index.vue';
|
||||
import SearchResult from './search-result/index.vue';
|
||||
import SearchMore from './search-more/index.vue';
|
||||
import { searchMessageTypeDefault } from './search-type-list';
|
||||
import { searchMessageTimeDefault } from './search-time-list';
|
||||
import { isPC, isUniFrameWork } from '../../utils/env';
|
||||
import { ISearchingStatus, SEARCH_TYPE } from './type';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
searchType?: SEARCH_TYPE;
|
||||
}>(),
|
||||
{
|
||||
searchType: () => {
|
||||
return isUniFrameWork ? 'conversation' : 'global';
|
||||
},
|
||||
},
|
||||
);
|
||||
const globalSearchRef = ref<HTMLElement | null>();
|
||||
const currentConversationID = ref<string>('');
|
||||
const searchingStatus = ref<boolean>(false);
|
||||
// Whether to display the search in the chat
|
||||
const isShowInConversationSearch = ref<boolean>(isUniFrameWork);
|
||||
// Whether to search in full screen - Search in full screen when the mobile terminal is searching
|
||||
const isFullScreen = computed(
|
||||
() =>
|
||||
!isPC
|
||||
&& ((props.searchType === 'global' && searchingStatus.value)
|
||||
|| (props.searchType === 'conversation' && isShowInConversationSearch.value)),
|
||||
);
|
||||
|
||||
const initSearchValue = (searchType: SEARCH_TYPE) => {
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
|
||||
value: '',
|
||||
searchType: searchType,
|
||||
});
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageType', {
|
||||
value: searchMessageTypeDefault[searchType],
|
||||
searchType: searchType,
|
||||
});
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageTime', {
|
||||
value: searchMessageTimeDefault,
|
||||
searchType: searchType,
|
||||
});
|
||||
};
|
||||
|
||||
function onCurrentConversationIDUpdate(conversationID: string) {
|
||||
if (!isUniFrameWork && currentConversationID.value !== conversationID) {
|
||||
// PC side single page switch session, close search
|
||||
closeInConversationSearch();
|
||||
}
|
||||
currentConversationID.value = conversationID;
|
||||
}
|
||||
|
||||
function onCurrentSearchingStatusChange(value: ISearchingStatus) {
|
||||
if (value?.searchType === props.searchType) {
|
||||
searchingStatus.value = value?.isSearching;
|
||||
// global search ui bind on click outside close
|
||||
if (value?.searchType === 'global' && globalSearchRef.value) {
|
||||
if (isPC && value.isSearching) {
|
||||
outsideClick.listen({
|
||||
domRefs: globalSearchRef.value,
|
||||
handler: closeGlobalSearch,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (value?.searchType === 'global' && isUniFrameWork) {
|
||||
// hide tab bar in uni-app when global searching
|
||||
value.isSearching ? TUIGlobal?.hideTabBar()?.catch(() => { /* ignore */ }) : TUIGlobal?.showTabBar()?.catch(() => { /* ignore */ });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onIsShowInConversationSearchChange(value: boolean) {
|
||||
isShowInConversationSearch.value = value ? true : false;
|
||||
isShowInConversationSearch.value && initSearchValue(props.searchType);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// init with default value
|
||||
['global', 'conversation'].forEach((type: string) => {
|
||||
initSearchValue(type as SEARCH_TYPE);
|
||||
});
|
||||
// watch store change
|
||||
TUIStore.watch(StoreName.CONV, {
|
||||
currentConversationID: onCurrentConversationIDUpdate,
|
||||
});
|
||||
TUIStore.watch(StoreName.SEARCH, {
|
||||
currentSearchingStatus: onCurrentSearchingStatusChange,
|
||||
isShowInConversationSearch: onIsShowInConversationSearchChange,
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// unwatch store change
|
||||
TUIStore.unwatch(StoreName.CONV, {
|
||||
currentConversationID: onCurrentConversationIDUpdate,
|
||||
});
|
||||
TUIStore.unwatch(StoreName.SEARCH, {
|
||||
currentSearchingStatus: onCurrentSearchingStatusChange,
|
||||
isShowInConversationSearch: onIsShowInConversationSearchChange,
|
||||
});
|
||||
});
|
||||
|
||||
function closeGlobalSearch() {
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
|
||||
isSearching: false,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
}
|
||||
|
||||
function closeInConversationSearch() {
|
||||
TUIStore.update(StoreName.SEARCH, 'isShowInConversationSearch', false);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped src="./style/index.scss"></style>
|
||||
2
TUIKit/components/TUISearch/search-container/index.ts
Normal file
2
TUIKit/components/TUISearch/search-container/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import SearchContainer from './index.vue';
|
||||
export default SearchContainer;
|
||||
259
TUIKit/components/TUISearch/search-container/index.vue
Normal file
259
TUIKit/components/TUISearch/search-container/index.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'tui-search-container',
|
||||
!isPC && 'tui-search-container-h5',
|
||||
isPC && `container-${props.popupPosition}`,
|
||||
`container-${props.searchType}`,
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
isPC && `tui-search-container-${props.popupPosition}`,
|
||||
!isPC && 'tui-search-container-h5-main',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="props.searchType === 'conversation' && !isUniFrameWork"
|
||||
class="tui-search-header"
|
||||
>
|
||||
<div class="tui-search-header-title">
|
||||
{{ TUITranslateService.t("TUISearch.搜索会话内容") }}
|
||||
</div>
|
||||
<div
|
||||
class="tui-search-header-close"
|
||||
@click="closeSearchContainer"
|
||||
>
|
||||
<Icon
|
||||
:file="closeDarkIcon"
|
||||
width="14px"
|
||||
height="14px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tui-search-tabs">
|
||||
<div
|
||||
v-for="(tabItem, tabKey) in searchTypeList"
|
||||
:key="tabKey"
|
||||
:class="[
|
||||
'tui-search-tabs-item',
|
||||
currentSearchMessageType.key === tabItem.key && 'tui-search-tabs-item-selected',
|
||||
]"
|
||||
@click="selectSearchType(tabItem)"
|
||||
>
|
||||
{{ TUITranslateService.t(`TUISearch.${tabItem.label}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- TUISearch search input slot -->
|
||||
<slot name="input" />
|
||||
<div
|
||||
v-if="isTimeTabsShow"
|
||||
class="tui-search-time"
|
||||
>
|
||||
<div
|
||||
v-for="(tabItem, tabKey) in searchMessageTimeList"
|
||||
:key="tabKey"
|
||||
:class="[
|
||||
'tui-search-time-item',
|
||||
currentSearchMessageTime.key === tabItem.key && 'tui-search-time-item-selected',
|
||||
]"
|
||||
@click="selectSearchTime(tabItem)"
|
||||
>
|
||||
<div
|
||||
v-if="tabItem.key === 'all'"
|
||||
class="tui-search-time-item-picker"
|
||||
>
|
||||
<div
|
||||
v-if="!isDatePickerShow"
|
||||
class="tui-search-time-item-all"
|
||||
@click.stop="handleSelectAllTimeClicked"
|
||||
>
|
||||
{{
|
||||
TUITranslateService.t(`TUISearch.选择时间`) +
|
||||
": " +
|
||||
TUITranslateService.t(`TUISearch.全部`)
|
||||
}}
|
||||
<Icon
|
||||
:file="downArrowIcon"
|
||||
width="14px"
|
||||
height="14px"
|
||||
/>
|
||||
</div>
|
||||
<div @click.stop>
|
||||
<DatePicker
|
||||
v-if="isDatePickerShow"
|
||||
type="range"
|
||||
:rangeTableType="datePickerRangeDisplayType"
|
||||
@pick="pickTimePeriod"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="isDatePickerShow"
|
||||
class="tui-search-time-item-close"
|
||||
@click="clearTimePicker"
|
||||
>
|
||||
<Icon
|
||||
class="icon"
|
||||
:file="closeIcon"
|
||||
width="14px"
|
||||
height="14px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ TUITranslateService.t(`TUISearch.${tabItem.label}`) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TUISearch search result slot -->
|
||||
<slot name="result" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from '../../../adapter-vue';
|
||||
import {
|
||||
TUITranslateService,
|
||||
TUIStore,
|
||||
StoreName,
|
||||
} from '@tencentcloud/chat-uikit-engine';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import {
|
||||
globalSearchTypeList,
|
||||
conversationSearchTypeList,
|
||||
searchMessageTypeDefault,
|
||||
} from '../search-type-list';
|
||||
import { searchMessageTimeList, searchMessageTimeDefault } from '../search-time-list';
|
||||
import Icon from '../../common/Icon.vue';
|
||||
import DatePicker from '../../common/DatePicker/index.vue';
|
||||
import downArrowIcon from '../../../assets/icon/down-icon.svg';
|
||||
import closeIcon from '../../../assets/icon/input-close.svg';
|
||||
import closeDarkIcon from '../../../assets/icon/close-dark.svg';
|
||||
import { isPC, isUniFrameWork } from '../../../utils/env';
|
||||
import { SEARCH_TYPE, ISearchMessageTime, ISearchMessageType, ISearchTimeTab, ISearchTypeTab } from '../type';
|
||||
|
||||
const props = defineProps({
|
||||
popupPosition: {
|
||||
type: String, // "bottom" / "aside"
|
||||
default: 'bottom',
|
||||
},
|
||||
searchType: {
|
||||
type: String,
|
||||
default: 'global', // "global" / "conversation"
|
||||
validator(value: string) {
|
||||
return ['global', 'conversation'].includes(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['searchConfigChange', 'closeInConversationSearch']);
|
||||
|
||||
const searchTypeList = computed(() =>
|
||||
props?.searchType === 'conversation' ? conversationSearchTypeList : globalSearchTypeList,
|
||||
);
|
||||
const currentSearchMessageType = ref(searchMessageTypeDefault[props?.searchType as SEARCH_TYPE]);
|
||||
const currentSearchMessageTime = ref(searchMessageTimeDefault);
|
||||
|
||||
const isTimeTabsShow = computed(() => {
|
||||
return (
|
||||
currentSearchMessageType.value.key !== 'contact'
|
||||
&& currentSearchMessageType.value.key !== 'group'
|
||||
);
|
||||
});
|
||||
const datePickerRangeDisplayType = computed((): string =>
|
||||
isPC && props.searchType === 'global' && !isUniFrameWork ? 'two' : 'one',
|
||||
);
|
||||
const isDatePickerShow = ref<boolean>(false);
|
||||
|
||||
function onCurrentSearchMessageTypeChange(typeObject: ISearchMessageType) {
|
||||
if (typeObject?.searchType === props?.searchType) {
|
||||
currentSearchMessageType.value
|
||||
= typeObject?.value || searchMessageTypeDefault[props?.searchType as SEARCH_TYPE];
|
||||
}
|
||||
}
|
||||
|
||||
function onCurrentSearchMessageTimeChange(timeObject: ISearchMessageTime) {
|
||||
if (timeObject?.searchType === props?.searchType) {
|
||||
currentSearchMessageTime.value = timeObject?.value || searchMessageTimeDefault;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
TUIStore.watch(StoreName.SEARCH, {
|
||||
currentSearchMessageType: onCurrentSearchMessageTypeChange,
|
||||
currentSearchMessageTime: onCurrentSearchMessageTimeChange,
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
TUIStore.unwatch(StoreName.SEARCH, {
|
||||
currentSearchMessageType: onCurrentSearchMessageTypeChange,
|
||||
currentSearchMessageTime: onCurrentSearchMessageTimeChange,
|
||||
});
|
||||
});
|
||||
|
||||
const selectSearchType = (item: ISearchTypeTab) => {
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageType', {
|
||||
value: item,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
};
|
||||
|
||||
const selectSearchTime = (item: ISearchTimeTab) => {
|
||||
if (isDatePickerShow.value && item.key === 'all') {
|
||||
isDatePickerShow.value = false;
|
||||
} else {
|
||||
isDatePickerShow.value = false;
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageTime', {
|
||||
value: item,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAllTimeClicked = () => {
|
||||
if (currentSearchMessageTime.value?.key !== 'all') {
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageTime', {
|
||||
value: searchMessageTimeDefault,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
} else {
|
||||
isDatePickerShow.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const pickTimePeriod = (time: typeof Dayjs) => {
|
||||
if (currentSearchMessageTime.value?.key === 'all') {
|
||||
const { startDate, endDate } = time;
|
||||
const timePosition = Number((endDate?.toDate()?.getTime() / 1000).toFixed(0));
|
||||
const timePeriod = timePosition - Number((startDate?.toDate()?.getTime() / 1000).toFixed(0));
|
||||
const newSearchMessageTime = {
|
||||
key: currentSearchMessageTime.value.key,
|
||||
label: currentSearchMessageTime.value.label,
|
||||
value: {
|
||||
timePosition,
|
||||
timePeriod,
|
||||
},
|
||||
};
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageTime', {
|
||||
value: newSearchMessageTime,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const clearTimePicker = () => {
|
||||
isDatePickerShow.value = false;
|
||||
if (currentSearchMessageTime.value?.key === 'all') {
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageTime', {
|
||||
value: searchMessageTimeDefault,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const closeSearchContainer = () => {
|
||||
emits('closeInConversationSearch');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped src="./style/index.scss"></style>
|
||||
36
TUIKit/components/TUISearch/search-container/style/h5.scss
Normal file
36
TUIKit/components/TUISearch/search-container/style/h5.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
.tui-search-container-h5 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tui-search-container-h5-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tui-search-tabs {
|
||||
.tui-search-tabs-item {
|
||||
cursor: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tui-search-time {
|
||||
background-color: #f4f4f4;
|
||||
|
||||
.tui-search-time-item {
|
||||
cursor: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.tui-search-time-item-selected {
|
||||
background-color: rgba(0,122,255,0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@import '../../../../assets/styles/common';
|
||||
@import './web';
|
||||
@import './h5';
|
||||
114
TUIKit/components/TUISearch/search-container/style/web.scss
Normal file
114
TUIKit/components/TUISearch/search-container/style/web.scss
Normal file
@@ -0,0 +1,114 @@
|
||||
.container-bottom {
|
||||
margin: 0 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container-conversation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tui-search-container {
|
||||
&-bottom {
|
||||
position: absolute;
|
||||
min-width: 400px;
|
||||
width: fit-content;
|
||||
height: 453px;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: rgba(0,0,0,0.16) 0 3px 6px, rgba(0,0,0,0.23) 0 3px 6px;
|
||||
left: 0;
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
&-aside {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tui-search-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px 10px;
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 19.6px;
|
||||
font-family: "PingFang SC", sans-serif;
|
||||
}
|
||||
|
||||
&-close {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.tui-search-tabs {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
|
||||
&-item {
|
||||
padding: 10px 0;
|
||||
margin: 0 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&-selected {
|
||||
color: #007aff;
|
||||
border-bottom: 2px solid #007aff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tui-search-time {
|
||||
width: 100%;
|
||||
height: 43px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
|
||||
&-item {
|
||||
height: calc(100% - 20px);
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-picker {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-close,
|
||||
&-all {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-selected {
|
||||
background-color: rgba(0,122,255,0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
TUIKit/components/TUISearch/search-input/index.ts
Normal file
2
TUIKit/components/TUISearch/search-input/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import SearchInput from './index.vue';
|
||||
export default SearchInput;
|
||||
215
TUIKit/components/TUISearch/search-input/index.vue
Normal file
215
TUIKit/components/TUISearch/search-input/index.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'tui-search-input-container',
|
||||
!isPC && 'tui-search-input-container-h5',
|
||||
props.searchType === 'global'
|
||||
? 'tui-search-input-container-global'
|
||||
: 'tui-search-input-container-conversation'
|
||||
]"
|
||||
>
|
||||
<div :class="['tui-search-input', !isPC && 'tui-search-input-h5']">
|
||||
<div class="tui-search-input-left">
|
||||
<Icon
|
||||
class="icon"
|
||||
:file="searchIcon"
|
||||
width="14px"
|
||||
height="14px"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-model="searchValueModel"
|
||||
class="tui-search-input-main"
|
||||
type="text"
|
||||
:placeholder="props.placeholder"
|
||||
:focus="false"
|
||||
enterkeyhint="search"
|
||||
@blur="onBlur"
|
||||
@keyup.enter="search"
|
||||
@confirm="search"
|
||||
@click.stop.prevent="onSearchInputClick"
|
||||
>
|
||||
<div
|
||||
v-if="searchingStatus"
|
||||
class="tui-search-input-right"
|
||||
@click="endSearching"
|
||||
>
|
||||
<Icon
|
||||
class="icon"
|
||||
:file="closeIcon"
|
||||
width="14px"
|
||||
height="14px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isPC && searchingStatus && props.searchType === 'global'"
|
||||
:class="[
|
||||
'tui-search-input-cancel',
|
||||
!isPC && 'tui-search-input-h5-cancel',
|
||||
]"
|
||||
@click="endSearching"
|
||||
>
|
||||
{{ TUITranslateService.t("TUISearch.取消") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from '../../../adapter-vue';
|
||||
import {
|
||||
TUIStore,
|
||||
StoreName,
|
||||
TUITranslateService,
|
||||
} from '@tencentcloud/chat-uikit-engine';
|
||||
import { TUIGlobal } from '@tencentcloud/universal-api';
|
||||
import Icon from '../../common/Icon.vue';
|
||||
import searchIcon from '../../../assets/icon/search.svg';
|
||||
import closeIcon from '../../../assets/icon/input-close.svg';
|
||||
import { isPC } from '../../../utils/env';
|
||||
import { ISearchInputValue, ISearchingStatus } from '../type';
|
||||
const props = defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: () => TUITranslateService.t('TUISearch.搜索'),
|
||||
},
|
||||
searchType: {
|
||||
type: String,
|
||||
default: 'global', // "global" / "conversation"
|
||||
validator(value: string) {
|
||||
return ['global', 'conversation'].includes(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const searchValueModel = ref<string>('');
|
||||
const currentSearchInputValue = ref<string>('');
|
||||
const searchingStatus = ref<boolean>(false);
|
||||
|
||||
function onCurrentSearchInputValueChange(obj: ISearchInputValue) {
|
||||
if (obj?.searchType === props?.searchType) {
|
||||
currentSearchInputValue.value = obj?.value;
|
||||
searchValueModel.value = obj?.value;
|
||||
}
|
||||
}
|
||||
|
||||
function onCurrentSearchingStatusChange(obj: ISearchingStatus) {
|
||||
if (obj?.searchType === props?.searchType) {
|
||||
searchingStatus.value = obj?.isSearching;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
TUIStore.watch(StoreName.SEARCH, {
|
||||
currentSearchInputValue: onCurrentSearchInputValueChange,
|
||||
currentSearchingStatus: onCurrentSearchingStatusChange,
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
TUIStore.unwatch(StoreName.SEARCH, {
|
||||
currentSearchInputValue: onCurrentSearchInputValueChange,
|
||||
currentSearchingStatus: onCurrentSearchingStatusChange,
|
||||
});
|
||||
});
|
||||
|
||||
const search = () => {
|
||||
// Avoid duplicate searches
|
||||
if (searchValueModel.value === currentSearchInputValue.value) {
|
||||
return;
|
||||
}
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
|
||||
value: searchValueModel.value,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
};
|
||||
|
||||
const endSearching = () => {
|
||||
searchingStatus.value = false;
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
|
||||
isSearching: false,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
|
||||
value: '',
|
||||
searchType: props.searchType,
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchInputClick = () => {
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
|
||||
isSearching: true,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
TUIGlobal?.hideKeyboard?.();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tui-search-input-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
|
||||
&-global {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tui-search-input {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: calc(100% - 20px);
|
||||
margin: 10px;
|
||||
background: #ededed;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
|
||||
&-main {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
caret-color: #007aff;
|
||||
font-size: 14px;
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-left,
|
||||
&-right {
|
||||
display: flex;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
padding: 0 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tui-search-input-container-h5 {
|
||||
.tui-search-input-h5 {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.tui-search-input-cancel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #007aff;
|
||||
font-size: 16px;
|
||||
padding: 7px 10px 7px 3px;
|
||||
font-family: "PingFang SC", sans-serif;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
TUIKit/components/TUISearch/search-more/index.ts
Normal file
3
TUIKit/components/TUISearch/search-more/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SearchMore from './index.vue';
|
||||
|
||||
export default SearchMore;
|
||||
162
TUIKit/components/TUISearch/search-more/index.vue
Normal file
162
TUIKit/components/TUISearch/search-more/index.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div
|
||||
ref="searchMoreRef"
|
||||
:class="['tui-search-more', !isPC && 'tui-search-more-h5']"
|
||||
>
|
||||
<div
|
||||
class="more"
|
||||
@click="toggleMore()"
|
||||
>
|
||||
<Icon
|
||||
class="more-icon"
|
||||
:file="searchMoreSVG"
|
||||
:width="isPC ? '28px' : '34px'"
|
||||
:height="isPC ? '28px' : '34px'"
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
v-if="isListShow"
|
||||
class="tui-search-more-list"
|
||||
>
|
||||
<li
|
||||
v-for="(extension, index) in extensionList"
|
||||
:key="index"
|
||||
class="list-item"
|
||||
@click="handleMenu(extension)"
|
||||
>
|
||||
<Icon
|
||||
v-if="extension.icon"
|
||||
class="list-item-icon"
|
||||
:file="extension.icon"
|
||||
/>
|
||||
<div class="list-item-title">
|
||||
{{ extension.text }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from '../../../adapter-vue';
|
||||
import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
|
||||
import TUICore, { ExtensionInfo, TUIConstants } from '@tencentcloud/tui-core';
|
||||
import { outsideClick } from '@tencentcloud/universal-api';
|
||||
import Icon from '../../common/Icon.vue';
|
||||
import searchMoreSVG from '../../../assets/icon/search-more.svg';
|
||||
import { isPC, isUniFrameWork } from '../../../utils/env';
|
||||
|
||||
const props = defineProps({
|
||||
searchType: {
|
||||
type: String,
|
||||
default: 'global', // "global" / "conversation"
|
||||
validator(value: string) {
|
||||
return ['global', 'conversation'].includes(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const searchMoreRef = ref<HTMLElement | null>();
|
||||
const isListShow = ref<boolean>(false);
|
||||
const toggleMore = () => {
|
||||
isListShow.value = !isListShow.value;
|
||||
if (!isUniFrameWork && isListShow.value) {
|
||||
outsideClick.listen({
|
||||
domRefs: searchMoreRef.value,
|
||||
handler: closeSearchMore,
|
||||
});
|
||||
}
|
||||
};
|
||||
const extensionList = ref<ExtensionInfo[]>([]);
|
||||
|
||||
const handleMenu = (item: ExtensionInfo) => {
|
||||
const { listener = { onClicked: () => { } } } = item;
|
||||
listener?.onClicked?.(item);
|
||||
toggleMore();
|
||||
};
|
||||
|
||||
const closeSearchMore = () => {
|
||||
isListShow.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// extensions
|
||||
extensionList.value = [
|
||||
...TUICore.getExtensionList(TUIConstants.TUISearch.EXTENSION.SEARCH_MORE.EXT_ID),
|
||||
];
|
||||
// hide conversation header
|
||||
TUICore.callService({
|
||||
serviceName: TUIConstants.TUIConversation.SERVICE.NAME,
|
||||
method: TUIConstants.TUIConversation.SERVICE.METHOD.HIDE_CONVERSATION_HEADER,
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => isListShow.value,
|
||||
() => {
|
||||
if (isListShow.value) {
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
|
||||
isSearching: false,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tui-search-more {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.more {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&-list {
|
||||
margin: 10px 0;
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
right: 6px;
|
||||
top: 20px;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 0;
|
||||
box-shadow: rgba(0,0,0,0.16) 0 3px 6px, rgba(0,0,0,0.23) 0 3px 6px;
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
|
||||
&-icon {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
text-wrap: nowrap;
|
||||
word-break: keep-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tui-search-more-h5{
|
||||
.more{
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
2
TUIKit/components/TUISearch/search-result/index.ts
Normal file
2
TUIKit/components/TUISearch/search-result/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import SearchResult from './index.vue';
|
||||
export default SearchResult;
|
||||
566
TUIKit/components/TUISearch/search-result/index.vue
Normal file
566
TUIKit/components/TUISearch/search-result/index.vue
Normal file
@@ -0,0 +1,566 @@
|
||||
<template>
|
||||
<SearchResultLoading
|
||||
v-if="isLoading"
|
||||
:class="['search-result-loading', !isPC && 'search-result-loading-h5']"
|
||||
/>
|
||||
<SearchResultDefault
|
||||
v-else-if="isSearchDefaultShow"
|
||||
:class="['search-result-default', !isPC && 'search-result-default-h5']"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:class="[
|
||||
'tui-search-result',
|
||||
!isPC && 'tui-search-result-h5',
|
||||
isPC && isResultDetailShow && 'tui-search-result-with-border',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="props.searchType !== 'conversation' && (isPC || !isResultDetailShow)"
|
||||
class="tui-search-result-main"
|
||||
>
|
||||
<div class="tui-search-result-list">
|
||||
<div
|
||||
v-for="result in searchResult"
|
||||
:key="result.key"
|
||||
class="tui-search-result-list-item"
|
||||
>
|
||||
<div
|
||||
v-if="props.searchType === 'global'"
|
||||
class="header"
|
||||
>
|
||||
{{ TUITranslateService.t(`TUISearch.${result.label}`) }}
|
||||
</div>
|
||||
<div class="list">
|
||||
<div
|
||||
v-for="item in result.list"
|
||||
:key="item.conversation.conversationID"
|
||||
:class="[generateListItemClass(item)]"
|
||||
>
|
||||
<SearchResultItem
|
||||
v-if="result.key === 'contact' || result.key === 'group' || item.conversation"
|
||||
:listItem="item"
|
||||
:type="result.key"
|
||||
displayType="info"
|
||||
:keywordList="keywordList"
|
||||
@showResultDetail="showResultDetail"
|
||||
@navigateToChatPosition="navigateToChatPosition"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentSearchTabKey === 'all' || result.cursor"
|
||||
class="more"
|
||||
@click="getMoreResult(result)"
|
||||
>
|
||||
<Icon
|
||||
class="more-icon"
|
||||
:file="searchIcon"
|
||||
width="12px"
|
||||
height="12px"
|
||||
/>
|
||||
<div class="more-text">
|
||||
<span>{{ TUITranslateService.t("TUISearch.查看更多") }}</span>
|
||||
<span>{{ TUITranslateService.t(`TUISearch.${result.label}`) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isResultDetailShow || props.searchType === 'conversation'"
|
||||
:class="[
|
||||
'tui-search-result-detail',
|
||||
props.searchType === 'conversation' && 'tui-search-result-in-conversation',
|
||||
]"
|
||||
>
|
||||
<SearchResultLoading
|
||||
v-if="isSearchDetailLoading"
|
||||
:class="['search-result-loading', !isPC && 'search-result-loading-h5']"
|
||||
/>
|
||||
<div
|
||||
v-if="!isSearchDetailLoading && isResultDetailShow && props.searchType !== 'conversation'"
|
||||
class="tui-search-message-header"
|
||||
>
|
||||
<div class="header-content">
|
||||
<div class="header-content-count normal">
|
||||
<span>{{ searchConversationMessageTotalCount }}</span>
|
||||
<span>{{ TUITranslateService.t("TUISearch.条与") }}</span>
|
||||
</div>
|
||||
<div class="header-content-keyword">
|
||||
<span
|
||||
v-for="(keyword, index) in keywordList"
|
||||
:key="index"
|
||||
>
|
||||
<span class="normal">"</span>
|
||||
<span class="highlight">{{ keyword }}</span>
|
||||
<span class="normal">"</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="header-content-type normal">
|
||||
<span>{{
|
||||
TUITranslateService.t("TUISearch.相关的")
|
||||
}}</span>
|
||||
<span>{{
|
||||
TUITranslateService.t(
|
||||
`TUISearch.${currentSearchTabKey === "allMessage"
|
||||
? "结果"
|
||||
: searchMessageTypeList[currentSearchTabKey].label
|
||||
}`
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="header-enter"
|
||||
@click="enterConversation({ conversationID: currentSearchConversationID })"
|
||||
>
|
||||
<span>{{ TUITranslateService.t("TUISearch.进入聊天") }}</span>
|
||||
<Icon
|
||||
class="enter-icon"
|
||||
:file="enterIcon"
|
||||
width="14px"
|
||||
height="14px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isSearchDetailLoading &&
|
||||
searchConversationMessageList &&
|
||||
searchConversationMessageList[0]
|
||||
"
|
||||
class="tui-search-message-list"
|
||||
>
|
||||
<template
|
||||
v-if="props.searchType === 'global' ||
|
||||
(currentSearchTabKey !== 'imageMessage' && currentSearchTabKey !== 'fileMessage')
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="item in searchConversationMessageList"
|
||||
:key="generateVueRenderKey(item.ID)"
|
||||
:class="['list-item']"
|
||||
>
|
||||
<SearchResultItem
|
||||
:listItem="item"
|
||||
:listItemContent="item.getMessageContent()"
|
||||
:type="currentSearchTabKey"
|
||||
:displayType="generateResultItemDisplayType()"
|
||||
:keywordList="keywordList"
|
||||
@showResultDetail="showResultDetail"
|
||||
@navigateToChatPosition="navigateToChatPosition"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Search within a conversation - messages such as files, pictures, and videos need to be displayed in groups according to time -->
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="group in searchConversationResultGroupByDate"
|
||||
:key="generateVueRenderKey(group.date)"
|
||||
:class="['list-group', currentSearchTabKey === 'fileMessage'? 'list-group-file' : 'list-group-image']"
|
||||
>
|
||||
<div :class="['list-group-date']">
|
||||
{{ group.date }}
|
||||
</div>
|
||||
<div
|
||||
v-for="item in group.list"
|
||||
:key="generateVueRenderKey(item.ID)"
|
||||
:class="['list-group-item']"
|
||||
>
|
||||
<SearchResultItem
|
||||
:listItem="item"
|
||||
:listItemContent="item.getMessageContent()"
|
||||
:type="currentSearchTabKey"
|
||||
:displayType="generateResultItemDisplayType()"
|
||||
:keywordList="keywordList"
|
||||
@showResultDetail="showResultDetail"
|
||||
@navigateToChatPosition="navigateToChatPosition"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="searchConversationResult && searchConversationResult.cursor"
|
||||
class="more"
|
||||
@click="getMoreResultInConversation"
|
||||
>
|
||||
<Icon
|
||||
class="more-icon"
|
||||
:file="searchIcon"
|
||||
width="12px"
|
||||
height="12px"
|
||||
/>
|
||||
<div class="more-text">
|
||||
{{ TUITranslateService.t("TUISearch.查看更多历史记录") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, onMounted, onUnmounted } from '../../../adapter-vue';
|
||||
import {
|
||||
TUITranslateService,
|
||||
TUIConversationService,
|
||||
TUIStore,
|
||||
StoreName,
|
||||
IMessageModel,
|
||||
SearchCloudMessagesParams,
|
||||
} from '@tencentcloud/chat-uikit-engine';
|
||||
import { TUIGlobal } from '@tencentcloud/universal-api';
|
||||
import SearchResultItem from './search-result-item/index.vue';
|
||||
import SearchResultDefault from './search-result-default/index.vue';
|
||||
import SearchResultLoading from './search-result-loading/index.vue';
|
||||
import { searchMessageTypeList, searchMessageTypeDefault } from '../search-type-list';
|
||||
import Icon from '../../common/Icon.vue';
|
||||
import searchIcon from '../../../assets/icon/search.svg';
|
||||
import enterIcon from '../../../assets/icon/right-icon.svg';
|
||||
import {
|
||||
searchCloudMessages,
|
||||
enterConversation,
|
||||
generateSearchResultYMD,
|
||||
debounce,
|
||||
} from '../utils';
|
||||
import { enableSampleTaskStatus } from '../../../utils/enableSampleTaskStatus';
|
||||
import { isPC, isUniFrameWork } from '../../../utils/env';
|
||||
import { SEARCH_TYPE, ISearchInputValue, ISearchMessageType, ISearchMessageTime } from '../type';
|
||||
import { ISearchCloudMessageResult, ISearchResultListItem } from '../../../interface';
|
||||
|
||||
const props = defineProps({
|
||||
searchType: {
|
||||
type: String,
|
||||
default: 'global', // "global" / "conversation"
|
||||
validator(value: string) {
|
||||
return ['global', 'conversation'].includes(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// search params
|
||||
const keywordList = ref<string[]>([]);
|
||||
const messageTypeList = ref<string | string[]>(
|
||||
searchMessageTypeDefault[props.searchType as SEARCH_TYPE]?.value as string[],
|
||||
);
|
||||
const timePosition = ref<number>(0);
|
||||
const timePeriod = ref<number>(0);
|
||||
// Search by "and" after splitting the whole string by space
|
||||
// For example: enter "111 222", search for messages with both 111 and 222, and also include messages that strictly search for "111 222"
|
||||
const keywordListMatchType = ref<string>('and');
|
||||
|
||||
// current search tab key
|
||||
const currentSearchTabKey = ref<string>(
|
||||
searchMessageTypeDefault[props.searchType as SEARCH_TYPE]?.key,
|
||||
);
|
||||
|
||||
// search results all
|
||||
const searchResult = ref<{
|
||||
[propsName: string]: { key: string; label: string; list: ISearchResultListItem[]; cursor: string | null };
|
||||
}>({});
|
||||
const searchAllMessageList = ref<ISearchResultListItem[]>([]);
|
||||
const searchAllMessageTotalCount = ref<number>(0);
|
||||
|
||||
// search results detail
|
||||
const currentSearchConversationID = ref<string>('');
|
||||
const searchConversationResult = ref<ISearchCloudMessageResult>();
|
||||
const searchConversationMessageList = ref<IMessageModel[]>([]);
|
||||
const searchConversationMessageTotalCount = ref<number>();
|
||||
|
||||
// search results for file messages/image and video messages, grouped by timeline
|
||||
const searchConversationResultGroupByDate = ref<
|
||||
Array<{ date: string; list: IMessageModel[] }>
|
||||
>([]);
|
||||
|
||||
// ui display control
|
||||
const isResultDetailShow = ref<boolean>(false);
|
||||
const isLoading = ref<boolean>(false);
|
||||
const isSearchDetailLoading = ref<boolean>(false);
|
||||
const isSearchDefaultShow = computed((): boolean => {
|
||||
if (isLoading.value) {
|
||||
return false;
|
||||
}
|
||||
if (props.searchType === 'global') {
|
||||
if (!keywordList?.value?.length || Object?.keys(searchResult.value)?.length) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (searchConversationMessageList?.value?.length) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function onCurrentConversationIDUpdate(id: string) {
|
||||
props.searchType === 'conversation' && (currentSearchConversationID.value = id);
|
||||
}
|
||||
|
||||
function onCurrentSearchInputValueUpdate(obj: ISearchInputValue) {
|
||||
if (obj?.searchType === props?.searchType) {
|
||||
keywordList.value = obj?.value ? obj.value.trim().split(/\s+/) : [];
|
||||
}
|
||||
}
|
||||
|
||||
function onCurrentSearchMessageTypeUpdate(typeObject: ISearchMessageType) {
|
||||
if (typeObject?.searchType === props?.searchType) {
|
||||
currentSearchTabKey.value
|
||||
= typeObject?.value?.key || searchMessageTypeDefault[props.searchType as SEARCH_TYPE]?.key;
|
||||
messageTypeList.value
|
||||
= typeObject?.value?.value
|
||||
|| searchMessageTypeDefault[props.searchType as SEARCH_TYPE]?.value;
|
||||
}
|
||||
}
|
||||
|
||||
function onCurrentSearchMessageTimeUpdate(timeObject: ISearchMessageTime) {
|
||||
if (timeObject?.searchType === props?.searchType) {
|
||||
timePosition.value = timeObject?.value?.value?.timePosition;
|
||||
timePeriod.value = timeObject?.value?.value?.timePeriod;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
TUIStore.watch(StoreName.CONV, {
|
||||
currentConversationID: onCurrentConversationIDUpdate,
|
||||
});
|
||||
TUIStore.watch(StoreName.SEARCH, {
|
||||
currentSearchInputValue: onCurrentSearchInputValueUpdate,
|
||||
currentSearchMessageType: onCurrentSearchMessageTypeUpdate,
|
||||
currentSearchMessageTime: onCurrentSearchMessageTimeUpdate,
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
TUIStore.unwatch(StoreName.CONV, {
|
||||
currentConversationID: onCurrentConversationIDUpdate,
|
||||
});
|
||||
TUIStore.unwatch(StoreName.SEARCH, {
|
||||
currentSearchInputValue: onCurrentSearchInputValueUpdate,
|
||||
currentSearchMessageType: onCurrentSearchMessageTypeUpdate,
|
||||
currentSearchMessageTime: onCurrentSearchMessageTimeUpdate,
|
||||
});
|
||||
});
|
||||
|
||||
const setMessageSearchResultList = (option?: { conversationID?: string | undefined; cursor?: string | undefined }) => {
|
||||
searchCloudMessages(
|
||||
{
|
||||
keywordList: keywordList?.value?.length ? keywordList.value : undefined,
|
||||
messageTypeList: typeof messageTypeList.value === 'string' ? [messageTypeList.value] : messageTypeList.value,
|
||||
timePosition: timePosition.value,
|
||||
timePeriod: timePeriod.value,
|
||||
conversationID: option?.conversationID || undefined,
|
||||
cursor: option?.cursor || undefined,
|
||||
keywordListMatchType: keywordListMatchType.value,
|
||||
} as SearchCloudMessagesParams,
|
||||
)
|
||||
.then((res: { data: ISearchCloudMessageResult }) => {
|
||||
enableSampleTaskStatus('searchCloudMessage');
|
||||
if (!option?.conversationID) {
|
||||
option?.cursor
|
||||
? (searchAllMessageList.value = [
|
||||
...searchAllMessageList.value,
|
||||
...res.data.searchResultList,
|
||||
])
|
||||
: (searchAllMessageList.value = res?.data?.searchResultList);
|
||||
searchAllMessageTotalCount.value = res?.data?.totalCount;
|
||||
const key = currentSearchTabKey.value === 'all' ? 'allMessage' : currentSearchTabKey.value;
|
||||
if (
|
||||
searchAllMessageList?.value?.length
|
||||
&& currentSearchTabKey.value !== 'contact'
|
||||
&& currentSearchTabKey.value !== 'group'
|
||||
) {
|
||||
searchResult.value = Object.assign({}, searchResult.value, {
|
||||
[key]: {
|
||||
key,
|
||||
label: searchMessageTypeList[key].label,
|
||||
list: currentSearchTabKey.value === 'all'
|
||||
? searchAllMessageList?.value?.slice(0, 3)
|
||||
: searchAllMessageList?.value,
|
||||
cursor: res?.data?.cursor || null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
delete searchResult?.value[key];
|
||||
}
|
||||
} else {
|
||||
searchConversationResult.value = res?.data;
|
||||
option?.cursor
|
||||
? (searchConversationMessageList.value = [
|
||||
...searchConversationMessageList.value,
|
||||
...(res?.data?.searchResultList[0]?.messageList as IMessageModel[]),
|
||||
])
|
||||
: (searchConversationMessageList.value = res?.data?.searchResultList[0]?.messageList);
|
||||
searchConversationMessageTotalCount.value = res?.data?.totalCount;
|
||||
if (
|
||||
props?.searchType === 'conversation'
|
||||
&& (currentSearchTabKey.value === 'fileMessage'
|
||||
|| currentSearchTabKey.value === 'imageMessage')
|
||||
) {
|
||||
searchConversationResultGroupByDate.value = groupResultListByDate(
|
||||
searchConversationMessageList.value,
|
||||
);
|
||||
} else {
|
||||
searchConversationResultGroupByDate.value = [];
|
||||
}
|
||||
}
|
||||
isLoading.value = false;
|
||||
isSearchDetailLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const setMessageSearchResultListDebounce = debounce(setMessageSearchResultList, 500);
|
||||
|
||||
const resetSearchResult = () => {
|
||||
searchResult.value = {};
|
||||
searchConversationResult.value = {} as ISearchCloudMessageResult;
|
||||
searchConversationMessageList.value = [];
|
||||
searchConversationResultGroupByDate.value = [];
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [keywordList.value, currentSearchTabKey.value, timePosition.value, timePeriod.value],
|
||||
(newValue, oldValue) => {
|
||||
if (newValue === oldValue) {
|
||||
return;
|
||||
}
|
||||
// Global search must have keywords, but search in conversation can be without keywords
|
||||
if (!keywordList?.value?.length && props?.searchType === 'global') {
|
||||
resetSearchResult();
|
||||
return;
|
||||
}
|
||||
isLoading.value = true;
|
||||
if (props.searchType === 'conversation') {
|
||||
resetSearchResult();
|
||||
setMessageSearchResultList({
|
||||
conversationID: currentSearchConversationID.value,
|
||||
});
|
||||
} else {
|
||||
if (oldValue && oldValue[1] === 'all' && newValue && newValue[1] === 'allMessage') {
|
||||
searchResult?.value['allMessage']?.list
|
||||
&& (searchResult.value['allMessage'].list = searchAllMessageList?.value);
|
||||
Object?.keys(searchResult?.value)?.forEach((key: string) => {
|
||||
if (key !== 'allMessage') {
|
||||
delete searchResult?.value[key];
|
||||
}
|
||||
});
|
||||
isLoading.value = false;
|
||||
return;
|
||||
} else {
|
||||
isResultDetailShow.value = false;
|
||||
resetSearchResult();
|
||||
}
|
||||
setMessageSearchResultListDebounce();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const getMoreResult = (result: { key: string; label: string; list: ISearchResultListItem[]; cursor: string | null }) => {
|
||||
if (currentSearchTabKey.value === 'all') {
|
||||
// View more at this time: Switch to the result corresponding to the corresponding result to display the full search results of its type
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageType', {
|
||||
value: searchMessageTypeList[result.key],
|
||||
searchType: props.searchType,
|
||||
});
|
||||
} else {
|
||||
// View more results for a single category: Use the cursor as the search start position to pull the next part of the results
|
||||
setMessageSearchResultList({ cursor: result?.cursor || undefined });
|
||||
}
|
||||
};
|
||||
|
||||
const getMoreResultInConversation = () => {
|
||||
setMessageSearchResultList({
|
||||
cursor: searchConversationResult?.value?.cursor,
|
||||
conversationID: currentSearchConversationID?.value,
|
||||
});
|
||||
};
|
||||
|
||||
const showResultDetail = (isShow: boolean, targetType?: string, targetResult?: IMessageModel | ISearchResultListItem) => {
|
||||
isResultDetailShow.value = isShow;
|
||||
if (targetType) {
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageType', {
|
||||
value: searchMessageTypeList[targetType],
|
||||
searchType: props.searchType,
|
||||
});
|
||||
}
|
||||
currentSearchConversationID.value = (targetResult as ISearchResultListItem)?.conversation?.conversationID || '';
|
||||
searchConversationMessageTotalCount.value = (targetResult as ISearchResultListItem)?.messageCount;
|
||||
if (targetResult) {
|
||||
isSearchDetailLoading.value = true;
|
||||
setMessageSearchResultListDebounce({
|
||||
conversationID: currentSearchConversationID.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const generateListItemClass = (item: ISearchResultListItem): string[] => {
|
||||
return currentSearchConversationID.value === item?.conversation?.conversationID
|
||||
? ['list-item', 'list-item-selected']
|
||||
: ['list-item'];
|
||||
};
|
||||
|
||||
const generateResultItemDisplayType = (): string => {
|
||||
if (props.searchType === 'conversation' && currentSearchTabKey.value === 'fileMessage') {
|
||||
return 'file';
|
||||
} else if (props.searchType === 'conversation' && currentSearchTabKey.value === 'imageMessage') {
|
||||
return 'image';
|
||||
} else if (isPC) {
|
||||
return 'bubble';
|
||||
} else {
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const groupResultListByDate = (
|
||||
messageList: IMessageModel[],
|
||||
): Array<{ date: string; list: IMessageModel[] }> => {
|
||||
const result: Array<{ date: string; list: IMessageModel[] }> = [];
|
||||
if (!messageList?.length) {
|
||||
return result;
|
||||
} else if (messageList?.length === 1) {
|
||||
result.push({ date: generateSearchResultYMD(messageList[0]?.time), list: messageList });
|
||||
return result;
|
||||
}
|
||||
let prevYMD = '';
|
||||
let currYMD = '';
|
||||
for (let i = 0; i < messageList?.length; i++) {
|
||||
currYMD = generateSearchResultYMD(messageList[i]?.time);
|
||||
if (prevYMD !== currYMD) {
|
||||
result.push({ date: currYMD, list: [messageList[i]] });
|
||||
} else {
|
||||
result[result?.length - 1]?.list?.push(messageList[i]);
|
||||
}
|
||||
prevYMD = currYMD;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const navigateToChatPosition = (message: IMessageModel) => {
|
||||
if (props.searchType === 'global') {
|
||||
// Global search, close the search container
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
|
||||
isSearching: false,
|
||||
searchType: props.searchType,
|
||||
});
|
||||
// switch conversation
|
||||
TUIConversationService.switchConversation(message?.conversationID).then(() => {
|
||||
TUIStore.update(StoreName.CHAT, 'messageSource', message);
|
||||
isUniFrameWork && TUIGlobal?.navigateTo({
|
||||
url: '/TUIKit/components/TUIChat/index',
|
||||
});
|
||||
});
|
||||
} else if (props.searchType === 'conversation') {
|
||||
// Search in conversation, close the search container
|
||||
TUIStore.update(StoreName.SEARCH, 'isShowInConversationSearch', false);
|
||||
TUIStore.update(StoreName.CHAT, 'messageSource', message);
|
||||
isUniFrameWork && TUIGlobal?.navigateBack();
|
||||
}
|
||||
};
|
||||
|
||||
const generateVueRenderKey = (value: string): string => {
|
||||
return `${currentSearchTabKey}-${value}`;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped src="./style/index.scss"></style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import SearchResultDefault from './index.vue';
|
||||
export default SearchResultDefault;
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div :class="['search-result-default', !isPC && 'search-result-default-h5']">
|
||||
<div class="search-result-default-main">
|
||||
<Icon
|
||||
:file="SearchDefaultIcon"
|
||||
width="88px"
|
||||
height="75px"
|
||||
/>
|
||||
<div class="default-text">
|
||||
{{ TUITranslateService.t("TUISearch.暂无搜索结果") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
|
||||
import { isPC } from '../../../../utils/env';
|
||||
import Icon from '../../../common/Icon.vue';
|
||||
import SearchDefaultIcon from '../../../../assets/icon/search-default.svg';
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.search-result-default {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-h5 {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
&-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.default-text {
|
||||
font-family: "PingFang SC", sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,211 @@
|
||||
<!-- Used to display the search results of [Contacts]/[Groups]/[All Conversations], which is a display of user/group/conversation dimensions -->
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'search-result-list-item',
|
||||
!isPC && 'search-result-list-item-h5',
|
||||
'search-result-list-item-' + displayType,
|
||||
isHovering && 'hover-' + displayType,
|
||||
]"
|
||||
@click="onResultItemClicked"
|
||||
@mouseenter="setHoverStatus(true)"
|
||||
@mouseleave="setHoverStatus(false)"
|
||||
>
|
||||
<div
|
||||
v-if="displayType === 'info' || displayType === 'bubble'"
|
||||
:class="[displayType]"
|
||||
>
|
||||
<div :class="displayType + '-left'">
|
||||
<img
|
||||
:class="displayType + '-left-avatar'"
|
||||
:src="avatarForShow || ''"
|
||||
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
|
||||
>
|
||||
</div>
|
||||
<div :class="[displayType + '-main']">
|
||||
<div :class="[displayType + '-main-name']">
|
||||
{{ nameForShow }}
|
||||
</div>
|
||||
<div :class="[displayType + '-main-content']">
|
||||
<MessageAbstractText
|
||||
v-if="displayType === 'info' || listItem.type === TYPES.MSG_TEXT"
|
||||
:content="contentForShow"
|
||||
:highlightType="displayType === 'info' ? 'font' : 'background'"
|
||||
:displayType="displayType"
|
||||
/>
|
||||
<MessageAbstractFile
|
||||
v-else-if="listItem.type === TYPES.MSG_FILE"
|
||||
:contentText="contentForShow"
|
||||
:messageContent="listItemContent"
|
||||
:displayType="displayType"
|
||||
/>
|
||||
<div v-else-if="listItem.type === TYPES.MSG_IMAGE" />
|
||||
<div v-else-if="listItem.type === TYPES.MSG_VIDEO" />
|
||||
<MessageAbstractCustom
|
||||
v-else-if="listItem.type === TYPES.MSG_CUSTOM"
|
||||
:contentText="contentForShow"
|
||||
:message="listItem"
|
||||
:messageContent="listItemContent"
|
||||
/>
|
||||
<div v-else>
|
||||
{{ getMessageAbstractType(listItem) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="displayType + '-right'">
|
||||
<div :class="displayType + '-right-time'">
|
||||
{{ timeForShow }}
|
||||
</div>
|
||||
<div
|
||||
v-if="displayType === 'bubble' && isHovering"
|
||||
:class="displayType + '-right-to'"
|
||||
@click.stop="navigateToChatPosition"
|
||||
>
|
||||
{{ TUITranslateService.t("TUISearch.定位到聊天位置") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="displayType === 'file'"
|
||||
:class="[displayType]"
|
||||
>
|
||||
<div :class="[displayType + '-header']">
|
||||
<img
|
||||
:class="displayType + '-header-avatar'"
|
||||
:src="avatarForShow"
|
||||
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
|
||||
>
|
||||
<div :class="[displayType + '-header-name']">
|
||||
{{ nameForShow }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isHovering"
|
||||
:class="displayType + '-header-to'"
|
||||
@click.stop="navigateToChatPosition"
|
||||
>
|
||||
{{ TUITranslateService.t("TUISearch.定位到聊天位置") }}
|
||||
</div>
|
||||
<div :class="displayType + '-header-time'">
|
||||
{{ timeForShow }}
|
||||
</div>
|
||||
</div>
|
||||
<div :class="[displayType + '-main-content']">
|
||||
<MessageAbstractFile
|
||||
:contentText="contentForShow"
|
||||
:messageContent="listItemContent"
|
||||
displayType="bubble"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="displayType === 'image'"
|
||||
:class="[displayType]"
|
||||
>
|
||||
<div
|
||||
class="image-container"
|
||||
@click.stop="navigateToChatPosition"
|
||||
>
|
||||
<MessageAbstractImage
|
||||
v-if="listItem.type === TYPES.MSG_IMAGE"
|
||||
:messageContent="listItemContent"
|
||||
/>
|
||||
<MessageAbstractVideo
|
||||
v-else-if="listItem.type === TYPES.MSG_VIDEO"
|
||||
:messageContent="listItemContent"
|
||||
/>
|
||||
<div
|
||||
v-if="isHovering"
|
||||
class="image-container-hover"
|
||||
>
|
||||
<div class="image-container-hover-text">
|
||||
{{ TUITranslateService.t("TUISearch.定位到聊天位置") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import TUIChatEngine, { TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-engine';
|
||||
import { ref, watchEffect, withDefaults } from '../../../../adapter-vue';
|
||||
import MessageAbstractText from './message-abstract/message-abstract-text.vue';
|
||||
import MessageAbstractFile from './message-abstract/message-abstract-file.vue';
|
||||
import MessageAbstractCustom from './message-abstract/message-abstract-custom.vue';
|
||||
import MessageAbstractImage from './message-abstract/message-abstract-image.vue';
|
||||
import MessageAbstractVideo from './message-abstract/message-abstract-video.vue';
|
||||
import {
|
||||
generateSearchResultShowName,
|
||||
generateSearchResultAvatar,
|
||||
generateSearchResultShowContent,
|
||||
generateSearchResultTime,
|
||||
enterConversation,
|
||||
} from '../../utils';
|
||||
import { messageTypeAbstractMap, searchResultItemDisplayTypeValues, searchMessageTypeValues, IHighlightContent } from '../../type';
|
||||
import { ISearchResultListItem } from '../../../../interface';
|
||||
import { isPC } from '../../../../utils/env';
|
||||
interface IProps {
|
||||
listItem: IMessageModel | ISearchResultListItem;
|
||||
listItemContent?: Record<string, unknown>;
|
||||
type: searchMessageTypeValues;
|
||||
displayType: searchResultItemDisplayTypeValues;
|
||||
keywordList: string[];
|
||||
}
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
listItem: () => ({}) as IMessageModel | ISearchResultListItem,
|
||||
listItemContent: () => ({}) as Record<string, unknown>,
|
||||
type: 'allMessage',
|
||||
displayType: 'info',
|
||||
keywordList: () => ([]) as string[],
|
||||
});
|
||||
|
||||
const emits = defineEmits(['showResultDetail', 'navigateToChatPosition']);
|
||||
const TYPES = ref(TUIChatEngine.TYPES);
|
||||
|
||||
const avatarForShow = ref<string>('');
|
||||
const nameForShow = ref<string>('');
|
||||
const contentForShow = ref<IHighlightContent[]>([]);
|
||||
const timeForShow = ref<string>('');
|
||||
|
||||
const isHovering = ref<boolean>(false);
|
||||
|
||||
watchEffect(() => {
|
||||
avatarForShow.value = generateSearchResultAvatar(props.listItem);
|
||||
nameForShow.value = generateSearchResultShowName(props.listItem, props?.listItemContent);
|
||||
contentForShow.value = generateSearchResultShowContent(
|
||||
props.listItem,
|
||||
props.type,
|
||||
props.keywordList as string[],
|
||||
props?.displayType === 'info',
|
||||
);
|
||||
timeForShow.value = (props.listItem as IMessageModel)?.time
|
||||
? generateSearchResultTime((props.listItem as IMessageModel)?.time * 1000)
|
||||
: '';
|
||||
});
|
||||
|
||||
const onResultItemClicked = () => {
|
||||
if (props.type === 'contact' || props.type === 'group') {
|
||||
enterConversation(props.listItem as IMessageModel);
|
||||
} else {
|
||||
if (props.displayType === 'info' && !(props.listItem as IMessageModel)?.ID) {
|
||||
emits('showResultDetail', true, props.type, props.listItem);
|
||||
} else {
|
||||
navigateToChatPosition();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setHoverStatus = (status: boolean) => {
|
||||
isHovering.value = status;
|
||||
};
|
||||
|
||||
const navigateToChatPosition = () => {
|
||||
emits('navigateToChatPosition', props.listItem);
|
||||
};
|
||||
|
||||
const getMessageAbstractType = (message: IMessageModel | ISearchResultListItem) => {
|
||||
return message?.type
|
||||
? TUITranslateService.t(`TUISearch.${messageTypeAbstractMap[message.type]}`)
|
||||
: TUITranslateService.t(`TUISearch.[合并消息]`);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped src="./style/index.scss"></style>
|
||||
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<!-- Custom message keyword keyword search description, so here only a few custom messages that need to display highlighted description type are parsed -->
|
||||
<div
|
||||
:class="['message-abstract-custom']"
|
||||
@click.capture.stop
|
||||
>
|
||||
<template v-if="businessID === CHAT_MSG_CUSTOM_TYPE.SERVICE">
|
||||
<div :class="['service']">
|
||||
<h1 :class="['service-header']">
|
||||
<label :class="['service-header-title']">{{ extensionJSON.title }}</label>
|
||||
<a
|
||||
v-if="extensionJSON.hyperlinks_text"
|
||||
:class="['service-header-link', 'link']"
|
||||
:href="extensionJSON.hyperlinks_text.value"
|
||||
target="view_window"
|
||||
>
|
||||
{{ extensionJSON.hyperlinks_text.key }}
|
||||
</a>
|
||||
</h1>
|
||||
<ul
|
||||
v-if="extensionJSON.item && extensionJSON.item.length > 0"
|
||||
:class="['service-list']"
|
||||
>
|
||||
<li
|
||||
v-for="(item, index) in extensionJSON.item"
|
||||
:key="index"
|
||||
:class="['service-list-item']"
|
||||
>
|
||||
<a
|
||||
v-if="isUrl(item.value)"
|
||||
:class="['service-list-item-link', 'link']"
|
||||
:href="item.value"
|
||||
target="view_window"
|
||||
>{{ item.key }}</a>
|
||||
<p
|
||||
v-else
|
||||
:class="['service-list-item-key']"
|
||||
>
|
||||
{{ item.key }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div :class="['service-description', 'description']">
|
||||
<span
|
||||
v-for="(contentItem, index) in descriptionForShow"
|
||||
:key="index"
|
||||
:class="[(contentItem && contentItem.isHighlight) ? 'highlight' : 'normal']"
|
||||
>
|
||||
{{ contentItem.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="businessID === CHAT_MSG_CUSTOM_TYPE.EVALUATE">
|
||||
<div class="evaluate">
|
||||
<div :class="['evaluate-description', 'description']">
|
||||
<span
|
||||
v-for="(contentItem, index) in descriptionForShow"
|
||||
:key="index"
|
||||
:class="[(contentItem && contentItem.isHighlight) ? 'highlight' : 'normal']"
|
||||
>
|
||||
{{ contentItem.text }}
|
||||
</span>
|
||||
</div>
|
||||
<ul
|
||||
v-if="extensionJSON.score"
|
||||
class="evaluate-list"
|
||||
>
|
||||
<li
|
||||
v-for="(item, index) in Math.max(extensionJSON.score, 0)"
|
||||
:key="index"
|
||||
class="evaluate-list-item"
|
||||
>
|
||||
<Icon
|
||||
:file="star"
|
||||
class="file-icon"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<article>{{ extensionJSON.comment }}</article>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="businessID === CHAT_MSG_CUSTOM_TYPE.ORDER">
|
||||
<div class="order">
|
||||
<img
|
||||
class="order-image"
|
||||
:src="extensionJSON.imageUrl"
|
||||
alt=""
|
||||
>
|
||||
<main class="order-main">
|
||||
<h1 class="order-main-title">
|
||||
{{ extensionJSON.title }}
|
||||
</h1>
|
||||
<div :class="['order-main-description', 'description']">
|
||||
<span
|
||||
v-for="(contentItem, index) in descriptionForShow"
|
||||
:key="index"
|
||||
:class="[(contentItem && contentItem.isHighlight) ? 'highlight' : 'normal']"
|
||||
>
|
||||
{{ contentItem.text }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="order-main-price">{{ extensionJSON.price }}</span>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="businessID === CHAT_MSG_CUSTOM_TYPE.LINK">
|
||||
<div class="text-link">
|
||||
<div :class="['text-link-description', 'description']">
|
||||
<p>{{ extensionJSON.text }}</p>
|
||||
</div>
|
||||
<a
|
||||
:class="['link']"
|
||||
:href="extensionJSON.link"
|
||||
target="view_window"
|
||||
>{{
|
||||
TUITranslateService.t("message.custom.查看详情>>")
|
||||
}}</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ defaultMessageContent }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-engine';
|
||||
import { ref, computed, withDefaults } from '../../../../../adapter-vue';
|
||||
import { CHAT_MSG_CUSTOM_TYPE } from '../../../../../constant';
|
||||
import { JSONToObject, isUrl } from '../../../../../utils/index';
|
||||
import Icon from '../../../../common/Icon.vue';
|
||||
import star from '../../../../../assets/icon/star-light.png';
|
||||
import { IHighlightContent } from '../../../type';
|
||||
import { ISearchResultListItem } from '../../../../../interface';
|
||||
interface IProps {
|
||||
contentText: IHighlightContent[];
|
||||
message: IMessageModel | ISearchResultListItem;
|
||||
messageContent: Record<string, unknown> | undefined;
|
||||
}
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
contentText: () => ([]) as IHighlightContent[],
|
||||
message: () => ({}) as IMessageModel,
|
||||
messageContent: () => ({}) as Record<string, unknown>,
|
||||
});
|
||||
|
||||
const custom = ref<{ data?: string; description?: string; extension?: string }>(
|
||||
(props?.message as IMessageModel)?.payload,
|
||||
);
|
||||
const extensionJSON = computed(() => custom?.value?.data ? JSONToObject(custom.value.data) : custom?.value?.data);
|
||||
const businessID = computed(() => extensionJSON?.value?.businessID);
|
||||
const descriptionForShow = ref<Array<{ text: string; isHighlight: boolean }>>(props?.contentText);
|
||||
const defaultMessageContent = ref<string>(props?.messageContent?.custom as string || '[自定义消息]');
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "../../../../../assets/styles/common";
|
||||
|
||||
.message-abstract-custom {
|
||||
.service {
|
||||
.service-header {
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.service-list {
|
||||
.service-list-item {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.evaluate {
|
||||
.evaluate-list {
|
||||
padding: 5px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.evaluate-item {
|
||||
padding: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order {
|
||||
display: flex;
|
||||
|
||||
.order-main {
|
||||
padding-left: 5px;
|
||||
|
||||
.order-main-title {
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.order-main-description {
|
||||
font-family: PingFangSC-Regular, sans-serif;
|
||||
width: 145px;
|
||||
line-height: 17px;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 6px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.order-main-price {
|
||||
font-family: PingFangSC-Regular, sans-serif;
|
||||
line-height: 25px;
|
||||
color: #ff7201;
|
||||
}
|
||||
}
|
||||
|
||||
.order-img {
|
||||
width: 67px;
|
||||
height: 67px;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 14px;
|
||||
color: #679ce1;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
|
||||
.highlight {
|
||||
background-color: #007aff33;
|
||||
}
|
||||
|
||||
.normal {
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div :class="['message-abstract-file', `message-abstract-file-${displayType}`]">
|
||||
<div :class="['message-abstract-file-left']">
|
||||
<img
|
||||
:class="['message-abstract-file-left-icon']"
|
||||
:src="typeIcon.iconSrc"
|
||||
>
|
||||
</div>
|
||||
<div :class="['message-abstract-file-main']">
|
||||
<div :class="['message-abstract-file-main-name']">
|
||||
<span
|
||||
v-for="(contentItem, index) in contentText"
|
||||
:key="index"
|
||||
:class="[(contentItem && contentItem.isHighlight) ? 'highlight' : 'normal']"
|
||||
>
|
||||
{{ contentItem.text }}
|
||||
</span>
|
||||
</div>
|
||||
<div :class="['message-abstract-file-main-size']">
|
||||
{{ fileSize }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, withDefaults } from '../../../../../adapter-vue';
|
||||
import { IHighlightContent } from '../../../type';
|
||||
interface IProps {
|
||||
contentText: Array<IHighlightContent>;
|
||||
messageContent: Record<string, unknown> | undefined;
|
||||
displayType: 'bubble' | 'info';
|
||||
}
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
contentText: () => ([]) as Array<IHighlightContent>,
|
||||
messageContent: () => ({}) as Record<string, unknown>,
|
||||
displayType: 'bubble',
|
||||
});
|
||||
|
||||
const contentText = ref<Array<{ text: string; isHighlight: boolean }>>(props.contentText);
|
||||
const typeIcon = computed(() => {
|
||||
const fileUrl = props?.messageContent?.url as string;
|
||||
const index = fileUrl?.lastIndexOf('.');
|
||||
const type = fileUrl?.substring(index + 1);
|
||||
return handleFileIconForShow(type);
|
||||
});
|
||||
const fileSize = computed(() => props?.messageContent?.size);
|
||||
const handleFileIconForShow = (type: string) => {
|
||||
const urlBase = 'https://web.sdk.qcloud.com/component/TUIKit/assets/file-';
|
||||
const fileTypes = [
|
||||
'image',
|
||||
'pdf',
|
||||
'text',
|
||||
'ppt',
|
||||
'presentation',
|
||||
'sheet',
|
||||
'zip',
|
||||
'word',
|
||||
'video',
|
||||
'unknown',
|
||||
];
|
||||
let url = '';
|
||||
let iconType = '';
|
||||
fileTypes?.forEach((typeName: string) => {
|
||||
if (type?.includes(typeName)) {
|
||||
url = urlBase + typeName + '.svg';
|
||||
iconType = typeName;
|
||||
}
|
||||
});
|
||||
return {
|
||||
iconSrc: url ? url : urlBase + 'unknown.svg',
|
||||
iconType: iconType ? iconType : 'unknown',
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../../../../assets/styles/common";
|
||||
|
||||
.message-abstract-file {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-left {
|
||||
width: 42px;
|
||||
height: 32px;
|
||||
|
||||
&-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
&-name {
|
||||
width: 100%;
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.highlight {
|
||||
background-color: #007aff33;
|
||||
}
|
||||
|
||||
.normal {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
&-size {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-bubble {
|
||||
background-color: #f1f1f1;
|
||||
|
||||
.message-abstract-file-main {
|
||||
.message-abstract-file-main-name {
|
||||
color: #1f2329;
|
||||
|
||||
.normal {
|
||||
color: #1f2329;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-file {
|
||||
margin: 8px 10px 5px;
|
||||
padding: 10px;
|
||||
background-color: #f1f1f1;
|
||||
height: 51px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div :class="['message-abstract-image-container']">
|
||||
<img
|
||||
:class="['message-abstract-image']"
|
||||
:src="imageUrl"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { withDefaults, computed } from '../../../../../adapter-vue';
|
||||
import { IImageMessageContent } from '../../../../../interface';
|
||||
interface IProps {
|
||||
messageContent: Record<string, unknown> | IImageMessageContent | undefined;
|
||||
}
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
messageContent: () => ({}) as IImageMessageContent,
|
||||
});
|
||||
|
||||
const imageUrl = computed<string>(() => (props.messageContent as IImageMessageContent).url || '');
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "../../../../../assets/styles/common";
|
||||
|
||||
.message-abstract-image-container {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
|
||||
.message-abstract-image {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'message-abstract-text',
|
||||
`message-abstract-text-${highlightType}`,
|
||||
`message-abstract-text-${displayType}`,
|
||||
]"
|
||||
>
|
||||
<span
|
||||
v-for="(contentItem, index) in contentText"
|
||||
:key="index"
|
||||
:class="[(contentItem && contentItem.isHighlight) ? 'highlight' : 'normal']"
|
||||
>
|
||||
{{ transformTextWithKeysToEmojiNames(contentItem.text) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, withDefaults } from '../../../../../adapter-vue';
|
||||
import { transformTextWithKeysToEmojiNames } from '../../../../TUIChat/emoji-config';
|
||||
import { IHighlightContent } from '../../../type';
|
||||
|
||||
interface IProps {
|
||||
content: IHighlightContent[];
|
||||
highlightType: 'font' | 'background';
|
||||
displayType: 'info' | 'bubble';
|
||||
}
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
content: () => ([]) as IHighlightContent[],
|
||||
highlightType: 'font',
|
||||
displayType: 'info',
|
||||
});
|
||||
|
||||
const contentText = ref<Array<{ text: string; isHighlight: boolean }>>(props.content);
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "../../../../../assets/styles/common";
|
||||
|
||||
.message-abstract-text {
|
||||
justify-content: flex-start;
|
||||
|
||||
&-font {
|
||||
color: #999;
|
||||
|
||||
.highlight {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.normal {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
&-background {
|
||||
color: #1f2329;
|
||||
|
||||
.highlight {
|
||||
background-color: #007aff33;
|
||||
}
|
||||
|
||||
.normal {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
|
||||
.highlight {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.normal {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-bubble {
|
||||
font-size: 14px;
|
||||
|
||||
.highlight {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.normal {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div :class="['message-abstract-video']">
|
||||
<div class="message-abstract-video-box">
|
||||
<img
|
||||
:src="videoUrl"
|
||||
:class="['video-snapshot']"
|
||||
>
|
||||
<Icon
|
||||
:file="playIcon"
|
||||
class="video-play"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from '../../../../../adapter-vue';
|
||||
import Icon from '../../../../common/Icon.vue';
|
||||
import playIcon from '../../../../../assets/icon/video-play.png';
|
||||
import { IVideoMessageContent } from '../../../../../interface';
|
||||
interface IProps {
|
||||
messageContent: Record<string, unknown> | IVideoMessageContent | undefined;
|
||||
}
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
messageContent: () => ({}) as IVideoMessageContent,
|
||||
});
|
||||
const videoUrl = computed<string>(() => {
|
||||
return (props.messageContent as IVideoMessageContent).snapshotUrl || (props.messageContent as IVideoMessageContent).url;
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "../../../../../assets/styles/common";
|
||||
|
||||
.message-abstract-video {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
|
||||
&-box {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
|
||||
.video-snapshot {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.video-play {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
.search-result-list-item-h5 {
|
||||
padding: 10px 0;
|
||||
border-radius: 0;
|
||||
|
||||
.bubble {
|
||||
.bubble-left {
|
||||
.bubble-left-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.bubble-main {
|
||||
.bubble-main-name {
|
||||
color: #333;
|
||||
font-family: "PingFang SC", sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@import '../../../../../assets/styles/common';
|
||||
@import './web';
|
||||
@import './h5';
|
||||
@@ -0,0 +1,262 @@
|
||||
.search-result-list-item {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
|
||||
&-image {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
box-sizing: content-box;
|
||||
border: 1px solid #f1f1f1;
|
||||
padding: 0;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
&-file {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
&-left {
|
||||
&-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
|
||||
&-name,
|
||||
&-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-name {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&-content {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
|
||||
.highlight {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.normal {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
width: fit-content;
|
||||
|
||||
&-time {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bubble {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
&-left {
|
||||
&-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 8px;
|
||||
|
||||
&-name {
|
||||
max-width: 100%;
|
||||
width: fit-content;
|
||||
padding-bottom: 4px;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
letter-spacing: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-content {
|
||||
max-width: 100%;
|
||||
width: fit-content;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
letter-spacing: 0;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
background: #eff0f1;
|
||||
border-radius: 0 10px 10px;
|
||||
|
||||
.highlight {
|
||||
background-color: #007aff33;
|
||||
}
|
||||
|
||||
.normal {
|
||||
color: #1f2329;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
&-time {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-to {
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #007aff;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
&-header {
|
||||
flex: 1;
|
||||
padding: 10px 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-name,
|
||||
&-time,
|
||||
&-to {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-to {
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
color: #007aff;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
padding-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&-main-content {
|
||||
padding: 10px;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
box-sizing: content-box;
|
||||
|
||||
.image-container {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.image-container-hover {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 40%;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
|
||||
.image-container-hover-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hover-info {
|
||||
border-radius: 5px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.hover-bubble {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import SearchResultLoading from './index.vue';
|
||||
export default SearchResultLoading;
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div :class="['search-result-loading', !isPC && 'search-result-loading-h5']">
|
||||
<Loading
|
||||
width="40px"
|
||||
height="40px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Loading from '../../../common/Loading/index.vue';
|
||||
import { isPC } from '../../../../utils/env';
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.search-result-loading {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-h5 {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
67
TUIKit/components/TUISearch/search-result/style/h5.scss
Normal file
67
TUIKit/components/TUISearch/search-result/style/h5.scss
Normal file
@@ -0,0 +1,67 @@
|
||||
.tui-search-result-h5 {
|
||||
background-color: #f4f4f4;
|
||||
|
||||
.tui-search-result-main {
|
||||
background-color: #f4f4f4;
|
||||
|
||||
.tui-search-result-list {
|
||||
.tui-search-result-list-item {
|
||||
background-color: #fff;
|
||||
padding: 0 10px 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tui-search-result-detail {
|
||||
background-color: #f4f4f4;
|
||||
border: none;
|
||||
|
||||
.list-item {
|
||||
margin: 0 10px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.list-group-date {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.list-group-image {
|
||||
.list-group-item {
|
||||
.search-result-list-item-h5 {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-file {
|
||||
.list-group-item {
|
||||
background-color: #fff;
|
||||
padding: 0 10px;
|
||||
border-bottom: 1px solid #f4f4f4;
|
||||
|
||||
.search-result-list-item-h5 {
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-loading,
|
||||
.search-result-default {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-h5 {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@import '../../../../assets/styles/common';
|
||||
@import './web';
|
||||
@import './h5';
|
||||
180
TUIKit/components/TUISearch/search-result/style/web.scss
Normal file
180
TUIKit/components/TUISearch/search-result/style/web.scss
Normal file
@@ -0,0 +1,180 @@
|
||||
.tui-search-result {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
|
||||
&-with-border {
|
||||
border-top: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
&-detail {
|
||||
width: 360px;
|
||||
overflow-y: hidden;
|
||||
border-left: 1px solid rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.tui-search-message-header {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
place-content: space-between space-between;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
|
||||
.header-content-count {
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-content-keyword {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-content-type {
|
||||
width: 110px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.normal {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #007aff;
|
||||
}
|
||||
}
|
||||
|
||||
.header-enter {
|
||||
margin-left: 10px;
|
||||
width: 70px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.tui-search-message-list {
|
||||
overflow-y: auto;
|
||||
|
||||
.list-item {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
&.list-group-image {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
.list-group-item {
|
||||
width: 111px;
|
||||
height: 111px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-date {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-family: "PingFang SC", sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0;
|
||||
text-align: left;
|
||||
padding: 10px 10px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.more {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 14px;
|
||||
padding: 8px 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
.more-text {
|
||||
padding-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #007aff;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
width: 350px;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.tui-search-result-list {
|
||||
&-item {
|
||||
.header {
|
||||
font-size: 14px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list-item-selected {
|
||||
background: #f2f2f2;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.more {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 14px;
|
||||
|
||||
// padding: 8px 0;
|
||||
padding-top: 10px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
.more-text {
|
||||
padding-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #007aff;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tui-search-result-in-conversation {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
45
TUIKit/components/TUISearch/search-time-list.ts
Normal file
45
TUIKit/components/TUISearch/search-time-list.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// const oneDay = 24 * 60 * 60 * 1000;
|
||||
const oneDay = 24 * 60 * 60;
|
||||
export const searchMessageTimeList: {
|
||||
[propsName: string]: {
|
||||
key: string;
|
||||
label: string;
|
||||
value: { timePosition: number; timePeriod: number };
|
||||
};
|
||||
} = {
|
||||
all: {
|
||||
key: 'all',
|
||||
label: '全部',
|
||||
value: {
|
||||
timePosition: 0,
|
||||
timePeriod: 0,
|
||||
},
|
||||
},
|
||||
oneDay: {
|
||||
key: 'today',
|
||||
label: '今天',
|
||||
value: {
|
||||
timePosition: 0,
|
||||
timePeriod: oneDay,
|
||||
},
|
||||
},
|
||||
threeDay: {
|
||||
key: 'threeDays',
|
||||
label: '近三天',
|
||||
value: {
|
||||
timePosition: 0,
|
||||
timePeriod: 3 * oneDay,
|
||||
},
|
||||
},
|
||||
sevenDay: {
|
||||
key: 'sevenDays',
|
||||
label: '近七天',
|
||||
value: {
|
||||
timePosition: 0,
|
||||
timePeriod: 7 * oneDay,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const searchMessageTimeKeys = Object.keys(searchMessageTimeList);
|
||||
export const searchMessageTimeDefault = searchMessageTimeList['all'];
|
||||
86
TUIKit/components/TUISearch/search-type-list.ts
Normal file
86
TUIKit/components/TUISearch/search-type-list.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import TUIChatEngine from '@tencentcloud/chat-uikit-engine';
|
||||
export interface ISearchMessageTypeList {
|
||||
[propsName: string]: {
|
||||
key: string;
|
||||
label: string;
|
||||
value: any[] | string;
|
||||
};
|
||||
}
|
||||
export const allMessageTypeList = [
|
||||
TUIChatEngine.TYPES.MSG_TEXT,
|
||||
TUIChatEngine.TYPES.MSG_FILE,
|
||||
TUIChatEngine.TYPES.MSG_IMAGE,
|
||||
TUIChatEngine.TYPES.MSG_VIDEO,
|
||||
TUIChatEngine.TYPES.MSG_AUDIO,
|
||||
TUIChatEngine.TYPES.MSG_LOCATION,
|
||||
TUIChatEngine.TYPES.MSG_CUSTOM,
|
||||
TUIChatEngine.TYPES.MSG_MERGER,
|
||||
];
|
||||
export const searchMessageTypeList: ISearchMessageTypeList = {
|
||||
allMessage: {
|
||||
key: 'allMessage',
|
||||
label: '全部',
|
||||
value: allMessageTypeList,
|
||||
},
|
||||
textMessage: {
|
||||
key: 'textMessage',
|
||||
label: '文本',
|
||||
value: [TUIChatEngine.TYPES.MSG_TEXT],
|
||||
},
|
||||
fileMessage: {
|
||||
key: 'fileMessage',
|
||||
label: '文件',
|
||||
value: [TUIChatEngine.TYPES.MSG_FILE],
|
||||
},
|
||||
imageMessage: {
|
||||
key: 'imageMessage',
|
||||
label: '图片/视频',
|
||||
value: [TUIChatEngine.TYPES.MSG_IMAGE, TUIChatEngine.TYPES.MSG_VIDEO],
|
||||
},
|
||||
otherMessage: {
|
||||
key: 'otherMessage',
|
||||
label: '其他',
|
||||
value: [
|
||||
TUIChatEngine.TYPES.MSG_AUDIO,
|
||||
TUIChatEngine.TYPES.MSG_LOCATION,
|
||||
TUIChatEngine.TYPES.MSG_CUSTOM,
|
||||
TUIChatEngine.TYPES.MSG_MERGER,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const searchMessageTypeKeys = Object.keys(searchMessageTypeList);
|
||||
export const searchMessageSingleTypeKeys = Object.keys(searchMessageTypeList).filter(
|
||||
(key: string) => key !== 'all',
|
||||
);
|
||||
export const searchInGlobalDefaultType = searchMessageTypeList['allMessage'];
|
||||
export const searchInConversationDefaultType = searchMessageTypeList['textMessage'];
|
||||
export const searchMessageTypeDefault = {
|
||||
global: searchInGlobalDefaultType,
|
||||
conversation: searchInConversationDefaultType,
|
||||
};
|
||||
|
||||
// Global search type key list
|
||||
export const globalSearchTypeKeys = ['allMessage', 'textMessage', 'fileMessage', 'otherMessage'];
|
||||
// Global search type list
|
||||
export const globalSearchTypeList = Object.keys(searchMessageTypeList)
|
||||
.filter((key: string) => globalSearchTypeKeys?.includes(key))
|
||||
.reduce((obj: ISearchMessageTypeList, key: string) => {
|
||||
obj[key] = searchMessageTypeList[key];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
// Search type key list in session
|
||||
export const conversationSearchTypeKeys = [
|
||||
'textMessage',
|
||||
'fileMessage',
|
||||
'imageMessage',
|
||||
'otherMessage',
|
||||
];
|
||||
// Search type list in session
|
||||
export const conversationSearchTypeList = Object.keys(searchMessageTypeList)
|
||||
.filter((key: string) => conversationSearchTypeKeys?.includes(key))
|
||||
.reduce((obj: ISearchMessageTypeList, key: string) => {
|
||||
obj[key] = searchMessageTypeList[key];
|
||||
return obj;
|
||||
}, {});
|
||||
73
TUIKit/components/TUISearch/server.ts
Normal file
73
TUIKit/components/TUISearch/server.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
|
||||
import {
|
||||
TUIUserService,
|
||||
TUIGroupService,
|
||||
TUIFriendService,
|
||||
TUIStore,
|
||||
StoreName,
|
||||
} from '@tencentcloud/chat-uikit-engine';
|
||||
import { isUniFrameWork } from '../../utils/env';
|
||||
import { TUIGlobal } from '@tencentcloud/universal-api';
|
||||
|
||||
export default class TUISearchServer {
|
||||
constructor() {
|
||||
TUICore.registerService(TUIConstants.TUISearch.SERVICE.NAME, this);
|
||||
TUICore.registerExtension(TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID, this);
|
||||
}
|
||||
|
||||
public onCall(method: string, params: { [propsName: string]: string }) {
|
||||
switch (method) {
|
||||
case TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_GROUP:
|
||||
return this.searchGroup(params?.groupID);
|
||||
case TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_USER:
|
||||
return this.searchUser(params?.userID);
|
||||
case TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_FRIEND:
|
||||
return this.searchFriend(params?.userID);
|
||||
case TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_GROUP_MEMBER:
|
||||
return this.searchGroupMember(params?.groupID, params?.userID);
|
||||
}
|
||||
}
|
||||
|
||||
public onGetExtension(extensionID: string) {
|
||||
if (extensionID === TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID) {
|
||||
const list: any[] = [];
|
||||
const searchExtension = {
|
||||
weight: 3000,
|
||||
text: '搜索',
|
||||
icon: 'https://web.sdk.qcloud.com/component/TUIKit/assets/message-search.svg',
|
||||
data: {
|
||||
name: 'search',
|
||||
},
|
||||
listener: {
|
||||
onClicked: () => {
|
||||
TUIStore.update(StoreName.SEARCH, 'isShowInConversationSearch', true);
|
||||
isUniFrameWork && TUIGlobal?.navigateTo({
|
||||
url: '/TUIKit/components/TUISearch/index',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
list.push(searchExtension);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
public async searchFriend(userID: string) {
|
||||
return TUIFriendService.getFriendProfile({ userIDList: [userID] });
|
||||
}
|
||||
|
||||
public async searchUser(userID: string) {
|
||||
return TUIUserService.getUserProfile({ userIDList: [userID] });
|
||||
}
|
||||
|
||||
public async searchGroup(groupID: string) {
|
||||
return TUIGroupService.searchGroupByID(groupID);
|
||||
}
|
||||
|
||||
public async searchGroupMember(groupID: string, userID: string) {
|
||||
return TUIGroupService.getGroupMemberProfile({
|
||||
groupID,
|
||||
userIDList: [userID],
|
||||
});
|
||||
}
|
||||
}
|
||||
39
TUIKit/components/TUISearch/style/h5.scss
Normal file
39
TUIKit/components/TUISearch/style/h5.scss
Normal file
@@ -0,0 +1,39 @@
|
||||
.tui-search-h5 {
|
||||
&-full-screen {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&-global,
|
||||
&-conversation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-conversation {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.tui-search-main-conversation {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.search-input,
|
||||
.search-container,
|
||||
.search-result {
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
3
TUIKit/components/TUISearch/style/index.scss
Normal file
3
TUIKit/components/TUISearch/style/index.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@import '../../../assets/styles/common';
|
||||
@import './web';
|
||||
@import './h5';
|
||||
28
TUIKit/components/TUISearch/style/web.scss
Normal file
28
TUIKit/components/TUISearch/style/web.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
.tui-search {
|
||||
&-main-global {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
&-main-conversation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 10px 0 rgba(2,16,43,0.15);
|
||||
}
|
||||
|
||||
.tui-search-global {
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tui-search-conversation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
76
TUIKit/components/TUISearch/type.ts
Normal file
76
TUIKit/components/TUISearch/type.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import TUIChatEngine from '@tencentcloud/chat-uikit-engine';
|
||||
|
||||
export type SEARCH_TYPE = 'global' | 'conversation';
|
||||
|
||||
// Message search result type display summary
|
||||
// Unsupported type:
|
||||
// TYPES.MSG_FACE / TYPES.MSG_GRP_TIP / TYPES.MSG_GRP_SYS_NOTICE
|
||||
export const messageTypeAbstractMap: Record<string, string> = {
|
||||
[TUIChatEngine.TYPES.MSG_TEXT]: '[文本]',
|
||||
[TUIChatEngine.TYPES.MSG_IMAGE]: '[图片]',
|
||||
[TUIChatEngine.TYPES.MSG_AUDIO]: '[语音]',
|
||||
[TUIChatEngine.TYPES.MSG_VIDEO]: '[视频]',
|
||||
[TUIChatEngine.TYPES.MSG_FILE]: '[文件]',
|
||||
[TUIChatEngine.TYPES.MSG_CUSTOM]: '[自定义消息]',
|
||||
[TUIChatEngine.TYPES.MSG_SYSTEM]: '[系统消息]',
|
||||
[TUIChatEngine.TYPES.MSG_MERGER]: '[合并消息]',
|
||||
[TUIChatEngine.TYPES.MSG_LOCATION]: '[位置消息]',
|
||||
};
|
||||
|
||||
export const searchResultItemDisplayType = {
|
||||
INFO: 'info', // Normal information flow display
|
||||
BUBBLE: 'bubble', // Message bubble display
|
||||
FILE: 'file', // File list type display
|
||||
IMAGE: 'image', // Picture collection display
|
||||
};
|
||||
|
||||
export type searchResultItemDisplayTypeKeys = keyof typeof searchResultItemDisplayType;
|
||||
export type searchResultItemDisplayTypeValues = typeof searchResultItemDisplayType[searchResultItemDisplayTypeKeys];
|
||||
|
||||
export const searchMessageType = {
|
||||
// CONTACT: "contact", // Contact search, not supported yet
|
||||
// GROUP: "group", // Group search, not supported yet
|
||||
ALL_MESSAGE: 'allMessage',
|
||||
TEXT_MESSAGE: 'textMessage',
|
||||
IMAGE_MESSAGE: 'imageMessage',
|
||||
FILE_MESSAGE: 'fileMessage',
|
||||
OTHER_MESSAGE: 'otherMessage',
|
||||
};
|
||||
|
||||
export type searchMessageTypeKeys = keyof typeof searchMessageType;
|
||||
export type searchMessageTypeValues = typeof searchMessageType[searchMessageTypeKeys];
|
||||
|
||||
export interface ISearchInputValue {
|
||||
value: string;
|
||||
searchType: SEARCH_TYPE;
|
||||
}
|
||||
export interface ISearchTypeTab {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string | string[];
|
||||
}
|
||||
export interface ISearchMessageType {
|
||||
value: ISearchTypeTab;
|
||||
searchType: SEARCH_TYPE;
|
||||
}
|
||||
export interface ISearchTimeTab {
|
||||
key: string;
|
||||
label: string;
|
||||
value: {
|
||||
timePosition: number;
|
||||
timePeriod: number;
|
||||
};
|
||||
}
|
||||
export interface ISearchMessageTime {
|
||||
value: ISearchTimeTab;
|
||||
searchType: SEARCH_TYPE;
|
||||
}
|
||||
export interface ISearchingStatus {
|
||||
isSearching: boolean;
|
||||
searchType: string;
|
||||
}
|
||||
|
||||
export interface IHighlightContent {
|
||||
text: string;
|
||||
isHighlight: boolean;
|
||||
}
|
||||
346
TUIKit/components/TUISearch/utils.ts
Normal file
346
TUIKit/components/TUISearch/utils.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import TUIChatEngine, {
|
||||
TUIFriendService,
|
||||
TUIConversationService,
|
||||
TUIGroupService,
|
||||
TUIChatService,
|
||||
TUITranslateService,
|
||||
SearchCloudMessagesParams,
|
||||
IGroupModel,
|
||||
TUIStore,
|
||||
StoreName,
|
||||
IMessageModel,
|
||||
} from '@tencentcloud/chat-uikit-engine';
|
||||
import { ISearchCloudMessageResult, IFriendType, ISearchResultListItem, IUserProfile } from '../../interface';
|
||||
import { searchMessageTypeList } from './search-type-list';
|
||||
import { Toast, TOAST_TYPE } from '../common/Toast/index';
|
||||
import { messageTypeAbstractMap } from './type';
|
||||
import { isUniFrameWork } from '../../utils/env';
|
||||
import { TUIGlobal } from '@tencentcloud/universal-api';
|
||||
|
||||
/**************************************
|
||||
* TUISearch search logic
|
||||
**************************************/
|
||||
|
||||
export const searchCloudMessages = (
|
||||
params: SearchCloudMessagesParams,
|
||||
): Promise<{ data: ISearchCloudMessageResult }> => {
|
||||
return TUIChatService.searchCloudMessages(params)
|
||||
.then((imResponse) => {
|
||||
return imResponse;
|
||||
})
|
||||
.catch((error) => {
|
||||
Toast({
|
||||
message: TUITranslateService.t('TUISearch.消息云端搜索失败:') + error?.message,
|
||||
type: TOAST_TYPE.ERROR,
|
||||
duration: 3000,
|
||||
});
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
export const searchFriends = (userIDList: any[]): Promise<any[]> => {
|
||||
// Only show users who are already friends
|
||||
return TUIFriendService.getFriendProfile({ userIDList })
|
||||
.then((imResponse) => {
|
||||
return imResponse;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('search user failed:', error?.message);
|
||||
Toast({
|
||||
message: TUITranslateService.t('TUISearch.查找联系人失败:') + error?.message,
|
||||
type: TOAST_TYPE.ERROR,
|
||||
duration: 1000,
|
||||
});
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
// Search all joined group chats
|
||||
export const searchGroups = (groupIDList: any[]): Promise<any[]> => {
|
||||
const promiseList: any[] = [];
|
||||
groupIDList.forEach((groupID: string) => {
|
||||
const promise = TUIGroupService.searchGroupByID(groupID)
|
||||
.then((imResponse) => {
|
||||
// Only show joined group chats
|
||||
if (imResponse?.data?.group?.isJoinedGroup) {
|
||||
return imResponse?.data?.group;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('search group failed:', error?.message);
|
||||
});
|
||||
promiseList.push(promise);
|
||||
});
|
||||
return Promise.all(promiseList)
|
||||
.then((imResponse) => {
|
||||
return imResponse.filter(x => x !== undefined);
|
||||
})
|
||||
.catch((error) => {
|
||||
Toast({
|
||||
message: TUITranslateService.t('TUISearch.查找群聊失败:') + error?.message,
|
||||
type: TOAST_TYPE.ERROR,
|
||||
duration: 1000,
|
||||
});
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
/**************************************
|
||||
* TUISearch interaction logic
|
||||
**************************************/
|
||||
// switch conversation
|
||||
export const enterConversation = (item: { conversationID?: string; groupID?: string; userID?: string }) => {
|
||||
const conversationID
|
||||
= item?.conversationID || (item?.groupID ? `GROUP${item?.groupID}` : `C2C${item?.userID}`);
|
||||
TUIConversationService.switchConversation(conversationID)
|
||||
.then(() => {
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
|
||||
isSearching: false,
|
||||
searchType: 'global',
|
||||
});
|
||||
TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
|
||||
value: '',
|
||||
searchType: 'global',
|
||||
});
|
||||
isUniFrameWork && TUIGlobal?.navigateTo({
|
||||
url: '/TUIKit/components/TUIChat/index',
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('switch conversation failed:', error?.message);
|
||||
Toast({
|
||||
message: TUITranslateService.t('TUISearch.进入会话失败'),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
duration: 1000,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**************************************
|
||||
* TUISearch UI display logic
|
||||
**************************************/
|
||||
export const generateSearchResultShowName = (result: IMessageModel | ISearchResultListItem | IGroupModel | IFriendType | IUserProfile, resultContent: Record<string, string>): string => {
|
||||
if (!result) {
|
||||
return '';
|
||||
}
|
||||
if ((result as IMessageModel).ID) {
|
||||
return resultContent?.showName;
|
||||
}
|
||||
if ((result as IGroupModel).groupID) {
|
||||
return (result as IGroupModel).name || (result as IGroupModel).groupID;
|
||||
}
|
||||
if ((result as IFriendType | IUserProfile).userID) {
|
||||
return (result as IFriendType).remark || (result as IUserProfile).nick || (result as IFriendType).userID || '';
|
||||
}
|
||||
if ((result as ISearchResultListItem).conversation?.conversationID) {
|
||||
if (typeof (result as ISearchResultListItem).conversation.getShowName === 'function') {
|
||||
return (result as ISearchResultListItem).conversation.getShowName();
|
||||
} else {
|
||||
return TUIStore.getConversationModel((result as ISearchResultListItem).conversation.conversationID)?.getShowName?.() || (result as ISearchResultListItem).conversation.conversationID;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const generateSearchResultAvatar = (result: IMessageModel | ISearchResultListItem | IGroupModel | IFriendType | IUserProfile): string => {
|
||||
if (!result) {
|
||||
return '';
|
||||
}
|
||||
if ((result as IMessageModel).ID) {
|
||||
return (result as IMessageModel).avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png';
|
||||
}
|
||||
if ((result as IGroupModel).groupID) {
|
||||
return (result as IGroupModel).avatar || `https://web.sdk.qcloud.com/im/assets/images/${(result as IGroupModel)?.type}.svg`;
|
||||
}
|
||||
if ((result as IUserProfile).userID) {
|
||||
return (result as IUserProfile).avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png';
|
||||
}
|
||||
if ((result as ISearchResultListItem)?.conversation?.conversationID) {
|
||||
if (typeof (result as ISearchResultListItem).conversation.getAvatar === 'function') {
|
||||
return (result as ISearchResultListItem).conversation?.getAvatar();
|
||||
} else {
|
||||
return TUIStore.getConversationModel((result as ISearchResultListItem).conversation.conversationID)?.getAvatar?.();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Generate the search results and display the content (including highlighting the keyword content matches)
|
||||
export const generateSearchResultShowContent = (
|
||||
result: IMessageModel | ISearchResultListItem | IGroupModel | IUserProfile,
|
||||
resultType: string,
|
||||
keywordList: any[],
|
||||
isTypeShow = true,
|
||||
): any[] => {
|
||||
if ((result as IGroupModel)?.groupID) {
|
||||
return [
|
||||
{ text: 'groupID: ', isHighlight: false },
|
||||
{ text: (result as IGroupModel).groupID, isHighlight: true },
|
||||
];
|
||||
}
|
||||
if ((result as IUserProfile)?.userID) {
|
||||
return [
|
||||
{ text: 'userID: ', isHighlight: false },
|
||||
{ text: (result as IUserProfile).userID, isHighlight: true },
|
||||
];
|
||||
}
|
||||
if ((result as ISearchResultListItem)?.conversation || (result as IMessageModel)?.flow) {
|
||||
if ((result as ISearchResultListItem)?.messageCount === 1 || (result as IMessageModel)?.flow) {
|
||||
// Single message summary display result:
|
||||
// Text message, display message content + keyword highlight
|
||||
// File type message, display [file] file name + keyword highlight
|
||||
// Custom type message, display [custom message] description + keyword highlight
|
||||
// Other types of messages, display [message type]
|
||||
const message: IMessageModel = (result as IMessageModel)?.flow
|
||||
? (result as IMessageModel)
|
||||
: (result as ISearchResultListItem)?.messageList[0];
|
||||
const text
|
||||
= message?.payload?.text || message?.payload?.fileName || message?.payload?.description;
|
||||
const abstract: any[] = [];
|
||||
if (message?.type && isTypeShow && message.type !== TUIChatEngine.TYPES.MSG_TEXT) {
|
||||
abstract.push({
|
||||
text: TUITranslateService.t(`TUISearch.${messageTypeAbstractMap[message.type]}`),
|
||||
isHighlight: false,
|
||||
});
|
||||
}
|
||||
abstract.push(...generateMessageContentHighlight(text, keywordList));
|
||||
return abstract;
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
text: `${(result as ISearchResultListItem)?.messageCount}${TUITranslateService.t(
|
||||
'TUISearch.条相关',
|
||||
)}${TUITranslateService.t(
|
||||
`TUISearch.${resultType === 'allMessage' ? '结果' : searchMessageTypeList[resultType]?.label
|
||||
}`,
|
||||
)}`,
|
||||
isHighlight: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// Parse the search message results [highlight keywords] position
|
||||
export const generateMessageContentHighlight = (
|
||||
content: string,
|
||||
keywordList: any[],
|
||||
): any[] => {
|
||||
if (!content || !keywordList || !keywordList.length) {
|
||||
return [{ text: content || '', isHighlight: false }];
|
||||
}
|
||||
// Get the start and end positions of all key matches
|
||||
const matches: any[] = [];
|
||||
for (let i = 0; i < keywordList.length; i++) {
|
||||
// Special character translation
|
||||
const substring = keywordList[i]?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(substring, 'gi'); // Global search and ignore case
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const start: number = match.index;
|
||||
const end: number = match.index + match[0].length - 1;
|
||||
matches.push([start, end]);
|
||||
}
|
||||
}
|
||||
// Merge repeated range results and arrange them in order of smallest arrival, for example, [[1,3],[2,4]] is merged into [[1,4]]
|
||||
const mergedRanges = [matches[0]];
|
||||
if (matches.length > 1) {
|
||||
matches.sort((a: any[], b: any[]) => a[0] - b[0]);
|
||||
// const mergedRanges = [matches[0]];
|
||||
for (let i = 1; i < matches.length; i++) {
|
||||
const currentRange = matches[i];
|
||||
const lastMergedRange = mergedRanges[mergedRanges.length - 1];
|
||||
// currentRange[0] - 1 is to handle the special case where [[1,2],[3,4]] can be merged into [[1,4]]
|
||||
if (currentRange[0] - 1 <= lastMergedRange[1]) {
|
||||
lastMergedRange[1] = Math.max(lastMergedRange[1], currentRange[1]);
|
||||
} else {
|
||||
mergedRanges.push(currentRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mergedRanges[0]) {
|
||||
return [{ text: content, isHighlight: false }];
|
||||
}
|
||||
// Split the original content string according to the highlight range and add highlight related identification fields
|
||||
const contentArray: any[] = [];
|
||||
let start = 0;
|
||||
for (let i = 0; i < mergedRanges.length; i++) {
|
||||
const str1 = content.substring(start, mergedRanges[i][0]);
|
||||
str1 && contentArray.push({ text: str1, isHighlight: false });
|
||||
const str2 = content.substring(mergedRanges[i][0], mergedRanges[i][1] + 1);
|
||||
str2 && contentArray.push({ text: str2, isHighlight: true });
|
||||
start = mergedRanges[i][1] + 1;
|
||||
}
|
||||
// Add the last string
|
||||
const lastStr = content.substring(start);
|
||||
lastStr && contentArray.push({ text: lastStr, isHighlight: false });
|
||||
return contentArray;
|
||||
};
|
||||
|
||||
// calculate timestamp
|
||||
export const generateSearchResultTime = (timestamp: number): string => {
|
||||
const todayZero = new Date().setHours(0, 0, 0, 0);
|
||||
const thisYear = new Date(new Date().getFullYear(), 0, 1, 0, 0, 0, 0).getTime();
|
||||
const target = new Date(timestamp);
|
||||
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const oneWeek = 7 * oneDay;
|
||||
const diff = todayZero - target.getTime();
|
||||
|
||||
function formatNum(num: number): string {
|
||||
return num < 10 ? '0' + num : num.toString();
|
||||
}
|
||||
|
||||
if (diff <= 0) {
|
||||
// today, only display hour:minute
|
||||
return `${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
|
||||
} else if (diff <= oneDay) {
|
||||
// yesterday, display yesterday:hour:minute
|
||||
return `${TUITranslateService.t('time.昨天')} ${formatNum(target.getHours())}:${formatNum(
|
||||
target.getMinutes(),
|
||||
)}`;
|
||||
} else if (diff <= oneWeek - oneDay) {
|
||||
// Within a week, display weekday hour:minute
|
||||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||||
const weekday = weekdays[target.getDay()];
|
||||
return `${TUITranslateService.t('time.' + weekday)} ${formatNum(target.getHours())}:${formatNum(
|
||||
target.getMinutes(),
|
||||
)}`;
|
||||
} else if (target.getTime() >= thisYear) {
|
||||
// Over a week, within this year, display mouth/day hour:minute
|
||||
return `${target.getMonth() + 1}/${target.getDate()} ${formatNum(
|
||||
target.getHours(),
|
||||
)}:${formatNum(target.getMinutes())}`;
|
||||
} else {
|
||||
// Not within this year, display year/mouth/day hour:minute
|
||||
return `${target.getFullYear()}/${target.getMonth() + 1}/${target.getDate()} ${formatNum(
|
||||
target.getHours(),
|
||||
)}:${formatNum(target.getMinutes())}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Calculated date functions
|
||||
export const generateSearchResultYMD = (timestamp: number): string => {
|
||||
const date = new Date(timestamp * 1000); // Convert timestamp to milliseconds
|
||||
const year = date.getFullYear();
|
||||
const month = ('0' + (date.getMonth() + 1)).slice(-2);
|
||||
const day = ('0' + date.getDate()).slice(-2);
|
||||
|
||||
return `${year}-${month}-${day}`; // Returns a string in year-month-day format
|
||||
};
|
||||
|
||||
export const debounce = <F extends (...args: any[]) => void>(func: F, waitFor: number) => {
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const debounced = (...args: Parameters<F>) => {
|
||||
if (timeout !== null) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
timeout = setTimeout(() => func(...args), waitFor);
|
||||
};
|
||||
|
||||
return debounced as (...args: Parameters<F>) => ReturnType<F>;
|
||||
};
|
||||
Reference in New Issue
Block a user