This commit is contained in:
zdg 2024-08-14 16:31:08 +08:00
parent 8df3871f7d
commit 1c67ce1b8f
14 changed files with 1640 additions and 99 deletions

View File

@ -33,6 +33,7 @@
"electron-updater": "^6.1.7", "electron-updater": "^6.1.7",
"element-plus": "^2.7.6", "element-plus": "^2.7.6",
"fabric": "^5.3.0", "fabric": "^5.3.0",
"im_electron_sdk": "^8.0.5904",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"jsondiffpatch": "0.6.0", "jsondiffpatch": "0.6.0",

20
src/main/chat.js Normal file
View File

@ -0,0 +1,20 @@
/**
* @description 腾讯云-即时通讯-sdkID
*/
// import { ipcMain } from 'electron'
// const TimMain = require('im_electron_sdk/dist/main')
import TimMain from 'im_electron_sdk/dist/main'
// import {TIMErrCode} from 'im_electron_sdk/dist/enumbers'
const sdkappidDef = 1600034736 // 可以去腾讯云即时通信IM控制台申请
// 初始化
function init(sdkappid = sdkappidDef) {
return new TimMain({sdkappid})
}
export function initialize(){
// ipcMain.handle('im-chat:init', (event, sdkappid) => {
// return init(sdkappid)
// })
return init()
}
export default { initialize, init }

View File

