Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
L
live
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
liujiaxin
live
Commits
6236ec53
Commit
6236ec53
authored
Feb 15, 2020
by
liujiaxin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
台上位置固定6人。下台为空闲状态
parent
da303d2d
Changes
20
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
617 additions
and
324 deletions
+617
-324
package.json
package.json
+1
-1
courseware-frame.tsx
src/components/courseware-frame.tsx
+2
-2
room.tsx
src/components/dialog/room.tsx
+12
-6
member-holder.tsx
src/components/member-holder.tsx
+2
-1
nav.tsx
src/components/nav.tsx
+13
-8
room-tools-bar.scss
src/components/room-tools-bar.scss
+12
-26
room-tools-bar.tsx
src/components/room-tools-bar.tsx
+47
-2
video-player.scss
src/components/video-player.scss
+24
-1
video-player.tsx
src/components/video-player.tsx
+44
-9
root-container.tsx
src/containers/root-container.tsx
+12
-1
use-streams.ts
src/hooks/use-streams.ts
+60
-9
index.scss
src/index.scss
+4
-4
index.tsx
src/pages/classroom/index.tsx
+5
-4
small-class.tsx
src/pages/classroom/small-class.tsx
+9
-9
home.tsx
src/pages/home.tsx
+58
-3
agora-end-points.ts
src/services/agora-end-points.ts
+5
-0
room.ts
src/stores/room.ts
+296
-235
agora-rtm-client.ts
src/utils/agora-rtm-client.ts
+9
-2
consts.ts
src/utils/consts.ts
+1
-1
helper.ts
src/utils/helper.ts
+1
-0
No files found.
package.json
View file @
6236ec53
...
@@ -109,7 +109,7 @@
...
@@ -109,7 +109,7 @@
"electron:copy:web"
:
"cross-env REACT_APP_RUNTIME_PLATFORM=electron react-app-rewired build"
,
"electron:copy:web"
:
"cross-env REACT_APP_RUNTIME_PLATFORM=electron react-app-rewired build"
,
"electron:copy:electron"
:
"cpx ./app/**/*.js ./build"
,
"electron:copy:electron"
:
"cpx ./app/**/*.js ./build"
,
"electron:build"
:
"npm run electron:copy:web && npm run electron:copy:electron"
,
"electron:build"
:
"npm run electron:copy:web && npm run electron:copy:electron"
,
"dev"
:
"cross-env REACT_APP_RUNTIME_PLATFORM=web react-app-rewired start"
,
"dev"
:
"cross-env REACT_APP_RUNTIME_PLATFORM=web react-app-rewired start
"
,
"build"
:
"cross-env REACT_APP_RUNTIME_PLATFORM=web react-app-rewired build"
,
"build"
:
"cross-env REACT_APP_RUNTIME_PLATFORM=web react-app-rewired build"
,
"test"
:
"react-app-rewired test --env=jsdom"
,
"test"
:
"react-app-rewired test --env=jsdom"
,
"analyze"
:
"source-map-explorer 'build/static/js/*.js'"
,
"analyze"
:
"source-map-explorer 'build/static/js/*.js'"
,
...
...
src/components/courseware-frame.tsx
View file @
6236ec53
...
@@ -36,8 +36,8 @@ const useStyles = makeStyles({
...
@@ -36,8 +36,8 @@ const useStyles = makeStyles({
}
}
});
});
const
CourseWareFrame
:
React
.
FC
=
()
=>
{
const
CourseWareFrame
:
React
.
FC
=
()
=>
{
const
classes
=
useStyles
();
const
classes
=
useStyles
();
// `${ENDPOINT}/airclass_ol?token=${this.userToken}`;
const
src
=
endPoint
.
getCourseUrl
();
const
src
=
endPoint
.
getCourseUrl
();
return
(
return
(
<
div
className=
{
classes
.
style1
}
>
<
div
className=
{
classes
.
style1
}
>
...
...
src/components/dialog/room.tsx
View file @
6236ec53
...
@@ -83,8 +83,14 @@ const DialogContainer = () => {
...
@@ -83,8 +83,14 @@ const DialogContainer = () => {
const
onConfirm
=
(
type
:
string
)
=>
{
const
onConfirm
=
(
type
:
string
)
=>
{
if
(
type
===
'
exitRoom
'
)
{
if
(
type
===
'
exitRoom
'
)
{
globalStore
.
removeDialog
();
// debugger
history
.
push
(
'
/
'
);
globalStore
.
showLoading
();
roomStore
.
exitAll
().
then
(()
=>
{
globalStore
.
stopLoading
();
globalStore
.
removeDialog
();
history
.
push
(
'
/
'
);
})
}
}
else
if
(
type
===
'
apply
'
)
{
else
if
(
type
===
'
apply
'
)
{
Promise
.
all
([
Promise
.
all
([
...
@@ -103,16 +109,16 @@ const DialogContainer = () => {
...
@@ -103,16 +109,16 @@ const DialogContainer = () => {
}
}
return
(
return
(
visible
?
visible
?
<
RoomDialog
<
RoomDialog
type=
{
dialog
.
type
}
type=
{
dialog
.
type
}
desc=
{
dialog
.
message
}
desc=
{
dialog
.
message
}
onClose=
{
onClose
}
onClose=
{
onClose
}
onConfirm=
{
onConfirm
}
onConfirm=
{
onConfirm
}
/>
:
/>
:
null
null
)
)
}
}
export
default
React
.
memo
(
DialogContainer
);
export
default
React
.
memo
(
DialogContainer
);
\ No newline at end of file
src/components/member-holder.tsx
View file @
6236ec53
...
@@ -231,7 +231,8 @@ const MemberHolder: React.FC<MemberHolderProps> = ({
...
@@ -231,7 +231,8 @@ const MemberHolder: React.FC<MemberHolderProps> = ({
{
account
?
{
account
?
<
div
className=
"user-profile"
>
<
div
className=
"user-profile"
>
<
span
className=
"account"
>
{
account
}
</
span
>
<
span
className=
"account"
>
{
account
}
</
span
>
{
me
.
uid
===
id
||
me
.
role
===
'
teacher
'
?
{
/*me.uid === id ||*/
}
{
me
.
role
===
'
teacher
'
?
<
span
className=
"media-btn"
>
<
span
className=
"media-btn"
>
<
Link
component=
"button"
onClick=
{
onAudioClick
}
>
{
audio
?
"
静音
"
:
"
开麦
"
}
</
Link
>
<
Link
component=
"button"
onClick=
{
onAudioClick
}
>
{
audio
?
"
静音
"
:
"
开麦
"
}
</
Link
>
{
me
.
role
===
'
teacher
'
?
<
Link
component=
"button"
onClick=
{
onStageUp
}
>
上台
</
Link
>
:
null
}
{
me
.
role
===
'
teacher
'
?
<
Link
component=
"button"
onClick=
{
onStageUp
}
>
上台
</
Link
>
:
null
}
...
...
src/components/nav.tsx
View file @
6236ec53
...
@@ -13,7 +13,9 @@ import { isElectron, platform } from '../utils/platform';
...
@@ -13,7 +13,9 @@ import { isElectron, platform } from '../utils/platform';
import
{
useRoomState
}
from
'
../containers/root-container
'
;
import
{
useRoomState
}
from
'
../containers/root-container
'
;
import
{
roomStore
}
from
'
../stores/room
'
;
import
{
roomStore
}
from
'
../stores/room
'
;
import
{
globalStore
}
from
'
../stores/global
'
;
import
{
globalStore
}
from
'
../stores/global
'
;
import
VoiceOverOffIcon
from
'
@material-ui/icons/VoiceOverOff
'
;
import
RecordVoiceOverIcon
from
'
@material-ui/icons/RecordVoiceOver
'
;
import
RoomToolsBar
from
"
./room-tools-bar
"
;
interface
NavProps
{
interface
NavProps
{
delay
:
string
delay
:
string
network
:
string
network
:
string
...
@@ -41,7 +43,7 @@ export function Nav ({
...
@@ -41,7 +43,7 @@ export function Nav ({
}:
NavProps
)
{
}:
NavProps
)
{
const
{
NavBtn
}
=
usePlatform
();
const
{
NavBtn
}
=
usePlatform
();
const
handleFinish
=
(
evt
:
any
)
=>
{
const
handleFinish
=
(
evt
:
any
)
=>
{
onCardConfirm
(
'
setting
'
);
onCardConfirm
(
'
setting
'
);
}
}
...
@@ -51,7 +53,7 @@ export function Nav ({
...
@@ -51,7 +53,7 @@ export function Nav ({
<
div
className=
{
`nav-container ${isElectron ? 'draggable' : ''}`
}
>
<
div
className=
{
`nav-container ${isElectron ? 'draggable' : ''}`
}
>
<
div
className=
"class-title"
>
<
div
className=
"class-title"
>
<
span
className=
"room-name"
>
{
roomName
}
</
span
>
<
span
className=
"room-name"
>
{
roomName
}
</
span
>
{
role
===
'
teacher
'
?
{
role
===
'
teacher
'
?
<
Button
className=
{
`nav-button ${classState ? "stop" : "start"}`
}
name=
{
classState
?
"
Class end
"
:
"
Class start
"
}
onClick=
{
(
evt
:
any
)
=>
{
<
Button
className=
{
`nav-button ${classState ? "stop" : "start"}`
}
name=
{
classState
?
"
Class end
"
:
"
Class start
"
}
onClick=
{
(
evt
:
any
)
=>
{
handleClick
(
"
classState
"
)
handleClick
(
"
classState
"
)
}
}
/>
:
null
}
}
}
/>
:
null
}
...
@@ -68,18 +70,21 @@ export function Nav ({
...
@@ -68,18 +70,21 @@ export function Nav ({
<
span
className=
"time"
>
{
moment
.
utc
(
time
).
format
(
'
HH:mm:ss
'
)
}
</
span
>
<
span
className=
"time"
>
{
moment
.
utc
(
time
).
format
(
'
HH:mm:ss
'
)
}
</
span
>
</
div
>
</
div
>
<
span
className=
"menu-split"
/>
<
span
className=
"menu-split"
/>
<
div
className=
{
platform
===
'
web
'
?
"
btn-group
"
:
'
electron-btn-group
'
}
>
{
roomStore
.
state
.
me
.
role
==
'
teacher
'
?
<
RoomToolsBar
onClick=
{
handleClick
}
/>
:
null
}
{
/*<div className={platform === 'web' ? "btn-group" : 'electron-btn-group' }>
{platform === 'web' ? <Icon className="icon-setting" onClick={(evt: any) => {
{platform === 'web' ? <Icon className="icon-setting" onClick={(evt: any) => {
handleClick("setting");
handleClick("setting");
}}/> : null}
}}/> : null}
<Icon className="icon-exit" onClick={(evt: any) => {
<Icon className="icon-exit" onClick={(evt: any) => {
handleClick("exit");
handleClick("exit");
}} />
}} />
</
div
>
</div>
*/
}
<
NavBtn
/>
<
NavBtn
/>
</
div
>
</
div
>
</
div
>
</
div
>
{
showSetting
?
{
showSetting
?
<
SettingCard
className=
"internal-card"
<
SettingCard
className=
"internal-card"
handleFinish=
{
handleFinish
}
/>
:
null
handleFinish=
{
handleFinish
}
/>
:
null
}
}
...
@@ -228,7 +233,7 @@ export default function NavContainer() {
...
@@ -228,7 +233,7 @@ export default function NavContainer() {
}
}
return
(
return
(
<
Nav
<
Nav
role=
{
me
.
role
}
role=
{
me
.
role
}
roomName=
{
course
.
roomName
}
roomName=
{
course
.
roomName
}
classState=
{
Boolean
(
course
.
courseState
)
}
classState=
{
Boolean
(
course
.
courseState
)
}
...
@@ -241,4 +246,4 @@ export default function NavContainer() {
...
@@ -241,4 +246,4 @@ export default function NavContainer() {
handleClick=
{
handleClick
}
handleClick=
{
handleClick
}
/>
/>
)
)
}
}
\ No newline at end of file
src/components/room-tools-bar.scss
View file @
6236ec53
.room-tool-box
{
.room-tool-box
{
right
:
10px
;
width
:
46px
;
border
:
1px
solid
#dbe2e5
;
bottom
:
10px
;
cursor
:
pointer
;
display
:
flex
;
display
:
flex
;
z-index
:
9
;
flex-direction
:
row
;
position
:
absolute
;
//background: #fff;
box-shadow
:
0
2px
4px
0
rgba
(
0
,
0
,
0
,.
1
);
box-sizing
:
border-box
;
align-items
:
center
;
line-height
:
42
;
user-select
:
none
;
border-radius
:
6px
;
top
:
10px
;
flex-direction
:
column
;
height
:
90px
;
padding
:
3px
;
background-color
:
rgba
(
51
,
51
,
51
,
0
.4
);
.tool-btn
{
.tool-btn
{
cursor
:
pointer
;
cursor
:
pointer
;
display
:
flex
;
display
:
flex
;
height
:
3
8
px
;
height
:
3
2
px
;
width
:
3
8
px
;
width
:
3
2
px
;
margin
:
2
px
;
margin
:
4
px
;
border-radius
:
4px
;
border-radius
:
4px
;
background
:
#565656
;
//
background: #565656;
box-sizing
:
border-box
;
box-sizing
:
border-box
;
color
:
#eee
;
//
color: #eee;
svg
{
svg
{
width
:
3
8
px
;
width
:
3
0
px
;
height
:
3
8
px
;
height
:
3
0
px
;
display
:
block
;
display
:
block
;
}
}
}
}
.tool-btn
:hover
{
background-color
:
rgba
(
0
,
0
,
0
,
0
.07
);
border-radius
:
4px
;
}
}
}
src/components/room-tools-bar.tsx
View file @
6236ec53
...
@@ -6,9 +6,38 @@ import { VolumeOff, VolumeUp, ImportExport } from '@material-ui/icons';
...
@@ -6,9 +6,38 @@ import { VolumeOff, VolumeUp, ImportExport } from '@material-ui/icons';
import
{
roomStore
}
from
"
../stores/room
"
;
import
{
roomStore
}
from
"
../stores/room
"
;
import
{
useRoomState
}
from
"
../containers/root-container
"
;
import
{
useRoomState
}
from
"
../containers/root-container
"
;
import
'
./room-tools-bar.scss
'
;
import
'
./room-tools-bar.scss
'
;
import
VoiceOverOffIcon
from
"
@material-ui/icons/VoiceOverOff
"
;
import
RecordVoiceOverIcon
from
"
@material-ui/icons/RecordVoiceOver
"
;
import
Category
from
"
@material-ui/icons/Category
"
;
import
ImportContacts
from
"
@material-ui/icons/ImportContacts
"
;
import
SettingsApplicationsIcon
from
'
@material-ui/icons/SettingsApplications
'
;
import
SettingsPowerIcon
from
'
@material-ui/icons/SettingsPower
'
;
import
{
TOOL_TYPE
}
from
"
../utils/types
"
;
type
Props
=
{
// zoomScale: number
// zoomChange: (scale: number) => void
onClick
:
(
type
:
string
)
=>
void
// onClickBoardLock: () => void
};
export
default
function
RoomToolsBar
(
props
:
any
)
{
export
default
function
RoomToolsBar
(
props
:
any
)
{
// export const RoomToolsBar: React.FC<Props> = ({onClick}) => {
const
roomState
=
useRoomState
();
const
roomState
=
useRoomState
();
const
changeTool
=
async
()
=>
{
console
.
log
(
roomStore
.
state
.
course
.
currentTool
);
if
(
roomStore
.
state
.
course
.
currentTool
==
TOOL_TYPE
.
WHITEBOARD
)
{
await
roomStore
.
setCurrentTool
(
TOOL_TYPE
.
COURSE_WARE
)
}
else
{
await
roomStore
.
setCurrentTool
(
TOOL_TYPE
.
WHITEBOARD
)
}
}
const
{
currentTool
}
=
useMemo
(()
=>
{
return
{
currentTool
:
roomStore
.
state
.
course
.
currentTool
}
},
[
roomState
]);
const
{
muteAudio
}
=
useMemo
(()
=>
{
const
{
muteAudio
}
=
useMemo
(()
=>
{
return
{
return
{
muteAudio
:
roomState
.
course
.
muteAudio
muteAudio
:
roomState
.
course
.
muteAudio
...
@@ -25,11 +54,27 @@ export default function RoomToolsBar (props: any) {
...
@@ -25,11 +54,27 @@ export default function RoomToolsBar (props: any) {
const
randomStageUp
=
async
()
=>
{
const
randomStageUp
=
async
()
=>
{
await
roomStore
.
randomStageUp
()
await
roomStore
.
randomStageUp
()
}
}
return
(
return
(
<>
<>
<
div
className=
{
'
room-tool-box
'
}
>
<
div
className=
{
'
room-tool-box
'
}
>
{
muteAudio
?
<
div
className=
"tool-btn"
onClick=
{
audioOn
}
><
VolumeUp
></
VolumeUp
></
div
>
:
<
div
onClick=
{
audioOff
}
className=
"tool-btn"
><
VolumeOff
></
VolumeOff
></
div
>
}
<
div
className=
"tool-btn"
><
ImportExport
onClick=
{
randomStageUp
}
></
ImportExport
></
div
>
{
muteAudio
?
<
div
className=
"tool-btn"
onClick=
{
audioOn
}
><
RecordVoiceOverIcon
/></
div
>
:
<
div
onClick=
{
audioOff
}
className=
"tool-btn"
><
VoiceOverOffIcon
/></
div
>
}
{
currentTool
==
TOOL_TYPE
.
WHITEBOARD
?
<
div
className=
"tool-btn"
onClick=
{
changeTool
}
><
ImportContacts
/></
div
>
:
<
div
onClick=
{
changeTool
}
className=
"tool-btn"
><
Category
/></
div
>
}
<
div
className=
"tool-btn"
onClick=
{
(
evt
:
any
)
=>
{
props
.
onClick
(
"
setting
"
);
}
}
><
SettingsApplicationsIcon
/></
div
>
<
div
className=
"tool-btn"
onClick=
{
(
evt
:
any
)
=>
{
props
.
onClick
(
"
exit
"
);
}
}
><
SettingsPowerIcon
/></
div
>
{
/*<div className="tool-btn"><ImportExport onClick={randomStageUp}></ImportExport></div>*/
}
</
div
>
</
div
>
</>
</>
)
)
...
...
src/components/video-player.scss
View file @
6236ec53
...
@@ -18,7 +18,30 @@
...
@@ -18,7 +18,30 @@
z-index
:
9
;
z-index
:
9
;
}
}
}
}
.menu-close
{
cursor
:
pointer
;
width
:
18px
;
top
:
15px
;
right
:
15px
;
position
:
absolute
;
background-size
:
18px
;
height
:
18px
;
border-radius
:
8px
;
&
:hover
{
background
:
rgba
(
0
,
0
,
0
,
0
.07
);
}
&
:
:
after
{
border-radius
:
8px
;
display
:
block
;
width
:
18px
;
height
:
18px
;
content
:
' '
;
background-position
:
center
;
background-repeat
:
no-repeat
;
background-size
:
18px
;
background-image
:
url('../assets/icon-resource-menu-close.png')
;
}
}
.video-profile
{
.video-profile
{
z-index
:
3
;
z-index
:
3
;
width
:
100%
;
width
:
100%
;
...
...
src/components/video-player.tsx
View file @
6236ec53
...
@@ -4,6 +4,21 @@ import './video-player.scss';
...
@@ -4,6 +4,21 @@ import './video-player.scss';
import
{
AgoraElectronStream
,
StreamType
,
nativeRTCClient
as
nativeClient
}
from
'
../utils/agora-electron-client
'
;
import
{
AgoraElectronStream
,
StreamType
,
nativeRTCClient
as
nativeClient
}
from
'
../utils/agora-electron-client
'
;
import
{
useRoomState
}
from
'
../containers/root-container
'
;
import
{
useRoomState
}
from
'
../containers/root-container
'
;
import
{
platform
}
from
'
../utils/platform
'
;
import
{
platform
}
from
'
../utils/platform
'
;
import
ArrowDownwardIcon
from
'
@material-ui/icons/ArrowDownward
'
;
import
{
makeStyles
}
from
'
@material-ui/core/styles
'
;
const
useStyles
=
makeStyles
({
stageDown
:
{
display
:
'
flex
'
,
width
:
18
,
height
:
24
,
color
:
'
#fff
'
,
lineHeight
:
24
,
opacity
:
.
8
,
cursor
:
'
pointer
'
}
});
const
contentMode
=
0
;
const
contentMode
=
0
;
...
@@ -41,6 +56,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
...
@@ -41,6 +56,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
handleClose
,
handleClose
,
close
close
})
=>
{
})
=>
{
const
style
=
useStyles
();
const
loadVideo
=
useRef
<
boolean
>
(
false
);
const
loadVideo
=
useRef
<
boolean
>
(
false
);
const
loadAudio
=
useRef
<
boolean
>
(
false
);
const
loadAudio
=
useRef
<
boolean
>
(
false
);
...
@@ -219,6 +235,12 @@ const onAudioClick = (evt: any) => {
...
@@ -219,6 +235,12 @@ const onAudioClick = (evt: any) => {
handleClick
(
'
audio
'
,
streamID
,
id
);
handleClick
(
'
audio
'
,
streamID
,
id
);
}
}
}
}
const
onStageDownClick
=
(
evt
:
any
)
=>
{
if
(
handleClick
&&
id
)
{
handleClick
(
'
stageDown
'
,
streamID
,
id
);
}
}
const
onVideoClick
=
(
evt
:
any
)
=>
{
const
onVideoClick
=
(
evt
:
any
)
=>
{
if
(
handleClick
&&
id
)
{
if
(
handleClick
&&
id
)
{
...
@@ -242,17 +264,30 @@ return (
...
@@ -242,17 +264,30 @@ return (
account
?
account
?
<
div
className=
"video-profile"
>
<
div
className=
"video-profile"
>
<
span
className=
"account"
>
{
account
}
</
span
>
<
span
className=
"account"
>
{
account
}
</
span
>
{
me
.
uid
===
id
||
me
.
role
===
'
teacher
'
?
{
streamID
<=
0
?
null
:
<
span
className=
"media-btn"
>
<
div
>
<
Icon
onClick=
{
onAudioClick
}
className=
{
audio
?
"
icon-speaker-on
"
:
"
icon-speaker-off
"
}
data=
{
"
audio
"
}
/>
{
<
Icon
onClick=
{
onVideoClick
}
className=
{
video
?
"
icons-camera-unmute-s
"
:
"
icons-camera-mute-s
"
}
data=
{
"
video
"
}
/>
me
.
uid
===
id
||
me
.
role
===
'
teacher
'
</
span
>
:
?
<
span
className=
"media-btn"
>
<
span
className=
"media-btn"
>
{
me
.
role
===
'
teacher
'
&&
me
.
uid
!==
id
?
<
Icon
className=
{
audio
?
"
icon-speaker-on disabled
"
:
"
icon-speaker-off disabled
"
}
data=
{
"
audio
"
}
/>
<
ArrowDownwardIcon
onClick=
{
onStageDownClick
}
className=
{
style
.
stageDown
}
></
ArrowDownwardIcon
>
<
Icon
className=
{
video
?
"
icons-camera-unmute-s disabled
"
:
"
icons-camera-mute-s disabled
"
}
data=
{
"
video
"
}
/>
:
null
}
</
span
>
<
Icon
onClick=
{
onAudioClick
}
className=
{
audio
?
"
icon-speaker-on
"
:
"
icon-speaker-off
"
}
data=
{
"
audio
"
}
/>
<
Icon
onClick=
{
onVideoClick
}
className=
{
video
?
"
icons-camera-unmute-s
"
:
"
icons-camera-mute-s
"
}
data=
{
"
video
"
}
/>
</
span
>
:
<
span
className=
"media-btn"
>
<
Icon
className=
{
audio
?
"
icon-speaker-on disabled
"
:
"
icon-speaker-off disabled
"
}
data=
{
"
audio
"
}
/>
<
Icon
className=
{
video
?
"
icons-camera-unmute-s disabled
"
:
"
icons-camera-mute-s disabled
"
}
data=
{
"
video
"
}
/>
</
span
>
}
</
div
>
}
}
</
div
>
</
div
>
:
null
:
null
}
}
...
...
src/containers/root-container.tsx
View file @
6236ec53
...
@@ -5,6 +5,7 @@ import { WhiteboardState, whiteboard } from '../stores/whiteboard';
...
@@ -5,6 +5,7 @@ import { WhiteboardState, whiteboard } from '../stores/whiteboard';
import
{
useHistory
,
useLocation
}
from
'
react-router-dom
'
;
import
{
useHistory
,
useLocation
}
from
'
react-router-dom
'
;
import
{
resolveMessage
,
resolvePeerMessage
,
resolveChannelAttrs
,
jsonParse
}
from
'
../utils/helper
'
;
import
{
resolveMessage
,
resolvePeerMessage
,
resolveChannelAttrs
,
jsonParse
}
from
'
../utils/helper
'
;
import
GlobalStorage
from
'
../utils/custom-storage
'
;
import
GlobalStorage
from
'
../utils/custom-storage
'
;
import
{
endPoint
}
from
"
../services/agora-end-points
"
;
export
type
IRootProvider
=
{
export
type
IRootProvider
=
{
globalState
:
GlobalState
globalState
:
GlobalState
...
@@ -105,9 +106,17 @@ export const RootProvider: React.FC<any> = ({children}) => {
...
@@ -105,9 +106,17 @@ export const RootProvider: React.FC<any> = ({children}) => {
}).
catch
(
console
.
warn
);
}).
catch
(
console
.
warn
);
});
});
rtmClient
.
on
(
"
AttributesUpdated
"
,
(
attributes
:
object
)
=>
{
rtmClient
.
on
(
"
AttributesUpdated
"
,
(
attributes
:
object
)
=>
{
if
(
Object
.
keys
(
attributes
).
length
==
0
)
{
globalStore
.
showToast
({
type
:
'
rtmClient
'
,
message
:
'
teacher close classroom
'
});
history
.
push
(
'
/
'
);
return
;
}
const
channelAttrs
=
resolveChannelAttrs
(
attributes
);
const
channelAttrs
=
resolveChannelAttrs
(
attributes
);
// console.log('[rtm-client] updated resolved attrs', channelAttrs);
// console.log('[rtm-client] updated resolved attrs', channelAttrs);
//
console.log('[rtm-client] updated origin attributes', attributes);
console
.
log
(
'
[rtm-client] updated origin attributes
'
,
attributes
);
roomStore
.
updateRoomAttrs
(
channelAttrs
)
roomStore
.
updateRoomAttrs
(
channelAttrs
)
});
});
rtmClient
.
on
(
"
MemberJoined
"
,
(
memberId
:
string
)
=>
{
rtmClient
.
on
(
"
MemberJoined
"
,
(
memberId
:
string
)
=>
{
...
@@ -152,6 +161,8 @@ export const RootProvider: React.FC<any> = ({children}) => {
...
@@ -152,6 +161,8 @@ export const RootProvider: React.FC<any> = ({children}) => {
me
:
room
.
me
,
me
:
room
.
me
,
course
:
room
.
course
,
course
:
room
.
course
,
mediaDevice
:
room
.
mediaDevice
,
mediaDevice
:
room
.
mediaDevice
,
cwLink
:
endPoint
.
userToken
,
studentsOrder
:
room
.
studentsOrder
});
});
// WARN: DEBUG ONLY MUST REMOVED IN PRODUCTION
// WARN: DEBUG ONLY MUST REMOVED IN PRODUCTION
//@ts-ignore
//@ts-ignore
...
...
src/hooks/use-streams.ts
View file @
6236ec53
...
@@ -143,17 +143,67 @@ export default function useStream () {
...
@@ -143,17 +143,67 @@ export default function useStream () {
console
.
log
(
100000
,
studentsOrder
);
console
.
log
(
100000
,
studentsOrder
);
console
.
log
(
11111111
,
studentIds
.
toArray
());
console
.
log
(
11111111
,
studentIds
.
toArray
());
studentIds
=
studentIds
.
sort
(
function
(
a
,
b
){
// studentIds = studentIds.sort(function(a, b){
if
(
studentsOrder
.
indexOf
(
+
a
)
<
0
)
{
return
0
}
// if (studentsOrder.indexOf(+a) < 0) {return 0}
if
(
studentsOrder
.
indexOf
(
+
b
)
<
0
)
{
return
0
}
// if (studentsOrder.indexOf(+b) < 0) {return 0}
return
studentsOrder
.
indexOf
(
+
a
)
-
studentsOrder
.
indexOf
(
+
b
);
// return studentsOrder.indexOf(+a) - studentsOrder.indexOf(+b);
});
// });
console
.
log
(
222222
,
studentIds
.
toArray
());
const
sids
:
any
=
[];
for
(
const
ouid
of
studentsOrder
)
{
if
(
ouid
==
0
)
{
sids
.
push
(
0
)
}
else
{
// find in peer
const
uid
=
studentIds
.
find
((
it
:
number
)
=>
`
${
ouid
}
`
==
`
${
it
}
`
);
if
(
uid
)
{
sids
.
push
(
uid
);
}
else
{
// find in channel
const
usr
=
userAttrs
.
get
(
`
${
uid
}
`
)
if
(
usr
)
{
sids
.
push
(
+
usr
.
uid
);
}
else
{
sids
.
push
(
-
1
);
}
}
}
}
console
.
log
(
222222
,
userAttrs
.
toArray
());
console
.
log
(
333333
,
sids
);
// capture all remote streams
// capture all remote streams
for
(
let
studentId
of
studentIds
)
{
for
(
let
studentId
of
/*studentIds*/
sids
)
{
console
.
log
(
'
*****************
'
,
studentId
)
if
(
studentId
==
undefined
)
{
continue
}
if
(
studentId
==
0
)
{
const
_tmpStream
=
{
account
:
'
空闲
'
,
streamID
:
studentId
,
video
:
0
,
audio
:
0
,
local
:
false
,
}
studentStreams
.
push
(
_tmpStream
);
continue
}
if
(
studentId
==
-
1
)
{
const
_tmpStream
=
{
account
:
'
等待中...
'
,
streamID
:
studentId
,
video
:
0
,
audio
:
0
,
local
:
false
,
}
studentStreams
.
push
(
_tmpStream
);
continue
}
if
(
me
.
role
===
'
student
'
&&
+
me
.
uid
==
studentId
)
{
if
(
me
.
role
===
'
student
'
&&
+
me
.
uid
==
studentId
)
{
if
(
myAttr
&&
roomState
.
rtc
.
localStream
)
{
if
(
myAttr
&&
roomState
.
rtc
.
localStream
)
{
console
.
log
(
'
use local stream
'
)
console
.
log
(
'
use local stream
'
)
...
@@ -168,6 +218,7 @@ export default function useStream () {
...
@@ -168,6 +218,7 @@ export default function useStream () {
continue
continue
}
}
}
}
const
studentAttr
=
userAttrs
.
get
(
''
+
studentId
);
const
studentAttr
=
userAttrs
.
get
(
''
+
studentId
);
const
stream
=
roomState
.
rtc
.
remoteStreams
.
get
(
+
studentId
);
const
stream
=
roomState
.
rtc
.
remoteStreams
.
get
(
+
studentId
);
if
(
studentAttr
)
{
if
(
studentAttr
)
{
...
@@ -190,7 +241,7 @@ export default function useStream () {
...
@@ -190,7 +241,7 @@ export default function useStream () {
}
}
}
}
userAttrs
.
forEach
((
v
,
k
)
=>
{
userAttrs
.
forEach
((
v
,
k
)
=>
{
if
(
!
s
tudentI
ds
.
includes
(
+
v
.
uid
))
{
if
(
!
s
i
ds
.
includes
(
+
v
.
uid
))
{
if
(
`
${
v
.
uid
}
`
!=
`
${
teacherUid
}
`
)
{
if
(
`
${
v
.
uid
}
`
!=
`
${
teacherUid
}
`
)
{
const
s
:
any
=
{
const
s
:
any
=
{
streamID
:
+
v
.
uid
,
streamID
:
+
v
.
uid
,
...
@@ -210,7 +261,7 @@ export default function useStream () {
...
@@ -210,7 +261,7 @@ export default function useStream () {
},
[
},
[
course
,
course
,
me
.
uid
,
me
.
uid
,
roomStore
.
state
.
studentsOrder
,
//
roomStore.state.studentsOrder,
roomState
.
rtc
.
users
,
roomState
.
rtc
.
users
,
roomState
.
rtc
.
remoteStreams
,
roomState
.
rtc
.
remoteStreams
,
roomState
.
rtc
.
localStream
roomState
.
rtc
.
localStream
...
...
src/index.scss
View file @
6236ec53
...
@@ -95,7 +95,7 @@ body {
...
@@ -95,7 +95,7 @@ body {
background
:
#F5F7F8
;
background
:
#F5F7F8
;
border-radius
:
8px
0px
0px
8px
;
border-radius
:
8px
0px
0px
8px
;
position
:
relative
;
position
:
relative
;
.cover-placeholder
{
.cover-placeholder
{
display
:
block
;
display
:
block
;
background-repeat
:
no-repeat
;
background-repeat
:
no-repeat
;
...
@@ -288,13 +288,13 @@ body {
...
@@ -288,13 +288,13 @@ body {
@media
screen
and
(
max-width
:
780px
)
{
@media
screen
and
(
max-width
:
780px
)
{
.home-cover-web
{
.home-cover-web
{
min-width
:
780px
;
//
min-width: 780px;
}
}
}
}
@media
screen
and
(
max-height
:
768px
)
{
@media
screen
and
(
max-height
:
768px
)
{
.home-cover-web
{
.home-cover-web
{
min-height
:
768px
;
//
min-height: 768px;
}
}
}
}
...
@@ -322,4 +322,4 @@ body {
...
@@ -322,4 +322,4 @@ body {
}
}
}
}
}
}
}
}
\ No newline at end of file
src/pages/classroom/index.tsx
View file @
6236ec53
...
@@ -213,7 +213,7 @@ export function RoomPage({ children }: any) {
...
@@ -213,7 +213,7 @@ export function RoomPage({ children }: any) {
]);
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
debugger
//
debugger
if
(
!
roomState
.
me
.
uid
||
!
roomState
.
course
.
rid
)
return
;
if
(
!
roomState
.
me
.
uid
||
!
roomState
.
course
.
rid
)
return
;
if
(
classroom
)
{
if
(
classroom
)
{
if
(
platform
===
'
web
'
)
{
if
(
platform
===
'
web
'
)
{
...
@@ -301,7 +301,7 @@ export function RoomPage({ children }: any) {
...
@@ -301,7 +301,7 @@ export function RoomPage({ children }: any) {
})
})
*/
*/
rtc
.
current
=
true
;
rtc
.
current
=
true
;
debugger
//
debugger
// WARN: IF YOU ENABLED APP CERTIFICATE, PLEASE SIGN YOUR TOKEN IN YOUR SERVER SIDE AND OBTAIN IT FROM YOUR OWN TRUSTED SERVER API
// WARN: IF YOU ENABLED APP CERTIFICATE, PLEASE SIGN YOUR TOKEN IN YOUR SERVER SIDE AND OBTAIN IT FROM YOUR OWN TRUSTED SERVER API
console
.
info
(
1.1
,
'
rtc join
'
)
console
.
info
(
1.1
,
'
rtc join
'
)
// if (roomState.me.role == 'teacher') {
// if (roomState.me.role == 'teacher') {
...
@@ -311,7 +311,7 @@ export function RoomPage({ children }: any) {
...
@@ -311,7 +311,7 @@ export function RoomPage({ children }: any) {
uid
:
+
roomState
.
me
.
uid
,
uid
:
+
roomState
.
me
.
uid
,
channel
:
roomState
.
course
.
rid
,
channel
:
roomState
.
course
.
rid
,
token
:
''
,
token
:
''
,
dual
:
isSmallClass
dual
:
true
}).
then
(()
=>
{
}).
then
(()
=>
{
console
.
info
(
2
,
'
rtc join ok
'
)
console
.
info
(
2
,
'
rtc join ok
'
)
roomStore
.
publishMeStream
();
roomStore
.
publishMeStream
();
...
@@ -452,10 +452,11 @@ export function RoomPage({ children }: any) {
...
@@ -452,10 +452,11 @@ export function RoomPage({ children }: any) {
console
.
log
(
1.15
,
'
stagedown?
'
,
222222
);
console
.
log
(
1.15
,
'
stagedown?
'
,
222222
);
roomStore
.
unPublishMeStream
();
roomStore
.
unPublishMeStream
();
}
}
console
.
log
(
1.2
,
studentsOrder
,
'
studentsOrder changed publish stream
'
);
}
}
console
.
log
(
1.2
,
studentsOrder
,
'
studentsOrder changed publish stream
'
);
},
[
studentsOrder
]);
},
[
studentsOrder
]);
...
...
src/pages/classroom/small-class.tsx
View file @
6236ec53
import
React
,
{
useEffect
,
useLayoutEffect
,
useMemo
,
useRef
,
useState
}
from
'
react
'
;
import
React
,
{
useEffect
,
useLayoutEffect
,
useMemo
,
useRef
,
useState
}
from
'
react
'
;
// import VideoMarquee from '../../components/video-marquee';
// import VideoMarquee from '../../components/video-marquee';
import
MediaBoard
from
'
../../components/mediaboard
'
;
import
MediaBoard
from
'
../../components/mediaboard
'
;
import
Roomboard
from
'
../../components/roomboard
'
;
//
import Roomboard from '../../components/roomboard';
import
'
./small-class.scss
'
;
import
'
./small-class.scss
'
;
import
CourseWareFrame
from
"
../../components/courseware-frame
"
;
import
CourseWareFrame
from
"
../../components/courseware-frame
"
;
import
VideoShowList
from
"
../../components/video-stage-list
"
;
import
VideoShowList
from
"
../../components/video-stage-list
"
;
import
{
ToolsSwitcherController
}
from
"
../../components/tools-switcher-controller
"
;
import
{
ToolsSwitcherController
}
from
"
../../components/tools-switcher-controller
"
;
import
{
roomStore
}
from
"
../../stores/room
"
;
import
{
roomStore
}
from
"
../../stores/room
"
;
import
{
TOOL_TYPE
}
from
"
../../utils/types
"
;
import
{
TOOL_TYPE
}
from
"
../../utils/types
"
;
import
{
platform
}
from
"
../../utils/platform
"
;
//
import {platform} from "../../utils/platform";
import
AgoraWebClient
from
"
../../utils/agora-rtc-client
"
;
//
import AgoraWebClient from "../../utils/agora-rtc-client";
import
{
AgoraElectronClient
}
from
"
../../utils/agora-electron-client
"
;
//
import {AgoraElectronClient} from "../../utils/agora-electron-client";
import
{
useRoomState
}
from
"
../../containers/root-container
"
;
import
{
useRoomState
}
from
"
../../containers/root-container
"
;
import
RoomToolsBar
from
"
../../components/room-tools-bar
"
;
//
import RoomToolsBar from "../../components/room-tools-bar";
export
default
function
SmallClass
()
{
export
default
function
SmallClass
()
{
const
[
tool
,
setTool
]
=
useState
(
true
);
const
[
tool
,
setTool
]
=
useState
(
true
);
...
@@ -39,15 +39,15 @@ export default function SmallClass() {
...
@@ -39,15 +39,15 @@ export default function SmallClass() {
{
/*<VideoMarquee />*/
}
{
/*<VideoMarquee />*/
}
<
VideoShowList
/>
<
VideoShowList
/>
<
div
className=
"container"
>
<
div
className=
"container"
>
{
roomStore
.
state
.
me
.
role
==
'
teacher
'
?<
ToolsSwitcherController
onClick=
{
()
=>
{
{
/*{roomStore.state.me.role == 'teacher' ?<ToolsSwitcherController onClick={() => {*/
}
changeTool
();
{
/* changeTool();*/
}
}
}
/>
:
null
}
{
/*}}/> : null}*/
}
{
currentTool
==
TOOL_TYPE
.
WHITEBOARD
{
currentTool
==
TOOL_TYPE
.
WHITEBOARD
?
<
MediaBoard
/>
?
<
MediaBoard
/>
:
<
CourseWareFrame
></
CourseWareFrame
>
}
:
<
CourseWareFrame
></
CourseWareFrame
>
}
{
roomStore
.
state
.
me
.
role
==
'
teacher
'
?
<
RoomToolsBar
/>
:
null
}
{
/*{ roomStore.state.me.role == 'teacher' ? <Roomboard currentActive={'media'} /> : null}*/
}
{
/*{ roomStore.state.me.role == 'teacher' ? <Roomboard currentActive={'media'} /> : null}*/
}
</
div
>
</
div
>
</
div
>
</
div
>
...
...
src/pages/home.tsx
View file @
6236ec53
...
@@ -79,17 +79,70 @@ function HomePage() {
...
@@ -79,17 +79,70 @@ function HomePage() {
setRequired
({...
required
,
yourPass
:
'
missing your password
'
});
setRequired
({...
required
,
yourPass
:
'
missing your password
'
});
return
;
return
;
}
}
///////////////////////////////
/*
session.roomName = "test_01__123";// userInfo["class_id"];
// session.yourName = userInfo["nick_name"];
session.role = `${session.yourName}`==="1"?"teacher":"student";
session.roomType = 1;
const path = roomTypes[session.roomType].path
const payload = {
uid: `${session.yourName}`, // genUid(),
rid: `${session.roomName}`, // `${session.roomType}${MD5(session.roomName)}`,
role: session.role,
roomName: session.roomName,
roomType: session.roomType,
video: 0,
audio: 0,
chat: 1,
account: session.yourName,
token: '',
boardId: '',
linkId: 0,
sharedId: 0,
lockBoard: 0,
}
ref.current = true;
globalStore.showLoading();
roomStore.loginAndJoin(payload).then(() => {
roomStore.updateSessionInfo(payload);
history.push(`/classroom/${path}`);
}).catch((err: any) => {
if (err.reason) {
globalStore.showToast({
type: 'rtmClient',
message: err.reason
})
} else {
globalStore.showToast({
type: 'rtmClient',
message: 'login failure, please checkout ur network'
})
}
console.warn(err);
})
.finally(() => {
ref.current = false;
globalStore.stopLoading();
})
*/
///////////////////////////////
endPoint
.
login
({
username
:
session
.
yourName
,
password
:
session
.
yourPass
}).
then
((
userInfo
:
any
)
=>
{
endPoint
.
login
({
username
:
session
.
yourName
,
password
:
session
.
yourPass
}).
then
((
userInfo
:
any
)
=>
{
console
.
log
(
userInfo
);
console
.
log
(
userInfo
);
session
.
roomName
=
userInfo
[
"
class_id
"
];
session
.
roomName
=
userInfo
[
"
class_id
"
];
// userInfo["class_id"];
session
.
yourName
=
userInfo
[
"
nick_name
"
];
//
session.yourName = userInfo["nick_name"];
session
.
role
=
userInfo
[
"
classRole
"
]
===
"
tea
"
?
"
teacher
"
:
"
student
"
;
session
.
role
=
userInfo
[
"
classRole
"
]
===
"
tea
"
?
"
teacher
"
:
"
student
"
;
session
.
roomType
=
1
;
session
.
roomType
=
1
;
if
(
!
roomTypes
[
session
.
roomType
])
return
;
if
(
!
roomTypes
[
session
.
roomType
])
return
;
const
path
=
roomTypes
[
session
.
roomType
].
path
const
path
=
roomTypes
[
session
.
roomType
].
path
const
payload
=
{
const
payload
=
{
uid
:
`
${
userInfo
[
"
id
"
]}
`
,
// genUid(),
uid
:
userInfo
[
"
classRole
"
]
===
"
tea
"
?
"
100
"
:
`
${
session
.
yourName
.
substr
(
-
2
)}
`
,
//
`${userInfo["id"]}`, // genUid(),
rid
:
`
${
session
.
roomName
}
`
,
// `${session.roomType}${MD5(session.roomName)}`,
rid
:
`
${
session
.
roomName
}
`
,
// `${session.roomType}${MD5(session.roomName)}`,
role
:
session
.
role
,
role
:
session
.
role
,
roomName
:
session
.
roomName
,
roomName
:
session
.
roomName
,
...
@@ -133,6 +186,8 @@ function HomePage() {
...
@@ -133,6 +186,8 @@ function HomePage() {
message
:
err
message
:
err
})
})
});
});
}
}
return
(
return
(
...
...
src/services/agora-end-points.ts
View file @
6236ec53
import
{
AgoraFetch
}
from
"
../utils/fetch
"
;
import
{
AgoraFetch
}
from
"
../utils/fetch
"
;
import
jwt_decode
from
'
jwt-decode
'
;
import
jwt_decode
from
'
jwt-decode
'
;
import
{
roomStore
}
from
"
../stores/room
"
;
const
ENDPOINT
:
string
=
process
.
env
.
REACT_APP_AGORA_ENDPOINT
as
string
;
const
ENDPOINT
:
string
=
process
.
env
.
REACT_APP_AGORA_ENDPOINT
as
string
;
...
@@ -78,6 +79,10 @@ export class EndPoint {
...
@@ -78,6 +79,10 @@ export class EndPoint {
}
}
getCourseUrl
(){
getCourseUrl
(){
if
(
!
this
.
userToken
)
{
this
.
userToken
=
roomStore
.
state
.
cwLink
;
}
console
.
log
(
'
getCourseUrl
'
,
this
.
userToken
)
return
`
${
ENDPOINT
}
/airclass_ol?token=
${
this
.
userToken
}
`
;
return
`
${
ENDPOINT
}
/airclass_ol?token=
${
this
.
userToken
}
`
;
}
}
...
...
src/stores/room.ts
View file @
6236ec53
This diff is collapsed.
Click to expand it.
src/utils/agora-rtm-client.ts
View file @
6236ec53
...
@@ -98,9 +98,12 @@ export default class AgoraRTMClient {
...
@@ -98,9 +98,12 @@ export default class AgoraRTMClient {
this
.
_logged
=
true
;
this
.
_logged
=
true
;
return
return
}
}
async
clearChannelAttributesByKeys
(
uids
:
string
[])
{
await
this
.
_client
.
deleteChannelAttributesByKeys
(
this
.
_currentChannelName
,
uids
,
{
enableNotificationToChannelMembers
:
true
});
}
async
clearChannelAttributes
()
{
async
clearChannelAttributes
()
{
await
this
.
_client
.
clearChannelAttributes
(
this
.
_currentChannelName
);
await
this
.
_client
.
clearChannelAttributes
(
this
.
_currentChannelName
,
{
enableNotificationToChannelMembers
:
true
}
);
}
}
async
logout
()
{
async
logout
()
{
...
@@ -155,7 +158,11 @@ export default class AgoraRTMClient {
...
@@ -155,7 +158,11 @@ export default class AgoraRTMClient {
this
.
destroyChannel
(
channel
);
this
.
destroyChannel
(
channel
);
}
}
}
}
async
leaveChannel
()
{
// await this._client.clearLocalUserAttributes();
await
this
.
leave
(
this
.
_currentChannelName
);
await
this
.
logout
();
}
async
exit
()
{
async
exit
()
{
await
this
.
deleteChannelAttributesByKey
();
await
this
.
deleteChannelAttributesByKey
();
await
this
.
leave
(
this
.
_currentChannelName
);
await
this
.
leave
(
this
.
_currentChannelName
);
...
...
src/utils/consts.ts
View file @
6236ec53
export
const
STAGE_NUM
:
number
=
1
;
export
const
STAGE_NUM
:
number
=
6
;
src/utils/helper.ts
View file @
6236ec53
...
@@ -256,6 +256,7 @@ export function resolveChannelAttrs(json: object) {
...
@@ -256,6 +256,7 @@ export function resolveChannelAttrs(json: object) {
let
students
:
any
[]
=
[];
let
students
:
any
[]
=
[];
for
(
let
key
of
Object
.
keys
(
json
))
{
for
(
let
key
of
Object
.
keys
(
json
))
{
if
(
key
===
'
teacher
'
)
continue
;
if
(
key
===
'
teacher
'
)
continue
;
if
(
!
/^
[
0-9
]
+$/
.
test
(
key
))
continue
;
const
student
=
jsonParse
(
_
.
get
(
json
,
`
${
key
}
.value`
));
const
student
=
jsonParse
(
_
.
get
(
json
,
`
${
key
}
.value`
));
if
(
student
&&
Object
.
keys
(
student
).
length
)
{
if
(
student
&&
Object
.
keys
(
student
).
length
)
{
student
.
uid
=
key
;
student
.
uid
=
key
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment