├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .umirc.js
├── README.md
├── index.html
├── jsconfig.json
├── mock
└── demo.mock.js
├── package.json
├── public
└── image
│ ├── head-logo.jpg
│ └── recording.gif
└── src
├── components
└── Test.js
├── global.less
├── layouts
├── index.js
└── index.less
├── models
└── public.js
├── pages
├── document.ejs
├── index.js
└── view
│ └── home
│ ├── coms
│ ├── chat
│ │ ├── coms
│ │ │ ├── historyTime
│ │ │ │ ├── index.js
│ │ │ │ └── index.less
│ │ │ ├── sendText
│ │ │ │ ├── index.js
│ │ │ │ └── index.less
│ │ │ └── sendVoice
│ │ │ │ ├── index.js
│ │ │ │ └── index.less
│ │ ├── index.js
│ │ └── index.less
│ ├── operate
│ │ ├── coms
│ │ │ ├── coms
│ │ │ │ ├── speak.js
│ │ │ │ └── speak.less
│ │ │ ├── more.js
│ │ │ ├── more.less
│ │ │ ├── text.js
│ │ │ ├── text.less
│ │ │ ├── voice.js
│ │ │ └── voice.less
│ │ ├── index.js
│ │ └── index.less
│ └── operateMore
│ │ ├── index.js
│ │ └── index.less
│ ├── index.js
│ ├── index.less
│ ├── models
│ └── index.js
│ └── services
│ └── index.js
├── services
└── publicApi.js
├── themes
└── vars.less
└── utils
├── fetch.js
├── index.js
└── request.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*
2 | config/*
3 | docs/*
4 | node_modules/*
5 | public/*
6 | scripts/*
7 | mock/*
8 | package.json
9 | umi
10 | .umi-production
11 |
12 | src/utils/tools/ramda.js
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csj5588/IM-Chat-Cli/a85544491e2930812a53cc8dcfd7035cb43db943/.eslintrc.js
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 | .umi
4 | .umi-production
5 | package-*
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | .uploadrc
10 | .DS_Store
11 |
12 |
--------------------------------------------------------------------------------
/.umirc.js:
--------------------------------------------------------------------------------
1 | const { join } = require("path");
2 |
3 | const umircExport = {
4 | hash: true,
5 | plugins: [
6 | [
7 | "umi-plugin-react",
8 | {
9 | antd: true,
10 | dva: true,
11 | routes: {
12 | exclude: [
13 | /models|services|components\//,
14 | o => /[A-Z]/.test(o.component),
15 | o => !/[\\/]((index)|(404)|(_layout))[\\.]js$/.test(o.component),
16 | ],
17 | },
18 | },
19 | ],
20 | ],
21 | alias: {
22 | components: join(process.cwd(), "src", "components"),
23 | utils: join(process.cwd(), "src", "utils"),
24 | assets: join(process.cwd(), "src", "assets"),
25 | themes: join(process.cwd(), "src", "themes"),
26 | config: join(process.cwd(), "src", "config"),
27 | public: join(process.cwd(), "public"),
28 | },
29 | };
30 |
31 | export default umircExport;
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## IM-CHAT-CLI
2 |
3 | ---
4 |
5 | This is an instant messaging layout scaffold to solve the problem of internal system instant messaging without framework。
6 | (移动端IM(即时通讯)布局脚手架。解决内部及时通讯无框架的问题)
7 |
8 |
9 | ### 使用
10 | 安装依赖
11 |
12 | ```
13 | cnpm install
14 | ```
15 | 启动项目
16 | ```
17 | cnpm run start
18 | ```
19 |
20 | ### 项目结构
21 |
22 |
23 | public // 公共文件 可以放一些第三方字体 样式库等
24 | src
25 | |-- components // 公共组件目录 当业务需要拆分组件的时候,可以在对应的业务文件夹下单独创建一个components文件夹
26 | |-- models // 公共model存放位置
27 | |-- public.js // 公共model文件 可以多个
28 | |-- pages // 容器组件
29 | |-- .umi // umi自动生成配置文件
30 | |-- view // 业务容器 相对路由/demo ***不可以有任何大写字母
31 | |-- home // IM业务
32 | |-- coms // IM业务组件
33 | |-- modules // 业务model目录 model自动加载
34 | |-- service // 业务api目录
35 | |-- index.js // 业务入口 入口文件只识别index.js 后缀必须是js
36 | |-- index.less // 业务样式
37 | |-- document.ejs // html模板
38 | |-- index.js // 入口文件
39 | |-- services // 公共api存放
40 | |-- themes // 公共主题样式
41 | |-- vars.less // 公共变量样式
42 | |-- utils // 工具
43 | |-- fetch // fetch封装
44 | |-- request // 请求方法封装
45 | |-- global.less // 移动端全局样式初始化,样式兼容性处理等
46 | .eslintignore // eslint过滤文件清单
47 | .eslintrc.js // eslint配置
48 | .gitignore
49 | package.json
50 | README.md
51 |
52 |
53 | ## 页面布局
54 |
55 | 
56 |
57 | ## 效果一览(gif较大)
58 |
59 | | 名称 | 示意图 |
60 | | ---------- | ----------------------------------------- |
61 | | 文字布局 | |
62 | | 键盘布局 | |
63 | | 语音效果 | |
64 | | 操作效果 | |
65 |
66 | ## TODO
67 |
68 | - [√] 移动端基础架构
69 | - [√] IM-Chat布局
70 | - [√] eslint
71 | - [√] 基础结构
72 | - [√] dav
73 | - [√] umi
74 | - [√] fetch
75 | - [√] mock
76 | - [√] 异常统一处理
77 |
78 | ## END
79 | 项目将持续稳定更新迭代,欢迎各种issue.
80 |
81 | [](https://github.com/anuraghazra/github-readme-stats)
82 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 | root
9 |
10 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/mock/demo.mock.js:
--------------------------------------------------------------------------------
1 | const mock = {
2 | "POST /-alarmWeixinApi/business/getChatMessageInfo": {}
3 | };
4 | module.exports = mock;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "IM-Chat-Cli",
3 | "version": "1.0.0",
4 | "description": "IM Chat Layout scaffolding",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "umi dev",
8 | "build": "umi build",
9 | "eslint": "eslint --ext .js --ext .jsx ./src",
10 | "lint-staged": "lint-staged"
11 | },
12 | "author": "",
13 | "license": "ISC",
14 | "devDependencies": {
15 | "babel-eslint": "^10.0.1",
16 | "eslint": "^5.12.1",
17 | "eslint-config-airbnb": "^17.1.0",
18 | "eslint-config-prettier": "^3.5.0",
19 | "eslint-plugin-import": "^2.14.0",
20 | "eslint-plugin-jsx-a11y": "^6.1.2",
21 | "eslint-plugin-react": "^7.12.4",
22 | "pre-commit": "^1.2.2",
23 | "cross-env": "^5.2.0",
24 | "lint-staged": "^8.1.0"
25 | },
26 | "dependencies": {
27 | "antd-mobile": "^2.2.8",
28 | "classnames": "^2.2.6",
29 | "dayjs": "^1.8.12",
30 | "dva": "^2.4.1",
31 | "isomorphic-fetch": "^2.2.1",
32 | "query-string": "^5.1.1",
33 | "react": "^16.4.2",
34 | "react-dom": "^16.4.2",
35 | "react-helmet": "^5.2.0",
36 | "react-router-dom": "^4.3.1",
37 | "swiper": "^4.5.0",
38 | "umi": "^2.4.3",
39 | "umi-plugin-react": "^1.4.1",
40 | "vconsole": "^3.3.0"
41 | },
42 | "prettier": {
43 | "trailingComma": true
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/public/image/head-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csj5588/IM-Chat-Cli/a85544491e2930812a53cc8dcfd7035cb43db943/public/image/head-logo.jpg
--------------------------------------------------------------------------------
/public/image/recording.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csj5588/IM-Chat-Cli/a85544491e2930812a53cc8dcfd7035cb43db943/public/image/recording.gif
--------------------------------------------------------------------------------
/src/components/Test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Test = () => {
4 | return 这是一个测试组件
;
5 | };
6 |
7 | export default Test;
8 |
--------------------------------------------------------------------------------
/src/global.less:
--------------------------------------------------------------------------------
1 | @import "./themes/vars.less";
2 |
3 | @all-font: "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB",
4 | "Microsoft Yahei", "微软雅黑", STHeiti, "华文细黑", sans-serif;
5 | html {
6 | font-size: 3.7vw;
7 | }
8 |
9 | @font-face {
10 | font-family: 'iconfont'; /* project id 1148765 */
11 | src: url('//at.alicdn.com/t/font_1148765_sdvlcmodt0s.eot');
12 | src: url('//at.alicdn.com/t/font_1148765_sdvlcmodt0s.eot?#iefix') format('embedded-opentype'),
13 | url('//at.alicdn.com/t/font_1148765_sdvlcmodt0s.woff2') format('woff2'),
14 | url('//at.alicdn.com/t/font_1148765_sdvlcmodt0s.woff') format('woff'),
15 | url('//at.alicdn.com/t/font_1148765_sdvlcmodt0s.ttf') format('truetype'),
16 | url('//at.alicdn.com/t/font_1148765_sdvlcmodt0s.svg#iconfont') format('svg');
17 | }
18 |
19 | html,
20 | body,
21 | :global(#root) {
22 | height: 100vh;
23 | overflow-scrolling: touch;
24 | -webkit-overflow-scrolling: touch; // 解决ios上下滑动卡顿问题
25 | background-color: @color-background;
26 | }
27 | :global {
28 |
29 | *{
30 | box-sizing: border-box;
31 | }
32 | body {
33 | font: 3.7vw/150% Arial, Verdana, "\5b8b\4f53";
34 | font-family: @all-font;
35 | color: @color-text-base;
36 | background: @color-text-base-inverse;
37 | transition: height 0.3s;
38 | height: 100vh;
39 | }
40 | html {
41 | height: 100vh;
42 | }
43 | html,
44 | body,
45 | ul,
46 | li,
47 | ol,
48 | dl,
49 | dd,
50 | dt,
51 | p,
52 | h1,
53 | h2,
54 | h3,
55 | h4,
56 | h5,
57 | h6,
58 | form,
59 | fieldset,
60 | legend,
61 | img {
62 | margin: 0;
63 | padding: 0;
64 | }
65 | fieldset,
66 | img,
67 | input,
68 | button {
69 | border: none;
70 | padding: 0;
71 | margin: 0;
72 | outline-style: none;
73 | }
74 | // ios系统中元素被触摸时产生的半透明灰色遮罩
75 | a,button,input,textarea{
76 | -webkit-tap-highlight-color: rgba(0,0,0,0);
77 | -webkit-appearance: none;
78 | }
79 | ul,
80 | ol {
81 | list-style: none;
82 | }
83 | input {
84 | padding-top: 0;
85 | padding-bottom: 0;
86 | font-family: @all-font;
87 | }
88 | select,
89 | input {
90 | vertical-align: middle;
91 | }
92 | select,
93 | input,
94 | textarea {
95 | font-size: 3.7vw;
96 | margin: 0;
97 | }
98 | textarea {
99 | resize: none;
100 | }
101 | img {
102 | border: 0;
103 | vertical-align: middle;
104 | }
105 | table {
106 | border-collapse: collapse;
107 | }
108 | .clearfix:before,
109 | .clearfix:after {
110 | content: "";
111 | display: table;
112 | }
113 |
114 | .clearfix:after {
115 | clear: both;
116 | }
117 | .clearfix {
118 | *zoom: 1;
119 | }
120 | a {
121 | text-decoration: none;
122 | }
123 | h1,
124 | h2,
125 | h3,
126 | h4,
127 | h5,
128 | h6 {
129 | text-decoration: none;
130 | font-weight: normal;
131 | font-size: 100%;
132 | }
133 | s,
134 | i,
135 | em,
136 | u {
137 | font-style: normal;
138 | text-decoration: none;
139 | }
140 | .iconfont {
141 | font-family: 'iconfont';
142 | }
143 | .hide {
144 | display: none !important;
145 | }
146 | .unShow {
147 | opacity: 0 !important;
148 | }
149 | .show {
150 | opacity: 1 !important;
151 | }
152 | .active {
153 | border: 1px solid @brand-primary!important;
154 | }
155 | // 组件样式覆盖
156 | .swiper-pagination-bullet-active{
157 | background: #8b9aab;
158 | }
159 | // vconsole兼容问题 勿动
160 | #__vconsole{
161 | user-select: none;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/layouts/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from 'dva';
3 | import VConsole from 'vconsole';
4 | import { Helmet } from "react-helmet";
5 |
6 |
7 | /* 启用svg图标 */
8 | import styles from "./index.less";
9 |
10 | class Layouts extends Component {
11 |
12 | componentDidMount() {
13 | // 打开调试
14 | const vConsole = new VConsole({});
15 | // 检测当前机型
16 | this.testingPhone();
17 | }
18 |
19 | renderChildren = () => {
20 | const { children } = this.props;
21 | return children;
22 | };
23 |
24 | testingPhone = () => {
25 | const { dispatch } = this.props;
26 | const u = navigator.userAgent;
27 | const isAndroid = u.indexOf("Android") > -1 || u.indexOf("Linux") > -1;
28 | const isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); // ios终端
29 | if (isAndroid) {
30 | dispatch({ type: "public/save/phoneModel", payload: "android" });
31 | }
32 | if (isIOS) {
33 | dispatch({ type: "public/save/phoneModel", payload: "iphone" });
34 | }
35 | };
36 |
37 | render() {
38 | const { location } = this.props;
39 | const { state } = location;
40 |
41 | let t = "IM-Chat-Cli";
42 | if (state && state.title) {
43 | t = state.title;
44 | }
45 | return (
46 |
47 |
48 | {t}
49 |
50 | {this.renderChildren()}
51 |
52 | );
53 | }
54 | }
55 |
56 | export default connect(stores => ({
57 | publicModel: stores.public
58 | }))(Layouts);
59 |
--------------------------------------------------------------------------------
/src/layouts/index.less:
--------------------------------------------------------------------------------
1 | @import "~themes/vars.less";
2 |
3 | .root {
4 | position: fixed;
5 | height: 100%;
6 | width: 100%;
7 | top: 0;
8 | .icon {
9 | font-size: 7vw;
10 | color: @color-text-secondary;
11 | }
12 | .selected {
13 | font-size: 3vw;
14 | color: @brand-primary;
15 | }
16 | :global .zteicon {
17 | font-size: 7vw;
18 | }
19 | :global .am-tab-bar-tab-title {
20 | font-size: 3vw;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/models/public.js:
--------------------------------------------------------------------------------
1 | // import { getInfo } from "../services/publicApi";
2 |
3 | export default {
4 | namespace: "public",
5 | state: {
6 | phoneModel: '', // 机型
7 | },
8 | // subscriptions: {
9 | // setup({ dispatch, history }) {
10 | // // ...
11 | // }
12 | // },
13 | effects: {
14 | // *getInfo({ payload }, { call, put }) {},
15 | },
16 | reducers: {
17 | "save/phoneModel": (state, { payload }) => ({
18 | ...state,
19 | phoneModel: payload,
20 | }),
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/pages/document.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Redirect from "umi/redirect";
3 |
4 | const Index = () => ;
5 |
6 | export default Index;
7 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/chat/coms/historyTime/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import dayjs from 'dayjs'
3 | import styles from './index.less';
4 |
5 | class HistoryTime extends React.Component {
6 | render() {
7 | const source = dayjs().format('YYYY年MM月DD日 A hh:mm');
8 | const date = source.replace('AM', '上午').replace('PM', '下午');
9 | return (
10 |
11 |
12 | {date}
13 |
14 |
15 | )
16 | }
17 | }
18 |
19 | export default HistoryTime;
20 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/chat/coms/historyTime/index.less:
--------------------------------------------------------------------------------
1 | .dateLine{
2 | width: 100%;
3 | text-align: center;
4 | padding: 10px 0;
5 | span{
6 | padding: 4px 8px;
7 | background: rgba(0,21,41,0.15);
8 | color: #fff;
9 | border-radius: 99px;
10 | font-size: 12px;
11 | }
12 | }
--------------------------------------------------------------------------------
/src/pages/view/home/coms/chat/coms/sendText/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './index.less';
3 |
4 | class sendText extends React.Component {
5 |
6 | render() {
7 | const { data } = this.props;
8 | return (
9 | data.map((item, index) => {
10 | return (
11 |
12 | {
13 | item.send_user_type === '0' && item.send_format === 'TEXT' ? (
14 |
15 |
16 |
{item.send_content}
17 |
18 |
19 |

23 |
24 |
25 | ) : (
26 |
27 |
28 |

32 |
33 |
34 |
{item.send_content}
35 |
36 |
37 | )
38 | }
39 |
40 | )
41 | })
42 | )
43 | }
44 | }
45 |
46 | export default sendText;
47 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/chat/coms/sendText/index.less:
--------------------------------------------------------------------------------
1 | .chatLine{
2 | // border: 1px solid yellow;
3 | padding: 10px 16px;
4 | .rightSide, .leftSide{
5 | width: 100%;
6 | height: 100%;
7 | display: flex;
8 | .headImg{
9 | width: 40px;
10 | height: 40px;
11 | img{
12 | width: 100%;
13 | height: 100%;
14 | border-radius: 4px;
15 | }
16 | }
17 | .content{
18 | max-width: 65vw;
19 | word-wrap: break-word;
20 | word-break: break-all;
21 | padding: 8.5px 15px;
22 | font-size: 14px;
23 | border: 1px solid rgba(0,21,41,0.15);
24 | border-radius: 4px;
25 | position: relative;
26 | transition: background 0.15s cubic-bezier(0.075, 0.82, 0.165, 1);
27 | user-select: none;
28 | z-index: 2;
29 | &::before{
30 | content: '';
31 | position: absolute;
32 | // bottom: calc(~'50% - 6px');
33 | top: 14px;
34 | display: block;
35 | width: 10px;
36 | height: 10px;
37 | transform: rotate(45deg);
38 | transition: background 0.15s cubic-bezier(0.075, 0.82, 0.165, 1);
39 | z-index: 1;
40 | }
41 | }
42 | }
43 | .rightSide{
44 | justify-content: flex-end;
45 | .content{
46 | margin-right: 10px;
47 | background: #0090FF;
48 | color: #fff;
49 | &::before{
50 | right: -6px;
51 | background: #0090FF;
52 | border-top: 1px solid rgba(0,21,41,0.15);
53 | border-right: 1px solid rgba(0,21,41,0.15);
54 | }
55 | &:active{
56 | background: #8cb6d6;
57 | &::before{
58 | background: #8cb6d6;
59 | }
60 | }
61 | }
62 | }
63 | .leftSide{
64 | justify-content: flex-start;
65 | .content{
66 | background: #fff;
67 | margin-left: 10px;
68 | &::before{
69 | left: -6px;
70 | background: #fff;
71 | border-bottom: 1px solid rgba(0,21,41,0.15);
72 | border-left: 1px solid rgba(0,21,41,0.15);
73 | }
74 | &:active{
75 | background: #f5f5f5;
76 | &::before{
77 | background: #f5f5f5;
78 | }
79 | }
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/src/pages/view/home/coms/chat/coms/sendVoice/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './index.less';
3 |
4 | class sendVoice extends React.Component {
5 | state = {
6 | isPlaying: false,
7 | };
8 |
9 | // 播放语音
10 | playVoice = (voiceId, file_url) => {
11 | // 节流阀
12 | const { isPlaying } = this.state;
13 | if (isPlaying) {
14 | return false;
15 | }
16 | // 播放语音
17 | }
18 |
19 | // 动态计算语音宽度
20 | calcLength = item => {
21 | const vLength = parseInt(item, 10);
22 | let contentLength = 'normal';
23 | if (vLength > 4) {
24 | contentLength = (vLength - 4) * 2 + 19;
25 | }
26 | return contentLength;
27 | }
28 |
29 | render() {
30 | const { isPlaying } = this.state;
31 | const { data } = this.props;
32 | return (
33 | data.map((item, index) => {
34 | const contentLength = this.calcLength(item.voiceLength);
35 | return (
36 |
37 | {
38 | item.send_user_type === '0' && item.send_format === 'VOICE' ? (
39 |
40 |
this.playVoice(item.voiceid, item.file_url)}
44 | >
45 | {item.voiceLength || 1}"
46 |
47 |
48 |
49 |

53 |
54 |
55 | ) : (
56 |
57 |
58 |

62 |
63 |
this.playVoice(item.voiceid)}
67 | >
68 |
69 | {item.voiceLength || 1}"
70 |
71 |
72 | )
73 | }
74 |
75 | )
76 | })
77 | )
78 | }
79 | }
80 |
81 | export default sendVoice;
82 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/chat/coms/sendVoice/index.less:
--------------------------------------------------------------------------------
1 | .chatLine{
2 | // border: 1px solid yellow;
3 | padding: 10px 16px;
4 | .rightSide, .leftSide{
5 | width: 100%;
6 | height: 100%;
7 | display: flex;
8 | .headImg{
9 | width: 40px;
10 | height: 40px;
11 | img{
12 | width: 100%;
13 | height: 100%;
14 | border-radius: 4px;
15 | }
16 | }
17 | .content{
18 | min-width: 19vw;
19 | max-width: 47vw;
20 | padding: 7.5px 10px;
21 | font-size: 14px;
22 | border: 1px solid rgba(0,21,41,0.15);
23 | border-radius: 4px;
24 | position: relative;
25 | transition: background 0.15s cubic-bezier(0.075, 0.82, 0.165, 1);
26 | user-select: none;
27 | z-index: 2;
28 | display: flex;
29 | align-items: center;
30 | &::after{
31 | content: '';
32 | position: absolute;
33 | bottom: calc(~'50% - 6px');
34 | display: block;
35 | width: 10px;
36 | height: 10px;
37 | transform: rotate(45deg);
38 | transition: background 0.15s cubic-bezier(0.075, 0.82, 0.165, 1);
39 | z-index: 1;
40 | }
41 | &.playing{
42 | animation: trigger 2s infinite;
43 | }
44 | i{
45 | font-size: 20px;
46 | }
47 | span{
48 | position: absolute;
49 | top: 8px;
50 | font-size: 12px;
51 | padding: 0 5px;
52 | color: #af9595;
53 | }
54 | }
55 | }
56 | .rightSide{
57 | justify-content: flex-end;
58 | text-align: right;
59 | .content{
60 | justify-content: flex-end;
61 | margin-right: 10px;
62 | background: #0090FF;
63 | &::after{
64 | right: -6px;
65 | background: #0090FF;
66 | border-top: 1px solid rgba(0,21,41,0.15);
67 | border-right: 1px solid rgba(0,21,41,0.15);
68 | }
69 | &:active{
70 | background: #8cb6d6;
71 | &::after{
72 | background: #8cb6d6;
73 | }
74 | }
75 | .icon{
76 | transform: rotate(180deg);
77 | color: #fff;
78 | }
79 | span{
80 | left: -35px;
81 | }
82 | }
83 | }
84 | .leftSide{
85 | justify-content: flex-start;
86 | text-align: left;
87 | .content{
88 | background: #fff;
89 | margin-left: 10px;
90 | justify-content: flex-start;
91 | color: #0090FF;
92 | &::after{
93 | left: -6px;
94 | background: #fff;
95 | border-bottom: 1px solid rgba(0,21,41,0.15);
96 | border-left: 1px solid rgba(0,21,41,0.15);
97 | }
98 | &:active{
99 | background: #f5f5f5;
100 | &::after{
101 | background: #f5f5f5;
102 | }
103 | }
104 | span{
105 | right: -35px;
106 | }
107 | }
108 | }
109 | }
110 |
111 | @keyframes trigger {
112 | 0% { opacity: 1 }
113 | 50% { opacity: 0.5 }
114 | 100% { opacity: 1 }
115 | }
--------------------------------------------------------------------------------
/src/pages/view/home/coms/chat/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'dva';
3 |
4 | import HistoryTime from './coms/historyTime';
5 | import SendText from './coms/sendText';
6 | import SendVoice from './coms/sendVoice';
7 |
8 | import styles from './index.less';
9 |
10 | class Chat extends React.Component {
11 |
12 | state = {};
13 |
14 | componentDidMount() {
15 | this.showView();
16 | }
17 |
18 | closeOperateMore = () => {
19 | const { keyBoardDown, homeModel } = this.props;
20 | const { upHeight } = homeModel;
21 | if (upHeight !== 0) {
22 | keyBoardDown();
23 | const text = document.querySelector("#text");
24 | text.blur();
25 | }
26 | }
27 |
28 | showView = () => {
29 | const { showView } = this.props;
30 | setTimeout(() => {
31 | showView();
32 | }, 200);
33 | }
34 |
35 | render() {
36 | const { homeModel, dispatch, showView } = this.props;
37 | const {
38 | operateMoreIO,
39 | upHeight,
40 | normalHeight,
41 | chatMsgList,
42 | } = homeModel;
43 | return (
44 |
54 |
58 |
59 | {
60 | chatMsgList.map((item, index) => {
61 | if (item.send_format === 'TEXT') {
62 | return (
63 |
68 | )
69 | } else if (item.send_format === 'VOICE') {
70 | return (
71 |
76 | )
77 | }
78 | })
79 | }
80 |
{}
81 |
82 |
83 | )
84 | }
85 | }
86 |
87 | export default connect(({ home }) => ({
88 | homeModel: home,
89 | }))(Chat);
90 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/chat/index.less:
--------------------------------------------------------------------------------
1 | .chat{
2 | width: 100%;
3 | transition: height 0.2s;
4 | background: #f4f5fa;
5 | overflow-y: scroll;
6 | .chatBox{
7 | width: 100%;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operate/coms/coms/speak.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'dva';
3 | import styles from './speak.less';
4 |
5 | class Speak extends React.Component {
6 |
7 | state = {
8 | isActive: false,
9 | isCancel: false,
10 | descript: '按住说话',
11 | currentRecordTimer: 0, // 当前录入语音时长
12 | pointY: 0, // 初始手指Y坐标
13 | };
14 |
15 | onTouchStart = e => {
16 | const pointY = e.touches[0].pageY;
17 | // 改变状态
18 | this.setState({
19 | isActive: true,
20 | descript: '松开结束',
21 | pointY,
22 | });
23 | // 计算时间
24 | this.setState({
25 | currentRecordTimer: new Date().getTime()
26 | });
27 |
28 | // 开始录音sdk
29 | }
30 |
31 | onTouchMove = e => {
32 | const { pointY, isCancel } = this.state;
33 | const moveY = e.touches[0].pageY;
34 | if (pointY - moveY > 150 && !isCancel) {
35 | this.setState({ isCancel: true });
36 | }
37 | if (pointY - moveY < 150 && isCancel) {
38 | this.setState({ isCancel: false });
39 | }
40 | }
41 |
42 | onTouchEnd = e => {
43 | const { currentRecordTimer, isCancel } = this.state;
44 | // 重置状态
45 | this.setState({
46 | isActive: false,
47 | isCancel: false,
48 | descript: '按住说话',
49 | });
50 | // 取消驳回
51 | if (isCancel) {
52 | // 停止录音
53 | return false;
54 | }
55 | // 计算时长
56 | const endRecordTimer = new Date().getTime();
57 | const voiceLength = Math.ceil((endRecordTimer - currentRecordTimer) / 1000);
58 |
59 | // 应改为调sdk 请自行更改
60 | this.sendVoice({ localId: '001', voiceLength });
61 | }
62 |
63 | // 上传语音
64 | sendVoice = res => {
65 | const { homeModel, dispatch, showView } = this.props;
66 | const { chatMsgList } = homeModel;
67 |
68 | const uuid = chatMsgList.length;
69 | const localId = res.localId;
70 | const voiceLength = res.voiceLength;
71 |
72 | // 整理列表-自定义规则
73 | chatMsgList.push({
74 | send_format: 'VOICE',
75 | voiceid: localId,
76 | voiceLength,
77 | send_time: (new Date()).getTime(),
78 | send_user_type : '0',
79 | // send_user_type : Math.round(Math.random()),
80 | uuid: uuid,
81 | isNew: '1'
82 | });
83 | dispatch({ type: 'home/save/chatMsgList', payload: chatMsgList });
84 | // 可视区域调整
85 | showView();
86 |
87 | // 上传至服务端
88 | }
89 |
90 | render() {
91 | const { isActive, descript, isCancel } = this.state;
92 | return (
93 |
99 |
{descript}
100 |
103 |

104 |
手指上滑, 取消发送
105 |
106 |
112 |
113 |
松开手指, 取消发送
114 |
115 |
116 | )
117 | }
118 | }
119 |
120 | export default connect(({ home }) => ({
121 | homeModel: home
122 | }))(Speak);
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operate/coms/coms/speak.less:
--------------------------------------------------------------------------------
1 | :global{
2 | *{
3 | }
4 | }
5 | .sendVoiceBox{
6 | width: 100%;
7 | height: 32px;
8 | border: 1px solid rgba(220,220,222,1);
9 | border-radius: 4px;
10 | background-color: #ffffff;
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | user-select: none;
15 | &.active{
16 | background: rgb(222,222,222);
17 | }
18 | .sendVoiceModal{
19 | position: fixed;
20 | width: 40vw;
21 | height: 40vw;
22 | top: calc(~'40vh - 20vw');
23 | left: 30vw;
24 | z-index: 99;
25 | background-color: rgba(0,21,41,0.85);;
26 | border-radius: 8px;
27 | display: none;
28 | justify-content: center;
29 | align-items: center;
30 | flex-direction: column;
31 | user-select: none;
32 | &.show{
33 | display: flex;
34 | }
35 | &.cancelShow{
36 | display: flex;
37 | background-color: rgba(255,84,42,0.85);
38 | i{
39 | font-size: 18vw;
40 | padding: 15px;
41 | }
42 | }
43 | img{
44 | width: 17vw;
45 | padding-bottom: 3vw;
46 | padding-top: 4vw;
47 | }
48 | i{
49 | font-size: 25vw;
50 | color: #fff;
51 | }
52 | p{
53 | font-size: 3vw;
54 | color: #fff;
55 | padding-top: 2vw;
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operate/coms/more.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'dva';
3 | import styles from './more.less';
4 |
5 | class More extends React.Component {
6 |
7 | sendText = () => {
8 | const { dispatch } = this.props;
9 | const onFocus = () => {
10 | const inputDom = document.getElementById('text');
11 | inputDom.focus();
12 | }
13 | // 1、发送信息
14 | dispatch({ type: 'home/sendText' });
15 | this.showView();
16 | // 2、保持焦点
17 | onFocus();
18 | // 3、假回应
19 | setTimeout(() => {
20 | dispatch({ type: 'home/save/sendText', payload: '终于有人回复了。' });
21 | dispatch({ type: 'home/sendText', person: 'other' });
22 | this.showView();
23 | }, 1000);
24 | }
25 |
26 | showOperateMore = () => {
27 | const { dispatch, onBlur, homeModel, publicModel } = this.props;
28 | const { operateMoreIO, upHeight, normalHeight } = homeModel;
29 | const { phoneModel } = publicModel;
30 |
31 | const onFocus = () => {
32 | const dom = document.querySelector('#text');
33 | dom.focus();
34 | }
35 |
36 | // 1、trigger键盘 - 核心 - 加入ios变化
37 | if (!operateMoreIO) {
38 | dispatch({ type: 'home/change/operateMoreIO', payload: true });
39 | if (upHeight === 0) {
40 | // 初始化适配原理
41 | const storageHeight = window.localStorage.getItem('upHeight');
42 | if (storageHeight !== '0') {
43 | dispatch({ type: 'home/change/upHeight', payload: storageHeight });
44 | } else {
45 | const defalutHeight = normalHeight * 0.58;
46 | dispatch({ type: 'home/change/upHeight', payload: defalutHeight });
47 | }
48 | // 内容进入视野
49 | this.showView();
50 | dispatch({ type: 'home/change/sendType', payload: 'text' });
51 | }
52 | } else {
53 | dispatch({ type: 'home/change/operateMoreIO', payload: false });
54 | if (phoneModel === "android") {
55 | onFocus();
56 | }
57 | }
58 | }
59 |
60 | showView = () => {
61 | const { showView } = this.props;
62 | // 等待动画响应
63 | setTimeout(() => {
64 | showView();
65 | }, 200);
66 | }
67 |
68 | render() {
69 |
70 | const { homeModel } = this.props;
71 | const { sendText, operateMoreIO } = homeModel;
72 |
73 | return (
74 |
75 |
80 |
81 | {
82 | !sendText ? (
83 |
87 |
88 |
89 | ) : (
90 |
91 |
94 | 发送
95 |
96 |
97 | )
98 | }
99 |
100 |
101 | )
102 | }
103 | }
104 |
105 | export default connect(stores => ({
106 | homeModel: stores.home,
107 | publicModel: stores.public
108 | }))(More);
109 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operate/coms/more.less:
--------------------------------------------------------------------------------
1 | .more{
2 | width: 26.6%;
3 | height: 100%;
4 | display: flex;
5 | .icon{
6 | width: 28px;
7 | height: 28px;
8 | border: 1px solid rgba(220,220,222,1);
9 | border-radius: 50%;
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | transition: all 0.2s;
14 | &:nth-of-type(2) {
15 | animation: sendIn 0.3s;
16 | }
17 | &:active{
18 | background: #f5f5f5;
19 | }
20 | &.rotate{
21 | transform: rotate(45deg);
22 | }
23 | i{
24 | font-size: 18px;
25 | color: rgba(0,21,41,0.65);
26 | }
27 | }
28 | .half{
29 | width: 50%;
30 | height: 100%;
31 | display: flex;
32 | justify-content: center;
33 | align-items: center;
34 | &:nth-of-type(2) {
35 | width: 50%;
36 | }
37 | .send{
38 | width: 100%;
39 | height: 100%;
40 | padding: 8px 8px 8px 0;
41 | span{
42 | display: inline-block;
43 | width: 100%;
44 | height: 100%;
45 | border-radius: 4px;
46 | background-color: #37d72d;
47 | color: #fff;
48 | display: flex;
49 | align-items: center;
50 | justify-content: center;
51 | transition: all 0.3s;
52 | animation: sendIn 0.3s;
53 | &:active{
54 | background-color: #49bb42;
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 | @keyframes sendIn {
62 | 0% {opacity: 0; transform: scale(0.8)}
63 | 100% {opacity: 1; transform: scale(1)}
64 | }
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operate/coms/text.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'dva';
3 | import { getIphoneUpHeight, saveStorage } from 'utils';
4 | import Speak from './coms/speak';
5 | import styles from './text.less';
6 |
7 | class Text extends React.Component {
8 |
9 | componentDidMount() {
10 | }
11 |
12 | sendText = e => {
13 | const { dispatch } = this.props;
14 | dispatch({ type: 'home/save/sendText', payload: e.target.value });
15 | }
16 |
17 | onFocus = () => {
18 | const { dispatch, publicModel, homeModel, showView } = this.props;
19 | const { phoneModel } = publicModel;
20 | const { normalHeight, operateMoreIO, upHeight } = homeModel;
21 | dispatch({ type: "home/change/operateMoreIO", payload: false });
22 | // 处理ios焦点兼容性问题
23 | if (phoneModel === "iphone") {
24 | const objs = getIphoneUpHeight();
25 | setTimeout(() => {
26 | window.scrollTo(0, 0);
27 | }, objs.time);
28 | setTimeout(() => {
29 | showView();
30 | }, objs.view);
31 | const nextUpHeight = normalHeight - objs.upHeight;
32 | dispatch({ type: "home/change/upHeight", payload: nextUpHeight });
33 | // 存储
34 | saveStorage("upHeight", nextUpHeight);
35 | }
36 | };
37 |
38 | render() {
39 |
40 | const { homeModel, showView } = this.props;
41 | const { sendText, sendType } = homeModel;
42 |
43 | return (
44 |
45 | {
46 | sendType === 'text' ? (
47 |
55 | ) : (
56 |
59 | )
60 | }
61 |
62 | )
63 | }
64 | }
65 |
66 | export default connect(stores => ({
67 | homeModel: stores.home,
68 | publicModel: stores.public
69 | }))(Text);
70 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operate/coms/text.less:
--------------------------------------------------------------------------------
1 | .text{
2 | width: 57.3%;
3 | height: 100%;
4 | display: flex;
5 | align-items: center;
6 | input{
7 | width: 100%;
8 | height: 32px;
9 | border: 1px solid rgba(220,220,222,1);
10 | border-radius: 4px;
11 | background-color: #ffffff;
12 | font-size: 14px;
13 | padding-left: 4px;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operate/coms/voice.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'dva';
3 | import styles from './voice.less';
4 |
5 | class Voice extends React.Component {
6 |
7 | voiceIcon = () => {
8 | // 切换为语音输入模式
9 | const { dispatch, homeModel } = this.props;
10 | const { sendType } = homeModel;
11 | if (sendType === 'text') {
12 | // trigger 布局
13 | dispatch({ type: 'home/change/sendType', payload: 'voice' });
14 | dispatch({ type: 'home/change/operateMoreIO', payload: false });
15 | dispatch({ type: 'home/change/upHeight', payload: 0 });
16 | } else {
17 | dispatch({ type: 'home/change/sendType', payload: 'text' });
18 | }
19 | }
20 |
21 | render() {
22 |
23 | const { homeModel } = this.props;
24 | const { sendType } = homeModel;
25 |
26 | return (
27 |
28 |
32 | {
33 | sendType === 'text' ? (
34 |
35 | ) : (
36 |
37 | )
38 | }
39 |
40 |
41 | )
42 | }
43 | }
44 |
45 | export default connect(({ home }) => ({
46 | homeModel: home
47 | }))(Voice);
48 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operate/coms/voice.less:
--------------------------------------------------------------------------------
1 | .voice{
2 | width: 16%;
3 | height: 100%;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | .icon{
8 | width: 28px;
9 | height: 28px;
10 | border: 1px solid rgba(220,220,222,1);
11 | border-radius: 50%;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | i{
16 | color: rgba(0,21,41,0.65);
17 | font-size: 18px;
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operate/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'dva';
3 |
4 | import Text from './coms/text';
5 | import More from './coms/more';
6 | import Voice from './coms/voice';
7 | import OperateMore from '../operateMore/index';
8 |
9 | import styles from './index.less';
10 |
11 | class Operate extends React.Component {
12 |
13 | onBlur = () => {
14 | const dom = document.querySelector('#text');
15 | dom.blur();
16 | }
17 |
18 | render() {
19 | const { homeModel, showView } = this.props;
20 | const { operateMoreIO } = homeModel;
21 | return (
22 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 | }
32 |
33 | export default connect(({ home }) => ({
34 | homeModel: home
35 | }))(Operate);
36 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operate/index.less:
--------------------------------------------------------------------------------
1 | .operate{
2 | // position: absolute;
3 | // bottom: 0;
4 | width: 100%;
5 | height: 48px;
6 | display: flex;
7 | flex-wrap: wrap;
8 | transition: all 0 1s;
9 | border-top: 1px solid rgba(216,215,220,1);
10 | border-bottom: 1px solid rgba(216,215,220,1);
11 | user-select: none;
12 | }
13 | // .fixed{
14 | // position: absolute;
15 | // bottom: 0;
16 | // }
17 | // .fixed42{
18 | // position: fixed;
19 | // bottom: 42vh;
20 | // }
21 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operateMore/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'dva';
3 | import Swiper from 'swiper/dist/js/swiper.js'
4 | import 'swiper/dist/css/swiper.min.css'
5 |
6 | import styles from './index.less';
7 |
8 | class OperateMore extends React.Component {
9 |
10 | componentDidMount() {
11 | new Swiper('.swiper-container', {
12 | loop: false,
13 | pagination: {
14 | el: '.swiper-pagination',
15 | clickable: true, // 允许点击跳转
16 | },
17 | });
18 | }
19 |
20 | render() {
21 | const { homeModel } = this.props;
22 | const { normalHeight, upHeight } = homeModel;
23 | return (
24 |
28 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
39 |
照片
40 |
41 |
42 |
46 |
47 |
48 |
拍摄
49 |
50 |
51 |
52 |
53 |
54 |
语音通话
55 |
56 |
57 |
58 |
59 |
60 |
位置
61 |
62 |
63 |
64 |
65 |
66 |
投诉与意见
67 |
68 |
69 |
70 |
71 |
72 |
73 |
77 |
78 |
79 |
照片
80 |
81 |
82 |
86 |
87 |
88 |
拍摄
89 |
90 |
91 |
92 |
93 |
94 |
语音通话
95 |
96 |
97 |
98 |
99 |
100 |
位置
101 |
102 |
103 |
104 |
105 |
106 |
投诉与意见
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | )
115 | }
116 | }
117 |
118 | export default connect(({ home }) => ({
119 | homeModel: home
120 | }))(OperateMore);
121 |
--------------------------------------------------------------------------------
/src/pages/view/home/coms/operateMore/index.less:
--------------------------------------------------------------------------------
1 | .operateMore{
2 | width: 100%;
3 | background: #FAFCFD;
4 | user-select: none;
5 | .smallBlock{
6 | width: 100%;
7 | height: 90%;
8 | display: flex;
9 | justify-content: flex-start;
10 | align-items: center;
11 | flex-wrap: wrap;
12 | padding: 0 8vw 6vw 8vw;
13 | }
14 | }
15 |
16 | .mark{
17 | width:25%;
18 | text-align:center;
19 | display: flex;
20 | align-items: center;
21 | flex-direction: column;
22 | .icon{
23 | width: 75%;
24 | height: 16vw;
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | background: #ffffff;
29 | border: 1px solid rgba(205,212,221,1);
30 | border-radius: 8px;
31 | transition: background 0.15s;
32 | &:active{
33 | background: #f5f5f5;
34 | }
35 | i{
36 | font-size: 6vw;
37 | color: rgba(0,21,41,0.55);
38 | }
39 | }
40 | p{
41 | width: 100%;
42 | padding-top: 4px;
43 | font-size: 12px;
44 | color: rgba(0,21,41,0.55);
45 | }
46 | }
47 | .hide{
48 | display: none;
49 | }
--------------------------------------------------------------------------------
/src/pages/view/home/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "dva";
3 | import { saveStorage, clearStorage } from 'utils';
4 |
5 | import Operate from './coms/operate';
6 | import OperateMore from './coms/operateMore';
7 | import Chat from './coms/chat';
8 |
9 | import styles from "./index.less";
10 |
11 | @connect(stores => ({ homeModel: stores.home }))
12 | class Home extends Component {
13 |
14 | componentDidMount() {
15 | // 聊天布局
16 | this.init();
17 | }
18 |
19 | componentWillUnmount() {
20 | const { dispatch } = this.props;
21 | dispatch({ type: "home/clear" });
22 | // 清除localstorage
23 | clearStorage();
24 | }
25 |
26 | init = () => {
27 | const { dispatch } = this.props;
28 | // 获取屏幕高度
29 | const normalHeight = document.querySelector('html').clientHeight;
30 |
31 | dispatch({ type: 'home/change/normalHeight', payload: normalHeight });
32 |
33 | // 获取resize高度
34 | let count = 0;
35 | window.onresize = (e) => {
36 | count++;
37 | const { homeModel } = this.props;
38 | const { normalHeight, operateMoreIO } = homeModel;
39 | const nextUpHeight = document.querySelector('html').clientHeight;
40 |
41 | setTimeout(() => {
42 | this.showView();
43 | }, 300);
44 |
45 | if (nextUpHeight < normalHeight && count % 2 === 1) {
46 | // 打开
47 | dispatch({ type: 'home/change/upHeight', payload: nextUpHeight });
48 | // 存储
49 | saveStorage(nextUpHeight);
50 | } else if (nextUpHeight === normalHeight && !operateMoreIO) {
51 | // 收起
52 | dispatch({ type: 'home/change/upHeight', payload: 0 });
53 | }
54 | }
55 | }
56 |
57 | // 键盘关闭 chat拉伸
58 | keyBoardDown = () => {
59 | const { dispatch } = this.props;
60 | // 高度收缩
61 | dispatch({ type: 'home/change/upHeight', payload: 0 });
62 | // 变量重置
63 | dispatch({ type: 'home/change/operateMoreIO', payload: false });
64 | }
65 |
66 | // 底边显示内容
67 | showView = () => {
68 | const view = document.querySelector('#view');
69 | if (view) {
70 | view.scrollIntoView({
71 | behavior: "smooth",
72 | block: "start",
73 | inline: "start",
74 | });
75 | }
76 | }
77 |
78 | render() {
79 | const { homeModel } = this.props;
80 | const { normalHeight, upHeight } = homeModel;
81 | return (
82 |
86 |
90 |
93 |
96 |
97 | );
98 | }
99 | }
100 |
101 | export default Home
102 |
--------------------------------------------------------------------------------
/src/pages/view/home/index.less:
--------------------------------------------------------------------------------
1 | @import "~themes/vars.less";
2 |
3 | .root {
4 | background-color: #fff;
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/view/home/models/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | namespace: "home",
3 | state: {
4 | title: "...",
5 | chatMsgList: [], // 用户聊天队列
6 | sendText: '', // 实时文字存储
7 | sendType: 'text', // 输入模式 text 文本 voice 语音
8 | normalHeight: 0, // 页面原始高度
9 | upHeight: 0, // 键盘弹出高度
10 | operateMoreIO: false, // 是否显示更多操作开关
11 | },
12 | effects: {
13 | *sendText({ person }, { call, put, select }) {
14 | const state = yield select(x => x.home);
15 | const { chatMsgList, sendText } = state;
16 | const uuid = chatMsgList.length;
17 | // 约定文字类型字段对象
18 | const textAttr = {
19 | send_format : 'TEXT',
20 | send_content : sendText,
21 | send_time : (new Date()).getTime(),
22 | send_user_type : person ? '1' : '0',
23 | uuid: uuid,
24 | };
25 | chatMsgList.push(textAttr);
26 | yield put({ type: 'save/chatMsgList', payload: chatMsgList });
27 | // 置空当前信息
28 | yield put({ type: 'save/sendText', payload: '' });
29 | },
30 | },
31 | reducers: {
32 | "save/title": (state, { payload: title }) => ({
33 | ...state,
34 | title,
35 | }),
36 | "save/chatMsgList": (state, { payload }) => ({
37 | ...state,
38 | chatMsgList: payload,
39 | }),
40 | "save/sendText": (state, { payload }) => ({
41 | ...state,
42 | sendText: payload,
43 | }),
44 | "change/normalHeight": (state, { payload }) => ({
45 | ...state,
46 | normalHeight: payload,
47 | }),
48 | "change/upHeight": (state, { payload }) => ({
49 | ...state,
50 | upHeight: payload,
51 | }),
52 | "change/sendType": (state, { payload }) => ({
53 | ...state,
54 | sendType: payload,
55 | }),
56 | "change/operateMoreIO": (state, { payload }) => ({
57 | ...state,
58 | operateMoreIO: payload,
59 | }),
60 | clear: () => ({ title: "" }),
61 | },
62 | };
63 |
--------------------------------------------------------------------------------
/src/pages/view/home/services/index.js:
--------------------------------------------------------------------------------
1 | import { post } from "utils/request";
2 |
3 | export const getHistory = data => post("/mock/getHistory", data);
4 |
--------------------------------------------------------------------------------
/src/services/publicApi.js:
--------------------------------------------------------------------------------
1 | import { post } from "utils/request";
2 |
3 | export const getInfo = data => post("/xxx", data);
--------------------------------------------------------------------------------
/src/themes/vars.less:
--------------------------------------------------------------------------------
1 | @import "../../node_modules/antd-mobile/lib/style/themes/default.less";
2 |
3 | // 定义网站样式变量
4 |
5 | // new
6 |
7 | // 字号
8 | @font-size-caption-sm: 1.2rem; // 说明 描述
9 | @font-size-base: 1.4rem; // 主字体
10 | @font-size-subhead: 1.6rem; // 副标题
11 | @font-size-caption: 2rem; // 标题
12 | @font-size-heading: 2.2rem; // 大标题
13 |
14 | // 圆角
15 | @radius-sm: 0.2rem;
16 | @radius-md: 0.4rem;
17 |
18 | // 间距
19 | @space-sm: 0.4rem;
20 | @space-md: 0.8rem;
21 | @space-lg: 1.2rem;
22 |
23 | // 字色
24 | @font-color-title: rgba(0, 0, 0, 0.85); // 标题
25 | @font-color-base: rgba(0, 0, 0, 0.65); // 正文
26 | @font-color-caption: rgba(0, 0, 0, 0.45); // 描述
27 | @font-color-disable: rgba(0, 0, 0, 0.25); // 失效
28 |
29 | @color-primary: #0081ff;
30 | @color-error: #ff3a00;
31 | @color-success: #0fb745;
32 | @color-warning: #f5a623;
33 |
34 | @color-background-input: #e2e9f3; // 输入框底色
35 | @color-border: #eeeeee; // 边框
36 | @color-background: #f2f4f7; // 页面底色
37 | @color-background-model: #f3f9ff; // 模块底色
38 |
--------------------------------------------------------------------------------
/src/utils/fetch.js:
--------------------------------------------------------------------------------
1 | import fetch from "isomorphic-fetch";
2 |
3 | const codeMessage = {
4 | 200: "服务器成功返回请求的数据。",
5 | 201: "新建或修改数据成功。",
6 | 202: "一个请求已经进入后台排队(异步任务)。",
7 | 204: "删除数据成功。",
8 | 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
9 | 401: "用户没有权限(令牌、用户名、密码错误)。",
10 | 403: "用户得到授权,但是访问是被禁止的。",
11 | 404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
12 | 406: "请求的格式不可得。",
13 | 410: "请求的资源被永久删除,且不会再得到的。",
14 | 422: "当创建一个对象时,发生一个验证错误。",
15 | 500: "服务器发生错误,请检查服务器。",
16 | 502: "网关错误。",
17 | 503: "服务不可用,服务器暂时过载或维护。",
18 | 504: "网关超时。",
19 | };
20 |
21 | function checkStatus(response) {
22 | if (response.status >= 200 && response.status < 300) {
23 | return response;
24 | }
25 | const errortext = codeMessage[response.status] || response.statusText;
26 | const error = new Error(response.statusText);
27 | error.status = response.status;
28 | error.errCode = response.errCode!=null?response.errCode:response.status;
29 | error.errortext = errortext;
30 | throw error;
31 | }
32 |
33 | function parseJSON(response) {
34 | return response.json();
35 | }
36 |
37 | export const request = (url, options) => {
38 | const newOptions = {
39 | // credentials: "include",
40 | credentials: "same-origin",
41 | mode: "cors",
42 | ...options,
43 | headers: {
44 | "x-requested-with": "XMLHttpRequest",
45 | "Access-Control-Allow-Origin": "*",
46 | "Content-Type": "application/json",
47 | Accept: "application/json",
48 | ...(options.headers || {}),
49 | },
50 | };
51 | return fetch(url, newOptions)
52 | .then(checkStatus)
53 | .then(parseJSON);
54 | };
55 |
56 | function getError(option, xhr) {
57 | const msg = `cannot post ${option.action} ${xhr.status}'`;
58 | const err = new Error(msg);
59 | err.status = xhr.status;
60 | err.method = "post";
61 | err.url = option.action;
62 | return err;
63 | }
64 |
65 | function getBody(xhr) {
66 | const text = xhr.responseText || xhr.response;
67 | if (!text) {
68 | return text;
69 | }
70 |
71 | try {
72 | return JSON.parse(text);
73 | } catch (e) {
74 | return text;
75 | }
76 | }
77 |
78 | /**
79 | * 发送文件的请求
80 | * action: 目标地址
81 | * headers: 请求头信息
82 | * withCredentials: 是否需要认证,布尔值
83 | * onProgress: 监听onProgress的回调。
84 | * onError: 监听出错的回调。
85 | * onSuccess: 监听成功的回调。
86 | * data: 传递需要POST的数据。
87 | * file: 传递要上传的文件对象。
88 | * @param {Object}
89 | */
90 | export const uploadFile = option => {
91 | const xhr = new XMLHttpRequest();
92 |
93 | if (option.onProgress && xhr.upload) {
94 | xhr.upload.onprogress = function progress(e) {
95 | if (e.total > 0) {
96 | // eslint-disable-next-line no-param-reassign
97 | e.percent = (e.loaded / e.total) * 100;
98 | }
99 | option.onProgress(e);
100 | };
101 | }
102 | const formData = new FormData();
103 | if (option.data) {
104 | Object.keys(option.data).forEach(key => {
105 | formData.append(key, option.data[key]);
106 | });
107 | }
108 | formData.append("file", option.file);
109 | xhr.onerror = function error(e) {
110 | option.onError(e);
111 | };
112 | xhr.onload = function onload() {
113 | if (xhr.status < 200 || xhr.status >= 300) {
114 | return option.onError(getError(option, xhr), getBody(xhr));
115 | }
116 | option.onSuccess(getBody(xhr), xhr);
117 | };
118 | xhr.open("post", option.action, true);
119 | if (option.withCredentials && "withCredentials" in xhr) {
120 | xhr.withCredentials = true;
121 | }
122 | const headers = option.headers || {};
123 | if (headers["X-Requested-With"] !== null) {
124 | xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
125 | }
126 | Object.keys(headers).forEach(h => {
127 | xhr.setRequestHeader(h, headers[h]);
128 | });
129 | xhr.send(formData);
130 | return {
131 | abort() {
132 | xhr.abort();
133 | },
134 | };
135 | };
136 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 |
2 | const getIphoneUpHeight = () => {
3 | const screenHeight = parseInt(window.screen.height, 10);
4 | let upHeight = 0;
5 | let time = 0;
6 | let view = 0;
7 | if (screenHeight === 812) {
8 | // iphoneX
9 | upHeight = 343;
10 | } else if (screenHeight === 568) {
11 | // iphoneSE
12 | upHeight = 301;
13 | time = 250;
14 | view = 300;
15 | } else if (screenHeight === 667) {
16 | // iphone8
17 | upHeight = 300;
18 | time = 100;
19 | view = 500;
20 | } else if (screenHeight === 736) {
21 | // iphone8Plus
22 | upHeight = 313;
23 | }
24 | return { time, view, upHeight };
25 | };
26 |
27 | const saveStorage = (name, value) => {
28 | const storage = window.localStorage;
29 | if (storage && storage.getItem(name) === '0') {
30 | storage.setItem(name, value);
31 | }
32 | };
33 |
34 | const clearStorage = value => {
35 | const storage = window.localStorage;
36 | if (storage) {
37 | storage.setItem('upHeight', 0);
38 | }
39 | };
40 |
41 | export {
42 | getIphoneUpHeight,
43 | saveStorage,
44 | clearStorage
45 | }
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import { request } from './fetch';
2 | import { stringify } from "query-string";
3 |
4 | const get = (url, data, more = {}) =>
5 | request(
6 | `${url}?${stringify(data)}`,
7 | {
8 | method: "GET",
9 | },
10 | more
11 | );
12 |
13 | const post = (url, data, more = {}) =>
14 | request(
15 | url,
16 | {
17 | method: "POST",
18 | body: JSON.stringify(data),
19 | },
20 | more
21 | );
22 |
23 | export { get, post };
--------------------------------------------------------------------------------