@ -3,6 +3,7 @@ import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils' import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import File from './file' import File from './file'
import chat from './chat' // chat封装
// 代理 electron/remote // 代理 electron/remote
// 第一步引入remote // 第一步引入remote
import remote from '@electron/remote/main' import remote from '@electron/remote/main'
@ -226,12 +227,15 @@ app.on('window-all-closed', () => {
// 监听全局事件 // 监听全局事件
function handleAll() { function handleAll() {
// chat.initialize() // im-chat 实例
const chatInstance = chat.initialize() // im-chat 实例
// 新窗口创建-监听 // 新窗口创建-监听
ipcMain.on('new-window', (e, data) => { ipcMain.on('new-window', (e, data) => {
const { id, type } = data const { id, type } = data
const win = BrowserWindow.fromId(id) const win = BrowserWindow.fromId(id)
win.type = type // 绑定独立标识 win.type = type // 绑定独立标识
remote.enable(win.webContents) // 开启远程服务 remote.enable(win.webContents) // 开启远程服务
chatInstance.enable(win.webContents) // 开启im-chat
}) })
// 用于监听-状态管理变化-同步所有窗口 // 用于监听-状态管理变化-同步所有窗口
ipcMain.handle('pinia-state-change', (e, storeName, jsonStr) => { ipcMain.handle('pinia-state-change', (e, storeName, jsonStr) => {
@ -245,16 +249,8 @@ function handleAll() {
}) })
// 用于监听-状态管理变化-初始同步 // 用于监听-状态管理变化-初始同步
ipcMain.handle('pinia-state-init', (e, wid, storeName, jsonStr) => { ipcMain.handle('pinia-state-init', (e, wid, storeName, jsonStr) => {
// for(const curWin of BrowserWindow.getAllWindows()){ // console.log('pinia-state-init', jsonStr)
// const id = curWin.webContents.id
// const bool = id !== e.sender.id && !curWin.isDestroyed()
// if (bool) { // 除了消息发送窗口和销毁的窗口 其他都发送
// curWin.webContents.send('pinia-state-set', storeName, jsonStr)
// }
// }
console.log('pinia-state-init', jsonStr)
const win = BrowserWindow.fromId(wid) const win = BrowserWindow.fromId(wid)
console.log(win)
win.webContents.send('pinia-state-set', storeName, jsonStr) win.webContents.send('pinia-state-set', storeName, jsonStr)
}) })
} }

View File

@ -1,78 +0,0 @@
/**
* @description: electron 封装的工具函数
* 消息整理
* tool-sphere:create 创建-悬浮球-窗口
*/
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { is } from '@electron-toolkit/utils'
// const baseUrl = 'http://localhost:5173/#' // 开发环境使用
const baseUrl = process.env['ELECTRON_RENDERER_URL']+'/#' // 开发环境使用
// 所有窗口
let allWindow = {}
// 其他已有窗口 wins
export function init() {
// 创建工具-悬浮球
ipcMain.on('tool-sphere:create', async(e, data) => {
// console.log('测试xxxx', data)
await createTools(data) // 执行逻辑
e.reply('tool-sphere:create-reply', {code: 200, msg: 'success'}) // 返回结果
})
}
/**
* @description: 创建工具
* @param {*} url 路由地址
* @param {number} [width=800] 窗口宽度
* @param {number} [height=600] 窗口高度
* @param {{}} [option={}] 自定义选项
* @author: zdg
* @date 2021-07-05 14:07:01
*/
export function createTools({url, width = 800, height = 600, option={}}) {
const { mainWindow } = allWindow||{} // 获取主窗口
const devUrl = `${baseUrl}${url}`
const buildUrl = `file://${__dirname}/index.html${url}`
const urlAll = is.dev ? devUrl : buildUrl
return new Promise((resolve) => {
let win = new BrowserWindow({
width, height,
type: 'toolbar', // 创建的窗口类型为工具栏窗口
frame: false, // 要创建无边框窗口
resizable: false, // 禁止窗口大小缩放
transparent: true, // 设置透明
alwaysOnTop: true, // 窗口是否总是显示在其他窗口之前
parent: mainWindow, // 父窗口
autoClose: true, // 关闭窗口后自动关闭
webPreferences: {
nodeIntegration: true, // nodeApi调用
contextIsolation: false, // 沙箱取消
webSecurity: false // 跨域关闭
},
...option
})
// console.log(urlAll)
// url = 'https://www.baidu.com'
console.log(urlAll)
win.loadURL(urlAll)
win.setFullScreen(true) // 设置窗口为全屏
win.setIgnoreMouseEvents(true) // 忽略鼠标事件|使窗口不可选中
win.once('ready-to-show', () => {
win.show()
resolve(win)
})
win.on('closed', () => {
win = null
})
})
}
// 保存窗口
export function setWin(win = {}) {
if (win && Object.keys(win).length){
Object.keys(win).forEach(key => {
if (!allWindow[key]) { // 不存在就保存
allWindow[key] = win[key]
}
})
}
}

View File

@ -1,8 +1,10 @@
import { contextBridge } from 'electron' import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
import TimRender from 'im_electron_sdk/dist/renderer' // im渲染部分实例
// Custom APIs for renderer // Custom APIs for renderer
const api = { const api = {
preloadPath: __dirname, // 当前preload地址
getTimRender: () => new TimRender(), // im渲染部分实例
} }
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise // renderer only if context isolation is enabled, otherwise

View File

@ -0,0 +1,26 @@
/**
* @description: 后端接口api
* @author zdg
* @date 2023-07-03
*/
import request from '@/utils/request'
// /system/user/txCloudSign
export class ApiService {
// zdg: 公共请求-处理(可进行特殊处理)
static publicHttp(url, data, method, option = {}, type) {
method = method || 'get' // 默认GET
const config = { url, method }
if (!!data) basic[method=='get'?'params':'data'] = data
if (!!option) Object.assign(config, option)
// 特殊格式处理
if (type == 'file') config.headers = { 'Content-Type': 'multipart/form-data' }
else if (type == 'json') config.headers = { 'Content-Type': 'application/json' }
else if (type == 'form') config.headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
return request(config)
}
}
// zdg: 腾讯云-即时通讯
export class imChat {
// 获取腾讯im-chat appid 签名
static getTxCloudSign = data => ApiService.publicHttp('/system/user/txCloudSign', data)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,198 @@
/**
* @description imChat 腾讯-即时通讯(无ui)
* 文档地址https://cloud.tencent.com/document/product/269/75285
* 文档地址https://cloud.tencent.com/document/product/269/63007 (electron)
* @author: zdg
* @date 2023-07-03
*/
// const TimRender = require('im_electron_sdk/dist/render')
import * as TYPES from './enumbers'
const API = window.api
// TIM生成签名
// import * as GenerateUserSig from './userSig' // 引入签名生成器
export class ImChat {
timChat // imChat对象
SDKAppID // sdkID
secretKey // key
userID // 用户id
timGroupId // 群组id
userSig // 签名
status = { // 状态
isLogin: false, // 是否登录
isConnect: false, // 是否连接
}
defOption = { // 默认配置
// 日志等级-全量日志
log_level: TYPES.TIMLogLevel.kTIMLog_Test,
// 群组类型-会议群Meeting成员上限 6000 人
group_type: TYPES.TIMGroupType.kTIMGroup_ChatRoom,
}
constructor(SDKAppID, userSig, userID, isInit) {
this.SDKAppID = SDKAppID
// this.userSig = userSig
const sig = 'eJwtjN0KgjAYQN9l16Vzcz8I3RhE9J*JV94IW-ZV6nASWfTurfTynAPnjdLNyXvoFkWIeBhN-gxK1x2cYdCMTQnlYmxW3QpjQKEo4BhjGgrKh6KfBlrtPGOMuDTYDqqfE26BWUjEeIHSrW1cL-SulHd5KI7zxDbpdh1cX0nuX7JK7HtroNerZhnnPpYz9PkCe5Mx1w__'
this.userSig = sig
this.userID = userID
window.test = this
if (isInit) return this.init()
}
// 设置配置
async setConfig() {
await this.timChat.TIMSetConfig({ // TIMSetConfigParam
json_config: { // JSONCongfig
set_config_log_level: this.defOption.log_level,
set_config_callback_log_level: this.defOption.log_level,
// set_config_is_log_output_console: true,
// set_config_user_config: { // 用户配置
// user_config_is_read_receipt: true, // true表示要收已读回执事件
// user_config_is_sync_report: true, // true表示服务端要删掉已读状态
// user_config_is_ignore_grouptips_unread: true, // true表示群tips不计入群消息已读计数
// user_config_is_is_disable_storage: false, // 是否禁用本地数据库true表示禁用false表示不禁用。默认是false
// user_config_group_getinfo_option // 获取群组信息默认选项
// user_config_group_member_getinfo_option // 获取群组成员信息默认选项
// },
// set_config_user_define_data // 自定义数据,如果需要,初始化前设置
},
user_data: '',
})
// 日志监听
this.timChat.TIMSetLogCallback({
callback: data => {
console.log('[im-chat]', data[1])
},
user_data: ''
})
}
// 初始化-imChat
init() {
return new Promise(async(resolve, reject) => {
try {
if(!API) reject('preload api获取失败, 初始化-未完成')
this.timChat = await API.getTimRender()
await this.timChat.TIMInit()
console.log('[im-chat]:初始化成功')
this.status.isConnect = true
this.setConfig() // 设置日志级别
resolve(this)
} catch (error) {reject(error)}
})
}
// 生成签名
genTestUserSig() {
const options = {
SDKAppID: this.SDKAppID,
secretKey: this.secretKey,
userID: this.userID,
}
const { userSig } = GenerateUserSig.genTestUserSig(options)
this.userSig = userSig
}
// 监听
watch(callback) {
this.timChat.TIMAddRecvNewMsgCallback({
callback, user_data: {type:'msg'}
})
}
// 登录
login() {
const fn = async (resolve, reject) => {
const option = {
userID: this.userID,
userSig: this.userSig,
}
// 获取登录状态
// [1,2,3,4] | [已登陆,登录中,未登录,登出中]
console.log('登录', this)
const status = await this.timChat.TIMGetLoginStatus()
if (status == 3) { // 未登录
const res = await this.timChat.TIMLogin(option)
if (res && res.code == 0) {
console.log('登录成功', res)
this.status.isLogin = true
resolve({status:0, msg:'登录成功', data:res})
} else reject(res)
} else {
if (status == 1) { // 已登录
console.log('已登录')
resolve({status, msg:'已登录'})
} else if (status == 2) { // 登录中
console.log('登录中')
resolve({status, msg:'登录中'})
} else if (status == 4) { // 登出中
console.log('登出中')
resolve({status, msg:'登出中'})
}
}
}
return new Promise(fn)
}
// 登出
logout() {
if (!this.timChat) return
return this.timChat.TIMLogout().then(res => {
console.log('登出成功', res)
this.status.isLogin = false
return res
}).catch(error => {
console.log('登出失败', error)
return error
})
}
// 创建群组 群名和初始成员 userID
createGroup(name, memberList=[]) {
if (!this.timChat) return
if (!!this.timGroupId) return console.log('群组已存在')
// 转换初始成员iduserID 转 group_member_info_identifier
if (memberList && memberList.length) {
memberList = memberList.map(o => ({group_member_info_identifier:o.userID}))
}
const option = { // CreateGroupParams
params: { // GroupParams
// create_group_param_group_name 群组名称(必填)
// create_group_param_group_id 群组ID,不填时创建成功回调会返回一个后台分配的群ID
// create_group_param_group_type 群组类型,默认为Public
// create_group_param_group_member_array 群组初始成员数组
// create_group_param_notification 群组公告
// create_group_param_introduction 群组简介
// create_group_param_face_url 群组头像URL
// create_group_param_add_option 加群选项默认为Any
// create_group_param_max_member_num 群组最大成员数
// create_group_param_custom_info 请参考自定义字段
create_group_param_group_name: name,
create_group_param_group_type: this.defOption.group_type,
create_group_param_max_member_num: 200,
create_group_param_group_member_array: memberList
},
data: '', // 用户自定义数据
}
// @TGS#3XVNI6ZOG
return this.timChat.TIMGroupCreate(option).then(res => {
if (res && res.code == 0) {
const timGroupId = res?.json_param?.create_group_result_groupid
if (!!timGroupId) this.timGroupId = timGroupId
}
return res
})
}
// 删除群组
deleteGroup() {
if (!this.timGroupId) return
return this.timChat.TIMGroupDelete({
groupId: this.timGroupId,
data: '', // 用户自定义数据
})
}
// 获取群组列表
getGroupList() {
return this.timChat.getGroupList().then(res => {
console.log('获取群组列表', res)
return res
}).catch(error => {
console.log('获取群组列表失败', error)
return error
})
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,44 @@
/**
* @description: 生成签名|客户端计算 UserSig
* @author: zdg
* @date 2021-07-05 14:07:01
*/
// TIM生成签名
import LibGenerateTestUserSig from './lib-generate-test-usersig-es.min.js';
/**
* Signature expiration time, which should not be too short
* Time unit: second
* Default time: 7 * 24 * 60 * 60 = 604800 = 7days
*/
const EXPIRETIME = 604800;
/**
* Module: GenerateTestUserSig
*
* Description: Generates UserSig for testing. UserSig is a security signature designed by Tencent Cloud for its cloud services.
* It is calculated based on `SDKAppID`, `UserID`, and `EXPIRETIME` using the HMAC-SHA256 encryption algorithm.
*
* Attention: For the following reasons, do not use the code below in your commercial application.
*
* The code may be able to calculate UserSig correctly, but it is only for quick testing of the SDKs basic features, not for commercial applications.
* `SECRETKEY` in client code can be easily decompiled and reversed, especially on web.
* Once your key is disclosed, attackers will be able to steal your Tencent Cloud traffic.
*
* The correct method is to deploy the `UserSig` calculation code and encryption key on your project server so that your application can request from your server a `UserSig` that is calculated whenever one is needed.
* Given that it is more difficult to hack a server than a client application, server-end calculation can better protect your key.
*
* Reference: https://cloud.tencent.com/document/product/647/17275#Server
*/
function genTestUserSig(options) {
const { SDKAppID, secretKey, userID } = options;
const generator = new LibGenerateTestUserSig(SDKAppID, secretKey, EXPIRETIME);
const userSig = generator.genTestUserSig(userID);
return {
SDKAppID,
userSig,
};
}
export { genTestUserSig, EXPIRETIME };

View File

@ -9,7 +9,7 @@ export function shareStorePlugin({store}) {
// mutation 变量包含了变化前后的状态 // mutation 变量包含了变化前后的状态
// mutation.events: key newValue target oldValue oldTarget // mutation.events: key newValue target oldValue oldTarget
// state 是变化后的状态 // state 是变化后的状态
console.log('store.$subscribe', mutation) // console.log('store.$subscribe', mutation)
// 在存储变化的时候执行 // 在存储变化的时候执行
// const storeName = store.$id // const storeName = store.$id
// const storeName = mutation.storeId // const storeName = mutation.storeId

View File

@ -10,8 +10,8 @@ const isNode = typeof require !== 'undefined' // 是否支持node函数
const path = isNode?require('path'):{} const path = isNode?require('path'):{}
const Remote = isNode?require('@electron/remote'):{} const Remote = isNode?require('@electron/remote'):{}
const { ipcRenderer } = isNode?require('electron'):window.electron || {} const { ipcRenderer } = isNode?require('electron'):window.electron || {}
const API = isNode?window.api:{} // preload-api
import { useToolState } from '@/store/modules/tool' // 获取store状态 import { useToolState } from '@/store/modules/tool' // 获取store状态
// 常用变量 // 常用变量
const BaseUrl = isNode?process.env['ELECTRON_RENDERER_URL']+'/#':'' const BaseUrl = isNode?process.env['ELECTRON_RENDERER_URL']+'/#':''
const isDev = isNode?process.env.NODE_ENV !== 'production':'' const isDev = isNode?process.env.NODE_ENV !== 'production':''
@ -86,6 +86,12 @@ export function ipcHandle(fn,key, cb) {
*/ */
let winPdf=null let winPdf=null
export const createWindow = async (type, data) => { export const createWindow = async (type, data) => {
// console.log(path.join(process.resourcesPath, 'src/preload/index.js'))
// console.log(path.join(API.preloadPath, '/preload/index.js'))
// console.log(path.join(API.preloadPath, '/index.js'))
// console.log(API.preloadPath)
// console.log(API.getTimRender())
// return
if (!type) return console.error('createWindow: type is null') if (!type) return console.error('createWindow: type is null')
switch(type) { switch(type) {
case 'tool-sphere': { // 创建-悬浮球 case 'tool-sphere': { // 创建-悬浮球
@ -99,6 +105,7 @@ export const createWindow = async (type, data) => {
// autoClose: true, // 关闭窗口后自动关闭 // autoClose: true, // 关闭窗口后自动关闭
} }
data.isConsole = true // 是否开启控制台 data.isConsole = true // 是否开启控制台
data.isWeb = false // 是否开启web安全
data.option = {...defOption, ...option} data.option = {...defOption, ...option}
const win = await toolWindow(data) const win = await toolWindow(data)
win.type = type // 唯一标识 win.type = type // 唯一标识
@ -148,7 +155,7 @@ export const createWindow = async (type, data) => {
* @author: zdg * @author: zdg
* @date 2021-07-05 14:07:01 * @date 2021-07-05 14:07:01
*/ */
export function toolWindow({url, isConsole, option={}}) { export function toolWindow({url, isConsole, isWeb=true, option={}}) {
// width = window.screen.width // width = window.screen.width
let width = option?.width || 800 let width = option?.width || 800
let height = option?.height || 600 let height = option?.height || 600
@ -160,14 +167,12 @@ export function toolWindow({url, isConsole, option={}}) {
const config = { const config = {
width, height, width, height,
type: 'toolbar', // 创建的窗口类型为工具栏窗口 type: 'toolbar', // 创建的窗口类型为工具栏窗口
icon: path.join(__dirname, '../../resources/logo2.ico'), // icon: path.join(__dirname, '../../resources/logo2.ico'),
webPreferences: { webPreferences: {
// preload: path.join(__dirname, '../preload/index.js'), preload: path.join(API.preloadPath, '/index.js'),
preload: '@root/src/preload/index.js',
sandbox: false, sandbox: false,
nodeIntegration: true, // nodeApi调用 nodeIntegration: true, // nodeApi调用
contextIsolation: false, // 沙箱取消 contextIsolation: false, // 沙箱取消
// webSecurity: false // 跨域关闭
}, },
...option ...option
} }

View File

@ -1,6 +1,60 @@
<script setup> <script setup>
// im-chat im chat // im-chat im chat
import { onMounted, ref, reactive, watchEffect } from 'vue'
import { ImChat } from '@/plugins/imChat'
import useUserStore from '@/store/modules/user'
import * as http from '@/api/apiService' // api service
// import { ipcMsgSend, ipcHandle, ipcMain, ipcMsgInvoke } from '@/utils/tool' //
const userStore = useUserStore()
let imChat
onMounted(() => {
// console.log(userStore)
initImChat()
})
// im-chat
const initImChat = async () => {
// console.log('im-chat', userStore.user.timuserid)
try {
//
const res = await http.imChat.getTxCloudSign()
if (res && res.code == 200) {
const { sdkAppId, sign } = res.data
const { timuserid, deptId, userId } = userStore.user
//
const groupName = `${deptId}-classteaching-${userId}`
// im-chat
// await ipcMsgInvoke('im-chat:init', sdkAppId)
imChat = new ImChat(sdkAppId, sign, timuserid)
// im-chat
await imChat.init()
// im-chat
await imChat.login()
imChat.watch(res => {
console.log('im-chat watch: ', res)
})
//
await createGroup(groupName)
}
} catch (error) {
console.log('im-error: ', error)
}
}
//
const createGroup = async (groupName) => {
if (!imChat) return
const res = await imChat.createGroup(groupName)
console.log('createGroup: ', res)
console.log('createGroup2: ', imChat)
// if (res && res.code == 0) {
// const timGroupId = res?.json_param?.create_group_result_groupid
// }
}
// 退
const logout = () => imChat?.logout()
//
const deleteGroup = () => imChat?.deleteGroup()
defineExpose({ logout, deleteGroup })
</script> </script>
<template> <template>

View File

@ -10,7 +10,7 @@
<upvote-vue></upvote-vue> <upvote-vue></upvote-vue>
<!-- im-chat 聊天组件 --> <!-- im-chat 聊天组件 -->
<im-chat /> <im-chat ref="imChatRef" />
<!-- 底部工具栏 --> <!-- 底部工具栏 -->
<div class="tool-bottom-all" @mouseenter="mouseChange(0)" @mouseleave="mouseChange(1)"> <div class="tool-bottom-all" @mouseenter="mouseChange(0)" @mouseleave="mouseChange(1)">
@ -55,6 +55,7 @@ const dragtime = ref(0) // 拖拽时间-计算点击还是拖动
const isShow = ref(false) // - const isShow = ref(false) // -
const toolStore = useToolState() // const toolStore = useToolState() //
const boardVueRef=ref(null) // ref const boardVueRef=ref(null) // ref
const imChatRef = ref(null) // im-chat ref
const btnList = [ // const btnList = [ //
{ label: '选择', value: 'select', icon: 'icon-mouse' }, { label: '选择', value: 'select', icon: 'icon-mouse' },
{ label: '画笔', value: 'brush', icon: 'icon-huabi' }, { label: '画笔', value: 'brush', icon: 'icon-huabi' },
@ -113,7 +114,7 @@ const sideMouse = e => {
mouseChange(type == 'mouseleave') mouseChange(type == 'mouseleave')
} }
// : // :
const sideChange = o => { const sideChange = async o => {
// console.log(o) // console.log(o)
switch(o.prop) { switch(o.prop) {
case 'resource': // case 'resource': //
@ -124,7 +125,10 @@ const sideChange = o => {
break break
case 'over': // case 'over': //
toolStore.isToolWin = false toolStore.isToolWin = false
ipcMsgSend('tool-sphere:close') console.log(imChatRef.value)
await imChatRef.value?.deleteGroup() //
await imChatRef.value?.logout() // 退im
ipcMsgSend('tool-sphere:close') //
break break
} }
} }