Commit 5d4a286a by wxl

呼叫功能完善

parent 38c0718d
import { UserData } from "src/types/auth";
import { useState } from "vue-vulcan";
export function useAuthData() {
const [authData, setAuth] = useState<UserData>({}, {storage: 'custome', key: 'auth'});
return {
authData,
setAuth
}
}
\ No newline at end of file
import { reactive } from "vue";
import { useInjector, useRequest } from "vue-vulcan";
import { useAuthData } from "./useAuthData";
interface UserData {
id: string;
company_id: string;
nickname: string;
avatar: string;
permission: string;
token: string;
}
export function useLogin() {
const loginData = reactive({
login_id: '',
login_password: ''
})
const [, request] = useRequest<UserData>('/loginIn', { auto: false });
const { setAuth } = useInjector(useAuthData);
const submitLogin = () => {
return request(loginData)
.then( data => setAuth(data) )
}
return {
loginData,
submitLogin
}
}
\ No newline at end of file
import { ConnectStatus, CustomeSocket, SocketSettings } from "../types/socket";
import { ref } from "vue";
import { useInjector, useState } from "vue-vulcan";
import { CUSTOME_SOCKET, SOCKET_SETTINGS } from "../communication/token";
export function useSocket<S = any>() {
const [status, setStatus] = useState<ConnectStatus>('ready');
const [currentMsg, setMsg] = useState<S>(null);
const customeSocket = useInjector<CustomeSocket<S>>(CUSTOME_SOCKET, 'optional');
const socketSettings = useInjector<SocketSettings>(SOCKET_SETTINGS, 'optional');
let ws: CustomeSocket<S>;
if(customeSocket) ws = customeSocket;
const connect = (url?: string) => {
if(customeSocket) {
ws.connect(url)
} else {
ws = new WebSocket(url) as unknown as CustomeSocket<S>
}
ws.addEventListener('message', event => setMsg(event.data));
ws.addEventListener('open', () => setStatus('opening'));
ws.addEventListener('open', () => socketSettings.heartData && startHeartConnect());
ws.addEventListener('close', () => setStatus('closed'));
ws.addEventListener('error', () => {
setStatus('onerror');
reconnect(url);
});
}
// 心跳链接
const startHeartConnect = () => {
const time = socketSettings.heartInterval;
setInterval( () => ws.send(socketSettings.heartData), time && time >= 3000 ? time : 5000 )
}
// 重连功能
const brokenTime = ref(0);
const limit = socketSettings.retryLimit || 5;
const reconnect = (url: string) => {
brokenTime.value ++;
if(brokenTime.value > limit) {
ws.close();
} else {
setStatus('reconnect');
setTimeout( () => connect(url), brokenTime.value*1000 );
}
}
return {
currentMsg,
status,
connect,
send: ws.send,
close: ws.close
}
}
\ No newline at end of file
export const AGORA_APP_STORE = Symbol('agora app store');
export const CUSTOME_SOCKET = Symbol('custome socket');
export const SOCKET_SETTINGS = Symbol('socket settings');
\ 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 { AgoraAppStore } from "any-hooks/types/agora";
import { useInjector } from "vue-vulcan";
import { AGORA_APP_STORE } from "./token";
/** 基于声网sdk封装,由于多平台特性,请在根组件通过useProvider和对应平台的SDK提供agora Client对象以及相关appid; */
export function useAgoraClient() {
const { appid, client } = useInjector<AgoraAppStore>(AGORA_APP_STORE);
return {
appid,
client
}
}
\ No newline at end of file
import { AnyRemoteSubFlag } from "any-hooks/types/socket";
import { UserData } from "any-hooks/types/user";
import { watch } from "vue";
import { useInjector, useState } from "vue-vulcan";
import { useChannelStore } from "./useChannelStore";
import { useNetSocketCenter } from "./useNetSocketCenter";
type CallingState = 'net_error' | 'calling' | 'call_successed' | 'call_accepted' | 'free';
interface Caller extends UserData {
action: AnyRemoteSubFlag | 'none';
channel?: string;
}
/** 呼叫中心功能,可以主动呼叫联系人、回应对方的呼叫,以及监听双方呼叫人的信息和状态 */
export function useCallCenter() {
const { sendMsg, currentMsg } = useInjector(useNetSocketCenter);
/** 主动呼叫功能 */
const [target, setTarget] = useState<Caller>(null);
const [myCallState, setCallState] = useState<CallingState>(null);
const {currentChannel, createChannel, joinCallingChannel} = useInjector(useChannelStore);
const callContact = (user: UserData) => {
setTarget({...user, action: 'none'}); //保存呼叫目标的信息
setCallState('calling'); //主动呼叫别人时将自己的呼叫状态更改为‘calling’
createChannel().then( _ => {
sendMsg({
toID: user.id,
toName: user.nickname,
channelID: currentChannel.value.channel_id,
msgMainFlag: 'CallOffer',
msgSubFlag: 'Request'
})
})
}
/** 回应呼叫功能 */
const answerCaller = (subFlag: AnyRemoteSubFlag) => {
if(subFlag === 'Connect') {
setCallState('call_accepted');
joinCallingChannel(caller.value.channel);
};
sendMsg({
toID: caller.value.id,
toName: caller.value.nickname,
msgMainFlag: 'CallAnswer',
msgSubFlag: subFlag
})
}
/** 呼叫监听功能 */
const [caller, setCaller] = useState<Caller>(null);
// 监听来自其他用户的呼叫
watch(currentMsg, msg => {
if(msg.msgMainFlag !== 'CallOffer') return;
if(myCallState.value !== 'free') answerCaller('Busying');
setCaller({
id: currentMsg.value.fromID,
nickname: currentMsg.value.fromName,
action: msg.msgSubFlag,
channel: msg.channelID
})
})
// 监听呼叫目标的回应
watch(currentMsg, msg => {
if(msg.msgMainFlag !== 'CallAnswer') return;
switch(msg.msgSubFlag) {
case 'Connect':
setCallState('call_successed');
break;
case 'Busying':
case 'Hangup':
setCallState('free');
target.value.action = msg.msgSubFlag;
break;
}
})
/** 呼叫挂断功能 */
const hangup = () => {
setCallState('free');
sendMsg({
channelID: currentChannel.value.channel_id,
msgMainFlag: myCallState.value === 'call_successed' ? 'CallOffer' : 'CallAnswer',
msgSubFlag: 'Hangup',
toID: myCallState.value === 'calling' ? target.value.id : caller.value.id
})
}
return {
/** 呼叫远程联系人 @param UserData */
callContact,
/** 回应远程联系人的呼叫 @param AnyRemoteSubFlag */
answerCaller,
/** 挂断当前呼叫 */
hangup,
/** 被本地用户呼叫目标联系人 */
target,
/** 向本地用户发起呼叫的远程联系人 */
caller,
/** 本地用户的呼叫状态 */
myCallState,
}
}
\ No newline at end of file
import { useState } from "../../common/useState";
import { useRequest } from "../../common/useRequest";
import { watch } from "vue"; import { watch } from "vue";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { useRequest, useState } from "vue-vulcan";
interface ChannelData { interface ChannelData {
channel_id: string; channel_id: string;
...@@ -9,14 +8,14 @@ interface ChannelData { ...@@ -9,14 +8,14 @@ interface ChannelData {
members: string[]; members: string[];
} }
export function useChannelInfo() { export function useChannelStore() {
const [current, setCurrent] = useState<ChannelData>(null); const [currentChannel, setCurrent] = useState<ChannelData>(null);
const [channelInfo, request] = useRequest<ChannelData>('/getAgoraToken', {}, {auto: false}); const [channelInfo, request] = useRequest<ChannelData>('/getAgoraToken', {auto: false});
const ready$ = new BehaviorSubject<boolean>(false); const ready$ = new BehaviorSubject<boolean>(false);
const createChannel = () => { const createChannel = () => {
request() return request()
} }
const joinCallingChannel = (id: string) => { const joinCallingChannel = (id: string) => {
request({channel_id: id}) request({channel_id: id})
...@@ -26,15 +25,13 @@ export function useChannelInfo() { ...@@ -26,15 +25,13 @@ export function useChannelInfo() {
ready$.subscribe( r => { r && callback() }) ready$.subscribe( r => { r && callback() })
} }
watch(channelInfo, val => { watch(channelInfo, val => {
console.log('channel', val) setCurrent(val);
setCurrent(val);
val && ready$.next(true);
}) })
return { return {
current, currentChannel,
joinCallingChannel, joinCallingChannel,
createChannel, createChannel,
onReady onReady
......
import { useAuthData } from "any-hooks/auth/useAuthData";
import { useSocket } from "any-hooks/common/useSocket";
import { AnyRemoteSocketMessage } from "any-hooks/types/socket";
import { watch } from "vue";
import { useInjector, useState } from "vue-vulcan";
enum NetStates {
off = 0,
bad,
normal,
good,
best
}
export function useNetSocketCenter() {
const { authData } = useInjector(useAuthData);
const { connect, send, currentMsg } = useSocket<AnyRemoteSocketMessage>();
const [ netState, setNetState ] = useState<NetStates>(0);
watch(authData, data => {
if(!data) return;
connect(
`wss://www.if-ar.com:3009?fromID=${data.id}&fromName=${data.nickname}&signID=SA&companyID=${data.company_id}`
)
})
const sendMsg = (data: AnyRemoteSocketMessage) => {
send({
...data,
fromID: authData.value.id,
fromName: authData.value.nickname,
avatar: authData.value.avatar
})
}
return {
sendMsg,
setNetState,
currentMsg,
netState
}
}
\ No newline at end of file
import { UserData } from "../types/user";
import { useInjector, useRequest } from "vue-vulcan";
import { computed, onMounted, watch} from "vue";
import { useAuthData } from "../auth/useAuthData";
export function useContacts() {
const { authData } = useInjector(useAuthData);
const [ userList, getUserList ] = useRequest<UserData[]>('/getUserList', { auto: false });
onMounted( () => {
if(authData) getUserList();
})
watch(authData, val => {
if(val) getUserList()
})
const contacts = computed( () => userList.value?.filter( item => item.id !== authData.value.id ))
return {
contacts
}
}
\ No newline at end of file
export interface AgoraSdkClient {
init: (cb: any) => void;
join: (cb: any) => void;
rejoin: (cb: any) => void;
publish: (cb: any) => void;
on: (cb: any) => void;
off: (cb: any) => void;
leave: (cb: any) => void;
destory: (cb: any) => void;
}
export interface AgoraAppStore {
appid: string;
client: AgoraSdkClient;
}
export interface CustomeSocket<S> {
connect?: (url: string) => void;
send: (data: S) => void;
close: () => void;
addEventListener<K extends keyof WebSocketEventMap>(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
}
export interface SocketSettings {
retryLimit?: number;
heartData?: any;
heartInterval?: number;
}
export type ConnectStatus = 'ready' | 'opening' | 'onerror' | 'closed' | 'reconnect';
export type AnyRemoteMainFlag = 'Heart' | 'Login' | 'Broadcast' | 'Home' | 'CallOffer' | 'CallAnswer';
export type AnyRemoteSubFlag = 'Request' | 'Busying' | 'Connect' | 'Hangup';
export interface AnyRemoteSocketMessage {
fromID?: string;
fromName?: string;
avatar?: string;
msgMainFlag: AnyRemoteMainFlag;
msgSubFlag?: AnyRemoteSubFlag;
msgData?: any;
toID: string;
toName?: string;
channelID?: string;
}
\ No newline at end of file
export interface UserData {
id: string;
company_id?: string;
nickname?: string;
avatar?: string;
permission?: string;
token?: string;
}
\ No newline at end of file
const path = require('path');
const config = { const config = {
projectName: 'anyremote-miniapp', projectName: 'anyremote-miniapp',
date: '2021-2-2', date: '2021-2-2',
...@@ -18,6 +20,10 @@ const config = { ...@@ -18,6 +20,10 @@ const config = {
options: { options: {
} }
}, },
alias: {
'any-hooks': path.resolve(__dirname, '..', 'any-hooks'),
'src': path.resolve(__dirname, '..', 'src'),
},
framework: 'vue3', framework: 'vue3',
mini: { mini: {
postcss: { postcss: {
......
declare module '*.png';
declare module '*.gif';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.svg';
declare module '*.css';
declare module '*.less';
declare module '*.scss';
declare module '*.sass';
declare module '*.styl';
declare namespace NodeJS {
interface ProcessEnv {
TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd'
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -35,30 +35,36 @@ ...@@ -35,30 +35,36 @@
], ],
"author": "", "author": "",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.7.7", "@babel/runtime": "^7.13.9",
"@tarojs/components": "3.0.26", "@tarojs/components": "3.0.26",
"@tarojs/extend": "^3.0.26", "@tarojs/extend": "^3.1.1",
"@tarojs/plugin-platform-alipay": "^3.1.1",
"@tarojs/plugin-platform-jd": "^3.1.1",
"@tarojs/plugin-platform-qq": "^3.1.1",
"@tarojs/plugin-platform-swan": "^3.1.1",
"@tarojs/plugin-platform-tt": "^3.1.1",
"@tarojs/plugin-platform-weapp": "^3.1.1",
"@tarojs/runtime": "3.0.26", "@tarojs/runtime": "3.0.26",
"@tarojs/taro": "3.0.26", "@tarojs/taro": "3.0.26",
"eminent-ui": "0.0.6", "eminent-ui": "0.0.6",
"rxjs": "^6.6.3", "rxjs": "^6.6.6",
"vue": "^3.0.0", "vue": "^3.0.7",
"vue-vulcan": "^0.1.0" "vue-vulcan": "^0.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/webpack-env": "^1.13.6", "@babel/core": "^7.13.8",
"@tarojs/mini-runner": "3.0.26", "@tarojs/mini-runner": "3.0.26",
"@babel/core": "^7.8.0",
"@tarojs/webpack-runner": "3.0.26", "@tarojs/webpack-runner": "3.0.26",
"@types/webpack-env": "^1.13.6",
"@typescript-eslint/eslint-plugin": "^2.x",
"@typescript-eslint/parser": "^2.x",
"@vue/compiler-sfc": "^3.0.7",
"babel-preset-taro": "3.0.26", "babel-preset-taro": "3.0.26",
"@vue/compiler-sfc": "^3.0.0",
"vue-loader": "^16.0.0-beta.8",
"eslint-plugin-vue": "^7.0.0",
"eslint-config-taro": "3.0.26",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-taro": "3.0.26",
"eslint-plugin-vue": "^7.6.0",
"stylelint": "9.3.0", "stylelint": "9.3.0",
"@typescript-eslint/parser": "^2.x", "typescript": "^3.9.9",
"@typescript-eslint/eslint-plugin": "^2.x", "vue-loader": "^16.0.0-beta.8"
"typescript": "^3.7.0"
} }
} }
import { authorize } from '@tarojs/taro';
import { createApp } from 'vue';
import './app.less'; import './app.less';
import { useProviders } from './hooks/common/useProvider'; import { authorize, } from '@tarojs/taro';
import { useCalling } from './hooks/store/video/useCalling'; import { createApp } from 'vue';
import { useCustomeRequest } from './hooks/http/useCustomeRequest';
import { useProviders } from 'vue-vulcan';
import { useRequestOption } from './hooks/http/useRequestOption';
import { useAuthCheck } from './hooks/user/useAuthCheck'; import { useAuthCheck } from './hooks/user/useAuthCheck';
import { useHttpIntercept } from './hooks/http/useHttpIntercept';
import { useCustomeStorage } from './hooks/storage/useCustomeStorage';
import { useAuthData } from 'any-hooks/auth/useAuthData';
import { useCustomeSocket } from './hooks/socket/useCustomeSocket';
import { useSocketSetting } from './hooks/socket/useSocketSettings';
import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { useChannelStore } from 'any-hooks/communication/useChannelStore';
import { useNetSocketCenter } from 'any-hooks/communication/useNetSocketCenter';
import { useCallerListener } from './hooks/call/useCallerListener';
const App = createApp({ const App = createApp({
onShow () {}, onShow () {},
// 入口组件不需要实现 render 方法,即使实现了也会被 taro 所覆盖 setup() {
setup() { useProviders(
useAuthCheck(); useCustomeStorage, //自定义基于taro的storage的接口,覆盖useState默认的sessionStorage/localStorage方法
useProviders(useCalling); useCustomeRequest, //自定义基于taro.request的请求接口,覆盖useRequet默认的fetch方法
authorize({ useAuthData, //全局的用户权限数据hook
scope: 'scope.camera' useHttpIntercept, //提供http请求拦截器
}) useRequestOption, //提供http请求配置项
useSocketSetting, //提供小程序socket通讯的全局配置
useCustomeSocket, //自定义基于taro的socket通讯接口,覆盖useSocket默认的websokcet
useNetSocketCenter, //全局的socket连接中心
useChannelStore, //全局的频道信息
useCallCenter, //全局的多人呼叫,
useCallerListener //在整个APP生命周期内监听远程联系人的呼叫请求
);
authorize({ scope: 'scope.camera' });
useAuthCheck();
} }
}) })
......
import { showModal } from "@tarojs/taro";
import { useCallCenter } from "any-hooks/communication/useCallCenter";
import { watch } from "vue";
import { useInjector } from "vue-vulcan";
/** 在小程序平台监听远程联系人的呼叫动作 */
export function useCallerListener() {
const { caller, answerCaller } = useInjector(useCallCenter);
watch(caller, value => {
if(caller) {
showModal({
title: `来自${value.nickname}的呼叫,是否接受?`,
success: () => answerCaller('Connect'),
fail: () => answerCaller('Hangup')
})
}
})
}
\ No newline at end of file
// export function useCanvasContext(selector: string) {
// let
// }
\ No newline at end of file
import { reactive, Ref, toRaw, UnwrapRef } from "vue";
import { useRequest, HttpStatus } from "./useRequest";
interface FormOption {
loadingTip?: string;
successTip?: string;
callback?: (res?: any) => void;
}
interface FormAction<T> {
submit: (data?: any) => void,
patchValues: (data: T, keys?: Array<keyof T>) => void
}
interface FormStates {
valid: boolean;
httpStatus: Ref<HttpStatus>;
}
export function useFormModel<T extends object, R = object>(
url: string, formData: T , option?: FormOption
): [T, FormAction<T>, FormStates, Ref<UnwrapRef<R>> ] {
const formDataRef = reactive<T>(formData) as T;
const [res, doRequest, httpStatus] = useRequest<R>(url, {}, {
auto: false,
successTip: option?.successTip || '提交成功',
loadingTip: option?.loadingTip || '正在提交'
});
/**
* @param data 要并入的表单对象
* @param keys 指定data哪些属性会被并入表单对象,若不传则会并入data对象所有属性
*/
const patchValues = (data: T , keys?: Array<keyof T>) => {
const patchkeys = keys ? keys : Object.keys(data)
for (const key of patchkeys) {
if (data[key as string]) {
formDataRef[key as keyof T] = data[key as string]
}
}
}
// 提交表单请求
const submit = (params: any = {}) => {
doRequest({...toRaw(formDataRef), ...params})
}
return [
formDataRef,
{
submit,
patchValues
},
{
valid: true,
httpStatus
},
res
]
}
import { FunctionalStore } from "../../types/common";
import { inject } from "vue";
type InjectType = 'root' | 'optional';
export function useInjector<T extends object>(func: FunctionalStore<T>, type?: InjectType) {
const token = func.token;
const root = func.root;
switch(type) {
default:
if(inject(token)) {
return inject<T>(token)
};
if(root) return func.root;
throw new Error(`状态钩子函数${func.name}未在上层组件通过调用useProvider提供`);
case 'optional':
if(!token) return null;
case 'root':
if(!func.root) func.root = func();
return func.root;
}
}
\ No newline at end of file
export function useLoginByAccounts() {
}
\ No newline at end of file
import { FunctionalStore } from "../../types/common";
import { provide } from "vue";
export function useProvider<T extends object>(func: FunctionalStore<T>): T {
!func.token && (func.token = Symbol('functional store'));
const depends = func();
provide(func.token, depends);
return depends;
}
export function useProviders(...funcs: FunctionalStore<any>[]) {
funcs.forEach( func => {
!func.token && (func.token = Symbol('functional store'));
provide(func.token, func());
});
}
\ No newline at end of file
import Taro, { navigateTo } from '@tarojs/taro';
import { useState } from "./useState";
import { ResBody } from '../../types/https';
import { from } from 'rxjs';
import { finalize, pluck, tap } from 'rxjs/operators';
import { onMounted, Ref, UnwrapRef, watch } from 'vue';
import { useInjector } from './useInjector';
import { useAuthStore } from '../store/auth/useAuthStore';
import { environment } from '../../contains/environment';
interface RequestOption {
auto?: boolean;
method?: 'GET' | 'POST' | 'OPTIONS';
pluck?: string[];
successTip?: string;
loadingTip?: string;
type?: 'ref' | 'reactive';
callback?: () => void;
}
export type HttpStatus = 'pending' | 'success' | 'failed' | 'error';
const defaultOption: RequestOption = {
auto: true,
method: 'POST',
pluck: ['data'],
type: 'ref',
}
// [Ref<UnwrapRef<T>>, (data?: any) => Observable<UnwrapRef<T>>, Ref<HttpStatus> ]
export function useRequest<T>(
url: string,
params: any = {},
options: RequestOption = defaultOption
): [Ref<UnwrapRef<T>>, (data?: any) => void, Ref<HttpStatus> ] {
const [ status, setStatus ] = useState<HttpStatus>(null);
const [ resData, setResData ] = useState<T>(null);
const opt = { ...defaultOption, ...options };
const { token } = useInjector(useAuthStore, 'root');
const authInfo = useInjector(useAuthStore, 'root');
onMounted(() => {
if (!opt.auto) { return };
doRequest();
})
watch(status, val => {
// Taro.hideToast()
switch (val ) {
case 'pending':
if (!opt.loadingTip) {return};
Taro.showToast({
title: opt.loadingTip,
icon: 'loading'
})
break;
case 'success':
if (!opt.successTip) return;
Taro.showToast({title: opt.successTip})
break;
case 'error':
case 'failed':
Taro.showToast({title: '请求失败', icon:'none'})
break;
}
})
const doRequest = (data: any = {}) => {
if(!token) {
navigateTo({url: 'pages/login/index'});
return;
}
setStatus('pending') ;
from(
Taro.request({
url: environment.baseUrl + url,
method: opt.method,
data: {...params, ...data, token: authInfo.token.value, uid: authInfo.user.value.id}
})
).pipe(
tap(console.log),
tap(
res => setStatus(res.data.success ?'success': 'failed'),
_ => {setStatus('error'); console.log(_)}
),
pluck<ResBody<T>, UnwrapRef<T>>('data', ...opt.pluck as any),
tap(setResData),
finalize(() => Taro.hideToast())
).subscribe()
}
return [ resData, doRequest, status ]
}
import { Ref, ref, UnwrapRef } from "vue";
interface Payload {
}
export function useState<T>(state: T): [Ref<UnwrapRef<T>>, (param: UnwrapRef<T>) => void ] {
const stateRef = ref(state);
function setState(payload: UnwrapRef<T>): void;
function setState(payload: (current: T) => T): void;
function setState(payload): void {
if(typeof payload === 'function') {
stateRef.value = payload(stateRef.value)
} else {
stateRef.value = payload
}
}
return [stateRef, setState]
}
import { CUSTOME_REQUESTER, RequesterFunc, RequestOptions } from 'vue-vulcan';
import { request } from '@tarojs/taro';
export function useCustomeRequest(): RequesterFunc {
return function (url: string, reqs: RequestOptions) {
console.log('my url', url)
return request({
...reqs,
url,
}) as Promise<any>
};
}
useCustomeRequest.token = CUSTOME_REQUESTER;
\ No newline at end of file
import { useAuthData } from "../../../any-hooks/auth/useAuthData";
import { HttpIntercept, HttpResponse, HTTP_INTERCEPT, RequestOptions, useInjector } from "vue-vulcan";
export function useHttpIntercept(): HttpIntercept {
const { authData } = useInjector(useAuthData);
const requestIntercept = (reqs: RequestOptions) => {
console.log(reqs)
const token = authData.value?.token || '';
reqs.data.token = token;
return new Promise( (resolve, reject) => {
if(token || reqs.path.includes('login')) {
resolve(reqs)
} else {
reject(`the http request ${reqs.path} need token`)
}
})
}
const responseIntercept = (res: HttpResponse) => {
return new Promise( resolve => {
resolve(res.data.data)
})
}
return {
requestIntercept,
responseIntercept
}
}
useHttpIntercept.token = HTTP_INTERCEPT;
\ No newline at end of file
import { HTTP_OPTIONS, RequestOptions } from "vue-vulcan";
export function useRequestOption(): RequestOptions {
return {
baseUrl: 'https://devserver.anyremote.cn/index.php/api/common',
method: 'POST'
}
}
useRequestOption.token = HTTP_OPTIONS;
\ No newline at end of file
import { HttpIntercept } from 'vue-vulcan';
import { useAuthStore } from '../store/auth/useAuthStore';
export function useAuthIntercept(): HttpIntercept {
const { token } = useAuthStore()
const requestIntercept = (req: RequestInit) => {
return new Promise((resolve, reject) => {
if(token) {
req.body.token = token;
}
return req
})
}
const responseIntercept = () => {}
return {
requestIntercept,
responseIntercept
}
}
\ No newline at end of file
import { closeSocket, connectSocket, onSocketClose, onSocketError, onSocketMessage, onSocketOpen, sendSocketMessage } from "@tarojs/taro";
import { CUSTOME_SOCKET } from "any-hooks/communication/token";
import { CustomeSocket } from "any-hooks/types/socket";
export function useCustomeSocket(): CustomeSocket {
return {
connect(url: string) {
connectSocket({url})
},
send(data: object) {
sendSocketMessage({
data: JSON.stringify(data)
})
},
addEventListener(type: keyof WebSocketEventMap, callback?: (event?: any) => void) {
switch(type) {
case 'message':
onSocketMessage( res => callback({data: JSON.parse(res.data)}) );
break;
case 'error':
onSocketError(callback);
break;
case 'open':
onSocketOpen(callback);
break;
case 'close':
onSocketClose(callback);
break;
}
},
close() {
closeSocket()
}
}
}
useCustomeSocket.token = CUSTOME_SOCKET;
\ No newline at end of file
import { useAuthData } from "any-hooks/auth/useAuthData";
import { SOCKET_SETTINGS } from "any-hooks/communication/token";
import { SocketSettings } from "any-hooks/types/socket";
import { useInjector } from "vue-vulcan";
export function useSocketSetting(): SocketSettings {
const { authData } = useInjector(useAuthData);
return {
heartData: {
msgMainFlag:"Heart",
fromID: authData.value.id,
fromName: authData.value.nickname,
toID: '0'
},
retryLimit: 10
}
}
useSocketSetting.token = SOCKET_SETTINGS;
\ No newline at end of file
import { getStorageSync, setStorageSync } from "@tarojs/taro";
import { CUSTOME_STORAGE } from "vue-vulcan";
export function useCustomeStorage() {
const setItem = (key: string, val: any) => {
setStorageSync(key, val)
}
const getItem = (key: string) => {
return getStorageSync(key)
}
return {
setItem,
getItem
}
}
useCustomeStorage.token = CUSTOME_STORAGE;
\ No newline at end of file
import { useState } from '../../../hooks/common/useState';
import { UserData } from 'src/types/auth';
import { onBeforeMount } from 'vue';
import Taro from '@tarojs/taro'
export function useAuthStore() {
const [token, setToken] = useState('');
const [user, setUser] = useState<UserData>(null);
onBeforeMount( () => {
const token = Taro.getStorageSync<string>('token');
const user = Taro.getStorageSync<UserData>('user');
setToken(token);
setUser(user);
})
const saveToken = (token: string) => {
setToken(token);
Taro.setStorageSync('token', token);
}
const saveUserInfo = (user: UserData) => {
setUser(user);
Taro.setStorageSync('user', user)
}
return {
token,
user,
saveToken,
saveUserInfo
}
}
// import { watch } from "vue";
// import { useInjector } from "../../../hooks/common/useInjector";
// import { useRequest } from "../../../hooks/common/useRequest";
// import { useState } from "../../../hooks/common/useState";
// import { useAuthStore } from "../auth/useAuthStore";
// import { useAgoraClient } from "./useAgoraClient";
// export function useVideoCallStore() {
// const [isMini, setMiniState] = useState(false);
// const [channelInfo, getChannelInfo, status] = useRequest<any>('/getAgoraToken', {}, {auto: false});
// const { user } = useInjector(useAuthStore);
// const agora = useAgoraClient();
// const joinChannel = (channel_id?: string) => {
// getChannelInfo({channel_id});
// watch( status, val => {
// if(val === 'success') {
// const token = channelInfo.value.agora_token;
// const cid = channelInfo.value.channel_id;
// agora.joinChannel(token, cid, user.value.id);
// }
// })
// }
// return {
// isMini,
// streams: agora.streamList,
// action: {
// setMiniState,
// joinChannel
// }
// }
// }
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
import { useState } from '../../../hooks/common/useState';
import { onMounted } from 'vue';
import * as AgoraMiniappSDK from '../../../libs/Agora_SDK_for_WeChat';
import { useInjector } from '../../common/useInjector';
import { useChannelInfo } from './useChannelInfo';
import { useAuthStore } from '../auth/useAuthStore';
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(useAuthStore, 'root');
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 { onSocketMessage, sendSocketMessage, showModal } from "@tarojs/taro";
import { Subject } from "rxjs";
import { useInjector } from "../../common/useInjector";
import { onMounted } from "vue";
import { useAuthStore } from "../auth/useAuthStore";
import { useChannelInfo } from "./useChannelInfo";
import { useState } from "../../common/useState";
import { UserData } from "../../../types/auth";
import { useSocket } from "./useSocket";
interface CallingMessage {
fromID: string;
fromName: string;
toID: string;
toName:string;
companyID: string;
channelID: string;
msgMainFlag:string;
msgSubFlag: string;
}
export function useCalling() {
const [targetUser, setTarget] = useState<UserData>(null)
const { user } = useInjector(useAuthStore, 'root');
const channel = useInjector(useChannelInfo, 'root');
const ws = useSocket()
// 初始化websocket
onMounted( () => {
ws.init(`wss://www.if-ar.com:3009?fromID=${user.value.id}&fromName=${user.value.nickname}&signID=SA&companyID=${user.value.company_id}`)
onSocketMessage( res => {
console.log(res)
const data = JSON.parse(res.data)
if(data.msgMainFlag === 'CallOffer') {
switch(data.msgSubFlag) {
case '':
showModal({
title: `收到来自${data.fromName}的呼叫,是否同意?`,
success: () => answer(data, 'Connect'),
fail: () => answer(data, 'Hangup')
})
}
}
})
})
// 发出呼叫
const send = () => {
channel.createChannel();
channel.onReady( () => {
const data = JSON.stringify({
fromID: user.value.id,
fromName: user.value.nickname,
toID: targetUser.value.id,
toNmame: targetUser.value.nickname,
channelID: channel.current.value.channel_id,
msgMainFlag: 'CallOffer',
msgSubFlag: 'Request'
})
sendSocketMessage({ data })
});
}
// 回应呼叫
const answer = (sendData: CallingMessage, action: 'Hangup' | 'Connect') => {
if(action === 'Connect') channel.joinCallingChannel(sendData.channelID);
const data = JSON.stringify({
fromID: user.value.id,
fromName: user.value.nickname,
toID: sendData.fromID,
toName: sendData.fromName,
channelID: sendData.channelID,
msgMainFlag: 'CallAnswer',
msgSubFlag: action
})
sendSocketMessage({ data })
}
// 挂断
const hangup = () => {
const data = JSON.stringify({
fromID: user.value.id,
fromName: user.value.nickname,
toID: targetUser.value.id,
toNmame: targetUser.value.nickname,
channelID: channel.current.value.channel_id,
msgMainFlag: 'CallOffer',
msgSubFlag: 'Hangup'
})
}
const callingState$ = new Subject()
return {
send,
targetUser,
setTarget,
callingState$
}
}
\ No newline at end of file
import { connectSocket, onSocketOpen, sendSocketMessage } from "@tarojs/taro"
import { timer } from "rxjs";
import { useInjector } from "../../common/useInjector";
import { useAuthStore } from "../auth/useAuthStore";
export function useSocket() {
const { user } = useInjector(useAuthStore);
const init = (url: string) => {
connectSocket({url});
startHeartConnect()
}
const startHeartConnect = () => {
const data = JSON.stringify({
msgMainFlag:"Heart",
fromID: user.value.id,
fromName: user.value.nickname,
toID: '0'
})
onSocketOpen( () => {
timer(0, 5000)
.subscribe( _ => {
sendSocketMessage({ data })
})
})
}
return {
init
}
}
\ No newline at end of file
import { onMounted } from "vue"; import { onMounted } from "vue";
import { useAuthStore } from "../store/auth/useAuthStore";
import { navigateTo } from '@tarojs/taro' import { navigateTo } from '@tarojs/taro'
import { useInjector } from "vue-vulcan";
import { useAuthData } from "../../../any-hooks/auth/useAuthData";
export function useAuthCheck() { export function useAuthCheck() {
const user = useAuthStore(); const {authData} = useInjector(useAuthData);
onMounted( () => { onMounted( () => {
console.log("check", user) if(!authData.value?.token) {
if(!user.token.value) {
navigateTo({url: '/pages/login/index'}) navigateTo({url: '/pages/login/index'})
} }
}) })
......
import { useRequest } from "../common/useRequest";
export function useContactList() {
const [ data ] = useRequest<any>('/getUserList');
return data
}
\ No newline at end of file
import Taro from '@tarojs/taro';
import { UserData } from "src/types/auth";
import { LoginData } from "src/types/user";
import { watch } from "vue";
import { useFormModel } from "../common/useFormModel";
import { useInjector } from "../common/useInjector";
import { useAuthStore } from "../store/auth/useAuthStore";
const loginTips = {
successTip: '登录成功',
loadingTip: '登录中...'
}
export function useLoginForm() {
const [forms, action, states, res ] = useFormModel<LoginData, UserData>('/loginIn', {}, loginTips);
const authStore = useInjector(useAuthStore, 'root');
watch(res, val => {
authStore.saveToken(val.token);
authStore.saveUserInfo(val);
})
watch(states.httpStatus, val => {
if(val === 'success') {
Taro.navigateTo({url: '/pages/meeting/index'})
}
})
return {
forms,
action
}
}
\ No newline at end of file
export default { export default {
navigationBarTitleText: '呼叫联系人' navigationBarTitleText: '呼叫联系人',
} }
<script setup> <script setup>
import { useInjector } from '../../hooks/common/useInjector'; import { useInjector } from 'vue-vulcan';
import { computed, getCurrentInstance, onMounted } from 'vue'; import { computed, getCurrentInstance, onMounted } from 'vue';
import { useCalling } from '../../hooks/store/video/useCalling'; import { useCallCenter } from 'any-hooks/communication/useCallCenter';
const calling = useInjector(useCalling); const { target } = useInjector(useCallCenter);
const targetUser = computed( () => calling.targetUser.value );
onMounted( () => {
calling.send();
})
</script> </script>
<template> <template>
<view class="page"> <view class="page">
<view class="call-box"> <view class="call-box">
<image class="avatar" :src="targetUser?.avatar"></image> <image class="avatar" :src="target?.avatar"></image>
<text class="tips">正在呼叫{{targetUser?.nickname}}...</text> <text class="tips">正在呼叫{{target?.nickname}}...</text>
</view> </view>
<image class="hangup" src="../../assets/hangup.png"></image> <image class="hangup" src="../../assets/hangup.png"></image>
</view> </view>
</template> </template>
<style lang="less"> <style lang="less">
...@@ -33,9 +28,11 @@ page{ ...@@ -33,9 +28,11 @@ page{
height:100%; height:100%;
.call-box{ .call-box{
position: absolute; position: absolute;
text-align: center;
top: 30%; top: 30%;
left:50%; left:50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width:100%;
.avatar{ .avatar{
width: 160px; width: 160px;
height: 160px; height: 160px;
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<view>{{item.nickname}}</view> <view>{{item.nickname}}</view>
<text>{{item.permission}}</text> <text>{{item.permission}}</text>
</view> </view>
<image @tap="callContacter(item)" class="call" src="../../assets/call.png" /> <image @tap="call(item)" class="call" src="../../assets/call.png" />
</view> </view>
</view> </view>
</view> </view>
...@@ -15,19 +15,18 @@ ...@@ -15,19 +15,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { navigateTo } from '@tarojs/taro'; import { navigateTo } from '@tarojs/taro';
import { UserData } from '../../types/auth'; import { useInjector } from 'vue-vulcan';
import { useInjector } from '../../hooks/common/useInjector'; import { useContacts } from '../../../any-hooks/contacts/useContacts';
import { useCalling } from '../../hooks/store/video/useCalling'; import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { useContactList } from '../../hooks/user/useContactsList'; import { UserData } from 'any-hooks/types/user';
import './index.less';
const contacts = useContactList(); const { callContact } = useInjector(useCallCenter);
const calling = useInjector(useCalling);
const callContacter = (data: UserData) => { const call= (data: UserData) => {
calling.setTarget(data); callContact(data);
navigateTo({url: '/pages/calling/index'}); navigateTo({url: '/pages/calling/index'});
} }
const { contacts } = useContacts()
</script> </script>
......
<script setup> <script setup>
import { useFormModel } from '../../hooks/common/useFormModel';
import { useAuthStore } from '../../hooks/store/auth/useAuthStore';
import { watch } from 'vue'; import { watch } from 'vue';
import { navigateTo, navigateBack } from '@tarojs/taro'; import { navigateTo, navigateBack } from '@tarojs/taro';
import { useLogin } from '../../../any-hooks/auth/useLogin';
const userInfo = useAuthStore() const { loginData, submitLogin } = useLogin();
const [formData, action, states, resData ] = useFormModel('/loginIn', {login_id: '', login_password: ''});
watch(states.httpStatus, status => { const onSubmit = () => {
if(status === 'success') { submitLogin().then( _ => navigateBack())
console.log('登陆成功') }
userInfo.saveToken(resData.value.token);
userInfo.saveUserInfo(resData.value);
navigateBack()
}
})
</script> </script>
<template> <template>
<view> <view>
<view class="login-box"> <view class="login-box">
<input class="login-item" placeholder="请输入用户名" v-model="formData.login_id"> <input class="login-item" placeholder="请输入用户名" v-model="loginData.login_id">
<input class="login-item" placeholder="请输入密码" v-model="formData.login_password"> <input class="login-item" placeholder="请输入密码" v-model="loginData.login_password">
<button @tap="action.submit()">登录</button> <button @tap="onSubmit()">登录</button>
</view> </view>
</view> </view>
</template> </template>
......
<script setup> <script setup>
import { onMounted, watch } from 'vue'; import { onMounted, watch } from 'vue';
import { useInjector } from '../../hooks/common/useInjector';
import { useVideoCallStore } from '../../hooks/store/video';
const videoMeeting = useInjector(useVideoCallStore, 'root')
onMounted( () => {
videoMeeting.action.joinChannel('3');
})
watch(videoMeeting.streams, console.log);
const onError = (e) => {console.log('err',e)}
const net = e => console.log(e)
</script> </script>
<template> <template>
<view class ="meeting-container"> <view class ="meeting-container">
<view <!-- <view
class="players" class="players"
v-for="(item, key) in videoMeeting.streams.value" v-for="(item, key) in videoMeeting.streams.value"
:key="key" :key="key"
...@@ -24,7 +14,7 @@ ...@@ -24,7 +14,7 @@
<live-player v-if="item.uid" :src="item.url" mode="RTC" style="width: 100%;height:200px;" /> <live-player v-if="item.uid" :src="item.url" mode="RTC" style="width: 100%;height:200px;" />
<live-pusher v-else :autopush="true" @error="onError" @statechange="net" :url="item.url" :local-mirror="'enable'" mode="RTC" style="width: 100%;height:100px;" /> <live-pusher v-else :autopush="true" @error="onError" @statechange="net" :url="item.url" :local-mirror="'enable'" mode="RTC" style="width: 100%;height:100px;" />
</view> </view> -->
</view> </view>
</template> </template>
\ No newline at end of file
export interface UserData { export interface UserData {
id: string; id?: string;
company_id: string; company_id?: string;
nickname: string; nickname?: string;
avatar: string; avatar?: string;
permission: string; permission?: string;
token: string; token?: string;
} }
\ No newline at end of file
...@@ -21,12 +21,12 @@ export interface VuetifyTable<T> { ...@@ -21,12 +21,12 @@ export interface VuetifyTable<T> {
export interface ImgFile { export interface ImgFile {
url: string; url: string;
id: string; id: string;
} }
export interface FunctionalStore<T extends object> { export interface FunctionalStore<T extends object> {
(...args: any[]): T; (...args: any[]): T;
token?: symbol; token?: symbol;
} }
\ No newline at end of file
...@@ -24,7 +24,6 @@ ...@@ -24,7 +24,6 @@
] ]
}, },
"exclude": [ "exclude": [
"node_modules",
"dist" "dist"
], ],
"compileOnSave": false "compileOnSave": false
......
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