mirror of
https://github.com/Teamlinker/Teamlinker.git
synced 2025-06-03 03:00:17 +00:00
init
This commit is contained in:
parent
efe535180b
commit
ff4db586b3
25
code/client/certs/cert.pem
Normal file
25
code/client/certs/cert.pem
Normal 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
28
code/client/certs/key.pem
Normal 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-----
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
BIN
code/client/src/assert/index_sample.png
Normal file
BIN
code/client/src/assert/index_sample.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 585 KiB |
@ -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>
|
||||
{{ 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
|
||||
})
|
||||
|
@ -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>
|
@ -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>
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
336
code/client/src/business/common/component/meeting/client.ts
Normal file
336
code/client/src/business/common/component/meeting/client.ts
Normal 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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
84
code/client/src/business/common/component/meeting/type.ts
Normal file
84
code/client/src/business/common/component/meeting/type.ts
Normal 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 {
|
||||
|
||||
}
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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"]> = {}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
46
code/client/src/business/controller/app/im/event.ts
Normal file
46
code/client/src/business/controller/app/im/event.ts
Normal 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)
|
||||
})
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
96
code/client/src/business/controller/app/im/userShortView.vue
Normal file
96
code/client/src/business/controller/app/im/userShortView.vue
Normal 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>
|
@ -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>
|
15
code/client/src/business/controller/app/meeting/event.ts
Normal file
15
code/client/src/business/controller/app/meeting/event.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
227
code/client/src/business/controller/app/meeting/meeting.vue
Normal file
227
code/client/src/business/controller/app/meeting/meeting.vue
Normal 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}}
|
||||
<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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
11
code/client/src/business/controller/app/meeting/type.ts
Normal file
11
code/client/src/business/controller/app/meeting/type.ts
Normal 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
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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}}
|
||||
<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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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);
|
||||
})
|
@ -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");
|
||||
|
@ -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 {
|
||||
|
44
code/client/src/business/controller/index/company.vue
Normal file
44
code/client/src/business/controller/index/company.vue
Normal file
File diff suppressed because one or more lines are too long
203
code/client/src/business/controller/index/index.vue
Normal file
203
code/client/src/business/controller/index/index.vue
Normal 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>
|
@ -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>
|
||||
|
66
code/client/src/business/controller/login/register.vue
Normal file
66
code/client/src/business/controller/login/register.vue
Normal 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>
|
65
code/client/src/business/controller/login/registerCode.vue
Normal file
65
code/client/src/business/controller/login/registerCode.vue
Normal 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>
|
47
code/client/src/business/controller/login/reset.vue
Normal file
47
code/client/src/business/controller/login/reset.vue
Normal 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>
|
66
code/client/src/business/controller/login/resetCode.vue
Normal file
66
code/client/src/business/controller/login/resetCode.vue
Normal 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
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
})
|
@ -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 = []
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
10
code/client/src/type.d.ts
vendored
Normal 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 {}
|
@ -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, //自动打开
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
31
code/common/model/meeting_room.ts
Normal file
31
code/common/model/meeting_room.ts
Normal 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
|
@ -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",
|
||||
|
94
code/common/routes/meeting.ts
Normal file
94
code/common/routes/meeting.ts
Normal 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
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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(){
|
||||
|
||||
|
53
code/server/common/cache/keys/auth.ts
vendored
53
code/server/common/cache/keys/auth.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
code/server/common/cache/keys/base.ts
vendored
3
code/server/common/cache/keys/base.ts
vendored
@ -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
13
code/server/common/cache/keys/file.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
13
code/server/common/cache/keys/meeting.ts
vendored
Normal file
13
code/server/common/cache/keys/meeting.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
26
code/server/common/cache/keys/organization.ts
vendored
Normal file
26
code/server/common/cache/keys/organization.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
20
code/server/common/cache/keys/user.ts
vendored
20
code/server/common/cache/keys/user.ts
vendored
@ -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
|
||||
}
|
||||
|
||||
}
|
5
code/server/common/cache/redis.ts
vendored
5
code/server/common/cache/redis.ts
vendored
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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"]={}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
33
code/server/common/mail/mail.ts
Normal file
33
code/server/common/mail/mail.ts
Normal 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
|
||||
}
|
611
code/server/common/meeting/server.ts
Normal file
611
code/server/common/meeting/server.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
122
code/server/common/meeting/type.ts
Normal file
122
code/server/common/meeting/type.ts
Normal 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[]
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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>[]
|
||||
|
@ -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());
|
||||
|
@ -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());
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user