├── .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 | ![](https://user-gold-cdn.xitu.io/2019/4/19/16a314d86c9c0256?w=764&h=925&f=png&s=37436) 56 | 57 | ## 效果一览(gif较大) 58 | 59 | | 名称 | 示意图 | 60 | | ---------- | ----------------------------------------- | 61 | | 文字布局 | ![文字布局](https://s1.ax1x.com/2020/09/21/wHuijS.gif)| 62 | | 键盘布局 | ![键盘布局](https://s1.ax1x.com/2020/09/21/wH8udS.gif)| 63 | | 语音效果 | ![语音效果](https://s1.ax1x.com/2020/09/21/wH8VMt.gif)| 64 | | 操作效果 | ![操作效果](https://s1.ax1x.com/2020/09/21/wH8kRA.gif)| 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 | [![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=csj5588)](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 |
76 |
77 | 78 |
79 |
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 }; --------------------------------------------------------------------------------