383 lines
13 KiB
Vue
383 lines
13 KiB
Vue
<template>
|
|
<n-flex vertical>
|
|
<n-flex style="padding-top: 10px;">
|
|
<n-input v-model:value="groupCode" type="text" placeholder="输入邀请码" style="flex:1">
|
|
<template #suffix>
|
|
<n-icon :component="CopyOutline" style="cursor: pointer;" @click='_toClipboard(groupCode)' />
|
|
</template>
|
|
</n-input>
|
|
<n-flex>
|
|
<n-button @click="createGroup" :disabled="groupCode !== ''" :type="groupCode !== '' ? 'default' : 'primary'"
|
|
ghost>
|
|
创建群组
|
|
</n-button>
|
|
<n-button @click="joinGroup(groupCode)" :disabled="groupCode === ''"
|
|
:type="groupCode === '' ? 'default' : 'primary'" ghost>
|
|
加入群组
|
|
</n-button>
|
|
<n-button @click="shareWithQR()" :disabled="activeGID === null" v-show="activeGID !== null"
|
|
:type="activeGID === null ? 'default' : 'primary'">
|
|
分享群组
|
|
</n-button>
|
|
<n-select v-model:value="activeGID" :options="joinedGroup.map(gid => { return { label: gid, value: gid } })" v-show="activeGID !== null"
|
|
placeholder="未加入群组" style="flex:1;user-select: none;" />
|
|
</n-flex>
|
|
</n-flex>
|
|
<!-- <n-flex style="padding-top: 10px;padding-bottom: 10px;">
|
|
<n-select v-model:value="activeGID" :options="joinedGroup.map(gid => { return { label: gid, value: gid } })"
|
|
placeholder="选择当前查看群组" style="flex:1" />
|
|
<n-button @click="shareWithQR()" :disabled="activeGID === null"
|
|
:type="activeGID === null ? 'default' : 'primary'">
|
|
分享群组
|
|
</n-button>
|
|
</n-flex> -->
|
|
<!-- <n-data-table :columns="columns"
|
|
:data="activeGID ? Messages[activeGID].map(v => { v.timestampString = new Date(v.timestamp).toLocaleString(); return v }).reverse() : []"
|
|
:row-props="null" :bordered="false" :single-line="false" :max-height="400" /> -->
|
|
<div style="flex:1; position: relative;min-height: none;">
|
|
<chatui style="margin-bottom: 10px; height: 65vh"
|
|
:messages="activeGID ? Messages[activeGID].map(v => { v.timestampString = new Date(v.timestamp).toLocaleString(); return v }).reverse() : []"/>
|
|
|
|
<n-input v-model:value="msgInput" type="textarea" placeholder="发送信息" :autosize="{
|
|
minRows: 1,
|
|
maxRows: 2
|
|
}" @keydown="handleKeyDown" style="margin-top: 0px;">
|
|
<template #suffix>
|
|
<n-icon :component="DocumentsOutline" style="cursor: pointer;margin-right: 5px;" @click='filePicker({fn:sendFiles,multiple:true})'/>
|
|
<n-icon :component="ImagesOutline" style="cursor: pointer;margin-right: 5px;" @click='filePicker({fn:sendImages,accept:"image/*",multiple:true})'/>
|
|
<n-icon :component="SendOutline" style="cursor: pointer;" @click='sendMsg(activeGID, msgInput)'/>
|
|
</template>
|
|
</n-input>
|
|
|
|
<!-- <n-button @click="sendMsg(activeGID, msg)" :disabled="msg === '' || activeGID === null"
|
|
:type="msg === '' || activeGID === null ? 'default' : 'primary'">
|
|
发送消息
|
|
</n-button> -->
|
|
</div>
|
|
</n-flex>
|
|
<!-- {{ dataTest }} -->
|
|
<!-- {{ Messages }} -->
|
|
|
|
|
|
<n-modal v-model:show="showModal">
|
|
<n-card
|
|
style="width: fit-content;"
|
|
title=""
|
|
:bordered="false"
|
|
size="huge"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
>
|
|
<n-qr-code :value="QRCodeData" style="padding: 0;" :size="256" error-correction-level="Q" :v-if="showModal"
|
|
:icon-src='0?QRIconUrl:null'
|
|
:icon-size="128"/>
|
|
</n-card>
|
|
</n-modal>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref,onMounted } from 'vue'
|
|
// import { useModal } from "naive-ui";
|
|
import useClipboard from "vue-clipboard3";
|
|
import { NIcon, NFlex, NDataTable, NButton, NInput, NSelect,NModal,NCard , NQrCode} from 'naive-ui'
|
|
import { useSettingStore } from "@/pinia/store";
|
|
import { generateKey, encryptString, encrypt } from '@/func/upload';
|
|
import { decryptString } from '@/func/download';
|
|
import WebsocketHeartbeatJs from 'websocket-heartbeat-js';
|
|
import { CopyOutline,SendOutline,ImagesOutline,DocumentsOutline } from '@vicons/ionicons5'
|
|
import getQueryString from '@/func/getQuery'
|
|
import chatui from "@/components/chatui.vue"
|
|
import { Base64 } from 'js-base64';
|
|
import { useMessage, useNotification } from "naive-ui";
|
|
import filePicker from "@/func/filePicker"
|
|
|
|
|
|
|
|
const message = useMessage()
|
|
|
|
// const modal = useModal();
|
|
const { toClipboard } = useClipboard();
|
|
|
|
const _toClipboard = async (text) => {
|
|
await toClipboard(text)
|
|
message.info(`已复制 ${text.length < 10 ? text : text.substring(0,7) + '...'}`)
|
|
}
|
|
|
|
const setting = useSettingStore();
|
|
const serverPort = setting.serverPort;
|
|
const wsBaseUrl = 'ws' + "://" + location.hostname + ":" + serverPort;
|
|
const baseUrl = location.protocol + "//" + location.hostname + ":" + serverPort + "/";
|
|
const QRIconUrl = ref(location.protocol + "//" + location.hostname + ":" + serverPort + "/favicon.ico")
|
|
|
|
const columns = [
|
|
{
|
|
"title": "type",
|
|
"key": "type",
|
|
width: 80
|
|
},
|
|
{
|
|
"title": "from",
|
|
"key": "from",
|
|
width: 80
|
|
},
|
|
{
|
|
"title": "timestamp",
|
|
"key": "timestampString",
|
|
width: 150,
|
|
// sorter: {
|
|
// compare: (a, b) => {
|
|
// // console.log(a,b,new Date(a),parseInt(new Date(a)) - parseInt(new Date(b)))
|
|
// return parseInt(new Date(a.timestamp)) - parseInt(new Date(b.timestamp))
|
|
// },
|
|
// }
|
|
},
|
|
{
|
|
"title": "msg",
|
|
"key": "msg"
|
|
}
|
|
]
|
|
const columnsTest = [
|
|
{
|
|
title: "from",
|
|
key: "from"
|
|
},
|
|
{
|
|
title: "message",
|
|
key: "msg"
|
|
},
|
|
];
|
|
function handleKeyDown(e){
|
|
// console.log(e,e.ctrlKey,e.code)
|
|
if(e.ctrlKey === true&&e.code === 'Enter'){
|
|
sendMsg(activeGID.value, msgInput.value)
|
|
}
|
|
}
|
|
|
|
// 用于二维码分享
|
|
const showModal = ref(false)
|
|
const QRCodeData = ref('')
|
|
function shareWithQR(){
|
|
let code = activeGID.value+gID2key[activeGID.value].key
|
|
QRCodeData.value = location.protocol + "//" + location.hostname + ":" + serverPort + `/?aP=chat&cG=${code}`
|
|
showModal.value = true
|
|
}
|
|
|
|
// 凭此code进入群组
|
|
const groupCode = ref('')
|
|
|
|
// 当前活跃的群组ID
|
|
const activeGID = ref(null)
|
|
|
|
// 临时生成的key,之后与返回的code结合形成groupCode
|
|
const key = ref('')
|
|
|
|
// 存储所有消息,通过gID查询一个群组的消息,{msg,from,timestamp}
|
|
const Messages = ref({})
|
|
|
|
const dataTest = ref([])
|
|
const gID = ref('')
|
|
// const codeInput = ref('')
|
|
const msgInput = ref('')
|
|
|
|
const joinedGroup = ref([])
|
|
// 由server随机分配uuid
|
|
const uuid = ref('')
|
|
|
|
|
|
const gID2key = {}
|
|
|
|
const options = {
|
|
url: wsBaseUrl,
|
|
pingTimeout: 20000,
|
|
pongTimeout: 10000,
|
|
reconnectTimeout: 3000,
|
|
pingMsg: JSON.stringify({ type: 'heartbeat' })
|
|
}
|
|
let socket = new WebsocketHeartbeatJs(options);
|
|
|
|
// const socket = new WebSocket(baseUrl);
|
|
// socket.addEventListener("open", (event) => {
|
|
// // socket.send("");
|
|
// });
|
|
// setInterval(()=>console.log(activeGID.value),1000)
|
|
|
|
|
|
socket.onmessage = (event) => {
|
|
const msg = JSON.parse(event.data)
|
|
dataTest.value.push({ from: 'server', msg: event.data })
|
|
if (msg.type) {
|
|
let type = msg.type
|
|
if (type === 'sMsg') {
|
|
let type1 = msg.type1
|
|
if (type1 === 'create') {
|
|
gID.value = msg.gID
|
|
groupCode.value = msg.gID + key.value
|
|
joinGroup(groupCode.value)
|
|
} else if (type1 === 'joinVerify') {
|
|
let gID = msg.gID
|
|
let key = gID2key[gID].key
|
|
let res = decryptString(key, msg.verify)
|
|
// console.log(res, key, msg.verify)
|
|
|
|
// 验证成功,正式加入群组
|
|
if (res === 'miaoSpring') {
|
|
socket.send(JSON.stringify({ type: 'join', type1: 'success', gID }))
|
|
joinedGroup.value.push(msg.gID)
|
|
Messages.value[msg.gID] = []
|
|
// if(activeGID.value === null){
|
|
activeGID.value = msg.gID
|
|
// }
|
|
}
|
|
} else if (type1 === 'history') {
|
|
let gID = msg.gID
|
|
let key = gID2key[gID].key
|
|
Messages.value[msg.gID].length = 0
|
|
for (let m of JSON.parse(msg.history)) {
|
|
Messages.value[msg.gID].push({ type: m.type,type1:m.type1, msg: decryptString(key, m.msg), from: m.from, timestamp: m.timestamp })
|
|
dataTest.value.push({ from: 'decrypt', msg: decryptString(key, m.msg) })
|
|
}
|
|
} else if (type1 === 'uuid') {
|
|
uuid.value = msg.uuid
|
|
}
|
|
} else if (type === 'gMsg') {
|
|
let gID = msg.gID
|
|
let key = gID2key[gID].key
|
|
Messages.value[msg.gID].push({ type: msg.type,type1:msg.type1, msg: decryptString(key, msg.msg), from: msg.from, timestamp: msg.timestamp })
|
|
dataTest.value.push({ from: 'decrypt', msg: decryptString(key, msg.msg) })
|
|
}
|
|
}
|
|
};
|
|
|
|
socket.onopen = () => {
|
|
if (uuid.value) {
|
|
socket.send(JSON.stringify({ type: 'uuid', type1: 'set', uuid: uuid.value }))
|
|
} else {
|
|
socket.send(JSON.stringify({ type: 'uuid', type1: 'get' }))
|
|
}
|
|
// console.log(joinedGroup)
|
|
let toJoin = [...joinedGroup.value]
|
|
joinedGroup.value.length = 0
|
|
for (let gID of toJoin) {
|
|
let code = gID + gID2key[gID].key
|
|
console.log(code)
|
|
joinGroup(code)
|
|
}
|
|
}
|
|
|
|
socket.onreconnect = (e) => {
|
|
console.log('reconnecting...');
|
|
}
|
|
|
|
function createGroup() {
|
|
let keyt = generateKey(setting)
|
|
key.value = keyt
|
|
socket.send(JSON.stringify({ type: 'create', verify: encryptString(keyt, 'miaoSpring', setting) }))
|
|
}
|
|
|
|
function joinGroup(code) {
|
|
var gID = code.substring(0, 8);
|
|
var key = code.substring(8);
|
|
if (joinedGroup.value.includes(gID)) {
|
|
return
|
|
}
|
|
gID2key[gID] = {}
|
|
gID2key[gID].key = key
|
|
joinGroup_Verify(gID)
|
|
}
|
|
|
|
function joinGroup_Verify(gID) {
|
|
socket.send(JSON.stringify({ type: 'join', type1: 'verify', gID }))
|
|
}
|
|
|
|
function sendMsg(gID, msg) {
|
|
if(msg === '' || msg === null || activeGID.value === null)return
|
|
let key = gID2key[gID].key
|
|
msg = encryptString(key, msg, setting)
|
|
socket.send(JSON.stringify({ type: 'message',type1:'text', msg, gID }))
|
|
msgInput.value = null
|
|
}
|
|
|
|
// async function encryptImg(file){
|
|
// var f = new Blob([file]);
|
|
// let key = gID2key[gID].key
|
|
// let eU8A = await encrypt(key, f, fileName, setting)
|
|
// let eb = Base64.fromUint8Array(eU8A)
|
|
// return eb
|
|
// }
|
|
|
|
async function uploadFile(file){
|
|
if(activeGID.value === null)return
|
|
var fileName = file.name;
|
|
let key = gID2key[activeGID.value].key
|
|
const encryptedFileName = encryptString(key, fileName, setting);
|
|
var f = new Blob([file]);
|
|
let data = await encrypt(key, f, fileName, setting)
|
|
const params = {
|
|
fileName: encryptedFileName,
|
|
downloadCredit:-1
|
|
};
|
|
message.info(`开始上传${fileName}`)
|
|
const xhr = new XMLHttpRequest();
|
|
// onProgress({ percent: 1 });
|
|
xhr.open(
|
|
"POST",
|
|
baseUrl + "file/upload?" + new URLSearchParams(params)
|
|
);
|
|
// console.log(setting.token)
|
|
xhr.setRequestHeader('Authorization', `Bearer ${setting.token}`);
|
|
xhr.send(data);
|
|
const code = await new Promise((resolve,reject)=>{
|
|
xhr.onreadystatechange = () => {
|
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|
let id = xhr.responseText;
|
|
let code = id + key
|
|
message.success(`上传${fileName}成功`)
|
|
resolve(code)
|
|
} else {
|
|
if(xhr.statusText !== 'OK'){
|
|
console.error(xhr.statusText);
|
|
message.error(`上传${fileName}失败`)
|
|
}
|
|
}
|
|
};
|
|
})
|
|
console.log(code)
|
|
return code
|
|
}
|
|
|
|
async function sendImages(files){
|
|
if(activeGID.value === null || files.length === 0)return
|
|
for(let file of files){
|
|
const code = await uploadFile(file)
|
|
let key = gID2key[activeGID.value].key
|
|
socket.send(JSON.stringify({ type: 'message',type1:'img', msg: encryptString(key, code), gID:activeGID.value }))
|
|
};
|
|
}
|
|
|
|
async function sendFiles(files){
|
|
if(activeGID.value === null || files.length === 0)return
|
|
for(let file of files){
|
|
let fileName = file.name;
|
|
const code = await uploadFile(file)
|
|
let key = gID2key[activeGID.value].key
|
|
socket.send(JSON.stringify({ type: 'message',type1:'file', msg: encryptString(key, code), gID:activeGID.value }))
|
|
};
|
|
}
|
|
onMounted(()=>{
|
|
let aP = getQueryString('aP')
|
|
let cG = getQueryString('cG')
|
|
if(aP === 'chat' && cG){
|
|
groupCode.value = cG
|
|
var st = setInterval(()=>{
|
|
if(joinedGroup.value.includes(cG.substring(0,8))){
|
|
clearInterval(st)
|
|
return
|
|
}
|
|
joinGroup(cG)
|
|
},2000)
|
|
}
|
|
})
|
|
|
|
</script>
|
|
|
|
<style></style> |