Merge branch 'xingyy'

# Conflicts:
#	env/.env.test   resolved by xingyy version
This commit is contained in:
Phoenix 2025-05-22 15:08:36 +08:00
commit e3d61107cb
41 changed files with 410 additions and 113 deletions

View File

@ -1,6 +1,6 @@
# LumenIM - 在线即时通讯应用
# IM - 在线即时通讯应用
LumenIM 是一个基于 Vue 3 开发的现代化在线即时通讯应用,提供实时聊天、消息管理、笔记等功能。
IM 是一个基于 Vue 3 开发的现代化在线即时通讯应用,提供实时聊天、消息管理、笔记等功能。
## 功能特性
@ -101,4 +101,4 @@ src/
## 许可证
Copyright © 2023 LumenIM
Copyright © 2023 IM

2
components.d.ts vendored
View File

@ -52,6 +52,7 @@ declare module 'vue' {
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NDropdown: typeof import('naive-ui')['NDropdown']
NEmpty: typeof import('naive-ui')['NEmpty']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
@ -68,7 +69,6 @@ declare module 'vue' {
RevokeMessage: typeof import('./src/components/talk/message/RevokeMessage.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SearchByCondition: typeof import('./src/components/search/searchByCondition.vue')['default']
SearchItem: typeof import('./src/components/search/searchItem.vue')['default']
SearchList: typeof import('./src/components/search/searchList.vue')['default']
SysGroupAdminMessage: typeof import('./src/components/talk/message/system/SysGroupAdminMessage.vue')['default']

8
env/.env.test vendored
View File

@ -2,9 +2,7 @@ ENV = 'development'
VITE_BASE=/
VUE_APP_PREVIEW=false
VITE_BASE_API=http://172.16.100.93:8503
# VITE_BASE_API=http://192.168.88.21:9503
VITE_BASE_API=http://114.218.158.24:8503
VITE_EPR_BASEURL=http://114.218.158.24:9020
VITE_SOCKET_API=ws://172.16.100.93:8504
# VITE_SOCKET_API=ws://192.168.88.21:9504
VUE_APP_WEBSITE_NAME="Lumen IM"
VITE_SOCKET_API=ws://114.218.158.24:8504
VUE_APP_WEBSITE_NAME=""

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="./src/assets/image/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lumen IM 在线聊天</title>
<title> 在线聊天</title>
<style>
.outer,
.middle,

4
package-lock.json generated
View File

@ -1,11 +1,11 @@
{
"name": "LumenIM",
"name": "IM",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "LumenIM",
"name": "IM",
"version": "0.0.0",
"dependencies": {
"@highlightjs/vue-plugin": "^2.1.0",

View File

@ -1,5 +1,5 @@
{
"name": "LumenIM",
"name": "IM",
"private": true,
"version": "0.0.0",
"type": "module",
@ -18,6 +18,8 @@
"@highlightjs/vue-plugin": "^2.1.0",
"@iconify-json/ion": "^1.2.3",
"@kangc/v-md-editor": "^2.3.18",
"@onlyoffice/document-editor-vue": "^1.5.0",
"@vicons/fluent": "^0.13.0",
"@vicons/ionicons5": "^0.13.0",
"@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^10.7.0",
@ -69,15 +71,15 @@
"wait-on": "^6.0.1"
},
"build": {
"appId": "com.gzydong.lumenim",
"productName": "LumenIM",
"copyright": "Copyright © 2023 LumenIM",
"appId": "com.gzydong.im",
"productName": "IM",
"copyright": "Copyright © 2023 IM",
"mac": {
"category": "public.app-category.utilities",
"icon": "build/icons/lumen-im-mac.png"
"icon": "build/icons/-im-mac.png"
},
"win": {
"icon": "build/icons/lumen-im-mac.png",
"icon": "build/icons/-im-mac.png",
"target": [
{
"target": "nsis"
@ -87,9 +89,9 @@
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"installerIcon": "build/icons/lumen-im-win.ico",
"uninstallerIcon": "build/icons/lumen-im-win.ico",
"installerHeaderIcon": "build/icons/lumen-im-win.ico",
"installerIcon": "build/icons/-im-win.ico",
"uninstallerIcon": "build/icons/-im-win.ico",
"installerHeaderIcon": "build/icons/-im-win.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "lumeim-icon"

View File

@ -20,6 +20,12 @@ importers:
'@kangc/v-md-editor':
specifier: ^2.3.18
version: 2.3.18(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.2.2))
'@onlyoffice/document-editor-vue':
specifier: ^1.5.0
version: 1.5.0(vue@3.5.13(typescript@5.2.2))
'@vicons/fluent':
specifier: ^0.13.0
version: 0.13.0
'@vicons/ionicons5':
specifier: ^0.13.0
version: 0.13.0
@ -594,6 +600,11 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@onlyoffice/document-editor-vue@1.5.0':
resolution: {integrity: sha512-HZEebUhBloP4LomspI5BddgoQdhtPq91h57yA9K/Lk70MMc1vgOTQ4Wq+N5TZYXNxdDTv+TSsEVFLnBCl1Y71A==}
peerDependencies:
vue: ^3.0.0
'@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
engines: {node: '>= 10.0.0'}
@ -988,6 +999,9 @@ packages:
peerDependencies:
vue: ^3.0.0
'@vicons/fluent@0.13.0':
resolution: {integrity: sha512-bYGZsOE3qzvm3Cm43e7tybgGlr5ZUpYqtRZq0g0Tfupe8jIzLolpvQLNUt1zS8Mgt6goTbUk5YH7Fkv16jkykg==}
'@vicons/ionicons5@0.13.0':
resolution: {integrity: sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==}
@ -4012,6 +4026,11 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
'@onlyoffice/document-editor-vue@1.5.0(vue@3.5.13(typescript@5.2.2))':
dependencies:
lodash: 4.17.21
vue: 3.5.13(typescript@5.2.2)
'@parcel/watcher-android-arm64@2.5.1':
optional: true
@ -4390,6 +4409,8 @@ snapshots:
dependencies:
vue: 3.5.13(typescript@5.2.2)
'@vicons/fluent@0.13.0': {}
'@vicons/ionicons5@0.13.0': {}
'@vitejs/plugin-vue-jsx@3.1.0(vite@6.3.5(@types/node@18.19.99)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.2))(vue@3.5.13(typescript@5.2.2))':

View File

@ -206,7 +206,7 @@ textarea {
border-radius: 2px;
cursor: default;
user-select: none;
background-color: #dee0e3;
transform: scale(0.84);
transform-origin: left;
flex-shrink: 0;

View File

@ -21,7 +21,7 @@ html {
// message
--im-message-bg-color: #f7f7f7;
--im-message-border-color: #efeff5;
--im-message-left-bg-color: #F4F4FC;
--im-message-left-bg-color: #fff;
--im-message-left-text-color: #333;
--im-message-right-bg-color: #46299D;
--im-message-right-text-color: #fff;

View File

@ -4,6 +4,9 @@
color: #fff!important;
}
.n-checkbox-box-wrapper .n-checkbox-box{
border-radius: 50%;
}
/*表格头多选框颜色调整避免和表头颜色冲突*/
.n-data-table-thead .n-data-table-tr .n-checkbox-box{
background: #fff;

BIN
src/assets/image/dofd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -577,7 +577,6 @@ function hideMentionDom() {
* @param data 消息数据
*/
function onSubscribeEdit(data: any) {
console.log('data', data)
const quill = getQuill()
if (!quill) return

View File

@ -5,22 +5,21 @@ import { ServeGetForwardRecords } from '@/api/chat'
import { MessageComponents } from '@/constant/message'
import { ITalkRecord } from '@/types/chat'
import { useInject } from '@/hooks'
const emit = defineEmits(['close'])
import customModal from '@/components/common/customModal.vue'
import { voiceToText } from '@/api/chat.js'
const props = defineProps({
msgId: {
type: String,
required: true
}
})
const isShow=defineModel<boolean>('show')
const { showUserInfoModal } = useInject()
const isShow = ref(true)
const items = ref<ITalkRecord[]>([])
const title = ref('会话记录')
const onMaskClick = () => {
emit('close')
isShow.value=false
}
const onLoadData = () => {
@ -30,18 +29,92 @@ const onLoadData = () => {
if (res.code == 200) {
items.value = res.data.items || []
title.value = `会话记录(${items.value.length})`
// title.value = `(${items.value.length})`
}
})
}
const dropdown=ref({
show:false,
x:'',
y:'',
options:[] as any,
item:{} as ITalkRecord,
})
const onConvertText =async (data: ITalkRecord) => {
data.is_convert_text = 1
const res = await voiceToText({msgId:data.msg_id,voiceUrl:data.extra.url})
if(res.code == 200){
data.extra.content = res.data.convText
}
}
const onloseConvertText=(data: ITalkRecord)=>{
data.is_convert_text = 0
}
const evnets = {
convertText: onConvertText,
closeConvertText:onloseConvertText
}
const onContextMenuHandle=(key:string)=>{
evnets[key] && evnets[key](dropdown.value.item)
closeDropdownMenu()
}
const closeDropdownMenu=()=>{
dropdown.value.show=false
}
onMounted(() => {
onLoadData()
})
const onContextMenu = (e:any,item: ITalkRecord) => {
dropdown.value.show=true
dropdown.value.x=e.clientX
dropdown.value.y=e.clientY
if(item.is_convert_text === 1){
dropdown.value.options=[{ label: '关闭转文字', key: 'closeConvertText' }]
}else{
dropdown.value.options=[{ label: '转文字', key: 'convertText' }]
}
dropdown.value.item=item
}
</script>
<template>
<n-modal
<customModal :closable="false" customCloseBtn v-model:show="isShow" :title="title" style="width: 997px;background-color: #F9F9FD;" :on-after-leave="onMaskClick">
<template #content>
<div class="main-box bg-#fff me-scrollbar me-scrollbar-thumb">
<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)">
<im-avatar :src="item.avatar" :size="38" :username="item.nickname" />
</div>
<div class="right-box">
<div class="msg-header">
<span class="name">{{ item.nickname }}</span>
<span class="time"> {{ item.created_at }}</span>
</div>
<component
@contextmenu.prevent="onContextMenu($event,item)"
:is="MessageComponents[item.msg_type] || 'unknown-message'"
:extra="item.extra"
:data="item"
/>
</div>
</div>
</div>
<!-- 右键菜单 -->
<n-dropdown :show="dropdown.show" :x="dropdown.x" :y="dropdown.y" style="width: 142px;" :options="dropdown.options"
@select="onContextMenuHandle" @clickoutside="closeDropdownMenu" />
</template>
</customModal>
<!-- <n-modal
v-model:show="isShow"
preset="card"
:title="title"
@ -80,7 +153,7 @@ onMounted(() => {
</div>
</div>
</div>
</n-modal>
</n-modal> -->
</template>
<style lang="less" scoped>
@ -94,10 +167,12 @@ onMounted(() => {
min-height: 38px;
display: flex;
margin-bottom: 10px;
padding: 5px 15px;
padding: 24px 42px;
.im-message-text{
background-color: #fff;
}
.left-box {
width: 30px;
width: 38px;
display: flex;
user-select: none;
padding-top: 8px;

View File

@ -7,7 +7,8 @@ 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'
import { ArrowDownload16Filled } from '@vicons/fluent'
import { download } from '@/utils/functions.js'
//
const props = defineProps({
//
@ -83,10 +84,36 @@ const circumference = computed(() => 2 * Math.PI * radius)
const strokeDashoffset = computed(() =>
circumference.value * (1 - (props.extra.percentage || 0) / 100)
)
//
const handleClick = () => {
console.log('handleClick')
window.open(
`${window.location.origin}/office?url=${props.extra.path}`,
'_blank',
'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
);
}
function downloadFileWithProgress(resourceUrl, filename) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = resourceUrl;
document.body.appendChild(iframe);
setTimeout(() => {
document.body.removeChild(iframe);
}, 60000);
}
//
const handleDownload = () => {
downloadFileWithProgress(props.extra.path,props.extra.name)
}
</script>
<template>
<div class="file-message">
<div class="file-message" @click="handleClick">
<!-- 文件头部信息 -->
<div class="file-header">
<!-- 文件名 -->
@ -136,7 +163,14 @@ const strokeDashoffset = computed(() =>
</div>
</div>
<!-- 文件大小信息 -->
<div class="file-size">{{ fileFormatSize(extra.size) }}</div>
<div class="flex justify-between items-center">
<div class="file-size">{{ fileFormatSize(extra.size) }}</div>
<div class="flex items-center" v-if="!extra.is_uploading">
<div class="flex items-center" @click.stop="handleDownload"> <img class="w-11.7px h-11.74px mr-7px" src="@/assets/image/dofd.png" alt=""> <span class="text-12px text-#46299D">下载</span></div>
</div>
</div>
</div>
</template>

View File

@ -33,7 +33,7 @@ const onClick = () => {
<span>转发聊天会话记录 ({{ extra.msg_ids.length }})</span>
</div>
<ForwardRecord v-if="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" />
<ForwardRecord v-model:show="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" />
</section>
</template>

View File

@ -43,7 +43,7 @@ textContent = textReplaceEmoji(textContent)
min-height: 30px;
padding: 3px;
color: var(--im-message-left-text-color);
background: var(--im-message-left-bg-color);
background: #F4F4FC;
border-radius: 0px 10px 10px 10px;
font-size: 14px;
&.right {

View File

@ -15,8 +15,7 @@ const { showUserInfoModal } = useInject()
<div class="sys-text">
<template v-for="(user, index) in extra.members" :key="index">
{{ data }}
<a @click="showUserInfoModal(user.user_id)">{{ user.nickname }}</a>
<a @click="showUserInfoModal(user.erp_user_id,user.user_id)">{{ user.nickname }}</a>
<em v-show="index < extra.members.length - 1"></em>
</template>

View File

@ -13,6 +13,7 @@ const { showUserInfoModal } = useInject()
<template>
<div class="im-message-sys-text">
<div class="sys-text">
<a @click="showUserInfoModal(extra.owner_id)">
{{ extra.owner_name }}
</a>

View File

@ -127,11 +127,12 @@ const onCancel = () => {
const onSubmit = () => {
let data = checkedFilter.value.map((item: any) => {
return {
id: item.id,
type: item.type
receiver_id: item.receiver_id,
talk_type: item.talk_type
}
})
console.log('data', data);
console.log('checkedFilter.value', checkedFilter.value);
emit('on-submit', data)
}

View File

@ -170,10 +170,25 @@ const onToTalk = () => {
const onAfterEnter = () => {
onLoadData()
}
const onAfterLeave = () => {
// loading.value = true
userInfo.value = {
id: 0,
avatar: '',
gender: 0,
mobile: '',
motto: '',
nickname: '',
remark: '',
email: '',
status: 1,
text: ''
}
}
</script>
<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-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-leave="onAfterLeave" :on-after-enter="onAfterEnter">
<div class="section relative px-7px pt-82px pb-20px">
<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="">
@ -181,8 +196,8 @@ const onAfterEnter = () => {
<template v-if="loading">
<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-skeleton circle height="59px" width="59px" />
<div class="w-59px h-59px rounded-8px mr-12px">
<n-skeleton height="59px" width="59px" />
</div>
<div class="w-full">
<n-skeleton text style="width: 80%; margin-bottom: 5px;" />

View File

@ -118,7 +118,7 @@ class Talk extends Base {
*/
showMessageNocice() {
if (useSettingsStore().isLeaveWeb) {
const notification = new Notification('LumenIM 在线聊天', {
const notification = new Notification('IM 在线聊天', {
dir: 'auto',
lang: 'zh-CN',
body: '您有新的消息请注意查收'

View File

@ -1,17 +1,5 @@
import { reactive, nextTick, computed, h, inject } from 'vue'
import { ISession } from '@/types/chat'
import { renderIcon } from '@/utils/util'
import {
ArrowUp,
ArrowDown,
Logout,
Delete,
Clear,
Remind,
CloseRemind,
EditTwo,
IdCard
} from '@icon-park/vue-next'
import { ServeTopTalkList, ServeDeleteTalkList, ServeSetNotDisturb } from '@/api/chat'
import { useDialogueStore, useTalkStore } from '@/store'
import { ServeSecedeGroup } from '@/api/group'
@ -52,45 +40,45 @@ export function useSessionMenu() {
if (item.talk_type == 1) {
options.push({
icon: renderIcon(IdCard),
label: '好友信息',
key: 'info'
})
options.push({
icon: renderIcon(EditTwo),
label: '修改备注',
key: 'remark'
})
}
options.push({
icon: renderIcon(item.is_top ? ArrowDown : ArrowUp),
label: item.is_top ? '取消置顶' : '会话置顶',
key: 'top'
})
options.push({
icon: renderIcon(item.is_disturb ? Remind : CloseRemind),
label: item.is_disturb ? '关闭免打扰' : '开启免打扰',
key: 'disturb'
})
options.push({
icon: renderIcon(Clear),
label: '移除会话',
key: 'remove'
})
if (item.talk_type == 1) {
options.push({
icon: renderIcon(Delete),
label: '删除好友',
key: 'delete_contact'
})
} else {
options.push({
icon: renderIcon(Logout),
label: '退出群聊',
key: 'signout_group'
})

View File

@ -79,6 +79,7 @@ export const useTalkRecord = (uid: number) => {
loadConfig.status = 0
let scrollHeight = 0
console.log('加载数据列表load')
const el = document.getElementById('imChatPanel')
if (el) {
scrollHeight = el.scrollHeight
@ -88,7 +89,6 @@ export const useTalkRecord = (uid: number) => {
if (code != 200) {
return (loadConfig.status = 1)
}
// 防止对话切换过快,数据渲染错误
if (
request.talk_type != loadConfig.talk_type ||
@ -116,11 +116,15 @@ 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
}, 50)
}, 500)
} else {
console.log('request.cursor !== 0')
el.scrollTop = el.scrollHeight - scrollHeight
}
}

View File

@ -131,7 +131,7 @@ const isActive = (menu) => {
<footer class="menu-footer">
<div>
<a class="pointer" href="https://github.com/gzydong/LumenIM" target="_blank">
<a class="pointer" href="https://github.com/gzydong/IM" target="_blank">
<github-one theme="outline" size="22" :fill="color" :strokeWidth="2" />
</a>
</div>

View File

@ -40,6 +40,11 @@ const routes = [
path: '/:pathMatch(.*)*',
name: '404 NotFound',
component: () => import('@/views/other/not-found.vue')
},
{
path: '/office',
name: 'office',
component: () => import('@/views/office/index.vue')
}
]

View File

@ -5,9 +5,8 @@ import {
ServePublishMessage,
ServeCollectEmoticon
} from '@/api/chat'
import { ServeGetGroupMembers } from '@/api/group'
import { ServeGetGroupMembers,ServeGroupDetail } from '@/api/group.js'
import { useEditorStore } from './editor'
// 键盘消息事件定时器
let keyboardTimeout = null
@ -46,7 +45,7 @@ export const useDialogueStore = defineStore('dialogue', {
// 是否显示会话列表
isShowSessionList: true,
groupInfo: {} ,
// 群成员列表
members: [],
@ -75,8 +74,6 @@ export const useDialogueStore = defineStore('dialogue', {
// 更新对话信息
setDialogue(data = {}) {
console.log('data',data)
this.online = data.is_online == 1
this.talk = {
username: data.remark || data.name,
@ -94,6 +91,8 @@ export const useDialogueStore = defineStore('dialogue', {
this.members = []
if (data.talk_type == 2) {
this.updateGroupMembers()
this.getGroupInfo()
}
},
@ -126,7 +125,14 @@ export const useDialogueStore = defineStore('dialogue', {
unshiftDialogueRecord(records) {
this.records.unshift(...records)
},
async getGroupInfo(){
const { code, data } = await ServeGroupDetail({
group_id: this.talk.receiver_id
})
if(code == 200){
this.groupInfo = data
}
},
// 推送对话记录
addDialogueRecord(record) {
// TOOD 需要通过 sequence 排序,保证消息一致性

View File

@ -93,6 +93,7 @@ export const useTalkStore = defineStore('talk', {
resp.then(({ code, data }) => {
if (code == 200) {
this.items = data.items.map((item: any) => {
const value = formatTalkItem(item)
@ -104,7 +105,6 @@ export const useTalkStore = defineStore('talk', {
if (value.is_robot == 1) {
value.is_online = 1
}
return value
})

View File

@ -130,3 +130,22 @@ export interface ITalkRecordExtraImage {
width: number
height: number
}
export interface GroupInfo {
avatar: string;
created_at: string;
deptInfos: any[]; // 如果有具体结构可以进一步细化
group_id: number;
group_name: string;
group_num: number;
group_type: number;
is_disturb: number;
is_last_manager: boolean;
is_manager: boolean;
is_mute: number;
is_overt: number;
latest_notice_content: string;
latest_notice_title: string;
positionInfos: any[]; // 如果有具体结构可以进一步细化
profile: string;
visit_card: string;
};

View File

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

View File

@ -3,14 +3,14 @@ import { isElectronMode } from '@/utils/common'
</script>
<template>
<div id="logo-name" v-if="!isElectronMode()">Lumen IM</div>
<div id="logo-name" v-if="!isElectronMode()"></div>
<section class="section">
<router-view />
</section>
<div class="copyright">
<span>©2020 - 2023 Lumen IM 在线聊天</span>
<span>©2020 - 2023 在线聊天</span>
<span><a href="http://beian.miit.gov.cn" target="_blank">黔ICP备20006767号-2</a></span>
<span>Github源码</span>
</div>

View File

@ -1,8 +1,8 @@
<template>
<div class="amicable flex-center">
<div class="content">
<img src="@/assets/image/welcome.svg" alt="" />
<p>LumenIM 欢迎您 (*^__^*)</p>
<img class="w-181px h-149px" src="@/assets/image/zu6146@2x.png" alt="" />
<p class="text-#999999 text-14px">开启你的聊天之旅</p>
</div>
</div>
</template>
@ -13,8 +13,8 @@
width: 100%;
-webkit-app-region: drag;
.content {
width: 400px;
height: 300px;
width: 181px;
height: 149px;
text-align: center;
color: #ccc;
margin-top: -10%;

View File

@ -258,15 +258,22 @@ const state = reactive({
})
const items = computed((): ISession[] => {
if (searchKeyword.value.length === 0) {
return talkStore.talkItems
let filtered = talkStore.talkItems
if (searchKeyword.value.length > 0) {
filtered = filtered.filter((item: ISession) => {
let keyword = item.remark || item.name
return keyword.toLowerCase().indexOf(searchKeyword.value.toLowerCase()) != -1
})
}
return talkStore.talkItems.filter((item: ISession) => {
let keyword = item.remark || item.name
//
const topItems = filtered
.filter(item => item.is_top === 1)
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
const normalItems = filtered.filter(item => item.is_top !== 1)
return keyword.toLowerCase().indexOf(searchKeyword.value.toLowerCase()) != -1
})
return [...topItems, ...normalItems]
})
watch(
() => talkStore,

View File

@ -19,7 +19,7 @@ const labelColor=[
</script>
<template>
<div class="talk pointer" :class="{ actived: active }" @click="emit('tab-talk', data)">
<div :class="`talk pointer ${data.is_top === 1 ? 'bg-#F3F3F3' : ''} ${active ? 'actived' : ''}`" @click="emit('tab-talk', data)">
<div class="avatar-box relative">
<avatarModule showGroupType :mode="data?.group_type === 0 ? 1 : 2"
@ -57,11 +57,13 @@ const labelColor=[
</div>
<div class="tip">
<div v-if="data.is_disturb" class="disturb">
<div v-if="data.is_disturb" class="disturb flex justify-center items-center">
<!-- <n-icon :component="CloseRemind" /> -->
<span class="badge">
{{ data.unread_num > 99 ? '99+' : data.unread_num }}
<span class="badge w-50px">
<!-- {{ data.unread_num > 99 ? '99+' : data.unread_num }} -->
<img src="@/assets/image/xxxx@2x.png" class="w-11.1px h-13px mr-6px" alt="">
<span v-if="data.unread_num>0" class="w-10px h-10px bg-#D03050 rounded-50%"></span>
</span>
</div>
@ -193,7 +195,6 @@ const labelColor=[
display: flex;
padding-left: 5px;
align-items: center;
.unread {
color: #8f959e;
font-size: 12px;
@ -216,7 +217,7 @@ const labelColor=[
}
}
--actived-bg: #ececec;
--actived-bg: #EEE9F8;
&:hover,
&.actived {

View File

@ -39,20 +39,20 @@ const onMultiDelete = () => {
dialogueStore.ApiDeleteRecord(msgIds)
}
const onContactModal = (data: { id: number; type: number }[]) => {
const onContactModal = (data: { receiver_id: number; talk_type: number }[]) => {
let msg_ids = dialogueStore.selectItems.map((item: any) => item.msg_id)
let user_ids: number[] = []
let group_ids: number[] = []
for (let o of data) {
if (o.type == 1) {
user_ids.push(o.id)
if (o.talk_type == 1) {
user_ids.push(o.receiver_id)
} else {
group_ids.push(o.id)
group_ids.push(o.receiver_id)
}
}
console.log('user_ids',user_ids)
dialogueStore.ApiForwardRecord({
mode: forwardMode.value,
message_ids: msg_ids,

View File

@ -227,6 +227,7 @@ const onClickNickname = (data: ITalkRecord) => {
//
const onContextMenu = (e: any, item: ITalkRecord) => {
console.log('item',item)
if (!dialogueStore.isShowEditor || dialogueStore.isOpenMultiSelect) {
return e.preventDefault()
}
@ -313,7 +314,7 @@ onMounted(() => {
'multi-select-check': item.isCheck
}">
<!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column">
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0">
<n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" />
</aside>
<!-- 头像信息 -->

View File

@ -7,6 +7,7 @@ const dialogueStore = useDialogueStore()
//
const onSkipBottom = () => {
console.log('onSkipBottom')
let el = document.getElementById('imChatPanel')
if (el) {
el.scrollTo({

View File

@ -1,4 +1,5 @@
import { reactive } from 'vue'
import { useDialogueStore } from '@/store/modules/dialogue.js'
interface IDropdown {
options: any[]
@ -19,7 +20,7 @@ const isRevoke = (uid: any, item: any): boolean => {
return Math.floor(time / 1000 / 60) <= 2
}
const dialogueStore = useDialogueStore()
export function useMenu() {
const dropdown: IDropdown = reactive({
options: [],
@ -47,20 +48,20 @@ export function useMenu() {
dropdown.options.push({ label: '多选', key: 'multiSelect' })
dropdown.options.push({ label: '引用', key: 'quote' })
if (isRevoke(uid, item)) {
if (isRevoke(uid, item)|| (dialogueStore.groupInfo as any).is_manager) {
dropdown.options.push({ label: `撤回`, key: 'revoke' })
}
dropdown.options.push({ label: '删除', key: 'delete' })
if ([3, 4, 5].includes(item.msg_type)) {
dropdown.options.push({ label: '下载', key: 'download' })
}
// if ([3, 4, 5].includes(item.msg_type)) {
// dropdown.options.push({ label: '下载', key: 'download' })
// }
if ([3].includes(item.msg_type)) {
dropdown.options.push({ label: '收藏', key: 'collect' })
}
// if ([3].includes(item.msg_type)) {
// dropdown.options.push({ label: '收藏', key: 'collect' })
// }
dropdown.x = e.clientX

116
src/views/office/index.vue Normal file
View File

@ -0,0 +1,116 @@
<template>
<DocumentEditor
id="docEditor"
:documentServerUrl="documentServerUrl"
:config="config"
:events_onDocumentReady="onDocumentReady"
:onLoadComponentError="onLoadComponentError"
/>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { DocumentEditor } from "@onlyoffice/document-editor-vue"
const documentServerUrl = 'https://onlyoffice.fontree.cn'
const route = useRoute()
onMounted(() => {
// Content-Security-Policy meta
if (!document.querySelector('meta[http-equiv="Content-Security-Policy"]')) {
const meta = document.createElement('meta')
meta.httpEquiv = 'Content-Security-Policy'
meta.content = 'upgrade-insecure-requests'
document.head.appendChild(meta)
}
})
//
function getDocumentTypes(url) {
const extension = url.split('.').pop().toLowerCase()
const types = {
'docx': { fileType: 'docx', documentType: 'word' },
'doc': { fileType: 'doc', documentType: 'word' },
'xlsx': { fileType: 'xlsx', documentType: 'cell' },
'xls': { fileType: 'xls', documentType: 'cell' },
'pptx': { fileType: 'pptx', documentType: 'slide' },
'ppt': { fileType: 'ppt', documentType: 'slide' },
'pdf': { fileType: 'pdf', documentType: 'word' }
}
return types[extension] || { fileType: 'docx', documentType: 'word' }
}
const url = route.query.url
if (!url) {
alert('请提供文档 URL 参数')
}
const fileName = url ? url.split('/').pop() : ''
const { fileType, documentType } = getDocumentTypes(url || '')
const config = {
document: {
fileType,
key: 'doc_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15),
title: fileName,
url
},
documentType,
editorConfig: {
mode: 'view',
lang: 'zh-CN',
user: {
id: 'user_' + new Date().getTime(),
name: '访客用户'
},
customization: {
hideRightMenu: true, //
about: false, //
help: false, //
chat: false,
commentAuthorOnly: false,
compactToolbar: true,
hideRightMenu: false,
compatibility: true,
showReviewChanges: false,
loaderLogo: '', // logo
logo: {
image: '', //
imageDark: '', //
url: '', //
visible: false // false logo
}
}
}
}
const onDocumentReady = () => {
console.log("文档加载完成")
}
const onLoadComponentError = (errorCode, errorDescription) => {
switch(errorCode) {
case -1: //
console.log(errorDescription)
break
case -2: // DocsAPI
console.log(errorDescription)
break
case -3: // DocsAPI
console.log(errorDescription)
break
}
}
</script>
<style>
iframe[name="frameEditor"] {
width: 100% !important;
height: 100vh !important;
min-height: 100vh !important;
border: none !important;
display: block;
}
</style>

View File

@ -39,5 +39,5 @@
"src/**/*.tsx",
"src/**/*.vue",
"assets/**/*.jpg"
],
, "src/store/modules/dialogue.js" ],
}