Commit f9eec267 by wxl

fix calling bug

parent c33b7955
import { useNetSocketStore } from "any-hooks/communication/useNetSocketStore";
import { UserData } from "any-hooks/types/user";
import { reactive } from "vue";
import { useInjector, useRequest } from "vue-vulcan";
......@@ -12,6 +13,7 @@ export function useLogin() {
})
const [, request] = useRequest<UserData>('/loginIn', { auto: false });
const { setAuth } = useInjector(useAuthData);
const { sendMsg } = useInjector(useNetSocketStore);
const peer_id = new Date().getTime().toString().substr(7,).replace('0', '1');
console.log('init peer_id', peer_id)
......
......@@ -98,7 +98,7 @@ export function useAgoraClient() {
case 'remove':
console.log('remove', input)
const res = streamList.value.filter( item => {
!input.includes(item.uid)
return !input.includes(item.uid)
});
setStream(res);
break;
......@@ -129,4 +129,5 @@ export function useAgoraClient() {
unmuteLocalStream,
unmuteRemoteStream
}
}
\ No newline at end of file
}
import { computed, onMounted, reactive, ref } from 'vue';
import { useRequest } from 'vue-vulcan';
interface DataListOption {
params?: {
[prop: string]: string | number | undefined;
}
}
interface DataListFilters {
page_index: number;
[prop: string]: any;
}
interface PageModel {
page_index: number;
page_size?: number;
page_count?: number;
}
const defaultOptions = {
type: 'scroll',
params: {}
}
export function usePagingData<T = any>(url: string, options: DataListOption = defaultOptions) {
const loading = ref(true);
const error = ref(false);
const refresh = ref(false);
const listData = ref<T[]>([]);
const total = ref(1);
const filters = reactive({ }) as DataListFilters;
const page = reactive({ page_index: 1 }) as PageModel;
const finished = computed(() => total.value <= listData.value.length);
const empty = computed(() => listData.value.length === 0 && !loading.value && !refresh.value);
onMounted(() => {
requestData()
})
const [, doRequest ] = useRequest<any>(url, { auto: false })
const requestData = () => {
doRequest({...options.params, ...filters, ...page}).then(data => {
listData.value = listData.value.concat(data.list);
total.value = parseInt(data.total, 10);
refresh.value = false;
}).catch( _ => error.value = true )
}
const setFilter = (key: string, value: any) => {
listData.value = [];
filters[key] = value;
page.page_index = 1;
requestData();
}
const setFresh = () => {
page.page_index = 1;
requestData();
}
const loadNextPage = () => {
page.page_index ++;
requestData();
}
return {
loading,
refresh,
finished,
empty,
page,
listData,
total,
loadNextPage,
setFilter,
setFresh
}
}
......@@ -64,6 +64,7 @@ export function useSocket<S = any>() {
close: () => {
setStatus('closed');
ws.close();
}
},
onOpen: (cb: any) => ws.addEventListener('open', cb)
}
}
......@@ -6,10 +6,11 @@ import { useInjector, useRequest, useState } from "vue-vulcan";
import { useChannelStore } from "./useChannelStore";
import { useNetSocketStore } from "./useNetSocketStore";
type CallingState = 'net_error' | 'calling' | 'call_successed' | 'callin' | 'call_accepted' | 'connecting' | 'free';
type CallingState = 'net_error' | 'callout' | 'calling' | 'call_successed' | 'callin' | 'call_accepted' | 'connecting' | 'idle';
interface Caller extends UserData {
action: AnyRemoteSubFlag | 'none';
state?: CallingState;
members?: string[];
channel?: string;
}
......@@ -19,44 +20,44 @@ export function useCallCenter() {
const { sendMsg, currentMsg } = useInjector(useNetSocketStore);
const { authData } = useInjector(useAuthData);
const [, changeCallingState] = useRequest('/updateUsersCallState', {auto: false});
const [, changeCallingState] = useRequest('/updateUsersCallState', {auto: false});
let timer;
let timer: NodeJS.Timeout;
/** 主动呼叫功能 */
const [target, setTarget] = useState<Caller>(null);
const [myCallState, setCallState] = useState<CallingState>('free');
const {currentChannel, channelMembers, isEmpty, createChannel, getTokenByChannel, updateMembers, clearChannel} = useInjector(useChannelStore);
const callContact = (user: UserData) => {
setTarget({...user, action: 'none'}); //保存呼叫目标的信息
const [myCallState, setCallState] = useState<CallingState>('idle');
const {currentChannel, isInChannel, setInChannel, createChannel, getTokenByChannel, updateMembers, clearChannel} = useInjector(useChannelStore);
const callContact = (user: UserData) => {
setTarget({...user, action: 'none', state: 'callin'}); //保存呼叫目标的信息
createChannel().then( data => {
setCallState('calling'); //主动呼叫别人时将自己的呼叫状态更改为‘calling
setCallState('callout'); //主动呼叫别人时将自己的呼叫状态更改为‘callout
sendMsg({
toID: user.id,
toName: user.nickname,
channelID: data.channel_id,
msgMainFlag: 'CallOffer',
msgSubFlag: 'Request',
msgData: {
members: channelMembers.value
}
msgSubFlag: 'Request'
})
})
//18S后若本地用户仍处于主动呼叫状态,则自动挂断
timer = setTimeout(() => {
if(myCallState.value === 'calling') hangup();
if(myCallState.value === 'callout') hangup();
}, 18000)
}
/** 回应呼叫功能 */
const answerCaller = (subFlag: AnyRemoteSubFlag, user?: UserData) => {
if(subFlag === 'Connect') {
getTokenByChannel(caller.value.channel).then( _ => setCallState('call_accepted'));
updateMembers(caller.value.members, 'join');
getTokenByChannel(caller.value.channel).then( _ => {
caller.value.state = 'calling';
setCallState('calling');
});
updateMembers();
};
const target = user || caller.value;
sendMsg({
const target = user || caller.value;
sendMsg({
toID: target.id,
toName: target.nickname,
msgMainFlag: 'CallAnswer',
......@@ -73,30 +74,29 @@ export function useCallCenter() {
switch(msg.msgSubFlag) {
case 'Request':
// 本地呼叫状态为free时,变更为callin;不为free,则发送繁忙的回应。
console.log(`${currentMsg.value.fromName}呼叫,我目前的状态${myCallState.value}`)
if(myCallState.value === 'free') {
setCallState('callin');
if(myCallState.value === 'idle') {
setCaller({
id: msg.fromID,
nickname: msg.fromName,
action: msg.msgSubFlag,
channel: msg.channelID,
members: msg.msgData?.members || []
state: 'callout',
channel: msg.channelID
})
setCallState('callin');
} else{
answerCaller('Busying', {id: msg.fromID, nickname: msg.fromName});
}
//15S秒后若本地用户仍然处于被呼叫状态,则自动发出繁忙恢复,且将状态恢复为free
//15S秒后若本地用户仍然处于被呼叫状态,则自动发出繁忙恢复,且将状态恢复为idle
timer = setTimeout( () => {
if(myCallState.value === 'callin'){
answerCaller('Busying', {id: currentMsg.value.fromID, nickname: currentMsg.value.fromName});
setCallState('free');
answerCaller('Busying', {id: msg.fromID, nickname: msg.fromName});
setCallState('idle');
}
}, 15000)
break;
case 'Hangup':
//对方中断呼叫,则将呼叫状态改为free
setCallState('free');
//对方中断呼叫,则将呼叫状态改为idle
setCallState('idle');
setCaller({
id: msg.fromID,
nickname: msg.fromName,
......@@ -107,18 +107,22 @@ export function useCallCenter() {
break;
}
})
// 监听呼叫目标的回应
watch(currentMsg, msg => {
if(msg.msgMainFlag !== 'CallAnswer') return;
if(msg.msgMainFlag !== 'CallAnswer') return;
setTarget({...target.value, action: msg.msgSubFlag});
switch(msg.msgSubFlag) {
case 'Connect':
setCallState('call_successed');
updateMembers([msg.fromID], 'join');
setCallState('calling');
setInChannel(true);
target.value.state = 'calling';
updateMembers();
break;
case 'Busying':
case 'Hangup':
setCallState(isEmpty.value ? 'free' : 'connecting');
target.value.state = 'idle';
setCallState(!isInChannel.value ? 'idle' : 'calling');
break;
}
})
......@@ -131,24 +135,24 @@ export function useCallCenter() {
flag = 'ChannelChat';
toID = '-2'; // '-2'表示向频道发送消息
break;
case 'calling':
case 'callout':
flag = 'CallOffer';
// flag = 'CallAnswer'
toID = target.value?.id;
break;
case 'callin':
flag = 'CallAnswer';
toID = caller.value?.id;
break;
}
}
sendMsg({
channelID: currentChannel.value?.channel_id,
msgMainFlag: flag,
msgSubFlag: 'Hangup',
toID
})
isEmpty.value && setCallState('free');
type === 'leave' && setCallState('free');
setCallState('idle');
// type === 'leave' && setCallState('idle');
setTarget(null);
setCaller(null);
clearTimeout(timer);
......@@ -156,19 +160,13 @@ export function useCallCenter() {
/** 监听呼叫状态,当状态变回free时,重置channel的信息,同时向服务器广播 */
watch(myCallState, state => {
console.log('state', state)
if(state === 'free') {
if(state === 'idle') {
clearChannel();
sendMsg({
msgMainFlag: 'NotifyUpdateUserList',
msgSubFlag: "UpdateUserList",
toID: '0'
})
}
changeCallingState({channel_id: currentChannel.value?.channel_id, userIDs: JSON.stringify([
{userID: authData.value.id, callState: state==='free' ? 'idle' : state==='calling' ? 'callout' : 'callin'},
{userID: target.value?.id || caller.value?.id, callState: state==='free' ? 'idle' : 'callin'}
])});
const caller_state = target.value?.state || caller.value?.state;
const userStates = [{userID: authData.value.id, callState: state }];
if(caller_state) userStates.push({userID: target.value?.id || caller.value?.id, callState: caller_state});
changeCallingState({channel_id: currentChannel.value?.channel_id, userIDs: JSON.stringify(userStates)});
})
return {
......
import { useAuthData } from "any-hooks/auth/useAuthData";
import { computed, watch } from "vue";
import { useInjector, useRequest, useState } from "vue-vulcan";
import { useNetSocketStore } from "./useNetSocketStore";
......@@ -11,44 +10,39 @@ interface ChannelData {
/** 频道中心功能,可以创建频道,并保存当前频道的信息,供呼叫中心、会议中心使用(目前同一时间仅允许存在一个频道) */
export function useChannelStore() {
const [currentChannel, setChannel] = useState<ChannelData>(null);
const [isInChannel, setInChannel] = useState(false);
const [channelMembers, setMembers] = useState<string[]>([]);
const [channelInfo, request] = useRequest<ChannelData>('/getAgoraToken', {auto: false});
const { authData } = useInjector(useAuthData);
const { currentMsg } = useInjector(useNetSocketStore);
const isEmpty = computed( () => channelMembers.value.length < 2); //频道剩余不到1人时,表示为空
const { currentMsg } = useInjector(useNetSocketStore);
/* 创建新的频道(如果已存在一个频道则不调用创建接口,直接返回一个promise) */
const createChannel = () => {
updateMembers([authData.value.id], 'join');
console.log('创建频道前判断是否已存在频道', currentChannel.value);
const createChannel = () => {
if(!currentChannel.value) return request();
updateMembers();
return new Promise<ChannelData>(resolve => resolve(channelInfo.value));
}
const getTokenByChannel = (cid: string) => {
console.log('根据对方呼叫人的频道获取token');
updateMembers([authData.value.id], 'join');
return request({channel_id: cid});
const getTokenByChannel = (cid: string) => {
return request({channel_id: cid}).then(_ => updateMembers());
}
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 [members, requestMembers] = useRequest<any[]>('/getChannelUser', {auto: false})
const isEmpty = computed( () => members.value && members.value.length < 2); //频道剩余不到1人时,表示为空
const updateMembers = () => {
if(!currentChannel.value?.channel_id) return new Promise(resolve => resolve(null));
return requestMembers({channel_id: currentChannel.value.channel_id})
}
const clearChannel = () => {
setChannel(null);
setInChannel(false);
setMembers([]);
}
watch(channelInfo, val => {
setChannel(val);
updateMembers([authData.value.id], 'join');
updateMembers();
})
//监听频道下的成员实时信息
......@@ -56,7 +50,7 @@ export function useChannelStore() {
if(msg.msgMainFlag !== 'ChannelChat') return;
switch(msg.msgSubFlag) {
case 'Hangup':
updateMembers([msg.fromID], 'leave');
updateMembers();
break;
}
})
......@@ -66,9 +60,11 @@ export function useChannelStore() {
currentChannel,
channelMembers,
isEmpty,
isInChannel,
getTokenByChannel,
createChannel,
updateMembers,
setInChannel,
updateMembers,
clearChannel
}
}
\ No newline at end of file
......@@ -50,12 +50,12 @@ export function useMeetingCenter() {
/** 监听视频流列表,长度为1时,自动退出频道 (目前直接用agora stream替代)*/
watch(streamList, (current, last) => {
if(myCallState.value === 'free') return;
if(myCallState.value === 'idle') return;
if(current.length > last.length) {
return
};
if(current.length < 2) {
setTimeout(leave, 1000)
if(current.length === 0) {
setTimeout(leave, 2000)
}
})
return {
......
......@@ -20,8 +20,8 @@ export function useNetSocketStore() {
const { authData } = useInjector(useAuthData);
const [ netState, setNetState ] = useState<NetStates>(0);
const { baseUrl } = useInjector<SocketSettings>(SOCKET_SETTINGS, 'optional');
const { connect, reconnect, send, close, currentMsg, startHeartConnect, status } = useSocket<AnyRemoteSocketMessage>();
const [deviceCode, setDevice] = useState(null, {key: 'device', storage: 'custome'})
const { connect, reconnect, send, close, onOpen, currentMsg, startHeartConnect, status } = useSocket<AnyRemoteSocketMessage>();
const [deviceCode, setDevice] = useState(null, {key: 'device', storage: 'custome'})
onMounted(() => {
if(!deviceCode.value) {
......@@ -39,6 +39,20 @@ export function useNetSocketStore() {
toID: '0',
msgMainFlag: 'Heart'
})
onOpen(() => {
console.log('ws初始化peerid', authData.value.peer_id)
sendMsg({
msgMainFlag: 'SetAtts',
msgSubFlag: "UpdateUserList",
toID: '0',
msgData: {
peerID: authData.value.peer_id,
avatar: authData.value.avatar,
permission: authData.value.permission
}
})
})
}
const sendMsg = (data: AnyRemoteSocketMessage) => {
......@@ -46,7 +60,13 @@ export function useNetSocketStore() {
...data,
fromID: authData.value.id,
fromName: authData.value.nickname,
avatar: authData.value.avatar
avatar: authData.value.avatar,
permission: authData.value.permission,
msgData: {
peerID: authData.value.peer_id,
avatar: authData.value.avatar,
permission: authData.value.permission
}
})
}
......
......@@ -45,8 +45,9 @@ export function useContacts() {
return frees
})
const getContactById = (pid: string, type?: 'uid'|'pid') => {
const freeList = computed(() => userList.value.filter( item => item.is_calling === '0' && item.is_signin === '1'))
const getContactById = (pid: string, type?: 'uid'|'pid') => {
const index = userList.value.findIndex( user => user[type==='uid' ? 'id' : 'peer_id'] === pid );
console.log(`根据${type === 'uid' ?'id': 'peer_id'}${pid}查找`, index)
return userList.value[index];
......@@ -81,6 +82,7 @@ export function useContacts() {
keyword,
groups,
filterGroups,
freeList,
requestGroups,
setKeyWord,
getUserList,
......
......@@ -23,6 +23,7 @@ export interface AnyRemoteSocketMessage {
fromID?: string;
fromName?: string;
avatar?: string;
permission?: string;
msgMainFlag: AnyRemoteMainFlag;
msgSubFlag?: AnyRemoteSubFlag;
msgData?: any;
......
interface TaskData {
id: string;
task_name?: string;
author_nickname?: string;
create_time?: string;
task_desc?: string;
start_time?: string;
end_time?: string;
type_name?: string;
step_list?: Array<FeebackData>;
attr_list?: any[];
receiver_users?: Array<{id: string; nickname: string}>
}
interface FeebackData {
author_avatar?: string;
author_nickname?: string;
company_id?: string;
create_time?: string;
create_uid?: string;
feed_back?: string;
file_list: any[];
finish_time?: string;
id?: string;
img_list: any[];
liable_uid?: string;
parent_step_id?: string;
receiver_count?: string;
receiver_uid?: string;
repair_id?: string;
reply_uid?: string;
service_star?: string;
task_id?: string;
video_list: any[];
}
\ No newline at end of file
......@@ -3,11 +3,13 @@ export default {
'pages/index/index',
'pages/login/index',
'pages/meeting/index',
'pages/task/index',
'pages/mine/index',
'pages/calling/index',
'pages/contact-detail/index',
'pages/group-contacts/index',
'pages/help/index'
'pages/help/index',
'pages/task-detail/index'
],
window: {
backgroundTextStyle: 'light',
......@@ -22,8 +24,14 @@ export default {
{
"pagePath": "pages/index/index",
"text": "联系人",
"iconPath": "assets/Contact@3x.png",
"selectedIconPath": "assets/Contact-Check@3x.png"
"iconPath": "assets/Contact.png",
"selectedIconPath": "assets/Contact-Check.png"
},
{
"pagePath": "pages/task/index",
"text": "任务",
"iconPath": "assets/Task@3x.png",
"selectedIconPath": "assets/Task-Check@3x.png"
},
{
"pagePath": "pages/mine/index",
......
src/assets/Contact - Check.png

506 Bytes | W: | H:

src/assets/Contact - Check.png

780 Bytes | W: | H:

src/assets/Contact - Check.png
src/assets/Contact - Check.png
src/assets/Contact - Check.png
src/assets/Contact - Check.png
  • 2-up
  • Swipe
  • Onion skin
src/assets/Contact.png

523 Bytes | W: | H:

src/assets/Contact.png

982 Bytes | W: | H:

src/assets/Contact.png
src/assets/Contact.png
src/assets/Contact.png
src/assets/Contact.png
  • 2-up
  • Swipe
  • Onion skin
src/assets/Personal - Check.png

497 Bytes | W: | H:

src/assets/Personal - Check.png

812 Bytes | W: | H:

src/assets/Personal - Check.png
src/assets/Personal - Check.png
src/assets/Personal - Check.png
src/assets/Personal - Check.png
  • 2-up
  • Swipe
  • Onion skin
src/assets/Personal.png

512 Bytes | W: | H:

src/assets/Personal.png

963 Bytes | W: | H:

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

1.05 KB | W: | H:

src/assets/call-no.png

1.39 KB | W: | H:

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

454 Bytes | W: | H:

src/assets/call-small.png

393 Bytes | W: | H:

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

1.2 KB | W: | H:

src/assets/call.png

1.51 KB | W: | H:

src/assets/call.png
src/assets/call.png
src/assets/call.png
src/assets/call.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -16,8 +16,8 @@
}
const onTapCall = (data: UserData) => {
if(myCallState.value !== 'free') {
showToast({title: '您当前已在通话中,无法发起呼叫'});
if(myCallState.value !== 'idle') {
showToast({title: '您当前已在通话中,无法发起呼叫', icon: 'none'});
} else {
checkCamearSetting().then( r => {
r ? callContact(data) : showToast({title: '您未授权摄像头权限,请在设置中打开摄像头授权', icon: 'none'});
......
<template>
<view class="text-field detail-item" v-if="controlName==='field'">
<view class="key">{{data.field_name}}</view>
<view class="value">{{field_text || data.field_value}}</view>
</view>
<view class="photo">
<view class="p-tt key">{{data.field_name}}</view>
<photo-wall :urls="photos" v-if="controlName==='photo-wall'"></photo-wall>
</view>
</template>
<script lang="ts" setup>
import { defineProps } from "@vue/runtime-core";
import { computed } from "vue";
import { useState } from "vue-vulcan";
import PhotoWall from 'src/components/photo-wall.vue';
interface Attr {
field_name: string;
field_value: any;
control_type: string;
field_options?: Array<any>;
}
const {data} = defineProps<{data: Attr}>();
const [controlName] = useState('field');
const [field_text, setFieldText] = useState('');
const photos = computed(() => data.field_value?.map( (item: any) => item.url));
switch (data.control_type) {
// default:
case '1':
case '6':
controlName.value = 'field';
break;
case '4':
setFieldText(data.field_value.join(','))
break;
case '2':
case '3':
controlName.value = 'field';
data.field_options?.forEach( (item: any) => {
if(item.id === data.field_value) { setFieldText(item.name) }
})
break;
case '9':
controlName.value = 'photo-wall';
break;
}
</script>
\ No newline at end of file
......@@ -6,12 +6,16 @@
const { myCallState } = useInjector(useCallCenter);
const gotoMeetingHome = () => {
navigateTo({url: '/pages/meeting/index'})
if(myCallState.value === 'calling') {
navigateTo({url: '/pages/meeting/index'})
} else {
navigateTo({url: '/pages/calling/index'})
}
}
</script>
<template>
<view class="meeting-bar" v-if="myCallState!=='free'">
<view class="meeting-bar" v-if="myCallState!=='idle'">
<image @tap="gotoMeetingHome()" class="bar-icon" src="../assets/during-call3x.png" mode="widthFix"></image>
</view>
</template>
......
<template>
<view class="image-list">
<image
class="p-image-item"
:src="item"
:key="key"
v-for="(item, key) in urls"
mode="widthFix"
@tap="preview(item)"
>
</image>
</view>
</template>
<script lang="ts" setup>
import { previewImage } from "@tarojs/taro";
import { defineProps } from "vue";
const { urls } = defineProps<{urls: string[]}>();
const preview = (url: string) => {
previewImage({
urls,
current: url
})
}
</script>
<style lang="less">
.p-image-item{
width: 120px;
height: 120px;
margin: 10px;
border-radius: 10px;
background: #999;
}
</style>
\ No newline at end of file
......@@ -3,11 +3,11 @@
import { useContacts } from 'any-hooks/contacts/useContacts';
import { AgoraStreamItem } from 'any-hooks/types/agora';
import { UserData } from 'src/types/auth';
import { defineProps, onMounted } from 'vue';
import { defineProps, onMounted, watch } from 'vue';
import { useInjector, useState } from 'vue-vulcan';
const { stream } = defineProps<{stream: AgoraStreamItem}>();
const { getContactById } = useInjector(useContacts);
const { getContactById, contacts } = useInjector(useContacts);
const [ contact, setContact ] = useState<UserData>(null);
const { mode } = useInjector(useMeetingCenter)
......@@ -15,6 +15,11 @@
const currentContact = getContactById(stream.uid+'');
setContact(currentContact);
})
watch(contacts, _ => {
const currentContact = getContactById(stream.uid+'');
setContact(currentContact);
})
</script>
<template>
......
......@@ -23,6 +23,7 @@
:url="url"
mode="RTC"
aspect="3:4"
waiting-image="https://webdemo.agora.io/away.png"
v-show="mode==='video'"
v-on:error="onError"
......@@ -47,6 +48,7 @@
left:0;
width: 100%;
height: 100%;
overflow: hidden;
.avatar{
position: absolute;
width: 50%;
......
......@@ -20,18 +20,21 @@ export function useCallerListener() {
switch(current.action) {
case 'Request':
break;
case 'Hangup':
showToast({title: '对方取消了呼叫', icon: 'none'});
case 'Hangup':
console.log('对方挂断')
showToast({title: '对方已取消呼叫', icon: 'none'});
break;
}
})
watch(target, val => {
console.log('呼叫目标的action', val)
switch(val?.action) {
case 'Busying':
showToast({title: '对方繁忙,暂时无法接受呼叫', icon: 'none'});
break;
case 'Hangup':
console.log('对方挂断')
showToast({title: '对方拒绝了您的呼叫', icon: 'none'});
break;
}
......@@ -40,8 +43,8 @@ export function useCallerListener() {
// 根据用户的呼叫状态变化,执行相应的页面跳转逻辑
watch(myCallState, (state, prev) => {
switch(state) {
case 'calling':
if(prev !== 'free') {
case 'callout':
if(prev !== 'idle') {
showToast({title:'已发出呼叫,等待对方回应...', icon:'none'});
}else {
navigateTo({url: '/pages/calling/index'});
......@@ -50,11 +53,9 @@ export function useCallerListener() {
case 'callin':
checkCamearSetting().then(r => {
r ? navigateTo({url: '/pages/calling/index'}) : answerCaller('NoDevice')
})
break;
case 'call_accepted':
case 'call_successed':
})
break;
case 'calling':
redirectTo({url: '/pages/meeting/index'});
break;
}
......
......@@ -2,7 +2,7 @@
import { useInjector } from 'vue-vulcan';
import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { navigateBack } from '@tarojs/taro';
import { onBeforeUnmount, watch } from '@vue/runtime-core';
import { watch } from '@vue/runtime-core';
const { target, caller, myCallState, answerCaller, hangup } = useInjector(useCallCenter);
const onTapHangup = () => {
......@@ -11,14 +11,10 @@
}
watch(myCallState, state => {
if(state === 'free') {
setTimeout(navigateBack, 1000)
if(state === 'idle') {
setTimeout(navigateBack, 1000);
};
})
onBeforeUnmount(() => {
console.log('leave')
})
</script>
......@@ -29,12 +25,12 @@
<text class="tips" >{{caller?.nickname}}向您发起呼叫</text>
</view>
<view class="call-box target" v-if="myCallState==='calling'">
<view class="call-box target" v-if="myCallState==='callout'">
<image class="avatar" :src="target?.avatar"></image>
<text class="tips">正在呼叫{{target?.nickname}}...</text>
</view>
<view class="operators" v-if="myCallState!=='free'">
<view class="operators" v-if="myCallState!=='idle'">
<image v-if="myCallState==='callin'" @tap="answerCaller('Connect')" class="op-icon" src="../../assets/answer3x.png"></image>
<image @tap="onTapHangup()" class="op-icon" src="../../assets/hangup.png"></image>
</view>
......
......@@ -6,25 +6,29 @@
import { useAppInitInfo } from 'src/hooks/common/useAppInitInfo';
import { UserData } from 'any-hooks/types/user';
import { reactive } from '@vue/reactivity';
import { useDebounce } from 'any-hooks/common/useDebounce';
const { callContact, myCallState } = useInjector(useCallCenter);
const { contacts, roleContacts } = useInjector(useContacts);
const { checkCamearSetting } = useInjector(useAppInitInfo);
const gotoDetail = (id: string) => {
navigateTo({url: '/pages/contact-detail/index?id='+ id})
}
const onTapCall = (data: UserData) => {
if(myCallState.value !== 'free') {
showToast({title: '您当前已在通话中,无法发起呼叫'})
} else {
console.log('检查摄像头权限')
checkCamearSetting().then( r => {
r ? callContact(data) : showToast({title: '您未授权摄像头权限,请在设置中打开摄像头授权', icon: 'none'});
})
}
}
const onTapCall = useDebounce(
(data: UserData) => {
if(myCallState.value !== 'idle') {
showToast({title: '您当前已在通话中,无法发起呼叫', icon: 'none'})
} else {
console.log('检查摄像头权限')
checkCamearSetting().then( r => {
r ? callContact(data) : showToast({title: '您未授权摄像头权限,请在设置中打开摄像头授权', icon: 'none'});
})
}
},
500
)
// 联系人折叠隐藏功能
const hideConfig = reactive({});
......
......@@ -91,6 +91,7 @@
border-top-left-radius: 25px;
min-height: 600px;
overflow: hidden;
background: #fff;
}
.contact-list{
.contact-item{
......
......@@ -12,7 +12,7 @@
<template>
<view class="work-group-list white-box">
<view class="group-item pd-1" v-for="(item, key) in filterGroups" :key="key" @tap="gotoGroupContacts(item.id)">
<image class="group-icon" src="../../assets/group3x.png" mode="widthFix"/>
<image class="group-icon" src="../../assets/group3x.png" mode="widthFix" />
<text class="group-name">{{item.title}}</text>
</view>
<data-empty v-if="!filterGroups?.length"></data-empty>
......@@ -34,6 +34,6 @@
.group-name{
font-weight: bold;
}
}
}
}
</style>
\ No newline at end of file
......@@ -47,8 +47,9 @@
<template>
<view class="page white-box login-wrapper">
<view class="titles">
<view class="header-title">欢迎使用anyremote</view>
<text class="sub-title">请输入您的账号密码</text>
<!-- <view class="header-title">欢迎使用anyremote</view>
<text class="sub-title">请输入您的账号密码</text> -->
<image src="../../assets/logo2@3x.png" mode="widthFix"></image>
</view>
<view class="login-box">
<input class="login-item" placeholder="请输入您的账号" v-model="loginData.login_id">
......@@ -64,6 +65,10 @@
}
.titles{
margin: 60px 0;
text-align: center;
image{
width:55%;
}
.header-title{
font-size: 46px;
font-weight: bold;
......@@ -78,18 +83,26 @@
padding-top: 20px;
.login-item{
display: block;
margin: 30px 0;
margin: 40px 30px;
padding: 15px;
border: 1px solid #efefef;
border: none;
border-bottom: 1px solid #efefef;
font-size: small;
}
button.login-btn{
display: block;
padding: 8px 0;
margin-top: 80px;
margin: 0 25px;
border: none;
padding: 0px 0;
margin-top: 60px;
font-size: 32px;
color: #999;
background: #4880FF;
color: #fff;
// box-shadow: 0 0 5px #4880FF;
border:none;
&.allow{
background: #2b91e2;
background: #4880FF;
color: #fff;
}
}
......
......@@ -10,20 +10,21 @@
import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { UserData } from 'any-hooks/types/user';
import { useChannelStore } from 'any-hooks/communication/useChannelStore';
import { onMounted } from 'vue';
const { target } = useInjector(useCallCenter);
const { currentChannel } = useInjector(useChannelStore);
const { streamList, localPushurl, voiceMute, mode, switchLocalMicState, switchVideoOrAudio, leave } = useInjector(useMeetingCenter);
const { topDistance, rect } = useAppInitInfo();
const { rect } = useAppInitInfo();
// onMounted(() => {
// join();
// })
onMounted(() => {
// join();
})
/* 监听流的数量,自动退出会议 */
watch(streamList, (current, last) => {
if(current.length > last.length) return;
if(current.length < 2) {
if(current.length < 1) {
showToast({title: '当前频道已无其他联系人,即将自动退出...', icon: 'none'});
setTimeout(() => navigateBack({delta: 2}), 1000);
}
......@@ -81,16 +82,16 @@
<image class="navi-back" src="../../assets/arrow_left.png" mode="widthFix" :style="{top: rect.top+'px'}" @tap="navigateBack()"></image>
<invite v-if="inviting" @choose="onInvite($event)" @cancel="setInviting(false)"></invite>
<view class ="meeting-container page col-page" @tap="setInviting(false)">
<view class="c-info" style="color:#fff">
<!-- <view class="c-info" style="color:#fff">
{{currentChannel?.channel_id}}
</view>
</view> -->
<view class="videos">
<stream-player
class="video-item"
v-for="(item, key) in streamList"
:key="key"
:stream="item"
:class="{active: activeVideo===key, hide: activeVideo!==-2, mini: streamList.length > 5, big: streamList.length < 3}"
:class="{active: activeVideo===key, hide: activeVideo!==-2, mini: streamList.length > 5, big: streamList.length < 2}"
@tap="toggleActive(key)"
>
</stream-player>
......@@ -98,12 +99,15 @@
class="video-item"
v-if="localPushurl"
:url="localPushurl"
:class="{active: activeVideo===-1, hide: activeVideo!==-2, mini: streamList.length > 5, big: streamList.length < 3}"
:class="{active: activeVideo===-1, hide: activeVideo!==-2, mini: streamList.length > 5, big: streamList.length < 2}"
@tap="toggleActive(-1)"
>
</stream-pusher>
<view class="waiting video-item" v-if="target?.action === 'none'">
<view
class="waiting video-item"
v-if="target?.action === 'none'"
>
<text>等待中...</text>
</view>
......@@ -169,18 +173,18 @@
color: #fff;
&.big{
width: 100%;
height: 400px;
height: 500px;
}
&.mini{
width: 33.3%;
height: 200px;
height: 300px;
}
&.waiting{
line-height: 300px;
}
&.active{
width: 100%;
height: 600px;
height: 800px;
display: block !important;
}
&.hide{
......
<script lang="ts" setup>
import { useAuthData } from "any-hooks/auth/useAuthData";
import { useContacts } from "any-hooks/contacts/useContacts";
import { UserData } from "any-hooks/types/user";
import { useAppInitInfo } from "src/hooks/common/useAppInitInfo";
......@@ -7,8 +8,10 @@
const { topDistance } = useAppInitInfo();
const {authData} = useInjector(useAuthData)
/* 获取在线且空闲的联系人 */
const { freeContacts, roleContacts } = useInjector(useContacts);
const { freeContacts, freeList, } = useInjector(useContacts);
const emit = defineEmit();
const onChoose = (user: UserData) => {
......@@ -30,17 +33,19 @@
<view class="contact-role-group" v-for="(group, key, index) in freeContacts" :key="index">
<view class="role-name pd-2" :class="{hide: hideConfig[index]}" @tap="toggleHideByIndex(index)">{{key}}</view>
<view class="white-box" :style="{display: hideConfig[index] ? 'none' : 'block'}">
<view class="contact-item" v-for="(item, i) in group" :key="i" >
<image class="avatar" :src="item.avatar" />
<view class="info">
<view>{{item.nickname}}</view>
<text style="color:#999">{{item.group_name}}</text>
</view>
<button class="call invite-btn" @tap.stop="onChoose(item)">邀请</button>
<view v-for="(item, i) in group" :key="i" >
<view class="contact-item" v-if="authData.id !== item.id">
<image class="avatar" :src="item.avatar" />
<view class="info">
<view>{{item.nickname}}</view>
<text style="color:#999">{{item.group_name}}</text>
</view>
<button class="call invite-btn" @tap.stop="onChoose(item)">邀请</button>
</view>
</view>
</view>
</view>
<data-empty v-if="!roleContacts?.length"></data-empty>
<data-empty v-if="!freeList?.length"></data-empty>
</scroll-view>
</template>
......@@ -102,7 +107,7 @@
display: inline-block;
background: #2b91e2;
color: #fff;
padding: 2px 15px;
padding: 0px 15px;
}
}
}
......
export default {
// navigationStyle: 'custom',
navigationBarTitleText: '任务详情'
}
<template>
<scroll-view :scrollY="true" class="task-detail" v-if="detail">
<view class="detail-title">基础信息</view>
<view class="basic-info info-block">
<view class="name detail-item">
<text class="key">任务名称</text>
<text class="value">{{detail.task_name}}</text>
</view>
<view class="name detail-item">
<text class="key">任务类型</text>
<text class="value">{{detail.type_name}}</text>
</view>
<view class="name detail-item">
<text class="key">发布人</text>
<text class="value">{{detail.author_nickname}}</text>
</view>
<view class="name detail-item">
<text class="key">参与人</text>
<text class="value">{{detail.receiver_users.map(item => item.nickname).join(',')}}</text>
</view>
<view class="name detail-item">
<text class="key">开始时间</text>
<text class="value">{{detail.start_time}}</text>
</view>
<view class="name detail-item">
<text class="key">结束时间</text>
<text class="value">{{detail.end_time}}</text>
</view>
</view>
<view class="detail-title">任务描述</view>
<view class="desc-info info-block">
{{detail.task_desc || '暂无'}}
</view>
<view class="detail-title">其他信息</view>
<view class="extend-info info-block" >
<dynamic-view v-for="(item, key) in detail?.attr_list" :key="key" :data="item"></dynamic-view>
</view>
<view class="detail-title">
反馈记录
</view>
<view class="feedback-info info-block">
<data-empty v-if="!detail.step_list?.length"></data-empty>
<view class="feedback-item" v-for="(item, key) in detail.step_list" :key="key">
<view class="user-info">
<image class="avatar" :src="item.author_avatar" mode="widthFix"></image>
<view class="name">{{item.author_nickname}}</view>
<text class="time">{{item.create_time}}</text>
</view>
<view class="content">
<text class="text">{{item.feed_back}}</text>
</view>
<photo-wall v-if="item.img_list" :urls="item?.img_list.map(i => i.url)"></photo-wall>
</view>
</view>
</scroll-view>
</template>
<script lang="ts" setup>
import { getCurrentInstance } from "@tarojs/taro";
import { onMounted } from "@vue/runtime-core";
import { useRequest } from "vue-vulcan";
import PhotoWall from 'src/components/photo-wall.vue';
import DynamicView from 'src/components/dynamic-view.vue';
const [detail, request] = useRequest<TaskData>('/getTaskDetail', {auto: false});
onMounted(() => {
const ins = getCurrentInstance();
const id = ins.router.params.id;
request({id});
})
</script>
<style lang="less">
page{
width: 100%;
height :100%;
}
.task-detail{
background: #efefef;
width: 100%;
height: 100%;
.detail-title{
padding: 20px;
}
.desc-info{
color: #999;
}
.feedback-info{
.feedback-item{
padding: 10px;
margin-bottom: 10px;
border-bottom: 2px solid #f7f7f7;
.user-info{
.avatar{
width: 100px;
height: 100px;
float: left;
margin-right: 10px;
}
.name{
padding: 10px;
font-weight: bold;
}
.time{
color: #999;
}
}
.content{
padding-left: 30px;
padding-top: 20px;
}
}
}
.info-block{
background: #fff;
margin-bottom: 20px;
padding :20px;
.detail-item{
margin: 15px 0;
padding: 15px 0;
border-bottom: 1px solid #efefef;
display: flex;
justify-content: space-between;
font-size: small;
.value{
color: #888;
width: 400px;
text-align: right;
}
}
}
}
</style>
\ No newline at end of file
export default {
navigationStyle: 'custom',
navigationBarTitleText: '任务'
}
<template>
<view class="wrapper top-bg">
<view class="top-nav" :style="{height: capHeight+ capTop+'px'}">
<view class="nav-title" :style="{paddingTop: capTop+5+'px'}">任务</view>
</view>
<view class="tabs">
<view class="tab-item" :class="{active: activeTab === '20'}" @tap="setActive('20')">
进行中
</view>
<view class="tab-item" :class="{active: activeTab === '30'}" @tap="setActive('30')">
已完成
</view>
<view class="tab-item" :class="{active: activeTab === '40'}" @tap="setActive('40')">
已取消
</view>
</view>
<scroll-view :scrollY="true" @scrolltolower="!finished && loadNextPage()" class="task-container">
<view class="task-item" v-for="(item, key) in listData" :key="key" @tap="gotoDetail(item.id)">
<view class="task-title">{{item.task_name}}</view>
<view class="publisher content-item">
<text class="title">发布人:</text>
<text class="value">{{item.author_nickname}}</text>
</view>
<view class="publisher content-item">
<text class="title">开始时间:</text>
<text class="value">{{item.start_time}}</text>
<text class="place"></text>
<!-- <text class="title">结束时间:</text>
<text class="value">{{item.end_time}}</text> -->
</view>
</view>
<view class="loading" v-if="listData.length">
{{finished ? '- 加载完成 -' : '加载中...'}}
</view>
<data-empty v-if="!listData?.length"></data-empty>
</scroll-view>
</view>
</template>
<script lang="ts" setup>
import { navigateTo } from "@tarojs/taro";
import { watch } from "@vue/runtime-core";
import { usePagingData } from "any-hooks/common/usePagingData";
import { useAppInitInfo } from "src/hooks/common/useAppInitInfo";
import { ref } from "vue";
import { useInjector, useState } from "vue-vulcan";
const { listData, loadNextPage, finished, setFilter } = usePagingData<TaskData>('/getTaskList', {params: {task_status: '20'}});
watch(listData, console.log)
const { rect } = useInjector(useAppInitInfo);
const capTop = ref(rect.top);
const capHeight = ref(rect.height+15);
const tabsInfo = ref(['进行中', '已完成', '已取消']);
const [activeTab, setActive] = useState('20');
watch(activeTab, value => {
setFilter('task_status', value)
})
const gotoDetail = (id: string) => {
navigateTo({url: `/pages/task-detail/index?id=${id}`})
}
</script>
<style lang="less">
page{
width:100%;
height: 100%;
}
.wrapper{
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.top-nav{
text-align: center;
.nav-title{
color: #fff;
}
}
.tabs{
display: flex;
.tab-item{
padding: 20px 0;
margin: 0 20px;
color: #fff;
&.active{
border-bottom: 2px solid #fff;
font-weight: bold;
}
}
}
.task-container{
flex: 1;
height: 300px;
background: #efefef;
.task-item{
background: #fff;
margin: 20px;
padding: 20px;
border-left: 5px solid #4880FF;
.task-title{
font-weight: bold;
}
.content-item{
padding-top: 10px;
.title{
color: #999;
}
}
.place{
display: inline-block;
width: 20px;
}
}
}
.loading{
text-align: center;
padding: 10px 0;
color: #999;
font-size: small;
}
}
</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