Compare commits

...

9 Commits

Author SHA1 Message Date
Phoenix
2c1ae41c3e feat(theme): 将主色调从#1890ff更改为#462AA0
统一修改多处UI组件的主色调,从蓝色(#1890ff)变更为紫色(#462AA0),以保持视觉一致性。同时优化了文件上传逻辑和滚动到底部功能。

refactor(dom): 提取滚动相关操作为工具函数
将滚动到底部逻辑封装为可复用的工具函数,并在多处调用位置进行替换,提高代码复用性。

fix(upload): 修复上传中文件点击打开问题
增加上传状态判断,避免在上传过程中点击文件时打开新窗口。

chore(deps): 更新依赖包版本
升级@types/node和watchpack等依赖包版本。
2025-05-28 11:29:13 +08:00
Phoenix
44a1dd0986 fix: 修复用户信息显示逻辑
- 在ForwardRecord.vue中,将用户ID的引用从item.user_id更改为item.erp_user_id,以确保正确显示用户信息。
2025-05-27 11:48:18 +08:00
Phoenix
8ce7d143ce 12 2025-05-27 11:43:58 +08:00
Phoenix
58b70f84d7 1 2025-05-27 11:42:34 +08:00
Phoenix
6d663d3d01 Merge branch 'main' of http://172.16.100.91:3000/scout666/chat-pc 2025-05-27 11:38:24 +08:00
Phoenix
b117765bdc fix: 修复用户信息处理和组件逻辑
- 在useSessionMenu.ts中,修正用户信息处理逻辑,使用正确的用户ID
- 在IndexSider.vue中,移除不必要的watch监听器,简化代码
- 在PanelContent.vue中,优化右键菜单逻辑,确保仅在特定条件下触发
2025-05-27 11:38:22 +08:00
1edb639ad9 Merge branch 'wyfMain-dev' 2025-05-27 11:24:42 +08:00
8ecee15180 完成聊天记录按日期搜索功能 2025-05-27 11:21:55 +08:00
efb410b657 再次重构从聊天app接入的按条件查询聊天记录组件,去除了按群成员查询、按日期查询的特异化,统一调用history接口,根据不同的场景处理参数,并处理空页面和分页等。处理不同交互场景下需要重置搜索条件的情况。目前按日期查询待接入,其他已完成 2025-05-26 18:57:02 +08:00
32 changed files with 604 additions and 237 deletions

View File

@ -883,8 +883,8 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@18.19.101':
resolution: {integrity: sha512-Ykg7fcE3+cOQlLUv2Ds3zil6DVjriGQaSN/kEpl5HQ3DIGM6W0F2n9+GkWV4bRt7KjLymgzNdTnSKCbFUUJ7Kw==}
'@types/node@18.19.103':
resolution: {integrity: sha512-hHTHp+sEz6SxFsp+SA+Tqrua3AbmlAw+Y//aEwdHrdZkYVRWdvWD3y5uPZ0flYOkgskaFWqZ/YGFm3FaFQ0pRw==}
'@types/node@18.19.99':
resolution: {integrity: sha512-tNGqoGjjI4vY5jfm3lnqgR6yS8wyT76SfsWefLWRyh/cEK4UHmPVyqHZdafI/SNu1PQzfo2JLBWfG8eMmD7KrQ==}
@ -3517,8 +3517,8 @@ packages:
warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
watchpack@2.4.3:
resolution: {integrity: sha512-adBYQLivcg1jbdKEJeqScJJFvgm4qY9+3tXw+jdG6lkVeqRJEtiQmSWjmth8GKmDZuX7sYM4YFxQsf0AzMfGGw==}
watchpack@2.4.4:
resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==}
engines: {node: '>=10.13.0'}
web-worker@1.5.0:
@ -3528,8 +3528,8 @@ packages:
resolution: {integrity: sha512-BCfKo2YkDe2ByqkEWe1Rw+zko4LsyS75LVr29C6xIrxAg9JHJ4pl8kaIZ396SUSNp6b4815dRZPSTAS8LlURRQ==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
webpack-sources@3.2.3:
resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
webpack-sources@3.3.0:
resolution: {integrity: sha512-77R0RDmJfj9dyv5p3bM5pOHa+X8/ZkO9c7kpDstigkC4nIDobadsfSGCwB4bKhMVxqAok8tajaoR8rirM7+VFQ==}
engines: {node: '>=10.13.0'}
webpack-virtual-modules@0.6.2:
@ -4228,7 +4228,7 @@ snapshots:
'@types/ms@2.1.0': {}
'@types/node@18.19.101':
'@types/node@18.19.103':
dependencies:
undici-types: 5.26.5
@ -5895,7 +5895,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
'@types/node': 18.19.101
'@types/node': 18.19.103
merge-stream: 2.0.0
supports-color: 8.1.1
@ -7244,7 +7244,7 @@ snapshots:
dependencies:
loose-envify: 1.4.0
watchpack@2.4.3:
watchpack@2.4.4:
dependencies:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
@ -7256,7 +7256,7 @@ snapshots:
deepmerge: 1.5.2
javascript-stringify: 1.6.0
webpack-sources@3.2.3: {}
webpack-sources@3.3.0: {}
webpack-virtual-modules@0.6.2: {}
@ -7284,8 +7284,8 @@ snapshots:
schema-utils: 4.3.2
tapable: 2.2.2
terser-webpack-plugin: 5.3.14(webpack@5.99.8)
watchpack: 2.4.3
webpack-sources: 3.2.3
watchpack: 2.4.4
webpack-sources: 3.3.0
transitivePeerDependencies:
- '@swc/core'
- esbuild

View File

@ -29,7 +29,7 @@ html {
// 黑色主题
html[theme-mode='dark'] {
--im-primary-color: #1890ff;
--im-primary-color: #462AA0;
--im-bg-color: #202124;
--line-border-color: rgb(255 255 255 / 9%);
--border-color: rgb(255 255 255 / 9%);

View File

@ -12,7 +12,7 @@
&:hover,
&.dropsize-resizing {
background-color: #1890ff;
background-color: #462AA0;
}
&.dropsize-line-top {

View File

@ -44,7 +44,7 @@
font-feature-settings: 'tnum';
position: absolute;
display: none;
color: #1890ff;
color: #462AA0;
text-align: center;
vertical-align: middle;
opacity: 0;
@ -177,7 +177,7 @@
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
background-color: #462AA0;
border-radius: 100%;
-webkit-transform: scale(0.75);
transform: scale(0.75);

View File

@ -143,6 +143,7 @@ const editorOption = {
},
//
source: function (searchTerm: string, renderList: any) {
console.log("source")
if (!props.members.length) {
return renderList([])
}

View File

@ -236,6 +236,8 @@ const handleModalConfirm = (closeLoading) => {
closeLoading()
window['$message'].error(err.message)
})
} else if (state.chatSettingOperateType == 'quit'){
//退
}
}
@ -988,7 +990,7 @@ const handleEditGroupNameConfirm = () => {
margin: 3px 0;
&:hover {
.nickname {
color: #1890ff;
color: #462AA0;
}
}
}

View File

@ -1,16 +1,17 @@
<template>
<div class="outer-layer search-by-condition-page">
<n-infinite-scroll style="height: 455px;" :distance="10">
<n-infinite-scroll style="height: 455px;" :distance="40" @load="loadMore">
<div class="root">
<div v-if="state.condition === 'dateTimePicker'" class="search-by-date">
<n-date-picker
<!-- <div v-if="state.condition === 'date'" class="search-by-date"> -->
<!-- <n-date-picker
:panel="true"
type="datetime"
:clearable="true"
:first-day-of-week="6"
:is-date-disabled="dateDisabled"
:actions="['clear', 'confirm']"
/>
@confirm="onDatePickConfirm"
/> -->
<!-- <tm-time-picker
:show="state.showMonthPicker"
:showDetail="{
@ -55,17 +56,24 @@
@getDArray="getDArray"
:showDefault="false"
></tm-calendar-view> -->
</div>
<!-- </div> -->
<div
class="search-by-condition-input-list"
v-if="
state.condition === 'imgAndVideo' ||
state.condition === 'file' ||
state.condition === 'link' ||
state.condition === 'member'
state.condition === 'member' ||
state.condition === 'all' ||
state.condition === 'date'
"
:style="{
padding: state.condition === 'imgAndVideo' ? '20px 38px' : ''
padding:
state.searchResultList.length > 0
? state.condition === 'imgAndVideo'
? '20px 38px'
: ''
: '0'
}"
>
<!-- <div
@ -117,7 +125,14 @@
padding: state.condition === 'imgAndVideo' ? '' : ''
}"
>
<div class="condition-result-member" v-if="state.condition === 'member'">
<div
class="condition-result-member"
v-if="
state.condition === 'member' ||
state.condition === 'all' ||
state.condition === 'date'
"
>
<searchItem
@click="toDialogueByMember(item)"
:searchResultKey="'search_by_member_condition'"
@ -216,7 +231,7 @@
<span
class="text-[14px] font-regular"
v-if="state.condition === 'file'"
style="color: #999999;"
style="color: #999999; flex-shrink: 0; margin: 0 0 0 20px;"
>
{{ item.dateTime }}
</span>
@ -236,7 +251,11 @@
<span class="text-[12px] font-regular" v-if="state.condition === 'file'">
{{ item?.nickname }}
</span>
<span class="text-[12px] font-regular" v-if="state.condition === 'file'">
<span
class="text-[12px] font-regular"
v-if="state.condition === 'file'"
style="flex-shrink: 0; margin: 0 0 0 20px;"
>
{{ item?.extra?.fileSize }}
</span>
</div>
@ -278,6 +297,10 @@
</div>
</teleport>
</div>
<div class="search-record-empty" v-if="state.searchResultList.length === 0">
<img src="@/assets/image/chatList/search-empty.png" alt="" />
<span>无内容</span>
</div>
</n-infinite-scroll>
</div>
</template>
@ -304,6 +327,8 @@ import { parseTime } from '@/utils/datetime'
import { fileFormatSize, fileSuffix } from '@/utils/strings'
import { NImage, NInfiniteScroll, NScrollbar, NIcon, NDatePicker } from 'naive-ui'
const emits = defineEmits(['clearSearchMemberByAlphabet', 'getDisabledDateArray'])
const dialogueStore = useDialogueStore()
//
const dialogueParams = reactive({
@ -318,6 +343,26 @@ const props = defineProps({
//
type: String,
default: ''
},
searchMemberItem: {
//
type: String,
default: ''
},
searchRecordByConditionText: {
//
type: String,
default: ''
},
selectedDateTime: {
//
type: [Number, null],
default: null
},
nowDateTime: {
//
type: Date,
default: new Date()
}
})
@ -338,7 +383,10 @@ const state = reactive({
cursor: 0, //
msg_type: 0, //
group_member_id: 0, //id
flatList: [] //
flatList: [], //
selectedDateTime: null, //
isLoadingChatRecord: false, //
hasNoMoreResults: false //
})
const videoContext = ref()
@ -409,6 +457,7 @@ const ServeQueryTalkDate = (month) => {
} else {
state.disabledDateArray = state.dArray
}
emits('getDisabledDateArray', state.disabledDateArray)
} else {
}
})
@ -416,16 +465,6 @@ const ServeQueryTalkDate = (month) => {
resp.catch(() => {})
}
//
const dateDisabled = (e) => {
const date = new Date(e)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const formattedDate = `${year}/${month}/${day}`
return state.disabledDateArray.includes(formattedDate)
}
//
const selectDate = async (e) => {
if (e == parseTime(state.nowDate, '{y}/{m}/{d}')) {
@ -518,8 +557,6 @@ const getDArray = (selectedMonth) => {
//
const inputSearchText = (e) => {
state.searchText = e
state.cursor = 0
queryAllSearch()
}
//
@ -546,28 +583,32 @@ const queryAllSearch = () => {
limit: 10, //
no_limit: '', //1
direction: 'up', //downup
start_time: '',
end_time: '',
start_time: state.selectedDateTime
? parseTime(new Date(state.selectedDateTime), '{y}-{m}-{d}')
: '',
end_time: state.selectedDateTime
? parseTime(new Date(state.selectedDateTime), '{y}-{m}-{d}')
: '',
group_member_user_id: state.group_member_id, //id
file_name: state.msg_type === 6 ? state.searchText : ''
file_name: props?.searchRecordByConditionText
}
console.log(params)
const resp = ServeFindTalkRecords(params)
console.log(resp)
resp.then(({ code, data }) => {
resp
.then(({ code, data }) => {
console.log(data)
if (code == 200) {
// cursor0searchResultList
let dateList = state.cursor === 0 ? [] : state.searchResultList
let noMore = false
if (data?.items?.length > 0) {
data.items.forEach((item) => {
item.dateTime = parseTime(item?.created_at, '{m}/{d}')
if (item?.extra) {
if (item?.extra && typeof item.extra === 'object') {
item.extra.fileSize = fileFormatSize(item?.extra?.size)
item.extra.typeText = item?.extra?.name ? fileSuffix(item?.extra?.name) : ''
item.extra.file_avatar = fileTypeAvatar(item?.extra?.typeText)
console.log(item.extra.type)
// console.log(item.extra.type)
}
let year = new Date(item.created_at).getFullYear()
let month = new Date(item.created_at).getMonth() + 1
@ -597,8 +638,6 @@ const queryAllSearch = () => {
})
}
})
} else {
noMore = true
}
//
@ -615,6 +654,11 @@ const queryAllSearch = () => {
// zPaging.value?.completeByNoMore(state.flatList, noMore)
}
state.cursor = data?.cursor
if (data?.cursor === 0) {
state.hasNoMoreResults = true
} else {
state.hasNoMoreResults = false
}
} else {
if (state.cursor === 0) {
state.searchResultList = []
@ -623,6 +667,9 @@ const queryAllSearch = () => {
// zPaging.value?.complete([])
}
})
.finally(() => {
state.isLoadingChatRecord = false //
})
resp.catch(() => {
if (state.cursor === 0) {
@ -630,6 +677,7 @@ const queryAllSearch = () => {
state.flatList = []
}
// zPaging.value?.complete([])
state.isLoadingChatRecord = false //
})
}
@ -699,22 +747,45 @@ const toDialogueByMember = async (msgInfo) => {
})
}
//
const resetSearchConditions = (newVal) => {
state.cursor = 0
state.searchResultList = []
state.flatList = []
if (newVal !== 'member') {
state.group_member_id = 0
emits('clearSearchMemberByAlphabet')
}
}
//
const loadMore = () => {
if (state.isLoadingChatRecord || state.hasNoMoreResults) return //
state.isLoadingChatRecord = true
queryAllSearch()
}
watch(
() => props?.conditionType,
(newVal, oldVal) => {
console.log(newVal, oldVal)
state.condition = newVal
if (newVal) {
resetSearchConditions(newVal)
if (newVal === 'member') {
//
state.showPageTitle = true
state.pageTitle = '按群成员查找'
// state.group_member_id = options.groupMemberId
state.msg_type = 0
queryAllSearch()
} else if (newVal === 'dateTimePicker') {
} else if (newVal === 'date') {
//
state.showPageTitle = true
state.pageTitle = '按日期查找'
state.msg_type = 0
ServeQueryTalkDate(parseTime(state.nowDate, '{y}{m}'))
getDArray(parseTime(state.nowDate, '{y}-{m}'))
queryAllSearch()
} else if (newVal === 'imgAndVideo') {
state.showPageTitle = true
state.pageTitle = '图片与视频'
@ -744,6 +815,10 @@ watch(
)
state.msg_type = 14
queryAllSearch()
} else if (newVal === 'all') {
//
state.msg_type = 0
queryAllSearch()
}
}
},
@ -752,12 +827,47 @@ watch(
deep: true
}
)
watch(
() => props?.searchMemberItem,
(newVal, oldVal) => {
const memberItem = newVal ? JSON.parse(decodeURIComponent(newVal)) : ''
state.group_member_id = memberItem?.user_id
resetSearchConditions('member')
queryAllSearch()
},
{
immediate: true,
deep: true
}
)
watch(
() => props?.searchRecordByConditionText,
(newVal, oldVal) => {
resetSearchConditions(state.condition)
queryAllSearch()
}
)
watch(
() => props?.selectedDateTime,
(newVal, oldVal) => {
state.selectedDateTime = newVal
resetSearchConditions('date')
queryAllSearch()
}
)
watch(
() => props?.nowDateTime,
(newVal, oldVal) => {
ServeQueryTalkDate(parseTime(newVal, '{y}{m}'))
getDArray(parseTime(newVal, '{y}-{m}'))
}
)
</script>
<style scoped lang="scss">
.search-by-date {
:deep(.n-date-panel-header) {
display: none;
}
.search-date-picker {
padding: 10px 16px;
display: flex;
@ -774,8 +884,6 @@ watch(
margin: 0 0 0 13px;
}
}
}
body:deep(.text-overflow-1) {
color: #666666 !important;
line-height: 22px !important;
@ -963,11 +1071,6 @@ body:deep(.round-3) {
}
}
}
.condition-dimensionality-each:nth-child(1) {
.condition-each-result-attachments {
padding: 0 0 14px !important;
}
}
}
}
}
@ -990,4 +1093,23 @@ body:deep(.round-3) {
height: 100%;
object-fit: contain;
}
.search-record-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 455px;
box-sizing: border-box;
img {
width: 160px;
height: 104px;
}
span {
font-size: 14px;
color: #999;
font-weight: 400;
margin: 13px 0 0;
}
}
</style>

