Merge branch 'main' into zhuhao_dev

This commit is contained in:
朱浩 2024-08-16 13:42:55 +08:00
commit 88177d610e
23 changed files with 2335 additions and 353 deletions

View File

@ -33,6 +33,7 @@
"electron-updater": "^6.1.7",
"element-plus": "^2.7.6",
"fabric": "^5.3.0",
"im_electron_sdk": "^8.0.5904",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"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 icon from '../../resources/icon.png?asset'
import File from './file'
import chat from './chat' // chat封装
// 代理 electron/remote
// 第一步引入remote
import remote from '@electron/remote/main'
@ -229,12 +230,15 @@ app.on('window-all-closed', () => {
// 监听全局事件
function handleAll() {
// chat.initialize() // im-chat 实例
const chatInstance = chat.initialize() // im-chat 实例
// 新窗口创建-监听
ipcMain.on('new-window', (e, data) => {
const { id, type } = data
const win = BrowserWindow.fromId(id)
win.type = type // 绑定独立标识
remote.enable(win.webContents) // 开启远程服务
chatInstance.enable(win.webContents) // 开启im-chat
})
// 用于监听-状态管理变化-同步所有窗口
ipcMain.handle('pinia-state-change', (e, storeName, jsonStr) => {
@ -246,4 +250,10 @@ function handleAll() {
}
}
})
// 用于监听-状态管理变化-初始同步
ipcMain.handle('pinia-state-init', (e, wid, storeName, jsonStr) => {
// console.log('pinia-state-init', jsonStr)
const win = BrowserWindow.fromId(wid)
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 { electronAPI } from '@electron-toolkit/preload'
import TimRender from 'im_electron_sdk/dist/renderer' // im渲染部分实例
// Custom APIs for renderer
const api = {
preloadPath: __dirname, // 当前preload地址
getTimRender: () => new TimRender(), // im渲染部分实例
}
// Use `contextBridge` APIs to expose Electron APIs to
// 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) config[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)
}

View File

@ -173,3 +173,15 @@ export function endClass(id) {
params: {id}
})
}
/**
* @description 获取课堂信息
* @param {*} id
* @returns
*/
export function getClassInfo(id) {
return request({
url: '/smarttalk/classReserv/selectById',
method: 'get',
params: {id}
})
}

View File

@ -1,8 +1,12 @@
<template>
pdfAdnFabric<template>
<div class="canvasitem">
<div class="pdfAdnFabric" id="pdfAdnFabric">
<canvas id="pdf-fabric"></canvas>
<canvas id="pdf-fabric1" v-if="props.pdfObj.numberOfPdf == 2"></canvas>
<div class="pdfAdnFabric" id="pdfAdnFabric" >
<div :class="ispointer ? 'ispointer' : ''">
<canvas ref="fabriccanvas" />
</div>
<div v-if="props.pdfObj.numberOfPdf === 2" :class="ispointer ? 'ispointer' : ''">
<canvas ref="fabriccanvas1" />
</div>
</div>
</div>
</template>
@ -13,12 +17,20 @@ import {
onMounted,
watch,
reactive,
defineProps,
defineExpose,
nextTick,
defineEmits,watchEffect
} from 'vue'
import { fabric } from 'fabric'
// import { fabric } from 'fabric'
import { ElMessage } from 'element-plus'
import { handleevent, savecanvsStore, initcanvasdata, displayData } from '@/utils/pdfAndFabric'
import { fabricVue, TYPES } from '@/plugins/fabric'
import { updateSmartBookMarkContent, addsmartBookMark,getBookMarkById } from '@/api/eTextbook/index'
import {useToolState} from '@/store/modules/tool'
const { ipcRenderer } = require('electron')
const toolState = useToolState();
const props = defineProps({
pdfObj: {
type: Object,
@ -29,13 +41,16 @@ const props = defineProps({
}
}
})
const ispointer = ref(false) //
// canvas
const canvsStore = reactive({
id: 'xxxx',
pageArr: []
})
const fabriccanvas = ref(null)
const fabriccanvas1 = ref(null)
const canvasFabricVue = ref(null)
const canvas1FabricVue = ref(null)
//
const numPagesTotal = ref(0)
const imgarr = ref([])
@ -47,9 +62,9 @@ const renderPage = async (canvasobj) => {
const pdf = await pdfjsLib.getDocument(props.pdfObj.pdfUrl).promise
//
const page = await pdf.getPage(canvasobj.page)
var screenWidth = window.innerWidth/2-100;
var screenHeight = window.innerHeight;
const viewport = page.getViewport({ scale:2})
var screenWidth = window.innerWidth / 2 - 100
var screenHeight = window.innerHeight
const viewport = page.getViewport({ scale: 2 })
const canvasElement = canvasobj.canvas
canvasElement.width = viewport.width
@ -58,6 +73,11 @@ const renderPage = async (canvasobj) => {
canvasContext: canvasobj.context,
viewport: viewport
}
// console.log(renderContext,22222222222222222222)
// const textContent = await page.getTextContent();
// console.log(textContent);
// const annotations = await page.getAnnotations();
// console.log(annotations);
page.render(renderContext).promise.then((res) => {
const img = document.createElement('img')
img.src = canvasobj.canvas.toDataURL('image/png')
@ -68,33 +88,65 @@ const renderPage = async (canvasobj) => {
// pdf fabric
if (props.pdfObj.numberOfPdf == 2) {
if (canvasobj.index == 0) {
fabriccanvas.value.setWidth(screenWidth)
fabriccanvas.value.setHeight(screenHeight)
displayData(fabriccanvas, canvsStore, canvasobj, fabric, img)
canvasFabricVue.value.canvas.setWidth(screenWidth)
canvasFabricVue.value.canvas.setHeight(screenHeight)
// updateCanvasBackgroundImage(canvasFabricVue,img)
displayData(canvasFabricVue, canvsStore, canvasobj, fabric, img)
} else {
fabriccanvas1.value.setWidth(screenWidth)
fabriccanvas1.value.setHeight(screenHeight)
displayData(fabriccanvas1, canvsStore, canvasobj, fabric, img)
canvas1FabricVue.value.canvas.setWidth(screenWidth)
canvas1FabricVue.value.canvas.setHeight(screenHeight)
displayData(canvas1FabricVue, canvsStore, canvasobj, fabric, img)
}
} else {
fabriccanvas.value.setWidth(screenWidth)
fabriccanvas.value.setHeight(screenHeight)
displayData(fabriccanvas, canvsStore, canvasobj, fabric, img)
canvasFabricVue.value.canvas.setHeight(screenHeight)
displayData(canvasFabricVue, canvsStore, canvasobj, fabric, img)
}
// console.log(imgarr.value)
img.remove()
}
// imgarrJSONdatacanvsStore.pageArr
canvsStore.pageArr.forEach((item) => {
if (item.page == canvasobj.page) {
imgarr.value.forEach((img) => {
if (img.page == canvasobj.page) {
img.JSONdata = item.JSONdata
})
}
//
const savaDataStore = () => {
if(!toolState.isToolWin){
toolState.isPdfWin=false
toolState.showBoardAll=true //
ipcRenderer.invoke('tool-sphere:reset') //tool
ipcRenderer.send('open-PDF:minimize')
return
}
imgarr.value.forEach((a) => {
if (a.index == 0) {
a.JSONdata = canvasFabricVue.value.canvas.toJSON()
} else {
a.JSONdata = canvas1FabricVue.value.canvas.toJSON()
}
})
const nameMap = new Map(canvsStore.pageArr.map((item) => [item.page, item.id]))
//
let promises = []
imgarr.value.forEach((item) => {
if (nameMap.has(item.page)) {
const params = {
id: nameMap.get(item.page),
contentData: JSON.stringify(item.JSONdata.objects)
}
promises.push(updateSmartBookMarkContent([params]))
} else {
promises.push(addsmartBookMark({
pageNum: item.page,
contentData: JSON.stringify(item.JSONdata.objects),
bookId: props.pdfObj.bookId,
type: '教材',
source: 'smarttalk'
}))
}
})
Promise.all(promises).then(res=>{
toolState.isPdfWin=false
toolState.showBoardAll=true //
ipcRenderer.invoke('tool-sphere:reset') //tool
ipcRenderer.send('open-PDF:minimize')
})
}
const updatePage = (canvasobj) => {
@ -105,16 +157,55 @@ const loadPdf = async (canvasobj) => {
}
const initPdf = async (type = 'default') => {
imgarr.value.forEach((a) => {
if (a.index == 0) {
a.JSONdata = canvasFabricVue.value.canvas.toJSON()
} else {
a.JSONdata = canvas1FabricVue.value.canvas.toJSON()
}
})
// +
if (type != 'default' && toolState.isToolWin) {
const nameMap = new Map(canvsStore.pageArr.map((item) => [item.page, item.id]))
//
let promises = []
imgarr.value.forEach((item) => {
if (nameMap.has(item.page)) {
const params = {
id: nameMap.get(item.page),
contentData: JSON.stringify(item.JSONdata.objects)
}
promises.push(updateSmartBookMarkContent([params]))
} else {
promises.push(addsmartBookMark({
pageNum: item.page,
contentData: JSON.stringify(item.JSONdata.objects),
bookId: props.pdfObj.bookId,
type: '教材',
source: 'smarttalk'
}))
}
})
Promise.all(promises).then(res=>{
getBookMarkById(props.pdfObj.bookId).then(res=>{
const pageArr=getUniqueArrayByLastOccurrence(res.data)
canvsStore.pageArr=[]
pageArr.forEach((a) => {
canvsStore.pageArr.push({ page: a.pageNum, id: a.id, JSONdata: a.contentData })
})
})
})
}
//
savecanvsStore(imgarr, canvsStore)
// initcanvasdata(fabriccanvas)
// initcanvasdata(fabriccanvas1)
//
if (type == 'restone') {
// canvas
fabriccanvas1.value.clear()
// canvas
fabriccanvas1.value.dispose()
// savecanvsStore(imgarr, canvsStore)
if (props.pdfObj.numberOfPdf == 1) {
// imgarr.value[0]
canvasFabricVue.value.history.clean()
} else {
canvasFabricVue.value.history.clean()
canvas1FabricVue.value.history.clean()
}
// canvas
canvasNumbsValue.value.forEach((canvasObj) => {
@ -146,7 +237,6 @@ const initPdf = async (type = 'default') => {
} else {
canvasNumbsValue.value[i].page = props.pdfObj.numPages + 1
}
canvasNumbsValue.value[i].index = i
// FabricVue
await loadPdf(canvasNumbsValue.value[i])
@ -154,47 +244,154 @@ const initPdf = async (type = 'default') => {
}
}
}
//page
const getUniqueArrayByLastOccurrence=(array)=> {
const uniqueItems = array.reduce((acc, current) => {
// 使 Map pageNum
acc.set(current.pageNum, current);
return acc;
}, new Map());
// Map
const resultArray = Array.from(uniqueItems.values());
return resultArray;
}
const initPdfone = async () => {
setTimeout(() => {
fabriccanvas1.value = new fabric.Canvas('pdf-fabric1')
fabriccanvas1.value.isDrawingMode = true
fabriccanvas1.value.freeDrawingBrush.color = '#A33AFE'
fabriccanvas1.value.freeDrawingCursor = 'default'
fabriccanvas1.value.setWidth(860)
handleevent(fabriccanvas1.value, imgarr, 'two')
setTimeout(async () => {
const option = { freeDrawingCursor: 'default' }
const canvas2 = new fabricVue()
await canvas2.initCanvas(fabriccanvas1.value, option)
canvas2.canvas.setWidth(window.innerWidth / 2 - 100)
canvas1FabricVue.value = canvas2
await initPdf('addOnePage')
}, 0)
initPdf('addOnePage')
}
onMounted(async () => {
try {
// canvas
const pdf = await pdfjsLib.getDocument(props.pdfObj.pdfUrl).promise
numPagesTotal.value = pdf.numPages
// console.log(pdf)
// fabriccanvas
fabriccanvas.value = new fabric.Canvas('pdf-fabric')
fabriccanvas.value.setWidth(860)
fabriccanvas.value.isDrawingMode = true
fabriccanvas.value.freeDrawingBrush.color = '#A33AFE'
fabriccanvas.value.freeDrawingCursor = 'default'
fabriccanvas1.value = new fabric.Canvas('pdf-fabric1')
fabriccanvas1.value.isDrawingMode = true
fabriccanvas1.value.freeDrawingBrush.color = '#A33AFE'
fabriccanvas1.value.freeDrawingCursor = 'default'
fabriccanvas1.value.setWidth(860)
const option = { freeDrawingCursor: 'default' }
const canvas1 = new fabricVue()
// canvas1.boardConfig.mode= TYPES.ActionMode.OTHER
// canvas1.boardConfig.mode= TYPES.ActionMode.ERASE
await canvas1.initCanvas(fabriccanvas.value, option)
canvas1.canvas.setWidth(window.innerWidth / 2 - 100)
canvasFabricVue.value = canvas1
const canvas2 = new fabricVue()
await canvas2.initCanvas(fabriccanvas1.value, option)
canvas2.canvas.setWidth(window.innerWidth / 2 - 100)
// canvas2.canvas.isDrawingMode=false
canvas1FabricVue.value = canvas2
window.test = { canvas1, canvas2 }
emit('update:numPagesTotal', pdf.numPages)
initPdf()
if (props.pdfObj.allPageData.length) {
props.pdfObj.allPageData.forEach((a) => {
if (a.pageNum == 1 || a.pageNum == 2) {
canvsStore.pageArr.push({ page: a.pageNum, id: a.id, JSONdata: a.contentData })
}
})
}
await initPdf()
} catch (error) {
console.log(error)
ElMessage.error('pdf文件错误')
}
setToolStatus()
// 2canvas
handleevent(fabriccanvas.value, imgarr)
handleevent(fabriccanvas1.value, imgarr, 'two')
// handleevent(fabriccanvas.value, imgarr)
// handleevent(fabriccanvas1.value, imgarr, 'two')
})
// zdg: --
const setToolStatus = () => {
toolState.showBoardAll = false
}
//
const handleMode = (vale,type)=>{
if(vale=='select'){
ispointer.value=true
}else{
ispointer.value=false
}
switch(vale) {
case 'select': //
canvasFabricVue.value?.handleMode(TYPES.ActionMode.OTHER)
canvas1FabricVue.value?.handleMode(TYPES.ActionMode.OTHER)
break
case 'brush': //
canvasFabricVue.value?.handleMode(TYPES.ActionMode.DRAW)
canvasFabricVue.value.canvas.freeDrawingCursor = 'default'
canvas1FabricVue.value?.handleMode(TYPES.ActionMode.DRAW)
canvas1FabricVue.value.canvas.freeDrawingCursor = 'default'
break
case 'erase': //
canvasFabricVue.value?.handleMode(TYPES.ActionMode.ERASE)
canvas1FabricVue.value?.handleMode(TYPES.ActionMode.ERASE)
break
case 'clear': //
clearCanvas()
// canvas1FabricVue.value.history?.clean()
break
}
}
// canvas
const clearCanvas=()=>{
if(canvasFabricVue.value){
const objects = canvasFabricVue.value.canvas.getObjects();
objects.forEach((obj) => {
//
if (obj !== canvasFabricVue.value.canvas.backgroundImage) {
//
canvasFabricVue.value.canvas.remove(obj);
}
});
canvasFabricVue.value.canvas.renderAll();
}
if(canvas1FabricVue.value){
const objects = canvas1FabricVue.value.canvas.getObjects();
objects.forEach((obj) => {
//
if (obj !== canvas1FabricVue.value.canvas.backgroundImage) {
//
canvas1FabricVue.value.canvas.remove(obj);
}
});
canvas1FabricVue.value.canvas.renderAll();
}
}
const watchToolState=()=>{
if(toolState.showBoardAll){
setTimeout(() => {
toolState.showBoardAll=false
}, 200);
}
//
handleMode(toolState.model)
}
defineExpose({
initPdf,
initPdfone
initPdfone,
savaDataStore
})
watchEffect(() => {
setTimeout(() => {
console.log(toolState,'监听')
}, 300)
if(toolState.isPdfWin){
// if(toolState.isToolWin){
// ispointer.value=false
// }else{
// ispointer.value=true
// }
watchToolState() //
}
})
</script>
@ -204,6 +401,8 @@ defineExpose({
flex-wrap: wrap;
width: 100%;
justify-content: center;
overflow: hidden;
max-height: 100vh;
}
.pdfAdnFabric {
position: relative;
@ -214,4 +413,7 @@ defineExpose({
margin-right: 10px;
}
}
.ispointer {
pointer-events: none;
}
</style>

View File

@ -1,15 +1,9 @@
pdfAdnFabric<template>
<div class="canvasitem">
<div class="pdfAdnFabric" id="pdfAdnFabric" >
<!-- @touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp" -->
<div :class="ispointer ? 'ispointer' : ''">
<canvas ref="fabriccanvas" />
</div>
<!-- style="pointer-events: none;" -->
<div v-if="props.pdfObj.numberOfPdf === 2" :class="ispointer ? 'ispointer' : ''">
<canvas ref="fabriccanvas1" />
</div>
@ -79,8 +73,6 @@ const renderPage = async (canvasobj) => {
canvasContext: canvasobj.context,
viewport: viewport
}
// console.log(renderContext,22222222222222222222)
page.render(renderContext).promise.then((res) => {
const img = document.createElement('img')
img.src = canvasobj.canvas.toDataURL('image/png')
@ -93,7 +85,6 @@ const renderPage = async (canvasobj) => {
if (canvasobj.index == 0) {
canvasFabricVue.value.canvas.setWidth(screenWidth)
canvasFabricVue.value.canvas.setHeight(screenHeight)
// updateCanvasBackgroundImage(canvasFabricVue,img)
displayData(canvasFabricVue, canvsStore, canvasobj, fabric, img)
} else {
canvas1FabricVue.value.canvas.setWidth(screenWidth)
@ -106,16 +97,6 @@ const renderPage = async (canvasobj) => {
}
img.remove()
}
// imgarrJSONdatacanvsStore.pageArr
// canvsStore.pageArr.forEach((item) => {
// if (item.page == canvasobj.page) {
// imgarr.value.forEach((img) => {
// if (img.page == canvasobj.page) {
// img.JSONdata = item.JSONdata
// }
// })
// }
// })
})
}
//
@ -211,10 +192,7 @@ const initPdf = async (type = 'default') => {
})
}
//
// savecanvsStore(imgarr, canvsStore)
if (props.pdfObj.numberOfPdf == 1) {
// imgarr.value[0]
canvasFabricVue.value.history.clean()
} else {
canvasFabricVue.value.history.clean()
@ -393,8 +371,18 @@ defineExpose({
savaDataStore
})
watchEffect(() => {
console.log(toolState.model,'监听')
setTimeout(() => {
console.log(toolState,'监听')
}, 300)
if(toolState.isPdfWin){
// if(toolState.isToolWin){
// ispointer.value=false
// }else{
// ispointer.value=true
// }
watchToolState() //
}
})
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,279 @@
/**
* @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' // sdk相关枚举
import MsgEnum from './msgEnum' // 消息相关枚举(自定义)
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
// this.timGroupId = '@TGS#3CYWMK2ON' // 测试使用
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])
this.setConsole('%cchat-log ', 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.TIMRemoveRecvNewMsgCallback()
// 消息监听
this.timChat.TIMAddRecvNewMsgCallback({
callback, user_data: this.toStr('msg')
})
// 群消息监听
// 群组系统消息事件包括 加入群、退出群、踢出群、设置管理员、取消管理员、群资料变更、群成员资料变更。此消息是针对所有群组成员下发的
this.timChat.TIMSetGroupTipsEventCallback({
// callback, user_data: this.toStr('msg-group')
callback: (data) => {
// console.log('群消息', group_tips_event)
this.setConsole('%c群消息', data)
},
})
}
// 登录
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 && timGroupId != 'undefined'){
this.setConsole('%c创建群组成功', timGroupId)
this.timGroupId = timGroupId
// this.setGroupMsgReceive()
}
}
return res
})
}
// 删除群组
deleteGroup() {
if (!this.timGroupId) return
return this.timChat.TIMGroupDelete({
groupId: this.timGroupId,
data: '', // 用户自定义数据
})
}
// 设置群消息接收
setGroupMsgReceive(timGroupId) {
if (!this.timGroupId) this.timGroupId = timGroupId || ''
if (!this.timGroupId) return console.log('timGroupId为空')
return this.timChat.TIMMsgSetGroupReceiveMessageOpt({
groupId: this.timGroupId,
opt: TYPES.TIMReceiveMessageOpt.kTIMRecvMsgOpt_Receive,
data: '', // 用户自定义数据
})
}
// 获取群组列表
getGroupList() {
return this.timChat.getGroupList().then(res => {
console.log('获取群组列表', res)
return res
}).catch(error => {
console.log('获取群组列表失败', error)
return error
})
}
// 发送消息
sendMsg(conv_id, msg) {
if (!conv_id) return console.log('conv_id为空')
if (typeof msg == 'object') msg = JSON.stringify(msg)
const option = {
conv_id,
conv_type: TYPES.TIMConvType.kTIMConv_Group,
params: {
message_elem_array: [{
elem_type: TYPES.TIMElemType.kTIMElem_Text,
text_elem_content: msg
}],
// message_conv_id: conv_id,
// message_conv_type: TYPES.TIMConvType.kTIMConv_Group,
// message_sender: this.userID
},
user_data: '', // 用户自定义数据
// callback: (data) => {}
}
console.log('发送消息', option)
return this.timChat.TIMMsgSendMessageV2(option)
}
// 发送关闭(下课)消息
sendMsgClosed(){
const msg = this.getMsgObj(MsgEnum.HEADS.MSG_closed, '下课', MsgEnum.TYPES.TEACHER)
return this.sendMsg(this.timGroupId, msg)
}
// 获取消息对象
getMsgObj(msgHead, msg, type, sender, option={}) {
if (!msgHead) throw new Error('msgHead is required')
if (!msg) throw new Error('msg is required')
if (typeof msg === 'object') msg = JSON.stringify(msg)
return {
msgKey: msgHead,
msgcontent: msg,
msgType: type ?? MsgEnum.TYPES.STUDENT, // 默认为学生
senduserid: sender ?? this.userID,
...option
}
}
// 设置控制台样式
setConsole(hearStr,...args) {
const css = 'color: #fff;background-color:#2ccb92;padding:3px 5px;border-radius:3px;'
const time = new Date().toLocaleTimeString()
if (!hearStr) hearStr = '%c' + time
console.log(hearStr, css, ...args)
}
// 获取数据字符串
toStr = (data) => {
if (typeof data === 'string') data = {type: data}
return JSON.stringify(data)
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,135 @@
/**
* @description 消息枚举
* @author zdg
* @date 2021-07-05 14:07:01
*/
export class MsgEnum {
/**
* @description: 消息类型
*
* | 名称 | 含义 | enum |
* | ---- | ---- | ---- |
* | SYSTEM | 系统消息 | system |
* | TEACHER | 老师消息 | teacher |
* | STUDENT | 学生消息 | student |
* | NOTICE | 通知消息 | notice |
*/
static TYPES = {
/** @desc: 系统消息 */
SYSTEM: 'system',
/** @desc: 老师消息 */
TEACHER: 'teacher',
/** @desc: 学生消息 */
STUDENT: 'student',
/** @desc: 通知消息 */
NOTICE: 'notice'
}
/**
* @description: 消息头-类型
*
* | 名称 | 含义 | enum |
* | ---- | ---- | ---- |
* | --- | 以下为旧定义-消息头 | --- |
* | MSG_closed | 结束课程(下课) | closed |
* | MSG_onlineStatus | 在线状态 | onlineStatus |
* | MSG_pushQuizOfClassWorkdata2Public | 老师端把选中的学生习题作业推到大屏 | pushQuizOfClassWorkdata2Public |
* | MSG_pushClassWorkdata2Public | 老师端把选中的学生作业推到大屏 | pushClassWorkdata2Public |
* | MSG_shareStudentPresentdata2All | 把某个学生的展示成果数据推给全班所有学生 | shareStudentPresentdata2All |
* | MSG_pushStudentPresentdata2Public | 老师端课堂展示活动把选中的学生展示数据推到大屏 | pushStudentPresentdata2Public |
* | MSG_pushClassWorkPresentList2Public | 老师端课堂展示活动任务列表推到大屏 | pushClassWorkPresentList2Public |
* | MSG_activePageType | 课标研读-分页切换 | activePageType |
* | MSG_slideFlapping | 幻灯片-切换 | slideFlapping |
* | MSG_anmationclick | 幻灯片-动画切换 | anmationclick |
* | MSG_classcourseopen | 群组创建成功 | classcourseopen |
* | MSG_classquizfeedback | 学生的测练结果反馈 | classquizfeedback |
* | MSG_classtaskfeedback | 老师端接收到学生反馈消息-课堂测练中的其他任务 | classtaskfeedback |
* | MSG_studentfeedback | 老师端学生反馈的消息具体要看其中的feedbackkey类别较繁杂 | studentfeedback |
* | MSG_studentfeedbackcancel | 老师端学生反馈的消息取消如取消学会了取消困惑 | studentfeedbackcancel |
* | MSG_classshowdata | 学生提交的课堂展示数据-要在老师端显示再由老师选择推送到公屏上 | classshowdata |
* | MSG_classWorkOfPresentDataUpdate | 学生在公屏上展示并完善后保存后老师端要更新 | classWorkOfPresentDataUpdate |
* | MSG_classlecturePagesrc | 课堂讲授活动选择不同的内容 | classlecturePagesrc |
* | --- | 以下为新定义-消息头 | --- |
* | MSG_0001 | 点赞 | 0x0001 |
* | MSG_0002 | xx | 0x0002 |
* | MSG_0003 | xx | 0x0003 |
*/
static HEADS = {
// === 旧定义-消息头(兼容以前) ===
/** @desc: 结束课程(下课) */
MSG_closed : 'closed',
/** @desc: 在线状态 */
MSG_onlineStatus : 'onlineStatus',
/** @desc: 老师端:把选中的学生习题作业,推到大屏 */
MSG_pushQuizOfClassWorkdata2Public : 'pushQuizOfClassWorkdata2Public',
/** @desc: 老师端:把选中的学生作业,推到大屏 */
MSG_pushClassWorkdata2Public : 'pushClassWorkdata2Public',
/** @desc: 把某个学生的展示成果数据推给全班所有学生 */
MSG_shareStudentPresentdata2All : 'shareStudentPresentdata2All',
/** @desc: 老师端:课堂展示活动,把选中的学生展示数据,推到大屏 */
MSG_pushStudentPresentdata2Public : 'pushStudentPresentdata2Public',
/** @desc: 老师端:课堂展示活动,任务列表,推到大屏 */
MSG_pushClassWorkPresentList2Public : 'pushClassWorkPresentList2Public',
/** @desc: 课标研读-分页切换 */
MSG_activePageType : 'activePageType',
/** @desc: 幻灯片-切换 */
MSG_slideFlapping : 'slideFlapping',
/** @desc: 幻灯片-动画切换 */
MSG_anmationclick : 'anmationclick',
/** @desc: 群组创建成功 */
MSG_classcourseopen : 'classcourseopen',
/** @desc: 学生的测练结果反馈 */
MSG_classquizfeedback : 'classquizfeedback',
/** @desc: 老师端:接收到学生反馈消息-课堂测练中的其他任务 */
MSG_classtaskfeedback : 'classtaskfeedback',
/** @desc: 老师端学生反馈的消息具体要看其中的feedbackkey类别较繁杂 */
MSG_studentfeedback : 'studentfeedback',
/** @desc: 老师端:学生反馈的消息取消,如取消学会了,取消困惑 */
MSG_studentfeedbackcancel : 'studentfeedbackcancel',
/** @desc: 学生提交的课堂展示数据-要在老师端显示,再由老师选择推送到公屏上 */
MSG_classshowdata : 'classshowdata',
/** @desc: 学生在公屏上展示并完善后,保存后,老师端要更新 */
MSG_classWorkOfPresentDataUpdate : 'classWorkOfPresentDataUpdate',
/** @desc: 课堂讲授活动,选择不同的内容 */
MSG_classlecturePagesrc : 'classlecturePagesrc',
// === 新定义-消息头 ===
/** @desc: 点赞 */
MSG_0001: 0x0001,
MSG_0002: 0x0002,
MSG_0003: 0x0003,
MSG_0004: 0x0004,
MSG_0005: 0x0005,
MSG_0006: 0x0006,
MSG_0007: 0x0007,
MSG_0008: 0x0008,
MSG_0009: 0x0009,
MSG_0010: 0x000a,
MSG_0011: 0x000b,
MSG_0012: 0x000c,
MSG_0013: 0x000d,
MSG_0014: 0x000e,
MSG_0015: 0x000f,
MSG_0016: 0x0010,
MSG_0017: 0x0011,
MSG_0018: 0x0012,
MSG_0019: 0x0013,
MSG_0020: 0x0014,
MSG_0021: 0x0015,
MSG_0022: 0x0016,
MSG_0023: 0x0017,
MSG_0024: 0x0018,
MSG_0025: 0x0019,
MSG_0026: 0x001a,
MSG_0027: 0x001b,
MSG_0028: 0x001c,
MSG_0029: 0x001d,
MSG_0030: 0x001e,
MSG_0031: 0x001f,
MSG_0032: 0x0020,
MSG_0033: 0x0021,
MSG_0034: 0x0022,
MSG_0035: 0x0023,
}
}
export { MsgEnum as default }

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

@ -3,26 +3,60 @@
*/
const isNode = typeof require !== 'undefined' // 是否支持node函数
const { ipcRenderer } = isNode?require('electron'):{} // app使用
// const Remote = isNode?require('@electron/remote'):{} // 远程模块
export function shareStorePlugin({store}) {
store.$subscribe(() => { // 自动同步
store.$subscribe((mutation, state) => { // 自动同步
// mutation 变量包含了变化前后的状态
// mutation.events: key newValue target oldValue oldTarget
// state 是变化后的状态
// console.log('store.$subscribe', mutation)
// 在存储变化的时候执行
const storeName = store.$id
// const storeName = store.$id
// const storeName = mutation.storeId
const { storeId: storeName, payload, events, type } = mutation // direct
// if (!Object.keys(payload).length) return
if (type != 'direct') return
// 用于多窗口共享(需要共享的状态名称)
const names = ['tool']
if (names.includes(storeName)) stateSync(store) // 需要同步
if (names.includes(storeName)) stateSync(storeName, events.key, events.newValue) // 需要同步
})
// 暴露方法-手动同步
store.stateSync = () => stateSync(store)
store.stateSync = (storeName, key, value) => {
if (!storeName && !!key && !!value) stateSync(storeName, key, value)
else stateSyncAll(store)
}
// 暴露方法-发送当前状态-新窗口
store.stateSyncInit = wid => stateSyncInit(wid, store)
// 监听主线程消息-同步数据
stateChange(store)
}
// 同步数据-发送给主线程
function stateSync(store) {
// 同步数据-发送给主线程-单独
function stateSync(storeName, key, value) {
// console.log('state-change', storeName, key, value)
let jsonStr = ''
if (typeof key === 'string') jsonStr = JSON.stringify({[key]:value})
else if (typeof value === 'object') jsonStr = JSON.stringify(key)
// 通知主线程更新
ipcRenderer?.invoke('pinia-state-change', storeName, jsonStr)
}
// 同步数据-发送给主线程-全量更新
function stateSyncAll(store) {
const storeName = store.$id
const jsonStr = circularSafeStringify(store.$state)
// 通知主线程更新
ipcRenderer?.invoke('pinia-state-change', storeName, jsonStr)
}
// 发送当前数据状态
function stateSyncInit(wid, store) {
const storeName = store.$id
const curJson = JSON.stringify(store.$state) // 当前数据
// 发送同步数据给新窗口-更新状态
ipcRenderer.invoke('pinia-state-init', wid, storeName, curJson)
}
// 同步数据-接收主线程消息
function stateChange(store) {
const storeName = store.$id

View File

@ -1,133 +0,0 @@
// 所有事件
export function handleevent(canvas, imgarr, type = 'defalut') {
// // 鼠标按下
// canvas.on('mouse:down', function (e) {})
// // // 监听鼠标移动事件
// // canvas.on('mouse:move', (options) => {
// // console.log('Mouse move event:', options);
// // });
// // 监听鼠标释放事件
// canvas.on('mouse:up', (options) => {
// //判断是点击的哪一个
// if (type == 'defalut') {
// if (imgarr.value[0].index == 0) {
// imgarr.value[0].JSONdata = canvas.toJSON()
// }
// if (imgarr.value[1]?.index == 0) {
// imgarr.value[1].JSONdata = canvas.toJSON()
// }
// } else {
// if (imgarr.value[0].index == 1) {
// imgarr.value[0].JSONdata = canvas.toJSON()
// }
// if (imgarr.value[1]?.index == 1) {
// imgarr.value[1].JSONdata = canvas.toJSON()
// }
// }
// console.log(imgarr.value)
// })
}
// 保存数据
export function savecanvsStore(imgarr, canvsStore) {
// canvsStore.pageArr = mergeAndReplace(canvsStore.pageArr, imgarr.value)
}
// 重显数据
export function displayData(canvas, canvsStore, canvasobj, fabric, img) {
// // 初始化
// if (!canvsStore.pageArr.length) {
// fabric.Image.fromURL(img.src, (img) => {
// img.set({
// left: 0,
// top: 0,
// scaleX: canvas.value.width / img.width,
// scaleY: canvas.value.height / img.height
// })
// canvas.value.setBackgroundImage(img, canvas.value.renderAll.bind(canvas.value))
// })
// return
// }
// canvsStore.pageArr.forEach((item) => {
// //初始化
// if (item.page == canvasobj.page) {
// // canvas.value.clear() // 清除 Canvas
// // console.log(item.JSONdata, '找到一样的数据')
// canvas.value.loadFromJSON(item.JSONdata, () => {
// // 在所有对象加载完成后重新渲染画布
// canvas.value.renderAll.bind(canvas.value)
// canvas.value.renderAll()
// // requestAnimationFrame(() => {
// // // 渲染所有对象
// // })
// })
// } else {
// // 使用 requestAnimationFrame 来更新画布,确保在下一帧进行重绘
// // // 清除 Canvas
// canvas.value.clear()
// requestAnimationFrame(function () {
// fabric.Image.fromURL(img.src, (img) => {
// img.set({
// left: 0,
// top: 0,
// scaleX: canvas.value.width / img.width,
// scaleY: canvas.value.height / img.height
// })
// canvas.value.setBackgroundImage(img, canvas.value.renderAll.bind(canvas.value))
// })
// // 渲染所有对象
// canvas.value.renderAll.bind(canvas.value)
// canvas.value.renderAll()
// })
// }
// })
}
//page 一样替换
const mergeAndReplace = (arr1, arr2) => {
// // 用于存储替换后的数组
// const resultArray = array1.map(item1 => {
// // 在 array2 中查找 page 相同的对象
// const replacement = array2.find(item2 => item2.page == item1.page);
// // 如果找到替换对象,则返回替换对象,否则返回原对象
// return replacement ? replacement : item1;
// });
// // 将 array2 中 page 不在 array1 中的对象追加到结果数组中
// array2.forEach(item2 => {
// const existsInArray1 = array1.some(item1 => item1.page == item2.page);
// if (!existsInArray1) {
// resultArray.push(item2);
// }
// });
// return resultArray;
// 创建一个映射,将 arr2 中的对象按 page 属性存储
let map = new Map(arr2.map((item) => [item.page, item]))
// 使用 map 替换 arr1 中相应 page 的对象,并添加 arr2 中的对象
arr1 = arr1.map((item) => (map.has(item.page) ? map.get(item.page) : item))
// 将 map 中存在但 arr1 中不存在的对象添加到 arr1
for (let [page, obj] of map) {
if (!arr1.some((item) => item.page === page)) {
arr1.push(obj)
}
}
return arr1
}
// 初始化数据
export function initcanvasdata(canvas) {
canvas.value.clear() // 清除 Canvas
// 设置画布的背景色或其他属性
canvas.value.backgroundColor = 'rgba(255, 255, 255, 1)' // 白色背景
// 使用 requestAnimationFrame 来更新画布,确保在下一帧进行重绘
requestAnimationFrame(function () {
// 渲染所有对象
canvas.value.renderAll.bind(canvas.value)
})
}

View File

@ -10,10 +10,12 @@ const isNode = typeof require !== 'undefined' // 是否支持node函数
const path = isNode?require('path'):{}
const Remote = isNode?require('@electron/remote'):{}
const { ipcRenderer } = isNode?require('electron'):window.electron || {}
const API = isNode?window.api:{} // preload-api
import { useToolState } from '@/store/modules/tool' // 获取store状态
// 常用变量
const BaseUrl = isNode?process.env['ELECTRON_RENDERER_URL']+'/#':''
const isDev = isNode?process.env.NODE_ENV !== 'production':''
const toolState = useToolState() // 获取store状态
// 暴露Remote中的属性
export const ipcMain = Remote?.ipcMain || {}
@ -83,11 +85,12 @@ export function ipcHandle(fn,key, cb) {
* @returns
*/
let wins_tool = null
let winPdf=null
export const createWindow = async (type, data) => {
if (wins_tool) return console.error('createWindow: win is have')
if (!type) return console.error('createWindow: type is null')
switch(type) {
case 'tool-sphere': { // 创建-悬浮球
if (wins_tool) return console.error('createWindow: win is have')
if (!type) return console.error('createWindow: type is null')
const option = data.option||{}
const defOption = {
frame: false, // 要创建无边框窗口
@ -98,6 +101,7 @@ export const createWindow = async (type, data) => {
// autoClose: true, // 关闭窗口后自动关闭
}
data.isConsole = true // 是否开启控制台
data.isWeb = false // 是否开启web安全
data.option = {...defOption, ...option}
wins_tool = await toolWindow(data)
wins_tool.type = type // 唯一标识
@ -119,12 +123,19 @@ export const createWindow = async (type, data) => {
}
data.isConsole = true // 是否开启控制台
data.option = {...defOption, ...option}
if(winPdf){ //判断是否已经打开
// if (winPdf.isMinimized()) winPdf.restore();
winPdf.focus();
// toolState.isPdfWin=true
return
}
const win = await toolWindow(data)
win.type = type // 唯一标识
win.show()
win.setFullScreen(true) // 设置窗口为全屏
// win.webContents.openDevTools() // 打开调试工具
eventHandles(type, win) // 事件监听处理
winPdf=win
break
}
default:
@ -140,7 +151,7 @@ export const createWindow = async (type, data) => {
* @author: zdg
* @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
let width = option?.width || 800
let height = option?.height || 600
@ -152,14 +163,12 @@ export function toolWindow({url, isConsole, option={}}) {
const config = {
width, height,
type: 'toolbar', // 创建的窗口类型为工具栏窗口
icon: path.join(__dirname, '../../resources/logo2.ico'),
// icon: path.join(__dirname, '../../resources/logo2.ico'),
webPreferences: {
// preload: path.join(__dirname, '../preload/index.js'),
preload: '@root/src/preload/index.js',
preload: path.join(API.preloadPath, '/index.js'),
sandbox: false,
nodeIntegration: true, // nodeApi调用
contextIsolation: false, // 沙箱取消
// webSecurity: false // 跨域关闭
},
...option
}
@ -167,11 +176,17 @@ export function toolWindow({url, isConsole, option={}}) {
let win = new Remote.BrowserWindow(config)
if (!isDev) win.loadFile(urlAll,{hash: url}) // 加载文件
else win.loadURL(urlAll) // 加载url
win.once('ready-to-show', () => {resolve(win)})
win.once('ready-to-show', () => { // 窗口加载完成
resolve(win)
})
// 主窗口关闭事件
mainWin.once('closed', () => { win.destroy()})
// 内部监听器
win.webContents.on('did-finish-load', () => {})
win.webContents.on('did-finish-load', () => {
setTimeout(() => {
toolState.stateSyncInit(win.id) // 同步状态
}, 200);
})
// 内部监听器-是否打印
if (!!isConsole) {
win.webContents.on('console-message', (e,leve,m,lin,s) => {
@ -191,12 +206,13 @@ const eventHandles = (type, win) => {
// 公共方法
const publicMethods = ({onClosed}={}) => {
// 监听主窗口-关闭事件
mainWin.once('close', () => {win.destroy()})
mainWin.once('close', () => {winPdf=null;win.destroy();})
win.on('closed', () => {
if(onClosed) onClosed() // 自定义关闭事件
win = null
wins_tool = null
})
// 新窗口-创建事件(如:主进程加载远程服务)
ipcRenderer.send('new-window', {id:win.id, type})
}
@ -220,7 +236,11 @@ const eventHandles = (type, win) => {
break}
case 'open-PDF': {
// 最小化窗口 minimize()
Remote.ipcMain.once('open-PDF:minimize', () => {win&&win.destroy()})
Remote.ipcMain.once('open-PDF:minimize', () => {
winPdf=null
win&&win.destroy()
// win&&win.minimize(); //缩小功能
})
publicMethods() // 加载公共方法
break}
default:

View File

@ -40,10 +40,8 @@ import { getStaticUrl } from '@/utils/tool'
const { ipcRenderer } = require('electron')
import { getBookMarkById } from '@/api/eTextbook/index'
import {useToolState} from '@/store/modules/tool'
// const getStaticUrl=(url)=>{
// return url
// }
pdfjsLib.GlobalWorkerOptions.workerSrc = getStaticUrl('/lib/build/pdf.worker.mjs')
const toolState = useToolState();
const route = useRoute();
const isOnLoadShow = ref(false) //
@ -73,14 +71,9 @@ const navtopage = (type) => {
if (pdfObj.numPages > numPagesTotal.value) return
pdfCanvaslist.value.initPdf('rest')
}
//
//
const minimize = async () => {
await pdfCanvaslist.value.savaDataStore()
// toolState.isPdfWin=false
// toolState.showBoardAll=true //
// console.log(toolState.showBoardAll,"")
// // ipcRenderer.send('tool-sphere:reset') //tool
// ipcRenderer.send('open-PDF:minimize')
}
const handleUpdate = (data) => {
numPagesTotal.value = data
@ -107,10 +100,12 @@ const switchPageMode = () => {
}
}
onMounted(async () => {
toolState.isPdfWin=true
pdfObj.pdfUrl = getStaticUrl(route.query.path, 'user', 'selfFile', true)
toolState.isPdfWin=true //pdf
// pdfObj.pdfUrl = getStaticUrl(route.query.path, 'user', 'selfFile', true) //线
pdfObj.pdfUrl = getStaticUrl('aaa.pdf', 'user', 'selfFile', true) //
textbookId.value = route.query.textbookId
pdfObj.bookId=textbookId.value
//
getBookMarkById(textbookId.value).then(res=>{
pdfObj.allPageData=getUniqueArrayByLastOccurrence(res.data)
isOnLoadShow.value=true

View File

@ -55,9 +55,11 @@ const toolStore = useToolState()
watch(
() => [dataList,toolStore.isToolWin],
() => {
setTimeout(()=>{
getSelfReserv().then((res) => {
dataList.value = [...res.data]
})
},300)
}
)
onMounted(() => {

View File

@ -0,0 +1,79 @@
<script setup>
// 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()
const emits = defineEmits(['change'])
const props = defineProps({
isGroup: { type: Boolean, default: false }, //
})
const imChatObj = reactive({imChat:null})
onMounted(() => {
// console.log(imChatObj)
initImChat()
})
// im-chat
const initImChat = async () => {
// console.log('im-chat', userStore.user.timuserid)
try {
const { timuserid, deptId, userId } = userStore.user
//
const res = await http.imChat.getTxCloudSign({imUserId: timuserid})
if (res && res.code == 200) {
const { sdkAppId, sign } = res.data
//
const groupName = `${deptId}-classteaching-${userId}`
// im-chat
// await ipcMsgInvoke('im-chat:init', sdkAppId)
imChatObj.imChat = new ImChat(sdkAppId, sign, timuserid)
// im-chat
await imChatObj.imChat.init()
// im-chat
await imChatObj.imChat.login()
// im-chat
imChatObj.imChat.watch((res) => {
const [msg] = res[0]?JSON.parse(res[0]):[]
imChatObj.imChat.setConsole('%cchat-msg', msg)
//
if(msg.message_sender == '@TIM#SYSTEM'){
emits('change', 'msg-system', null, msg)
} else { // -
(msg?.message_elem_array||[]).forEach(o => {
const msgData = !!o.text_elem_content ? JSON.parse(o.text_elem_content)||'' : ''
emits('change', 'msg', msgData, msg)
})
}
})
//
if (props.isGroup) await createGroup(groupName)
}
} catch (error) {
console.log('im-error: ', error)
}
}
//
const createGroup = async (groupName) => {
if (!imChatObj.imChat) return
await imChatObj.imChat.createGroup(groupName)
emits('change', 'createGroup', imChatObj.imChat.timGroupId)
}
// 退
const logout = () => imChatObj.imChat?.logout()
//
const deleteGroup = () => imChatObj.imChat?.deleteGroup()
defineExpose({ logout, deleteGroup, imChatObj })
</script>
<template>
<div class="im-chat">
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
</style>

View File

@ -17,8 +17,11 @@
<!-- 内容部分 -->
<transition name="el-fade-in">
<div class="c-popover" :style="`--top: ${topPos}px;--height:${hPost}px;`" v-show="isVisible">
<div class="content">
<homework/>
<div class="content" v-if="isVisible">
<slot name="content">
<homework v-if="activeObj?.prop === 'resource'" />
<span v-else style="color:red;">{{activeObj}}</span>
</slot>
</div>
</div>
</transition>
@ -135,8 +138,4 @@ const clickHandel = (o, e) => {
transform: rotate(45deg);
}
}
.content{
color: red;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<button v-if="props.test" @click="trigger">测试</button>
<el-button v-if="props.test" type="primary" @click="trigger">测试</el-button>
<div ref="warpRef" class="c-warp">
<template v-for="i in sum">
<slot><el-icon><Star /></el-icon></slot>

View File

@ -7,9 +7,12 @@
<side-vue v-ignore @ignore-mounted="sideMouse" @change="sideChange"></side-vue>
<!-- 点赞组件 -->
<upvote-vue></upvote-vue>
<upvote-vue ref="upvoteRef"></upvote-vue>
<!-- 底部工具栏 :style="dataPos.style"-->
<!-- im-chat 聊天组件 -->
<im-chat ref="imChatRef" @change="chatChange" />
<!-- 底部工具栏 -->
<div class="tool-bottom-all" @mouseenter="mouseChange(0)" @mouseleave="mouseChange(1)">
<div v-drag="{handle:'.tool-bottom-all', dragtime}"
@v-drag-start="dragtime = Date.now()">
@ -36,23 +39,33 @@
// electron
import { onMounted, ref, reactive, watchEffect } from 'vue'
import { useRoute } from 'vue-router';
import { endClass } from '@/api/classManage'
import { ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import * as classManageApi from '@/api/classManage'
import logo from '@root/resources/icon.png' // logo
import boardVue from './components/board.vue' // -
import sideVue from './components/side.vue' // -
import upvoteVue from './components/upvote.vue' // -
import imChat from './components/imChat.vue' // im-chat-
import vDrag from './directive/drag' // -
import vIgnore from './directive/ignore' // -穿
import { useToolState } from '@/store/modules/tool' // -
import { ipcMsgSend, ipcHandle, ipcMain, ipcMsgInvoke } from '@/utils/tool' //
import MsgEnum from '@/plugins/imChat/msgEnum' // -(nuem)
const route = useRoute();
const tabActive = ref('select') //
const isFold = ref(false) //
const isDrag = ref(false) //
const dragtime = ref(0) // -
const isShow = ref(false) // -
const isOver = ref(false) //
const toolStore = useToolState() //
const boardVueRef=ref(null) // ref
const upvoteRef = ref(null) // ref
const imChatRef = ref(null) // im-chat ref
const classObj = reactive({ //
id: route.query.reservId, // id
data: {} //
})
const btnList = [ //
{ label: '选择', value: 'select', icon: 'icon-mouse' },
{ label: '画笔', value: 'brush', icon: 'icon-huabi' },
@ -64,12 +77,27 @@ const btnList = [ // 工具栏按钮列表
]
// === ===
onMounted(async() => {
setTimeout(() => {
getClassInfo() // ex3
resetStatus() // -
}, 200);
})
// ==== ===
// const test = (e) => { console.log('test', e) }
//
const getClassInfo = async () => {
const { data } = await classManageApi.getClassInfo(classObj.id)
classObj.data = data
if(!data.ex3 || data.ex3 == 'undefined') { //
await imChatRef.value.imChatObj.imChat.createGroup(data.className)
const timGroupId = imChatRef.value.imChatObj.imChat.timGroupId
classManageApi.startClass(classObj.id, timGroupId) //
} else { //
// console.log(': ', data.ex3)
imChatRef.value.imChatObj.imChat.timGroupId = data.ex3
// imChatRef.value.imChatObj.imChat.setGroupMsgReceive(data.ex3)
}
}
// tab-change
const tabChange = (val) => {
const bool = !toolStore.isPdfWin && !toolStore.showBoardAll
@ -90,6 +118,27 @@ const mouseChange = (bool) => {
if (!isShow.value) resBool = !!bool
setIgnore(resBool)
}
// im-chat: {type, data}
const chatChange = (type, data) => {
if (type == 'createGroup') { // -
console.log('创建群:', data)
!!data && classManageApi.startClass(classObj.id, data)
} else if (type == 'msg') { // im-chat
if (!data) return // msg
const { msgKey:head, msgcontent:msg, senduserid:sendId, msgType } = data
switch(head) {
case MsgEnum.HEADS.MSG_0001:
// console.log(':', data)
upvoteRef.value.trigger()
break
default:
console.log('未知消息:', data)
break
}
}
// console.log('im-chat-sphere:', type, data)
}
// 穿
const setIgnore = (bool) => {ipcMsgSend('tool-sphere:set:ignore', bool)}
// : |
@ -102,10 +151,15 @@ const resetStatus = () => {
}, 500)
})
toolStore.isToolWin = true //
}
// :
const sideMouse = e => {
const {type} = e.detail
if (isOver.value && type == 'mouseleave') {
setIgnore(false) // -穿
return
}
mouseChange(type == 'mouseleave')
}
// :
@ -119,9 +173,31 @@ const sideChange = async o => {
case 'win': //
break
case 'over': //
isOver.value = true
ElMessageBox.confirm('确认结束课程吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
await imChatRef.value?.imChatObj?.imChat?.sendMsgClosed() //
// const elMsg = ElMessage.warning({duration:0,message:'...'})
const elMsg = ElLoading.service({lock: true, text: '正在下课...', background: 'rgba(0, 0, 0, 0.7)'})
// 2
setTimeout(async() => {
elMsg.close()
toolStore.isToolWin = false
await endClass(route.query.reservId)
ipcMsgSend('tool-sphere:close')
await classManageApi.endClass(route.query.reservId)
await imChatRef.value?.deleteGroup() //
await imChatRef.value?.logout() // 退im
ipcMsgSend('tool-sphere:close') //
}, 2000);
// isOver.value = false
// setIgnore(true) // -穿
}).catch(() => {
isOver.value = false
setIgnore(true) // -穿
})
break
}
}