Commit 1c345c2e by wxl

x

parent 491d1c00
import { UserData } from "src/types/auth"; import { UserData } from "any-hooks/types/user";
import { useState } from "vue-vulcan"; import { useState } from "vue-vulcan";
/** 当前客户端用户的基本信息(含token) */
export function useAuthData() { export function useAuthData() {
const [authData, setAuth] = useState<UserData>({}, {storage: 'custome', key: 'auth'}); const [authData, setAuth] = useState<UserData>(null, {storage: 'custome', key: 'auth'});
return { return {
authData, authData,
......
import { useInjector, useRequest } from "vue-vulcan";
import { useAuthData } from "./useAuthData";
export function useLogOut() {
const [, request] = useRequest<any>('/loginOut', { auto: false });
const { setAuth } = useInjector(useAuthData);
const submitLogOut = (data: {login_id: string}) => {
return request(data).then( _ => setAuth(null));
}
return { submitLogOut }
}
\ No newline at end of file
import { AgoraAppStore } from "any-hooks/types/agora"; import { AgoraAppStore, StreamType } from "any-hooks/types/agora";
import { onMounted, watch } from "vue"; import { onMounted, watch } from "vue";
import { useInjector, useState } from "vue-vulcan"; import { useInjector, useState } from "vue-vulcan";
import { AGORA_APP_SDK } from "../communication/token"; import { AGORA_APP_SDK } from "../communication/token";
interface AgoraStreamItem { interface AgoraStreamItem {
url?: string; url?: string;
uid: string; uid: number; //agora仅支持类型为number的uid
} }
// type AgoraState = 'unready' | 'inited' | 'joined'; /** 基于声网sdk封装,由于多平台特性,请在应用根组件将对应平台的声网SDK对象以及appid通过useProvider提供; */
/** 基于声网sdk封装,由于多平台特性,请在根组件将对应平台的声网SDK对象以及appid通过useProvider提供; */
export function useAgoraClient() { export function useAgoraClient() {
const [agoraErr, setError] = useState(null); const [agoraErr, setError] = useState(null);
const [streamList, setStream] = useState<AgoraStreamItem[]>([]); const [streamList, setStream] = useState<AgoraStreamItem[]>([]);
const { appid, client } = useInjector<AgoraAppStore>(AGORA_APP_SDK); const { appid, client } = useInjector<AgoraAppStore>(AGORA_APP_SDK);
onMounted(() => { onMounted(() => {
client.init(appid, subscribeRemoteStream, setError); client.init(appid, () => {}, setError);
}) })
const joinChannelWithVideo = (token: string, cid: string, uid: string) => { const joinChannelWithAgora = (token: string, cid: string, uid: number) => {
console.log('准备加入视频频道 cid:',cid)
client.join( client.join(
token, token,
cid, cid,
uid, uid,
pushVideoStream, () => {pushVideoStream(); subscribeRemoteStream },
setError setError
); );
} }
...@@ -33,7 +32,7 @@ export function useAgoraClient() { ...@@ -33,7 +32,7 @@ export function useAgoraClient() {
//推送本地视频流 //推送本地视频流
const [localPushurl, setPushUrl] = useState(''); const [localPushurl, setPushUrl] = useState('');
const pushVideoStream = () => { const pushVideoStream = () => {
client.publish( url => setPushUrl(url)); client.publish( url => {setPushUrl(url);console.log('成功生成推流地址'+url)});
} }
//监听远程视频流 //监听远程视频流
...@@ -41,36 +40,80 @@ export function useAgoraClient() { ...@@ -41,36 +40,80 @@ export function useAgoraClient() {
client.on('stream-added', evt => { client.on('stream-added', evt => {
client.subscribe( client.subscribe(
evt.uid, evt.uid,
url => updateStremList('add', {uid: evt.uid, url}), url => {updateStremList('add', {uid: evt.uid, url}); console.log(`用户${evt.uid}加入频道${url}`)},
setError setError
) )
}); });
client.on('stream-removed', evt => updateStremList('remove', {uid: evt.uid})); client.on('stream-removed', evt => updateStremList('remove', evt.uid));
}
watch(streamList, val => console.log(`远端流变化val`, val));
// 暂停本地流推送
const muteLocalStream = (type: StreamType) => {
return new Promise (resolve => {
client.muteLocal(type, () => resolve(null))
})
}
// 恢复本地流推送
const unmuteLocalStream = (type: StreamType) => {
return new Promise (resolve => {
client.unmuteLocal(type, () => resolve(null))
})
}
// 暂停远程流接收
const muteRemoteStream = (uid: number, type: StreamType) => {
return new Promise( resolve => {
client.mute(uid, type, () => resolve(null) )
})
}
// 恢复远程流接收
const unmuteRemoteStream = (uid: number, type: StreamType) => {
return new Promise( resolve => {
client.unmute(uid, type , () => resolve(null))
})
} }
watch(streamList, console.log)
// 根据uid来新增或移除远端视频流列表 // 根据uid来新增或移除远端视频流列表
const updateStremList = (action: 'add' | 'remove', input: AgoraStreamItem) => { function updateStremList(act: 'add', input: {uid: number, url: string}): void;
function updateStremList(act: 'remove', input: number[]): void;
function updateStremList (action, input) {
switch(action) { switch(action) {
case 'add': case 'add':
setStream(streamList.value.concat(input)); setStream(streamList.value.concat(input));
break; break;
case 'remove': case 'remove':
const index = streamList.value.findIndex(item => item.uid === input.uid); console.log('remove', input)
index && setStream(streamList.value.splice(index, 1)); const res = streamList.value.filter( item => {
!input.includes(item.uid)
});
setStream(res);
break; break;
} }
} }
// 退出频道
const leaveChannelWithAgora = () => {
return new Promise( resolve => {
client.leave(() => resolve(''), setError);
})
}
// 监听来自Agora Client的错误提示 // 监听来自Agora Client的错误提示
watch(agoraErr, err => { watch(agoraErr, err => {
console.error(err) console.error(err)
}) })
return { return {
localPushurl, localPushurl,
streamList, streamList,
joinChannelWithVideo joinChannelWithAgora,
leaveChannelWithAgora,
muteLocalStream,
muteRemoteStream,
unmuteLocalStream,
unmuteRemoteStream
} }
} }
\ No newline at end of file
import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { onUnmounted } from 'vue';
export function useDebounce(func: any, time: number) {
const trigger = new Subject();
trigger
.pipe(
debounceTime(time)
)
.subscribe(e => func(e))
const next = (e: any) => trigger.next(e);
onUnmounted(() => trigger.complete());
return next
}
\ No newline at end of file
...@@ -10,6 +10,7 @@ export function useSocket<S = any>() { ...@@ -10,6 +10,7 @@ export function useSocket<S = any>() {
const socketSettings = useInjector<SocketSettings>(SOCKET_SETTINGS, 'optional'); const socketSettings = useInjector<SocketSettings>(SOCKET_SETTINGS, 'optional');
let ws: CustomeSocket<S>; let ws: CustomeSocket<S>;
const brokenTime = ref(0);
if(customeSocket) ws = customeSocket; if(customeSocket) ws = customeSocket;
const connect = (url?: string) => { const connect = (url?: string) => {
...@@ -19,9 +20,12 @@ export function useSocket<S = any>() { ...@@ -19,9 +20,12 @@ export function useSocket<S = any>() {
ws = new WebSocket(url) as unknown as CustomeSocket<S> ws = new WebSocket(url) as unknown as CustomeSocket<S>
} }
ws.addEventListener('message', event => setMsg(event.data)); ws.addEventListener('message', event => setMsg(event.data));
ws.addEventListener('open', () => setStatus('opening')); ws.addEventListener('open', () => {
ws.addEventListener('open', () => socketSettings.heartData && startHeartConnect()); setStatus('opening');
ws.addEventListener('close', () => setStatus('closed')); brokenTime.value = 0;
socketSettings.heartData && startHeartConnect();
} );
ws.addEventListener('close', () => {setStatus('closed'); reconnect(url);});
ws.addEventListener('error', () => { ws.addEventListener('error', () => {
setStatus('onerror'); setStatus('onerror');
reconnect(url); reconnect(url);
...@@ -35,7 +39,6 @@ export function useSocket<S = any>() { ...@@ -35,7 +39,6 @@ export function useSocket<S = any>() {
} }
// 重连功能 // 重连功能
const brokenTime = ref(0);
const limit = socketSettings.retryLimit || 5; const limit = socketSettings.retryLimit || 5;
const reconnect = (url: string) => { const reconnect = (url: string) => {
brokenTime.value ++; brokenTime.value ++;
......
import { Subject } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
import { onUnmounted } from 'vue';
export function useThrottle(func: any, time: number) {
const trigger = new Subject();
trigger
.pipe(
throttleTime(time)
)
.subscribe(e => func(e))
const next = (e: any) => trigger.next(e);
onUnmounted(() => trigger.complete());
return next
}
\ No newline at end of file
// import { BehaviorSubject, Observable, Subject } from 'rxjs';
// import { filter, tap } from 'rxjs/operators';
// import { useState, useInjector } from 'vue-vulcan';
// import { onMounted } from 'vue';
// import * as AgoraMiniappSDK from '../../src/libs/Agora_SDK_for_WeChat';
// // import { useChannelInfo } from './useChannelInfo';
// import { useAuthData } from '../auth/useAuthData';
// interface VideoItemData {
// url: string;
// uid?: string;
// rotation?: string;
// }
// AgoraMiniappSDK.LOG.onlog = (text) => {
// console.log(text);
// };
// const AGORA_APPID = '0a67966ff4ae4eb3b32446df0151e16a';
// const streamLog$ = new Subject<string>();
// const client = new AgoraMiniappSDK.Client();
// const agoraInit$ =
// new Observable<boolean>( observer => {
// client.init(AGORA_APPID, () => {
// streamLog$.next('初始化成功')
// observer.next(true);
// observer.complete();
// }, (e: any) => observer.error(e));
// })
// export function useAgoraClient() {
// const channelState$ = new BehaviorSubject<string>('leave');
// const [streamList, setStreamList] = useState<VideoItemData[]>([]);
// const { current, onReady } = useInjector(useChannelInfo, 'root');
// const { user } = useInjector(useAuthData);
// onMounted( () => {
// subscribeRemoteStream();
// })
// onReady( () => {
// agoraInit$
// .pipe(tap(_=>console.log('准备加入频道')))
// .subscribe(
// _ => client.join(current.value.agora_token, current.value.channel_id, user.value.id, () => {
// channelState$.next('joined');
// streamLog$.next('频道加入成功'+current.value.agora_token);
// pushLocalStream();
// }, console.log),
// console.log
// )
// })
// const leaveChannel = () => {
// client.leave( () => channelState$.next('leave'))
// }
// const subscribeRemoteStream = () => {
// client.on("update-url", e => {
// console.log('genx', e)
// })
// client.on("stream-added", (e: any) => {
// console.log('新人加入', e)
// const uid = e.uid
// client.subscribe(
// uid,
// (url: string) => {
// const newStream = {url, uid}
// setStreamList(streamList.value.concat([newStream]))
// }
// );
// });
// client.on("stream-removed", e => {
// console.log('有人退出', e)
// const uid = e.uid;
// const index = streamList.value.findIndex( item => item.uid === uid );
// const list = streamList.value.splice(index, 1);
// setStreamList(list);
// })
// }
// const pushLocalStream = () => {
// channelState$
// .pipe(
// filter( state => state === 'joined' )
// )
// .subscribe( _ => {
// console.log('推送本地流')
// client.publish( (url: string) => {
// console.log('推送成功', url)
// const list = streamList.value.concat([{ url }])
// setStreamList(list)
// } )
// })
// }
// return {
// streamList,
// leaveChannel,
// pushLocalStream
// }
// }
\ No newline at end of file
import { AnyRemoteSubFlag } from "any-hooks/types/socket"; import { AnyRemoteMainFlag, AnyRemoteSubFlag } from "any-hooks/types/socket";
import { UserData } from "any-hooks/types/user"; import { UserData } from "any-hooks/types/user";
import { watch } from "vue"; import { watch } from "vue";
import { useInjector, useState } from "vue-vulcan"; import { useInjector, useState } from "vue-vulcan";
import { useChannelStore } from "./useChannelStore"; import { useChannelStore } from "./useChannelStore";
import { useNetSocketCenter } from "./useNetSocketCenter"; import { useNetSocketStore } from "./useNetSocketStore";
type CallingState = 'net_error' | 'calling' | 'call_successed' | 'call_accepted' | 'free'; type CallingState = 'net_error' | 'calling' | 'call_successed' | 'being_called' | 'call_accepted' | 'free';
interface Caller extends UserData { interface Caller extends UserData {
action: AnyRemoteSubFlag | 'none'; action: AnyRemoteSubFlag | 'none';
members?: string[];
channel?: string; channel?: string;
} }
/** 呼叫中心功能,可以主动呼叫联系人、回应对方的呼叫,以及监听双方呼叫人的信息和状态 */ /** 呼叫中心功能,可以主动呼叫联系人、回应对方的呼叫,以及监听相关用户的实时呼叫状态 */
export function useCallCenter() { export function useCallCenter() {
const { sendMsg, currentMsg } = useInjector(useNetSocketCenter); const { sendMsg, currentMsg } = useInjector(useNetSocketStore);
/** 主动呼叫功能 */ /** 主动呼叫功能 */
const [target, setTarget] = useState<Caller>(null); const [target, setTarget] = useState<Caller>(null);
const [myCallState, setCallState] = useState<CallingState>('free'); const [myCallState, setCallState] = useState<CallingState>('free');
const {currentChannel, createChannel, joinCallingChannel} = useInjector(useChannelStore); const {currentChannel, channelMembers, createChannel, getTokenByChannel, updateMembers, clearChannel} = useInjector(useChannelStore);
const callContact = (user: UserData) => { const callContact = (user: UserData) => {
setTarget({...user, action: 'none'}); //保存呼叫目标的信息 setTarget({...user, action: 'none'}); //保存呼叫目标的信息
setCallState('calling'); //主动呼叫别人时将自己的呼叫状态更改为‘calling’ setCallState('calling'); //主动呼叫别人时将自己的呼叫状态更改为‘calling’
...@@ -30,7 +31,10 @@ export function useCallCenter() { ...@@ -30,7 +31,10 @@ export function useCallCenter() {
toName: user.nickname, toName: user.nickname,
channelID: currentChannel.value.channel_id, channelID: currentChannel.value.channel_id,
msgMainFlag: 'CallOffer', msgMainFlag: 'CallOffer',
msgSubFlag: 'Request' msgSubFlag: 'Request',
msgData: {
members: channelMembers.value
}
}) })
}) })
} }
...@@ -39,7 +43,8 @@ export function useCallCenter() { ...@@ -39,7 +43,8 @@ export function useCallCenter() {
const answerCaller = (subFlag: AnyRemoteSubFlag) => { const answerCaller = (subFlag: AnyRemoteSubFlag) => {
if(subFlag === 'Connect') { if(subFlag === 'Connect') {
setCallState('call_accepted'); setCallState('call_accepted');
joinCallingChannel(caller.value.channel); getTokenByChannel(caller.value.channel);
updateMembers(caller.value.members, 'join')
}; };
sendMsg({ sendMsg({
toID: caller.value.id, toID: caller.value.id,
...@@ -54,13 +59,23 @@ export function useCallCenter() { ...@@ -54,13 +59,23 @@ export function useCallCenter() {
// 监听来自其他用户的呼叫请求 // 监听来自其他用户的呼叫请求
watch(currentMsg, msg => { watch(currentMsg, msg => {
if(msg.msgMainFlag !== 'CallOffer') return; if(msg.msgMainFlag !== 'CallOffer') return;
if(myCallState.value !== 'free') answerCaller('Busying'); switch(msg.msgSubFlag) {
console.log(msg.msgSubFlag); case 'Request':
// 本地呼叫状态为free时,变更为being_called;不为free,则发送繁忙的回应。
myCallState.value === 'free' ? setCallState('being_called') : answerCaller('Busying');
break;
case 'Hangup':
//对方中断呼叫,则将呼叫状态改为free
setCallState('free');
break;
}
setCaller({ setCaller({
id: currentMsg.value.fromID, id: currentMsg.value.fromID,
nickname: currentMsg.value.fromName, nickname: currentMsg.value.fromName,
action: msg.msgSubFlag, action: msg.msgSubFlag,
channel: msg.channelID channel: msg.channelID,
members: msg.msgData?.members || []
}) })
}) })
// 监听呼叫目标的回应 // 监听呼叫目标的回应
...@@ -69,10 +84,11 @@ export function useCallCenter() { ...@@ -69,10 +84,11 @@ export function useCallCenter() {
switch(msg.msgSubFlag) { switch(msg.msgSubFlag) {
case 'Connect': case 'Connect':
setCallState('call_successed'); setCallState('call_successed');
updateMembers([msg.fromID], 'join');
break; break;
case 'Busying': case 'Busying':
case 'Hangup': case 'Hangup':
console.log('对方拒绝呼叫请求') console.log('对方拒绝呼叫请求')
setCallState('free'); setCallState('free');
setTarget({...target.value, action: msg.msgSubFlag}); setTarget({...target.value, action: msg.msgSubFlag});
break; break;
...@@ -81,15 +97,36 @@ export function useCallCenter() { ...@@ -81,15 +97,36 @@ export function useCallCenter() {
/** 呼叫挂断功能 */ /** 呼叫挂断功能 */
const hangup = () => { const hangup = () => {
let flag: AnyRemoteMainFlag, toID: string;
switch(myCallState.value) {
default:
flag = 'ChannelChat';
toID = '-2'; // '-2'表示向频道发送消息
break;
case 'calling':
flag = 'CallOffer';
toID = target.value.id;
break;
case 'being_called':
flag = 'CallAnswer';
toID = caller.value.id;
break;
}
sendMsg({ sendMsg({
channelID: currentChannel.value.channel_id, channelID: currentChannel.value?.channel_id,
msgMainFlag: myCallState.value === 'calling' ? 'CallOffer' : 'CallAnswer', msgMainFlag: flag,
msgSubFlag: 'Hangup', msgSubFlag: 'Hangup',
toID: myCallState.value === 'calling' ? target.value.id : caller.value.id toID
}) })
setCallState('free'); setCallState('free');
} }
/** 监听呼叫状态,当状态变回free时,重置channel的信息 */
watch(myCallState, state => {
if(state === 'free') clearChannel()
})
return { return {
/** 呼叫远程联系人 @param UserData */ /** 呼叫远程联系人 @param UserData */
callContact, callContact,
......
import { useAuthData } from "any-hooks/auth/useAuthData";
import { watch } from "vue"; import { watch } from "vue";
import { useRequest, useState } from "vue-vulcan"; import { useInjector, useRequest, useState } from "vue-vulcan";
import { UserData } from "any-hooks/types/user"; import { useNetSocketStore } from "./useNetSocketStore";
interface ChannelData { interface ChannelData {
channel_id: string; channel_id: string;
agora_token: string; agora_token: string;
members?: UserData[];
} }
/** 频道中心功能,可以创建频道,并保存当前频道的信息,供呼叫中心、会议中心使用(目前只允许同一时间仅存在一个频道) */ /** 频道中心功能,可以创建频道,并保存当前频道的信息,供呼叫中心、会议中心使用(目前同一时间仅允许存在一个频道) */
export function useChannelStore() { export function useChannelStore() {
const [currentChannel, setCurrent] = useState<ChannelData>(null); const [currentChannel, setChannel] = useState<ChannelData>(null);
const [channelMembers, setMembers] = useState<string[]>([]);
const [channelInfo, request] = useRequest<ChannelData>('/getAgoraToken', {auto: false}); const [channelInfo, request] = useRequest<ChannelData>('/getAgoraToken', {auto: false});
const { authData } = useInjector(useAuthData);
const { currentMsg } = useInjector(useNetSocketStore);
const createChannel = () => { const createChannel = () => {
return request() return request();
} }
const joinCallingChannel = (cid: string) => { const getTokenByChannel = (cid: string) => {
request({channel_id: cid}); request({channel_id: cid});
} }
const updateMembers = (ids: string[], type: 'join' | 'leave') => {
const current = channelMembers.value;
if(type === 'join') {
setMembers([...new Set([...current, ...ids])])
} else {
setMembers(current.filter(item => !ids.includes(item)))
}
}
const clearChannel = () => {
setChannel(null);
setMembers([]);
}
watch(channelInfo, val => { watch(channelInfo, val => {
setCurrent(val); setChannel(val);
updateMembers([authData.value.id], 'join');
})
//监听频道下的成员实时信息
watch(currentMsg, msg => {
if(msg.msgMainFlag !== 'ChannelChat') return;
switch(msg.msgSubFlag) {
case 'Hangup':
updateMembers([msg.fromID], 'leave');
break;
}
}) })
return { return {
currentChannel, currentChannel,
joinCallingChannel, channelMembers,
createChannel getTokenByChannel,
createChannel,
updateMembers,
clearChannel
} }
} }
\ No newline at end of file
import { setKeepScreenOn } from "@tarojs/taro";
import { useAuthData } from "any-hooks/auth/useAuthData"; import { useAuthData } from "any-hooks/auth/useAuthData";
import { useAgoraClient } from "any-hooks/common/useAgoraClient"; import { useAgoraClient } from "any-hooks/common/useAgoraClient";
import { computed, watch } from "vue"; import { onMounted, watch } from "vue";
import { useInjector } from "vue-vulcan"; import { useInjector, useState } from "vue-vulcan";
import { useCallCenter } from "./useCallCenter";
import { useChannelStore } from "./useChannelStore"; import { useChannelStore } from "./useChannelStore";
/** 会议中心功能, 该功能在呼叫功能生效后使用,通过调用agoraSDK实现多人视频会议、语音会议的功能 */ /** 会议中心功能, 该功能在呼叫功能生效后使用,通过调用agoraSDK实现多人视频会议、语音会议的功能 */
export function useMeetingCenter() { export function useMeetingCenter() {
const { currentChannel } = useInjector(useChannelStore); const { currentChannel } = useInjector(useChannelStore);
const { authData } = useInjector(useAuthData) const { authData } = useInjector(useAuthData);
const {joinChannelWithVideo, streamList, localPushurl} = useAgoraClient(); const { hangup, myCallState } = useInjector(useCallCenter);
const {
joinChannelWithAgora,
leaveChannelWithAgora,
muteLocalStream,
unmuteLocalStream,
streamList,
localPushurl,
} = useAgoraClient();
// const meetingMembers = computed()
/** 监听频道信息,有值或变更后将agora通讯功能加入频道 */
watch(currentChannel, channel => { watch(currentChannel, channel => {
joinChannelWithVideo( if(!channel) return;
joinChannelWithAgora(
channel.agora_token, channel.agora_token,
channel.channel_id, channel.channel_id,
authData.value.id parseInt(authData.value.id)
) )
}) })
/** 离开会议(分两步:1.挂断当前呼叫, 2.退出agora音视频流) */
const leave = () => {
hangup();
leaveChannelWithAgora();
}
/** 保持屏幕常亮 */
onMounted(() => setKeepScreenOn({keepScreenOn: true}));
/** 监听频道人数,人数为1时,自动退出频道 (目前直接用agora stream替代)*/
watch(streamList, (current, last) => {
if(myCallState.value === 'free') return;
if(current.length > last.length) return;
if(current.length < 2) {
setTimeout(leave, 3000)
}
})
/** 切换麦克风使用状态 */
const [voiceMute, setVoiceMute] = useState(false);
const switchLocalMicState = () => {
if(!voiceMute.value) {
setVoiceMute(true)
muteLocalStream('audio');
} else {
setVoiceMute(false);
unmuteLocalStream('audio');
}
}
return { return {
streamList, streamList,
localPushurl localPushurl,
voiceMute,
leave,
switchLocalMicState,
} }
} }
\ No newline at end of file
import { useAuthData } from "any-hooks/auth/useAuthData"; import { useAuthData } from "any-hooks/auth/useAuthData";
import { useSocket } from "any-hooks/common/useSocket"; import { useSocket } from "any-hooks/common/useSocket";
import { AnyRemoteSocketMessage } from "any-hooks/types/socket"; import { AnyRemoteSocketMessage, SocketSettings } from "any-hooks/types/socket";
import { watch } from "vue"; import { watch } from "vue";
import { useInjector, useState } from "vue-vulcan"; import { useInjector, useState } from "vue-vulcan";
import { SOCKET_SETTINGS } from "./token";
enum NetStates { enum NetStates {
off = 0, off = 0,
...@@ -12,16 +13,19 @@ enum NetStates { ...@@ -12,16 +13,19 @@ enum NetStates {
best best
} }
export function useNetSocketCenter() { // 在线socket连接中心,会议中心、呼叫中心的基础
export function useNetSocketStore() {
const { authData } = useInjector(useAuthData); const { authData } = useInjector(useAuthData);
const [ netState, setNetState ] = useState<NetStates>(0); const [ netState, setNetState ] = useState<NetStates>(0);
const { connect, send, currentMsg, startHeartConnect } = useSocket<AnyRemoteSocketMessage>(); const { baseUrl } = useInjector<SocketSettings>(SOCKET_SETTINGS, 'optional');
const { connect, send, currentMsg, startHeartConnect, status } = useSocket<AnyRemoteSocketMessage>();
watch(authData, data => { watch(authData, data => {
console.log('authData', data)
if(!data) return; if(!data) return;
connect( connect(
`wss://www.if-ar.com:3009?fromID=${data.id}&fromName=${data.nickname}&signID=SA&companyID=${data.company_id}` `${baseUrl}?fromID=${data.id}&fromName=${data.nickname}&signID=SA&companyID=${data.company_id}`
) )
startHeartConnect({ startHeartConnect({
fromID: authData.value.id, fromID: authData.value.id,
...@@ -44,6 +48,7 @@ export function useNetSocketCenter() { ...@@ -44,6 +48,7 @@ export function useNetSocketCenter() {
sendMsg, sendMsg,
setNetState, setNetState,
currentMsg, currentMsg,
netState netState,
status
} }
} }
\ No newline at end of file
import { UserData } from "../types/user"; import { UserData } from "../types/user";
import { useInjector, useRequest } from "vue-vulcan"; import { useInjector, useRequest, useState } from "vue-vulcan";
import { computed, onMounted, watch} from "vue"; import { computed, watch} from "vue";
import { useAuthData } from "../auth/useAuthData"; import { useAuthData } from "../auth/useAuthData";
import { useNetSocketStore } from "any-hooks/communication/useNetSocketStore";
export function useContacts() { export function useContacts() {
const { authData } = useInjector(useAuthData); const { authData } = useInjector(useAuthData);
const [ userList, getUserList ] = useRequest<UserData[]>('/getUserList', { auto: false }); const [ userList, getUserList ] = useRequest<UserData[]>('/getUserList', { auto: false });
const { currentMsg } = useInjector(useNetSocketStore);
const [keyword, setKeyWord] = useState('');
onMounted( () => { const contacts = computed( () => {
if(authData) getUserList(); return userList.value?.filter( item => item.id !== authData.value?.id )
}) .filter( item => item.nickname.includes(keyword.value))
});
watch(authData, val => { const getContactById = (id: string) => {
if(val) getUserList() const index = userList.value.findIndex( user => user.id === id );
}) return userList.value[index];
}
const contacts = computed( () => userList.value?.filter( item => item.id !== authData.value.id )) const getContactsByGroup = (groupId: string) => {
return userList.value.filter( item => item.diy_group === groupId )
}
// 监听socket的home类型消息 实时更新contacts联系人数据
watch(currentMsg, msg => {
if(msg.msgMainFlag !== 'Home') return;
if(msg.msgSubFlag === 'UpdateUserList') getUserList();
})
return { return {
contacts contacts,
setKeyWord,
keyword,
getUserList,
getContactById,
getContactsByGroup
} }
} }
\ No newline at end of file
import { WorkGroupData } from "any-hooks/types/user";
import { useRequest } from "vue-vulcan";
export function useWorkGroups() {
const [groups, requestGroups] = useRequest<WorkGroupData[]>('/getWorkGroupList');
return {
groups,
requestGroups
}
}
\ No newline at end of file
type AgoraEventType = 'stream-added' | 'stream-removed' | 'update-url"' export type StreamType = 'video' | 'audio' | 'all';
export interface AgoraSdkClient { export interface AgoraSdkClient {
init: (appid: string, success?: any, err?: any) => void; init: (appid: string, success?: any, err?: any) => void;
join: (token: string, cid: string, uid: string, success?: any, err?: any) => void; join: (token: string, cid: string, uid: number, success?: any, err?: any) => void;
rejoin: (cb: any) => void; rejoin: (cb: any) => void;
publish(onSuccess: (url: string) => void, onFailure?: (err: { code: number; reason: string }) => void): void; publish(onSuccess: (url: string) => void, onFailure?: (err: { code: number; reason: string }) => void): void;
on(event: "error", callback: (evt: { code: number; reason: string }) => void): void; on(event: "error", callback: (evt: { code: number; reason: string }) => void): void;
on(event: AgoraEventType, callback: (evt: { uid: string, url?: string }) => void): void; on(event: "stream-added", callback: (evt: { uid: number, url?: string }) => void): void;
on(event: "stream-removed", callback: (evt: { uid: number[] }) => void): void;
off: (cb: any) => void; off: (cb: any) => void;
subscribe(uid: string, onSuccess: (url: string) => void, onFailure?: (err: any) => void): void subscribe(uid: number, onSuccess: (url: string) => void, onFailure?: (err: any) => void): void;
leave: (cb: any) => void; mute(uid: number, target: string, onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void;
unmute(uid: number, target: string, onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void;
muteLocal(target: StreamType, onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void;
unmuteLocal(target: string, onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void;
leave(onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void
destory: (cb: any) => void; destory: (cb: any) => void;
} }
export interface AgoraAppStore { export interface AgoraAppStore {
appid: string; appid: string;
platform: 'web' | 'mini-app' | 'electron';
client: AgoraSdkClient; client: AgoraSdkClient;
stream?: any;
} }
...@@ -4,18 +4,20 @@ export interface CustomeSocket<S> { ...@@ -4,18 +4,20 @@ export interface CustomeSocket<S> {
send: (data: S) => void; send: (data: S) => void;
close: () => void; close: () => void;
addEventListener<K extends keyof WebSocketEventMap>(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; addEventListener<K extends keyof WebSocketEventMap>(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof WebSocketEventMap>(type: K, listener?: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
} }
export interface SocketSettings { export interface SocketSettings {
retryLimit?: number; retryLimit?: number;
heartData?: any; heartData?: any;
heartInterval?: number; heartInterval?: number;
baseUrl: string;
} }
export type ConnectStatus = 'ready' | 'opening' | 'onerror' | 'closed' | 'reconnect'; export type ConnectStatus = 'ready' | 'opening' | 'onerror' | 'closed' | 'reconnect';
export type AnyRemoteMainFlag = 'Heart' | 'Login' | 'Broadcast' | 'Home' | 'CallOffer' | 'CallAnswer'; export type AnyRemoteMainFlag = 'Heart' | 'Login' | 'Broadcast' | 'Home' | 'CallOffer' | 'CallAnswer' | 'ChannelChat';
export type AnyRemoteSubFlag = 'Request' | 'Busying' | 'Connect' | 'Hangup'; export type AnyRemoteSubFlag = 'Request' | 'Busying' | 'Connect' | 'Hangup' | 'UpdateUserList';
export interface AnyRemoteSocketMessage { export interface AnyRemoteSocketMessage {
fromID?: string; fromID?: string;
......
...@@ -8,4 +8,24 @@ export interface UserData { ...@@ -8,4 +8,24 @@ export interface UserData {
token?: string; token?: string;
is_signin?: '1' | '2'; is_signin?: '1' | '2';
is_calling?: '1' | '0'; is_calling?: '1' | '0';
group_name?: string;
diy_group?: string;
phone?: string;
email?: string;
}
export interface WorkGroupData {
id: string;
title: string;
create_time: string;
update_time: string;
sort: string;
pid: string;
pid_list: string;
sid_list: string;
node_deep: string;
company_id: string;
progeny_ids: string;
ancestry_ids: string;
title_show: string;
} }
\ No newline at end of file
...@@ -4,7 +4,8 @@ export default { ...@@ -4,7 +4,8 @@ export default {
'pages/login/index', 'pages/login/index',
'pages/meeting/index', 'pages/meeting/index',
'pages/mine/index', 'pages/mine/index',
'pages/calling/index' 'pages/calling/index',
'pages/contact-detail/index'
], ],
window: { window: {
backgroundTextStyle: 'light', backgroundTextStyle: 'light',
......
view,text,input{
padding: 0;
margin: 0;
font-size: 30px;
}
.top-bg{
background-image: url(./assets/bg2x.png);
background-size: 100% auto;
background-repeat: no-repeat;
background-position-x: left;
&.mine{
background-image: url(./assets/bg-big3x.png);
}
}
.top-header{
padding-top: 55px !important;
}
page, .page{
height: 100%;
overflow: hidden;
// background-color: #f7f7f7;
}
.col-page{
height: 100%;
display: flex;
flex-direction: column;
}
.layout { .layout {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.full{
height: 100%;
width: 100%;
}
.white-box{
padding: 15px;
background: #fff;
}
.pd-1{
padding: 15px;
}
.pd-2{
padding: 25px;
}
.avatar{
width: 120px;
height: 120px;
border-radius: 50%;
}
.info-item{
display: flex;
justify-content: space-between;
.title{
color: #333;
}
.value{
color: #999;
}
}
\ No newline at end of file
import './app.less'; import './app.less';
import { authorize, } from '@tarojs/taro'; import { authorize } from '@tarojs/taro';
import { createApp } from 'vue'; import { createApp } from 'vue';
import { useCustomeRequest } from './hooks/http/useCustomeRequest'; import { useCustomeRequest } from './hooks/http/useCustomeRequest';
import { useProviders } from 'vue-vulcan'; import { useProviders } from 'vue-vulcan';
...@@ -12,32 +12,55 @@ import { useCustomeSocket } from './hooks/socket/useCustomeSocket'; ...@@ -12,32 +12,55 @@ import { useCustomeSocket } from './hooks/socket/useCustomeSocket';
import { useSocketSetting } from './hooks/socket/useSocketSettings'; import { useSocketSetting } from './hooks/socket/useSocketSettings';
import { useCallCenter } from 'any-hooks/communication/useCallCenter'; import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { useChannelStore } from 'any-hooks/communication/useChannelStore'; import { useChannelStore } from 'any-hooks/communication/useChannelStore';
import { useNetSocketCenter } from 'any-hooks/communication/useNetSocketCenter'; import { useNetSocketStore } from 'any-hooks/communication/useNetSocketStore';
import { useCallerListener } from './hooks/call/useCallerListener'; import { useCallerListener } from './hooks/call/useCallerListener';
import { useAgoraSDK } from './hooks/meeting/useAgoraSDK'; import { useAgoraSDK } from './hooks/meeting/useAgoraSDK';
import { useMeetingCenter } from 'any-hooks/communication/useMeetingCenter'; import { useMeetingCenter } from 'any-hooks/communication/useMeetingCenter';
import { useContacts } from 'any-hooks/contacts/useContacts';
import { useAppInitInfo } from './hooks/common/useAppInitInfo';
import { useSocketErrorHandle } from './hooks/socket/useSocketErrorHandle';
import dataEmpty from './components/data-empty.vue';
import MeetingBar from './components/mini-meeting-bar.vue';
const App = createApp({ const App = createApp({
onShow () {}, onShow () {
},
setup() { setup() {
/* 提供全局配置类hook */
useProviders( useProviders(
useCustomeStorage, //自定义基于taro的storage的接口,覆盖useState默认的sessionStorage/localStorage方法
useCustomeRequest, //自定义基于taro.request的请求接口,覆盖useRequet默认的fetch方法
useAuthData, //全局的用户权限数据hook
useHttpIntercept, //提供http请求拦截器
useRequestOption, //提供http请求配置项 useRequestOption, //提供http请求配置项
useSocketSetting, //提供小程序socket通讯的全局配置 useSocketSetting, //提供小程序socket通讯的全局配置
useCustomeSocket, //自定义基于taro的socket通讯接口,覆盖useSocket默认的websokcet
useNetSocketCenter, //全局的socket连接中心
useChannelStore, //全局的频道信息
useCallCenter, //全局的多人呼叫
useCallerListener, //在整个APP生命周期内监听远程联系人的呼叫请求
useAgoraSDK, //提供声网skd配置信息 useAgoraSDK, //提供声网skd配置信息
useMeetingCenter //全局的多人会议中心 useAppInitInfo
)
/* 提供用于覆盖原生api的hook(如 fetch/webSocket) */
useProviders(
useCustomeStorage, //自定义基于taro的storage的接口,覆盖useState内置的sessionStorage/localStorage接口
useCustomeRequest, //自定义基于taro.request的请求接口,覆盖useRequet内置的fetch接口
useCustomeSocket, //自定义基于taro的socket通讯接口,覆盖useSocket内置的websokcet接口
)
/* 提供全局共享的业务逻辑hook */
useProviders(
useAuthData, //用户权限数据
useHttpIntercept, //http请求拦截器
useNetSocketStore, //socket连接中心
useChannelStore, //频道信息中心
useCallCenter, //多人呼叫中心
useMeetingCenter, //多人会议中心
useContacts //实时联系人数据
); );
authorize({ scope: 'scope.camera' }); authorize({ scope: 'scope.camera' });
useAuthCheck(); useAuthCheck();
useCallerListener();
useSocketErrorHandle();
} }
}) })
export default App App.component('data-empty', dataEmpty);
App.component('meeting-bar', MeetingBar);
export default App;
src/assets/add.png

235 Bytes | W: | H:

src/assets/add.png

224 Bytes | W: | H:

src/assets/add.png
src/assets/add.png
src/assets/add.png
src/assets/add.png
  • 2-up
  • Swipe
  • Onion skin
src/assets/answer.png

2.27 KB | W: | H:

src/assets/answer.png

1.59 KB | W: | H:

src/assets/answer.png
src/assets/answer.png
src/assets/answer.png
src/assets/answer.png
  • 2-up
  • Swipe
  • Onion skin
src/assets/call.png

1.1 KB | W: | H:

src/assets/call.png

1.2 KB | W: | H:

src/assets/call.png
src/assets/call.png
src/assets/call.png
src/assets/call.png
  • 2-up
  • Swipe
  • Onion skin
src/assets/experience.png

828 Bytes | W: | H:

src/assets/experience.png

352 Bytes | W: | H:

src/assets/experience.png
src/assets/experience.png
src/assets/experience.png
src/assets/experience.png
  • 2-up
  • Swipe
  • Onion skin
src/assets/recording.png

2.62 KB | W: | H:

src/assets/recording.png

2.14 KB | W: | H:

src/assets/recording.png
src/assets/recording.png
src/assets/recording.png
src/assets/recording.png
  • 2-up
  • Swipe
  • Onion skin
src/assets/voice.png

480 Bytes | W: | H:

src/assets/voice.png

2.09 KB | W: | H:

src/assets/voice.png
src/assets/voice.png
src/assets/voice.png
src/assets/voice.png
  • 2-up
  • Swipe
  • Onion skin
<template>
<view class="empty">
<image class="empty-icon" src="../assets/empty-state3x.png" mode="widthFix"></image>
</view>
</template>
<style lang="less">
.empty{
padding-top: 200px;
text-align: center;
.empty-icon{
width: 300px;
height: auto;
}
}
</style>
\ No newline at end of file
<script lang="ts" setup>
import { navigateTo } from "@tarojs/taro";
import { useMeetingCenter } from "any-hooks/communication/useMeetingCenter";
import { useInjector } from "vue-vulcan";
const { streamList } = useInjector(useMeetingCenter);
const gotoMeetingHome = () => {
navigateTo({url: '/pages/meeting/index'})
}
</script>
<template>
<cover-view class="meeting-bar" v-if="streamList.length">
<image @tap="gotoMeetingHome()" class="bar-icon" src="../assets/during-call3x.png" mode="widthFix"></image>
</cover-view>
</template>
<style lang="less">
.meeting-bar{
position: fixed;
bottom: 80px;
right: 5px;
.bar-icon{
width: 100px;
height: 100px;
box-shadow: 0 0 15px #eee;
}
}
</style>
\ No newline at end of file
<script lang="ts" setup>
import { useContacts } from 'any-hooks/contacts/useContacts';
import { UserData } from 'src/types/auth';
import { defineProps, onMounted } from 'vue';
import { useInjector, useState } from 'vue-vulcan';
const { id, url } = defineProps({id: String, url: String});
const { getContactById } = useInjector(useContacts);
const [ contact, setContact ] = useState<UserData>(null);
onMounted( () => {
console.log('player ready')
// console.log(props)
const currentContact = getContactById(id+'');
setContact(currentContact);
})
</script>
<template>
<view class="player-container" >
<live-player
class="full"
:src="url"
mode="RTC"
:autoplay="true"
:autoPauseIfNavigate="false"
:autoPauseIfOpenNative="false"
:backgroundMute="true"
waitingImage="https://webdemo.agora.io/away.png"
/>
<view class="cover" v-if="contact">
<text class="name">{{contact.nickname}}</text>
<!-- <image class="avatar" :src="contact.avatar"/> -->
</view>
</view>
</template>
<style lang="less">
.player-container{
position: relative;
width: 50%;
height: 280px;
background: #000;
.cover{
color: #fff;
position: absolute;
width: 100%;
height: 100%;
top:0;
left:0;
.avatar{
width: 100%;
height: 100%
}
.name{
position: absolute;
top: 10px;
left: 10px;
}
}
}
</style>
\ No newline at end of file
<script lang="ts" setup>
import { useAuthData } from 'any-hooks/auth/useAuthData';;
import { defineProps } from 'vue';
import { useInjector } from 'vue-vulcan';
const { authData } = useInjector(useAuthData);
const { url } = defineProps({url: String});
</script>
<template>
<view class="pusher-container" >
<live-pusher
class="full"
:autopush="true"
:url="url"
:local-mirror="'enable'"
mode="RTC"
waiting-image="https://webdemo.agora.io/away.png"
/>
<view class="cover" >
<text class="name">{{authData.nickname}}</text>
<!-- <image class="avatar" :src="authData.avatar"/> -->
</view>
</view>
</template>
<style lang="less">
.pusher-container{
position: relative;
width: 50%;
height: 280px;
background: #000;
.cover{
color: #fff;
position: absolute;
top: 0;
left:0;
width: 100%;
height: 100%;
.avatar{
width: 100%;
height: 100%
}
.name{
position: absolute;
top: 10px;
left: 10px;
}
}
}
</style>
\ No newline at end of file
import { navigateBack, navigateTo, redirectTo, showModal, showToast } from "@tarojs/taro"; import { navigateBack, navigateTo, redirectTo, showToast } from "@tarojs/taro";
import { useCallCenter } from "any-hooks/communication/useCallCenter"; import { useCallCenter } from "any-hooks/communication/useCallCenter";
import { watch } from "vue"; import { watch } from "vue";
import { useInjector } from "vue-vulcan"; import { useInjector } from "vue-vulcan";
...@@ -6,25 +6,25 @@ import { useInjector } from "vue-vulcan"; ...@@ -6,25 +6,25 @@ import { useInjector } from "vue-vulcan";
/** 在小程序平台监听远程联系人的呼叫动作 */ /** 在小程序平台监听远程联系人的呼叫动作 */
export function useCallerListener() { export function useCallerListener() {
const { caller, myCallState, answerCaller } = useInjector(useCallCenter); const { caller, target, myCallState } = useInjector(useCallCenter);
watch(caller, current => { watch(caller, current => {
switch(current.action) { switch(current.action) {
case 'Request': case 'Request':
showModal({ break;
content: `来自${current.nickname}的呼叫,是否接受?`, case 'Hangup':
cancelText: '拒绝', showToast({title: '对方取消了呼叫', icon: 'none'});
cancelColor: 'red', break;
confirmText: '接受', }
success: res => {
if(caller.value.action !== 'Request') return; //此次必须通过caller访问action值
res.confirm ? answerCaller('Connect') : answerCaller('Hangup');
},
fail: () => answerCaller('Hangup')
}) })
watch(target, val => {
switch(val.action) {
case 'Busying':
showToast({title: '对方繁忙,暂时无法接受呼叫', icon: 'none'});
break; break;
case 'Hangup': case 'Hangup':
showToast({title: '对方取消了呼叫,您可以点击接受关闭对话框', icon: 'none'}); showToast({title: '对方拒绝了您的呼叫', icon: 'none'});
break; break;
} }
}) })
...@@ -32,17 +32,16 @@ export function useCallerListener() { ...@@ -32,17 +32,16 @@ export function useCallerListener() {
// 根据用户的呼叫状态变化,执行相应的页面跳转逻辑 // 根据用户的呼叫状态变化,执行相应的页面跳转逻辑
watch(myCallState, state => { watch(myCallState, state => {
switch(state) { switch(state) {
case 'call_accepted': case 'calling':
navigateTo({url: '/pages/meeting/index'}); case 'being_called':
navigateTo({url: '/pages/calling/index'});
break; break;
case 'call_accepted':
case 'call_successed': case 'call_successed':
redirectTo({url: '/pages/meeting/index'}); redirectTo({url: '/pages/meeting/index'});
break; break;
case 'calling':
navigateTo({url: '/pages/calling/index'});
break;
case 'free': case 'free':
navigateBack({delta: 2}) setTimeout(() => navigateBack({delta: 2}), 1000);
break; break;
} }
}) })
......
import { getMenuButtonBoundingClientRect } from "@tarojs/taro";
export function useAppInitInfo() {
const rect = getMenuButtonBoundingClientRect();
return {
rect
}
}
\ No newline at end of file
import { useAuthData } from "../../../any-hooks/auth/useAuthData"; import { useAuthData } from "../../../any-hooks/auth/useAuthData";
import { HttpIntercept, HttpResponse, HTTP_INTERCEPT, RequestOptions, useInjector } from "vue-vulcan"; import { HttpIntercept, HttpResponse, HTTP_INTERCEPT, RequestOptions, useInjector } from "vue-vulcan";
import { showToast } from "@tarojs/taro";
export function useHttpIntercept(): HttpIntercept { export function useHttpIntercept(): HttpIntercept {
...@@ -8,7 +9,9 @@ export function useHttpIntercept(): HttpIntercept { ...@@ -8,7 +9,9 @@ export function useHttpIntercept(): HttpIntercept {
const requestIntercept = (reqs: RequestOptions) => { const requestIntercept = (reqs: RequestOptions) => {
console.log(reqs) console.log(reqs)
const token = authData.value?.token || ''; const token = authData.value?.token || '';
const company_id = authData.value?.company_id || '';
reqs.data.token = token; reqs.data.token = token;
reqs.data.company_id = company_id;
return new Promise( (resolve, reject) => { return new Promise( (resolve, reject) => {
if(token || reqs.path.includes('login')) { if(token || reqs.path.includes('login')) {
resolve(reqs) resolve(reqs)
...@@ -18,9 +21,17 @@ export function useHttpIntercept(): HttpIntercept { ...@@ -18,9 +21,17 @@ export function useHttpIntercept(): HttpIntercept {
}) })
} }
const checkResData = (data: any) => {
if(data.success) return true;
showToast({title: data.msg, icon: 'none'});
return false;
}
const responseIntercept = (res: HttpResponse) => { const responseIntercept = (res: HttpResponse) => {
return new Promise( resolve => { return new Promise( resolve => {
if(checkResData(res.data)) {
resolve(res.data.data) resolve(res.data.data)
}
}) })
} }
......
...@@ -6,7 +6,7 @@ const appid = '0a67966ff4ae4eb3b32446df0151e16a'; ...@@ -6,7 +6,7 @@ const appid = '0a67966ff4ae4eb3b32446df0151e16a';
const client = new AgoraMiniappSDK.Client(); const client = new AgoraMiniappSDK.Client();
export function useAgoraSDK(): AgoraAppStore { export function useAgoraSDK(): AgoraAppStore {
return { appid, client } return { appid, client, platform: 'mini-app' }
} }
useAgoraSDK.token = AGORA_APP_SDK; useAgoraSDK.token = AGORA_APP_SDK;
\ No newline at end of file
import { closeSocket, connectSocket, onSocketClose, onSocketError, onSocketMessage, onSocketOpen, sendSocketMessage } from "@tarojs/taro"; import { closeSocket, connectSocket, onSocketClose, onSocketError, onSocketMessage, onSocketOpen, sendSocketMessage } from "@tarojs/taro";
import { CUSTOME_SOCKET } from "any-hooks/communication/token"; import { CUSTOME_SOCKET } from "any-hooks/communication/token";
import { CustomeSocket } from "any-hooks/types/socket"; import { AnyRemoteSocketMessage, CustomeSocket } from "any-hooks/types/socket";
export function useCustomeSocket(): CustomeSocket { export function useCustomeSocket(): CustomeSocket<AnyRemoteSocketMessage> {
return { return {
connect(url: string) { connect(url: string) {
connectSocket({url}) connectSocket({url})
......
import { watch } from "@vue/runtime-core";
import { useNetSocketStore } from "any-hooks/communication/useNetSocketStore";
import { useInjector } from "vue-vulcan";
export function useSocketErrorHandle() {
const { status } = useInjector(useNetSocketStore);
watch(status, val => {
console.log('socket status', val)
})
}
\ No newline at end of file
...@@ -4,16 +4,10 @@ import { SocketSettings } from "any-hooks/types/socket"; ...@@ -4,16 +4,10 @@ import { SocketSettings } from "any-hooks/types/socket";
// import { useInjector } from "vue-vulcan"; // import { useInjector } from "vue-vulcan";
export function useSocketSetting(): SocketSettings { export function useSocketSetting(): SocketSettings {
// const { authData } = useInjector(useAuthData);
return { return {
// heartData: { retryLimit: 10,
// msgMainFlag:"Heart", baseUrl: 'wss://www.if-ar.com:3009'
// fromID: authData.value.id,
// fromName: authData.value.nickname,
// toID: '0'
// },
retryLimit: 10
} }
} }
......
<script setup> <script lang="ts" setup>
import { useInjector } from 'vue-vulcan'; import { useInjector } from 'vue-vulcan';
import { computed, getCurrentInstance, onMounted, watch } from 'vue';
import { useCallCenter } from 'any-hooks/communication/useCallCenter'; import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { navigateBack, showToast } from '@tarojs/taro';
const { target, hangup } = useInjector(useCallCenter);
watch(target, val => {
if(val.action === 'Hangup') {
showToast({
title: '对方拒绝了您的呼叫请求',
icon: 'none'
});
}
})
const { target, caller, myCallState, answerCaller, hangup } = useInjector(useCallCenter);
</script> </script>
<template> <template>
<view class="page"> <view class="page">
<view class="call-box"> <view class="call-box caller" v-if="myCallState==='being_called'">
<image class="avatar" :src="caller?.avatar"></image>
<text class="tips" >{{caller?.nickname}}向您发起呼叫</text>
</view>
<view class="call-box target" v-if="myCallState==='calling'">
<image class="avatar" :src="target?.avatar"></image> <image class="avatar" :src="target?.avatar"></image>
<text class="tips">正在呼叫{{target?.nickname}}...</text> <text class="tips">正在呼叫{{target?.nickname}}...</text>
</view> </view>
<image @tap="hangup()" class="hangup" src="../../assets/hangup.png"></image>
<view class="operators" v-if="myCallState!=='free'">
<image v-if="myCallState==='being_called'" @tap="answerCaller('Connect')" class="op-icon" src="../../assets/answer3x.png"></image>
<image @tap="hangup()" class="op-icon" src="../../assets/hangup.png"></image>
</view>
</view> </view>
</template> </template>
...@@ -56,14 +53,19 @@ page{ ...@@ -56,14 +53,19 @@ page{
font-size: 38px; font-size: 38px;
} }
} }
.hangup{ .operators{
width: 120px;
height: 120px;
display: block;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
position: absolute; position: absolute;
bottom: 100px; bottom: 100px;
display: flex;
justify-content: center;
.op-icon{
width: 120px;
height: 120px;
display: block;
margin: 0 120px;
}
} }
} }
</style> </style>
\ No newline at end of file
export default {
navigationBarTitleText: '联系人详情'
}
<script lang="ts" setup>
import MeetingBar from '../../components/mini-meeting-bar.vue';
import { getCurrentInstance } from '@tarojs/taro';
import { onMounted } from '@vue/runtime-core';
import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { useContacts } from 'any-hooks/contacts/useContacts';
import { UserData } from 'any-hooks/types/user';
import { useInjector, useState } from 'vue-vulcan';
const { getContactById } = useInjector(useContacts);
const { callContact } = useInjector(useCallCenter);
const [currentContact, setContact] = useState<UserData>(null)
onMounted( () => {
const ins = getCurrentInstance();
const id = ins.router.params.id;
const current = getContactById(id);
setContact(current);
})
</script>
<template>
<view class="contact-detail" v-if="currentContact">
<view class="base-info white-box pd-2">
<image class="avatar" :src="currentContact.avatar"></image>
<view class="text-info">
<view class="nickname">{{currentContact.nickname}}</view>
<view class="role">{{currentContact.group_name}}</view>
</view>
</view>
<view class="extend-info white-box">
<view class="info-item pd-2">
<text class="title">手机号</text>
<text class="value">{{currentContact.phone}}</text>
</view>
<view class="info-item pd-2">
<text class="title">邮箱</text>
<text class="value">{{currentContact.email}}</text>
</view>
</view>
<view class="call-btn" @tap="callContact(currentContact)" v-if="currentContact.is_signin==='1'">
<image class="call-icon" src="../../assets/call-small2x.png"></image>
<text>视频通话</text>
</view>
</view>
<meeting-bar></meeting-bar>
</template>
<style lang="less">
.contact-detail{
.base-info{
display: flex;
align-items: center;
padding: 45px 25px;
border-bottom: 2px solid #efefef;
.text-info{
padding-left: 30px;
}
}
.call-btn{
background-color: #2b91e2;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
padding: 25px 0;
margin: 15px;
margin-top: 40px;
border-radius: 8px;
.call-icon{
width: 30px;
height: 40px;
}
}
}
</style>
\ No newline at end of file
<script lang="ts" setup>
import { useInjector } from 'vue-vulcan';
import { useContacts } from 'any-hooks/contacts/useContacts';
import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { navigateTo } from '@tarojs/taro';
const { callContact } = useInjector(useCallCenter);
const { contacts } = useInjector(useContacts);
const gotoDetail = (id: string) => {
navigateTo({url: '/pages/contact-detail/index?id='+ id})
}
</script>
<template>
<view class="contact-list pd-1">
<view class="contact-item" v-for="(item, key) in contacts" :key="key" @tap="gotoDetail(item.id)">
<image class="avatar" :src="item.avatar" />
<view class="info">
<view>{{item.nickname}}</view>
<text>{{item.group_name}}</text>
</view>
<image v-if="item.is_signin === '1'" @tap.stop="callContact(item)" class="call" src="../../assets/call3x.png" mode="widthFix"/>
<image v-else class="call" src="../../assets/call-no3x.png" mode="widthFix"/>
</view>
<data-empty v-if="!contacts?.length"></data-empty>
</view>
</template>
\ No newline at end of file
export default { export default {
navigationStyle: 'custom',
navigationBarTitleText: '首页' navigationBarTitleText: '首页'
} }
\ No newline at end of file
<script setup lang="ts"> <script setup lang="ts">
import { useInjector } from 'vue-vulcan'; import MeetingBar from '../../components/mini-meeting-bar.vue';
import { useContacts } from '../../../any-hooks/contacts/useContacts'; import { useInjector, useState } from 'vue-vulcan';
import { useCallCenter } from 'any-hooks/communication/useCallCenter'; import ContactsList from './contacts-list.vue';
import WorkGroupsList from './work-groups-list.vue';
import { useAppInitInfo } from 'src/hooks/common/useAppInitInfo';
import { ref } from '@vue/reactivity';
import { useThrottle } from 'any-hooks/common/useThrottle';
import { useContacts } from 'any-hooks/contacts/useContacts';
const { callContact } = useInjector(useCallCenter); const [showLogo, setShowLogo] = useState(true);
const { contacts } = useContacts(); const [viewMode, setViewMode] = useState<'items' | 'groups'>('items');
const { keyword } = useInjector(useContacts);
const { rect } = useInjector(useAppInitInfo);
const capTop = ref(`${rect.top}px`);
const capHeight = ref(rect.height-2);
const capWidth = ref(`${rect.width+15}px`);
const onScroll = useThrottle((event: any) => {
setShowLogo(event.detail.deltaY >= 0);
}, 500)
</script> </script>
<template> <template>
<view class="index"> <view class="index page col-page top-bg">
<view class="contact-list"> <view class="search-box" :style="{'marginTop': capTop, 'marginRight': capWidth, height: capHeight+'px'}">
<view class="contact-item" v-for="(item, key) in contacts" :key="key"> <image class="logo" v-if="showLogo" src="../../assets/logo3x.png" mode="widthFix"></image>
<image class="avatar" :src="item.avatar" /> <view class="input-box" :style="{height: capHeight+'px','borderRadius': capHeight/2+'px'}">
<view class="info"> <image class="search-icon" src="../../assets/serch2x.png" mode="widthFix"></image>
<view>{{item.nickname}}</view> <input class="search" type="text" placeholder="搜索" v-model="keyword">
<text>{{item.permission}}</text>
</view> </view>
<image @tap="callContact(item)" class="call" src="../../assets/call.png" />
</view> </view>
<scroll-view class="home-contents" :scrollY="true" @scroll="onScroll">
<view class="tabs pd-1">
<text class="tab-item contacts" :class="{active: viewMode==='items'}" @tap="setViewMode('items')">联系人</text>
<text class="tab-item group" :class="{active: viewMode==='groups'}" @tap="setViewMode('groups')">群组</text>
</view>
<view class="list white-box">
<contacts-list v-if="viewMode === 'items'"></contacts-list>
<work-groups-list v-else></work-groups-list>
</view> </view>
</scroll-view>
</view> </view>
<meeting-bar></meeting-bar>
</template> </template>
<style lang="less" >
.search-box{
display: flex;
align-items: center;
padding-left: 15px;
.logo{
width: 200px;
height: 28px;
}
.input-box{
flex: 1;
box-sizing: border-box;
margin-left: 15px;
background: #fff;
display: flex;
align-items: center;
padding: 5px 10px;
.search-icon{
width: 32px;
height: 32px;
padding: 8px;
}
}
}
<style lang="less"> .home-contents{
flex: 1;
height: 100px;
margin-top: 20px;
.tabs{
.tab-item{
color: #fff;
margin-right: 25px;
font-size: 34px;
&.active{
font-size: 42px;
// font-weight: bold;
}
}
}
.list{
border-top-right-radius: 20px;
border-top-left-radius: 20px;
min-height: 600px;
}
.contact-list{ .contact-list{
padding: 20px 30px;
.contact-item{ .contact-item{
margin: 25px 0; margin: 30px 0;
padding: 25px 0; padding: 30px 0;
border-bottom: 1px solid #f7f7f7; border-bottom: 1px solid #f8f8f8;
display: flex; display: flex;
align-items: center; align-items: center;
.avatar{ .avatar{
width: 100px; width: 80px;
height: 100px; height: 80px;
border-radius: 50%; border-radius: 50%;
overflow:hidden; overflow:hidden;
} }
.info{ .info{
flex: 1; flex: 1;
padding: 0 30px; padding: 0 20px;
} }
.call{ .call{
width: 80px; width: 65px;
height: 80px; height: 65px;
}
} }
} }
} }
</style> </style>
<script lang="ts" setup>
import { useWorkGroups } from 'any-hooks/contacts/useWorkGroups';
const { groups } = useWorkGroups();
</script>
<template>
<view class="work-group-list">
<view class="group-item pd-1" v-for="(item, key) in groups" :key="key">
<image class="group-icon" src="../../assets/group3x.png" mode="widthFix"/>
<text class="group-name">{{item.title}}</text>
</view>
<data-empty v-if="!groups?.length"></data-empty>
</view>
</template>
<style lang="less">
.work-group-list{
.group-item{
display: flex;
align-items: center;
margin-bottom: 30px;
.group-icon{
width: 72px;
height: 62px;
margin-right: 20px;
}
.group-name{
font-weight: bold;
}
}
}
</style>
\ No newline at end of file
export default { export default {
navigationBarTitleText: '登录页' navigationBarTitleText: 'anyremote'
} }
<script setup> <script setup>
import { watch } from 'vue'; import { computed, watch } from 'vue';
import { navigateTo, navigateBack } from '@tarojs/taro'; import { navigateTo, navigateBack } from '@tarojs/taro';
import { useLogin } from '../../../any-hooks/auth/useLogin'; import { useLogin } from '../../../any-hooks/auth/useLogin';
const { loginData, submitLogin } = useLogin(); const { loginData, submitLogin } = useLogin();
const allowSubmit = computed(() => !!loginData.login_id && loginData.login_password)
const onSubmit = () => { const onSubmit = () => {
submitLogin().then( _ => navigateBack()) submitLogin().then( _ => navigateBack())
...@@ -11,27 +12,50 @@ ...@@ -11,27 +12,50 @@
</script> </script>
<template> <template>
<view> <view class="page white-box">
<view class="titles">
<view class="header-title">欢迎使用anyremote</view>
<text class="sub-title">请输入您的账号密码</text>
</view>
<view class="login-box"> <view class="login-box">
<input class="login-item" placeholder="请输入用户名" v-model="loginData.login_id"> <input class="login-item" placeholder="请输入您的账号" v-model="loginData.login_id">
<input class="login-item" placeholder="请输入密码" v-model="loginData.login_password"> <input class="login-item" placeholder="请输入密码" v-model="loginData.login_password">
<button @tap="onSubmit()">登录</button> <button class="login-btn" @tap="onSubmit()" :disabled="!allowSubmit" :class="{allow: allowSubmit}">登录</button>
</view> </view>
</view> </view>
</template> </template>
<style lang="less"> <style lang="less">
.titles{
margin: 60px 0;
.header-title{
font-size: 46px;
font-weight: bold;
padding-bottom: 20px;
}
.sub-title{
color: #999;
color: 28px;
}
}
.login-box{ .login-box{
background: #999; padding-top: 20px;
border-radius: 10px;
padding: 10px;
width: 90%;
position: absolute;
top:50%;
left:50%;
transform: translate(-50%,-50%);
.login-item{ .login-item{
margin: 20px 0; display: block;
margin: 30px 0;
padding: 15px;
border: 1px solid #efefef;
}
button.login-btn{
display: block;
padding: 8px 0;
margin-top: 80px;
font-size: 32px;
color: #999;
&.allow{
background: #2b91e2;
color: #fff;
}
} }
} }
</style> </style>
\ No newline at end of file
export default { export default {
navigationBarTitleText: '通讯页' navigationStyle: 'custom'
} }
<script setup> <script lang="ts" setup>
import { useMeetingCenter } from 'any-hooks/communication/useMeetingCenter'; import { useMeetingCenter } from 'any-hooks/communication/useMeetingCenter';
import { onMounted, watch } from 'vue';
import { useInjector } from 'vue-vulcan'; import { useInjector } from 'vue-vulcan';
import StreamPlayer from 'src/components/stream-player.vue';
import StreamPusher from 'src/components/stream-pusher.vue';
import { createLivePusherContext, navigateBack, showModal, showToast } from '@tarojs/taro';
import { watch } from '@vue/runtime-core';
const { streamList, localPushurl, voiceMute, switchLocalMicState, leave } = useInjector(useMeetingCenter);
watch(streamList, (current, last) => {
if(current.length > last.length) return;
if(current.length < 2) {
showToast({title: '当前频道已无其他联系人,即将自动退出...', icon: 'none'})
}
})
const onTapLeave = () => {
showModal({
title: '是否退出此次通讯?',
success: res => {
if(res.confirm) leave();
}
})
}
/** 切换摄像头 */
const context = createLivePusherContext();
const switchCamera = () => {
context.switchCamera();
}
/** 返回上一页 */
const backToIndex = () => {
navigateBack({delta: 2})
}
const { streamList, localPushurl } = useInjector(useMeetingCenter);
</script> </script>
<template> <template>
<view class ="meeting-container layout"> <view class ="meeting-container page col-page">
<view <view class="videos">
class="players" <stream-player v-for="(item, key) in streamList" :key="key" :id="item.uid.toString()" :url="item.url"></stream-player>
v-for="(item, key) in streamList" <stream-pusher v-if="localPushurl" :url="localPushurl"></stream-pusher>
:key="key"
>
<live-player v-if="item.uid" :src="item.url" mode="RTC" style="width: 100%;height:200px;" waiting-image="https://webdemo.agora.io/away.png"/>
</view> </view>
<live-pusher :autopush="true" :url="localPushurl" :local-mirror="'enable'" mode="RTC" style="width: 100%;height:100px;" />
<view class="operators pd-2">
<view class="op-item">
<image v-if="voiceMute" @tap="switchLocalMicState()" class="x-o-icon" src="../../assets/Microphone-on3x.png" />
<image v-else @tap="switchLocalMicState()" class="x-o-icon" src="../../assets/microphone3x.png"/>
<text>麦克风</text>
</view>
<view class="op-item">
<image class="x-o-icon" src="../../assets/recording3x.png"></image>
<text>录制</text>
</view>
<view class="op-item">
<image class="x-o-icon" src="../../assets/voice3x.png"></image>
<text>语音通话</text>
</view>
<view class="op-item" @tap="backToIndex()">
<image class="s-o-icon" src="../../assets/minimize3x.png" mode="heightFix"></image>
</view>
<view class="op-item" @tap="onTapLeave()">
<image class="x-o-icon" src="../../assets/hang-up-big2x.png"></image>
</view>
<view class="op-item" @tap="switchCamera()">
<image class="s-o-icon" src="../../assets/switch-camera3x.png" mode="heightFix"></image>
</view>
</view>
</view> </view>
</template> </template>
<style> <style lang="less">
page{
width: 100%;
height: 100%;
}
.meeting-container{ .meeting-container{
background: #333; background: #333;
.videos{
display: flex;
flex: 1;
flex-wrap: wrap;
}
}
.operators{
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
.op-item{
margin: 20px;
width: 24%;
color: #fff;
text-align: center;
image{
display: block;
margin: 0 auto;
}
text{
display: inline-block;
margin-top: 10px;
font-size: 26px;
}
.x-o-icon{
width: 120px;
height: 120px;
}
.s-o-icon{
height: 36px;
}
}
} }
</style> </style>
\ No newline at end of file
export default {
navigationStyle: 'custom',
navigationBarTitleText: '首页'
}
<script setup> <script setup>
import MeetingBar from '../../components/mini-meeting-bar.vue';
import { navigateTo, showModal } from '@tarojs/taro';
import { useAuthData } from 'any-hooks/auth/useAuthData';
import { useLogOut } from 'any-hooks/auth/useLogOut';
import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { useInjector } from 'vue-vulcan';
const { authData } = useInjector(useAuthData);
const { submitLogOut } = useLogOut();
const onLogOut = () => {
showModal({
title: '是否确认退出当前账号?',
}).then( res => {
if(res.confirm) return submitLogOut({login_id: authData.value.id});
return null
}).then(res => {
res!== null && navigateTo({url: '/pages/login/index'});
})
}
</script> </script>
<template> <template>
<view>个人中心</view> <view class="mine page" v-if="authData">
<view class="profile pd-2 top-bg mine">
<image class="mine-avatar" :src="authData.avatar"></image>
<view class="mine-info">
<view>{{authData.nickname}}</view>
<view>{{authData.group_name}}</view>
</view>
<view class="logout" @tap="onLogOut()">
切换账号
</view>
</view>
<view class="menus pd-2">
<view class="menu-item pd-2">
<text>帮助文档</text>
<image class="arrow icon" src="../../assets/arrow-right2x.png"></image>
</view>
</view>
</view>
<meeting-bar></meeting-bar>
</template> </template>
<style lang="less">
.mine{
background-color: #f6f6f6;
.profile{
// border: 1px solid red;
padding:140px 25px;
padding-top: 200px;
display: flex;
align-items: center;
color: #fff;
.mine-avatar{
width: 120px;
height: 120px;
border-radius: 50%;
margin-right: 20px;
}
.mine-info{
flex: 1;
}
.logout{
color: #2b91e2;
padding: 15px 30px;
background-color: #fff;
border-radius: 8px;
}
}
.menus{
margin-top: -75px;
.menu-item{
background-color: #fff;
height: 70px;
line-height: 70px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: space-between;
.arrow.icon{
width: 15px;
height: 26px;
}
}
}
}
</style>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment