更新组件和API,新增NImage支持,优化文件上传功能,调整主题颜色,删除不必要的图片,改进用户界面和交互体验。

This commit is contained in:
Phoenix 2025-05-15 16:07:56 +08:00
parent 661472a70a
commit fad84e5bf3
23 changed files with 383 additions and 477 deletions

1
components.d.ts vendored
View File

@ -52,6 +52,7 @@ declare module 'vue' {
NButton: typeof import('naive-ui')['NButton'] NButton: typeof import('naive-ui')['NButton']
NEmpty: typeof import('naive-ui')['NEmpty'] NEmpty: typeof import('naive-ui')['NEmpty']
NIcon: typeof import('naive-ui')['NIcon'] NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput'] NInput: typeof import('naive-ui')['NInput']
NModal: typeof import('naive-ui')['NModal'] NModal: typeof import('naive-ui')['NModal']
NoticeEditor: typeof import('./src/components/group/manage/NoticeEditor.vue')['default'] NoticeEditor: typeof import('./src/components/group/manage/NoticeEditor.vue')['default']

View File

@ -25,3 +25,7 @@ export const ServeRefreshToken = () => {
export const ServeForgetPassword = (data) => { export const ServeForgetPassword = (data) => {
return post('/api/v1/auth/forget', data) return post('/api/v1/auth/forget', data)
} }
// 获取用户信息服务
export const GetUserInfo = (data) => {
return post('/api/v1/users/info', data)
}

View File

@ -45,10 +45,12 @@ export const ServeFindFriendApplyNum = () => {
} }
// 搜索用户信息服务接口 // 搜索用户信息服务接口
// export const ServeSearchUser = (data) => {
// return get('/api/v1/contact/detail', data)
// }
export const ServeSearchUser = (data) => { export const ServeSearchUser = (data) => {
return get('/api/v1/contact/detail', data) return post('/api/v1/users/info', data)
} }
// 搜索用户信息服务接口 // 搜索用户信息服务接口
export const ServeContactGroupList = (data) => { export const ServeContactGroupList = (data) => {
return get('/api/v1/contact/group/list', data) return get('/api/v1/contact/group/list', data)

View File

@ -1,6 +1,6 @@
// 默认主题 // 默认主题
html { html {
--im-primary-color: #1890ff; --im-primary-color: #462AA0;
--im-bg-color: #ffffff; --im-bg-color: #ffffff;
--line-border-color: #f5f5f5; --line-border-color: #f5f5f5;
--border-color: #eeeaea; --border-color: #eeeaea;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,7 +1,12 @@
<script setup> <script setup>
import { fileFormatSize } from '@/utils/strings' import { fileFormatSize } from '@/utils/strings'
import { download, getFileNameSuffix } from '@/utils/functions'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useUploadsStore } from '@/store'
import pptText from '@/assets/image/ppt-text.png'
import excelText from '@/assets/image/excel-text.png'
import wordText from '@/assets/image/word-text.png'
import pdfText from '@/assets/image/pdf-text.png'
import fileText from '@/assets/image/file-text.png'
// //
const props = defineProps({ const props = defineProps({
@ -22,54 +27,76 @@ const props = defineProps({
} }
}) })
// const uploadsStore = useUploadsStore()
const isPlaying = ref(false) const isPlaying = ref(false)
/** //
* 切换播放状态 const fileTypes = {
* 在上传过程中可以暂停/继续 PDF: { icon: pdfText, color: '#DE4E4E', type: 'PDF' },
*/ PPT: { icon: pptText, color: '#B74B2B', type: 'PPT' },
const togglePlay = () => { EXCEL: { icon: excelText, color: '#3C7F4B', type: 'EXCEL' },
isPlaying.value = !isPlaying.value WORD: { icon: wordText, color: '#2750B2', type: 'WORD' },
console.log('播放状态:', isPlaying.value ? '播放中' : '暂停') DEFAULT: { icon: fileText, color: '#747474', type: '文件' }
} }
/** // Excel
* 从文件URL中提取并返回大写的文件扩展名 const EXCEL_EXTENSIONS = ['XLS', 'XLSX', 'CSV']
* @param {string} url - 文件的URL或名称 // Word
* @returns {string} 大写的文件扩展名 const WORD_EXTENSIONS = ['DOC', 'DOCX', 'RTF', 'DOT', 'DOTX']
*/ // PPT
function getFileExtensionUpperCase(url) { const PPT_EXTENSIONS = ['PPT', 'PPTX', 'PPS', 'PPSX']
// URL
const fileName = url.split('/').pop() //
// const fileInfo = computed(() => {
return fileName.split('.').pop().toUpperCase() const extension = getFileExtension(props.extra.name)
if (EXCEL_EXTENSIONS.includes(extension)) {
return fileTypes.EXCEL
}
if (WORD_EXTENSIONS.includes(extension)) {
return fileTypes.WORD
}
if (PPT_EXTENSIONS.includes(extension)) {
return fileTypes.PPT
}
return fileTypes[extension] || fileTypes.DEFAULT
})
//
function getFileExtension(filename) {
const parts = filename.split('.')
return parts.length > 1 ? parts.pop().toUpperCase() : ''
}
//
const togglePlay = () => {
isPlaying.value = !isPlaying.value
if (props.extra.is_uploading && props.extra.upload_id) {
const action = isPlaying.value ? 'resumeUpload' : 'pauseUpload'
uploadsStore[action](props.extra.upload_id)
}
} }
// SVG // SVG
const radius = 9 // const radius = 9
const circumference = computed(() => 2 * Math.PI * radius) // const circumference = computed(() => 2 * Math.PI * radius)
//
const strokeDashoffset = computed(() => const strokeDashoffset = computed(() =>
circumference.value * (1 - props.extra.percentage / 100) circumference.value * (1 - (props.extra.percentage || 0) / 100)
) )
</script> </script>
<template> <template>
<div class="w-243px bg-#fff rounded-8px shadow-md px-14px pointer"> <div class="file-message">
<!-- 文件头部信息 --> <!-- 文件头部信息 -->
<div class="flex py-14px pr-5px justify-between w-full" style="border-bottom: 1px solid #EEEEEE;"> <div class="file-header">
<!-- 文件名 --> <!-- 文件名 -->
<div class="text-#1A1A1A text-14px">{{ extra.name }}</div> <div class="file-name">{{ extra.name }}</div>
<!-- 文件图标区域 --> <!-- 文件图标区域 -->
<div class="relative"> <div class="file-icon-container">
<img class="w-47.91px h-47.91px" src="@/assets/image/file-paper-line@2x.png" alt="文件图标"> <img class="file-icon" :src="fileInfo.icon" alt="文件图标">
<!-- 文件扩展名显示 - 非上传状态 -->
<div v-if="!extra.is_uploading" class="absolute top-11px left-16px text-#DE4E4E text-10px font-bold">
{{ getFileExtensionUpperCase(extra.name) }}
</div>
<!-- 上传进度圆环 - 上传状态 --> <!-- 上传进度圆环 - 上传状态 -->
<div v-else class="absolute top-9px left-16px w-20px h-20px"> <div v-if="extra.is_uploading" class="progress-overlay">
<div class="circle-progress-container" @click="togglePlay"> <div class="circle-progress-container" @click="togglePlay">
<svg class="circle-progress" width="20" height="20" viewBox="0 0 20 20"> <svg class="circle-progress" width="20" height="20" viewBox="0 0 20 20">
<!-- 底色圆环 --> <!-- 底色圆环 -->
@ -87,7 +114,7 @@ const strokeDashoffset = computed(() =>
cy="10" cy="10"
r="9" r="9"
fill="transparent" fill="transparent"
stroke="#D54C4B" :stroke="fileInfo.color"
stroke-width="2" stroke-width="2"
:stroke-dasharray="circumference" :stroke-dasharray="circumference"
:stroke-dashoffset="strokeDashoffset" :stroke-dashoffset="strokeDashoffset"
@ -95,15 +122,13 @@ const strokeDashoffset = computed(() =>
class="progress-circle" class="progress-circle"
/> />
<!-- 暂停图标 - 播放中显示 --> <!-- 暂停/播放图标 -->
<g v-if="isPlaying" class="pause-icon transform-rotate-90"> <g v-if="isPlaying" class="pause-icon">
<rect x="7" y="5" width="2" height="10" fill="#D54C4B" /> <rect x="7" y="5" width="2" height="10" :fill="fileInfo.color" />
<rect x="11" y="5" width="2" height="10" fill="#D54C4B" /> <rect x="11" y="5" width="2" height="10" :fill="fileInfo.color" />
</g> </g>
<!-- 播放图标 - 暂停时显示 -->
<g v-else class="play-icon"> <g v-else class="play-icon">
<rect x="6" y="6" width="8" height="8" fill="#D54C4B" /> <rect x="6" y="6" width="8" height="8" :fill="fileInfo.color" />
</g> </g>
</svg> </svg>
</div> </div>
@ -111,11 +136,66 @@ const strokeDashoffset = computed(() =>
</div> </div>
</div> </div>
<!-- 文件大小信息 --> <!-- 文件大小信息 -->
<div class="text-#747474 text-12px pt-5px pb-11px">{{ fileFormatSize(extra.size) }}</div> <div class="file-size">{{ fileFormatSize(extra.size) }}</div>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.file-message {
width: 243px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 0 14px;
cursor: pointer;
}
.file-header {
display: flex;
padding: 14px 5px 14px 0;
justify-content: space-between;
width: 100%;
border-bottom: 1px solid #EEEEEE;
}
.file-name {
color: #1A1A1A;
font-size: 14px;
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.file-icon-container {
position: relative;
}
.file-icon {
width: 48px;
height: 48px;
}
.progress-overlay {
background-color: #fff;
position: absolute;
top: 6px;
left: 11px;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
}
.file-size {
color: #747474;
font-size: 12px;
padding: 5px 0 11px;
}
.circle-progress-container { .circle-progress-container {
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -136,7 +216,7 @@ const strokeDashoffset = computed(() =>
transform-origin: center; transform-origin: center;
} }
.transform-rotate-90 { .pause-icon {
transform: rotate(90deg); transform: rotate(90deg);
} }
</style> </style>

View File

@ -100,7 +100,7 @@ async function onPlay() {
function pauseUpload(e) { function pauseUpload(e) {
e.stopPropagation() e.stopPropagation()
if (props.extra.is_uploading && props.extra.upload_id) { if (props.extra.is_uploading && props.extra.upload_id) {
uploadsStore.pauseVideoUpload(props.extra.upload_id) uploadsStore.pauseUpload(props.extra.upload_id)
isPaused.value = true isPaused.value = true
} }
} }
@ -110,7 +110,7 @@ function resumeUpload(e) {
console.log('resumeUpload') console.log('resumeUpload')
e.stopPropagation() e.stopPropagation()
if (props.extra.is_uploading && props.extra.upload_id) { if (props.extra.is_uploading && props.extra.upload_id) {
uploadsStore.resumeVideoUpload(props.extra.upload_id) uploadsStore.resumeUpload(props.extra.upload_id)
isPaused.value = false isPaused.value = false
} }
} }
@ -123,7 +123,7 @@ function retryUpload(e) {
uploadFailed.value = false uploadFailed.value = false
// //
uploadsStore.resumeVideoUpload(props.extra.upload_id) uploadsStore.resumeUpload(props.extra.upload_id)
message.success('正在重新上传视频...') message.success('正在重新上传视频...')
} }
} }

View File

@ -4,7 +4,7 @@ import { NModal, NInput, NScrollbar, NCheckbox, NTabs, NTab } from 'naive-ui'
import { Search, Delete } from '@icon-park/vue-next' import { Search, Delete } from '@icon-park/vue-next'
import { ServeGetContacts } from '@/api/contact' import { ServeGetContacts } from '@/api/contact'
import { ServeGetGroups } from '@/api/group' import { ServeGetGroups } from '@/api/group'
import XNModal from '@/components/x-naive-ui/x-n-modal/index.vue'
const emit = defineEmits(['close', 'on-submit']) const emit = defineEmits(['close', 'on-submit'])
interface Item { interface Item {
@ -130,20 +130,12 @@ onLoad()
</script> </script>
<template> <template>
<n-modal <x-n-modal
v-model:show="isShowBox" v-model:show="isShowBox"
preset="card"
title="选择联系人" title="选择联系人"
class="modal-radius" class="modal-radius"
style="max-width: 650px; height: 550px" style="width: 997px; height: 740px;background-color: #F9F9FD"
:on-after-leave="onMaskClick" :on-after-leave="onMaskClick"
:segmented="{
content: true,
footer: true
}"
:content-style="{
padding: 0
}"
> >
<section class="el-container launch-box"> <section class="el-container launch-box">
<aside class="el-aside bdr-r" style="width: 240px"> <aside class="el-aside bdr-r" style="width: 240px">
@ -154,6 +146,7 @@ onLoad()
<n-tab name="2"> 群聊 </n-tab> <n-tab name="2"> 群聊 </n-tab>
<!-- <n-tab name="企业"> 企业 </n-tab> --> <!-- <n-tab name="企业"> 企业 </n-tab> -->
</n-tabs> </n-tabs>
</header> </header>
<header class="el-header sub-header"> <header class="el-header sub-header">
@ -256,7 +249,7 @@ onLoad()
</div> </div>
</div> </div>
</template> </template>
</n-modal> </x-n-modal>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, reactive } from 'vue' import { ref, computed, reactive } from 'vue'
import { NIcon, NModal, NButton, NInput, NDropdown, NPopover,NImage } from 'naive-ui'
import { CloseOne, Male, Female, SendOne } from '@icon-park/vue-next' import { CloseOne, Male, Female, SendOne } from '@icon-park/vue-next'
import { ServeSearchUser } from '@/api/contact' import { ServeSearchUser } from '@/api/contact'
import { ServeCreateContact } from '@/api/contact' import { ServeCreateContact } from '@/api/contact'
@ -8,11 +7,10 @@ import { ServeContactGroupList, ServeContactMoveGroup, ServeEditContactRemark }
import { useTalkStore } from '@/store' import { useTalkStore } from '@/store'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import xNModal from '@/components/x-naive-ui/x-n-modal/index.vue' import xNModal from '@/components/x-naive-ui/x-n-modal/index.vue'
import { NSkeleton } from 'naive-ui'
const router = useRouter() const router = useRouter()
const talkStore = useTalkStore() const talkStore = useTalkStore()
const emit = defineEmits(['update:show', 'update:uid', 'updateRemark']) const emit = defineEmits(['update:show', 'update:uid', 'updateRemark'])
const props = defineProps({ const props = defineProps({
show: { show: {
type: Boolean, type: Boolean,
@ -26,7 +24,7 @@ const props = defineProps({
const loading = ref(true) const loading = ref(true)
const isOpenFrom = ref(false) const isOpenFrom = ref(false)
const state: any = reactive({ const userInfo: any = ref({
id: 0, id: 0,
avatar: '', avatar: '',
gender: 0, gender: 0,
@ -43,26 +41,26 @@ const editCardPopover: any = ref(false)
const modelRemark = ref('') const modelRemark = ref('')
const options = ref<any>([]) const options = ref<any>([])
const groupName = computed(() => { // const groupName = computed(() => {
const item = options.value.find((item: any) => { // const item = options.value.find((item: any) => {
return item.key == state.group_id // return item.key == state.group_id
}) // })
if (item) { // if (item) {
return item.label // return item.label
} // }
return '未设置分组' // return ''
}) // })
const onLoadData = () => { const onLoadData = () => {
ServeSearchUser({ ServeSearchUser({
user_id: props.uid erp_user_id: props.uid
}).then(({ code, data }) => { }).then(({ code, data }) => {
if (code == 200) { if (code == 200) {
Object.assign(state, data) userInfo.value = data
modelRemark.value = state.remark // modelRemark.value = state.remark
loading.value = false loading.value = false
} else { } else {
@ -70,15 +68,15 @@ const onLoadData = () => {
} }
}) })
ServeContactGroupList().then((res) => { // ServeContactGroupList().then((res) => {
if (res.code == 200) { // if (res.code == 200) {
let items = res.data.items || [] // let items = res.data.items || []
options.value = [] // options.value = []
for (const iter of items) { // for (const iter of items) {
options.value.push({ label: iter.name, key: iter.id }) // options.value.push({ label: iter.name, key: iter.id })
} // }
} // }
}) // })
} }
const onToTalk = () => { const onToTalk = () => {
@ -86,84 +84,84 @@ const onToTalk = () => {
emit('update:show', false) emit('update:show', false)
} }
const onJoinContact = () => { // const onJoinContact = () => {
if (!state.text.length) { // if (!state.text.length) {
return window['$message'].info('备注信息不能为空') // return window['$message'].info('')
} // }
ServeCreateContact({ // ServeCreateContact({
friend_id: props.uid, // friend_id: props.uid,
remark: state.text // remark: state.text
}).then((res) => { // }).then((res) => {
if (res.code == 200) { // if (res.code == 200) {
isOpenFrom.value = false // isOpenFrom.value = false
window['$message'].success('申请发送成功') // window['$message'].success('')
} else { // } else {
window['$message'].error(res.message) // window['$message'].error(res.message)
} // }
}) // })
} // }
const onChangeRemark = () => { // const onChangeRemark = () => {
ServeEditContactRemark({ // ServeEditContactRemark({
friend_id: props.uid, // friend_id: props.uid,
remark: modelRemark.value // remark: modelRemark.value
}).then(({ code, message }) => { // }).then(({ code, message }) => {
if (code == 200) { // if (code == 200) {
editCardPopover.value.setShow(false) // editCardPopover.value.setShow(false)
window['$message'].success('备注成功') // window['$message'].success('')
state.remark = modelRemark.value // state.remark = modelRemark.value
emit('updateRemark', { // emit('updateRemark', {
user_id: props.uid, // user_id: props.uid,
remark: modelRemark.value // remark: modelRemark.value
}) // })
} else { // } else {
window['$message'].error(message) // window['$message'].error(message)
} // }
}) // })
} // }
const handleSelectGroup = (value) => { // const handleSelectGroup = (value) => {
ServeContactMoveGroup({ // ServeContactMoveGroup({
user_id: props.uid, // user_id: props.uid,
group_id: value // group_id: value
}).then(({ code, message }) => { // }).then(({ code, message }) => {
if (code == 200) { // if (code == 200) {
state.group_id = value // state.group_id = value
window['$message'].success('分组修改成功') // window['$message'].success('')
} else { // } else {
window['$message'].error(message) // window['$message'].error(message)
} // }
}) // })
} // }
const reset = () => { // const reset = () => {
loading.value = true // loading.value = true
Object.assign(state, { // Object.assign(state, {
id: 0, // id: 0,
avatar: '', // avatar: '',
gender: 0, // gender: 0,
mobile: '', // mobile: '',
motto: '', // motto: '',
nickname: '', // nickname: '',
remark: '', // remark: '',
email: '', // email: '',
status: 1, // status: 1,
text: '' // text: ''
}) // })
isOpenFrom.value = false // isOpenFrom.value = false
} // }
const onUpdate = (value) => { // const onUpdate = (value) => {
if (!value) { // if (!value) {
setTimeout(reset, 100) // setTimeout(reset, 100)
} // }
emit('update:show', value) // emit('update:show', value)
} // }
const onAfterEnter = () => { const onAfterEnter = () => {
onLoadData() onLoadData()
@ -171,201 +169,80 @@ const onAfterEnter = () => {
</script> </script>
<template> <template>
<x-n-modal content-style="padding:0;" :closable="false" class="w-311px min-h-445px" style="border-radius: 10px;overflow:hidden;" :show="show" :on-update:show="onUpdate" :on-after-enter="onAfterEnter"> <x-n-modal content-style="padding:0;" :closable="false" class="w-311px min-h-445px" style="border-radius: 10px;overflow:hidden;" :show="show" :on-after-enter="onAfterEnter">
<div class="section relative px-7px pt-82px pb-20px" v-loading="loading"> <div class="section relative px-7px pt-82px pb-20px">
<div class="absolute top-9px right-7px pointer" @click="onUpdate(false)" > <div class="absolute top-9px right-7px pointer z-10" @click="emit('update:show', false)">
<img class="w-20px h-20px" src="@/assets/image/close.png" alt=""> <img class="w-20px h-20px" src="@/assets/image/close.png" alt="">
</div> </div>
<div class="flex py-10px bg-#fff px-16px rounded-4px items-center mb-10px">
<div class="w-59px h-59px rounded-8px mr-12px overflow-hidden"> <template v-if="loading">
<n-image width="59" :src="state.avatar" > <div class="flex py-10px bg-#fff px-16px rounded-4px items-center mb-10px">
<div class="w-59px h-59px rounded-8px mr-12px overflow-hidden">
</n-image> <n-skeleton circle height="59px" width="59px" />
</div>
<div class="w-full">
<n-skeleton text style="width: 80%; margin-bottom: 5px;" />
<n-skeleton text style="width: 60%;" />
</div>
</div>
<div class="bg-#fff rounded-4px mb-20px">
<div class="flex px-15px py-9px" v-for="i in 6" :key="i">
<n-skeleton text style="width: 30%; margin-right: 10px;" />
<n-skeleton text style="width: 60%;" />
</div>
</div> </div>
<div> <div>
<div class="text-#000 text-16px mb-5px">{{ state.nickname }}</div> <n-skeleton text style="width: 100%; height: 42px; border-radius: 4px;" />
<div class="text-#ACACAC text-12px">工号{{ state.job_num }}</div>
</div> </div>
</div> </template>
<div class="bg-#fff rounded-4px mb-20px">
<div class="flex px-15px py-9px"> <template v-else>
<div class="text-#000 text-12px w-84px">公司别</div> <div class="flex py-10px bg-#fff px-16px rounded-4px items-center mb-10px">
<div class="text-#747474 text-12px">江苏泰丰文化传播股份有限公司</div> <div class="w-59px h-59px rounded-8px mr-12px overflow-hidden">
</div> <n-image width="59" :src="userInfo.avatar" >
<div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">主管</div>
<div class="text-#747474 text-12px">江苏泰丰文化传播股份有限公司</div>
</div>
<div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">部门</div>
<div class="text-#747474 text-12px">江苏泰丰文化传播股份有限公司</div>
</div>
<div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">手机号</div>
<div class="text-#747474 text-12px">{{ state.mobile }}</div>
</div>
<div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">岗位</div>
<div class="text-#747474 text-12px">江苏泰丰文化传播股份有限公司</div>
</div>
<div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">入职日期</div>
<div class="text-#747474 text-12px">江苏泰丰文化传播股份有限公司</div>
</div>
</div>
<div>
<n-button block color="#EEE9F8" text-color="#46299D" @click="onToTalk">
<div class="flex items-center justify-center py-11px">
<img class="w-19.8px h-20px mr-15px" src="@/assets/image/faxi@2x.png" alt="">
<span>发送消息</span>
</div>
</n-button>
</div>
<!-- <section class="el-container is-vertical">
<header class="el-header header">
<im-avatar
class="avatar"
:size="100"
:src="state.avatar"
:username="state.remark || state.nickname"
:font-size="30"
/>
<div class="gender" v-show="state.gender > 0"> </n-image>
<n-icon v-if="state.gender == 1" :component="Male" color="#508afe" />
<n-icon v-if="state.gender == 2" :component="Female" color="#ff5722" />
</div> </div>
<div>
<div class="close" @click="onUpdate(false)"> <div class="text-#000 text-16px mb-5px">{{ userInfo.nickname }}</div>
<close-one theme="outline" size="22" fill="#fff" :strokeWidth="2" /> <div class="text-#ACACAC text-12px">工号{{ userInfo.job_num }}</div>
</div> </div>
</div>
<div class="nickname text-ellipsis"> <div class="bg-#fff rounded-4px mb-20px">
{{ state.remark || state.nickname || '未设置昵称' }} <div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">公司别</div>
<div class="text-#747474 text-12px">{{ userInfo.company_name }}</div>
</div> </div>
</header> <div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">主管</div>
<main class="el-main main me-scrollbar me-scrollbar-thumb"> <div class="text-#747474 text-12px">{{ userInfo.leaders?.map(x=>x.user_name)?.join(',') }}</div>
<div class="motto">
{{ state.motto || '编辑个签,展示我的独特态度。' }}
</div> </div>
<div class="flex px-15px py-9px">
<div class="infos"> <div class="text-#000 text-12px w-84px">部门</div>
<div class="info-item"> <div class="text-#747474 text-12px">{{ userInfo.erp_dept_position?.map(x=>x.department_name)?.join(',') }}</div>
<span class="name">工号 :</span>
<span class="text">{{ state.job_num}}</span>
</div>
<div class="info-item">
<span class="name">手机 :</span>
<span class="text">{{ state.mobile }}</span>
</div>
<div class="info-item">
<span class="name">昵称 :</span>
<span class="text text-ellipsis">{{ state.nickname || '-' }} </span>
</div>
<div class="info-item">
<span class="name">性别 :</span>
<span class="text">{{
state.gender == 1 ? '男' : state.gender == 2 ? '女' : '未知'
}}</span>
</div>
<div class="info-item" v-if="state.friend_status == 2">
<span class="name">备注 :</span>
<n-popover trigger="click" placement="top-start" ref="editCardPopover">
<template #trigger>
<span class="text edit pointer text-ellipsis">
{{ state.remark || '未设置' }}&nbsp;&nbsp;
</span>
</template>
<template #header> 设置备注 </template>
<div style="display: flex">
<n-input
type="text"
placeholder="请填写备注"
:autofocus="true"
maxlength="10"
v-model:value="modelRemark"
@keydown.enter="onChangeRemark"
/>
<n-button
type="primary"
text-color="#ffffff"
class="mt-l5"
@click="onChangeRemark"
>
确定
</n-button>
</div>
</n-popover>
</div>
<div class="info-item">
<span class="name">邮箱 :</span>
<span class="text">{{ state.email || '-' }}</span>
</div>
<div class="info-item" v-if="state.friend_status == 2">
<span class="name">分组 :</span>
<n-dropdown
trigger="click"
placement="top-start"
:show-arrow="true"
:options="options"
@select="handleSelectGroup"
>
<span class="text edit pointer">{{ groupName }}</span>
</n-dropdown>
</div>
<div class="info-item">
<span class="name">入职时间 :</span>
<span class="text">{{ state.enter_date}}</span>
</div>
</div> </div>
</main> <div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">手机号</div>
<footer v-if="state.friend_status == 2" class="el-footer footer bdr-t flex-center"> <div class="text-#747474 text-12px">{{ userInfo.tel_num }}</div>
<n-button </div>
round <div class="flex px-15px py-9px">
block <div class="text-#000 text-12px w-84px">岗位</div>
type="primary" <div class="text-#747474 text-12px">{{ userInfo.erp_dept_position?.map(x=>x.position_name)?.join(',') }}</div>
text-color="#ffffff" </div>
@click="onToTalk" <div class="flex px-15px py-9px">
style="width: 91%" <div class="text-#000 text-12px w-84px">入职日期</div>
> <div class="text-#747474 text-12px">{{ userInfo.enter_date }}</div>
<template #icon> </div>
<n-icon :component="SendOne" /> </div>
</template> <div>
发送消息 <n-button block color="#EEE9F8" text-color="#46299D" @click="onToTalk">
<div class="flex items-center justify-center py-11px">
<img class="w-19.8px h-20px mr-15px" src="@/assets/image/faxi@2x.png" alt="">
<span>发送消息</span>
</div>
</n-button> </n-button>
</footer> </div>
</template>
<footer v-else-if="state.friend_status == 1" class="el-footer footer bdr-t flex-center">
<template v-if="isOpenFrom">
<n-input
type="text"
placeholder="请填写申请备注"
v-model:value="state.text"
@keydown.enter="onJoinContact"
/>
<n-button type="primary" text-color="#ffffff" class="mt-l5" @click="onJoinContact">
确定
</n-button>
</template>
<template v-else>
<n-button
type="primary"
text-color="#ffffff"
block
round
style="width: 91%"
@click="isOpenFrom = true"
>
添加好友
</n-button>
</template>
</footer>
</section> -->
</div> </div>
</x-n-modal> </x-n-modal>
</template> </template>

View File

@ -80,26 +80,26 @@ export const useUploadsStore = defineStore('uploads', {
return this.items.find((item) => item.client_upload_id === clientUploadId) return this.items.find((item) => item.client_upload_id === clientUploadId)
}, },
// 暂停文件上传 // // 暂停文件上传
pauseUpload(uploadId: string) { // pauseUpload(uploadId: string) {
const item = this.findItem(uploadId) // const item = this.findItem(uploadId)
if (!item) return // if (!item) return
item.is_paused = true // item.is_paused = true
console.log(`暂停上传: ${uploadId}`) // console.log(`暂停上传: ${uploadId}`)
}, // },
// 恢复文件上传 // 恢复文件上传
resumeUpload(uploadId: string) { // resumeUpload(uploadId: string) {
const item = this.findItem(uploadId) // const item = this.findItem(uploadId)
if (!item) return // if (!item) return
item.is_paused = false // item.is_paused = false
console.log(`恢复上传: ${uploadId}`) // console.log(`恢复上传: ${uploadId}`)
// 继续上传 // // 继续上传
this.triggerUpload(uploadId) // this.triggerUpload(uploadId)
}, // },
// 发送上传消息 // 发送上传消息
async sendUploadMessage(item: any) { async sendUploadMessage(item: any) {
@ -114,7 +114,7 @@ export const useUploadsStore = defineStore('uploads', {
} }
}, },
// 初始化视频上传(使用分片上传方式) // 初始化上传(使用分片上传方式)
async initUploadFile( async initUploadFile(
file: File, file: File,
talkType: number, talkType: number,
@ -306,16 +306,16 @@ export const useUploadsStore = defineStore('uploads', {
} }
}, },
// 暂停视频上传
pauseVideoUpload(clientUploadId: string) { pauseUpload(clientUploadId: string) {
const item = this.findItemByClientId(clientUploadId) const item = this.findItemByClientId(clientUploadId)
if (!item) return if (!item) return
item.is_paused = true item.is_paused = true
}, },
// 恢复视频上传 // 恢复上传
resumeVideoUpload(clientUploadId: string) { resumeUpload(clientUploadId: string) {
const item = this.findItemByClientId(clientUploadId) const item = this.findItemByClientId(clientUploadId)
if (!item) return if (!item) return

View File

@ -4,7 +4,7 @@ import { ServeFindFriendApplyNum } from '@/api/contact'
import { ServeGroupApplyUnread } from '@/api/group' import { ServeGroupApplyUnread } from '@/api/group'
import { delAccessToken } from '@/utils/auth' import { delAccessToken } from '@/utils/auth'
import { storage } from '@/utils/storage' import { storage } from '@/utils/storage'
import { GetUserInfo } from '@/api/auth'
interface UserStoreState { interface UserStoreState {
uid: number uid: number
nickname: string nickname: string
@ -35,7 +35,7 @@ export const useUserStore = defineStore('user', {
online: false, // 在线状态 online: false, // 在线状态
isQiye: false, isQiye: false,
isContactApply: false, isContactApply: false,
isGroupApply: false isGroupApply: false,
} }
}, },
getters: {}, getters: {},

View File

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

View File

@ -13,7 +13,8 @@ import SkipBottom from './SkipBottom.vue'
import { ITalkRecord } from '@/types/chat' import { ITalkRecord } from '@/types/chat'
import { EditorConst } from '@/constant/event-bus' import { EditorConst } from '@/constant/event-bus'
import { useInject, useTalkRecord, useUtil } from '@/hooks' import { useInject, useTalkRecord, useUtil } from '@/hooks'
import { ExclamationCircleFilled } from '@ant-design/icons-vue';
import { useUserStore } from '@/store'
const props = defineProps({ const props = defineProps({
uid: { uid: {
type: Number, type: Number,
@ -39,17 +40,20 @@ const { useMessage } = useUtil()
const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu() const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu()
const { showUserInfoModal } = useInject() const { showUserInfoModal } = useInject()
const dialogueStore = useDialogueStore() const dialogueStore = useDialogueStore()
const userStore = useUserStore()
// const showUserInfoModal = (uid: number) => {
// userStore.getUserInfo(uid)
// }
watch(() => records, (newValue, oldValue) => { watch(() => records, (newValue, oldValue) => {
console.log(newValue); console.log(newValue);
},{deep:true,immediate:true}) }, { deep: true, immediate: true })
// //
const skipBottom = ref(false) const skipBottom = ref(false)
setTimeout(()=>{ setTimeout(() => {
console.log(records.value,'records.value'); console.log(records.value, 'records.value');
},1000) }, 1000)
// //
const isShowTalkTime = (index: number, datetime: string) => { const isShowTalkTime = (index: number, datetime: string) => {
if (datetime == undefined) { if (datetime == undefined) {
@ -276,11 +280,7 @@ onMounted(() => {
<template> <template>
<section class="section"> <section class="section">
<div <div id="imChatPanel" class="me-scrollbar me-scrollbar-thumb talk-container" @scroll="onPanelScroll($event)">
id="imChatPanel"
class="me-scrollbar me-scrollbar-thumb talk-container"
@scroll="onPanelScroll($event)"
>
<!-- 数据加载状态栏 --> <!-- 数据加载状态栏 -->
<div class="load-toolbar pointer"> <div class="load-toolbar pointer">
<span v-if="loadConfig.status == 0"> 正在加载数据中 ... </span> <span v-if="loadConfig.status == 0"> 正在加载数据中 ... </span>
@ -288,88 +288,54 @@ onMounted(() => {
<span v-else class="no-more"> 没有更多消息了 </span> <span v-else class="no-more"> 没有更多消息了 </span>
</div> </div>
<div <div class="message-item" v-for="(item, index) in records" :key="item.msg_id" :id="item.msg_id">
class="message-item"
v-for="(item, index) in records"
:key="item.msg_id"
:id="item.msg_id"
>
<!-- 系统消息 --> <!-- 系统消息 -->
<div v-if="item.msg_type >= 1000" class="message-box"> <div v-if="item.msg_type >= 1000" class="message-box">
<component <component :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra" :data="item" />
:is="MessageComponents[item.msg_type] || 'unknown-message'"
:extra="item.extra"
:data="item"
/>
</div> </div>
<!-- 撤回消息 --> <!-- 撤回消息 -->
<div v-else-if="item.is_revoke == 1" class="message-box"> <div v-else-if="item.is_revoke == 1" class="message-box">
<revoke-message <revoke-message :login_uid="uid" :user_id="item.user_id" :nickname="item.nickname" :talk_type="item.talk_type"
:login_uid="uid" :datetime="item.created_at" />
:user_id="item.user_id"
:nickname="item.nickname"
:talk_type="item.talk_type"
:datetime="item.created_at"
/>
</div> </div>
<div <div v-else class="message-box record-box" :class="{
v-else 'direction-rt': item.float == 'right',
class="message-box record-box" 'multi-select': dialogueStore.isOpenMultiSelect,
:class="{ 'multi-select-check': item.isCheck
'direction-rt': item.float == 'right', }">
'multi-select': dialogueStore.isOpenMultiSelect,
'multi-select-check': item.isCheck
}"
>
<!-- 多选按钮 --> <!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column"> <aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column">
<n-checkbox <n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" />
size="small"
:checked="item.isCheck"
@update:checked="item.isCheck = !item.isCheck"
/>
</aside> </aside>
<!-- 头像信息 --> <!-- 头像信息 -->
<aside class="avatar-column"> <aside class="avatar-column">
<im-avatar <im-avatar class="pointer" :src="item.avatar" :size="42" :username="item.nickname"
class="pointer" @click="showUserInfoModal(item.erp_user_id)" />
:src="item.avatar"
:size="42"
:username="item.nickname"
@click="showUserInfoModal(item.user_id)"
/>
</aside> </aside>
<!-- 主体信息 --> <!-- 主体信息 -->
<main class="main-column"> <main class="main-column">
<div class="talk-title"> <div class="talk-title">
<span <span class="nickname pointer" v-show="talk_type == 2 && item.float == 'left'"
class="nickname pointer" @click="onClickNickname(item)">
v-show="talk_type == 2 && item.float == 'left'"
@click="onClickNickname(item)"
>
<span class="at">@</span>{{ item.nickname }} <span class="at">@</span>{{ item.nickname }}
</span> </span>
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span> <span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
</div> </div>
<div <div class="talk-content" :class="{ pointer: dialogueStore.isOpenMultiSelect }" @click="onRowClick(item)">
class="talk-content"
:class="{ pointer: dialogueStore.isOpenMultiSelect }"
@click="onRowClick(item)"
>
<component
:is="MessageComponents[item.msg_type] || 'unknown-message'"
:extra="item.extra"
:data="item"
:max-width="true"
:source="'panel'"
@contextmenu.prevent="onContextMenu($event, item)"
/>
<component :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra" :data="item"
:max-width="true" :source="'panel'" @contextmenu.prevent="onContextMenu($event, item)" />
<div class="mr-10px"> <n-button text style="font-size: 20px">
<n-icon color="#CF3050">
<ExclamationCircleFilled />
</n-icon>
</n-button></div>
<!-- <div class="talk-tools"> <!-- <div class="talk-tools">
<template v-if="talk_type == 1 && item.float == 'right'"> <template v-if="talk_type == 1 && item.float == 'right'">
<loading <loading
@ -385,19 +351,11 @@ onMounted(() => {
<span v-show="item.send_status != 1"> 已送达 </span> <span v-show="item.send_status != 1"> 已送达 </span>
</template> </template>
<n-icon <n-icon class="more-tools pointer" :component="MoreThree" @click="onContextMenu($event, item)" />
class="more-tools pointer" </div> -->
:component="MoreThree"
@click="onContextMenu($event, item)"
/>
</div> -->
</div> </div>
<div <div v-if="item.extra.reply" class="talk-reply pointer" @click="onJumpMessage(item.extra?.reply?.msg_id)">
v-if="item.extra.reply"
class="talk-reply pointer"
@click="onJumpMessage(item.extra?.reply?.msg_id)"
>
<n-icon :component="ToTop" size="14" class="icon-top" /> <n-icon :component="ToTop" size="14" class="icon-top" />
<span class="ellipsis"> <span class="ellipsis">
回复 {{ item.extra?.reply?.nickname }}: 回复 {{ item.extra?.reply?.nickname }}:
@ -418,15 +376,8 @@ onMounted(() => {
</section> </section>
<!-- 右键菜单 --> <!-- 右键菜单 -->
<n-dropdown <n-dropdown :show="dropdown.show" :x="dropdown.x" :y="dropdown.y" style="width: 142px;" :options="dropdown.options"
:show="dropdown.show" @select="onContextMenuHandle" @clickoutside="closeDropdownMenu" />
:x="dropdown.x"
:y="dropdown.y"
style="width: 142px;"
:options="dropdown.options"
@select="onContextMenuHandle"
@clickoutside="closeDropdownMenu"
/>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
@ -451,6 +402,7 @@ onMounted(() => {
text-align: center; text-align: center;
line-height: 38px; line-height: 38px;
font-size: 13px; font-size: 13px;
.no-more { .no-more {
color: #b9b3b3; color: #b9b3b3;
} }
@ -528,6 +480,7 @@ onMounted(() => {
color: var(--im-text-color); color: var(--im-text-color);
margin-right: 5px; margin-right: 5px;
font-size: 12px; font-size: 12px;
.at { .at {
display: none; display: none;
} }

View File

@ -144,34 +144,32 @@ const onSendVideoEvent = async ({ data }) => {
dialogueStore.talk.username, dialogueStore.talk.username,
uploadId, uploadId,
async (percentage) => { async (percentage) => {
console.log('percentage', percentage)
//
dialogueStore.updateUploadProgress(uploadId, percentage) dialogueStore.updateUploadProgress(uploadId, percentage)
}, },
async (videoData) => { async () => {
console.log('videoData', videoData) dialogueStore.batchDelDialogueRecord([uploadId])
// // console.log('videoData', videoData)
if (videoData.code != 0) return // //
// //
// dialogueStore.completeUpload(uploadId, {
// url: videoData.data.ori_url,
// cover: videoData.data.cover_url
// })
// // //
dialogueStore.completeUpload(uploadId, { // let finalMessage = {
url: videoData.data.ori_url, // type: 'video',
cover: videoData.data.cover_url // url: videoData.data.ori_url,
})
//
let finalMessage = {
type: 'video',
url: videoData.data.ori_url,
size: data.size // size: data.size
} // }
// // //
onSendMessage(finalMessage, () => { // onSendMessage(finalMessage, () => {
// // //
dialogueStore.batchDelDialogueRecord([uploadId]) // dialogueStore.batchDelDialogueRecord([uploadId])
}) // })
} }
) )
} }
@ -218,10 +216,8 @@ const onSendFileEvent = ({ data }) => {
async (percentage) => { async (percentage) => {
dialogueStore.updateUploadProgress(uploadId, percentage) dialogueStore.updateUploadProgress(uploadId, percentage)
}, },
async (data) => { async () => {
console.log('data', data) dialogueStore.batchDelDialogueRecord([uploadId])
//
if (data.code != 0) return
} }
) )
} }