View File

@ -334,6 +334,7 @@ const resultDetail = computed(() => {
.searchRecordDetail-fastLocal {
display: none;
line-height: 20px;
flex-shrink: 0;
span {
color: #46299d;
font-size: 12px;

View File

@ -89,7 +89,7 @@ const onContextMenu = (e:any,item: ITalkRecord) => {
<Loading v-if="items.length === 0" />
<div v-for="item in items" :key="item.msg_id" class="message-item">
<div class="left-box pointer" @click="showUserInfoModal(item.user_id)">
<div class="left-box pointer" @click="showUserInfoModal(item.erp_user_id)">
<im-avatar :src="item.avatar" :size="38" :username="item.nickname" />
</div>

View File

@ -86,7 +86,7 @@ const strokeDashoffset = computed(() =>
//
const handleClick = () => {
console.log('handleClick')
if(!props.extra.is_uploading){
window.open(
`${window.location.origin}/office?url=${props.extra.path}`,
'_blank',
@ -94,6 +94,8 @@ const handleClick = () => {
);
}
}
function downloadFileWithProgress(resourceUrl, filename) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';

View File

@ -27,7 +27,7 @@ let textContent = props.extra?.content || ''
textContent = textReplaceLink(textContent)
if (props.data.talk_type == 2) {
textContent = textReplaceMention(textContent, '#1890ff')
textContent = textReplaceMention(textContent, '#462AA0')
}
textContent = textReplaceEmoji(textContent)

View File

@ -17,7 +17,7 @@ let textContent = props.extra?.content || ''
textContent = textReplaceLink(textContent)
if (props.data.talk_type == 2) {
textContent = textReplaceMention(textContent, '#1890ff')
textContent = textReplaceMention(textContent, '#462AA0')
}
textContent = textReplaceEmoji(textContent)

View File

@ -121,7 +121,7 @@ onMounted(() => {
:height="5"
:show-indicator="false"
:percentage="parseInt(option.progress)"
color="#1890ff"
color="#462AA0"
/>
</p>
</div>

View File

@ -28,7 +28,7 @@
font-weight: 400;
&:hover {
color: #1890ff;
color: #462AA0;
}
}
}

View File

@ -1514,7 +1514,7 @@ const numWidth = computed(() => {
.x-upload-preview-button:hover {
background-color: #e6f7ff;
color: #1890ff;
color: #462AA0;
}
.x-upload-download-button:hover {

View File

@ -45,7 +45,7 @@
font-feature-settings: 'tnum';
position: absolute;
display: none;
color: #1890ff;
color: #462AA0;
text-align: center;
vertical-align: middle;
opacity: 0;
@ -103,7 +103,7 @@
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
background-color: #462AA0;
border-radius: 100%;
transform: scale(0.75);
transform-origin: 50% 50%;

View File

@ -7,6 +7,7 @@ import { formatTalkItem, palyMusic, formatTalkRecord } from '@/utils/talk'
import { isElectronMode } from '@/utils/common'
import { ServeClearTalkUnreadNum, ServeCreateTalkList } from '@/api/chat'
import { useTalkStore, useDialogueStore, useSettingsStore } from '@/store'
import { isScrollAtBottom, scrollToBottom } from '@/utils/dom'
/**
* 好友状态事件
@ -189,12 +190,10 @@ class Talk extends Base {
if (!el) return
// 判断的滚动条是否在底部
const isBottom = Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight
const isBottom = isScrollAtBottom(el)
if (isBottom || record.user_id == this.getAccountId()) {
nextTick(() => {
el.scrollTop = el.scrollHeight + 1000
})
scrollToBottom()
} else {
useDialogueStore().setUnreadBubble()
}

View File

@ -108,8 +108,8 @@ export function useSessionMenu() {
const onUserInfo = (item: ISession) => {
console.error('item',item)
debugger
user(item.receiver_id)
user(item.id)
}
// 移除会话

View File

@ -3,7 +3,7 @@ import { ServeTalkRecords } from '@/api/chat'
import { useDialogueStore } from '@/store'
import { ITalkRecord } from '@/types/chat'
import { formatTalkRecord } from '@/utils/talk'
import { addClass, removeClass } from '@/utils/dom'
import { addClass, removeClass, scrollToBottom, isScrollAtBottom } from '@/utils/dom'
interface Params {
receiver_id: number
@ -14,7 +14,8 @@ interface Params {
interface SpecialParams extends Params {
msg_id?: string
cursor?: number
direction?: 'up' | 'down'
direction?: 'up' | 'down',
sort_sequence?: string
}
interface LoadOptions {
@ -136,16 +137,8 @@ export const useTalkRecord = (uid: number) => {
if (el) {
if (request.cursor == 0) {
el.scrollTop = el.scrollHeight
setTimeout(() => {
console.log('el.scrollHeight',el.scrollHeight)
console.log('request.cursor == 0')
el.scrollTop = el.scrollHeight + 1000
}, 500)
scrollToBottom()
} else {
console.log('request.cursor !== 0')
el.scrollTop = el.scrollHeight - scrollHeight
}
}
@ -201,9 +194,9 @@ export const useTalkRecord = (uid: number) => {
loadConfig.status = 2
return
}
dialogueStore.clearDialogueRecord()
// dialogueStore.clearDialogueRecord()
const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item))
dialogueStore.unshiftDialogueRecord(items.reverse())
dialogueStore.unshiftDialogueRecord(contextParams.direction === 'down' ? items : items.reverse())
loadConfig.status = items.length >= contextParams.limit ? 1 : 2
loadConfig.cursor = data.cursor
nextTick(() => {
@ -221,7 +214,7 @@ export const useTalkRecord = (uid: number) => {
addClass(target, 'border')
setTimeout(() => removeClass(target, 'border'), 3000)
} else if (el) {
el.scrollTop = el.scrollHeight
scrollToBottom()
}
}, 50)
})
@ -255,6 +248,7 @@ export const useTalkRecord = (uid: number) => {
specifiedMsg: {
...loadConfig.specialParams,
direction: 'up',
sort_sequence: '',
cursor: getMinSequence()
}
}

View File

@ -119,7 +119,7 @@ const isActive = (menu) => {
<component
:is="nav.icon"
:theme="isActive(nav) ? 'filled' : 'outline'"
:fill="isActive(nav) ? '#1890ff' : color"
:fill="isActive(nav) ? '#462AA0' : color"
:strokeWidth="2"
:size="22"
/>

View File

@ -54,6 +54,8 @@ export const useDialogueStore = defineStore('dialogue', {
groupInfo: {} ,
// 群成员列表
members: [],
// 群成员列表按字母分组
membersByAlphabet: {},
// 对话记录
items: {
@ -101,6 +103,7 @@ export const useDialogueStore = defineStore('dialogue', {
// }
this.members = []
this.membersByAlphabet = []
if (data.talk_type == 2) {
this.updateGroupMembers()
this.getGroupInfo()
@ -126,6 +129,17 @@ export const useDialogueStore = defineStore('dialogue', {
online: false,
value: o.nickname
}))
const groupMap = {};
data.sortItems.forEach(member => {
const alpha = (member.key || member.nickname?.[0] || '#').toUpperCase();
if (!groupMap[alpha]) groupMap[alpha] = [];
groupMap[alpha].push(member);
});
const alphabets = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
this.membersByAlphabet = alphabets.map(alpha => ({
alphabet: alpha,
members: groupMap[alpha] || []
})).filter(group => group.members.length > 0);
},
// 清空对话记录

View File

@ -18,7 +18,7 @@ export function isLoggedIn() {
*/
export function getAccessToken() {
// return storage.get(AccessToken) || ''
return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22ab23a552e0c3f606946dcb914a52b692e10d823cc7f43027127359e7ee8555d956e7e095946931ceaa3877675584b0a0a4fc690c8018712b306050ebbdea92037aea31d66d65004be26d3c696abc4c29'
return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d227de310c4e6f5d7ff11a9e1ea73aba3f6c749f75a50a2aeaed07b14bc0d8b1db6428caf891f0a0b0c84a49697f4a4e7c8b87d972340ecbf02ddbc4d4f1e51b057c822f8351524e19d52a3ec5ce8c83e2f'
}
/**

View File

@ -52,7 +52,12 @@ export function throttle(fn, delay, call = function () {}) {
* @param {Function} callback 复制成功回调方法
*/
export function clipboard(text, callback) {
navigator.clipboard
// 在wujie环境下使用主应用的clipboard
const clipboardObj = window.__POWERED_BY_WUJIE__
? window.parent.navigator.clipboard
: navigator.clipboard
clipboardObj
.writeText(text)
.then(() => {
callback && callback()

View File

@ -1,3 +1,5 @@
import { nextTick } from 'vue'
function trim(string: string) {
return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
}
@ -54,3 +56,26 @@ export function removeClass(el: Element, cls: string) {
el.className = trim(curClass)
}
}
/**
*
* @param el DOM元素
* @returns boolean
*/
export function isScrollAtBottom(el: HTMLElement): boolean {
return Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight
}
/**
*
* @param id DOM id 'imChatPanel'
* @param offset 0
*/
export function scrollToBottom(id = 'imChatPanel', offset = 0) {
nextTick(() => {
const el = document.getElementById(id)
if (el) {
el.scrollTop = el.scrollHeight + offset
}
})
}

View File

@ -58,7 +58,7 @@ export function fileFormatSize(value) {
return '0'
}
let unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
let unitArr = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
let index = 0
let srcsize = parseFloat(value)
index = Math.floor(Math.log(srcsize) / Math.log(1000))

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { reactive, computed, watch } from 'vue'
import { NDrawer, NCard, NTag } from 'naive-ui'
import { reactive, computed, watch, ref } from 'vue'
import { NDrawer, NCard, NTag, NInput, NDatePicker } from 'naive-ui'
import { useUserStore, useDialogueStore, useUploadsStore } from '@/store'
import PanelHeader from './panel/PanelHeader.vue'
import PanelContent from './panel/PanelContent.vue'
@ -16,9 +16,31 @@ import avatarModule from '@/components/avatar-module/index.vue'
const userStore = useUserStore()
const dialogueStore = useDialogueStore()
const uploadsStore = useUploadsStore()
console.log('dialogueStore',dialogueStore);
console.log('dialogueStore', dialogueStore)
const members = computed(() => dialogueStore.members)
const membersByAlphabet = computed(() => {
if (state.searchMemberByAlphabet) {
//
return (
(dialogueStore.membersByAlphabet as any)
.map((group: any) => {
//
const filteredMembers = group.members.filter(
(member) => member.nickname && member.nickname.includes(state.searchMemberByAlphabet)
)
return {
...group,
members: filteredMembers
}
})
//
.filter((group) => group.members.length > 0)
)
}
return dialogueStore.membersByAlphabet
})
const isShowEditor = computed(() => dialogueStore.isShowEditor)
//
@ -46,8 +68,8 @@ const state = reactive({
backgroundColor: '#F9F9FD'
}, //
searchRecordByConditionText: '', //
conditionTag: '', //
conditionType: '', //
conditionTag: 'all', //
conditionType: 'all', //
isShowGroupNoticeModal: false, //
customGroupNoticeModalStyle: {
width: '997px',
@ -81,7 +103,14 @@ const state = reactive({
content: ''
}, //
isAdmin: false, //
groupNoticeContentChange: '' //
groupNoticeContentChange: '', //
searchMemberByAlphabet: '', //
searchMemberItem: '', //item
showMemberListByAlphabetPopover: false, //
disabledDateArray: [] as string[], //
selectedDateTime: null, //
nowDateTime: new Date(), //
showDateConditionPopover: false //
})
const events = {
@ -118,7 +147,7 @@ const changeConditionTag = (tag) => {
} else if (tag === 'member') {
state.conditionTag = '群成员'
} else {
state.conditionTag = ''
state.conditionTag = 'all'
}
}
@ -370,13 +399,94 @@ const handleGroupNoticeModalShow = (isAdmin) => {
state.isShowGroupNoticeModal = true
getGroupNotices()
}
//item
const handleMemberItemClick = (memberItem) => {
state.searchMemberItem = encodeURIComponent(JSON.stringify(memberItem))
state.showMemberListByAlphabetPopover = false //
state.searchMemberByAlphabet = ''
}
//A-Z
const clearSearchMemberByAlphabet = () => {
state.searchMemberByAlphabet = ''
}
//
const handleSearchRecordByConditionModalClose = () => {
state.isShowSearchRecordByConditionModal = false
resetSearchRecordByCondition()
}
//
const resetSearchRecordByCondition = () => {
state.searchRecordByConditionText = ''
state.conditionType = 'all'
state.conditionTag = 'all'
state.searchMemberItem = ''
state.showMemberListByAlphabetPopover = false
state.searchMemberByAlphabet = ''
}
//
const dateDisabled = (e) => {
const date = new Date(e)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const formattedDate = `${year}/${month}/${day}`
return state.disabledDateArray.includes(formattedDate)
}
//
const getDisabledDateArray = (disabledDateArray) => {
state.disabledDateArray = disabledDateArray
}
//
const onDatePickConfirm = (datePicker) => {
console.log(datePicker)
state.selectedDateTime = datePicker
state.showDateConditionPopover = false
}
//
const onDatePickClear = () => {
state.selectedDateTime = null
state.showDateConditionPopover = false
state.nowDateTime = new Date()
}
//
const onPrevMonth = () => {
state.nowDateTime = new Date(state.nowDateTime.getFullYear(), state.nowDateTime.getMonth() - 1, 1)
}
//
const onNextMonth = () => {
state.nowDateTime = new Date(state.nowDateTime.getFullYear(), state.nowDateTime.getMonth() + 1, 1)
}
//
const onPrevYear = () => {
state.nowDateTime = new Date(state.nowDateTime.getFullYear() - 1, state.nowDateTime.getMonth(), 1)
}
//
const onNextYear = () => {
state.nowDateTime = new Date(state.nowDateTime.getFullYear() + 1, state.nowDateTime.getMonth(), 1)
}
// popover
const onDatePickShow = (show) => {
if (show) {
// state.nowDateTime = new Date()
}
}
</script>
<template>
<section id="drawer-container" class="el-container is-vertical">
<!-- 头部区域 -->
<header class="el-header bdr-b">
<PanelHeader
:type="talkParams.type"
:username="talkParams.username"
@ -468,6 +578,8 @@ const handleGroupNoticeModalShow = (isAdmin) => {
:style="state.customSearchRecordByConditionModalStyle"
:customCloseBtn="true"
:closable="false"
:customCloseEvent="true"
@customCloseModal="handleSearchRecordByConditionModalClose"
>
<template #content>
<div class="search-record-modal-searchArea">
@ -484,7 +596,11 @@ const handleGroupNoticeModalShow = (isAdmin) => {
<img src="@/assets/image/icon/close-btn-grey-line.png" alt="close" />
</template>
<template #prefix>
<n-tag closable v-if="state.conditionTag" @close="changeConditionTag('')">
<n-tag
closable
v-if="state.conditionTag && state.conditionTag !== 'all'"
@close="changeConditionTag('all')"
>
{{ state.conditionTag }}
</n-tag>
</template>
@ -493,23 +609,87 @@ const handleGroupNoticeModalShow = (isAdmin) => {
<div class="search-area-condition">
<span @click="changeConditionTag('file')">文件</span>
<span @click="changeConditionTag('imgAndVideo')">图片与视频</span>
<n-popover trigger="click" placement="bottom-start" style="height: 312px; padding: 0;">
<n-popover
v-model:show="state.showDateConditionPopover"
trigger="click"
placement="bottom-start"
style="height: 312px; padding: 0;"
@update:show="onDatePickShow"
>
<template #trigger>
<span id="date-condition" @click="changeConditionTag('date')">日期</span>
</template>
<historyRecord conditionType="dateTimePicker" v-if="state.conditionType === 'date'" />
<div class="search-by-date">
<n-date-picker
:panel="true"
type="datetime"
:clearable="true"
:first-day-of-week="6"
:is-date-disabled="dateDisabled"
:actions="['clear', 'confirm']"
@confirm="onDatePickConfirm"
@clear="onDatePickClear"
@prev-month="onPrevMonth"
@next-month="onNextMonth"
@prev-year="onPrevYear"
@next-year="onNextYear"
v-model:value="state.selectedDateTime"
/>
</div>
</n-popover>
<n-popover
v-model:show="state.showMemberListByAlphabetPopover"
trigger="click"
placement="bottom-start"
style="height: 505px; padding: 0;"
style="width: 290px; height: 505px; padding: 0;"
v-if="talkParams.type === 2"
>
<template #trigger>
<span @click="changeConditionTag('member')">群成员</span>
</template>
<div>
<text>这里是memberList</text>
<div class="member-list-by-alphabet-container">
<n-input
placeholder="请输入群成员"
style="margin: 0 0 17px;"
v-model:value="state.searchMemberByAlphabet"
/>
<n-scrollbar style="height: 430px;">
<div
class="member-list-by-alphabet"
v-for="(alphabetMembersItem, alphabetMembersIndex) in membersByAlphabet"
:key="alphabetMembersIndex"
>
<div class="member-list-each-alphabet-header">
<span>{{ (alphabetMembersItem as any).alphabet }}</span>
</div>
<div class="member-list-each-alphabet">
<div
class="member-item-each-alphabet"
v-for="(memberItem, memberItemIndex) in (alphabetMembersItem as any).members"
:key="memberItemIndex"
@click="handleMemberItemClick(memberItem)"
>
<avatarModule
:mode="1"
:avatar="memberItem.avatar"
:userName="memberItem.nickname"
:groupType="0"
:customStyle="{
width: '38px',
height: '38px'
}"
:customTextStyle="{
fontSize: '12px',
fontWeight: 'bold',
color: '#fff',
lineHeight: '17px'
}"
></avatarModule>
<span>{{ memberItem.nickname }}</span>
</div>
</div>
</div>
</n-scrollbar>
</div>
</n-popover>
</div>
@ -517,18 +697,16 @@ const handleGroupNoticeModalShow = (isAdmin) => {
</div>
<div class="search-record-modal-content">
<n-card>
<div
class="search-record-card"
v-if="state.searchRecordByConditionText || state.conditionType"
>
<historyRecord :conditionType="state.conditionType" />
</div>
<div
class="search-record-empty"
v-if="!state.searchRecordByConditionText && !state.conditionType"
>
<img src="@/assets/image/chatList/search-empty.png" alt="" />
<span>无内容</span>
<div class="search-record-card">
<historyRecord
:conditionType="state.conditionType"
:searchMemberItem="state.searchMemberItem"
@clearSearchMemberByAlphabet="clearSearchMemberByAlphabet"
:searchRecordByConditionText="state.searchRecordByConditionText"
@getDisabledDateArray="getDisabledDateArray"
:selectedDateTime="state.selectedDateTime"
:nowDateTime="state.nowDateTime"
/>
</div>
</n-card>
</div>
@ -663,6 +841,11 @@ const handleGroupNoticeModalShow = (isAdmin) => {
}
}
}
.search-by-date {
:deep(.n-date-panel-header) {
display: none;
}
}
.search-record-modal-content {
box-sizing: border-box;
width: 100%;
@ -677,24 +860,6 @@ const handleGroupNoticeModalShow = (isAdmin) => {
}
.search-record-card {
}
.search-record-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 455px;
box-sizing: border-box;
img {
width: 160px;
height: 104px;
}
span {
font-size: 14px;
color: #999;
font-weight: 400;
margin: 13px 0 0;
}
}
}
.group-notice-modal-content {
.group-notice-text-area {
@ -772,4 +937,43 @@ const handleGroupNoticeModalShow = (isAdmin) => {
color: #1f2225;
}
}
.member-list-by-alphabet-container {
padding: 12px;
width: 290px;
.member-list-by-alphabet {
.member-list-each-alphabet-header {
border-bottom: 1px solid #e5e5e5;
span {
font-size: 14px;
color: #999;
font-weight: 400;
line-height: 20px;
}
}
.member-list-each-alphabet {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 10px;
padding: 12px 0;
.member-item-each-alphabet {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
width: 100%;
cursor: pointer;
span {
font-size: 12px;
color: #000;
font-weight: 400;
line-height: 17px;
}
}
}
}
}
</style>

View File

@ -298,13 +298,6 @@ const items = computed((): ISession[] => {
return [...topItems, ...normalItems]
})
watch(
() => talkStore,
(newValue, oldValue) => {
// console.log(newValue)
},
{ deep: true, immediate: true }
)
watch(
() => state.addressBookSearchNickName,
(newValue, oldValue) => {

View File

@ -95,7 +95,6 @@ const onPanelScroll = (e: any) => {
if (!skipBottom.value && dialogueStore.unreadBubble) {
dialogueStore.setUnreadBubble(0)
}
//
if (skipBottom.value == false) {
let len = dialogueStore.records.length
@ -232,7 +231,7 @@ const onClickNickname = (data: ITalkRecord) => {
//
const onContextMenu = (e: any, item: ITalkRecord) => {
console.log('item',item)
if (!dialogueStore.isShowEditor || dialogueStore.isOpenMultiSelect) {
return e.preventDefault()
}
@ -243,7 +242,7 @@ const onContextMenu = (e: any, item: ITalkRecord) => {
}
const onConvertText = async (data: ITalkRecord) => {
console.log('data', data)
data.is_convert_text = 1
const res = await voiceToText({ msgId: data.msg_id, voiceUrl: data.extra.url })
if (res.code == 200) {
@ -324,13 +323,14 @@ const retry=(item:any)=>{
}
const onContextMenuAvatar=(e:any,item:any)=>{
console.log('item',item)
e.preventDefault()
if(item.float!=='right'){
bus.emit(EditorConst.Mention, {
id: item.user_id,
value: item.nickname
})
}
}
</script>

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, nextTick } from 'vue'
import {
useTalkStore,
useDialogueStore,
@ -15,7 +15,8 @@ import { parseTime } from '@/utils/datetime'
import Editor from '@/components/editor/Editor.vue'
import MultiSelectFooter from './MultiSelectFooter.vue'
import HistoryRecord from '@/components/talk/HistoryRecord.vue'
import { uploadImg } from '@/api/upload'
import {scrollToBottom} from '@/utils/dom.ts'
import SimpleEditorExample from '@/components/editor/SimpleEditorExample.vue'
const userStore = useUserStore()
const talkStore = useTalkStore()
const editorStore = useEditorStore()
@ -135,6 +136,9 @@ const onSendVideoEvent = async ({ data }) => {
//
dialogueStore.addDialogueRecord(tempMessage)
nextTick(()=>{
scrollToBottom()
})
uploadsStore.initUploadFile(
data,
props.talk_type,
@ -145,6 +149,7 @@ const onSendVideoEvent = async ({ data }) => {
},
async () => {
dialogueStore.batchDelDialogueRecord([uploadId])
}
)
}
@ -185,13 +190,16 @@ const onSendFileEvent = ({ data }) => {
float: 'right'
}
dialogueStore.addDialogueRecord(tempMessage)
nextTick(()=>{
scrollToBottom()
})
uploadsStore.initUploadFile(data, props.talk_type, props.receiver_id,clientUploadId,
async (percentage) => {
dialogueStore.updateUploadProgress(clientUploadId, percentage)
},
async () => {
dialogueStore.batchDelDialogueRecord([clientUploadId])
}
)
}
@ -282,6 +290,7 @@ onMounted(() => {
<template>
<footer class="el-footer">
<MultiSelectFooter v-if="dialogueStore.isOpenMultiSelect" />
<Editor v-else @editor-event="onEditorEvent" :vote="talk_type == 2" :members="members" />
</footer>

View File

@ -156,7 +156,7 @@ const onSetMenu = () => {
text-align: center;
&.color {
color: #1890ff;
color: #462AA0;
}
.online-status {
@ -168,7 +168,7 @@ const onSetMenu = () => {
vertical-align: middle;
border-radius: 50%;
position: relative;
background-color: #1890ff;
background-color: #462AA0;
margin-right: 5px;
&:after {
@ -177,7 +177,7 @@ const onSetMenu = () => {
left: -1px;
width: 100%;
height: 100%;
border: 1px solid #1890ff;
border: 1px solid #462AA0;
border-radius: 50%;
-webkit-animation: antStatusProcessing 1.2s ease-in-out infinite;
animation: antStatusProcessing 1.2s ease-in-out infinite;

View File

@ -1,6 +1,8 @@
<script lang="ts" setup>
import { useDialogueStore } from '@/store'
import { DoubleDown } from '@icon-park/vue-next'
import { scrollToBottom } from '@/utils/dom'
defineProps(['modelValue'])
const dialogueStore = useDialogueStore()
@ -8,13 +10,7 @@ const dialogueStore = useDialogueStore()
//
const onSkipBottom = () => {
console.log('onSkipBottom')
let el = document.getElementById('imChatPanel')
if (el) {
el.scrollTo({
top: el.scrollHeight + 1000,
behavior: 'smooth'
})
}
scrollToBottom()
}
</script>
@ -35,7 +31,7 @@ const onSkipBottom = () => {
min-width: 100px;
height: 28px;
font-size: 12px;
background-color: #1ebafc;
background-color: #462AA0;
color: #ffffff;
display: flex;
align-items: center;

View File

@ -47,7 +47,7 @@ export default defineConfig(({ mode }) => {
compressPlugin(),
UnoCSS(),
vueDevTools({
launchEditor: 'cursor',
launchEditor: 'trae',
})
],
define: {