14 changed files with 914 additions and 175 deletions
@ -1,5 +1,64 @@ |
|||||||
该仓库是go-chat的前端界面。 |
## go-chat |
||||||
后端是使用go开发的,基于WebSocket的聊天程序。 |
使用Go基于WebSocket的通讯聊天软件。 |
||||||
后端仓库地址: |
|
||||||
https://github.com/kone-net/go-chat |
|
||||||
|
|
||||||
|
### 功能列表: |
||||||
|
* 登录注册 |
||||||
|
* 修改头像 |
||||||
|
* 群聊天 |
||||||
|
* 群好友列表 |
||||||
|
* 单人聊天 |
||||||
|
* 添加好友 |
||||||
|
* 添加群组 |
||||||
|
* 文本消息 |
||||||
|
* 剪切板图片 |
||||||
|
* 图片消息 |
||||||
|
* 文件发送 |
||||||
|
* 语音消息 |
||||||
|
* 视频消息 |
||||||
|
* 屏幕共享(基于图片) |
||||||
|
* 视频通话(基于webrtc的p2p视频通话) |
||||||
|
|
||||||
|
## 后端 |
||||||
|
[代码仓库](https://github.com/kone-net/go-chat) |
||||||
|
go中协程是非常轻量级的。在每个client接入的时候,为每一个client开启一个协程,能够在单机实现更大的并发。同时go的channel,可以非常完美的解耦client接入和消息的转发等操作。 |
||||||
|
|
||||||
|
通过go-chat,可以掌握channel的和Select的配合使用,ORM框架的使用,web框架Gin的使用,配置管理,日志操作,还包括proto buffer协议的使用,等一些列项目中常用的技术。 |
||||||
|
|
||||||
|
|
||||||
|
### 后端技术和框架 |
||||||
|
* web框架Gin |
||||||
|
* 长连接WebSocket |
||||||
|
* 日志框架Uber的zap |
||||||
|
* 配置管理viper |
||||||
|
* ORM框架gorm |
||||||
|
* 通讯协议Google的proto buffer |
||||||
|
* makefile 的编写 |
||||||
|
* 数据库MySQL |
||||||
|
* 图片文件二进制操作 |
||||||
|
|
||||||
|
## 前端 |
||||||
|
基于react,UI和基本组件是使用ant design。可以很方便搭建前端界面。 |
||||||
|
|
||||||
|
界面选择单页框架可以更加方便写聊天界面,比如像消息提醒,可以在一个界面接受到消息进行提醒,不会因为换页面或者查看其他内容影响消息接受。 |
||||||
|
[前端代码仓库](https://github.com/kone-net/go-chat-web): |
||||||
|
https://github.com/kone-net/go-chat-web |
||||||
|
|
||||||
|
|
||||||
|
### 前端技术和框架 |
||||||
|
* React |
||||||
|
* Redux状态管理 |
||||||
|
* AntDesign |
||||||
|
* proto buffer的使用 |
||||||
|
* WebSocket |
||||||
|
* 剪切板的文件读取和操作 |
||||||
|
* 聊天框发送文字显示底部 |
||||||
|
* FileReader对文件操作 |
||||||
|
* ArrayBuffer,Blob,Uint8Array之间的转换 |
||||||
|
* 获取摄像头视频(mediaDevices) |
||||||
|
* 获取麦克风音频(Recorder) |
||||||
|
* 获取屏幕共享(mediaDevices) |
||||||
|
* WebRTC的p2p视频通话 |
||||||
|
|
||||||
|
|
||||||
|
### 截图 |
||||||
|
 |
||||||
|
|||||||
|
After Width: | Height: | Size: 161 KiB |
@ -0,0 +1,159 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { |
||||||
|
Avatar, |
||||||
|
Button, |
||||||
|
Dropdown, |
||||||
|
Menu, |
||||||
|
Modal, |
||||||
|
Upload, |
||||||
|
message |
||||||
|
} from 'antd'; |
||||||
|
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons'; |
||||||
|
|
||||||
|
import { connect } from 'react-redux' |
||||||
|
import { actions } from '../redux/module/userInfo' |
||||||
|
import * as Params from '../common/param/Params' |
||||||
|
|
||||||
|
function getBase64(img, callback) { |
||||||
|
const reader = new FileReader(); |
||||||
|
reader.addEventListener('load', () => callback(reader.result)); |
||||||
|
reader.readAsDataURL(img); |
||||||
|
} |
||||||
|
|
||||||
|
function beforeUpload(file) { |
||||||
|
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'; |
||||||
|
if (!isJpgOrPng) { |
||||||
|
message.error('You can only upload JPG/PNG file!'); |
||||||
|
} |
||||||
|
const isLt2M = file.size / 1024 / 1024 < 2; |
||||||
|
if (!isLt2M) { |
||||||
|
message.error('Image must smaller than 2MB!'); |
||||||
|
} |
||||||
|
return isJpgOrPng && isLt2M; |
||||||
|
} |
||||||
|
|
||||||
|
class UserInfo extends React.Component { |
||||||
|
constructor(props) { |
||||||
|
super(props) |
||||||
|
let user = {} |
||||||
|
if (props.user) { |
||||||
|
user = props.user |
||||||
|
} |
||||||
|
this.state = { |
||||||
|
user: user, |
||||||
|
isModalVisible: false, |
||||||
|
loading: false, |
||||||
|
imageUrl: '' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
componentDidMount() { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
modifyAvatar = () => { |
||||||
|
this.setState({ |
||||||
|
isModalVisible: true |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
handleCancel = () => { |
||||||
|
this.setState({ |
||||||
|
isModalVisible: false |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
loginout = () => { |
||||||
|
this.props.history.push("/login") |
||||||
|
} |
||||||
|
|
||||||
|
handleChange = info => { |
||||||
|
if (info.file.status === 'uploading') { |
||||||
|
this.setState({ loading: true }); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (info.file.status === 'done') { |
||||||
|
let response = info.file.response |
||||||
|
if (response.code !== 0) { |
||||||
|
message.error(info.file.response.msg) |
||||||
|
} |
||||||
|
|
||||||
|
let user = { |
||||||
|
...this.props.user, |
||||||
|
avatar: Params.HOST + "/file/" + info.file.response.data |
||||||
|
} |
||||||
|
this.props.setUser(user) |
||||||
|
// Get this url from response in real world. |
||||||
|
getBase64(info.file.originFileObj, imageUrl => |
||||||
|
this.setState({ |
||||||
|
imageUrl, |
||||||
|
loading: false, |
||||||
|
}), |
||||||
|
); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
render() { |
||||||
|
const menu = ( |
||||||
|
<Menu> |
||||||
|
<Menu.Item key={1}> |
||||||
|
<Button type='link'>{this.props.user.username}</Button> |
||||||
|
</Menu.Item> |
||||||
|
<Menu.Item key={2}> |
||||||
|
<Button type='link' onClick={this.modifyAvatar}>更新头像</Button> |
||||||
|
</Menu.Item> |
||||||
|
<Menu.Item key={3}> |
||||||
|
<Button type='link' onClick={this.loginout}>退出</Button> |
||||||
|
</Menu.Item> |
||||||
|
</Menu> |
||||||
|
); |
||||||
|
|
||||||
|
const { loading, imageUrl } = this.state; |
||||||
|
const uploadButton = ( |
||||||
|
<div> |
||||||
|
{loading ? <LoadingOutlined /> : <PlusOutlined />} |
||||||
|
<div style={{ marginTop: 8 }}>Upload</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Dropdown overlay={menu} placement="bottomCenter" arrow> |
||||||
|
<Avatar src={this.props.user.avatar} alt={this.props.user.username} /> |
||||||
|
</Dropdown> |
||||||
|
|
||||||
|
<Modal title="更新头像" visible={this.state.isModalVisible} onCancel={this.handleCancel} footer={null}> |
||||||
|
<Upload |
||||||
|
name="file" |
||||||
|
listType="picture-card" |
||||||
|
className="avatar-uploader" |
||||||
|
showUploadList={false} |
||||||
|
action={Params.FILE_URL} |
||||||
|
beforeUpload={beforeUpload} |
||||||
|
onChange={this.handleChange} |
||||||
|
data={{uuid: this.props.user.uuid}} |
||||||
|
> |
||||||
|
{imageUrl ? <img src={imageUrl} alt="avatar" style={{ width: '100%' }} /> : uploadButton} |
||||||
|
</Upload> |
||||||
|
</Modal> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function mapStateToProps(state) { |
||||||
|
return { |
||||||
|
user: state.userInfoReducer.user, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) { |
||||||
|
return { |
||||||
|
setUser: (data) => dispatch(actions.setUser(data)), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
UserInfo = connect(mapStateToProps, mapDispatchToProps)(UserInfo) |
||||||
|
|
||||||
|
export default UserInfo |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
import { createStore, combineReducers, applyMiddleware } from 'redux' |
||||||
|
import thunk from 'redux-thunk' |
||||||
|
import userInfoReducer from './userInfo' |
||||||
|
|
||||||
|
const reducer = combineReducers({ |
||||||
|
userInfoReducer |
||||||
|
}); |
||||||
|
|
||||||
|
export default createStore(reducer, applyMiddleware(thunk)); |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
const initialState = { |
||||||
|
user: {} |
||||||
|
} |
||||||
|
|
||||||
|
export const types = { |
||||||
|
USER_SET: 'USER/SET', |
||||||
|
} |
||||||
|
|
||||||
|
export const actions = { |
||||||
|
setUser: (user) => ({ |
||||||
|
type: types.USER_SET, |
||||||
|
user: user |
||||||
|
}), |
||||||
|
} |
||||||
|
|
||||||
|
const userInfoReducer = (state = initialState, action) => { |
||||||
|
switch (action.type) { |
||||||
|
case types.USER_SET: |
||||||
|
return { ...state, user: action.user } |
||||||
|
default: |
||||||
|
return state |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default userInfoReducer |
||||||
Loading…
Reference in new issue