This commit is contained in:
sx1989827 2023-05-27 20:53:32 +08:00
parent efe535180b
commit ff4db586b3
130 changed files with 6634 additions and 510 deletions

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIELjCCApagAwIBAgIQXasrLEwSwhmrN7pzzSHxyjANBgkqhkiG9w0BAQsFADBh
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExGzAZBgNVBAsMEnN1bnhp
bkBNYWNCb29rLVBybzEiMCAGA1UEAwwZbWtjZXJ0IHN1bnhpbkBNYWNCb29rLVBy
bzAeFw0yMzA0MjgxMzE2NDRaFw0yNTA3MjgxMzE2NDRaME4xJzAlBgNVBAoTHm1r
Y2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEjMCEGA1UECwwac3VueGluQE1h
Y0Jvb2stUHJvLTMubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQDsmvFnOJSxXoafj/ygI/AQnZC6kdSr4ma9sCI93QXYJS+uaXWlpAjweDjLmNBY
33oYeqkcI5nShPzKlX4HfiE+eW336z4dZaLx/d0zrwGc1M3+Go5bRQ5NsZFdZ/sW
CuB0WQ74BRW3Se2vBWISOtrWZHHIYUJ3fGRlJIeZBnVyAYGKHlswuWxgclZWbb2E
wLKSThMZWI9gP0/iSmffzhDYW8EyRwpQNzUQQPe/5KbICvYdXvu2f/uRALufUqfR
truFmK4/IWr0CMVWgfWyUGT0/TCWE81lZsh1a6n2NI0NGsux7ipOKsmpmcZwRMmZ
A4tSItWcorOXSI1vKn/zRn1lAgMBAAGjdTBzMA4GA1UdDwEB/wQEAwIFoDATBgNV
HSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBRoTUPg3+4+ht0WwhN/HmZHU2+W
pzArBgNVHREEJDAigg50ZWFtbGlua2VyLmNvbYIQKi50ZWFtbGlua2VyLmNvbTAN
BgkqhkiG9w0BAQsFAAOCAYEAFWmRN9VvuPkIpTe6RTumc4QZve2KjnZVi6FMXpKw
ywUhBQqxd0XlTFdFmXUDEFcPIR9zoDD9l8Y37ZuAd0Cb0k4r0bSV4IWWa7gc8ihN
PwVQqrcnwbTFT5udBl09oSKH9BNrw2yiv1jOgz2NpWm0HwOYBxBOlLwoVaCqvFg4
bNp3xutXcUqoS3TP5S2wnArBMJ2+ZvtzO1YjnfpF3jZZxh27ewRxzYcSxrrWkCyc
Xkrt4im5PQJ188+pke+wuNFPSZij5NZYr2UZmRsHA7sR2jIiRRBUNeYxuEMpWNUq
YQfpLPgk189yP8xMDZhUyMgQ0eOH12nXDoMamESCgPknACFJ0wffuoQqpVvtEyB/
4KOzYvMZkW/apdmzI1LCSwyBJH8QO9m8la2960CiQ02upN/lRTG1Ylo09N+leFQ9
x3mLrfjwK0t+McX4d4lD9bPPQbzjujYJK1ra8MEagvEdZMCMaenAyDqK8D3v0DO+
NdELh+MOxvuEcs1LvFi58yuJ
-----END CERTIFICATE-----

28
code/client/certs/key.pem Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDsmvFnOJSxXoaf
j/ygI/AQnZC6kdSr4ma9sCI93QXYJS+uaXWlpAjweDjLmNBY33oYeqkcI5nShPzK
lX4HfiE+eW336z4dZaLx/d0zrwGc1M3+Go5bRQ5NsZFdZ/sWCuB0WQ74BRW3Se2v
BWISOtrWZHHIYUJ3fGRlJIeZBnVyAYGKHlswuWxgclZWbb2EwLKSThMZWI9gP0/i
SmffzhDYW8EyRwpQNzUQQPe/5KbICvYdXvu2f/uRALufUqfRtruFmK4/IWr0CMVW
gfWyUGT0/TCWE81lZsh1a6n2NI0NGsux7ipOKsmpmcZwRMmZA4tSItWcorOXSI1v
Kn/zRn1lAgMBAAECggEBAIzJFWNqE3AG4uwG7TcMq0f3uaqKI5WzPZcZOwa8gUG/
vsN1FP/ev0L3BjR/VVnMgAYY1o9bz6eoYhLZKQikUHuwHXMrkuZDF9YOTJT4SWlc
ZsYQXyyHxp8MTYba3FidWDli1LlXrThG0RsXhOd8BcMUOXAWD0qxxbs5JUY0xaVz
gwew95LUB12vBpMb3DMjLuUOFpCxVLtThsLYb+TMVzYm706qavmnbHs8xgFeOzUh
lHyIBdoLhrg/aeD0rWQVepyq2j8YP2N9N2sU8SNzsPhtsR5bZAU5DuRlrVy+ZTuL
IOFlk4aq4bjzCe7KxvHb+82DnaT7O4cyf7qvL/LYe/ECgYEA+iKWiqYSbICAN9Vn
58uwZFX5VEANCmAhVufZe4I4MrMWqhJjbpiJBmnoVXXfHHyLvHO52/FKu1xE6TUB
mqD7H32nWuXw+3wuscE01furXIfrVhZjhx86nWOjub932N4L/OgKxlkfKWVR4oM7
AJuzwsVMkTz2pg1bvk8AVc6Cjg8CgYEA8ickqkHWpTeVPS/XBPg4jCcvnhyZBu0a
kkokizmb4Yr7yqXF30gdsMx3WVPJ2XhlFjmsQD1BbhUGYkKfUIOCS2APAWg+3JOT
1y9wmTY0uE8JwWvH5GBLEw8gBqz7w+Z5rfYXqpD7gBm1tpc4BBg1QDP64Ldsm4g0
c3KFNsLKMUsCgYEA7TsYo+7V0modMNcJcOHSLZcMnUcSFyEM/aturKDYQ91uRWaj
PyUT8C1J2KOuMwo8TUNEpsC2K/RatwM6vjinczptGtyyLRGeB6BCSCAkaeHO5Rre
0ixgHx19DpH1TI1ruTUp4uxrjYs2min0L6N8XeFZuPWkx+ArftbWbospbykCgYA+
ESZmtWVtB6gq/L4iOfVUhx6/ahkXG2L6KCLhxKxdzR/ou0DSkEt764yTytQr954B
alrqREknDCCMwwLOwkZ9R2vRMoUaSIFWYIR94NT2gNvBRf0AXmYfxnqw+1m0xrhd
jHxYbbzpAq3+1Re4hPPxNuGRA7LE0s6O4MWgWaE1rQKBgQD0tCVevGWS3A/cE/ye
a+Mu7IakKTBqC0tNzsG6VuzqjcNrR4+h0nJDJjMsOL/pKAzfI7WnruFOA2xMvI4W
36ij9rm2r3zG7K19q04eF0bz+kPTFefJHXYwmeQkEbryeibzlFc458U9aSWUHXof
QVhit6C7sVVp7so48x/eBdiVsg==
-----END PRIVATE KEY-----

View File

@ -10,27 +10,30 @@
},
"dependencies": {
"@logicflow/core": "^1.1.31",
"blueimp-md5": "^2.19.0",
"eventemitter3": "^5.0.0",
"mediasoup-client": "^3.6.84",
"moment": "^2.29.4",
"moment-timezone": "^0.5.42",
"pinia": "^2.0.28",
"socket.io-client": "^4.6.1",
"uuid": "^9.0.0",
"vue": "^3.2.47",
"vue": "^3.3.4",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@arco-design/web-vue": "^2.45.1",
"@arco-design/web-vue": "^2.46.0",
"@rollup/plugin-typescript": "^10.0.1",
"@types/blueimp-md5": "^2.18.0",
"@types/node": "^18.15.11",
"@types/uuid": "^9.0.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue": "^4.2.0",
"rollup-plugin-typescript2": "^0.34.1",
"tslib": "^2.4.1",
"ttypescript": "^1.5.15",
"typescript": "^4.6.4",
"vite": "^4.0.4",
"vite": "^4.3.2",
"vite-plugin-typescript": "^1.0.4",
"vue-tsc": "^1.0.12"
"vue-tsc": "^1.4.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

View File

@ -2,9 +2,9 @@
<div style="width: 100%;height: 100%;display: flex;flex-direction: column" ref="root">
<div style="flex: 0 0 36px;display: flex;align-items: center;justify-content: space-between;padding:0 10px;border-bottom: 1px solid lightgray">
<div style="height: 24px;display: flex;align-items: center">
<div style="height: 24px;width:24px;border-radius: 100%;overflow: hidden;margin-right: 10px">
<img :src="info.photo" style="height: 100%">
</div>
<a-avatar :size="24" :image-url="info.photo">
{{calculateShortName(info.name)}}
</a-avatar>&nbsp;
{{ info.name }}
</div>
<div v-if="type===ECommon_IM_Message_EntityType.TEAM">
@ -29,7 +29,9 @@
<div style="height: 30px;display: flex">
<div style="flex:0 0 30px;display: flex;justify-content: center;align-items: center">
<div style="height: 24px;width:24px;border-radius: 100%;overflow: hidden;margin-right: 10px">
<img :src="item.photo" style="height: 100%;cursor: pointer" @click="onClickPhoto($event,item)">
<a-avatar :image-url="item.photo" :size="24" style="cursor: pointer" @click="onClickPhoto($event,item)">
{{calculateShortName(item.name)}}
</a-avatar>
</div>
</div>
<div style="flex: 1 1 auto;display: flex;align-items: center;">
@ -42,7 +44,10 @@
</div>
<div style="flex: 1 1 auto;display: flex;align-items: center;">
<div style="background-color: #e5e3e3;padding: 5px;border-radius: 5px">
{{item.content}}
<template v-if="item.type===ECommon_IM_Message_ContentType.TEXT">
{{item.content}}
</template>
<ImageMessage :file-id="item.content" v-else-if="item.type===ECommon_IM_Message_ContentType.IMAGE" style="max-width: 100%" @complete="onImageComplete"></ImageMessage>
</div>
</div>
</div>
@ -50,21 +55,24 @@
</div>
</template>
<template #second>
<div style="height: 100%;box-sizing: border-box;display: flex">
<a-textarea style="height: 100%;border: 0;background-color: transparent;flex: 1 1 auto" v-model="content">
</a-textarea>
<a-button type="primary" style="margin-top: 2px;height: auto;flex: 0 0 60px;border-radius: 5px" tabindex="-1" @click="onSend">
<template #icon>
<icon-send style="color: white;font-size: x-large"></icon-send>
</template>
</a-button>
<div style="height: 100%;box-sizing: border-box;display: flex;flex-direction: column">
<div style="flex:0 0 30px;padding: 2px;box-sizing: border-box;background-color: rgb(249,249,249);">
<Image @update-start="loading=true" @update-end="onUpdateImage" @update-error="loading=false" ref="imageRef"></Image>
</div>
<a-spin :loading="loading" style="flex: 1 1 auto;display: flex;margin-top: 2px;height: auto;">
<a-textarea style="flex:1 1 auto;border: 0;background-color: transparent;" v-model="content" @paste="onPaste"></a-textarea>
<a-button type="primary" style="margin-top: 2px;height: auto;flex: 0 0 60px;border-radius: 5px" tabindex="-1" @click="onSend">
<template #icon>
<icon-send style="color: white;font-size: x-large"></icon-send>
</template>
</a-button>
</a-spin>
</div>
</template>
</a-split>
</div>
<a-drawer v-model:visible="isInfoVisible" :popup-container="root" unmountOnClose :header="false" :footer="false">
<slot name="info" :info="info">
<slot name="info" :info="info" v-if="isInfoVisible">
</slot>
</a-drawer>
@ -76,8 +84,11 @@ import {IClient_Chat_Message_Item} from "./type";
import {nextTick, ref, watch} from "vue";
import {ECommon_IM_Message_EntityType} from "../../../../../../common/model/im_unread_message";
import {ECommon_IM_Message_ContentType} from "../../../../../../common/model/im_user_message";
import moment from "moment";
import {SessionStorage} from "../../storage/session";
import Image from "./operation/image.vue"
import moment from "moment";
import ImageMessage from "./messageType/imageMessage.vue";
import {calculateShortName} from "../../util/helper";
const root=ref()
const emit=defineEmits<{
@ -98,11 +109,14 @@ const props = defineProps<{
id: string
}
}>()
const loading=ref(false)
const contentRef=ref<HTMLDivElement>()
const content=ref("")
const isInfoVisible=ref(false)
const messageList = ref<IClient_Chat_Message_Item[]>([])
const organizationUserId=SessionStorage.get("organizationUserId")
const imageRef=ref<InstanceType<typeof Image>>()
let isWheel=false
let preId=""
watch(() => props.data, () => {
messageList.value = JSON.parse(JSON.stringify(props.data))
@ -123,7 +137,12 @@ watch(() => props.data, () => {
deep:true,
immediate: true
})
watch(()=>props.info,()=>{
isWheel=false
isInfoVisible.value=false
})
const onWheel=(event:WheelEvent)=>{
isWheel=true;
let ele=event.currentTarget as HTMLDivElement
if(event.deltaY<0 && ele.scrollTop<=0) {
emit("updateMore",messageList.value[0],props.type,props.info)
@ -142,6 +161,30 @@ const onSend=()=>{
const scrollToBottom=()=>{
contentRef.value.scrollTop=contentRef.value.scrollHeight
}
const onUpdateImage=(fileId:string)=>{
loading.value=false
emit("send",fileId,ECommon_IM_Message_ContentType.IMAGE,props.type,props.type===ECommon_IM_Message_EntityType.USER?props.info.id:null,props.type===ECommon_IM_Message_EntityType.TEAM?props.info.id:null)
}
const onPaste=async (event:ClipboardEvent)=>{
let items=event.clipboardData.items
for(let i=0;i<items.length;i++) {
let obj=items[i]
if(obj.type.includes("image")) {
let file=obj.getAsFile()
event.stopPropagation()
event.preventDefault()
imageRef.value.handleUpload(file)
}
}
}
const onImageComplete=()=>{
if(!isWheel) {
contentRef.value.scrollTop=contentRef.value.scrollHeight
}
}
defineExpose({
scrollToBottom
})

View File

@ -0,0 +1,41 @@
<template>
<div>
<a-image :src="path" show-loader @load="onLoad($event)"></a-image>
</div>
</template>
<script setup lang="ts">
import {onBeforeMount, ref} from "vue";
import {apiFile} from "../../../request/request";
const emit=defineEmits<{
(e:"complete",width:number,height:number)
}>()
const props=defineProps<{
fileId:string
}>()
const path=ref("")
const getPath=async ()=>{
let ret=await apiFile.getPath({
fileId:props.fileId
})
if(ret?.code==0) {
path.value=ret?.data.uri
}
}
const onLoad=(event:Event)=>{
let ele=event.currentTarget as HTMLImageElement
emit("complete",ele.width,ele.height)
}
onBeforeMount(()=>{
getPath()
})
</script>
<style scoped>
:deep .arco-image-img {
max-width: 300px;
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<a-upload accept=".png,.jpg,.gif,.jpeg,.bmp,.webp" :custom-request="onUpload" :show-file-list="false">
<template #upload-button>
<a-button size="mini" type="text">
<template #icon>
<icon-image style="font-size: 20px"></icon-image>
</template>
</a-button>
</template>
</a-upload>
</template>
<script setup lang="ts">
import {apiFile} from "../../../request/request";
import {compressImage} from "../../../util/helper";
import {Message} from "@arco-design/web-vue";
const emit=defineEmits<{
(e:"updateStart"):void,
(e:"updateEnd",fileId:string):void,
(e:"updateError"):void
}>()
const onUpload=async (option)=>{
const {onProgress, onError, onSuccess, fileItem, name} = option
let file:File=fileItem.file
handleUpload(file)
}
const handleUpload=async (file:File)=>{
if(file.size>5*1024*1024) {
Message.error("file size overflow")
} else if(file.size>1024*1024) {
emit("updateStart")
let reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (e) => {
let result = e.target.result
let image = new Image()
image.src = result as string
image.onload = async () => {
let width = image.width
let height = image.height
let dataUrl = compressImage(image, width, height, 0.7)
let blob=await fetch(dataUrl).then(item=>item.blob())
let fileTemp=new File([blob],file.name,{type:"image/jpeg",lastModified:Date.now()})
let ret=await apiFile.upload({
file:fileTemp as any
})
if(ret?.code==0) {
emit("updateEnd",ret.data.id)
} else {
emit("updateError")
}
}
}
} else {
emit("updateStart")
let ret=await apiFile.upload({
file:file as any
})
if(ret?.code==0) {
emit("updateEnd",ret.data.id)
} else {
emit("updateError")
}
}
}
defineExpose({
handleUpload
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,15 @@
export class AutoExecuteArray {
private static arr=[]
static async push(func:any) {
if(this.arr.length>0) {
this.arr.unshift(func)
} else {
this.arr.unshift(func)
while(this.arr.length>0) {
let func=this.arr[this.arr.length-1]
await func()
this.arr.pop()
}
}
}
}

View File

@ -0,0 +1,336 @@
import * as mediaSoup from "mediasoup-client";
import {Device} from "mediasoup-client";
import {Transport} from "mediasoup-client/lib/Transport";
import {Socket} from "socket.io-client";
import {Meeting_ClientToServerEvents, Meeting_ServerToClientEvents} from "./type";
import {AutoExecuteArray} from "./AutoExecuteArray";
import {MediaKind, RtpCapabilities} from "mediasoup-client/lib/RtpParameters";
export class MeetingClient {
private device:Device
private producerAudio:mediaSoup.types.Producer<mediaSoup.types.AppData>
private producerVideo:mediaSoup.types.Producer<mediaSoup.types.AppData>
private producerSet=new Set<string>()
private transportReceive:Transport
private transportSend:Transport
private socket:Socket<Meeting_ServerToClientEvents,Meeting_ClientToServerEvents>
private roomInfo:{
roomId:string,
roomName:string
}
onProducerStateChange:(state:"new"|"close"|"pause"|"resume", kind: mediaSoup.types.MediaKind, businessId:string,stream?: MediaStream,producerId?:string)=>void
onLocalProducerInit:(stream:MediaStream)=>void
onLocalProducerStart:(kind:MediaKind)=>void
onJoinedRoom:(roomInfo:typeof this.roomInfo)=>void
onLeavedRoom:(roomInfo:typeof this.roomInfo)=>void
onSpeaker:(businessId:string)=>void
onKick:()=>void
private onDisconnect:any
constructor(socket:any) {
this.onDisconnect=this._onDisconnect.bind(this)
this.socket=socket
this.socket.on('newProducer', async ( producerId) => {
if(!this.producerSet.has(producerId)) {
this.producerSet.add(producerId)
AutoExecuteArray.push(this.subscribe.bind(this,producerId))
}
});
this.socket.on('producerClosed', (producerId, kind, businessId)=>{
if(this.onProducerStateChange) {
this.onProducerStateChange("close",kind,businessId,null,producerId)
}
this.producerSet.delete(producerId)
});
this.socket.on("producerPause",(producerId, kind, businessId) => {
if(this.onProducerStateChange) {
this.onProducerStateChange("pause",kind,businessId,null,producerId)
}
})
this.socket.on("producerResume",(producerId, kind, businessId) => {
if(this.onProducerStateChange) {
this.onProducerStateChange("resume",kind,businessId,null,producerId)
}
})
this.socket.on("kick",() => {
this.clearRoomConnection()
if(this.onKick) {
this.onKick()
}
})
this.socket.on("disconnect",this.onDisconnect)
this.socket.on("speaker",businessId => {
if(this.onSpeaker) {
this.onSpeaker(businessId)
}
})
}
private _onDisconnect(reason) {
this.clearRoomConnection()
}
getRoomInfo() {
return this.roomInfo
}
async join(roomId:string,extraData:any):Promise<{
success:boolean,
msg?:string
}> {
if(this.roomInfo) {
return {
success:false,
msg:"you have joined a meeting"
}
}
let ret=await this.socket.emitWithAck("joinRoom",roomId,extraData)
if(ret) {
this.roomInfo=ret;
}
if(!this.device) {
const data = await this.socket.emitWithAck('getRouterRtpCapabilities');
await this.loadDevice(data);
}
await this.publish()
return {
success:ret?true:false
}
}
async leave():Promise<boolean> {
if(!this.roomInfo) {
return false
}
await this.socket.emitWithAck("leaveRoom")
this.clearRoomConnection()
return true
}
async pause(kind:MediaKind) {
let ret=await this.socket.emitWithAck("pauseSelf",kind)
return ret;
}
async resume(kind:MediaKind) {
let ret=await this.socket.emitWithAck("resumeSelf",kind)
return ret;
}
async mute(kind:MediaKind,businessId:string) {
let ret=await this.socket.emitWithAck("pauseOther",kind,businessId)
return ret;
}
async unmute(kind:MediaKind,businessId:string) {
let ret=await this.socket.emitWithAck("resumeOther",kind,businessId)
return ret;
}
async kick(businessId:string) {
let ret=await this.socket.emitWithAck("kick",businessId)
return ret;
}
async end() {
let ret=await this.socket.emitWithAck("end")
return ret;
}
async states() {
let ret=await this.socket.emitWithAck("states")
return ret;
}
private clearRoomConnection() {
if(this.roomInfo) {
if(this.onLeavedRoom) {
this.onLeavedRoom(Object.assign({},this.roomInfo))
}
this.roomInfo=null;
}
this.device=null;
this.producerSet=new Set
if(this.producerAudio) {
this.producerAudio.removeAllListeners();
this.producerAudio.close()
this.producerAudio=null;
}
if(this.producerVideo) {
this.producerVideo.removeAllListeners()
this.producerVideo.close()
this.producerVideo=null;
}
if(this.transportReceive) {
this.transportReceive.removeAllListeners()
this.transportReceive.close()
this.transportReceive=null
}
if(this.transportSend) {
this.transportSend.removeAllListeners()
this.transportSend.close()
this.transportSend=null
}
this.socket.removeAllListeners("newProducer")
this.socket.removeAllListeners("producerClosed")
this.socket.removeAllListeners("producerPause")
this.socket.removeAllListeners("producerResume")
this.socket.removeAllListeners("kick")
this.socket.removeAllListeners("speaker")
this.socket.off("disconnect",this.onDisconnect)
this.socket=null;
}
private async loadDevice(routerRtpCapabilities:RtpCapabilities) {
try {
this.device = new mediaSoup.Device();
} catch (error) {
if (error.name === 'UnsupportedError') {
console.error('browser not supported');
}
}
await this.device.load({ routerRtpCapabilities });
}
private async subscribe(remoteProducerId:string) {
if(!this.transportReceive) {
const data = await this.socket.emitWithAck('createConsumerTransport');
if (!data) {
this.producerSet.delete(remoteProducerId)
return;
}
this.transportReceive = this.device.createRecvTransport({...data, iceServers : []});
this.transportReceive.on('connect', async ({ dtlsParameters }, callback, errback) => {
this.socket.emitWithAck('connectConsumerTransport', {
dtlsParameters
})
.then(callback)
.catch(errback);
});
this.transportReceive.on('connectionstatechange', async (state) => {
switch (state) {
case 'connecting':
console.log("Connecting to consumer for audio, transport id: " + this.transportReceive.id)
break;
case 'connected':
console.log("Connected to consumer for audio, transport id: " + this.transportReceive.id)
break;
case 'failed':
case "closed":
case "disconnected": {
this.leave()
break
}
default: break;
}
});
}
this.consume(this.transportReceive, remoteProducerId).then(async value=>{
await this.socket.emitWithAck("resume",value.consumer.id)
if(this.onProducerStateChange) {
this.onProducerStateChange("new",value.consumer.kind,value.businessId,value.stream,remoteProducerId)
}
})
}
private async consume(transport: mediaSoup.types.Transport<mediaSoup.types.AppData>, remoteProducerId:string) {
const { rtpCapabilities } = this.device;
const transportId = transport.id;
const data = await this.socket.emitWithAck('consume', { rtpCapabilities, remoteProducerId, transportId});
const {
producerId,
id,
kind,
rtpParameters,
} = data;
const consumer = await transport.consume({
id,
producerId,
kind,
rtpParameters,
});
const stream = new MediaStream();
stream.addTrack(consumer.track);
return {stream,consumer,businessId:data.businessId};
}
private async publish() {
const data = await this.socket.emitWithAck('createProducerTransport');
if (!data) {
return;
}
this.transportSend = this.device.createSendTransport({...data, iceServers : []});
this.transportSend.on('connect', async ({ dtlsParameters }, callback, errback) => {
this.socket.emitWithAck('connectProducerTransport', { dtlsParameters })
.then(callback)
.catch(errback);
});
this.transportSend.on('produce', async ({ kind, rtpParameters }, callback, errback) => {
try {
const { id, producersExist } = await this.socket.emitWithAck('produce', {
kind,
rtpParameters,
});
if(this.onLocalProducerStart) {
this.onLocalProducerStart(kind)
}
if (producersExist){
this.getProducers()
}
callback({ id });
} catch (err) {
errback(err);
}
});
this.transportSend.on('connectionstatechange', (state) => {
switch (state) {
case 'connecting':
console.log("Connecting to publish")
break;
case 'connected':
console.log("Connected")
if(this.onJoinedRoom) {
this.onJoinedRoom(this.roomInfo)
}
break;
case 'failed':
this.transportSend.close();
console.log("Failed connection")
break;
default: break;
}
});
const mediaConstraints:MediaStreamConstraints = {
audio: {
echoCancellation:true,
noiseSuppression:true
},
video: true,
}
navigator.mediaDevices.getUserMedia(mediaConstraints).then( async (stream) => {
if(this.onLocalProducerInit) {
this.onLocalProducerInit(stream)
}
let track = stream.getAudioTracks()[0];
if(track) {
let params:any = { track };
params.codecOptions = {
opusStereo: 1,
opusDtx: 1
}
this.producerAudio =await this.transportSend.produce(params);
}
track=stream.getVideoTracks()[0]
if(track) {
let params={track}
this.producerVideo=await this.transportSend.produce(params)
}
}).catch(reason => {
console.log(reason)
})
}
private getProducers(){
this.socket.emit('getProducers', async producerIds => {
for(let id of producerIds) {
if(!this.producerSet.has(id)) {
this.producerSet.add(id)
AutoExecuteArray.push(this.subscribe.bind(this,id))
}
}
})
}
}

View File

@ -0,0 +1,84 @@
import type {MediaKind, RtpCapabilities, RtpParameters} from "mediasoup-client/lib/RtpParameters";
import {DtlsParameters, IceCandidate, IceParameters} from "mediasoup-client/lib/Transport";
import {SctpParameters} from "mediasoup-client/lib/SctpParameters";
export interface Meeting_ServerToClientEvents {
newProducer:(producerId:string,kind:MediaKind,businessId:string)=>void
producerClosed:(producerId:string,kind:MediaKind,businessId:string)=>void
producerPause:(producerId:string,kind:MediaKind,businessId:string)=>void
producerResume:(producerId:string,kind:MediaKind,businessId:string)=>void
kick:()=>void
speaker:(businessId:string)=>void
}
export interface Meeting_ClientToServerEvents {
joinRoom:(roomId:string,extraData:any,callback:(info:{
roomId:string,
roomName:string
},msg?:string)=>void)=>void
leaveRoom:(callback:()=>void)=>void
getRouterRtpCapabilities:(callback:(capabilities:RtpCapabilities)=>void)=>void
createProducerTransport:(callback:(param:{
id: string,
iceParameters: IceParameters,
iceCandidates: IceCandidate[],
dtlsParameters: DtlsParameters,
sctpParameters: SctpParameters,
})=>void)=>void
createConsumerTransport:(callback:(param:{
id: string,
iceParameters: IceParameters,
iceCandidates: IceCandidate[],
dtlsParameters: DtlsParameters,
sctpParameters: SctpParameters,
})=>void)=>void
connectProducerTransport:(data:{
dtlsParameters:DtlsParameters
},callback:()=>void)=>void
connectConsumerTransport:(data:{
dtlsParameters:DtlsParameters
},callback:()=>void)=>void
produce:(data:{
kind:MediaKind,
rtpParameters:RtpParameters
},callback:(producerInfo:{
id:string,
producersExist:boolean
})=>void)=>void
consume:(data:{
rtpCapabilities:RtpCapabilities,
remoteProducerId:string,
transportId:string
},callback:(data:{
businessId:string,
producerId: string,
id: string,
kind: MediaKind,
rtpParameters: RtpParameters,
type: 'simple' | 'simulcast' | 'svc' | 'pipe',
producerPaused: boolean
})=>void)=>void
getProducers:(callback:(producerList:string[])=>void)=>void
resume:(consumerId:string,callback:()=>void)=>void
pauseSelf:(kind:MediaKind,callback:()=>void)=>void
resumeSelf:(kind:MediaKind,callback:()=>void)=>void
pauseOther:(kind:MediaKind,businessId:string,callback:(success:boolean)=>void)=>void
resumeOther:(kind:MediaKind,businessId:string,callback:(success:boolean)=>void)=>void
kick:(businessId:string,callback:(success:boolean)=>void)=>void
end:(callback:(success:boolean)=>void)=>void
states:(callback:(list:{
businessId:string,
kinds:{
[kind:string]:boolean
}
}[])=>void)=>void
}
export interface Meeting_InterServerEvents {
}
export interface Meeting_Data {
}

View File

@ -3,7 +3,8 @@ import {h} from "vue";
export enum NotificationType {
CALENDAR="calendar",
IM="im"
IM="im",
MEETING="meeting"
}
export type NotificationCallbackFunc=(type:NotificationType,data:any)=>void
export class NotificationWrapper {

View File

@ -1,20 +1,33 @@
<template>
<a-popover position="left" :content-style="{backgroundColor:'rgb(249,249,249)',padding:'0px'}" :id="'popover'+organizationUserId" @popup-visible-change="onPopup">
<a-space @mouseenter="showCloseable=true" @mouseleave="showCloseable=false" size="small" :style="{border:(closeable!==undefined && showCloseable)?'1px lightgray solid':0}">
<a-popover position="left" :content-style="{backgroundColor:'rgb(249,249,249)',padding:'0px'}" :id="'popover'+organizationUserId" @popup-visible-change="onPopup" :trigger="trigger??'hover'">
<a-space @mouseenter="showCloseable=true" @mouseleave="showCloseable=false" size="small" :style="{border:(closeable===true && showCloseable)?'1px lightgray solid':0}">
<a-avatar :size="24" :image-url="photo">{{imgName}}</a-avatar>
<span>{{name}}</span>
<a-button type="text" size="mini" v-if="closeable!==undefined && showCloseable" @click="emit('close',organizationUserId)">
<template #icon>
<icon-delete style="color:red;"></icon-delete>
</template>
</a-button>
<template v-if="!onlyPhoto">
<span>{{name}}</span>
<a-button type="text" size="mini" v-if="closeable===true && showCloseable" @click="emit('close',organizationUserId)">
<template #icon>
<icon-delete style="color:red;"></icon-delete>
</template>
</a-button>
</template>
</a-space>
<template #content>
<a-list :bordered="false" :loading="loading" size="small">
<a-list-item>
<a-list-item-meta :title="name">
<a-list-item-meta>
<template #title>
<a-space size="mini">
{{name}}
<span style="height: 12px;width: 12px;background-color: #03ad03;border-radius: 6px;" v-if="status===ECommon_User_Online_Status.ONLINE"></span>
<icon-stop :stroke-width="5" style="color: darkred" v-else-if="status===ECommon_User_Online_Status.BUSY"></icon-stop>
<icon-video-camera :stroke-width="5" style="color: red" v-else-if="status===ECommon_User_Online_Status.MEETING"></icon-video-camera>
<span style="height: 12px;width: 12px;background-color: gray;border-radius: 6px" v-else-if="status===ECommon_User_Online_Status.OFFLINE"></span>
</a-space>
</template>
<template #avatar>
<a-avatar :image-url="photo" :size="64">{{imgName}}</a-avatar>
<a-avatar :image-url="photo" :size="64" :trigger-icon-style="{right:'-9px',bottom:'-9px',backgroundColor:'transparent'}">
{{imgName}}
</a-avatar>
</template>
<template #description v-if="info">
<a-space v-if="info.job">
@ -29,7 +42,18 @@
</template>
</a-list-item-meta>
<template #actions>
<a-button type="outline" size="small" style="margin-left: 20px" @click="onProfile">Profile</a-button>
<template v-if="organizationUserId===myOrganizationUserId">
<a-button type="outline" size="small" style="margin-left: 20px" @click="onProfile">Profile</a-button>
</template>
<a-row v-else style="flex-direction: column">
<a-button type="outline" size="mini" style="margin-left: 20px" @click="onProfile">Profile</a-button>
<a-button type="outline" size="mini" style="margin-left: 20px;margin-top: 10px" @click="onMessage">
<template #icon>
<icon-message></icon-message>
</template>
Message
</a-button>
</a-row>
</template>
</a-list-item>
</a-list>
@ -42,19 +66,26 @@ import {computed, ref} from "vue";
import {ICommon_Model_Organization_User} from "../../../../../common/model/organization_user";
import {apiOrganization, DCSType} from "../request/request";
import {EClient_EVENTBUS_TYPE, eventBus} from "../event/event";
import {SessionStorage} from "../storage/session";
import {ECommon_IM_Message_EntityType} from "../../../../../common/model/im_unread_message";
import {ECommon_User_Online_Status} from "../../../../../common/types";
const loading=ref(true)
const props=defineProps<{
name:string,
photo?:string,
organizationUserId:string,
closeable?:boolean
closeable?:boolean,
onlyPhoto?:boolean,
trigger?:'hover' | 'click' | 'focus' | 'contextMenu'
}>()
const emit=defineEmits<{
(e:"close",organizationUserId:string):void
}>()
const myOrganizationUserId=SessionStorage.get("organizationUserId")
const showCloseable=ref(false)
const root=ref(null);
const status=ref(ECommon_User_Online_Status.OFFLINE)
const imgName=computed(()=>{
if(props.name.includes(" ")) {
let arr=props.name.split(" ")
@ -71,15 +102,29 @@ const onProfile=()=>{
}
const onPopup=async (visible:boolean)=>{
if(!info.value && visible) {
let res=await apiOrganization.user({
organizationUserId:props.organizationUserId
})
if(res?.code==0) {
info.value=res.data
Promise.all([
apiOrganization.user({
organizationUserId:props.organizationUserId
}).then(res => {
if(res?.code==0) {
info.value=res.data
}
}),
apiOrganization.getUserStatus({
organizationUserId:props.organizationUserId
}).then(res => {
if(res?.code==0) {
status.value=res.data.status
}
})
]).then(value => {
loading.value=false
}
})
}
}
const onMessage=()=>{
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,props.organizationUserId,ECommon_IM_Message_EntityType.USER)
}
</script>
<style scoped>

View File

@ -1,16 +1,24 @@
import {apiOrganization} from "../request/request";
import {EClient_EVENTBUS_TYPE, eventBus} from "../event/event";
import {ECommon_IM_Message_EntityType} from "../../../../../common/model/im_unread_message";
import {ECommon_User_Online_Status} from "../../../../../common/types";
class UserTeamInfoPick {
private map = new Map<string, {
id: string,
name: string,
photo: string,
type:ECommon_IM_Message_EntityType
type:ECommon_IM_Message_EntityType,
status?:ECommon_User_Online_Status
}>()
private pendingSet = new Set<string>()
constructor () {
eventBus.on(EClient_EVENTBUS_TYPE.UPDATE_ORGANIZATION_USER_STATUS, (organizationUserId, status) => {
if(this.map.has(organizationUserId)) {
this.map.get(organizationUserId).status=status
}
})
}
getInfos(ids: {
id:string,
type:ECommon_IM_Message_EntityType
@ -19,7 +27,8 @@ class UserTeamInfoPick {
id: string,
name: string,
photo: string,
type:ECommon_IM_Message_EntityType
type:ECommon_IM_Message_EntityType,
status?:ECommon_User_Online_Status
}
} {
let arr: ReturnType<UserTeamInfoPick["getInfos"]> = {}

View File

@ -1,6 +1,7 @@
import EventEmitter from "eventemitter3"
import {ECommon_IM_Message_EntityType} from "../../../../../common/model/im_unread_message";
import {ECommon_IM_Message_ContentType} from "../../../../../common/model/im_user_message";
import {ECommon_User_Online_Status} from "../../../../../common/types";
export enum EClient_EVENTBUS_TYPE {
OPEN_PEOPLE_PROFILE="open_people_profile",
@ -11,7 +12,11 @@ export enum EClient_EVENTBUS_TYPE {
OPEN_WIKI_PROFILE="open_wiki_profile",
OPEN_WIKI_ITEM="open_wiki_item",
UPDATE_USER_INFO="update_user_info",
RECEIVE_IM_MESSAGE="receive_im_message"
RECEIVE_IM_MESSAGE="receive_im_message",
OPEN_IM_CHAT="open_im_chat",
UPDATE_ORGANIZATION_USER_STATUS="update_organization_user_status",
OPEN_MEETING="open_meeting",
LEAVE_MEETING="leave_meeting"
}
interface IClient_EventBus_Func {
@ -29,6 +34,10 @@ interface IClient_EventBus_Func {
type:ECommon_IM_Message_EntityType
})=>void
[EClient_EVENTBUS_TYPE.RECEIVE_IM_MESSAGE]:(chatType:ECommon_IM_Message_EntityType,fromOrganizationUserId:string,content:string,contentType:ECommon_IM_Message_ContentType,date:Date,toOrganizationUserId?:string,teamId?:string)=>void
[EClient_EVENTBUS_TYPE.OPEN_IM_CHAT]:(id:string,chatType:ECommon_IM_Message_EntityType)=>void
[EClient_EVENTBUS_TYPE.UPDATE_ORGANIZATION_USER_STATUS]:(organizationUserId:string,status:ECommon_User_Online_Status)=>void
[EClient_EVENTBUS_TYPE.OPEN_MEETING]:(meetingId:string,password:string)=>void
[EClient_EVENTBUS_TYPE.LEAVE_MEETING]:()=>void
}
interface IClient_EventBus_Emit_Func {

View File

@ -11,6 +11,8 @@ import workflow from "../../../../../common/routes/workflow"
import user from "../../../../../common/routes/user"
import wiki from "../../../../../common/routes/wiki"
import calendar from "../../../../../common/routes/calendar"
import meeting from "../../../../../common/routes/meeting"
import gateway from "../../../../../common/routes/gateway"
import {Ref} from "vue";
import {SessionStorage} from "../storage/session";
@ -166,5 +168,7 @@ export const apiIssue=generatorApi(issue)
export const apiIssueType=generatorApi(issueType)
export const apiWiki=generatorApi(wiki)
export const apiCalendar=generatorApi(calendar)
export const apiMeeting=generatorApi(meeting)
export const apiGateway=generatorApi(gateway)

View File

@ -13,6 +13,12 @@ export class SocketIOClient {
static getSocket(name:ECommon_Socket_Type) {
return this.map.get(name)
}
static clear() {
for(let obj of this.map.values()) {
obj.close()
}
this.map.clear()
}
constructor(name:ECommon_Socket_Type) {
this.name=name
this.socket=io("/"+name,{
@ -21,9 +27,6 @@ export class SocketIOClient {
token:SessionStorage.get("userToken")
}
})
this.socket.on("connect_error", err => {
SocketIOClient.map.delete(this.name)
})
this.socket.on("disconnect", reason => {
SocketIOClient.map.delete(this.name)
})

View File

@ -69,4 +69,23 @@ export function clone(o){
}
}
return ret;
}
}
export function calculateShortName(name:string) {
if(name.includes(" ")) {
let arr=name.split(" ")
return arr[0][0].toUpperCase()+arr[1][0].toUpperCase()
} else {
return name[0].toUpperCase()
}
}
export function compressImage (img:CanvasImageSource, width, height, ratio) {
let canvas = document.createElement('canvas')
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
let img64 = canvas.toDataURL("image/jpeg", ratio);
return img64;
}

View File

@ -3,12 +3,7 @@
</template>
<script setup lang="ts">
import {onBeforeMount} from "vue";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
onBeforeMount(()=>{
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_PEOPLE_PROFILE,"7013352022736896002");
})
</script>

View File

@ -28,6 +28,7 @@ import {reactive} from "vue";
import {apiUser} from "../../../common/request/request";
import {Message} from "@arco-design/web-vue";
import {SessionStorage} from "../../../common/storage/session";
import md5 from "blueimp-md5";
const store=useDesktopStore()
const form=reactive({
@ -57,7 +58,7 @@ const onSubmit=async ()=>{
if(form.password) {
arrPromise.push(apiUser.resetPassword({
userId:SessionStorage.get("userId"),
password:form.password
password:md5(form.password)
}))
}
let [res1,res2]=await Promise.all(arrPromise)

View File

@ -1,6 +1,6 @@
<template>
<a-space wrap>
<UserAvatar v-for="value in showValue" v-if="showValue && showValue.length>0" :organization-user-id="value.organizationUserId" :name="value.nickname" :photo="value.photo" :key="value.userId" @close="onClose"></UserAvatar>
<UserAvatar v-for="value in showValue" v-if="showValue && showValue.length>0" :organization-user-id="value.organizationUserId" :name="value.nickname" :photo="value.photo" :key="value.userId" @close="onClose" :closeable="true"></UserAvatar>
<span v-else style="line-height: 30px;width: 100%;color: gray">None</span>
<a-select v-model="addValue" allow-search @search="onSearch" v-if="showInput" @change="onChange">
<a-option v-for="item1 in searchValueList" :label="item1.nickname" :value="item1.userId"></a-option>

View File

@ -0,0 +1,46 @@
import {NotificationType, NotificationWrapper} from "../../../common/component/notification/notification";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
import {ECommon_IM_Message_EntityType} from "../../../../../../common/model/im_unread_message";
import {ECommon_IM_Message_ContentType} from "../../../../../../common/model/im_user_message";
import {SocketIOClient} from "../../../common/socket/socket";
import {SessionStorage} from "../../../common/storage/session";
import {ECommon_User_Online_Status} from "../../../../../../common/types";
export function handleIMEvent(socket:SocketIOClient["socket"],statue:ECommon_User_Online_Status) {
let myOrganizationUserId=SessionStorage.get("organizationUserId")
socket.on("im_user_relay_text_message",(fromOrganizationUserId, toOrganizationUserId, content,date) => {
if(myOrganizationUserId!==fromOrganizationUserId && statue===ECommon_User_Online_Status.ONLINE) {
NotificationWrapper.show("IM User Message",content.substring(0,20),NotificationType.IM,(type, data) => {
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,fromOrganizationUserId,ECommon_IM_Message_EntityType.USER)
},null,3000)
}
eventBus.emit(EClient_EVENTBUS_TYPE.RECEIVE_IM_MESSAGE,ECommon_IM_Message_EntityType.USER,fromOrganizationUserId,content,ECommon_IM_Message_ContentType.TEXT,date,toOrganizationUserId)
})
socket.on("im_team_relay_text_message",(organizationUserId, teamId, content,date) => {
if(myOrganizationUserId!==organizationUserId && statue===ECommon_User_Online_Status.ONLINE) {
NotificationWrapper.show("IM Team Message",content.substring(0,20),NotificationType.IM,(type, data) => {
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,teamId,ECommon_IM_Message_EntityType.TEAM)
},null,3000)
}
eventBus.emit(EClient_EVENTBUS_TYPE.RECEIVE_IM_MESSAGE,ECommon_IM_Message_EntityType.TEAM,organizationUserId,content,ECommon_IM_Message_ContentType.TEXT,date,null,teamId)
})
socket.on("im_user_relay_image_message",(fromOrganizationUserId, toOrganizationUserId, fileId,date) => {
if(myOrganizationUserId!==fromOrganizationUserId && statue===ECommon_User_Online_Status.ONLINE) {
NotificationWrapper.show("IM User Message","Image",NotificationType.IM,(type, data) => {
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,fromOrganizationUserId,ECommon_IM_Message_EntityType.USER)
},null,3000)
}
eventBus.emit(EClient_EVENTBUS_TYPE.RECEIVE_IM_MESSAGE,ECommon_IM_Message_EntityType.USER,fromOrganizationUserId,fileId,ECommon_IM_Message_ContentType.IMAGE,date,toOrganizationUserId)
})
socket.on("im_team_relay_image_message",(organizationUserId, teamId, fileId,date) => {
if(myOrganizationUserId!==organizationUserId && statue===ECommon_User_Online_Status.ONLINE) {
NotificationWrapper.show("IM Team Message","Image",NotificationType.IM,(type, data) => {
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,teamId,ECommon_IM_Message_EntityType.TEAM)
},null,3000)
}
eventBus.emit(EClient_EVENTBUS_TYPE.RECEIVE_IM_MESSAGE,ECommon_IM_Message_EntityType.TEAM,organizationUserId,fileId,ECommon_IM_Message_ContentType.IMAGE,date,null,teamId)
})
socket.on("im_organization_user_status_change",(organizationUserId, status) => {
eventBus.emit(EClient_EVENTBUS_TYPE.UPDATE_ORGANIZATION_USER_STATUS,organizationUserId,status)
})
}

View File

@ -21,12 +21,25 @@
</a-optgroup>
</a-select>
<a-divider>Recent</a-divider>
<RecentList ref="recentListRef" @select="onSelectChat"></RecentList>
<RecentList ref="recentListRef" @select="onSelectChat" :init-data="id?{id,type:chatType}:undefined"></RecentList>
</div>
</a-layout-sider>
<a-layout-content style="padding: 10px;">
<Chat :data="messageList" :type="selectedChat.type" :info="selectedChat" @send="onSend" v-if="selectedChat" ref="chatRef"></Chat>
<Chat :data="messageList" :type="selectedChat.type" :info="selectedChat" @send="onSend" v-if="selectedChat" ref="chatRef" @update-more="onShowMore" @click-photo="onClickChatPhoto">
<template #info v-if="selectedChat.type===ECommon_IM_Message_EntityType.TEAM">
<a-collapse :default-active-key="['members']">
<a-collapse-item header="Members" key="members">
<a-space wrap>
<UserAvatar v-for="item in teamMemberList" :organization-user-id="item.id" :name="item.name" :photo="item.photo"></UserAvatar>
</a-space>
</a-collapse-item>
</a-collapse>
</template>
</Chat>
</a-layout-content>
<teleport to="body" v-if="chatPhotoProfileInfo">
<UserShortView :organization-user-id="chatPhotoProfileInfo.id" :photo="chatPhotoProfileInfo.photo" style="position:absolute;z-index: 1000;box-shadow: rgba(169, 169, 169, 0.2) 0px 0px 2px 2px;border-radius: 5px;background-color: white" :style="{left:chatPhotoProfileInfo.x+'px',top:chatPhotoProfileInfo.y+'px'}" @mouseleave="chatPhotoProfileInfo=null" @click="chatPhotoProfileInfo=null"></UserShortView>
</teleport>
</a-layout>
</template>
@ -34,7 +47,7 @@
import RecentList, {RecentItem} from "./recentList.vue";
import {nextTick, onBeforeMount, onBeforeUnmount, ref, watch} from "vue";
import {ICommon_Route_Res_Organization_FilterUserAndTeam} from "../../../../../../common/routes/response";
import {apiOrganization} from "../../../common/request/request";
import {apiOrganization, apiTeam} from "../../../common/request/request";
import {ECommon_IM_Message_EntityType} from "../../../../../../common/model/im_unread_message";
import {IClient_Chat_Message_Item} from "../../../common/component/chat/type";
import {SocketIOClient} from "../../../common/socket/socket";
@ -46,13 +59,31 @@ import Chat from "../../../common/component/chat/chat.vue";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
import {SessionStorage} from "../../../common/storage/session";
import {Message} from "@arco-design/web-vue";
import UserAvatar from "../../../common/component/userAvatar.vue";
import UserShortView from "./userShortView.vue";
const props=defineProps<{
id?:string,
chatType?:ECommon_IM_Message_EntityType
}>()
const recentListRef=ref<InstanceType<typeof RecentList>>()
const selectList=ref<ICommon_Route_Res_Organization_FilterUserAndTeam>()
const messageList=ref<IClient_Chat_Message_Item[]>([])
const selectedChat=ref<RecentItem>()
const socket=SocketIOClient.get(ECommon_Socket_Type.IM)
const chatRef=ref<InstanceType<typeof Chat>>()
const chatPhotoProfileInfo=ref<{
id:string,
photo:string,
x:number,
y:number
}>()
let isShowMorePending=false
const teamMemberList=ref<{
id:string,
photo:string,
name:string
}[]>([])
const onSearch=async (key:string)=>{
if(!key) {
return
@ -93,12 +124,11 @@ const onChange=(id:string)=>{
type=ECommon_IM_Message_EntityType.TEAM
}
})
recentListRef.value.addRecentItem(id,type)
recentListRef.value.addRecentItem(id,type,true)
}
}
const onSelectChat=async (item:RecentItem)=>{
if(item && item !==selectedChat.value) {
selectedChat.value=item
if(item.type===ECommon_IM_Message_EntityType.USER) {
let ret=await socket.getSocket().emitWithAck("im_user_message_list",item.id,20,0)
messageList.value=ret.map(item=>{
@ -110,7 +140,7 @@ const onSelectChat=async (item:RecentItem)=>{
id:item.from_organization_user_id,
type:item.content_type,
date:moment(item.created_time).format("YYYY-MM-DD HH:mm:ss.SSS"),
content:item.content,
content:item.content || item.file_id,
name:obj[item.from_organization_user_id]?obj[item.from_organization_user_id].name:"unknown",
photo:obj[item.from_organization_user_id]?obj[item.from_organization_user_id].photo:""
}
@ -126,13 +156,20 @@ const onSelectChat=async (item:RecentItem)=>{
id:item.from_organization_user_id,
type:item.content_type,
date:moment(item.created_time).format("YYYY-MM-DD HH:mm:ss.SSS"),
content:item.content,
content:item.content || item.file_id,
name:obj[item.from_organization_user_id]?obj[item.from_organization_user_id].name:"unknown",
photo:obj[item.from_organization_user_id]?obj[item.from_organization_user_id].photo:""
}
})
getTeamMemberList(item.id)
}
if(item.count) {
item.count=0;
await socket.getSocket().emit("im_read_message",item.id)
}
selectedChat.value=item
} else if(!item) {
selectedChat.value=null;
}
}
@ -145,9 +182,9 @@ const onSend=(content:string,contentType:ECommon_IM_Message_ContentType,chatType
contentType
})
if(chatType===ECommon_IM_Message_EntityType.TEAM) {
recentListRef.value.updateRecentItem(teamId)
recentListRef.value.addRecentItem(teamId,chatType,true)
} else if(chatType===ECommon_IM_Message_EntityType.USER) {
recentListRef.value.updateRecentItem(toOrganizationUserId)
recentListRef.value.addRecentItem(toOrganizationUserId,chatType,true)
}
}
@ -164,9 +201,12 @@ const sendMessage=async (item:SendItem)=>{
} else if(item.chatType===ECommon_IM_Message_EntityType.TEAM) {
ret=await socket.getSocket().timeout(5000).emitWithAck("im_team_send_text_message",item.teamId,item.content)
}
} else if(item.contentType===ECommon_IM_Message_ContentType.IMAGE) {
if(item.chatType===ECommon_IM_Message_EntityType.USER) {
ret=await socket.getSocket().timeout(5000).emitWithAck("im_user_send_image_message",item.toOrganizationUserId,item.content)
} else if(item.chatType===ECommon_IM_Message_EntityType.TEAM) {
ret = await socket.getSocket().timeout(5000).emitWithAck("im_team_send_image_message", item.teamId, item.content)
}
}
sendBufferList.value.pop()
if(!ret.success) {
@ -189,7 +229,7 @@ const onReceiveMessage=(chatType, fromOrganizationUserId, content, contentType,d
id:toOrganizationUserId,
type:ECommon_IM_Message_EntityType.USER
}])
if([toOrganizationUserId,fromOrganizationUserId].includes(selectedChat.value.id)) {
if([toOrganizationUserId,fromOrganizationUserId].includes(selectedChat.value?.id)) {
messageList.value.push({
id:fromOrganizationUserId,
content:content,
@ -203,6 +243,12 @@ const onReceiveMessage=(chatType, fromOrganizationUserId, content, contentType,d
chatRef.value.scrollToBottom()
})
}
} else {
let item=recentListRef.value.addRecentItem(fromOrganizationUserId,ECommon_IM_Message_EntityType.USER, false)
if(!item.count) {
item.count=1;
socket.getSocket().emit("im_unread_message",fromOrganizationUserId,ECommon_IM_Message_EntityType.USER)
}
}
} else if(chatType===ECommon_IM_Message_EntityType.TEAM) {
let obj=userTeamInfoPick.getInfos([{
@ -212,7 +258,7 @@ const onReceiveMessage=(chatType, fromOrganizationUserId, content, contentType,d
id:teamId,
type:ECommon_IM_Message_EntityType.TEAM
}])
if(selectedChat.value.id===teamId) {
if(selectedChat.value?.id===teamId) {
messageList.value.push({
id:fromOrganizationUserId,
content:content,
@ -226,10 +272,73 @@ const onReceiveMessage=(chatType, fromOrganizationUserId, content, contentType,d
chatRef.value.scrollToBottom()
})
}
} else {
let item=recentListRef.value.addRecentItem(teamId,ECommon_IM_Message_EntityType.TEAM, false)
if(!item.count) {
item.count=1;
socket.getSocket().emit("im_unread_message",teamId,ECommon_IM_Message_EntityType.TEAM)
}
}
}
}
const onShowMore=async (lastItem:IClient_Chat_Message_Item, type: ECommon_IM_Message_EntityType, info: {
name: string,
photo: string,
id: string
})=>{
if(!isShowMorePending) {
isShowMorePending=true
try {
if(type===ECommon_IM_Message_EntityType.USER) {
let ret=await socket.getSocket().emitWithAck("im_user_message_list",info.id,20,lastItem?moment(lastItem.date).toDate().getTime():0)
messageList.value.push(...ret.map(item=>{
let obj=userTeamInfoPick.getInfos([{
id:item.from_organization_user_id,
type:ECommon_IM_Message_EntityType.USER
}])
return {
id:item.from_organization_user_id,
type:item.content_type,
date:moment(item.created_time).format("YYYY-MM-DD HH:mm:ss.SSS"),
content:item.content || item.file_id,
name:obj[item.from_organization_user_id]?obj[item.from_organization_user_id].name:"unknown",
photo:obj[item.from_organization_user_id]?obj[item.from_organization_user_id].photo:""
}
}))
} else if(type===ECommon_IM_Message_EntityType.TEAM) {
let ret=await socket.getSocket().emitWithAck("im_team_message_list",info.id,20,lastItem?moment(lastItem.date).toDate().getTime():0)
messageList.value.push(...ret.map(item=>{
let obj=userTeamInfoPick.getInfos([{
id:item.from_organization_user_id,
type:ECommon_IM_Message_EntityType.USER
}])
return {
id:item.from_organization_user_id,
type:item.content_type,
date:moment(item.created_time).format("YYYY-MM-DD HH:mm:ss.SSS"),
content:item.content || item.file_id,
name:obj[item.from_organization_user_id]?obj[item.from_organization_user_id].name:"unknown",
photo:obj[item.from_organization_user_id]?obj[item.from_organization_user_id].photo:""
}
}))
}
isShowMorePending=false
} catch {
isShowMorePending=false
}
}
}
const onClickChatPhoto=(item:IClient_Chat_Message_Item, x:number, y:number)=>{
chatPhotoProfileInfo.value={
id:item.id,
photo:item.photo,
x,
y
}
}
const handleUserInfo = (id: string, info: {
id: string,
name: string,
@ -241,15 +350,44 @@ const handleUserInfo = (id: string, info: {
obj.photo=info.photo
}
}
if(selectedChat.value?.id===id) {
selectedChat.value.name=info.name
selectedChat.value.photo=info.photo
}
}
const getTeamMemberList=async (teamId:string)=>{
teamMemberList.value=[]
let ret=await apiTeam.members({
teamId,
page:0,
size:Number.MAX_SAFE_INTEGER
})
if(ret?.code==0) {
teamMemberList.value=ret.data.data.map(item=>{
return {
id:item.organizationUser.id,
name:item.organizationUser.nickname,
photo:item.user.photo
}
})
}
}
const openSpecificChat=(id:string, chatType:ECommon_IM_Message_EntityType)=>{
recentListRef.value.addRecentItem(id,chatType,true)
}
onBeforeMount(()=>{
eventBus.on(EClient_EVENTBUS_TYPE.RECEIVE_IM_MESSAGE, onReceiveMessage)
eventBus.on(EClient_EVENTBUS_TYPE.UPDATE_USER_INFO, handleUserInfo)
eventBus.on(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,openSpecificChat)
})
onBeforeUnmount(()=>{
eventBus.off(EClient_EVENTBUS_TYPE.RECEIVE_IM_MESSAGE ,onReceiveMessage)
eventBus.off(EClient_EVENTBUS_TYPE.UPDATE_USER_INFO, handleUserInfo)
eventBus.off(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,openSpecificChat)
})
</script>
<style scoped>

View File

@ -1,10 +1,28 @@
<template>
<a-menu v-model:selected-keys="selectedKeys">
<a-menu-item v-for="item in recentList" :key="item.id">
<a-menu-item v-for="(item,index) in recentList" :key="item.id">
<template #icon>
<a-avatar :image-url="item.photo" :size="24"></a-avatar>
<a-avatar :image-url="item.photo" :size="24" :trigger-icon-style="{height:'10px',width:'10px',lineHeight:'10px',right:'-2px',bottom:'-2px',...(item.status===ECommon_User_Online_Status.MEETING && {backgroundColor:'transparent'})}">
{{calculateShortName(item.name)}}
<template #trigger-icon v-if="item.type===ECommon_IM_Message_EntityType.USER">
<div style="height: 100%;width: 100%;background-color: #03ad03;border-radius: 6px" v-if="item.status===ECommon_User_Online_Status.ONLINE"></div>
<icon-stop :stroke-width="5" style="color: darkred" v-else-if="item.status===ECommon_User_Online_Status.BUSY"></icon-stop>
<icon-video-camera :stroke-width="5" style="color: red" v-else-if="item.status===ECommon_User_Online_Status.MEETING"></icon-video-camera>
<div style="height: 100%;width: 100%;background-color: gray;border-radius: 6px" v-else-if="item.status===ECommon_User_Online_Status.OFFLINE"></div>
</template>
</a-avatar>
</template>
{{item.name}}
<a-dropdown trigger="contextMenu" alignPoint >
<div>
<a-badge :count="item.count!==undefined?item.count:0" dot :offset="[6,-2]">
{{item.name}}
</a-badge>
</div>
<template #content>
<a-doption @click="onProfile(item)">Profile</a-doption>
<a-doption @click="onClose(item,index)">Close</a-doption>
</template>
</a-dropdown>
</a-menu-item>
</a-menu>
</template>
@ -17,16 +35,25 @@ import {ECommon_Socket_Type} from "../../../../../../common/socket/types";
import {ECommon_IM_Message_EntityType} from "../../../../../../common/model/im_unread_message";
import {userTeamInfoPick} from "../../../common/component/userInfoPick";
import {SessionStorage} from "../../../common/storage/session";
import {calculateShortName} from "../../../common/util/helper";
import {ECommon_User_Online_Status} from "../../../../../../common/types";
const emit=defineEmits<{
(e:"select",item:RecentItem):void
}>()
const props=defineProps<{
initData?:{
id:string,
type:ECommon_IM_Message_EntityType
}
}>()
export type RecentItem = {
id: string,
name: string,
photo: string,
count?:number,
type:ECommon_IM_Message_EntityType
type:ECommon_IM_Message_EntityType,
status?:ECommon_User_Online_Status
}
const selectedKeys=ref<string[]>([])
const recentList = ref<RecentItem[]>([])
@ -55,7 +82,7 @@ watch(selectedKeys,()=>{
const socket=SocketIOClient.get(ECommon_Socket_Type.IM)
const getRecentList = async () => {
let localList = SessionStorage.get("imRecentList")??[];
let unReadList=await socket.getSocket().emitWithAck("im_unread_messages")
let unReadList=await socket.getSocket().emitWithAck("im_unread_message_list")
let obj = userTeamInfoPick.getInfos([...localList,...unReadList.map(item=>{
return {
id:item.entity_id,
@ -66,7 +93,7 @@ const getRecentList = async () => {
return {
id:item.entity_id,
type:item.entity_type,
count:item.count
count:item.count,
}
}),...localList.filter(item=>{
return !unReadList.map(item=>item.entity_id).includes(item.id)
@ -81,25 +108,33 @@ const getRecentList = async () => {
name:obj[item.id]?obj[item.id].name:"unknown",
photo:obj[item.id]?obj[item.id].photo:"",
type:item.type,
count:item.count
count:item.count,
status:obj[item.id]!==undefined?obj[item.id].status:ECommon_User_Online_Status.OFFLINE
}
})
if(props.initData) {
addRecentItem(props.initData.id,props.initData.type,true)
}
}
const handleUserInfo = (id: string, info: {
id: string,
name: string,
photo: string
photo: string,
status?:ECommon_User_Online_Status
}) => {
for(let obj of recentList.value) {
if(obj.id==id) {
obj.name=info.name;
obj.photo=info.photo
if(info.status!==undefined) {
obj.status=info.status
}
break
}
}
}
const addRecentItem=(id:string,type:ECommon_IM_Message_EntityType)=>{
let index=-1
const addRecentItem=(id:string,type:ECommon_IM_Message_EntityType,selected:boolean):RecentItem=>{
let index=-1,item:RecentItem
for(let i=0;i<recentList.value.length;i++) {
if(recentList.value[i].id===id) {
index=i;
@ -107,9 +142,9 @@ const addRecentItem=(id:string,type:ECommon_IM_Message_EntityType)=>{
}
}
if(index>-1) {
let obj=recentList.value[index]
item=recentList.value[index]
recentList.value.splice(index,1)
recentList.value.unshift(obj)
recentList.value.unshift(item)
} else {
let obj=userTeamInfoPick.getInfos([
{
@ -117,42 +152,70 @@ const addRecentItem=(id:string,type:ECommon_IM_Message_EntityType)=>{
type
}
])
recentList.value.unshift({
item={
id:id,
type:type,
name:obj[id]?obj[id].name:"unknown",
photo:obj[id]?obj[id].photo:"",
count:0
})
count:0,
status:obj[id]!==undefined?obj[id].status:ECommon_User_Online_Status.OFFLINE
}
recentList.value.unshift(item)
}
selectedKeys.value=[id]
if(selected && !selectedKeys.value.includes(id)) {
selectedKeys.value=[id]
}
return item
}
const updateRecentItem=(id:string) =>{
let item:RecentItem,index
const checkRecentItem=(id:string)=> {
let item:RecentItem
for(let i=0;i<recentList.value.length;i++) {
let obj=recentList.value[i]
if(obj.id==id) {
item=obj;
index=i
break
}
}
if(item) {
recentList.value.splice(index,1)
recentList.value.unshift(item)
return item
}
const onProfile=(item:RecentItem)=>{
if(item.type===ECommon_IM_Message_EntityType.USER) {
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_PEOPLE_PROFILE,item.id);
} else if(item.type===ECommon_IM_Message_EntityType.TEAM) {
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_TEAM_PROFILE,item.id);
}
}
const onClose=(item:RecentItem,index:number)=>{
recentList.value.splice(index,1)
if(selectedKeys.value.includes(item.id)) {
selectedKeys.value=[]
}
}
const handleStatus=(organizationUserId:string,status:ECommon_User_Online_Status)=>{
for(let obj of recentList.value) {
if(obj.id===organizationUserId) {
obj.status=status
break
}
}
}
onBeforeMount(() => {
eventBus.on(EClient_EVENTBUS_TYPE.UPDATE_USER_INFO, handleUserInfo)
eventBus.on(EClient_EVENTBUS_TYPE.UPDATE_ORGANIZATION_USER_STATUS, handleStatus)
getRecentList()
})
onBeforeUnmount(() => {
eventBus.off(EClient_EVENTBUS_TYPE.UPDATE_USER_INFO, handleUserInfo)
eventBus.off(EClient_EVENTBUS_TYPE.UPDATE_ORGANIZATION_USER_STATUS, handleStatus)
})
defineExpose({
addRecentItem,
updateRecentItem
checkRecentItem
})
</script>

View File

@ -0,0 +1,96 @@
<template>
<a-list :loading="loading" :bordered="false" size="small">
<a-list-item v-if="info">
<a-list-item-meta>
<template #title>
<a-space>
{{info.nickname}}
<span style="height: 12px;width: 12px;background-color: #03ad03;border-radius: 6px;vertical-align: middle" v-if="status===ECommon_User_Online_Status.ONLINE"></span>
<icon-stop :stroke-width="5" style="color: darkred" v-else-if="status===ECommon_User_Online_Status.BUSY"></icon-stop>
<icon-video-camera :stroke-width="5" style="color: red" v-else-if="status===ECommon_User_Online_Status.MEETING"></icon-video-camera>
<span style="height: 12px;width: 12px;background-color: gray;border-radius: 6px;vertical-align: middle" v-else-if="status===ECommon_User_Online_Status.OFFLINE"></span>
</a-space>
</template>
<template #avatar>
<a-avatar :image-url="photo" :size="64">{{calculateShortName(info.nickname)}}</a-avatar>
</template>
<template #description>
<a-space v-if="info.job">
<sicon size="" color="" name="Ant" type="user"></sicon>
<span>{{info.job}}</span>
</a-space>
<br>
<a-space v-if="info.department">
<sicon size="" color="" name="Ant" type="apartment"></sicon>
<span>{{info.department}}</span>
</a-space>
</template>
</a-list-item-meta>
<template #actions>
<template v-if="organizationUserId===myOrganizationUserId">
<a-button type="outline" size="small" style="margin-left: 20px" @click="onProfile">Profile</a-button>
</template>
<a-row v-else style="flex-direction: column">
<a-button type="outline" size="mini" style="margin-left: 20px" @click="onProfile">Profile</a-button>
<a-button type="outline" size="mini" style="margin-left: 20px;margin-top: 10px" @click="onMessage">
<template #icon>
<icon-message></icon-message>
</template>
Message
</a-button>
</a-row>
</template>
</a-list-item>
</a-list>
</template>
<script setup lang="ts">
import {calculateShortName} from "../../../common/util/helper";
import {ref, watch} from "vue";
import {apiOrganization, DCSType} from "../../../common/request/request";
import {ICommon_Model_Organization_User} from "../../../../../../common/model/organization_user";
import {SessionStorage} from "../../../common/storage/session";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
import {ECommon_IM_Message_EntityType} from "../../../../../../common/model/im_unread_message";
import {ECommon_User_Online_Status} from "../../../../../../common/types";
const props=defineProps<{
organizationUserId:string,
photo:string
}>()
const loading=ref(false)
const info=ref<DCSType<ICommon_Model_Organization_User>>()
const myOrganizationUserId=SessionStorage.get("organizationUserId")
const status=ref(ECommon_User_Online_Status.OFFLINE)
watch(()=>props.organizationUserId,async ()=>{
loading.value=true
Promise.all([
apiOrganization.user({
organizationUserId:props.organizationUserId
}).then(res=>{
info.value=res.data
}),
apiOrganization.getUserStatus({
organizationUserId:props.organizationUserId
}).then(res=>{
status.value=res.data.status
})
]).then(res=>{
loading.value=false
})
},{
immediate:true
})
const onProfile=()=>{
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_PEOPLE_PROFILE,props.organizationUserId);
}
const onMessage=()=>{
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,props.organizationUserId,ECommon_IM_Message_EntityType.USER)
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,68 @@
<template>
<a-form auto-label-width :model="form" ref="formEle">
<a-form-item field="name" label="name" required>
<a-input v-model="form.name"></a-input>
</a-form-item>
<a-form-item field="description" label="description">
<a-textarea v-model="form.description" allow-clear></a-textarea>
</a-form-item>
<a-form-item field="startTime" label="start date" required>
<a-date-picker show-time v-model="form.startTime"></a-date-picker>
</a-form-item>
<a-form-item field="endTime" label="end date" required>
<a-date-picker show-time v-model="form.endTime"></a-date-picker>
</a-form-item>
<a-form-item field="password" label="password" required>
<a-input v-model="form.password"></a-input>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {apiMeeting, DCSType} from "../../../common/request/request";
import {ICommon_Model_Meeting_Room} from "../../../../../../common/model/meeting_room";
import {reactive, ref} from "vue";
import moment from "moment";
import {onDialogOk} from "../../../common/component/dialog/dialog";
import {dialogFuncGenerator} from "../../../common/util/helper";
import {SessionStorage} from "../../../common/storage/session";
const props=defineProps<{
type:"add"|"edit",
data?:DCSType<ICommon_Model_Meeting_Room>
}>()
const formEle=ref()
const now=moment();
const form=reactive({
name:props.type==="edit"?props.data.name:"",
description:props.type==="edit"?props.data.description:"",
startTime:props.type==="edit"?moment(props.data.start_time).format('YYYY-MM-DD HH:mm:ss'):now.format('YYYY-MM-DD HH:mm:ss'),
endTime:props.type==="edit"?moment(props.data.end_time).format('YYYY-MM-DD HH:mm:ss'):now.format('YYYY-MM-DD HH:mm:ss'),
password:props.type==="edit"?props.data.password:"",
})
const organizationUserId=SessionStorage.get("organizationUserId")
onDialogOk(dialogFuncGenerator({
form:()=>formEle.value,
func:()=>{
return props.type=="add"?apiMeeting.createRoom({
startTime:moment(form.startTime).toDate().getTime(),
endTime:moment(form.endTime).toDate().getTime(),
password:form.password,
description:form.description,
name:form.name,
related_id:organizationUserId
}):apiMeeting.editRoom({
name:form.name,
description:form.description,
password:form.password,
startTime:moment(form.startTime).toDate().getTime(),
endTime:moment(form.endTime).toDate().getTime(),
meetingRoomId:props.data.id
})
}
}))
</script>
<style scoped>
</style>

View File

@ -0,0 +1,15 @@
import {SocketIOClient} from "../../../common/socket/socket";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
import {apiMeeting} from "../../../common/request/request";
import {NotificationType, NotificationWrapper} from "../../../common/component/notification/notification";
export function handleMeetingEvent(socket:SocketIOClient["socket"]) {
socket.on("meeting_invite",async (fromOrganizationUserId, fromOrganizationUserName, roomId, password) => {
let res=await apiMeeting.getCurrentRoom()
if(res?.code!==0 || (res?.code==0 && res.data.id!=roomId)) {
NotificationWrapper.show("Meeting Invite",`${fromOrganizationUserName} is inviting you`,NotificationType.MEETING,(type, data) => {
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_MEETING,roomId,password)
},null,30000)
}
})
}

View File

@ -0,0 +1,227 @@
<template>
<a-layout style="height: 100%">
<a-layout-content>
<a-row style="height: 100%;display: flex;flex-direction: column;justify-content: center;align-items: center">
<a-dropdown class="startMeeting" trigger="hover" position="top">
<a-button size="large" type="primary" style="width: 300px" status="success" @click="onStartMeeting">
Start Meeting
</a-button>
<template #content>
<a-doption @click="onPersonalMeetingSetting">Person Meeting Setting</a-doption>
</template>
</a-dropdown>
<a-button size="large" type="primary" style="margin-top: 30px;width: 300px" status="warning" @click="onJoinMeeting">Join Meeting</a-button>
<a-button size="large" type="primary" style="margin-top: 30px;width: 300px" @click="onScheduleMeeting">Schedule Meeting</a-button>
</a-row>
</a-layout-content>
<a-layout-sider :resize-directions="['left']" :width="300">
<a-row style="height: 100%;width: 100%;padding: 5px;box-sizing: border-box">
<a-row style="height: 30px;width: 100%;">
<a-space>
Meeting List:
<a-input-search search-button placeholder="type meeting name" size="mini" @search="onSearch" v-model="keyword"></a-input-search>
</a-space>
</a-row>
<a-row style="height: calc(100% - 30px);width: 100%;border-top: 1px solid lightgray;overflow-y: auto;flex-direction: column">
<template v-if="roomList.length>0">
<a-list :bordered="false" style="width: 100%;margin-top: 10px" :pagination-props="pagination" @pageChange="onChangePage">
<a-list-item v-for="item in roomList" :key="item.id" style="border-bottom: 1px solid var(--color-fill-3)">
<a-list-item-meta>
<template #title>
{{item.name}}&nbsp;
<span style="font-size: smaller">{{moment(item.start_time).format('MM-DD HH:mm')}}</span>
</template>
<template #description>
{{item.description}}
</template>
</a-list-item-meta>
<template #actions>
<icon-video-camera style="color: red" @click="onStartScheduleMeeting(item)"></icon-video-camera>
<icon-edit @click="onEditMeeting(item)"></icon-edit>
<icon-delete @click="onDeleteMeeting(item)"></icon-delete>
</template>
</a-list-item>
</a-list>
</template>
<a-empty v-else style="margin-top: auto;margin-bottom: auto"></a-empty>
</a-row>
</a-row>
</a-layout-sider>
</a-layout>
</template>
<script setup lang="ts">
import {getCurrentInstance, markRaw, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from "vue";
import {apiMeeting, DCSType} from "../../../common/request/request";
import {ECommon_Model_Meeting_Room_Type, ICommon_Model_Meeting_Room} from "../../../../../../common/model/meeting_room";
import {Dialog} from "../../../common/component/dialog/dialog";
import {
ETeamOS_Navigator_Action,
getCurrentNavigator,
getRootNavigatorRef,
onNavigatorShow
} from "../../../../teamOS/common/component/navigator/navigator";
import PersonalMeetingSetting from "./personalMeetingSetting.vue";
import EditScheduleMeeting from "./editScheduleMeeting.vue";
import moment from "moment";
import {Message} from "@arco-design/web-vue";
import MeetingJoinInput from "./meetingJoinInput.vue";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
import {useDesktopStore} from "../../desktop/store/desktop";
import {ECommon_User_Online_Status} from "../../../../../../common/types";
const props=defineProps<{
meetingInitInfo?:{
id:string,
password:string
}
}>()
const store=useDesktopStore()
const appContext=getCurrentInstance().appContext
const root=getRootNavigatorRef()
const roomList=ref<DCSType<ICommon_Model_Meeting_Room>[]>([])
const pagination=reactive({
total:0,
current:1,
pageSize:10
})
let meetingWillJoinInfo:{
id:string,
password:string
}=null
const keyword=ref("");
const navigator=getCurrentNavigator()
const onStartMeeting=async ()=>{
let res=await apiMeeting.getPersonalRoom()
if(res?.code==0) {
navigator.push("meetingProfile",{
meetingId:res.data.id,
password:res.data.password
})
}
}
const onPersonalMeetingSetting=async()=>{
Dialog.open(root.value,appContext,"edit setting",markRaw(PersonalMeetingSetting))
}
const onChangePage=(page:number)=>{
getMeetingList(page)
}
const onScheduleMeeting=async ()=>{
let ret=await Dialog.open(root.value,appContext,"shcedule meeting",markRaw(EditScheduleMeeting),{
type:"add"
})
if(ret) {
getMeetingList(pagination.current)
}
}
const onEditMeeting=async (item:DCSType<ICommon_Model_Meeting_Room>)=>{
let ret=await Dialog.open(root.value,appContext,"Edit Meeting",markRaw(EditScheduleMeeting),{
type:"edit",
data:item
})
if(ret) {
getMeetingList(pagination.current)
}
}
const onDeleteMeeting=async (item:DCSType<ICommon_Model_Meeting_Room>)=>{
let ret=await Dialog.confirm(root.value,appContext,"Do you want to delete this meeting?")
if(ret) {
let res=await apiMeeting.deleteRoom({
meetingRoomId:item.id
})
if(res?.code==0) {
Message.success("delete success")
getMeetingList(pagination.current)
}
}
}
const onSearch=async ()=>{
getMeetingList(1)
}
const onJoinMeeting=async ()=>{
let ret:any=await Dialog.open(root.value,appContext,"Join Meeting",markRaw(MeetingJoinInput))
if(ret) {
navigator.push("meetingProfile",{
meetingId:ret.data.meetingRoomId,
password:ret.data.password
})
}
}
const onStartScheduleMeeting=async (item:DCSType<ICommon_Model_Meeting_Room>)=>{
navigator.push("meetingProfile",{
meetingId:item.id,
password:item.password
})
}
const getMeetingList=async (page:number)=>{
let res=await apiMeeting.listRoom({
keyword:keyword.value,
size:10,
page:page-1,
type:ECommon_Model_Meeting_Room_Type.SCHEDULE
})
if(res?.code==0) {
roomList.value=res.data.data
pagination.total=res.data.count;
pagination.current=page
}
}
const handleInvite=async (roomId, password) =>{
if(store.status===ECommon_User_Online_Status.MEETING) {
meetingWillJoinInfo={
id:roomId,
password
}
eventBus.emit(EClient_EVENTBUS_TYPE.LEAVE_MEETING)
} else {
navigator.push("meetingProfile",{
meetingId:roomId,
password:password
})
}
}
onBeforeMount(()=>{
getMeetingList(1)
eventBus.on(EClient_EVENTBUS_TYPE.OPEN_MEETING, handleInvite)
})
onBeforeUnmount(()=>{
eventBus.off(EClient_EVENTBUS_TYPE.OPEN_MEETING, handleInvite)
})
onMounted(()=>{
if(props.meetingInitInfo) {
navigator.push("meetingProfile",{
meetingId:props.meetingInitInfo.id,
password:props.meetingInitInfo.password
})
}
})
onNavigatorShow(action => {
if(action===ETeamOS_Navigator_Action.POP) {
if(meetingWillJoinInfo) {
navigator.push("meetingProfile",{
meetingId:meetingWillJoinInfo.id,
password:meetingWillJoinInfo.password
})
meetingWillJoinInfo=null;
}
}
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,37 @@
<template>
<a-form auto-label-width :model="form" ref="formEle">
<a-form-item field="id" label="meeting id" required>
<a-input v-model="form.id"></a-input>
</a-form-item>
<a-form-item field="password" label="meeting password" required>
<a-input v-model="form.password"></a-input>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {reactive, ref} from "vue";
import {onDialogOk} from "../../../common/component/dialog/dialog";
import {dialogFuncGenerator} from "../../../common/util/helper";
import {apiMeeting} from "../../../common/request/request";
const formEle=ref()
const form=reactive({
id:"",
password:""
})
onDialogOk(dialogFuncGenerator({
form:()=>formEle.value,
func:()=>{
return apiMeeting.validateRoom({
meetingRoomId:form.id,
password:form.password
})
}
}))
</script>
<style scoped>
</style>

View File

@ -0,0 +1,155 @@
<template>
<a-row style="width: 100%;height: 100%;display: flex;align-items: center;justify-content: space-between;padding:0 5px;box-sizing: border-box">
<a-space>
<a-button size="large" type="text" v-if="me" @click="onToggleAudio">
<template #icon>
<sicon size="20" color="" name="Ant" type="audio" v-if="me.audio"></sicon>
<sicon size="20" color="gray" name="Ant" type="audio static" v-else></sicon>
</template>
</a-button>
<a-button size="large" type="text" v-if="me" @click="onToggleVideo">
<template #icon>
<sicon size="20" color="" name="Ant" type="video" v-if="me.video"></sicon>
<sicon size="20" color="gray" name="Ant" type="video-mute" v-else></sicon>
</template>
</a-button>
</a-space>
<a-popover>
<a-button size="large" type="text">
<template #icon>
<sicon size="20" color="" name="Ant" type="add user"></sicon>
</template>
</a-button>
<template #content>
<a-descriptions title="Meeting Info" :data="[
{
label:'Meeting Id',
value:currentMeeting.id
},
{
label:'Password',
value:currentMeeting.password
}
]" v-if="currentMeeting"></a-descriptions>
<a-divider :margin="10"></a-divider>
<a-form :model="form" auto-label-width>
<a-form-item field="userIds" label="Users" required>
<a-select multiple allow-clear allow-search v-model="form.userIds" @search="onSearch">
<a-option v-for="item in organizationUserList" :key="item.organizationUserId" :label="item.name" :value="item.organizationUserId"></a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button html-type="button" type="primary" @click="onInvite">Invite</a-button>
</a-form-item>
</a-form>
</template>
</a-popover>
<a-space>
<a-button size="mini" type="primary" @click="onLeave">
Leave
</a-button>
<a-button size="mini" type="primary" status="danger" v-if="me?.permission===ECommon_Meeting_Room_Permission.PRESENTER" @click="onEnd">
End
</a-button>
</a-space>
</a-row>
</template>
<script setup lang="ts">
import {ECommon_Meeting_Room_Permission, ICommon_Model_Meeting_Room} from "../../../../../../common/model/meeting_room";
import {OrganizationUserItem} from "./type";
import {apiOrganization, DCSType} from "../../../common/request/request";
import {Dialog} from "../../../common/component/dialog/dialog";
import {Message} from "@arco-design/web-vue";
import {getCurrentNavigator, getRootNavigatorRef} from "../../../../teamOS/common/component/navigator/navigator";
import {MeetingClient} from "../../../common/component/meeting/client";
import {getCurrentInstance, reactive, ref} from "vue";
import {SessionStorage} from "../../../common/storage/session";
import {SocketIOClient} from "../../../common/socket/socket";
import {ECommon_Socket_Type} from "../../../../../../common/socket/types";
const props=defineProps<{
me:OrganizationUserItem,
currentMeeting:DCSType<ICommon_Model_Meeting_Room>,
meetingClient:MeetingClient
}>()
const form=reactive({
userIds:[]
})
const organizationUserList=ref<{
organizationUserId:string,
name:string
}[]>([])
const navigator=getCurrentNavigator()
const root=getRootNavigatorRef()
const appContext=getCurrentInstance().appContext
const socket=SocketIOClient.getSocket(ECommon_Socket_Type.MEETING).getSocket()
const onToggleAudio=()=>{
if(props.me.audio) {
props.meetingClient.pause("audio")
props.me.audio=false
} else {
props.meetingClient.resume("audio")
props.me.audio=true
}
}
const onToggleVideo=()=>{
if(props.me.video) {
props.meetingClient.pause("video")
props.me.video=false
} else {
props.meetingClient.resume("video")
props.me.video=true
}
}
const onLeave=async ()=>{
let ret=await Dialog.confirm(root.value,appContext,"Do you leave this meeting?")
if(ret) {
await props.meetingClient.leave()
navigator.pop()
}
}
const onEnd=async ()=>{
let ret=await Dialog.confirm(root.value,appContext,"Do you end this meeting?")
if(ret) {
let res=await props.meetingClient.end()
if(res) {
navigator.pop()
} else {
Message.error("operation failed")
}
}
}
const onSearch=async (keyword:string)=>{
let res=await apiOrganization.listUser({
keyword,
page:0,
size:10,
organizationId:SessionStorage.get("organizationId")
})
if(res?.code==0) {
organizationUserList.value=res.data.data.map(item=>{
return {
name:item.organizationUser.nickname,
organizationUserId:item.organizationUser.id
}
})
}
}
const onInvite=async()=>{
if(form.userIds.length>0) {
socket.emit("meeting_invite",form.userIds)
form.userIds=[]
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,107 @@
<template>
<a-list :bordered="false">
<a-list-item v-for="item in organizationUserList">
<a-list-item-meta>
<template #title>
{{item.name}}
</template>
</a-list-item-meta>
<template #actions v-if="item.organizationUserId!==myOrganizationUserId">
<a-button size="mini" type="text" :disabled="me.permission!==ECommon_Meeting_Room_Permission.PRESENTER" @click="onToggleAudio(item)">
<template #icon>
<sicon size="15" color="rgb(22, 93, 255)" name="Ant" type="audio" v-if="item.audio"></sicon>
<sicon size="15" color="gray" name="Ant" type="audio static" v-else></sicon>
</template>
</a-button>
<a-button size="mini" type="text" :disabled="me.permission!==ECommon_Meeting_Room_Permission.PRESENTER" @click="onToggleVideo(item)">
<template #icon>
<sicon size="15" color="rgb(22, 93, 255)" name="Ant" type="video" v-if="item.video"></sicon>
<sicon size="15" color="gray" name="Ant" type="video-mute" v-else></sicon>
</template>
</a-button>
<a-button size="mini" type="text" v-if="me.permission===ECommon_Meeting_Room_Permission.PRESENTER">
<template #icon>
<sicon size="15" color="red" name="Ant" type="delete user" @click="onKick(item)"></sicon>
</template>
</a-button>
<a-button size="mini" type="text" v-if="me.permission===ECommon_Meeting_Room_Permission.PRESENTER" @click="onTogglePresenter(item)">
<template #icon>
<sicon size="15" color="rgb(22, 93, 255)" name="Ant" type="admin" v-if="item.permission===ECommon_Meeting_Room_Permission.PRESENTER" title="presenter"></sicon>
<sicon size="15" color="gray" name="Ant" type="user" v-else></sicon>
</template>
</a-button>
</template>
<template #actions v-else>
<a-button size="mini" type="text" :disabled="true">
<template #icon>
<sicon size="15" color="rgb(22, 93, 255)" name="Ant" type="admin" v-if="me.permission===ECommon_Meeting_Room_Permission.PRESENTER" title="presenter"></sicon>
<sicon size="15" color="gray" name="Ant" type="user" v-else></sicon>
</template>
</a-button>
</template>
</a-list-item>
</a-list>
</template>
<script setup lang="ts">
import {ECommon_Meeting_Room_Permission} from "../../../../../../common/model/meeting_room";
import {OrganizationUserItem} from "./type";
import {SessionStorage} from "../../../common/storage/session";
import {Dialog} from "../../../common/component/dialog/dialog";
import {getCurrentNavigator, getRootNavigatorRef} from "../../../../teamOS/common/component/navigator/navigator";
import {getCurrentInstance} from "vue";
import {MeetingClient} from "../../../common/component/meeting/client";
import {SocketIOClient} from "../../../common/socket/socket";
import {ECommon_Socket_Type} from "../../../../../../common/socket/types";
const props=defineProps<{
organizationUserList:OrganizationUserItem[],
me:OrganizationUserItem,
meetingClient:MeetingClient
}>()
const myOrganizationUserId=SessionStorage.get("organizationUserId")
const navigator=getCurrentNavigator()
const root=getRootNavigatorRef()
const appContext=getCurrentInstance().appContext
const socket=SocketIOClient.getSocket(ECommon_Socket_Type.MEETING).getSocket()
const onKick=async (item:OrganizationUserItem)=>{
let ret=await Dialog.confirm(root.value,appContext,`Do you want to kick ${item.name}?`)
if(ret) {
props.meetingClient.kick(item.organizationUserId)
}
}
const onTogglePresenter=async (item:OrganizationUserItem)=>{
let promote=item.permission===ECommon_Meeting_Room_Permission.NORMAL;
let ret=await Dialog.confirm(root.value,appContext,`Do you want to ${promote?`promote ${item.name} to presenter`:`demote ${item.name} to normal member`}?`)
if(ret) {
if(promote) {
let ret=await socket.emitWithAck("meeting_change_presenter",item.organizationUserId,ECommon_Meeting_Room_Permission.PRESENTER)
} else {
let ret=await socket.emitWithAck("meeting_change_presenter",item.organizationUserId,ECommon_Meeting_Room_Permission.NORMAL)
}
}
}
const onToggleAudio=async(item:OrganizationUserItem)=>{
if(item.audio) {
props.meetingClient.mute("audio",item.organizationUserId)
} else {
props.meetingClient.unmute("audio",item.organizationUserId)
}
}
const onToggleVideo=async(item:OrganizationUserItem)=>{
if(item.video) {
props.meetingClient.mute("video",item.organizationUserId)
} else {
props.meetingClient.unmute("video",item.organizationUserId)
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,271 @@
<template>
<a-layout style="height: 100%">
<a-layout-content>
<a-spin :loading="loading" style="height: 100%;width: 100%;">
<a-row style="height: 100%;width: 100%;flex-direction: column">
<a-row style="flex: 0 0 130px;width: 100%;display: flex;align-items: center;overflow-x: auto;overflow-y:hidden;background-color: dimgray" id="meetingPresent">
<div v-for="item in organizationUserList" :key="item.organizationUserId" style="flex: 0 0 250px;height: 100%;overflow: hidden;border: 1px solid black;background-color: black;position: relative">
<div style="position:absolute;bottom: 0;left: 0;color: white ">
{{item.name}}
</div>
<template v-if="item.organizationUserId===myOrganizationUserId">
<video autoplay muted :srcObject="item.videoStream" style="width: 100%;height: 100%" v-if="item.video"></video>
</template>
<template v-else>
<audio autoplay :srcObject="item.audioStream" v-if="item.audio"></audio>
<video autoplay :srcObject="item.videoStream" style="width: 100%;height: 100%" v-if="item.video"></video>
</template>
</div>
</a-row>
<a-row style="flex:1 1 calc(100% - 170px);width: 100%;overflow: hidden;display: flex;justify-content: center;background-color: dimgray;position: relative">
<div style="position:absolute;bottom: 0;left: 0;color: white;font-size: 20px">
{{speaker?.name}}
</div>
<template v-if="speaker?.organizationUserId===myOrganizationUserId">
<video autoplay muted :srcObject="speaker.videoStream" style="width: 100%;height: 100%" v-if="speaker.video"></video>
</template>
<template v-else-if="speaker">
<audio autoplay :srcObject="speaker.audioStream" v-if="speaker.audio"></audio>
<video autoplay :srcObject="speaker.videoStream" style="width: 100%;height: 100%" v-if="speaker.video"></video>
</template>
</a-row>
<a-row style="flex: 0 0 40px;width: 100%">
<MeetingOperation :current-meeting="currentMeeting" :me="me" :meeting-client="meetingClient"></MeetingOperation>
</a-row>
</a-row>
</a-spin>
</a-layout-content>
<a-layout-sider :resize-directions="['left']" :width="250" id="meetingRight">
<a-tabs size="mini">
<a-tab-pane title="participant">
<MeetingParticipant :organization-user-list="organizationUserList" :me="me" :meeting-client="meetingClient"></MeetingParticipant>
</a-tab-pane>
</a-tabs>
</a-layout-sider>
</a-layout>
</template>
<script setup lang="ts">
import {SocketIOClient} from "../../../common/socket/socket";
import {ECommon_Socket_Type} from "../../../../../../common/socket/types";
import {getCurrentInstance, onBeforeMount, onBeforeUnmount, ref} from "vue";
import {MeetingClient} from "../../../common/component/meeting/client";
import {ECommon_Meeting_Room_Permission, ICommon_Model_Meeting_Room} from "../../../../../../common/model/meeting_room";
import {SessionStorage} from "../../../common/storage/session";
import {userTeamInfoPick} from "../../../common/component/userInfoPick";
import {ECommon_IM_Message_EntityType} from "../../../../../../common/model/im_unread_message";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
import {getCurrentNavigator, getRootNavigatorRef} from "../../../../teamOS/common/component/navigator/navigator";
import {Message} from "@arco-design/web-vue";
import {apiMeeting, DCSType} from "../../../common/request/request";
import {OrganizationUserItem} from "./type";
import MeetingParticipant from "./meetingParticipant.vue";
import MeetingOperation from "./meetingOperation.vue";
const props=defineProps<{
meetingId:string,
password:string
}>()
const loading=ref(true)
const organizationUserList=ref<OrganizationUserItem[]>([])
const speaker=ref<OrganizationUserItem>()
const myOrganizationUserId=SessionStorage.get("organizationUserId")
const me=ref<OrganizationUserItem>({
organizationUserId:myOrganizationUserId,
name:"",
permission:ECommon_Meeting_Room_Permission.NORMAL,
audioStream:null,
videoStream:null,
video:true,
audio:true
})
const socket=SocketIOClient.get(ECommon_Socket_Type.MEETING)
const navigator=getCurrentNavigator()
const root=getRootNavigatorRef()
const appContext=getCurrentInstance().appContext
const currentMeeting=ref<DCSType<ICommon_Model_Meeting_Room>>()
let meetingClient=new MeetingClient(socket.getSocket())
meetingClient.onProducerStateChange=async (state, kind, businessId, stream, producerId) => {
let objOrganizationUser=organizationUserList.value.find(value => value.organizationUserId===businessId)
if(state=="new") {
if(!objOrganizationUser) {
let obj=userTeamInfoPick.getInfos([{
id:businessId,
type:ECommon_IM_Message_EntityType.USER
}])
organizationUserList.value.push({
organizationUserId:businessId,
name:obj[businessId]?obj[businessId].name:"",
permission:ECommon_Meeting_Room_Permission.NORMAL,
audioStream:kind==="audio"?stream:null,
videoStream:kind==="video"?stream:null,
audio:kind==="audio"?true:false,
video:kind==="video"?true:false,
})
} else {
if(kind=="video") {
objOrganizationUser.videoStream=stream
objOrganizationUser.video=true
} else if(kind=="audio") {
objOrganizationUser.audioStream=stream
objOrganizationUser.audio=true
}
}
} else if(state=="close") {
if(objOrganizationUser) {
let index=organizationUserList.value.findIndex(value => value.organizationUserId===businessId)
organizationUserList.value.splice(index,1)
if(objOrganizationUser===speaker.value) {
speaker.value=null
}
}
} else if(state=="pause") {
if(objOrganizationUser) {
if(kind=="video") {
objOrganizationUser.video=false
} else if(kind=="audio") {
objOrganizationUser.audio=false
}
}
} else if(state=="resume") {
if(objOrganizationUser) {
if(kind=="video") {
objOrganizationUser.video=true
} else if(kind=="audio") {
objOrganizationUser.audio=true
}
}
}
handleState()
}
meetingClient.onKick=() => {
navigator.pop()
}
meetingClient.onJoinedRoom=async roomInfo => {
getCurrentMeeting()
}
meetingClient.onLeavedRoom=roomInfo => {
}
meetingClient.onSpeaker=async businessId => {
speaker.value=organizationUserList.value.find(item=>item.organizationUserId===businessId)
}
meetingClient.onLocalProducerInit=async stream => {
let obj=userTeamInfoPick.getInfos([{
id:myOrganizationUserId,
type:ECommon_IM_Message_EntityType.USER
}])
me.value={
organizationUserId:myOrganizationUserId,
name:obj[myOrganizationUserId]?obj[myOrganizationUserId].name:"",
permission:ECommon_Meeting_Room_Permission.NORMAL,
audioStream:stream,
videoStream:stream,
video:true,
audio:true
}
organizationUserList.value.push(me.value)
}
meetingClient.onLocalProducerStart=kind => {
if(kind=="video") {
handleState()
loading.value=false
}
}
const initMeeting=async ()=>{
let ret=await meetingClient.join(props.meetingId,props.password)
if(!ret?.success) {
Message.error(ret.msg)
navigator.pop()
}
}
const handleState=async ()=>{
let [retState,retPermission]=await Promise.all([
meetingClient.states(),
socket.getSocket().emitWithAck("meeting_get_presenters")
])
for(let obj of organizationUserList.value) {
if(retPermission[obj.organizationUserId]) {
obj.permission=retPermission[obj.organizationUserId]
}
}
for(let objState of retState) {
for(let objOrganizationUser of organizationUserList.value) {
if (objState.businessId===objOrganizationUser.organizationUserId) {
objOrganizationUser.video=objState.kinds["video"]
objOrganizationUser.audio=objState.kinds["audio"]
}
}
}
}
const handleUserInfo = (id: string, info: {
id: string,
name: string,
photo: string
}) => {
for(let obj of organizationUserList.value) {
if(obj.organizationUserId==id) {
obj.name=info.name;
}
}
}
const onPresenterChange=async (organizationUserId, permission) => {
let obj=organizationUserList.value.find(item=>item.organizationUserId===organizationUserId)
if(obj) {
obj.permission=permission
}
}
const getCurrentMeeting=async ()=>{
let res=await apiMeeting.getCurrentRoom()
if(res?.code==0) {
currentMeeting.value=res.data
}
}
const handleLeaveMeeting=async ()=>{
await meetingClient.leave()
navigator.pop()
}
onBeforeMount(()=>{
eventBus.on(EClient_EVENTBUS_TYPE.UPDATE_USER_INFO, handleUserInfo)
eventBus.on(EClient_EVENTBUS_TYPE.LEAVE_MEETING, handleLeaveMeeting)
socket.getSocket().on("meeting_presenter_change", onPresenterChange)
initMeeting()
})
onBeforeUnmount(()=>{
eventBus.off(EClient_EVENTBUS_TYPE.UPDATE_USER_INFO, handleUserInfo)
eventBus.off(EClient_EVENTBUS_TYPE.LEAVE_MEETING, handleLeaveMeeting)
socket.getSocket().off("meeting_presenter_change", onPresenterChange)
if(meetingClient.getRoomInfo()) {
meetingClient.leave()
}
})
</script>
<style scoped>
#meetingPresent :first-child {
margin-left: auto;
}
#meetingPresent :last-child {
margin-right: auto;
}
#meetingRight :deep .arco-tabs-content {
padding-top: 0px;
}
#meetingRight :deep .arco-list-item {
padding: 5px 10px!important;
}
#meetingRight :deep .arco-list-item .arco-list-item-action > li:not(:last-child) {
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<a-form :model="form" auto-label-width ref="formEle">
<a-form-item field="id" label="meeting id">
{{form.id}}
</a-form-item>
<a-form-item field="password" label="meeting password" required>
<a-input v-model="form.password"></a-input>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {onBeforeMount, reactive, ref} from "vue";
import {apiMeeting} from "../../../common/request/request";
import {onDialogOk} from "../../../common/component/dialog/dialog";
import {dialogFuncGenerator} from "../../../common/util/helper";
const formEle=ref()
const form=reactive({
id:"",
password:""
})
const getPersonalMeeting=async ()=>{
let res=await apiMeeting.getPersonalRoom()
if(res?.code==0) {
form.id=res.data.id
form.password=res.data.password
}
}
onBeforeMount(()=>{
getPersonalMeeting()
})
onDialogOk(dialogFuncGenerator({
form:()=>formEle.value,
func:()=>apiMeeting.editRoom({
meetingRoomId:form.id,
password:form.password
})
}))
</script>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
import {ECommon_Meeting_Room_Permission} from "../../../../../../common/model/meeting_room";
export type OrganizationUserItem = {
organizationUserId:string,
name:string,
permission:ECommon_Meeting_Room_Permission,
videoStream:MediaStream,
audioStream:MediaStream,
video:boolean,
audio:boolean
}

View File

@ -3,7 +3,13 @@
<a-layout-sider :resize-directions="['right']">
<a-row style="flex-direction: column;align-items: center">
<a-avatar :size="72" :image-url="info.user?.photo">{{imgName}}</a-avatar>
<h3 style="margin-top: 10px;">{{info.organizationUser?.nickname}}</h3>
<a-space>
<h3>{{info.organizationUser?.nickname}}</h3>
<span style="height: 12px;width: 12px;background-color: #03ad03;border-radius: 6px;vertical-align: middle" v-if="status===ECommon_User_Online_Status.ONLINE"></span>
<icon-stop :stroke-width="5" style="color: darkred" v-else-if="status===ECommon_User_Online_Status.BUSY"></icon-stop>
<icon-video-camera :stroke-width="5" style="color: red" v-else-if="status===ECommon_User_Online_Status.MEETING"></icon-video-camera>
<span style="height: 12px;width: 12px;background-color: gray;border-radius: 6px;vertical-align: middle" v-else-if="status===ECommon_User_Online_Status.OFFLINE"></span>
</a-space>
<div style="font-size: 13px;color: #6b778c;">{{info.user?.sign}}</div>
<a-list style="margin-top: 30px;width: 80%" v-if="info.organizationUser?.job || info.organizationUser?.department || info.organizationUser?.phone || info.organizationUser?.email || info.organizationUser?.location">
<a-list-item v-if="info.organizationUser?.job">
@ -71,7 +77,18 @@
</template>
</a-list-item-meta>
<template #actions>
<a-button type="outline" size="small" @click="onTeamProfile(item.id)">Profile</a-button>
<template v-if="organizationUserId===myOrganizationUserId">
<a-button type="outline" size="small" @click="onTeamProfile(item.id)">Profile</a-button>
</template>
<template v-else>
<a-button type="outline" size="small" style="margin-left: 20px" @click="onTeamProfile(item.id)">Profile</a-button>
<a-button type="outline" size="small" style="margin-left: 20px;margin-top: 10px" @click="onMessage">
<template #icon>
<icon-message></icon-message>
</template>
Message
</a-button>
</template>
</template>
</a-list-item>
</a-list>
@ -85,6 +102,9 @@ import {apiOrganization, DCSType} from "../../../common/request/request";
import {ICommon_Route_Res_Organization_User_Profile} from "../../../../../../common/routes/response";
import {getCurrentNavigator} from "../../../../teamOS/common/component/navigator/navigator";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
import {SessionStorage} from "../../../common/storage/session";
import {ECommon_IM_Message_EntityType} from "../../../../../../common/model/im_unread_message";
import {ECommon_User_Online_Status} from "../../../../../../common/types";
const props=defineProps<{
organizationUserId:string
@ -95,17 +115,23 @@ const info=ref<DCSType<ICommon_Route_Res_Organization_User_Profile>>({
organizationUser:null
});
const navigator=getCurrentNavigator()
const myOrganizationUserId=SessionStorage.get("organizationUserId")
const status=ref(ECommon_User_Online_Status.OFFLINE)
onBeforeMount(async ()=>{
let res=await apiOrganization.userProfile({
apiOrganization.userProfile({
organizationUserId:props.organizationUserId
})
if(res?.code==0) {
}).then(res=>{
info.value=res.data
let title=navigator.getPath()[navigator.getIndex().value]
if(title=="profile") {
navigator.getPath()[navigator.getIndex().value]=res.data.organizationUser.nickname
}
}
})
apiOrganization.getUserStatus({
organizationUserId:props.organizationUserId
}).then(res=>{
status.value=res.data.status
})
})
const imgName=computed(()=>{
if(info.value.organizationUser?.nickname.includes(" ")) {
@ -118,6 +144,9 @@ const imgName=computed(()=>{
const onTeamProfile=(teamId:string)=>{
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_TEAM_PROFILE,teamId)
}
const onMessage=()=>{
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,props.organizationUserId,ECommon_IM_Message_EntityType.USER)
}
</script>
<style scoped>

View File

@ -0,0 +1,104 @@
<template>
<div>
<a-form :model="form" ref="eleForm" style="width: 80%">
<a-form-item field="username" label="username" required>
<a-input v-model="form.username"></a-input>
</a-form-item>
<a-form-item field="password" label="password" required>
<a-input-password v-model="form.password"></a-input-password>
</a-form-item>
<a-form-item field="nickname" label="nickname" required>
<a-input v-model="form.nickname"></a-input>
</a-form-item>
<a-form-item field="job" label="job">
<a-input v-model="form.job"></a-input>
</a-form-item>
<a-form-item field="email" label="email">
<a-input v-model="form.email"></a-input>
</a-form-item>
<a-form-item field="location" label="location">
<a-input v-model="form.location"></a-input>
</a-form-item>
<a-form-item field="phone" label="phone">
<a-input v-model="form.phone"></a-input>
</a-form-item>
<a-form-item field="department" label="department">
<a-input v-model="form.department"></a-input>
</a-form-item>
<a-form-item field="active" label="active">
<a-switch v-model="form.active" :checked-value="1" :unchecked-value="0"></a-switch>
</a-form-item>
<a-form-item field="remark" label="remark">
<a-textarea v-model="form.remark" allow-clear></a-textarea>
</a-form-item>
<a-form-item field="roleId" label="role" required>
<a-select placeholder="select role" v-model="form.roleId">
<a-option v-for="item in roles" :value="item.id" :label="item.name"></a-option>
</a-select>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import {onBeforeMount, reactive, ref} from "vue";
import {onDialogOk} from "../../../../common/component/dialog/dialog";
import {apiOrganization} from "../../../../common/request/request";
import {Message} from "@arco-design/web-vue";
import {SessionStorage} from "../../../../common/storage/session";
import md5 from "blueimp-md5";
const eleForm=ref(null)
const form=reactive({
nickname:"",
job:"",
email:"",
location:"",
phone:"",
department:"",
remark:"",
active:1,
roleId:"",
username:"",
password:""
})
const roles=ref<{
id:string,
name:string
}[]>([])
onBeforeMount(async ()=>{
let res=await apiOrganization.listRole({
organizationId:SessionStorage.get("organizationId")
})
if(res?.code==0) {
roles.value=[res.data.admin,...res.data.users].map(item=>{
return {
id:item.id,
name:item.name
}
})
}
})
onDialogOk(async ()=>{
let ret=await eleForm.value.validate()
if(ret) {
return false;
}
let res=await apiOrganization.createUser({
organizationId:SessionStorage.get("organizationId"),
...form,
password:md5(form.password)
})
if(res?.code==0) {
Message.success("operation success")
return true
} else {
Message.error(res.msg)
return false
}
})
</script>
<style scoped>
</style>

View File

@ -2,7 +2,8 @@
<div ref="root">
<a-space>
<a-input-search @search="onSearch" v-model="keyword" style="width: 300px" search-button placeholder="please type username or nickname"></a-input-search>
<a-button type="primary" @click="onInvite">Invite</a-button>
<a-button type="primary" @click="onInvite" v-if="$deployMode.value===ECommon_Application_Mode.ONLINE">Invite</a-button>
<a-button type="primary" @click="onCreate" v-else>Create</a-button>
</a-space>
<a-table style="margin-top: 10px" :columns="columns" :data="data" :pagination="pagination" @pageChange="onPageChange">
<template #username="{record}">
@ -36,6 +37,7 @@
<template #content>
<a-doption @click="onEditProfile(record)">Profile & Role</a-doption>
<a-doption @click="onEditTag(record)">Tag</a-doption>
<a-doption v-if="$deployMode.value===ECommon_Application_Mode.OFFLINE" @click="onResetPassword(record)">Reset Password</a-doption>
</template>
</a-dropdown-button>
<a-button status="danger" size="small" @click="onDelete(record)">Remove</a-button>
@ -50,11 +52,13 @@ import {getCurrentInstance, markRaw, reactive, ref} from "vue";
import {apiOrganization, DCSType} from "../../../../common/request/request";
import {ICommon_Route_Res_Organization_User_Item} from "../../../../../../../common/routes/response";
import {Message} from "@arco-design/web-vue";
import moment from "moment";
import {Dialog} from "../../../../common/component/dialog/dialog";
import EditUserProfile from "./editUserProfile.vue";
import BindTag from "./bindTag.vue";
import {SessionStorage} from "../../../../common/storage/session";
import {ECommon_Application_Mode} from "../../../../../../../common/types";
import CreateUserProfileOffline from "./createUserProfileOffline.vue";
import moment from "moment";
const columns=[
{
@ -147,15 +151,37 @@ const onEditTag=async (item:DCSType<ICommon_Route_Res_Organization_User_Item>)=>
const onDelete=async (item:DCSType<ICommon_Route_Res_Organization_User_Item>)=>{
let ret=await Dialog.confirm(root.value,appContext,"Do you want to remove this user from this organization?")
if(ret) {
let res=await apiOrganization.deleteUser({
let res=await (appContext.config.globalProperties.$deployMode.value===ECommon_Application_Mode.ONLINE?apiOrganization.deleteUser({
organizationUserId:item.organizationUser.id
})
}):apiOrganization.deleteUserForOffline({
organizationUserId:item.organizationUser.id
}))
if(res?.code==0) {
Message.success("remove success")
search(pagination.current)
}
}
}
const onCreate=async ()=>{
let ret=await Dialog.open(root.value,appContext,"Create",markRaw(CreateUserProfileOffline))
if(ret) {
search(pagination.current)
}
}
const onResetPassword=async (item:DCSType<ICommon_Route_Res_Organization_User_Item>)=>{
let ret=await Dialog.input(root.value,appContext,"type new password")
if(ret) {
let res=await apiOrganization.resetUserPassword({
organizationUserId:item.organizationUser.id,
password:ret
})
if(res?.code==0) {
Message.info("reset success")
}
}
}
</script>
<style scoped>

View File

@ -20,6 +20,12 @@
<a-space size="large">
<a-button type="primary" html-type="submit">Save</a-button>
<a-button status="danger" html-type="button" v-if="checkPermission(permission,Permission_Types.Team.DELETE)" @click="onDelete">Delete</a-button>
<a-button type="outline" size="small" html-type="button" @click="onMessage">
<template #icon>
<icon-message></icon-message>
</template>
Message
</a-button>
</a-space>
</a-form-item>
</a-form>
@ -29,7 +35,13 @@
<a-list-item>
<a-list-item-meta>
<template #title>
{{basic.name}}
{{basic.name}}&nbsp;&nbsp;
<a-button type="outline" size="mini" html-type="button" @click="onMessage">
<template #icon>
<icon-message></icon-message>
</template>
Message
</a-button>
</template>
<template #description>
<a-space direction="vertical">
@ -107,6 +119,7 @@ import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
import EditTeamMemberRole from "../setting/user&team/editTeamMemberRole.vue";
import EditTeamRole from "../setting/role/team/editTeamRole.vue";
import {checkPermission} from "../../../common/util/helper";
import {ECommon_IM_Message_EntityType} from "../../../../../../common/model/im_unread_message";
const props=defineProps<{
teamId:string
@ -354,6 +367,10 @@ const onAddMember=async ()=>{
listUser(1);
}
}
const onMessage=()=>{
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,props.teamId,ECommon_IM_Message_EntityType.TEAM)
}
</script>
<style scoped>

View File

@ -0,0 +1,50 @@
<template>
<a-form :model="form" style="width: 80%;margin-top: 20px" ref="formEle">
<a-form-item field="name" label="name" required>
<a-input v-model="form.name"></a-input>
</a-form-item>
<a-form-item field="description" label="description">
<a-textarea v-model="form.description" allow-clear></a-textarea>
</a-form-item>
<a-form-item field="photo" label="logo">
<Upload types=".png,.jpg,.jpeg,.gif,.bmp,.svg" :default-uri="form.photo" @upload="onUpload"></Upload>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {reactive, ref} from "vue";
import {apiOrganization} from "../../common/request/request";
import {onDialogOk} from "../../common/component/dialog/dialog";
import {dialogFuncGenerator} from "../../common/util/helper";
import Upload from "../../common/component/upload.vue";
const formEle=ref()
const form=reactive({
name:"",
description:"",
photo:""
})
const uploadUriId=ref("")
const onUpload=(id:string)=> {
uploadUriId.value=id
}
onDialogOk(dialogFuncGenerator({
form:()=>formEle.value,
func:()=>{
return apiOrganization.create({
name:form.name,
description: form.description,
...(uploadUriId.value && {
photo:uploadUriId.value
})
})
}
}))
</script>
<style scoped>
</style>

View File

@ -1,5 +1,71 @@
<template>
<TeamOS/>
<TeamOS>
<template #topLeft>
<a-dropdown trigger="hover">
<a-avatar :size="36" :image-url="avatar" :trigger-icon-style="{height:'12px',width:'12px',lineHeight:'12px',right:'-2px',bottom:'-2px',...(store.status===ECommon_User_Online_Status.MEETING && {backgroundColor:'transparent'})}">
T
<template #trigger-icon>
<div style="height: 100%;width: 100%;background-color: #03ad03;border-radius: 6px" v-if="store.status===ECommon_User_Online_Status.ONLINE"></div>
<icon-stop :stroke-width="5" style="color: darkred" v-else-if="store.status===ECommon_User_Online_Status.BUSY"></icon-stop>
<icon-video-camera :stroke-width="5" style="color: red" v-else-if="store.status===ECommon_User_Online_Status.MEETING"></icon-video-camera>
<div style="height: 100%;width: 100%;background-color: gray;border-radius: 6px" v-else-if="store.status===ECommon_User_Online_Status.OFFLINE"></div>
</template>
</a-avatar>
<template #content>
<a-dsubmenu trigger="hover" :disabled="store.status===ECommon_User_Online_Status.MEETING">
Status
<template #content>
<a-doption @click="onChangeStatus(ECommon_User_Online_Status.ONLINE)">
<template #icon v-if="store.status===ECommon_User_Online_Status.ONLINE">
<icon-check></icon-check>
</template>
online
</a-doption>
<a-doption @click="onChangeStatus(ECommon_User_Online_Status.BUSY)">
<template #icon v-if="store.status===ECommon_User_Online_Status.BUSY">
<icon-check></icon-check>
</template>
busy
</a-doption>
</template>
</a-dsubmenu>
<a-doption @click="onAccount">Account</a-doption>
<a-doption @click="onLogout">Logout</a-doption>
</template>
</a-dropdown>
</template>
<template #topRight>
<a-row style="height: 100%;" align="center">
<a-col flex="80px" style="align-items: center;display: flex">
<img :src="store.organizationInfo.photo" style="height: 36px;width:80px;object-fit: cover;object-position: 50% 50%;" v-if="store.organizationInfo?.photo"/>
</a-col>
<a-col flex="170px">
<a-select style="max-width: 170px;color: white" placeholder="organization" :bordered="false" @change="onMenuClick" v-model:model-value="menuValue" v-if="menu.length>0">
<a-optgroup v-for="(item,index) in menu" :label="item.group">
<a-option v-for="(item1,index1) in item.data" :label="item1.name" :value="item1.value"></a-option>
</a-optgroup>
<template #footer v-if="$deployMode.value===ECommon_Application_Mode.ONLINE">
<a-row style="width: 100%;height: 30px;display: flex;justify-content: center;align-items: center">
<a-button type="text" style="width: 100%;color: black" @click="onCreate">
Create
</a-button>
</a-row>
</template>
</a-select>
<a-button type="primary" status="success" style="float: right;margin-right: 5px" @click="onCreate" v-else>
Create Organization
</a-button>
</a-col>
</a-row>
</template>
</TeamOS>
<a-skeleton :animation="true" v-if="loading" style="position: absolute;left: 0;top: 0;height: 100%;width: 100%;background-color: white;z-index: 10000">
<a-space direction="vertical" :style="{width:'100%'}" size="large">
<a-skeleton-line :rows="3" />
<a-skeleton-shape />
<a-skeleton-shape />
</a-space>
</a-skeleton>
</template>
<script setup lang="ts">
@ -7,7 +73,7 @@
import TeamOS from "../../../teamOS/index.vue";
import img from "../../../assert/back.png"
import {getDesktopInstance} from "../../../teamOS/teamOS";
import {getCurrentInstance, markRaw, nextTick, onBeforeMount, watchEffect} from "vue";
import {getCurrentInstance, markRaw, nextTick, onBeforeMount, onBeforeUnmount, ref, watchEffect} from "vue";
import {useDesktopStore} from "./store/desktop";
import {useRouter} from "vue-router";
import {Message} from "@arco-design/web-vue";
@ -16,64 +82,23 @@ import {ETeamOS_Window_Type, Window} from "../../../teamOS/window/window";
import {v4} from "uuid";
import Account from "../app/account/account.vue";
import {SessionStorage} from "../../common/storage/session";
import {ECommon_Application_Mode, ECommon_User_Online_Status} from "../../../../../common/types";
import {apiOrganization} from "../../common/request/request";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../common/event/event";
import {ITeamOS_Desktop_Menu} from "../../../teamOS/desktop/desktop";
import CreateOrganization from "./createOrganization.vue";
let desktop=getDesktopInstance().desktop;
const menu=ref<ITeamOS_Desktop_Menu[]>([])
const menuValue=ref("")
desktop.setBackgroundImage(img)
const store=useDesktopStore()
const router=useRouter();
let iconManager=getDesktopInstance().iconManager
let windowManager=getDesktopInstance().windowManager
const appContext=getCurrentInstance().appContext
onBeforeMount(async ()=>{
let isAuth=await store.isAuth();
if(!isAuth) {
await router.replace("login");
return
}
await store.getOrganizationList()
if(SessionStorage.get("organizationId")) {
await store.enterOrganization(SessionStorage.get("organizationId"))
}
desktop.addEventListener("menuClick",async value => {
await store.enterOrganization(value);
})
desktop.setAvatarOptions([{
name:"Account",
clickFunc:()=>{
const win=new Window("account",ETeamOS_Window_Type.SIMPLE, "account",true,[
{
id:v4(),
meta:{
title:"account"
},
components:{
account:markRaw(Account)
},
default:{
name:"account"
}
}
],"account");
windowManager.open(win);
}
},{
name:"Logout",
clickFunc:async ()=>{
let ret=await Dialog.confirm(document.body,appContext,"Do you want to log out?")
if(ret) {
let res=await store.logout();
if(res) {
Message.success("logout success")
watchStop();
desktop.clear()
iconManager.clear()
windowManager.clear();
await router.replace("login");
}
}
}
}])
})
const loading=ref(true)
let avatar=ref("")
const watchStop=watchEffect(()=>{
if(store.organizationList) {
let arr=[]
@ -99,16 +124,13 @@ const watchStop=watchEffect(()=>{
})
})
}
if(store.organizationInfo?.photo) {
desktop.setLogo(store.organizationInfo.photo)
}
if(store.userInfo?.photo) {
desktop.setAvatar(store.userInfo.photo)
avatar.value=store.userInfo?.photo
}
if(SessionStorage.get("organizationId")) {
desktop.setMenu(arr,SessionStorage.get("organizationId"))
setMenu(arr,SessionStorage.get("organizationId"))
} else {
desktop.setMenu(arr)
setMenu(arr)
}
}
iconManager.setList(store.appList)
@ -117,6 +139,91 @@ const watchStop=watchEffect(()=>{
})
})
const setMenu=(menu1:ITeamOS_Desktop_Menu[],defaultValue?:string) => {
if(menu.value.length>0) {
menu.value.splice(0)
}
menu.value.push(...menu1);
if(defaultValue) {
menuValue.value=defaultValue
}
}
const onAccount=()=>{
const win=new Window("account",ETeamOS_Window_Type.SIMPLE, "account",true,[
{
id:v4(),
meta:{
title:"account"
},
components:{
account:markRaw(Account)
},
default:{
name:"account"
}
}
],"account");
windowManager.open(win);
}
const onLogout=async ()=>{
let ret=await Dialog.confirm(document.body,appContext,"Do you want to log out?")
if(ret) {
let res=await store.logout();
if(res) {
Message.success("logout success")
watchStop();
desktop.clear()
iconManager.clear()
windowManager.clear();
await router.replace("login");
}
}
}
const onChangeStatus=(status:ECommon_User_Online_Status.BUSY|ECommon_User_Online_Status.ONLINE)=> {
apiOrganization.changeUserStatus({
status:status
})
}
const onHandleStatus=(organizationUserId:string,status:ECommon_User_Online_Status)=> {
if(organizationUserId===SessionStorage.get("organizationUserId")) {
store.status=status
}
}
const onMenuClick=async (value)=>{
windowManager.clear()
await store.enterOrganization(value);
}
const onCreate=async ()=>{
let ret=await Dialog.open(document.body,appContext,"Create Organization",markRaw(CreateOrganization))
if(ret) {
store.$update()
}
}
onBeforeMount(async ()=>{
let isAuth=await store.isAuth();
if(!isAuth) {
await router.replace("login");
return
}
await store.getOrganizationList()
if(SessionStorage.get("organizationId")) {
await store.initOrganization(SessionStorage.get("organizationId"))
}
eventBus.on(EClient_EVENTBUS_TYPE.UPDATE_ORGANIZATION_USER_STATUS,onHandleStatus)
loading.value=false
})
onBeforeUnmount(()=>{
eventBus.off(EClient_EVENTBUS_TYPE.UPDATE_ORGANIZATION_USER_STATUS,onHandleStatus)
})
</script>
<style scoped>

View File

@ -6,6 +6,7 @@ import {windowManager} from "../../../../teamOS/window/windowManager";
import {markRaw} from "vue";
import Im from "../../../controller/app/im/im.vue";
import {SessionStorage} from "../../../common/storage/session";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
export const iconIM=new Icon("im",iconGroupMap["im"])
iconIM.addEventListener("dbClick",item => {
@ -28,4 +29,25 @@ iconIM.addEventListener("dbClick",item => {
}
],"im");
windowManager.open(win);
})
eventBus.on(EClient_EVENTBUS_TYPE.OPEN_IM_CHAT,(id, chatType) => {
const win=new Window("im",ETeamOS_Window_Type.SIMPLE, "im",false,[
{
id:v4(),
meta:{
title:"im"
},
components:{
im:markRaw(Im)
},
default:{
name:"im",
props:{
id:id,
chatType:chatType
}
}
}
],"im");
windowManager.open(win);
})

View File

@ -1,11 +1,13 @@
import {Icon, iconGroupMap} from "../../../../teamOS/icon/icon";
import {v4} from "uuid"
import {ETeamOS_Window_Type, Window} from "../../../../teamOS/window/window";
import Setting from "../../app/setting/setting.vue";
import {Message} from "@arco-design/web-vue";
import {windowManager} from "../../../../teamOS/window/windowManager";
import {markRaw} from "vue";
import {SessionStorage} from "../../../common/storage/session";
import Meeting from "../../app/meeting/meeting.vue";
import MeetingProfile from "../../app/meeting/meetingProfile.vue";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
export const iconMeeting=new Icon("meeting",iconGroupMap["meeting"])
iconMeeting.addEventListener("dbClick",item => {
@ -13,17 +15,42 @@ iconMeeting.addEventListener("dbClick",item => {
Message.error("you must choose organization")
return;
}
const win=new Window("meeting",ETeamOS_Window_Type.SIMPLE, "meeting",true,[
const win=new Window("meeting",ETeamOS_Window_Type.SIMPLE, "meeting",false,[
{
id:v4(),
meta:{
title:"meeting"
},
components:{
setting:markRaw(Setting)
meeting:markRaw(Meeting),
meetingProfile:markRaw(MeetingProfile)
},
default:{
name:"setting"
name:"meeting"
}
}
],"meeting");
windowManager.open(win);
})
eventBus.on(EClient_EVENTBUS_TYPE.OPEN_MEETING,(meetingId, password) => {
const win=new Window("meeting",ETeamOS_Window_Type.SIMPLE, "meeting",false,[
{
id:v4(),
meta:{
title:"meeting"
},
components:{
meeting:markRaw(Meeting),
meetingProfile:markRaw(MeetingProfile)
},
default:{
name:"meeting",
props:{
meetingInitInfo:{
id:meetingId,
password
}
}
}
}
],"meeting");

View File

@ -16,9 +16,9 @@ import {SocketIOClient} from "../../../common/socket/socket";
import {ECommon_Socket_Type} from "../../../../../../common/socket/types";
import {NotificationType, NotificationWrapper} from "../../../common/component/notification/notification";
import {SessionStorage} from "../../../common/storage/session";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
import {ECommon_IM_Message_EntityType} from "../../../../../../common/model/im_unread_message";
import {ECommon_IM_Message_ContentType} from "../../../../../../common/model/im_user_message";
import {handleIMEvent} from "../../app/im/event";
import {ECommon_User_Online_Status} from "../../../../../../common/types";
import {handleMeetingEvent} from "../../app/meeting/event";
export const useDesktopStore=defineStore("desktop",{
state:()=>({
@ -35,7 +35,9 @@ export const useDesktopStore=defineStore("desktop",{
iconIM,
iconTeam
] as Icon[],
userInfo:{} as DCSType<Omit<ICommon_Model_User,"password">>
userInfo:{} as DCSType<Omit<ICommon_Model_User,"password">>,
status:ECommon_User_Online_Status.OFFLINE,
heartbeatInterval:null
}),
actions:{
async getOrganizationList() {
@ -44,53 +46,62 @@ export const useDesktopStore=defineStore("desktop",{
this.organizationList=ret.data;
}
},
async requestStatus() {
let ret=await apiOrganization.getUserStatus({})
if(ret?.code==0) {
this.status=ret.data.status
}
},
async enterOrganization (organizationId:string){
let retEnter=await apiOrganization.enter({
organizationId
});
if(retEnter?.code==0) {
let myOrganizationUserId=retEnter.data.organizationUserId
SessionStorage.set("organizationUserId",retEnter.data.organizationUserId)
SocketIOClient.getSocket(ECommon_Socket_Type.CALENDAR)?.close()
await Promise.all([
(async ()=>{
let retOrganization=await apiOrganization.info({
organizationId
})
if(retOrganization?.code==0) {
this.organizationInfo=retOrganization.data
SessionStorage.set("organizationId",retOrganization.data.id);
}
})(),
(async ()=>{
let ret=await apiOrganization.getPermission({
organizationId
})
if(ret?.code==0) {
this.organizationPermission=ret.data.value
}
})()
])
let objSocketCalendar=SocketIOClient.create(ECommon_Socket_Type.CALENDAR)
objSocketCalendar.getSocket().on("calendar_event_reminder",(id, name, start_time) => {
NotificationWrapper.show("Calendar Event Reminder",`${name} will start in ${Math.floor((start_time-Date.now())/1000/60)} minutes`,NotificationType.CALENDAR,null,null,60000)
})
SocketIOClient.getSocket(ECommon_Socket_Type.IM)?.close()
let objSocketIM=SocketIOClient.create(ECommon_Socket_Type.IM)
objSocketIM.getSocket().on("im_user_relay_text_message",(fromOrganizationUserId, toOrganizationUserId, content,date) => {
if(myOrganizationUserId!==fromOrganizationUserId) {
NotificationWrapper.show("IM User Message",content.substring(0,20),NotificationType.IM,null,null,3000)
}
eventBus.emit(EClient_EVENTBUS_TYPE.RECEIVE_IM_MESSAGE,ECommon_IM_Message_EntityType.USER,fromOrganizationUserId,content,ECommon_IM_Message_ContentType.TEXT,date,toOrganizationUserId)
})
objSocketIM.getSocket().on("im_team_relay_text_message",(organizationUserId, teamId, content,date) => {
if(myOrganizationUserId!==organizationUserId) {
NotificationWrapper.show("IM Team Message",content.substring(0,20),NotificationType.IM,null,null,3000)
}
eventBus.emit(EClient_EVENTBUS_TYPE.RECEIVE_IM_MESSAGE,ECommon_IM_Message_EntityType.TEAM,organizationUserId,content,ECommon_IM_Message_ContentType.TEXT,date,null,teamId)
})
await this.initOrganization(organizationId)
}
},
async initOrganization(organizationId:string) {
SocketIOClient.getSocket(ECommon_Socket_Type.CALENDAR)?.close()
await Promise.all([
this.requestStatus(),
(async ()=>{
let retOrganization=await apiOrganization.info({
organizationId
})
if(retOrganization?.code==0) {
this.organizationInfo=retOrganization.data
SessionStorage.set("organizationId",retOrganization.data.id);
}
})(),
(async ()=>{
let ret=await apiOrganization.getPermission({
organizationId
})
if(ret?.code==0) {
this.organizationPermission=ret.data.value
}
})()
])
let objSocketCalendar=SocketIOClient.create(ECommon_Socket_Type.CALENDAR)
objSocketCalendar.getSocket().on("calendar_event_reminder",(id, name, start_time) => {
if(this.status===ECommon_User_Online_Status.ONLINE) {
NotificationWrapper.show("Calendar Event Reminder",`${name} will start in ${Math.floor((start_time-Date.now())/1000/60)} minutes`,NotificationType.CALENDAR,null,null,60000)
}
})
SocketIOClient.getSocket(ECommon_Socket_Type.IM)?.close()
let objSocketIM=SocketIOClient.create(ECommon_Socket_Type.IM)
handleIMEvent(objSocketIM.getSocket(),this.status)
if(this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
}
this.heartbeatInterval=setInterval(()=>{
objSocketIM.getSocket().emit("im_heartbeat")
},1000*60*20)
SocketIOClient.getSocket(ECommon_Socket_Type.MEETING)?.close()
let objSocketMeeting=SocketIOClient.create(ECommon_Socket_Type.MEETING)
handleMeetingEvent(objSocketMeeting.getSocket())
},
async isAuth():Promise<boolean> {
if(!SessionStorage.get("userToken")) {
return false
@ -108,6 +119,9 @@ export const useDesktopStore=defineStore("desktop",{
let res=await apiUser.logout()
if(res?.code==0) {
SessionStorage.clear();
SocketIOClient.clear()
clearInterval(this.heartbeatInterval)
this.heartbeatInterval=null;
this.$reset();
return true
} else {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,203 @@
<template>
<div style="width: 100%;height: 100%;overflow-x: hidden;overflow-y:auto;background-color: rgb(25,27,31)">
<div style="height: 70px;width: 100%;display: flex;justify-content: space-between;align-items: center;padding: 0 10px;box-sizing: border-box">
<a-space size="large">
<h2 style="color: white;">
Teamlinker
</h2>
<a-space size="large" style="padding-left: 10px">
<a href="/" class="title">
Home
</a>
<a href="javascript:void(0)" class="title">
Doc
</a>
<a href="javascript:void(0)" class="title">
Resource
</a>
<a href="javascript:void(0)" class="title">
Contact
</a>
</a-space>
</a-space>
<a-space>
<a-button size="large" type="primary" shape="round" status="success" @click="onLogin">
Sign In
</a-button>
<a-button size="large" type="primary" shape="round" status="warning">
Sign Up
</a-button>
</a-space>
</div>
<div style="color: white;text-align: center;margin-top: 100px;font-size: 50px;font-weight: bolder">
Teamlinker : Link Every Team Easier And Faster
</div>
<div style="text-align: center;font-size: 30px;margin-top: 20px;color: #9373ee;font-weight: bold">
Free , Strong , Efficient , Simple
</div>
<!-- <div style="color: rgba(255,255,255,0.8);text-align: center;font-size: 20px;margin-top: 30px">-->
<!-- It is not only a collaboration platform,-->
<!-- </div>-->
<!-- <div style="color: rgba(255,255,255,0.8);text-align: center;font-size: 20px;margin-top: 10px">-->
<!-- but also your virtual workstation, enjoy it just like using your desktop-->
<!-- </div>-->
<div style="display: flex;justify-content: center;margin-top: 50px">
<a-button type="primary" size="large" status="success" shape="round" style="width: 100px;font-weight: bold">
Try It
</a-button>
</div>
<div style="display: flex;justify-content: center;margin-top: 50px;">
<img :src="img1" style="width: 70%;border-radius: 5px">
</div>
<div style="text-align: center;margin-top: 150px;font-size: 20px;color: #9373ee;font-weight: bold">
HOW IT WORKS
</div>
<div style="color: white;font-weight: bold;text-align: center;font-size: 30px;margin-top: 80px">
One Platform,One Piece Of Data,Resolve Basic Online Work Flow
</div>
<div style="color: rgba(255,255,255,0.8);text-align: center;font-size: 20px;margin-top: 30px">
Teamlinker is a team collaboration solution for enterprise users,
</div>
<div style="color: rgba(255,255,255,0.8);text-align: center;font-size: 20px;margin-top: 10px">
which allows team members to achieve remote office and collaboration
</div>
<div style="display: flex;margin-top: 80px;padding:0 100px">
<div style="flex: 1 1 30%;padding-left: 130px">
<h1 style="color: orangered;margin-left: 25px">Project</h1>
<div style="color: rgba(255,255,255,0.8);font-size: 20px">
<ul class="li">
<li>
Any workflow can be edited in a graphic way
</li>
<li>
Users can create their own issue types
</li>
<li>
Users can get started without any configuration
</li>
</ul>
</div>
</div>
<div style="flex: 1 1 70%;padding-left: 100px">
<img :src="img1" style="width: 80%;border-radius: 5px">
</div>
</div>
<div style="display: flex;margin-top: 100px;border-top: 1px solid rgba(255,255,255,0.3);padding:100px 100px 0 100px">
<div style="flex: 1 1 70%;display: flex;justify-content: center">
<img :src="img1" style="width: 80%;border-radius: 5px">
</div>
<div style="flex: 1 1 30%;padding-right: 130px">
<h1 style="color: orange;margin-left: 25px">Wiki</h1>
<div style="color: rgba(255,255,255,0.8);font-size: 20px">
<ul class="li">
<li>
Can record daily work
</li>
<li>
It can deeply integrate modules such as issue and calendar
</li>
</ul>
</div>
</div>
</div>
<div style="display: flex;margin-top: 100px;border-top: 1px solid rgba(255,255,255,0.3);;padding:100px 100px 0 100px">
<div style="flex: 1 1 30%;padding-left: 130px">
<h1 style="color: deepskyblue;margin-left: 25px">Calendar</h1>
<div style="color: rgba(255,255,255,0.8);font-size: 20px">
<ul class="li">
<li>
You can freely switch between different calendar types
</li>
<li>
One-button meeting
</li>
<li>
Create conference room management for real conference rooms
</li>
</ul>
</div>
</div>
<div style="flex: 1 1 70%;;padding-left: 100px">
<img :src="img1" style="width: 80%;border-radius: 5px">
</div>
</div>
<div style="display: flex;margin-top: 100px;border-top: 1px solid rgba(255,255,255,0.3);;padding:100px 100px 0 100px">
<div style="flex: 1 1 70%;display: flex;justify-content: center">
<img :src="img1" style="width: 80%;border-radius: 5px">
</div>
<div style="flex: 1 1 30%;padding-right: 130px">
<h1 style="color: lightseagreen;margin-left: 25px">IM</h1>
<div style="color: rgba(255,255,255,0.8);font-size: 20px">
<ul class="li">
<li>
All members in the company can be directly contacted
</li>
<li>
Member status is online, meeting, offline, do not disturb
</li>
</ul>
</div>
</div>
</div>
<div style="display: flex;margin-top: 100px;border-top: 1px solid rgba(255,255,255,0.3);;padding:100px 100px 0 100px">
<div style="flex: 1 1 30%;padding-left: 130px">
<h1 style="color: mediumpurple;margin-left: 25px">Meeting</h1>
<div style="color: rgba(255,255,255,0.8);font-size: 20px">
<ul class="li">
<li>
Voice, video, desktop share. Everything is on the web
</li>
<li>
Meeting can be recorded as a video and saved
</li>
</ul>
</div>
</div>
<div style="flex: 1 1 70%;padding-left: 100px">
<img :src="img1" style="width: 80%;border-radius: 5px">
</div>
</div>
<div style="text-align: center;margin-top: 150px;font-size: 40px;color: #9373ee;font-weight: bold">
You Are In Good Company
</div>
<div style="text-align: center;color: gray;margin-top: 30px;font-size: 20px">
A wide variety of institutions and organizations across the world have adopted Teamlinker as their virtual workstation.
</div>
<div style="display: flex;justify-content: center;margin-top: 50px;border-bottom: 1px solid rgba(255,255,255,0.3);padding-bottom: 150px">
<Company></Company>
</div>
<p style="color: white;text-align: center">© 2016-2023 合肥阿尔方斯科技有限公司 <a href="https://beian.miit.gov.cn/" style="color: white" target="_blank">皖ICP备2023006742号</a></p>
</div>
</template>
<script setup lang="ts">
import img1 from "../../../assert/index_sample.png"
import Company from "./company.vue";
import {useRouter} from "vue-router";
const router=useRouter()
const onLogin=async () =>{
await router.push("login")
}
</script>
<style scoped>
.title {
text-decoration:none;
color: white;
font-weight: bold;
font-size: 16px
}
.title:hover {
color: #9373ee;
}
.li li:not(:first-child) {
margin-top: 50px;
}
.li li:first-child {
margin-top: 50px;
}
.li {
font-weight: bold;
}
</style>

View File

@ -1,7 +1,9 @@
<template>
<a-row align="center" justify="center" style="height: 100%">
<a-form :model="form" style="width: 600px" @submit="onSubmit">
<h3 style="text-align: center;margin-bottom: 50px">Teamlinker</h3>
<router-link :to="{name:'index'}" style="text-decoration: none;">
<h3 style="text-align: center;margin-bottom: 50px">Teamlinker</h3>
</router-link>
<a-form-item field="username" label="username" required>
<a-input v-model="form.username" placeholder="please enter username"></a-input>
</a-form-item>
@ -9,7 +11,13 @@
<a-input v-model="form.password" type="password" placeholder="please enter password"></a-input>
</a-form-item>
<a-form-item>
<a-button html-type="submit">Submit</a-button>
<a-row style="justify-content: space-between;width: 100%">
<a-button html-type="submit" type="primary">Submit</a-button>
<a-space v-if="$deployMode.value===ECommon_Application_Mode.ONLINE">
<a-button html-type="button" type="primary" status="success" @click="onRegister">Register</a-button>
<a-button html-type="button" type="primary" status="warning" @click="onReset">Reset</a-button>
</a-space>
</a-row>
</a-form-item>
</a-form>
</a-row>
@ -22,6 +30,8 @@ import {useRouter} from "vue-router";
import {Message} from "@arco-design/web-vue";
import {NotificationWrapper} from "../../common/component/notification/notification";
import {SessionStorage} from "../../common/storage/session";
import md5 from "blueimp-md5"
import {ECommon_Application_Mode} from "../../../../../common/types";
let form=reactive({
username:"",
@ -32,7 +42,7 @@ const onSubmit=async ()=>{
NotificationWrapper.init()
let ret=await apiUser.login({
username:form.username,
password:form.password
password:md5(form.password)
})
if(ret.code==0) {
SessionStorage.remove("organizationId")
@ -41,6 +51,14 @@ const onSubmit=async ()=>{
Message.error(ret.msg);
}
}
const onRegister=async()=>{
await router.push("register")
}
const onReset=async ()=>{
await router.push("reset")
}
</script>
<style scoped>

View File

@ -0,0 +1,66 @@
<template>
<a-row align="center" justify="center" style="height: 100%" v-if="$deployMode.value===ECommon_Application_Mode.ONLINE">
<a-form :model="form" style="width: 700px" @submit="onSubmit">
<router-link :to="{name:'index'}" style="text-decoration: none;">
<h3 style="text-align: center;margin-bottom: 50px">Teamlinker</h3>
</router-link>
<a-form-item field="username" label="email" required>
<a-input v-model="form.username" placeholder="please enter email"></a-input>
</a-form-item>
<a-form-item field="password" label="password" required>
<a-input v-model="form.password" type="password" placeholder="please enter password"></a-input>
</a-form-item>
<a-form-item field="passwordRepeat" label="password confirm" required>
<a-input v-model="form.passwordRepeat" type="password" placeholder="please enter password again"></a-input>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary">Register</a-button>
</a-form-item>
</a-form>
</a-row>
</template>
<script setup lang="ts">
import {ECommon_Application_Mode} from "../../../../../common/types";
import {reactive} from "vue";
import {apiUser} from "../../common/request/request";
import {useRouter} from "vue-router";
import {Message} from "@arco-design/web-vue";
import md5 from "blueimp-md5";
let form=reactive({
username:"",
password:"",
passwordRepeat:""
})
const router=useRouter()
const onSubmit=async ()=>{
if(!form.username) {
Message.error("please type username")
return
} else if(!form.password) {
Message.error("please type password")
return
} else if(form.password!==form.passwordRepeat) {
Message.error("twice password not match")
return
}
let res=await apiUser.register({
username:form.username,
password:md5(form.password)
})
if(res?.code==0) {
await router.push({
name:"registerCode",
query:{
username:form.username
}
})
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,65 @@
<template>
<a-row align="center" justify="center" style="height: 100%" v-if="$deployMode.value===ECommon_Application_Mode.ONLINE && username">
<a-form :model="form" style="width: 700px" @submit="onSubmit">
<router-link :to="{name:'index'}" style="text-decoration: none;">
<h3 style="text-align: center;margin-bottom: 50px">Teamlinker</h3>
</router-link>
<a-form-item field="code" label="code">
<template #extra>
if you don't receive verification code email,you can <a href="javascript:void(0)" style="text-decoration: none" @click="onResend">resend</a>
</template>
<a-input v-model="form.code"></a-input>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary">Submit</a-button>
</a-form-item>
</a-form>
</a-row>
</template>
<script setup lang="ts">
import {ECommon_Application_Mode} from "../../../../../common/types";
import {reactive} from "vue";
import {apiUser} from "../../common/request/request";
import {Message} from "@arco-design/web-vue";
import {useRouter} from "vue-router";
const props=defineProps<{
username:string
}>()
const form=reactive({
code:""
})
const router=useRouter()
const onResend=async ()=>{
let res=await apiUser.resendCode({
username:props.username
})
if(res?.code==0) {
Message.info("send success")
} else {
Message.error(res.msg)
}
}
const onSubmit=async()=>{
if(!form.code) {
return
}
let res=await apiUser.confirmRegister({
username:props.username,
code:form.code
})
if(res?.code==0) {
Message.info("register success")
await router.replace("login")
} else {
Message.error(res.msg)
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,47 @@
<template>
<a-row align="center" justify="center" style="height: 100%" v-if="$deployMode.value===ECommon_Application_Mode.ONLINE">
<a-form :model="form" style="width: 700px" @submit="onSubmit">
<router-link :to="{name:'index'}" style="text-decoration: none;">
<h3 style="text-align: center;margin-bottom: 50px">Teamlinker</h3>
</router-link>
<a-form-item field="username" label="username">
<a-input v-model="form.username"></a-input>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary">Submit</a-button>
</a-form-item>
</a-form>
</a-row>
</template>
<script setup lang="ts">
import {ECommon_Application_Mode} from "../../../../../common/types";
import {reactive} from "vue";
import {apiUser} from "../../common/request/request";
import {useRouter} from "vue-router";
const form=reactive({
username:""
})
const router=useRouter()
const onSubmit=async ()=>{
if(form.username) {
let res=await apiUser.resetCode({
username:form.username
})
if(res?.code==0) {
await router.push({
name:"resetCode",
query:{
username:form.username
}
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,66 @@
<template>
<a-row align="center" justify="center" style="height: 100%" v-if="$deployMode.value===ECommon_Application_Mode.ONLINE && username">
<a-form :model="form" style="width: 700px" @submit="onSubmit">
<router-link :to="{name:'index'}" style="text-decoration: none;">
<h3 style="text-align: center;margin-bottom: 50px">Teamlinker</h3>
</router-link>
<a-form-item field="code" label="code">
<a-input v-model="form.code"></a-input>
</a-form-item>
<a-form-item field="password" label="password">
<a-input v-model="form.password"></a-input>
</a-form-item>
<a-form-item field="passwordRepeat" label="password confirm">
<a-input v-model="form.passwordRepeat"></a-input>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary">Submit</a-button>
</a-form-item>
</a-form>
</a-row>
</template>
<script setup lang="ts">
import {ECommon_Application_Mode} from "../../../../../common/types";
import {reactive} from "vue";
import {Message} from "@arco-design/web-vue";
import {apiUser} from "../../common/request/request";
import md5 from "blueimp-md5";
import {useRouter} from "vue-router";
const props=defineProps<{
username:string
}>()
const form=reactive({
code:"",
password:"",
passwordRepeat:""
})
const router=useRouter()
const onSubmit=async ()=>{
if(!form.code) {
Message.error("please type code")
return
} else if(!form.password) {
Message.error("please type password")
return
} else if(form.password!==form.passwordRepeat) {
Message.error("twice password not match")
return
}
let res=await apiUser.reset({
username:props.username,
code:form.code,
password:md5(form.password)
})
if(res?.code==0) {
Message.info("reset success")
await router.replace("login")
}
}
</script>
<style scoped>
</style>

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Ant, AntJson, AntName } from './Ant';
import {onMounted, ref} from 'vue';
import {Ant, AntJson, AntName} from './Ant';
const val = {
[AntName]: AntJson,
@ -29,7 +29,7 @@ const props = defineProps<{
const iconRef = ref<HTMLDivElement>({} as any)
const str = {
svg: ['<svg style="width: 1em;height: 1em;vertical-align: middle;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">', '</svg>'],
svg: [`<svg style="width: 1em;height: 1em;vertical-align: middle;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">`, '</svg>'],
getPath: (param: { d: string, fill?: string }) => {
return `<path d="${param.d}" fill="${param.fill ?? 'currentColor'}" />`
}
@ -47,6 +47,9 @@ onMounted(() => {
res = `${str.svg[0]}<path d="${obj}" fill="currentColor" />${str.svg[1]}`
} else {
res = str.svg[0]
if(obj.viewBox) {
res=res.replace("0 0 1024 1024",obj.viewBox)
}
let objt: { d: string[]; fill: { [key: string]: number[] }; } = obj
const getFill = (fill: { [key: string]: number[] }, index: number) => {
const fillkeys = Object.keys(fill)

View File

@ -1,4 +1,4 @@
import {createApp} from 'vue'
import {createApp, ref} from 'vue'
import './style.css'
import App from './App.vue'
import '@arco-design/web-vue/dist/arco.css';
@ -7,10 +7,16 @@ import sicon from "./icon/sicon.vue"
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
import {createPinia} from "pinia";
import Login from "./business/controller/login/login.vue";
import Desktop from "./business/controller/desktop/desktop.vue";
import {createRouter, createWebHashHistory} from "vue-router";
import "@logicflow/core/dist/style/index.css";
import Index from "./business/controller/index/index.vue";
import {apiGateway} from "./business/common/request/request";
import Register from "./business/controller/login/register.vue";
import RegisterCode from "./business/controller/login/registerCode.vue";
import Reset from "./business/controller/login/reset.vue";
import ResetCode from "./business/controller/login/resetCode.vue";
const Desktop =()=>import("./business/controller/desktop/desktop.vue")
const routes=[
{
name:"login",
@ -21,6 +27,33 @@ const routes=[
name:"desktop",
path:"/desktop",
component:Desktop
},
{
name:"index",
path:"/",
component: Index
},
{
name:"register",
path:"/register",
component: Register
},
{
name:"registerCode",
path:"/registerCode",
component: RegisterCode,
props:route=>({username:route.query.username})
},
{
name:"reset",
path:"/reset",
component: Reset
},
{
name:"resetCode",
path:"/resetCode",
component: ResetCode,
props:route=>({username:route.query.username})
}
]
const router=createRouter({
@ -28,6 +61,7 @@ const router=createRouter({
routes
});
let app=createApp(App)
app.config.globalProperties.$deployMode=ref()
Message._context=app._context
app.use(ArcoVue)
app.use(ArcoVueIcon)
@ -35,3 +69,8 @@ app.use(createPinia())
app.use(router);
app.component("sicon",sicon)
app.mount('#app')
apiGateway.deployInfo().then(value => {
if(value?.code==0) {
app.config.globalProperties.$deployMode.value=value.data.type
}
})

View File

@ -2,9 +2,6 @@ import {reactive, ref} from "vue";
import {getBaseColor} from "../common/util/color";
import {Base} from "../common/util/base";
export interface ITeamOS_Desktop_Event {
"menuClick":(value:string)=>void
}
export interface ITeamOS_Desktop_Menu {
group:string,
@ -15,8 +12,6 @@ export interface ITeamOS_Desktop_Menu {
}
class Desktop extends Base{
private backgroundImage=ref("")
private logo=ref("")
private menu=reactive<ITeamOS_Desktop_Menu[]>([])
private baseColor:{
r:number,
g:number,
@ -26,34 +21,6 @@ class Desktop extends Base{
g:0,
b:0
});
private menuValue=ref("")
private avatar=ref("")
private avatarOptions=ref<{
name:string,
clickFunc:any
}[]>([])
onMenuClick:(value:string)=>void
setLogo(logo:string) {
this.logo.value=logo
}
setMenu(menu:ITeamOS_Desktop_Menu[],defaultValue?:string) {
if(this.menu.length>0) {
this.menu.splice(0)
}
this.menu.push(...menu);
if(defaultValue) {
this.menuValue.value=defaultValue
}
}
getMenuValue() {
return this.menuValue
}
getLogo() {
return this.logo
}
getMenu() {
return this.menu
}
getBackgroundImage() {
return this.backgroundImage
}
@ -68,36 +35,12 @@ class Desktop extends Base{
getBaseColor() {
return this.baseColor;
}
addEventListener<T extends keyof ITeamOS_Desktop_Event>(eventType:T,func:ITeamOS_Desktop_Event[T]) {
if(eventType=="menuClick") {
this.onMenuClick=func
}
}
getAvatar() {
return this.avatar
}
setAvatar(uri:string) {
this.avatar.value=uri;
}
getAvatarOptions() {
return this.avatarOptions
}
setAvatarOptions(options:{
name:string,
clickFunc:any
}[]) {
this.avatarOptions.value=options;
}
clear() {
this.backgroundImage.value = ""
this.logo.value = ""
this.menu.splice(0, this.menu.length)
this.baseColor.r = 0;
this.baseColor.g = 0;
this.baseColor.b = 0;
this.menuValue.value = ""
this.avatar.value = ""
this.avatarOptions.value = []
}
}

View File

@ -1,12 +1,7 @@
<template>
<a-row>
<a-col flex="80px" style="height: 100%;text-align: center;">
<a-dropdown trigger="hover">
<a-avatar :size="32" :image-url="avatar" style="margin-top: 4px">T</a-avatar>
<template #content>
<a-doption v-for="item in avatarOptions" @click="item.clickFunc">{{item.name}}</a-doption>
</template>
</a-dropdown>
<a-col flex="80px" style="height: 100%;justify-content: center;align-items: center;display: flex">
<slot name="topLeft"></slot>
</a-col>
<a-col flex="auto" style="height: 100%;padding-left: 30px">
<a-row style="height: 100%" align="center" justify="start" :wrap="false">
@ -19,18 +14,7 @@
</a-row>
</a-col>
<a-col flex="250px" style="height: 100%;" id="teamOS-Desktop-Menu">
<a-row style="height: 100%;" align="center">
<a-col flex="80px" style="align-items: center;display: flex">
<img :src="logo" style="height: 36px;width:80px;object-fit: cover;object-position: 50% 50%;" v-if="logo"/>
</a-col>
<a-col flex="170px">
<a-select style="max-width: 170px;color: white" placeholder="organization" :bordered="false" @change="onMenuClick" v-model:model-value="menuValue">
<a-optgroup v-for="(item,index) in menu" :label="item.group">
<a-option v-for="(item1,index1) in item.data" :label="item1.name" :value="item1.value"></a-option>
</a-optgroup>
</a-select>
</a-col>
</a-row>
<slot name="topRight"></slot>
</a-col>
</a-row>
</template>
@ -42,18 +26,8 @@ import {ETeamOS_Window_Status} from "../window/window.js";
import Sicon from "../../icon/sicon.vue";
import {iconGroupMap} from "../icon/icon";
const logo=desktop.getLogo();
const avatar=desktop.getAvatar();
const avatarOptions=desktop.getAvatarOptions()
const menu=desktop.getMenu();
const list=windowManager.getList();
const color=desktop.getBaseColor();
const menuValue=desktop.getMenuValue()
const onMenuClick=(value:string)=>{
if(desktop.onMenuClick) {
desktop.onMenuClick(value)
}
}
</script>
<style scoped>

View File

@ -1,6 +1,7 @@
<template>
<div id="teamOS" :style="{backgroundImage:'url('+backImag+')'}">
<DesktopBar style="height: 40px;background: rgba(255,255,255,0.3);">
<template v-for="(_, slot) of $slots" v-slot:[slot]="scope"><slot :name="slot" v-bind="scope"/></template>
</DesktopBar>
<IconContainer style="height: calc(100% - 40px)">
<WindowContainer></WindowContainer>

View File

@ -1,9 +1,12 @@
<template>
<DesktopContainer></DesktopContainer>
<DesktopContainer>
<template v-for="(_, slot) of $slots" v-slot:[slot]="scope"><slot :name="slot" v-bind="scope"/></template>
</DesktopContainer>
</template>
<script setup lang="ts">
import DesktopContainer from "./desktop/desktopContainer.vue";</script>
import DesktopContainer from "./desktop/desktopContainer.vue";
</script>
<style scoped>

10
code/client/src/type.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import {ECommon_Application_Mode} from "../../common/types";
import {Ref} from "vue";
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$deployMode: Ref<ECommon_Application_Mode>;
}
}
export {}

View File

@ -1,5 +1,7 @@
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import * as fs from "fs";
import * as path from "path";
// https://vitejs.dev/config/
export default defineConfig({
@ -11,6 +13,11 @@ export default defineConfig({
// })
],
server:{
host:"0.0.0.0",
https:{
key:fs.readFileSync(path.join(__dirname,'./certs/key.pem')),
cert: fs.readFileSync(path.join(__dirname,'./certs/cert.pem'))
},
port: 3000,
hmr:true,
open: false, //自动打开

View File

@ -9,6 +9,7 @@ export interface ICommon_Model_IM_Team_Message {
created_time:Date,
content:string,
content_type:ECommon_IM_Message_ContentType
file_id:string
}
export const Table_IM_Team_Message="im_team_message"

View File

@ -11,6 +11,7 @@ export interface ICommon_Model_IM_User_Message {
created_time:Date,
content:string,
content_type:ECommon_IM_Message_ContentType
file_id:string
}
export const Table_IM_User_Message="im_user_message"

View File

@ -0,0 +1,31 @@
import {BaseModel} from "./base"
export enum ECommon_Model_Meeting_Room_Type {
PRIVATE,
SCHEDULE
}
export enum ECommon_Meeting_Room_Permission {
NORMAL="normal",
PRESENTER="presenter"
}
export interface ICommon_Model_Meeting_Room {
id:string,
name:string,
description:string,
created_by:string,
type:ECommon_Model_Meeting_Room_Type,
related_id:string,
start_time:Date,
end_time:Date,
password:string
}
export const Table_Meeting_Room="meeting_room"
class MeetingRoomModel extends BaseModel {
table=Table_Meeting_Room
model=<ICommon_Model_Meeting_Room>{}
}
export let meetingRoomModel=new MeetingRoomModel

View File

@ -1,6 +1,5 @@
import { ECommon_Services } from "../types";
import { ECommon_Application_Mode } from './../../server/common/app/app';
import { ECommon_HttpApi_Method } from "./types";
import {ECommon_Application_Mode, ECommon_Services} from "../types";
import {ECommon_HttpApi_Method} from "./types";
const api={
baseUrl:"/gateway",

View File

@ -0,0 +1,94 @@
import {ECommon_Services} from "../types";
import {ECommon_HttpApi_Method} from "./types";
import {Permission_Types} from "../permission/permission";
import {ECommon_Model_Meeting_Room_Type, ICommon_Model_Meeting_Room} from "../model/meeting_room";
const api= {
baseUrl: "/meeting",
service: ECommon_Services.Meeting,
routes: {
listRoom: {
method: ECommon_HttpApi_Method.GET,
path: "/room/list",
req: <{
keyword?:string,
type?:ECommon_Model_Meeting_Room_Type,
page:number,
size:number
}>{},
res: <{
data:ICommon_Model_Meeting_Room[],
count:number,
totalPage:number,
page:number
}>{},
permission: [Permission_Types.Organization.READ]
},
createRoom: {
method: ECommon_HttpApi_Method.POST,
path: "/room",
req: <{
name:string,
description?:string,
related_id:string,
startTime:number,
endTime:number,
password:string
}>{},
res: <ICommon_Model_Meeting_Room>{},
permission: [Permission_Types.Organization.READ]
},
editRoom: {
method: ECommon_HttpApi_Method.PUT,
path: "/room",
req: <{
meetingRoomId:string,
name?:string,
description?:string,
startTime?:number,
endTime?:number,
password?:string
}>{},
res: <ICommon_Model_Meeting_Room>{},
permission: [Permission_Types.Organization.READ,Permission_Types.Common.SELF]
},
deleteRoom: {
method: ECommon_HttpApi_Method.DELETE,
path: "/room",
req: <{
meetingRoomId:string
}>{},
res: {},
permission: [Permission_Types.Organization.READ,Permission_Types.Common.SELF]
},
getPersonalRoom: {
method: ECommon_HttpApi_Method.GET,
path: "/room/private",
req: {},
res: <ICommon_Model_Meeting_Room>{},
permission: [Permission_Types.Organization.READ]
},
validateRoom:{
method: ECommon_HttpApi_Method.GET,
path: "/room/validate",
req: <{
meetingRoomId:string,
password:string
}>{},
res: <{
meetingRoomId:string,
password:string
}>{},
permission: [Permission_Types.Organization.READ]
},
getCurrentRoom: {
method: ECommon_HttpApi_Method.GET,
path: "/room/current",
req: {},
res: <ICommon_Model_Meeting_Room>{},
permission: [Permission_Types.Organization.READ]
},
}
}
export default api

View File

@ -1,6 +1,6 @@
import {Permission_Types} from '../permission/permission';
import {ICommon_Model_Organization} from './../model/organization';
import {ECommon_Services} from './../types';
import {ECommon_Services, ECommon_User_Online_Status} from './../types';
import {
ICommon_Route_Res_Member_Tag_Member,
ICommon_Route_Res_Organization_FilterUserAndTeam,
@ -301,7 +301,8 @@ const api={
id:string,
name:string,
photo:string,
type:ECommon_IM_Message_EntityType
type:ECommon_IM_Message_EntityType,
status?:ECommon_User_Online_Status
}
}>{},
permission:[Permission_Types.Organization.READ]
@ -315,6 +316,76 @@ const api={
}>{},
res:<ICommon_Route_Res_Organization_FilterUserAndTeam>{},
permission:[Permission_Types.Organization.READ]
},
changeUserStatus:{
method:ECommon_HttpApi_Method.PUT,
path:"/user/status",
req:<{
status:ECommon_User_Online_Status.ONLINE|ECommon_User_Online_Status.BUSY
}>{},
res:{},
permission:[Permission_Types.Organization.READ]
},
getUserStatus:{
method:ECommon_HttpApi_Method.GET,
path:"/user/status",
req:<{
organizationUserId?:string
}>{},
res:<{
status:ECommon_User_Online_Status
}>{},
permission:[Permission_Types.Organization.READ]
},
getUserStatusList:{
method:ECommon_HttpApi_Method.GET,
path:"/user/statuslist",
req:<{
organizationUserIds:string[]
}>{},
res:<{
[organizationUserIds:string]:ECommon_User_Online_Status
}>{},
permission:[Permission_Types.Organization.READ]
},
createUser:{
method:ECommon_HttpApi_Method.POST,
path:"/user/offline/new",
req:<{
organizationId:string,
password:string,
username:string,
roleId:string,
nickname:string,
active:number,
title?:string,
job?:string,
email?:string,
phone?:string,
location?:string,
remark?:string
}>{},
res:<ICommon_Route_Res_Organization_User_Item>{},
permission:[Permission_Types.Organization.ADMIN]
},
resetUserPassword:{
method:ECommon_HttpApi_Method.POST,
path:"/user/offline/password",
req:<{
organizationUserId:string,
password:string
}>{},
res:{},
permission:[Permission_Types.Organization.ADMIN]
},
deleteUserForOffline:{
method:ECommon_HttpApi_Method.DELETE,
path:"/user/offline",
req:<{
organizationUserId:string
}>{},
res:{},
permission:[Permission_Types.Organization.ADMIN]
}
}
}

View File

@ -68,7 +68,7 @@ const api={
username:string,
password:string
}>{},
res:<Omit<ICommon_Model_User,"password">>{},
res:{},
ignoreValidate:true
},
create:{//创建用户
@ -154,7 +154,46 @@ const api={
id:string,
photo:string
}[]>{},
}
},
confirmRegister:{
method:ECommon_HttpApi_Method.POST,
path:"/register/confirm",
req:<{
username:string,
code:string
}>{},
res:{},
ignoreValidate:true
},
resendCode:{
method:ECommon_HttpApi_Method.POST,
path:"/register/code",
req:<{
username:string
}>{},
res:{},
ignoreValidate:true
},
resetCode:{
method:ECommon_HttpApi_Method.POST,
path:"/reset/code",
req:<{
username:string
}>{},
res:{},
ignoreValidate:true
},
reset:{
method:ECommon_HttpApi_Method.POST,
path:"/reset",
req:<{
username:string,
password:string,
code:string
}>{},
res:{},
ignoreValidate:true
},
}
}

View File

@ -1,10 +1,13 @@
import {ICommon_Model_IM_UnRead_Message} from "../model/im_unread_message";
import {ECommon_IM_Message_EntityType, ICommon_Model_IM_UnRead_Message} from "../model/im_unread_message";
import {ICommon_Model_IM_User_Message} from "../model/im_user_message";
import {ICommon_Model_IM_Team_Message} from "../model/im_team_message";
import {ECommon_User_Online_Status} from "../types";
import {ECommon_Meeting_Room_Permission} from "../model/meeting_room";
export enum ECommon_Socket_Type {
CALENDAR = "calendar",
IM = "im"
IM = "im",
MEETING="meeting"
}
@ -14,6 +17,12 @@ export interface ICommon_Socket_ServerToClientEvents {
//im
im_user_relay_text_message: (fromOrganizationUserId: string, toOrganizationUserId: string, content: string,date:Date) => void
im_team_relay_text_message: (organizationUserId: string, teamId: string, content: string,date:Date) => void
im_user_relay_image_message: (fromOrganizationUserId: string, toOrganizationUserId: string, fileId: string,date:Date) => void
im_team_relay_image_message: (organizationUserId: string, teamId: string, fileId: string,date:Date) => void
im_organization_user_status_change:(organizationUserId:string, status:ECommon_User_Online_Status)=>void
//meeting
meeting_presenter_change:(organizationUserId:string,permission:ECommon_Meeting_Room_Permission)=>void
meeting_invite:(fromOrganizationUserId:string,fromOrganizationUserName:string,roomId:string,password:string)=>void
}
export interface ICommon_Socket_ClientToServerEvents {
@ -28,10 +37,28 @@ export interface ICommon_Socket_ClientToServerEvents {
msg?:string,
success:boolean
}) => void) => void
im_unread_messages: ( callback: (list: ICommon_Model_IM_UnRead_Message[]) => void) => void
im_user_send_image_message: (toOrganizationUserId: string, fileId: string, callback: (data:{
code?:number,
msg?:string,
success:boolean
}) => void) => void
im_team_send_image_message: (teamId: string, fileId: string, callback: (data:{
code?:number,
msg?:string,
success:boolean
}) => void) => void
im_unread_message_list: (callback: (list: ICommon_Model_IM_UnRead_Message[]) => void) => void
im_user_message_list: (toOrganizationUserId: string, size: number, lastTime: number, callback: (list: ICommon_Model_IM_User_Message[]) => void) => void
im_team_message_list: (teamId: string, size: number, lastTime: number, callback: (list: ICommon_Model_IM_Team_Message[]) => void) => void
im_read_message:(entityId:string)=>void
im_unread_message:(entityId:string,entityType:ECommon_IM_Message_EntityType)=>void
im_heartbeat:()=>void
//meeting
meeting_change_presenter:(businessId:string,permission:ECommon_Meeting_Room_Permission,callback:(success:boolean)=>void)=>void
meeting_get_presenters:(callback:(presenters:{
[id:string]:ECommon_Meeting_Room_Permission
})=>void)=>void
meeting_invite:(inviteBusinessIds:string[])=>void
}
export interface ICommon_Socket_InterServerEvents {
@ -39,5 +66,6 @@ export interface ICommon_Socket_InterServerEvents {
export interface ICommon_Socket_Data {
userId: string,
organizationUserId: string
organizationUserId: string,
organizationId:string
}

View File

@ -27,6 +27,10 @@ export namespace Err {
admin:{
code:7,
msg:"user is admin"
},
interfaceForbidden:{
code:8,
msg:"interface is forbidden"
}
}
export const User = {
@ -57,6 +61,26 @@ export namespace Err {
accessDenied:{
code:1006,
msg:"access denied"
},
notInMeeting:{
code:1007,
msg:"not in meeting"
},
userNameNotMail:{
code:1008,
msg:"username is not email"
},
userCacheExpired:{
code:1009,
msg:"user cache expire"
},
codeNotMatch:{
code:1010,
msg:"code not match"
},
timeTooShort:{
code:1011,
msg:"time interval is too short"
}
}
export const Http = {
@ -471,5 +495,36 @@ export namespace Err {
msg:"team not found"
}
}
export const Meeting={
roomNotFound:{
code:13000,
msg:"room not found"
},
roomStillActive:{
code:13001,
msg:"room still active"
},
privateRoomDeleteForbidden:{
code:13002,
msg:"private room delete forbidden"
},
endTimeLarger:{
code:13003,
msg:"start time less than end time"
},
startTimeLess:{
code:13004,
msg:"start time less than current time one hour"
},
passwordWrong:{
code:13005,
msg:"password wrong"
},
notInMeeting:{
code:13006,
msg:"not in meeting"
}
}
}

View File

@ -5,5 +5,18 @@ export enum ECommon_Services {
File="file",
AUTH="auth",
Wiki="wiki",
Calendar="calendar"
Calendar="calendar",
Meeting="meeting"
}
export enum ECommon_User_Online_Status {
OFFLINE,
ONLINE,
BUSY,
MEETING
}
export enum ECommon_Application_Mode {
ONLINE,
OFFLINE
}

View File

@ -1,30 +1,29 @@
import {DComponent} from "../../common/decorate/component";
import {DEventListener} from "../../common/event/event";
import {EServer_Common_Event_Types} from "../../common/event/types";
import {REDIS_AUTH} from "../../common/cache/keys/auth";
@DComponent
class PermissionClear {
@DEventListener(EServer_Common_Event_Types.Auth.ORGANIZATION_USER_ADD)
@DEventListener("organizationUserAdd")
async organizationUserAdd(organizationId:string,user:string) {
let obj=new REDIS_AUTH.Permission.Organization.OrganizationUsers(organizationId);
await obj.del()
}
@DEventListener(EServer_Common_Event_Types.Auth.ORGANIZATION_USER_EDIT)
@DEventListener("organizationUserEdit")
async organizationUserEdit(organizationId:string,user:string) {
let obj=new REDIS_AUTH.Permission.Organization.OrganizationUsers(organizationId);
await obj.del()
}
@DEventListener(EServer_Common_Event_Types.Auth.ORGANIZATION_USER_DELETE)
@DEventListener("organizationUserDelete")
async organizationUserDelete(organizationId:string,user:string) {
let obj=new REDIS_AUTH.Permission.Organization.OrganizationUsers(organizationId);
await obj.del()
}
@DEventListener(EServer_Common_Event_Types.Auth.PROJECT_MEMBER_ADD)
@DEventListener("projectMemberAdd")
async projectMemberAdd(projectId:string,memberId:string) {
await Promise.all([(async ()=>{
let obj=new REDIS_AUTH.Permission.Project.ProjectOrganizationUsers(projectId);
@ -43,7 +42,7 @@ class PermissionClear {
}])
}
@DEventListener(EServer_Common_Event_Types.Auth.PROJECT_MEMBER_EDIT)
@DEventListener("projectMemberEdit")
async projectMemberEdit(projectId:string,memberId:string) {
await Promise.all([(async ()=>{
let obj=new REDIS_AUTH.Permission.Project.ProjectOrganizationUsers(projectId);
@ -62,7 +61,7 @@ class PermissionClear {
}])
}
@DEventListener(EServer_Common_Event_Types.Auth.PROJECT_MEMBER_DELETE)
@DEventListener("projectMemberDelete")
async projectMemberDelete(projectId:string,memberId:string) {
await Promise.all([(async ()=>{
let obj=new REDIS_AUTH.Permission.Project.ProjectOrganizationUsers(projectId);
@ -81,19 +80,19 @@ class PermissionClear {
}])
}
@DEventListener(EServer_Common_Event_Types.Auth.TEAM_USER_ADD)
@DEventListener("teamUserAdd")
async teamUserAdd(teamId:string,userId:string) {
let obj=new REDIS_AUTH.Permission.Team.OrganizationUsers(teamId);
await obj.del()
}
@DEventListener(EServer_Common_Event_Types.Auth.TEAM_USER_EDIT)
@DEventListener("teamUserEdit")
async teamUserEdit(teamId:string,userId:string) {
let obj=new REDIS_AUTH.Permission.Team.OrganizationUsers(teamId);
await obj.del()
}
@DEventListener(EServer_Common_Event_Types.Auth.TEAM_USER_DELETE)
@DEventListener("teamUserDelete")
async teamUserDelete(teamId:string,userId:string) {
let obj=new REDIS_AUTH.Permission.Team.OrganizationUsers(teamId);
await obj.del()

View File

@ -5,7 +5,7 @@ import {Permission_Types} from "../../../common/permission/permission";
@DComponent
export class PermissionSelf extends PermissionBase {
async translateToField({commentId,projectIssueId,projectReleaseId}: { [param: string]: any; userId?: string; isAdmin?: boolean; }): Promise<string>
async translateToField({commentId,projectIssueId,projectReleaseId,meetingRoomId}: { [param: string]: any; userId?: string; isAdmin?: boolean; }): Promise<string>
{
let createdByPureId:string
if(commentId) {
@ -20,6 +20,10 @@ export class PermissionSelf extends PermissionBase {
let obj=new REDIS_AUTH.Permission.Project.createdByPureFromProjectRelease(projectReleaseId);
createdByPureId=await obj.getValue();
return createdByPureId;
} else if(meetingRoomId) {
let obj=new REDIS_AUTH.Permission.Meeting.createdByPureFromMeetingRoom(meetingRoomId);
createdByPureId=await obj.getValue();
return createdByPureId;
}
}
fieldName: string="created_by_pure"

View File

@ -22,9 +22,9 @@ class CalendarMq extends BaseMq {
"x-delayed-type":"direct"
}
})
await this.consumer.assertQueue(queue,{durable:true})
await this.publisher.bindQueue(queue, exchange, queue)
await this.publisher.purgeQueue(queue)
await this.consumer.assertQueue(queue,{durable:true})
this.consumer.consume(queue,async msg => {
if(msg) {
let emit=getSocketEmitterInstance()

View File

@ -56,7 +56,7 @@ class RpcCalendarApi {
let obj=await CalendarSettingService.getItemByExp({
organization_user_id:organizationUserId
})
if(obj) {
if(!obj) {
throw Err.Calendar.settingNotFound
}
await obj.delete()

View File

@ -2,7 +2,7 @@ import {Entity} from "../../common/entity/entity";
import {calendarModel} from "../../../common/model/calendar";
import {calendarEventMapper, calendarMapper, calendarSettingMapper} from "../mapper/calendar";
import {calendarSettingModel} from "../../../common/model/calendar_setting";
import {EServer_Common_Event_Types} from "../../common/event/types";
import {IServer_Common_Event_Types} from "../../common/event/types";
import {calendarEventModel, ECommon_Calendar_Recurring_Type} from "../../../common/model/calendar_event";
import userRpcApi from "../../user/rpc/user"
import {generateSnowId} from "../../common/util/sql";
@ -18,7 +18,7 @@ export class CalendarService extends Entity<typeof calendarModel,typeof calendar
return ret;
}
override async delete(eventPublish?: EServer_Common_Event_Types.Types): Promise<void> {
override async delete(eventPublish?: keyof IServer_Common_Event_Types): Promise<void> {
await super.delete(eventPublish);
await calendarMapper.clearEvent(this.getId())
}
@ -122,7 +122,7 @@ export class CalendarEventService extends Entity<typeof calendarEventModel,typeo
}
}
override async delete(eventPublish?: EServer_Common_Event_Types.Types): Promise<void> {
override async delete(eventPublish?: keyof IServer_Common_Event_Types): Promise<void> {
await super.delete(eventPublish);
await calendarEventMapper.clearGuestList(this.getId())
}

View File

@ -14,6 +14,7 @@ import {generateHttpErrorResponse} from "../util/http";
import {init} from "../util/init";
import * as fs from "fs";
import {SocketIO} from "../socket/socket";
import {ECommon_Application_Mode} from "../../../common/types";
var pipe = function (from, to): Promise<Buffer> {
return new Promise(function (resolve) {
@ -29,10 +30,7 @@ var pipe = function (from, to): Promise<Buffer> {
})
})
}
export enum ECommon_Application_Mode {
ONLINE,
OFFLINE
}
export default abstract class Application{
private app=new Koa()
public static needReset:boolean=false
@ -57,7 +55,13 @@ export default abstract class Application{
"port":number,
"jwt": string,
"version":string,
"mq":string
"mq":string,
"mail":{
"host": string,
"port": number,
"user":string,
"pass":string
}
}
Application(){

View File

@ -24,6 +24,7 @@ import {organizationUserModel} from "../../../../common/model/organization_user"
import {memberTagMemberModel} from "../../../../common/model/member_tag_member";
import {wikiModel} from "../../../../common/model/wiki";
import {wikiItemModel} from "../../../../common/model/wiki_item";
import {meetingRoomModel} from "../../../../common/model/meeting_room";
export namespace REDIS_AUTH {
export namespace Permission {
@ -689,6 +690,56 @@ export namespace REDIS_AUTH {
}
}
}
export namespace Meeting {
export class createdByPureFromMeetingRoom extends BaseRedisStringCache<string> {
key="permission:meeting:room:{0}:createdByPure"
constructor(private meetingRoomId:string) {
super()
this.objRedis=new RedisStringKey(StringUtil.format(this.key,meetingRoomId),cacheRedisType<string>().String,1000*10);
}
override async getValue(): Promise<string> {
let value:string;
let mysql=getMysqlInstance()
let exists=await this.objRedis.exists()
if(exists) {
value=await this.objRedis.get()
} else {
let sql=generateLeftJoin2Sql({
model:meetingRoomModel
},{
model:organizationUserModel,
expression:{
id:{
model:meetingRoomModel,
field:"created_by"
}
}
},{
model:userModel,
columns:["id"],
expression:{
id:{
model:organizationUserModel,
field:"user_id"
}
}
},{
id:{
model:meetingRoomModel,
value:this.meetingRoomId
}
})
let ret=await mysql.executeOne(sql)
if(ret) {
value=ret.id
await this.objRedis.set(value)
} else {
throw Err.Meeting.roomNotFound
}
}
return value;
}
}
}
}
}

View File

@ -101,6 +101,9 @@ export class RedisHashKey extends RedisBaseKey<string> {
},ttl?:number){
await this.redis.hSet(this.name,value,ttl??this.ttl);
}
async delField(...fields:string[]) {
await this.redis.hDel(this.name,...fields)
}
async getAll():Promise<object> {
let ret=await this.redis.hGetAll(this.name)
return ret;

13
code/server/common/cache/keys/file.ts vendored Normal file
View File

@ -0,0 +1,13 @@
import {ECommon_Services} from '../../../../common/types';
import {cacheRedisType} from "../../types/cache";
import StringUtil from "../../util/string";
import {RedisStringKey} from './base';
export namespace REDIS_FILE {
let FILE_PATH_KEY=`${ECommon_Services.File}:file:{0}:path`
export function filePath(fileId:string)
{
let obj=new RedisStringKey(StringUtil.format(FILE_PATH_KEY,fileId),cacheRedisType<string>().String,3600)
return obj
}
}

View File

@ -0,0 +1,13 @@
import {ECommon_Services} from "../../../../common/types";
import {RedisHashKey} from "./base";
import StringUtil from "../../util/string";
export namespace REDIS_MEETING {
let ROOM_INFO_KEY=`${ECommon_Services.Meeting}:room:{0}:info`
export function roomInfo(meetingRoomId:string)
{
let obj=new RedisHashKey(StringUtil.format(ROOM_INFO_KEY,meetingRoomId),-1)
return obj
}
}

View File

@ -0,0 +1,26 @@
import {ECommon_Services} from "../../../../common/types";
import {RedisStringKey} from "./base";
import StringUtil from "../../util/string";
import {cacheRedisType} from "../../types/cache";
export namespace REDIS_ORGANIZATION {
let USER_STATUS_KEY=`${ECommon_Services.Cooperation}:user:{0}:status`
let USER_PRE_STATUS_KEY=`${ECommon_Services.Cooperation}:user:{0}:pre_status`
let USER_MEETING_KEY=`${ECommon_Services.Cooperation}:user:{0}:meeting`
export function status(organizationUserId:string)
{
let obj=new RedisStringKey(StringUtil.format(USER_STATUS_KEY,organizationUserId),cacheRedisType<number>().Number,-1)
return obj
}
export function preStatus(organizationUserId:string)
{
let obj=new RedisStringKey(StringUtil.format(USER_PRE_STATUS_KEY,organizationUserId),cacheRedisType<number>().Number,-1)
return obj
}
export function meetingId(organizationUserId:string)
{
let obj=new RedisStringKey(StringUtil.format(USER_MEETING_KEY,organizationUserId),cacheRedisType<string>().String,-1)
return obj
}
}

View File

@ -5,11 +5,20 @@ import {ICommon_Model_User} from './../../../../common/model/user';
import {RedisStringKey} from './base';
import {IServer_Common_RPC_User_CheckSession_Organization} from "../../../user/types/config";
export interface ICommon_Register_Cache_Info {
username?:string,
password?:string,
userId?:string
sendTime:number,
code:string
}
export namespace REDIS_USER {
let USER_TOKEN_KEY=`${ECommon_Services.User}:user:{0}:token`
let USER_INFO_KEY=`${ECommon_Services.User}:user:{0}:info`
let ADMIN_INFO_KEY=`${ECommon_Services.User}:admin:{0}:info`
let USER_ORGANIZATION_KEY=`${ECommon_Services.User}:user:{0}:organization`
let USER_REGISTER_KEY=`${ECommon_Services.User}:username:{0}:register`
let USER_RESET_CODE_KEY=`${ECommon_Services.User}:user:{0}:reset`
export function token(userId:string)
{
let obj=new RedisStringKey(StringUtil.format(USER_TOKEN_KEY,userId),cacheRedisType<string>().String,3600)
@ -30,4 +39,15 @@ export namespace REDIS_USER {
let obj=new RedisStringKey(StringUtil.format(USER_ORGANIZATION_KEY,userId),cacheRedisType<IServer_Common_RPC_User_CheckSession_Organization>().Object,-1)
return obj
}
export function registerCacheInfo(username:string)
{
let obj=new RedisStringKey(StringUtil.format(USER_REGISTER_KEY,username),cacheRedisType<ICommon_Register_Cache_Info>().Object,600)
return obj
}
export function resetCode(userId:string)
{
let obj=new RedisStringKey(StringUtil.format(USER_RESET_CODE_KEY,userId),cacheRedisType<ICommon_Register_Cache_Info>().Object,600)
return obj
}
}

View File

@ -181,6 +181,11 @@ export class Redis {
await this.setTTL(key,ttl);
}
}
async hDel(key:string,...fields:string[]){
await this.redis.hdel(key,...fields)
}
async hGetAll(key:string) :Promise<object>{
let exist=await this.exists(key)
if(exist) {

View File

@ -1,5 +1,5 @@
import Application from "../app/app";
import {IServer_Common_Config_Mysql, IServer_Common_Config_Redis} from "../types/config";
import {IServer_Common_Config_Mail, IServer_Common_Config_Mysql, IServer_Common_Config_Redis} from "../types/config";
class Config {
constructor() {
@ -24,6 +24,9 @@ class Config {
get mqUri():string {
return Application.privateConfig.mq
}
get mailInfo():IServer_Common_Config_Mail {
return Application.privateConfig.mail
}
}
var g_config:InstanceType<typeof Config>
export function getConfigInstance() {

View File

@ -1,14 +1,15 @@
import {BaseModel} from '../../../common/model/base';
import {Err} from '../../../common/status/error';
import {EServer_Common_Event_Types} from '../event/types';
import {Mapper} from './mapper';
import {emitServiceEvent} from "../event/event";
import {IServer_Common_Event_Types} from "../event/types";
let imagefields=[
"photo",
"image",
"img",
"icon"
"icon",
"file_id"
]
export abstract class Entity<T extends BaseModel,M extends Mapper<T>> {
protected item:T["model"];
@ -54,7 +55,7 @@ export abstract class Entity<T extends BaseModel,M extends Mapper<T>> {
await this.loadItem();
imagefields.forEach(item=>{
if(this.item[item]) {
emitServiceEvent(EServer_Common_Event_Types.File.REF,this.item[item])
emitServiceEvent("fileRef",this.item[item])
}
})
return this.item;
@ -67,25 +68,25 @@ export abstract class Entity<T extends BaseModel,M extends Mapper<T>> {
await this.mapper.update(this.item)
imagefields.forEach(item=>{
if(!ret[item] && this.item[item]) {
emitServiceEvent(EServer_Common_Event_Types.File.REF,this.item[item])
emitServiceEvent("fileRef",this.item[item])
} else if(ret[item] && ret[item]!=this.item[item]) {
if(this.item[item]) {
emitServiceEvent(EServer_Common_Event_Types.File.REF,this.item[item])
emitServiceEvent("fileRef",this.item[item])
}
emitServiceEvent(EServer_Common_Event_Types.File.UNREF,ret[item])
emitServiceEvent("fileUnref",ret[item])
}
})
await this.loadItem();
return this.item;
}
async delete(eventPublish?:EServer_Common_Event_Types.Types){
async delete(eventPublish?:keyof IServer_Common_Event_Types){
await this.mapper.delete(this.item.id);
if(eventPublish) {
emitServiceEvent(eventPublish,this.item.id);
}
imagefields.forEach(item=>{
if(this.item[item]) {
emitServiceEvent(EServer_Common_Event_Types.File.UNREF,this.item[item])
emitServiceEvent("fileUnref",this.item[item])
}
})
@ -141,6 +142,27 @@ export abstract class Entity<T extends BaseModel,M extends Mapper<T>> {
return null;
}
}
static async getItemsByExp<Type>(this:{new():Type},exp:{
[param in keyof GET<Type>["model"]]?:GET<Type>["model"][param]
}):Promise<Type[]>{
if(!exp) {
return null
}
let user = new this() as any;
let arr = await user.mapper.getItemsByExp(exp);
if(arr.length>0) {
let ret=[]
for(let obj of arr) {
let objService=new this() as any
objService.setItem(obj)
ret.push(objService)
}
return ret;
} else {
return [];
}
}
}
type GET<T>=T extends Entity<infer T1,Mapper<infer T1>>?T1:never

View File

@ -37,6 +37,17 @@ export abstract class Mapper<T extends BaseModel> {
let ret=await mysql.executeOne(generateQuerySql(this.model,[],exp))
return ret
}
async getItemsByExp(exp:{
[param in keyof T["model"]]:T["model"][param]
}):Promise<T["model"][]> {
if(!exp) {
throw Err.Common.itemNotFound
}
var mysql=getMysqlInstance();
let ret=await mysql.execute(generateQuerySql(this.model,[],exp))
return ret
}
async updateConfig(info:T["model"]){}
async update(data:T["model"]):Promise<void> {
let info:T["model"]={}

View File

@ -1,4 +1,4 @@
import {EServer_Common_Event_Types} from "./types"
import {IServer_Common_Event_Types} from "./types"
var g_events=<{
[param:string]: ((...args: any[]) =>any)[]
@ -10,7 +10,7 @@ export function getEventsFunc(){
return g_events
}
export function DEventListener(eventName: EServer_Common_Event_Types.Types) {
export function DEventListener(eventName: keyof IServer_Common_Event_Types) {
return function (target, propertyKey: string, desc: PropertyDescriptor) {
let handle = desc.value.bind(target)
if(!g_events[eventName]) {
@ -20,7 +20,7 @@ export function DEventListener(eventName: EServer_Common_Event_Types.Types) {
}
}
export function emitServiceEvent(event: EServer_Common_Event_Types.Types, ...obj: any) {
export function emitServiceEvent<T extends keyof IServer_Common_Event_Types>(event: T, ...obj: Parameters<IServer_Common_Event_Types[T]>) {
let objFunc = getEventsFunc()
for (let key in objFunc) {
if (key == event) {

View File

@ -1,30 +1,20 @@
export namespace EServer_Common_Event_Types {
export type Types= User|Project|Team|File|IssueType|Auth
export enum User {
DELETE="userDelete" //userId:string
}
export enum Project {
DELETE="projectDelete" //projectId:string
}
export enum Team {
DELETE="teamDelete" //teamId:string
}
export enum File {
REF="fileRef",
UNREF="fileUnref" //fileId:string
}
export enum IssueType {
DELETE //issueTypeId:string
}
export enum Auth {
ORGANIZATION_USER_ADD="authOrganizationUserAdd", //organizationId:string,user:string
ORGANIZATION_USER_EDIT="authOrganizationUserEdit", //organizationId:string,user:string
ORGANIZATION_USER_DELETE="authOrganizationUserDelete", //organizationId:string,user:string
PROJECT_MEMBER_ADD="authProjectMemberAdd", //projectId:string,memberId:string,
PROJECT_MEMBER_EDIT="authProjectMemberEdit", //projectId:string,memberId:string,
PROJECT_MEMBER_DELETE="authProjectMemberDelete",//projectId:string,memberId:string,
TEAM_USER_ADD="authTeamUserAdd",//teamId:string,userId:string,
TEAM_USER_EDIT="authTeamUserEdit",//teamId:string,userId:string,
TEAM_USER_DELETE="authTeamUserDelete"//teamId:string,userId:string,
}
import {ECommon_User_Online_Status} from "../../../common/types";
export interface IServer_Common_Event_Types {
userDelete:(userId:string)=>void
projectDelete:(projectId:string)=>void
teamDelete:(teamId:string)=>void
fileRef:(fileId:string)=>void
fileUnref:(fileId:string)=>void
issueTypeDelete:(issueTypeId:string)=>void
organizationUserAdd:(organizationId:string,user:string)=>void
organizationUserEdit:(organizationId:string,user:string)=>void
organizationUserDelete:(organizationId:string,user:string)=>void
organizationUserStatusChange:(organizationId:string,organizationUserId:string,status:ECommon_User_Online_Status)=>void
projectMemberAdd:(projectId:string,memberId:string)=>void
projectMemberEdit:(projectId:string,memberId:string)=>void
projectMemberDelete:(projectId:string,memberId:string)=>void
teamUserAdd:(teamId:string,organizationUserId:string)=>void
teamUserEdit:(teamId:string,organizationUserId:string)=>void
teamUserDelete:(teamId:string,organizationUserId:string)=>void
}

View File

@ -0,0 +1,33 @@
import * as nodemailer from "nodemailer"
import {IServer_Common_Config_Mail} from "../types/config";
let g_mail:Mail
export class Mail {
private transporter:nodemailer.Transporter
private config:IServer_Common_Config_Mail
constructor(config:IServer_Common_Config_Mail) {
g_mail=this;
this.config=config
this.transporter=nodemailer.createTransport({
host:config.host,
port:config.port,
secure:true,
auth:{
user:config.user,
pass:config.pass
}
})
}
async send(to:string,subject:string,content:string) {
await this.transporter.sendMail({
from:this.config.user,
to,
subject,
html:content
})
}
}
export function getMailInstance() {
return g_mail
}

View File

@ -0,0 +1,611 @@
import * as SocketIO from "socket.io";
import {
Meeting_ClientToServerEvents,
Meeting_Data,
Meeting_InterServerEvents,
Meeting_ServerToClientEvents,
MeetingConfig,
PeerInfoItem
} from "./type";
import * as mediaSoup from "mediasoup";
import {Worker} from "mediasoup/node/lib/Worker";
import * as os from "os"
import {Consumer} from "mediasoup/node/lib/Consumer";
import {Producer} from "mediasoup/node/lib/Producer";
import {WebRtcTransport} from "mediasoup/node/lib/WebRtcTransport";
import {MediaKind, RtpCapabilities} from "mediasoup/node/lib/RtpParameters";
import {AudioLevelObserver} from "mediasoup/node/lib/AudioLevelObserver";
export class MeetingServer {
private io:SocketIO.Server<Meeting_ClientToServerEvents,Meeting_ServerToClientEvents,Meeting_InterServerEvents,Meeting_Data>
private config:MeetingConfig
private roomMap=new Map<string,{
roomId:string,
roomName:string,
router:mediaSoup.types.Router,
peerList:Set<string>,
audioLevelObserver:AudioLevelObserver
}>()
private workList:Worker[]=new Array(os.cpus().length)
private peerInfoMap=new Map<string,PeerInfoItem>()
private workerIndex=0
onJoinRoom:(roomId:string,extraData:any,socketData:any,socketId:string)=>Promise<{
roomName:string,
businessId:string,
error?:string
}>
onJoinedRoom:(roomId:string,businessId:string,socketId:string)=>void
onLeaveRoom:(type:"self"|"kick"|"end",roomId:string,businessId:string,socketId:string)=>Promise<void>
onLeavedRoom:(type:"self"|"kick"|"end",roomId:string,businessId:string,socketId:string)=>void
onHandleOperation:(type:"pause"|"resume"|"kick"|"end",roomId:string,fromBusinessId:string,toBusinessId?:string,kind?:MediaKind)=>Promise<boolean>
constructor(io:any,config:MeetingConfig) {
this.io=io
this.config=config
this.io.addListener("connection",(socket:SocketIO.Socket<Meeting_ClientToServerEvents,Meeting_ServerToClientEvents,Meeting_InterServerEvents,Meeting_Data>)=>{
socket.on("joinRoom",async (roomId, extraData,callback) => {
let roomName="",businessId="";
if(this.onJoinRoom) {
let ret=await this.onJoinRoom(roomId,extraData,socket.data,socket.id)
if(ret.error) {
callback(null,ret.error)
return
} else if(!ret.businessId) {
callback(null,"businessId can't be empty")
return
}else {
roomName=ret.roomName
businessId=ret.businessId
}
}
await this.createRoom(roomId,roomName,socket.id,businessId)
callback({
roomId,
roomName
})
if(this.onJoinedRoom) {
this.onJoinedRoom(roomId,businessId,socket.id)
}
})
socket.on("leaveRoom",async (callback) => {
let obj=this.peerInfoMap.get(socket.id)
if(!obj) {
callback()
return
}
if(this.onLeaveRoom) {
await this.onLeaveRoom("self",obj.roomId,obj.businessId,socket.id)
}
this.leaveRoom(socket.id)
callback()
if(this.onLeavedRoom) {
this.onLeavedRoom("self",obj.roomId,obj.businessId,socket.id)
}
})
socket.on("getRouterRtpCapabilities",(callback)=>{
let objPeer=this.peerInfoMap.get(socket.id)
if(!objPeer) {
callback(null)
return
}
let obj=this.roomMap.get(objPeer.roomId)
callback(obj.router.rtpCapabilities)
})
socket.on("createProducerTransport",async (callback)=>{
let objPeer=this.peerInfoMap.get(socket.id)
if(!objPeer) {
callback(null)
return
}
const {transport,params}=await this.createRoomTransport(objPeer.roomId)
this.addTransport(transport,objPeer.roomId,socket.id,false)
callback(params)
})
socket.on("createConsumerTransport",async (callback)=>{
let objPeer=this.peerInfoMap.get(socket.id)
if(!objPeer) {
callback(null)
return
}
const {transport,params}=await this.createRoomTransport(objPeer.roomId)
this.addTransport(transport,objPeer.roomId,socket.id,true)
callback(params)
})
socket.on('connectProducerTransport', async (data, callback) => {
try {
console.log("Connecting Producer Transport")
let obj=await this.getTransport(socket.id,false)
if(!obj) {
callback()
return
}
await obj.connect({dtlsParameters: data.dtlsParameters});
} catch (err) {
console.error(err)
}
callback();
});
socket.on('connectConsumerTransport', async (data, callback) => {
try {
console.log("Connecting Consumer Transport")
const consumerTransport = await this.getTransport(socket.id,true)
if(!consumerTransport) {
callback()
return
}
await consumerTransport.connect({ dtlsParameters: data.dtlsParameters });
} catch (err) {
console.error(err)
}
callback();
});
socket.on('produce', async (data, callback) => {
const {kind, rtpParameters} = data;
console.log("Starting the producer")
let producer = await this.getTransport(socket.id,false).produce({ kind, rtpParameters });
producer.on('transportclose', () => {
console.log('transport for this producer closed ')
producer.close()
})
let objPeer=this.peerInfoMap.get(socket.id)
let peerList=this.roomMap.get(objPeer.roomId).peerList
callback({ id: producer.id, producersExist: peerList.size>1 ? true : false });
this.addProducer(producer, socket.id)
if(kind==="audio") {
let audioLevelObserver=this.roomMap.get(objPeer.roomId).audioLevelObserver
await audioLevelObserver.addProducer({
producerId:producer.id
})
}
io.to(objPeer.roomId).except(socket.id).emit('newProducer', producer.id )
});
socket.on('consume', async (data, callback) => {
console.log("Consume call on the server side, data is below")
let obj=await this.createConsumer(data.rtpCapabilities, data.remoteProducerId, data.transportId, socket.id)
let objPeer=this.getPeerFromProducerId(socket.id,data.remoteProducerId)
callback(Object.assign({},obj,{
businessId:objPeer.businessId
}));
});
socket.on('resume', async ( consumerId,callback) => {
let obj=this.peerInfoMap.get(socket.id)
for(let consumer of obj.receive.consumer) {
if(consumer.id===consumerId) {
await consumer.resume()
break
}
}
callback();
});
socket.on('getProducers', callback => {
let producerList:string[] = []
let objPeer=this.peerInfoMap.get(socket.id)
if(!objPeer) {
callback([])
return
}
let objRoom=this.roomMap.get(objPeer.roomId)
for(let socketId of objRoom.peerList) {
if(socketId!=socket.id) {
let objPeer=this.peerInfoMap.get(socketId)
producerList = [...producerList,...objPeer.send.producer.map(item=>item.id)]
}
}
callback(producerList)
});
socket.on("pauseSelf",async (kind, callback) => {
let objPeer=this.peerInfoMap.get(socket.id)
for(let producer of objPeer.send.producer) {
if(producer.kind===kind) {
await producer.pause()
}
}
callback()
})
socket.on("resumeSelf",async (kind, callback) => {
let objPeer=this.peerInfoMap.get(socket.id)
for(let producer of objPeer.send.producer) {
if(producer.kind===kind) {
await producer.resume()
}
}
callback()
})
socket.on("pauseOther",async (kind, businessId, callback) => {
if(this.onHandleOperation) {
let objPeer=this.peerInfoMap.get(socket.id)
let ret=await this.onHandleOperation("pause",objPeer.roomId,objPeer.businessId,businessId,kind);
if(!ret) {
callback(false)
return
}
}
let obj=this.getPeerFromBusinessId(socket.id,businessId)
if(obj) {
for(let producer of obj.send.producer) {
if(producer.kind===kind) {
await producer.pause()
}
}
callback(true)
} else {
callback(false)
}
})
socket.on("resumeOther",async (kind, businessId, callback) => {
if(this.onHandleOperation) {
let objPeer=this.peerInfoMap.get(socket.id)
let ret=await this.onHandleOperation("resume",objPeer.roomId,objPeer.businessId,businessId,kind);
if(!ret) {
callback(false)
return
}
}
let obj=this.getPeerFromBusinessId(socket.id,businessId)
if(obj) {
for(let producer of obj.send.producer) {
if(producer.kind===kind) {
await producer.resume()
}
}
callback(true)
} else {
callback(false)
}
})
socket.on("kick",async (businessId, callback) => {
if(this.onHandleOperation) {
let objPeer=this.peerInfoMap.get(socket.id)
let ret=await this.onHandleOperation("kick",objPeer.roomId,objPeer.businessId,businessId,null);
if(!ret) {
callback(false)
return
}
}
let obj=this.getPeerFromBusinessId(socket.id,businessId)
if(obj) {
if(this.onLeaveRoom) {
await this.onLeaveRoom("kick",obj.roomId,obj.businessId,obj.socketId)
}
this.leaveRoom(obj.socketId)
callback(true)
if(this.onLeavedRoom) {
this.onLeavedRoom("kick",obj.roomId,obj.businessId,obj.socketId)
}
this.io.in(obj.socketId).emit("kick")
} else {
callback(false)
}
})
socket.on("end",async callback => {
if(this.onHandleOperation) {
let objPeer=this.peerInfoMap.get(socket.id)
let ret=await this.onHandleOperation("end",objPeer.roomId,objPeer.businessId,null,null);
if(!ret) {
callback(false)
return
}
}
let objPeer=this.peerInfoMap.get(socket.id)
if(objPeer) {
let objRoom=this.roomMap.get(objPeer.roomId)
for(let id of objRoom.peerList) {
let obj=this.peerInfoMap.get(id)
if(obj) {
if(this.onLeaveRoom) {
await this.onLeaveRoom("end",obj.roomId,obj.businessId,obj.socketId)
}
this.leaveRoom(obj.socketId)
if(this.onLeavedRoom) {
this.onLeavedRoom("end",obj.roomId,obj.businessId,obj.socketId)
}
this.io.in(obj.socketId).emit("kick")
}
}
}
callback(true)
})
socket.on("states",callback => {
let objPeer=this.peerInfoMap.get(socket.id)
if(objPeer) {
let objRoom=this.roomMap.get(objPeer.roomId)
if(objRoom) {
let arr:Parameters<typeof callback>[0]=[]
for(let socketId of objRoom.peerList) {
let obj=this.peerInfoMap.get(socketId)
if(obj) {
let kinds={}
obj.send.producer.forEach(item=>{
kinds[item.kind]=!item.paused
})
arr=[...arr,{
businessId:obj.businessId,
kinds:kinds
}]
}
}
callback(arr)
} else {
callback([])
}
} else {
callback([])
}
})
socket.addListener("disconnect",async (reason)=>{
let obj=this.peerInfoMap.get(socket.id)
if(obj && this.onLeaveRoom) {
await this.onLeaveRoom("self",obj.roomId,obj.businessId,socket.id)
}
this.leaveRoom(socket.id)
if(obj && this.onLeavedRoom) {
this.onLeavedRoom("self",obj.roomId,obj.businessId,socket.id)
}
})
})
}
async start() {
for(let i=0;i<this.workList.length;i++) {
let worker=await mediaSoup.createWorker(this.config.worker)
worker.on("died",args => {
console.log(args)
})
this.workList[i]=worker
}
}
private getPeerFromProducerId(socketId:string,producerId:string,isAll:boolean=false) {
let objPeer=this.peerInfoMap.get(socketId)
let objRoom=this.roomMap.get(objPeer.roomId)
for(let id of objRoom.peerList) {
if(isAll || id!==socketId) {
let obj=this.peerInfoMap.get(id)
for(let produce of obj.send.producer) {
if(produce.id===producerId) {
return obj
}
}
}
}
}
private getPeerFromProducerIdAndRoomId(roomId:string,producerId:string) {
let objRoom=this.roomMap.get(roomId)
for(let id of objRoom.peerList) {
let obj=this.peerInfoMap.get(id)
for(let produce of obj.send.producer) {
if(produce.id===producerId) {
return obj
}
}
}
}
private getPeerFromBusinessId(socketId:string,businessId:string,isAll:boolean=false) {
let objPeer=this.peerInfoMap.get(socketId)
let objRoom=this.roomMap.get(objPeer.roomId)
for(let id of objRoom.peerList) {
if(isAll || id!==socketId) {
let obj=this.peerInfoMap.get(id)
if(obj.businessId===businessId) {
return obj;
}
}
}
}
private getWorker() {
let obj=this.workList[this.workerIndex]
this.workerIndex++;
if(this.workerIndex>=this.workList.length) {
this.workerIndex=0
}
return obj;
}
private async createRoom(roomId:string,roomName:string,socketId:string,businessId:string) {
let objRoom=this.roomMap.get(roomId)
if(!objRoom) {
let worker=this.getWorker()
let mediaSoupRouter=await worker.createRouter({
mediaCodecs:this.config.codecs
})
let audioLevelObserver=await mediaSoupRouter.createAudioLevelObserver({
threshold:-55
})
audioLevelObserver.on("volumes",args => {
let produce=args[0]?.producer
if(produce) {
let objPeer=this.getPeerFromProducerIdAndRoomId(roomId,produce.id)
if(objPeer) {
let objRoom=this.roomMap.get(roomId)
for(let socketId of objRoom.peerList) {
this.io.in(socketId).emit("speaker",objPeer.businessId)
}
}
}
})
this.roomMap.set(roomId,{
roomId,
peerList:new Set([socketId]),
roomName,
router:mediaSoupRouter,
audioLevelObserver:audioLevelObserver
})
} else {
objRoom.peerList.add(socketId)
}
this.peerInfoMap.set(socketId,{
businessId,
roomId:roomId,
socketId:socketId,
send:{
transport:null,
producer:[]
},
receive:{
transport:null,
consumer:[]
}
})
}
private async createRoomTransport(roomId:string) {
let obj=this.roomMap.get(roomId)
if(!obj) {
return;
}
const transport=await obj.router.createWebRtcTransport(this.config.webRtcTransport)
transport.on("dtlsstatechange",state => {
if(state=="closed") {
transport.close()
}
})
return {
transport,
params: {
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters,
sctpParameters: transport.sctpParameters,
},
}
}
private addConsumer(consumer:Consumer, socketId:string){
let obj=this.peerInfoMap.get(socketId)
if(obj) {
obj.receive.consumer.push(consumer)
}
}
private addProducer(producer:Producer,socketId:string){
let obj=this.peerInfoMap.get(socketId)
if(obj) {
obj.send.producer.push(producer)
}
}
private addTransport(transport:WebRtcTransport, roomId:string, socketId:string,isConsumer:boolean){
let obj=this.peerInfoMap.get(socketId)
if(obj) {
if(isConsumer) {
obj.receive.transport=transport
} else {
obj.send.transport=transport
}
}
}
private getProducer(socketId:string, type:MediaKind){
const [producerTransport] = this.peerInfoMap.get(socketId).send.producer.filter(item=>item.kind===type)
return producerTransport
}
private getTransport(socketId:string,isConsumer:boolean){
const producerTransport = this.peerInfoMap.get(socketId)
return isConsumer?producerTransport.receive.transport:producerTransport.send.transport
}
private async createConsumer( rtpCapabilities:RtpCapabilities, remoteProducerId:string, serverConsumerTransportId:string, socketId:string) {
const objTransport = this.peerInfoMap.get(socketId)
const roomId = objTransport.roomId
const router=this.roomMap.get(roomId).router
console.log("Creating consumer for remote producerId = " + remoteProducerId)
const consumerTransport=objTransport.receive.transport
if (!router.canConsume(
{
producerId: remoteProducerId,
rtpCapabilities,
})
) {
console.error('can not consume');
return;
}
try {
let consumer = await consumerTransport.consume({
producerId: remoteProducerId,
rtpCapabilities,
paused:true
});
let objPeer=this.getPeerFromProducerId(socketId,remoteProducerId)
consumer.on('transportclose', () => {
console.log('transport close from consumer')
})
consumer.on('producerclose', async () => {
console.log('producer of consumer closed')
this.io.in(roomId).emit('producerClosed',remoteProducerId,consumer.kind,objPeer.businessId)
objTransport.receive.consumer = objTransport.receive.consumer.filter(transportData => transportData.id !== consumerTransport.id)
consumer.close()
if(consumer.kind==="audio") {
let objRoom=this.roomMap.get(objPeer.roomId)
try {
await objRoom.audioLevelObserver.removeProducer({
producerId:consumer.producerId
})
} catch {
console.log(`${consumer.producerId} not found`)
}
}
})
consumer.on("producerpause",() => {
this.io.in(roomId).emit('producerPause',remoteProducerId,consumer.kind,objPeer.businessId)
})
consumer.on("producerresume",() => {
this.io.in(roomId).emit('producerResume',remoteProducerId,consumer.kind,objPeer.businessId)
})
this.addConsumer(consumer, socketId)
if (consumer.type === 'simulcast') {
await consumer.setPreferredLayers({ spatialLayer: 2, temporalLayer: 2 });
}
return {
producerId: remoteProducerId,
id: consumer.id,
kind: consumer.kind,
rtpParameters: consumer.rtpParameters,
type: consumer.type,
producerPaused: consumer.producerPaused
};
} catch (error) {
console.error('consume failed', error);
return;
}
}
private leaveRoom(socketId:string) {
let obj=this.peerInfoMap.get(socketId)
if(obj) {
if(obj.send.transport) {
obj.send.transport.close()
}
obj.send.producer.forEach(item=>item.close())
if(obj.receive.transport) {
obj.receive.transport.close()
}
obj.receive.consumer.forEach(item=>item.close())
this.peerInfoMap.delete(socketId)
let objRoom=this.roomMap.get(obj.roomId)
if(objRoom) {
objRoom.peerList.delete(socketId)
if(objRoom.peerList.size==0){
objRoom.router.close()
objRoom.audioLevelObserver.close()
this.roomMap.delete(obj.roomId)
}
}
}
}
}

View File

@ -0,0 +1,122 @@
import type {MediaKind, RtpCapabilities, RtpCodecCapability, RtpParameters} from "mediasoup/node/lib/RtpParameters";
import {DtlsParameters, IceCandidate, IceParameters, WebRtcTransport} from "mediasoup/node/lib/WebRtcTransport";
import {Producer} from "mediasoup/node/lib/Producer";
import {Consumer, ConsumerType} from "mediasoup/node/lib/Consumer";
import {WorkerLogLevel, WorkerLogTag} from "mediasoup/node/lib/Worker";
import {SctpParameters} from "mediasoup/node/lib/SctpParameters";
export interface Meeting_ServerToClientEvents {
newProducer:(producerId:string,kind:MediaKind,businessId:string)=>void
producerClosed:(producerId:string,kind:MediaKind,businessId:string)=>void
producerPause:(producerId:string,kind:MediaKind,businessId:string)=>void
producerResume:(producerId:string,kind:MediaKind,businessId:string)=>void
kick:()=>void
speaker:(businessId:string)=>void
}
export interface Meeting_ClientToServerEvents {
joinRoom:(roomId:string,extraData:any,callback:(info:{
roomId:string,
roomName:string
},msg?:string)=>void)=>void
leaveRoom:(callback:()=>void)=>void
getRouterRtpCapabilities:(callback:(capabilities:RtpCapabilities)=>void)=>void
createProducerTransport:(callback:(param:{
id: string,
iceParameters: IceParameters,
iceCandidates: IceCandidate[],
dtlsParameters: DtlsParameters,
sctpParameters: SctpParameters,
})=>void)=>void
createConsumerTransport:(callback:(param:{
id: string,
iceParameters: IceParameters,
iceCandidates: IceCandidate[],
dtlsParameters: DtlsParameters,
sctpParameters: SctpParameters,
})=>void)=>void
connectProducerTransport:(data:{
dtlsParameters:DtlsParameters
},callback:()=>void)=>void
connectConsumerTransport:(data:{
dtlsParameters:DtlsParameters
},callback:()=>void)=>void
produce:(data:{
kind:MediaKind,
rtpParameters:RtpParameters
},callback:(producerInfo:{
id:string,
producersExist:boolean
})=>void)=>void
consume:(data:{
rtpCapabilities:RtpCapabilities,
remoteProducerId:string,
transportId:string
},callback:(data:{
businessId:string,
producerId: string,
id: string,
kind: MediaKind,
rtpParameters: RtpParameters,
type: ConsumerType,
producerPaused: boolean
})=>void)=>void
getProducers:(callback:(producerList:string[])=>void)=>void
resume:(consumerId:string,callback:()=>void)=>void
pauseSelf:(kind:MediaKind,callback:()=>void)=>void
resumeSelf:(kind:MediaKind,callback:()=>void)=>void
pauseOther:(kind:MediaKind,businessId:string,callback:(success:boolean)=>void)=>void
resumeOther:(kind:MediaKind,businessId:string,callback:(success:boolean)=>void)=>void
kick:(businessId:string,callback:(success:boolean)=>void)=>void
end:(callback:(success:boolean)=>void)=>void
states:(callback:(list:{
businessId:string,
kinds:{
[kind:string]:boolean
}
}[])=>void)=>void
}
export interface Meeting_InterServerEvents {
}
export interface Meeting_Data {
}
export type MeetingConfig = {
worker:{
logLevel:WorkerLogLevel,
logTags:WorkerLogTag[],
rtcMinPort:number,
rtcMaxPort:number
},
codecs:RtpCodecCapability[],
webRtcTransport:{
listenIps:{
ip: string,
announcedIp: string,
}[],
enableUdp:boolean,
enableTcp:boolean,
preferUdp:boolean,
enableSctp:boolean,
initialAvailableOutgoingBitrate : number,
maxSctpMessageSize : number,
}
}
export type PeerInfoItem={
businessId:string
socketId:string,
send:{
transport:WebRtcTransport,
producer:Producer[]
},
roomId:string,
receive:{
transport:WebRtcTransport,
consumer:Consumer[]
}
}

View File

@ -11,6 +11,7 @@ import {
} from "../../../common/socket/types";
import {instrument} from "@socket.io/admin-ui";
import userRpcApi from "../../user/rpc/user";
import {REDIS_USER} from "../cache/keys/user";
let g_emitter:Emitter<ICommon_Socket_ServerToClientEvents>
export function getSocketEmitterInstance() {
@ -65,11 +66,13 @@ export class SocketIO {
} else {
socket.data.organizationUserId=objOrganization.organizationUserId
socket.data.userId=objHandshake.userId
socket.data.organizationId=objOrganization.organizationId
next()
}
})
io.on("connection",(socket)=>{
io.on("connection",async (socket)=>{
socket.join(socket.data.organizationUserId)
socket.join(socket.data.organizationId)
if(callback) {
callback(socket)
}

View File

@ -17,6 +17,13 @@ export interface IServer_Common_Config_Mysql {
password:string
}
export interface IServer_Common_Config_Mail {
"host": string,
"port": number,
"user":string,
"pass":string
}
export interface IServer_Common_Config {
services:{
[name:string]:any

View File

@ -104,4 +104,18 @@ export default class CommonUtil {
setTimeout(resolve,ms)
})
}
static generateRandomNumbers(len:number) {
let result="";
for(let i=0;i<6;i++){
let intValue=Math.floor(Math.random()*10);
result=result+intValue;
}
return result
}
static md5(password:string) {
let md5 = crypto.createHash('md5');
return md5.update(password).digest('hex');
}
}

View File

@ -4,10 +4,12 @@ import {getConfigInstance} from '../config/config';
import {IServer_Common_Config_Base} from "../types/config";
import {Log} from './../log/log';
import {BaseMq} from "../mq/mq";
import {Mail} from "../mail/mail";
export async function init<T extends IServer_Common_Config_Base>() {
new Log()
new Redis(getConfigInstance().redisInfo)
new Mysql(getConfigInstance().mysqlInfo);
new Mail(getConfigInstance().mailInfo)
await BaseMq.initChannel(getConfigInstance().mqUri)
}

View File

@ -2,11 +2,15 @@ import * as intFormat from "biguint-format";
import * as FlakeId from 'flake-idgen';
import {BaseModel} from '../../../common/model/base';
import * as moment from "moment";
import CommonUtil from "./common";
import Mysql from "../db/mysql";
const uid = new FlakeId();
type EXPR={
[key:string]:EXPRVALUE
[key:string]:EXPRVALUE|{
[key:string]:EXPRVALUE
}
}
type EXPRVALUEEXP ={
value?:string|number|boolean|string[]|[number,number]|string|Date|{
@ -111,23 +115,51 @@ function generateExp(objExpr?:EXPR,exprMode?:"and"|"or"):string{
let expr="",arrExpr=[]
if(objExpr) {
for(let key in objExpr) {
let value=objExpr[key]
if(typeof(value)!="object") {
let val=typeof(value)=="number"?value:typeof(value)=="boolean"?(value?1:0):("'"+value+"'")
arrExpr.push(`${key}=${val}`)
} else if (typeof(value)=="object" && (value instanceof Date)){
arrExpr.push(`${key}=${value.getTime()}`)
} else if(value===null) {
arrExpr.push(`isnull(${key})`)
} else if (isEXPRVALUEEXP(value)) {
handleExp(key,value,arrExpr)
}else {
let arr=[];
for(let obj of value.values) {
handleExp(key,obj,arr)
if(key.startsWith("$")) {
let value=objExpr[key] as {
[param:string]:EXPRVALUE
}
let arr=[]
for(let k in value){
let v=value[k];
if(typeof(v)!="object") {
let val=typeof(v)=="number"?v:typeof(v)=="boolean"?(v?1:0):("'"+v+"'")
arr.push(`${k}=${val}`)
} else if (typeof(v)=="object" && (v instanceof Date)){
arr.push(`${k}=${v.getTime()}`)
} else if(v===null) {
arr.push(`isnull(${k})`)
} else if (isEXPRVALUEEXP(v)) {
handleExp(k,v,arr)
}else {
let arr1=[];
for(let obj of v.values) {
handleExp(k,obj,arr1)
}
arr.push(`(${arr1.join(` ${v.type?v.type:"and"} `)})`)
}
}
arrExpr.push(`(${arr.join(` ${key.startsWith("$and")?"and":"or"} `)})`)
} else {
let value=objExpr[key] as EXPRVALUE
if(typeof(value)!="object") {
let val=typeof(value)=="number"?value:typeof(value)=="boolean"?(value?1:0):("'"+value+"'")
arrExpr.push(`${key}=${val}`)
} else if (typeof(value)=="object" && (value instanceof Date)){
arrExpr.push(`${key}=${value.getTime()}`)
} else if(value===null) {
arrExpr.push(`isnull(${key})`)
} else if (isEXPRVALUEEXP(value)) {
handleExp(key,value,arrExpr)
}else {
let arr=[];
for(let obj of value.values) {
handleExp(key,obj,arr)
}
arrExpr.push(`(${arr.join(` ${value.type?value.type:"and"} `)})`)
}
arrExpr.push(`(${arr.join(` ${value.type?value.type:"and"} `)})`)
}
}
expr=arrExpr.join(` ${exprMode?exprMode:"and"} `)
}
@ -1041,6 +1073,19 @@ export function convertCountSql(sql:{
}
}
export async function generatePageAndCount(mysql:Mysql,sql:{
value:string,
type:any
},size:number) {
let countSql=convertCountSql(sql);
let count=Number(Object.values(await mysql.executeOne(countSql))[0])
let totalPage=CommonUtil.pageTotal(count,size)
return {
count,
totalPage
}
}
export function generateMaxSql<T extends BaseModel,K1 extends keyof T["model"]>(model:T,columnMax:K1,objExpr?:{
[param in keyof T["model"]]?:EXPRVALUE
},exprMode?:"and"|"or"):{
@ -1190,26 +1235,6 @@ function generateLeftJoinHavingExp<T extends BaseModel>(obj:{
return ret;
}
export function testSql<T extends BaseModel,K1 extends keyof T["model"]=null,R extends keyof T["model"]=null,RR extends string=null,O extends boolean=false>(model:T,groups:(keyof T["model"])[],columns:{
columns:K1[],
calcColumns:COLUMNSVALUETYPE<R,RR>[]
},objExpr?:{
[param in keyof T["model"]]?:EXPRVALUE
},exprMode?:"and"|"or",having?:{
[param in keyof T["model"]]?:HAVINGEXP
},havingMode?:"and"|"or",order?:{
field:O extends false?(keyof T["model"]):RR,
isVirtualField:O,
type:"asc"|"desc"
},limit?:number,size?:number):{
value:string,
type:Rename<{
[key in K1|R]:T["model"][key]
},R,RR>
} {
return null
}
export function generateGroupSql<T extends BaseModel,K1 extends keyof T["model"],R extends keyof T["model"],RR extends string=null,O extends boolean=false>(model:T,groups:(keyof T["model"])[],columns:{
columns:K1[],
calcColumns:COLUMNSVALUETYPE<R,RR>[]

View File

@ -1,11 +1,11 @@
import {ICommon_Model_Workflow_Node_Field_Type_Config} from "../../../common/model/workflow_node_field_type_config";
import {Err} from "../../../common/status/error";
import {Entity} from "../../common/entity/entity";
import {EServer_Common_Event_Types} from "../../common/event/types";
import {workflowNodeFieldTypeModel} from '../../../common/model/workflow_node_field_type';
import {workflowNodeFieldTypeMapper} from './../mapper/field';
import {ECommon_Field_Type, Field_Types} from "../../../common/field/type";
import {ProjectIssueService} from "./issue";
import {IServer_Common_Event_Types} from "../../common/event/types";
export class FieldTypeService {
@ -27,7 +27,7 @@ export class WorkflowNodeFieldTypeService extends Entity<typeof workflowNodeFiel
super(workflowNodeFieldTypeMapper)
}
override async delete(eventPulish?: EServer_Common_Event_Types.Types): Promise<void> {
override async delete(eventPulish?: keyof IServer_Common_Event_Types): Promise<void> {
await super.delete()
await workflowNodeFieldTypeMapper.deleteWorkflowNodeField(this.getId());
await ProjectIssueService.clearIssueValueByFieldTypeId(this.getId());

View File

@ -7,7 +7,6 @@ import {
} from "../../../common/routes/response";
import {Err} from '../../../common/status/error';
import {Entity} from "../../common/entity/entity";
import {EServer_Common_Event_Types} from '../../common/event/types';
import {generateSnowId} from '../../common/util/sql';
import {ProjectService} from "../service/project";
import {ICommon_Model_Project_Issue, projectIssueModel} from './../../../common/model/project_issue';
@ -19,6 +18,7 @@ import {ProjectModuleService} from './module';
import {WorkflowActionService, WorkflowNodeService} from './workflow';
import {ECommon_Field_Type, ICommon_Field_Type} from "../../../common/field/type";
import {ProjectReleaseService} from "./release";
import {IServer_Common_Event_Types} from "../../common/event/types";
export class ProjectIssueService extends Entity<typeof projectIssueModel,typeof projectIssueMapper> {
constructor(){
@ -247,7 +247,7 @@ export class ProjectIssueService extends Entity<typeof projectIssueModel,typeof
return ret;
}
override async delete(eventPulish?: EServer_Common_Event_Types.Types): Promise<void> {
override async delete(eventPulish?: keyof IServer_Common_Event_Types): Promise<void> {
await super.delete()
await projectIssueMapper.clear(this.getId())
await commentMapper.clear(this.getId());

View File

@ -2,7 +2,7 @@ import {Err} from "../../../common/status/error";
import {Entity} from "../../common/entity/entity";
import {projectLabelModel} from './../../../common/model/project_label';
import {labelMapper} from '../mapper/label';
import {EServer_Common_Event_Types} from "../../common/event/types";
import {IServer_Common_Event_Types} from "../../common/event/types";
export class ProjectLabelService extends Entity<typeof projectLabelModel,typeof labelMapper> {
constructor(){
@ -16,7 +16,7 @@ export class ProjectLabelService extends Entity<typeof projectLabelModel,typeof
return ret
}
override async delete(eventPublish?: EServer_Common_Event_Types.Types): Promise<void> {
override async delete(eventPublish?: keyof IServer_Common_Event_Types): Promise<void> {
await super.delete(eventPublish);
await labelMapper.clearByLabelId(this.getId());
}

View File

@ -11,7 +11,6 @@ export class ProjectModuleService extends Entity<typeof projectModuleModel,typeo
return ret
}
override async delete(){
await super.loadItem()
await super.delete()
moduleMapper.deleteChildren(this.item.id,this.item.project_id);
}

View File

@ -2,7 +2,6 @@ import {ECommon_Model_Role_Reserved, ECommon_Model_Role_Type} from "../../../com
import {ICommon_Route_Res_Project_List} from "../../../common/routes/response";
import {Err} from "../../../common/status/error";
import {Entity} from "../../common/entity/entity";
import {EServer_Common_Event_Types} from "../../common/event/types";
import {moduleMapper} from "../mapper/module";
import {projectMapper} from '../mapper/project';
import rpcAuthApi from "../../auth/rpc/auth";
@ -15,13 +14,14 @@ import {labelMapper} from '../mapper/label';
import {WorkflowNodeFieldTypeService} from './field';
import {IssueTypeSolutionService} from './issueType';
import {WorkflowService} from './workflow';
import {IServer_Common_Event_Types} from "../../common/event/types";
export class ProjectService extends Entity<typeof projectModel,typeof projectMapper> {
constructor(){
super(projectMapper)
}
override async delete(type?:EServer_Common_Event_Types.Types) {
await super.delete(EServer_Common_Event_Types.Project.DELETE)
override async delete(type?:keyof IServer_Common_Event_Types) {
await super.delete("projectDelete")
await moduleMapper.deleteByProjectId(this.item.id)
await labelMapper.deleteByProjectId(this.item.id)
await rpcAuthApi.clearRoleByItemId(this.item.id);

View File

@ -9,7 +9,6 @@ import {
} from "../../../common/routes/response";
import {Err} from "../../../common/status/error";
import {Entity} from "../../common/entity/entity";
import {EServer_Common_Event_Types} from "../../common/event/types";
import {
ECommon_Model_Workflow_Node_Status,
ICommon_Model_Workflow_Node,
@ -19,6 +18,7 @@ import {workflowActionMapper, workflowMapper, workflowNodeMapper} from './../map
import {WorkflowNodeFieldTypeService} from './field';
import {ICommon_Field_Type} from "../../../common/field/type";
import {IssueTypeSolutionService} from "./issueType";
import {IServer_Common_Event_Types} from "../../common/event/types";
export class WorkflowService {
@ -168,7 +168,7 @@ export class WorkflowNodeService extends Entity<typeof workflowNodeModel,typeof
return ret;
}
override async delete(eventPulish?: EServer_Common_Event_Types.Types): Promise<void> {
override async delete(eventPulish?: keyof IServer_Common_Event_Types): Promise<void> {
await super.delete()
await WorkflowNodeFieldTypeService.clearItemsByWorkflowNodeId(this.getId())
}

Some files were not shown because too many files have changed in this diff Show More