├── .env ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── config ├── htmlAfterWebpackPlugin.js ├── webpack.development.js └── webpack.production.js ├── developement.md ├── dist ├── .env ├── app.js ├── assets │ ├── favicon.ico │ ├── images │ │ ├── 02d9a1cfacacfd02-c33ab.min.png │ │ ├── 2fb5c91b6d9c4f02-3548d.min.png │ │ ├── 303b0c915e984e02-41211.min.png │ │ ├── 33bfeb065105c002-efcf5.min.png │ │ ├── 4cf2f7c2e63f4a02-991ec.min.png │ │ ├── 4d709e27fa18aa02-f0e20.min.png │ │ ├── 536fa0d98d69d102-55138.min.png │ │ ├── 6ea30b7cd9472a02-a2eb4.min.png │ │ ├── 79273a9754b54002-fc63e.min.png │ │ ├── 8bde275d8233602-4cee0.min.gif │ │ ├── 9fd2391a3cf49702-cd804.min.png │ │ ├── a3faf4373242d1-991ec.min.png │ │ ├── a4ae5891171faf02-0f81e.min.png │ │ ├── c463d610f0e32702-ca387.min.png │ │ └── f883b0fcd93f602-870c0.min.png │ ├── scripts │ │ ├── _startalk_sdk.55783cb1.js │ │ ├── common.55783cb1.js │ │ └── index.55783cb1.js │ └── styles │ │ └── index.55783cb1.css ├── dotenv.js ├── middlewares │ ├── devMiddleware.js │ ├── errorHandler.js │ ├── hotMiddleware.js │ └── proxyMiddleware.js ├── package.json ├── profiles │ └── production │ │ └── startalk.env ├── routes │ └── index.js ├── utils │ └── formatter.js └── views │ └── index.html ├── dotenv.js ├── gulpfile.js ├── package-lock.json ├── package.json ├── profiles ├── development │ └── startalk.env └── production │ └── startalk.env ├── src ├── assets │ ├── README.md │ ├── chat │ │ ├── 17e02.png │ │ ├── 38c02.png │ │ ├── 4f302.png │ │ ├── 75702.png │ │ ├── 80702.png │ │ ├── 87702.png │ │ ├── cd902.png │ │ ├── e8002.png │ │ ├── efb02.png │ │ ├── fd802.png │ │ └── packing-logo.png │ ├── favicon.ico │ ├── footer │ │ ├── 4cf2f7c2e63f4a02.png │ │ ├── a3faf4373242d1.png │ │ └── ff1a003aa731b0d4e2dd3d39687c8a54.png │ ├── index │ │ ├── 02d9a1cfacacfd02.png │ │ ├── 2fb5c91b6d9c4f02.png │ │ ├── 33bfeb065105c002.png │ │ ├── 4d709e27fa18aa02.png │ │ ├── 536fa0d98d69d102.png │ │ ├── 6ea30b7cd9472a02.png │ │ ├── 9fd2391a3cf49702.png │ │ ├── a4ae5891171faf02.png │ │ ├── c463d610f0e32702.png │ │ └── f883b0fcd93f602.png │ └── jstree │ │ ├── 303b0c915e984e02.png │ │ ├── 79273a9754b54002.png │ │ └── 8bde275d8233602.gif ├── nodeuii │ ├── app.js │ ├── middlewares │ │ ├── devMiddleware.js │ │ ├── errorHandler.js │ │ ├── hotMiddleware.js │ │ └── proxyMiddleware.js │ ├── routes │ │ └── index.js │ └── utils │ │ └── formatter.js └── web │ ├── app │ ├── common │ │ ├── components │ │ │ ├── message-box │ │ │ │ └── index.js │ │ │ ├── modal │ │ │ │ ├── index.js │ │ │ │ └── index.less │ │ │ └── select2-one │ │ │ │ ├── index.js │ │ │ │ └── index.less │ │ ├── lib │ │ │ ├── caret.js │ │ │ ├── jstree.js │ │ │ └── kindeditor │ │ │ │ └── kindeditor-all.js │ │ ├── styles │ │ │ ├── animate.less │ │ │ ├── chat.less │ │ │ ├── icon.less │ │ │ ├── index.less │ │ │ ├── jstree.css │ │ │ ├── login.less │ │ │ ├── modals.less │ │ │ ├── panel.less │ │ │ ├── phone │ │ │ │ ├── chat.less │ │ │ │ ├── modals.less │ │ │ │ └── panel.less │ │ │ └── reset.less │ │ └── utils │ │ │ ├── namespace-actions.js │ │ │ ├── namespace-reducers.js │ │ │ └── router.js │ ├── index.html │ └── pages │ │ └── index │ │ ├── actions.js │ │ ├── consts.js │ │ ├── entry.js │ │ ├── entry.settings.js │ │ ├── import-less.js │ │ ├── index.js │ │ ├── phone-ui │ │ ├── chat │ │ │ ├── emotions.js │ │ │ ├── empty.js │ │ │ ├── footer.js │ │ │ ├── groupCard.js │ │ │ ├── header.js │ │ │ ├── index.js │ │ │ ├── members.js │ │ │ ├── message.js │ │ │ └── userCard.js │ │ ├── login │ │ │ └── index.js │ │ ├── modal │ │ │ ├── addFriends.js │ │ │ ├── addUser.js │ │ │ ├── contentmenu.js │ │ │ ├── groupCard.js │ │ │ ├── members.js │ │ │ └── userCard.js │ │ ├── panel │ │ │ ├── friends.js │ │ │ ├── index.js │ │ │ ├── info.js │ │ │ ├── search.js │ │ │ ├── session.js │ │ │ └── tab.js │ │ └── tree │ │ │ ├── index.js │ │ │ └── index.less │ │ ├── reducer.js │ │ ├── sdk.js │ │ └── ui │ │ ├── chat │ │ ├── emotions.js │ │ ├── empty.js │ │ ├── footer.js │ │ ├── groupCard.js │ │ ├── header.js │ │ ├── index.js │ │ ├── members.js │ │ ├── message.js │ │ └── userCard.js │ │ ├── login │ │ └── index.js │ │ ├── modal │ │ ├── addFriends.js │ │ ├── addUser.js │ │ ├── contentmenu.js │ │ ├── groupCard.js │ │ ├── members.js │ │ └── userCard.js │ │ ├── panel │ │ ├── friends.js │ │ ├── index.js │ │ ├── info.js │ │ ├── search.js │ │ ├── session.js │ │ └── tab.js │ │ └── tree │ │ ├── index.js │ │ └── index.less │ ├── index.js │ └── sdk │ ├── common │ ├── assets │ │ └── 20180423_qtalk_msg.mp3 │ ├── lib │ │ ├── jquery.fileupload.js │ │ ├── jquery.iframe.transport.js │ │ ├── jquery.md5.js │ │ ├── jquery.ui.widget.js │ │ └── strophejs-plugin-iexdomain.js │ └── utils │ │ ├── messageHelper.js │ │ ├── publicEncrypt.js │ │ ├── randomBytes.js │ │ └── utils.js │ ├── core │ ├── auth.js │ ├── buildMessage.js │ ├── connection.js │ ├── emotions │ │ ├── index.js │ │ └── oneEmotions.js │ ├── index.js │ ├── message.js │ ├── ping.js │ ├── strophe.js │ └── upload.js │ ├── entry.js │ └── options.js ├── webpack.config.js └── 开发人员使用手册.md /.env: -------------------------------------------------------------------------------- 1 | /* 2 | * 开发环境 development 3 | * 生产环境 production 4 | */ 5 | 6 | NODE_ENV=production -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /.tmp/** 2 | /docs/** 3 | /mock/** 4 | /prd/** 5 | /node_modules/** 6 | /src/pages/** 7 | /src/sdk/common/** -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint-config-qunar/base' 4 | ].map(require.resolve), 5 | rules: { 6 | 'prefer-arrow-callback': 0, 7 | 'object-curly-newline': 0, 8 | 'complexity': 0, 9 | 'prefer-promise-reject-errors': 0 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # kdiff3 ignore 2 | *.orig 3 | 4 | # maven ignore 5 | target/ 6 | 7 | # eclipse ignore 8 | .settings/ 9 | .project 10 | .classpath 11 | 12 | # idea ignore 13 | .idea/ 14 | *.ipr 15 | *.iml 16 | *.iws 17 | 18 | # temp ignore 19 | *.log 20 | *.cache 21 | *.diff 22 | *.patch 23 | *.tmp 24 | 25 | # system ignore 26 | .DS_Store 27 | Thumbs.db 28 | 29 | # package ignore (optional) 30 | # *.jar 31 | # *.war 32 | # *.zip 33 | # *.tar 34 | # *.tar.gz 35 | 36 | # pods ignore 37 | Pods/ 38 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 8 | ## Startalk Web 9 | ### 简介 10 | - Startalk Web是IM网页版聊天工具。 11 | - Startalk Web依赖[后端服务](https://github.com/qunarcorp/ejabberd-open),否则无法正常使用该工具。 12 | - 涉及技术:node、koa、react、websocket、webpack等。 13 | - 主要功能:单聊、群聊、好友、组织架构、个人名片等,消息支持:文本、表情、图片、文件等。 14 | - 如果Startalk Web对您有所帮助或启发的话,还望给个star鼓励,我们团队会尽全力提供优化和支持,力求做出最优秀的企业级IM。 15 | - 此外,为有效、流畅体验Startalk Web,还请仔细阅读安装说明,如若遇到问题,欢迎进群咨询[QQ群:852987381]。 16 | ### 安装 17 | #### 环境要求 18 | - node@ >= 8.6.0 19 | - pm2 @>= 2.0.0 20 | #### 服务器环境安装(root用户) 21 | - 登录服务器后,进入下载目录,安装node: 22 | ``` 23 | cd /startalk/download 24 | wget https://npm.taobao.org/mirrors/node/v8.6.0/node-v8.6.0-linux-x64.tar.xz 25 | tar -xvf node-v8.6.0-linux-x64.tar.xz 26 | cd node-v8.6.0-linux-x64/bin 27 | ``` 28 | - 执行以下命令,若显示 v8.6.0 ,则表明安装成功 29 | ``` 30 | ./node -v 31 | ``` 32 | - 配置软连接,便于全局使用 node npm命令 33 | ``` 34 | ln -s /startalk/download/node-v8.6.0-linux-x64/bin/node /usr/local/bin/node 35 | ln -s /startalk/download/node-v8.6.0-linux-x64/bin/npm /usr/local/bin/npm 36 | ``` 37 | - 分别执行以下命令,若返回版本号,则表示配置成功 38 | ``` 39 | node -v 40 | npm -v 41 | ``` 42 | - 安装pm2,并配置软连接,便于全局使用 pm2命令 43 | ``` 44 | npm install -g pm2 45 | ln -s /startalk/download/node-v8.6.0-linux-x64/bin/pm2 /usr/local/bin/pm2 46 | ``` 47 | - 执行以下命令,若返回版本号,则表示配置成功 48 | ``` 49 | pm2 -v 50 | ``` 51 | #### 下载代码到服务器 52 | - 在 /startalk/download 目录下下载源码,然后将项目 /dist 目录下文件copy到 /startalk/startalk_web 目录下。 53 | ``` 54 | cd /startalk/download 55 | git clone https://github.com/qunarcorp/startalk_web.git 56 | cp -rf startalk_web/dist /startalk/startalk_web 57 | ``` 58 | #### 修改配置 59 | - 进入项目目录,配置 startalk.env 文件,将 BASEURL=http://IP:8080 的IP改成服务器IP,NAVIGATION=/ 改为后台导航 60 | - 其他配置:端口、公共路径可根据实际情况选择性配置 61 | - 建议采用淘宝镜像: npm config set registry https://registry.npm.taobao.org 62 | ``` 63 | cd /startalk/startalk_web 64 | npm install -production 65 | vim profiles/production/startalk.env 66 | ``` 67 | - 编辑完成后,保存退出vim编辑 68 | ``` 69 | 按下 esc键 70 | 输入 :wq 71 | 回车 72 | ``` 73 | #### 项目启动与预览 74 | - 使用pm2启动项目 75 | ``` 76 | pm2 start /startalk/startalk_web/app.js --watch 77 | ``` 78 | - 执行以下命令,查看是否启动成功,若该项目对应的status为online,则表明启动成功 79 | ``` 80 | pm2 list 81 | ``` 82 | - 注意:本次部署是以后台服务和前端服务部署在同一台机器上为背景,如需部署多台机器,则需通过nginx配置转发,以解决接口的跨域问题。 83 | - 项目预览: 84 | - 项目启动成功后,在电脑浏览器中输入 [本机IP:8080/web],回车键访问,输入测试账号登录,(测试账号admin,密码testpassword) 85 | - 至此,您便可享用web版及时通信聊天工具。 86 | 87 | ### 注意 88 | #### 注意事项 89 | - 前端服务默认端口为5000,默认公共路径为根路径(startalk.env 文件配置) 90 | - 如果需要修改端口或者公共路径,需要同步修改ng转发配置,确保后端接口正常调用 91 | - .env 文件默认为生产环境 NODE_ENV=production 92 | - 如若需要修改源代码,则需要重新打包生成dist目录 93 | - 先fork项目到自己的GitHub 94 | - 参考本项目下的developement.md文件,进行本地开发调试 95 | - 本地启动成功后便可将最新的dist文件上传至服务器启动 96 | - 接口位于entry.js 97 | - 获取直属领导,员工编号和查询用户电话功能已写好,接口需使用者实现 98 | #### 其他辅助命令(部署成功后不需要执行) 99 | - sudo netstat -anlp| grep 5000 查看5000 端口的进程 100 | - sudo kill -9 [进程code] 结束进程 101 | - df -h 查看不同分区的大小 102 | - rm -rf *** 删除文件夹 103 | - pm2 start [启动文件] 104 | - pm2 log 查看pm2日志 105 | - pm2 list 查看pm2启动项目清单及状态 106 | - pm2 delete [id] 删除pm2进程 107 | - pm2 show [id] 查看项目启动详情 -------------------------------------------------------------------------------- /config/htmlAfterWebpackPlugin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: your name 4 | * @Date: 2019-08-05 14:54:15 5 | * @LastEditTime: 2019-08-13 15:49:43 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | const HtmlWebpackPlugin = require('html-webpack-plugin') 9 | const pluginName = "htmlAfterWebpackPlugin" 10 | 11 | const assetHelper = (assetJson = []) => { 12 | const css = [] 13 | const js = [] 14 | const assets = { 15 | js: (item) => ``, 16 | css: (item) => `` 17 | } 18 | const cssReg = /.css$/ 19 | const jsReg = /.js$/ 20 | 21 | assetJson.map(item => { 22 | if (cssReg.test(item)) { 23 | css.push(assets.css(item.slice(item.lastIndexOf('/')+1))) 24 | } 25 | if (jsReg.test(item)) { 26 | js.push(assets.js(item.slice(item.lastIndexOf('/')+1))) 27 | } 28 | }) 29 | 30 | return { 31 | js, 32 | css 33 | } 34 | } 35 | 36 | class htmlAfterWebpackPlugin { 37 | apply(compiler) { 38 | compiler.hooks.compilation.tap(pluginName, (compilation) => { 39 | HtmlWebpackPlugin.getHooks(compilation).afterTemplateExecution.tapAsync( 40 | pluginName, 41 | (data, cb) => { 42 | const { html, plugin } = data 43 | let _html = html 44 | const assets = assetHelper(JSON.parse(plugin.assetJson)) 45 | 46 | _html = _html.replace('', assets.css.join('')) 47 | _html = _html.replace('', assets.js.join('')) 48 | 49 | data.html = _html 50 | cb(null, data) 51 | } 52 | ) 53 | }) 54 | } 55 | } 56 | module.exports = htmlAfterWebpackPlugin -------------------------------------------------------------------------------- /config/webpack.development.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require("mini-css-extract-plugin") 2 | 3 | module.exports = { 4 | watch:true, 5 | watchOptions:{ 6 | ignored: /node_modules/ 7 | }, 8 | mode: 'development', 9 | stats: { 10 | children: false, 11 | }, 12 | plugins: [ 13 | new MiniCssExtractPlugin({ 14 | filename: './styles/[name].css' 15 | }) 16 | ] 17 | } -------------------------------------------------------------------------------- /config/webpack.production.js: -------------------------------------------------------------------------------- 1 | const CleanWebpackPlugin = require('clean-webpack-plugin') 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin") 3 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 4 | 5 | // 生产环境打包文件加 hash 6 | module.exports = { 7 | mode: "production", 8 | output: { 9 | filename: 'scripts/[name].[hash:8].js' 10 | }, 11 | plugins: [ 12 | new CleanWebpackPlugin({ 13 | root: __dirname + 'dist/assets' 14 | }), 15 | new MiniCssExtractPlugin({ 16 | filename: './styles/[name].[hash:8].css' 17 | }), 18 | // 分析文件打包 19 | new BundleAnalyzerPlugin() 20 | ] 21 | } -------------------------------------------------------------------------------- /developement.md: -------------------------------------------------------------------------------- 1 | ### 本地开发环境要求 2 | - node@ >= 8.6.0 npm git等工具 3 | 4 | ### 项目启动开发 5 | - git clone https://github.com/qunarcorp/startalk_web.git 克隆代码到本地 6 | - npm install 安装项目依赖 7 | - 修改 .env 文件为 NODE_ENV=development 8 | - 进入目录文件 profiles/development/startalk.env, 配置后台地址 9 | 如: BASEURL=http://127.0.0.1:8080 10 | NAVIGATION=startalk_nav 11 | - 执行 npm run build 12 | - 新开 tab 页执行 npm run dev 启动项目 13 | 14 | ### 项目打包上线 15 | - 修改 .env 文件为 NODE_ENV=production 16 | - 进入目录文件 profiles/production/startalk.env 配置线上环境后台地址 17 | 如: BASEURL=http://127.0.0.1:8080 18 | NAVIGATION=startalk_nav 19 | - npm run build 20 | - npm run client 21 | - 将生产的dist目录上传至服务器 22 | - 服务器启动: pm2 start app.js --watch 23 | 24 | ### 书写规范 25 | - 驼峰命名 26 | - 中英文之间空格 27 | - 不写分号 28 | 29 | ### 注意事项 30 | #### 1.模块化引用,减少包的体积 31 | - 比如lodash 32 | - import omit from 'lodash/omit' //best 33 | - import { omit } from 'lodash' //bad -------------------------------------------------------------------------------- /dist/.env: -------------------------------------------------------------------------------- 1 | /* 2 | * 开发环境 development 3 | * 生产环境 production 4 | */ 5 | 6 | NODE_ENV=production -------------------------------------------------------------------------------- /dist/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _koa = require("koa"); 4 | 5 | var _koa2 = _interopRequireDefault(_koa); 6 | 7 | var _koaCompress = require("koa-compress"); 8 | 9 | var _koaCompress2 = _interopRequireDefault(_koaCompress); 10 | 11 | var _koaViews = require("koa-views"); 12 | 13 | var _koaViews2 = _interopRequireDefault(_koaViews); 14 | 15 | var _koaStatic = require("koa-static"); 16 | 17 | var _koaStatic2 = _interopRequireDefault(_koaStatic); 18 | 19 | var _index = require("./routes/index"); 20 | 21 | var _index2 = _interopRequireDefault(_index); 22 | 23 | var _path = require("path"); 24 | 25 | var _path2 = _interopRequireDefault(_path); 26 | 27 | require("./dotenv"); 28 | 29 | var _log4js = require("log4js"); 30 | 31 | var _log4js2 = _interopRequireDefault(_log4js); 32 | 33 | var _errorHandler = require("./middlewares/errorHandler.js"); 34 | 35 | var _errorHandler2 = _interopRequireDefault(_errorHandler); 36 | 37 | var _proxyMiddleware = require("./middlewares/proxyMiddleware"); 38 | 39 | var _proxyMiddleware2 = _interopRequireDefault(_proxyMiddleware); 40 | 41 | var _formatter = require("./utils/formatter"); 42 | 43 | var _request = require("request"); 44 | 45 | var _request2 = _interopRequireDefault(_request); 46 | 47 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 48 | 49 | /* 50 | * @Description: In User Settings Edit 51 | * @Author: xi.guo 52 | * @Date: 2019-08-05 14:54:15 53 | * @LastEditTime: 2019-08-19 10:24:27 54 | * @LastEditors: Please set LastEditors 55 | */ 56 | // development webpack-dev-middleware 57 | let webpack, webpackConfig, devMiddleware, hotMiddleware, compiler; 58 | 59 | if (process.env.NODE_ENV === 'development') { 60 | webpack = require('webpack'); 61 | 62 | const RawModule = require('webpack/lib/RawModule'); 63 | 64 | webpackConfig = require('../webpack.config.js'); 65 | devMiddleware = require('./middlewares/devMiddleware'); 66 | hotMiddleware = require('./middlewares/hotMiddleware'); 67 | compiler = webpack(webpackConfig); 68 | compiler.plugin('emit', (compilation, callback) => { 69 | const assets = compilation.assets; 70 | let data; 71 | Object.keys(assets).forEach(key => { 72 | if (key.match(/\.html$/)) { 73 | data = assets[key].source(); 74 | data = data.replace('<%=navConfig%>', JSON.stringify(global.startalkNavConfig)); 75 | data = data.replace('<%=keys%>', JSON.stringify(global.startalkKeys)); 76 | assets[key] = new RawModule(data).source(); 77 | } 78 | }); 79 | callback(); 80 | }); 81 | } 82 | 83 | const app = new _koa2.default(); 84 | const { 85 | PORT, 86 | IP, 87 | BASEURL, 88 | NAVIGATION 89 | } = process.env; 90 | global.startalkNavConfig = {}; 91 | global.startalkKeys = {}; 92 | (0, _request2.default)(`${BASEURL}/${NAVIGATION}`, (error, response, body) => { 93 | if (!error && response.statusCode == 200) { 94 | global.startalkNavConfig = JSON.parse(body); 95 | (0, _request2.default)(`${global.startalkNavConfig.baseaddess.javaurl}/qtapi/nck/rsa/get_public_key.do`, (error, response, body) => { 96 | if (!error && response.statusCode == 200) { 97 | global.startalkKeys = JSON.parse(body).data; 98 | } 99 | }); 100 | } 101 | }); 102 | app.use((0, _koaCompress2.default)({ 103 | threshold: 2048 104 | })); // development webpack-dev-middleware 105 | 106 | if (process.env.NODE_ENV === 'development') { 107 | app.use(devMiddleware(compiler, { 108 | publicPath: webpackConfig.output.publicPath, 109 | quiet: true 110 | })); 111 | app.use(hotMiddleware(compiler)); 112 | } 113 | 114 | app.use((0, _koaViews2.default)(_path2.default.join(__dirname, './views'), { 115 | map: { 116 | html: 'ejs' 117 | } 118 | })); 119 | app.use((0, _koaStatic2.default)(_path2.default.join(__dirname, './assets'))); // 错误日志处理 120 | 121 | _log4js2.default.configure({ 122 | appenders: { 123 | cheese: { 124 | type: 'file', 125 | filename: _path2.default.join(__dirname, `logs/${(0, _formatter.getNowDate)()}.log`) 126 | } 127 | }, 128 | categories: { 129 | default: { 130 | appenders: ['cheese'], 131 | level: 'error' 132 | } 133 | } 134 | }); 135 | 136 | const logger = _log4js2.default.getLogger('cheese'); 137 | 138 | _errorHandler2.default.error(app, logger); // 代理 139 | 140 | 141 | app.use((0, _proxyMiddleware2.default)()); //路由 142 | 143 | app.use(_index2.default.routes()); 144 | app.listen(PORT, IP, () => { 145 | console.log(`请访问端口:${PORT}`); 146 | }); -------------------------------------------------------------------------------- /dist/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/favicon.ico -------------------------------------------------------------------------------- /dist/assets/images/02d9a1cfacacfd02-c33ab.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/02d9a1cfacacfd02-c33ab.min.png -------------------------------------------------------------------------------- /dist/assets/images/2fb5c91b6d9c4f02-3548d.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/2fb5c91b6d9c4f02-3548d.min.png -------------------------------------------------------------------------------- /dist/assets/images/303b0c915e984e02-41211.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/303b0c915e984e02-41211.min.png -------------------------------------------------------------------------------- /dist/assets/images/33bfeb065105c002-efcf5.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/33bfeb065105c002-efcf5.min.png -------------------------------------------------------------------------------- /dist/assets/images/4cf2f7c2e63f4a02-991ec.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/4cf2f7c2e63f4a02-991ec.min.png -------------------------------------------------------------------------------- /dist/assets/images/4d709e27fa18aa02-f0e20.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/4d709e27fa18aa02-f0e20.min.png -------------------------------------------------------------------------------- /dist/assets/images/536fa0d98d69d102-55138.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/536fa0d98d69d102-55138.min.png -------------------------------------------------------------------------------- /dist/assets/images/6ea30b7cd9472a02-a2eb4.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/6ea30b7cd9472a02-a2eb4.min.png -------------------------------------------------------------------------------- /dist/assets/images/79273a9754b54002-fc63e.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/79273a9754b54002-fc63e.min.png -------------------------------------------------------------------------------- /dist/assets/images/8bde275d8233602-4cee0.min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/8bde275d8233602-4cee0.min.gif -------------------------------------------------------------------------------- /dist/assets/images/9fd2391a3cf49702-cd804.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/9fd2391a3cf49702-cd804.min.png -------------------------------------------------------------------------------- /dist/assets/images/a3faf4373242d1-991ec.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/a3faf4373242d1-991ec.min.png -------------------------------------------------------------------------------- /dist/assets/images/a4ae5891171faf02-0f81e.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/a4ae5891171faf02-0f81e.min.png -------------------------------------------------------------------------------- /dist/assets/images/c463d610f0e32702-ca387.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/c463d610f0e32702-ca387.min.png -------------------------------------------------------------------------------- /dist/assets/images/f883b0fcd93f602-870c0.min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/dist/assets/images/f883b0fcd93f602-870c0.min.png -------------------------------------------------------------------------------- /dist/dotenv.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const glob = require('glob') 3 | 4 | dotenv.config() 5 | 6 | const { NODE_ENV } = process.env 7 | 8 | if (!NODE_ENV) { 9 | console.log([ 10 | '[error]: The .env file is not found. ', 11 | 'Please run the following command to create the file in the root of the project:', 12 | 'echo NODE_ENV=development > .env' 13 | ].join('\n')); 14 | process.exit(1); 15 | } 16 | 17 | const envs = glob.sync(`${__dirname}/profiles/${NODE_ENV}/**/*.env`) 18 | 19 | envs.forEach(env => { 20 | dotenv.config({ path: env }) 21 | console.log(`[dotenv]\`${env}\` loaded.`) 22 | }) 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /dist/middlewares/devMiddleware.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _webpackDevMiddleware = require("webpack-dev-middleware"); 4 | 5 | var _webpackDevMiddleware2 = _interopRequireDefault(_webpackDevMiddleware); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 8 | 9 | // 改造成koa中间件 10 | const devMiddleware = (compiler, opts) => { 11 | const middleware = (0, _webpackDevMiddleware2.default)(compiler, opts); 12 | return async (ctx, next) => { 13 | await middleware(ctx.req, { 14 | end: content => { 15 | ctx.body = content; 16 | }, 17 | setHeader: (name, value) => { 18 | ctx.set(name, value); 19 | } 20 | }, next); 21 | }; 22 | }; 23 | 24 | module.exports = devMiddleware; -------------------------------------------------------------------------------- /dist/middlewares/errorHandler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | const errorHandler = { 7 | error(app, logger) { 8 | app.use(async (ctx, next) => { 9 | try { 10 | await next(); 11 | } catch (error) { 12 | logger.error(error); 13 | ctx.status = error.status || 500; 14 | ctx.body = "error page"; 15 | } 16 | }); 17 | app.use(async (ctx, next) => { 18 | await next(); 19 | if (404 != ctx.status) return; 20 | ctx.status = 404; 21 | ctx.response.redirect('/'); 22 | }); 23 | } 24 | 25 | }; // 26 | 27 | exports.default = errorHandler; -------------------------------------------------------------------------------- /dist/middlewares/hotMiddleware.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _webpackHotMiddleware = require("webpack-hot-middleware"); 4 | 5 | var _webpackHotMiddleware2 = _interopRequireDefault(_webpackHotMiddleware); 6 | 7 | var _stream = require("stream"); 8 | 9 | var _stream2 = _interopRequireDefault(_stream); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | // 改造成koa中间件 14 | const PassThrough = _stream2.default.PassThrough; 15 | 16 | const hotMiddleware = (compiler, opts) => { 17 | const middleware = (0, _webpackHotMiddleware2.default)(compiler, opts); 18 | return async (ctx, next) => { 19 | let stream = new PassThrough(); 20 | ctx.body = stream; 21 | await middleware(ctx.req, { 22 | write: stream.write.bind(stream), 23 | writeHead: (status, headers) => { 24 | ctx.status = status; 25 | ctx.set(headers); 26 | } 27 | }, next); 28 | }; 29 | }; 30 | 31 | module.exports = hotMiddleware; -------------------------------------------------------------------------------- /dist/middlewares/proxyMiddleware.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _koa2Connect = require("koa2-connect"); 4 | 5 | var _koa2Connect2 = _interopRequireDefault(_koa2Connect); 6 | 7 | var _httpProxyMiddleware = require("http-proxy-middleware"); 8 | 9 | var _httpProxyMiddleware2 = _interopRequireDefault(_httpProxyMiddleware); 10 | 11 | var _url = require("url"); 12 | 13 | var _url2 = _interopRequireDefault(_url); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | /* 18 | * @Description: In User Settings Edit 19 | * @Author: xi.guo 20 | * @Date: 2019-08-05 14:54:15 21 | * @LastEditTime: 2019-08-13 14:33:20 22 | * @LastEditors: Please set LastEditors 23 | */ 24 | const proxyMap = { 25 | '/package': { 26 | url: ['baseaddess', 'javaurl'] 27 | }, 28 | '/py/search': { 29 | url: ['ability', 'searchurl'] 30 | }, 31 | '/newapi': { 32 | url: ['baseaddess', 'httpurl'] 33 | }, 34 | '/api': { 35 | url: ['baseaddess', 'httpurl'] 36 | }, 37 | '/file': { 38 | url: ['baseaddess', 'fileurl'] 39 | } 40 | }; 41 | 42 | const getIn = (obj, arr = []) => { 43 | return arr.reduce((accumulator, currentValue) => { 44 | if (typeof accumulator === 'object') { 45 | return accumulator[currentValue]; 46 | } 47 | }, obj); 48 | }; 49 | 50 | const proxyMiddleware = () => { 51 | return async (ctx, next) => { 52 | for (var proxyKey in proxyMap) { 53 | if (ctx.url.startsWith(proxyKey)) { 54 | ctx.respond = false; 55 | const { 56 | body 57 | } = ctx.request; 58 | const contentType = ctx.request.header['content-type']; 59 | 60 | const urlObj = _url2.default.parse(global.startalkNavConfig && getIn(global.startalkNavConfig, proxyMap[proxyKey].url)); 61 | 62 | const defaultOpt = {}; 63 | 64 | if (proxyMap[proxyKey].pathRewrite) { 65 | defaultOpt.pathRewrite = { 66 | pathRewrite: { 67 | [proxyMap.proxyKey.pathRewrite]: urlObj.pathname 68 | } 69 | }; 70 | } 71 | 72 | await (0, _koa2Connect2.default)((0, _httpProxyMiddleware2.default)(proxyKey, Object.assign({ 73 | target: `${urlObj.protocol}//${urlObj.host}`, 74 | changeOrigin: true, 75 | onProxyReq: proxyReq => { 76 | if (body && contentType.indexOf('application/json') > -1) { 77 | const bodyData = JSON.stringify(body); 78 | proxyReq.setHeader('Content-Type', 'application/json'); 79 | proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); 80 | proxyReq.write(bodyData); 81 | } else if (body && contentType.indexOf('application/x-www-form-urlencoded') > -1) { 82 | const bodyData = Object.keys(body).map(key => `${key}=${body[key]}`).join('&'); 83 | proxyReq.setHeader('Content-Type', 'application/x-www-form-urlencoded'); 84 | proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); 85 | proxyReq.write(bodyData); 86 | } 87 | } 88 | }, defaultOpt)))(ctx, next); 89 | } 90 | } 91 | 92 | await next(); 93 | }; 94 | }; 95 | 96 | module.exports = proxyMiddleware; -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "startalk_admin_web", 3 | "version": "1.0.0", 4 | "description": "startalk_admin_web", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "gulp", 9 | "dev": "node ./dist/app.js", 10 | "client": "webpack", 11 | "start": "pm2 start ./app.js", 12 | "eslint": "eslint --fix src" 13 | }, 14 | "author": "", 15 | "private": true, 16 | "license": "ISC", 17 | "dependencies": { 18 | "dotenv": "^8.0.0", 19 | "ejs": "^2.6.2", 20 | "glob": "^7.1.4", 21 | "http-proxy-middleware": "^0.19.1", 22 | "koa": "^2.7.0", 23 | "koa-compress": "^3.0.0", 24 | "koa-router": "^7.4.0", 25 | "koa-static": "^5.0.0", 26 | "koa-views": "^6.2.0", 27 | "koa2-connect": "^1.0.2", 28 | "log4js": "^4.1.0", 29 | "request": "^2.88.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.4.3", 33 | "@babel/plugin-proposal-class-properties": "^7.4.0", 34 | "@babel/plugin-proposal-decorators": "^7.4.0", 35 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 36 | "@babel/plugin-transform-runtime": "^7.4.4", 37 | "@babel/preset-env": "^7.4.3", 38 | "@babel/preset-react": "^7.0.0", 39 | "@babel/runtime": "^7.0.0-beta.55", 40 | "autoprefixer": "^9.5.1", 41 | "axios": "^0.19.0", 42 | "babel-loader": "^8.0.5", 43 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 44 | "classnames": "^2.2.6", 45 | "clean-webpack-plugin": "^2.0.1", 46 | "clipboard": "^2.0.4", 47 | "copy-webpack-plugin": "^5.0.2", 48 | "css-loader": "^2.1.1", 49 | "dayjs": "^1.8.15", 50 | "del": "^4.1.0", 51 | "events": "^3.0.0", 52 | "file-loader": "^3.0.1", 53 | "gulp": "^4.0.0", 54 | "gulp-babel": "^8.0.0", 55 | "gulp-better-rollup": "^4.0.1", 56 | "gulp-watch": "^5.0.1", 57 | "html-loader": "^0.5.5", 58 | "html-webpack-plugin": "^4.0.0-beta.3", 59 | "imagemin": "^6.1.0", 60 | "imagemin-pngquant": "^7.0.0", 61 | "img-loader": "^3.0.1", 62 | "immutable": "^3.8.2", 63 | "jquery": "^3.4.1", 64 | "js-cookie": "^2.2.0", 65 | "less": "^3.9.0", 66 | "less-loader": "^4.1.0", 67 | "mini-css-extract-plugin": "^0.5.0", 68 | "optimize-css-assets-webpack-plugin": "^4.0.0", 69 | "polyfill-crypto.getrandomvalues": "^1.0.0", 70 | "postcss-loader": "^3.0.0", 71 | "react": "^16.8.6", 72 | "react-a11y": "^1.1.0", 73 | "react-dom": "^16.8.6", 74 | "react-lazyload": "^2.6.2", 75 | "react-redux": "^7.1.0", 76 | "react-router-dom": "^5.0.1", 77 | "react-transform-catch-errors": "^1.0.2", 78 | "redbox-react": "^1.6.0", 79 | "redux": "^4.0.4", 80 | "redux-actions": "^2.6.5", 81 | "redux-immutable": "^4.0.0", 82 | "strophe.js": "^1.3.3", 83 | "strophejs-plugin-disco": "0.0.2", 84 | "strophejs-plugin-ping": "0.0.3", 85 | "strophejs-plugin-vcard": "0.0.1", 86 | "style-loader": "^0.23.1", 87 | "url-loader": "^1.1.2", 88 | "webpack": "^4.29.6", 89 | "webpack-bundle-analyzer": "^3.3.2", 90 | "webpack-cli": "^3.3.0", 91 | "webpack-dev-middleware": "^3.7.0", 92 | "webpack-hot-middleware": "^2.25.0", 93 | "webpack-merge": "^4.2.1" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /dist/profiles/production/startalk.env: -------------------------------------------------------------------------------- 1 | // 项目启动端口 2 | PORT=5000 3 | 4 | // 项目启动 IP 5 | IP=0.0.0.0 6 | 7 | // 后台接口地址(例:http://127.0.0.1:8080) 8 | BASEURL= 9 | 10 | //后台导航地址(例:startalk_nav或newapi/nck/qtalk_nav.qunar) 11 | NAVIGATION= 12 | 13 | //公共路径 14 | PUBLICPATH=/ -------------------------------------------------------------------------------- /dist/routes/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _koaRouter = require("koa-router"); 8 | 9 | var _koaRouter2 = _interopRequireDefault(_koaRouter); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | /* 14 | * @Description: In User Settings Edit 15 | * @Author: your name 16 | * @Date: 2019-08-05 14:54:15 17 | * @LastEditTime: 2019-08-13 17:38:00 18 | * @LastEditors: Please set LastEditors 19 | */ 20 | const router = new _koaRouter2.default(); 21 | router.get('/reterievepassword', async (ctx, next) => { 22 | await ctx.render('reterievepassword'); 23 | }); 24 | router.get('/', async (ctx, next) => { 25 | const navConfig = JSON.stringify(global.startalkNavConfig); 26 | const keys = JSON.stringify(global.startalkKeys); 27 | await ctx.render('index', { 28 | navConfig, 29 | keys 30 | }); 31 | }); 32 | exports.default = router; -------------------------------------------------------------------------------- /dist/utils/formatter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | /* 8 | * @Description: In User Settings Edit 9 | * @Author: your name 10 | * @Date: 2019-08-05 14:54:15 11 | * @LastEditTime: 2019-08-12 19:55:02 12 | * @LastEditors: Please set LastEditors 13 | */ 14 | const getNowDate = exports.getNowDate = () => { 15 | const time = new Date(); 16 | const year = time.getFullYear(); 17 | const month = time.getMonth() + 1; 18 | const day = time.getDate(); 19 | return `${year}-${month}-${day}`; 20 | }; 21 | 22 | const insertStr = exports.insertStr = (soure, start, newStr) => { 23 | return soure.slice(0, start) + newStr + soure.slice(start); 24 | }; -------------------------------------------------------------------------------- /dist/views/index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | startalk 16 | 17 | 18 |
19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /dotenv.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const glob = require('glob') 3 | 4 | dotenv.config() 5 | 6 | const { NODE_ENV } = process.env 7 | 8 | if (!NODE_ENV) { 9 | console.log([ 10 | '[error]: The .env file is not found. ', 11 | 'Please run the following command to create the file in the root of the project:', 12 | 'echo NODE_ENV=development > .env' 13 | ].join('\n')); 14 | process.exit(1); 15 | } 16 | 17 | const envs = glob.sync(`${__dirname}/profiles/${NODE_ENV}/**/*.env`) 18 | 19 | envs.forEach(env => { 20 | dotenv.config({ path: env }) 21 | console.log(`[dotenv]\`${env}\` loaded.`) 22 | }) 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const babel = require('gulp-babel') 3 | const watch = require('gulp-watch') 4 | const del = require('del') 5 | const dotenv = require('dotenv') 6 | dotenv.config() 7 | 8 | const ASSETS = './src/nodeuii/**/*.js' 9 | const DIST_PATH = './dist' 10 | 11 | gulp.task('clean', () => { 12 | return del(DIST_PATH, { 13 | force: true 14 | }) 15 | }) 16 | 17 | gulp.task('copyEnv', () => { 18 | return gulp.src('.env') 19 | .pipe(gulp.dest('./dist')) 20 | }) 21 | 22 | gulp.task('copyDotenv', () => { 23 | return gulp.src('./dotenv.js') 24 | .pipe(gulp.dest('./dist')) 25 | }) 26 | 27 | gulp.task('copyEnvs', () => { 28 | return gulp.src(`./profiles/${process.env.NODE_ENV}/**/*.env`) 29 | .pipe(gulp.dest(`./dist/profiles/${process.env.NODE_ENV}`)) 30 | }) 31 | 32 | gulp.task('build:dev', () => { 33 | return watch(ASSETS, { ignoreInitial: false }, () => { 34 | gulp.src(ASSETS) 35 | .pipe(babel({ 36 | babelrc: false, 37 | 'plugins': [ 38 | 'transform-es2015-modules-commonjs' 39 | ] 40 | })) 41 | .pipe(gulp.dest('./dist')) 42 | }) 43 | }) 44 | 45 | gulp.task('build:prod', () => { 46 | return gulp.src(ASSETS) 47 | .pipe(babel({ 48 | babelrc: false, 49 | 'plugins': [ 50 | 'transform-es2015-modules-commonjs' 51 | ] 52 | })) 53 | .pipe(gulp.dest('./dist')) 54 | }) 55 | 56 | let _task = gulp.series('clean', 'copyEnv', 'copyDotenv', 'copyEnvs', 'build:dev') 57 | 58 | if (process.env.NODE_ENV === 'production') { 59 | _task = gulp.series('clean', 'copyEnv', 'copyDotenv', 'copyEnvs', 'build:prod') 60 | } 61 | 62 | gulp.task('default', _task) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "startalk_admin_web", 3 | "version": "1.0.0", 4 | "description": "startalk_admin_web", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "gulp", 9 | "dev": "node ./dist/app.js", 10 | "client": "webpack", 11 | "start": "pm2 start ./app.js", 12 | "eslint": "eslint --fix src" 13 | }, 14 | "author": "", 15 | "private": true, 16 | "license": "ISC", 17 | "dependencies": { 18 | "dotenv": "^8.0.0", 19 | "ejs": "^2.6.2", 20 | "glob": "^7.1.4", 21 | "http-proxy-middleware": "^0.19.1", 22 | "koa": "^2.7.0", 23 | "koa-compress": "^3.0.0", 24 | "koa-router": "^7.4.0", 25 | "koa-static": "^5.0.0", 26 | "koa-views": "^6.2.0", 27 | "koa2-connect": "^1.0.2", 28 | "log4js": "^4.1.0", 29 | "request": "^2.88.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.4.3", 33 | "@babel/plugin-proposal-class-properties": "^7.4.0", 34 | "@babel/plugin-proposal-decorators": "^7.4.0", 35 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 36 | "@babel/plugin-transform-runtime": "^7.4.4", 37 | "@babel/preset-env": "^7.4.3", 38 | "@babel/preset-react": "^7.0.0", 39 | "@babel/runtime": "^7.0.0-beta.55", 40 | "autoprefixer": "^9.5.1", 41 | "axios": "^0.21.1", 42 | "babel-loader": "^8.0.5", 43 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 44 | "classnames": "^2.2.6", 45 | "clean-webpack-plugin": "^2.0.1", 46 | "clipboard": "^2.0.4", 47 | "copy-webpack-plugin": "^5.0.2", 48 | "css-loader": "^2.1.1", 49 | "dayjs": "^1.8.15", 50 | "del": "^4.1.0", 51 | "events": "^3.0.0", 52 | "file-loader": "^3.0.1", 53 | "gulp": "^4.0.0", 54 | "gulp-babel": "^8.0.0", 55 | "gulp-better-rollup": "^4.0.1", 56 | "gulp-watch": "^5.0.1", 57 | "html-loader": "^0.5.5", 58 | "html-webpack-plugin": "^4.0.0-beta.3", 59 | "imagemin": "^6.1.0", 60 | "imagemin-pngquant": "^7.0.0", 61 | "img-loader": "^3.0.1", 62 | "immutable": "^3.8.2", 63 | "jquery": "^3.5.0", 64 | "js-cookie": "^2.2.0", 65 | "less": "^3.9.0", 66 | "less-loader": "^4.1.0", 67 | "mini-css-extract-plugin": "^0.5.0", 68 | "optimize-css-assets-webpack-plugin": "^4.0.0", 69 | "polyfill-crypto.getrandomvalues": "^1.0.0", 70 | "postcss-loader": "^3.0.0", 71 | "react": "^16.8.6", 72 | "react-a11y": "^1.1.0", 73 | "react-dom": "^16.8.6", 74 | "react-lazyload": "^2.6.2", 75 | "react-redux": "^7.1.0", 76 | "react-router-dom": "^5.0.1", 77 | "react-transform-catch-errors": "^1.0.2", 78 | "redbox-react": "^1.6.0", 79 | "redux": "^4.0.4", 80 | "redux-actions": "^2.6.5", 81 | "redux-immutable": "^4.0.0", 82 | "strophe.js": "^1.3.3", 83 | "strophejs-plugin-disco": "0.0.2", 84 | "strophejs-plugin-ping": "0.0.3", 85 | "strophejs-plugin-vcard": "0.0.1", 86 | "style-loader": "^0.23.1", 87 | "url-loader": "^1.1.2", 88 | "webpack": "^4.29.6", 89 | "webpack-bundle-analyzer": "^3.3.2", 90 | "webpack-cli": "^3.3.0", 91 | "webpack-dev-middleware": "^3.7.0", 92 | "webpack-hot-middleware": "^2.25.0", 93 | "webpack-merge": "^4.2.1" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /profiles/development/startalk.env: -------------------------------------------------------------------------------- 1 | // 项目启动端口 2 | PORT=5000 3 | 4 | // 项目启动 IP 5 | IP=0.0.0.0 6 | 7 | // 后台接口地址(例:http://127.0.0.1:8080) 8 | BASEURL= 9 | 10 | //后台导航地址(例:startalk_nav或newapi/nck/qtalk_nav.qunar) 11 | NAVIGATION= 12 | 13 | //公共路径 14 | PUBLICPATH=/ 15 | -------------------------------------------------------------------------------- /profiles/production/startalk.env: -------------------------------------------------------------------------------- 1 | // 项目启动端口 2 | PORT=5000 3 | 4 | // 项目启动 IP 5 | IP=0.0.0.0 6 | 7 | // 后台接口地址(例:http://127.0.0.1:8080) 8 | BASEURL= 9 | 10 | //后台导航地址(例:startalk_nav或newapi/nck/qtalk_nav.qunar) 11 | NAVIGATION= 12 | 13 | //公共路径 14 | PUBLICPATH=/ -------------------------------------------------------------------------------- /src/assets/README.md: -------------------------------------------------------------------------------- 1 | # assets 2 | 存放静态文件的目录,如图片、字体等,不存放代码类文件,如CSS、JavaScript 3 | 4 | ## 约定 5 | - 网页模版中对静态资源引用时使用绝对路径,如 `/images/packing-logo.png` 6 | 7 | ## js文件中如何使用图片、字体等静态资源 8 | 假设文件目录结构如下: 9 | ``` 10 | ├── /hotel/ 11 | │ └── /entries/ 12 | │ └── /index.js 13 | └── /assets/ 14 | └── /images/ 15 | └── /logo.png 16 | 17 | ``` 18 | 有两种方式能将静态资源引入JavaScript中: 19 | 20 | 1. 使用webpack的require机制(推荐) 21 | require或import时使用静态资源相对路径,有两种相对路径可用: 22 | - 静态文件相对于当前JavaScript文件的相对路径 23 | 24 | ```js 25 | // index.js 26 | import logo from '../../assets/images/logo.png'; 27 | ``` 28 | 当文件目录层级比较深时,这种方式书写较费劲 29 | - 静态文件相对于`assets`的相对路径 30 | 31 | ```js 32 | // index.js 33 | import logo from 'images/logo.png'; 34 | ``` 35 | 这种方式比较简洁 36 | 无论使用上述哪种方式引入的静态资源,使用时都必须使用绝对路径 37 | 38 | ```js 39 | // index.js 40 | import logo from '../../assets/images/logo.png'; 41 | // import logo from 'images/logo.png'; 42 | var a = new Image(); 43 | a.src = `/${logo}`; 44 | ``` 45 | 46 | 2. 手动拼资源的URL地址,获取到静态资源的uri地址 `process.env.CDN_ROOT`,从而手工拼接url,这种方式引入的静态资源不会做md5 47 | 48 | ```js 49 | // index.js 50 | var a = new Image(); 51 | a.src = process.env.CDN_ROOT + '/images/logo.png'; 52 | ``` 53 | -------------------------------------------------------------------------------- /src/assets/chat/17e02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/17e02.png -------------------------------------------------------------------------------- /src/assets/chat/38c02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/38c02.png -------------------------------------------------------------------------------- /src/assets/chat/4f302.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/4f302.png -------------------------------------------------------------------------------- /src/assets/chat/75702.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/75702.png -------------------------------------------------------------------------------- /src/assets/chat/80702.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/80702.png -------------------------------------------------------------------------------- /src/assets/chat/87702.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/87702.png -------------------------------------------------------------------------------- /src/assets/chat/cd902.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/cd902.png -------------------------------------------------------------------------------- /src/assets/chat/e8002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/e8002.png -------------------------------------------------------------------------------- /src/assets/chat/efb02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/efb02.png -------------------------------------------------------------------------------- /src/assets/chat/fd802.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/fd802.png -------------------------------------------------------------------------------- /src/assets/chat/packing-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/chat/packing-logo.png -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/footer/4cf2f7c2e63f4a02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/footer/4cf2f7c2e63f4a02.png -------------------------------------------------------------------------------- /src/assets/footer/a3faf4373242d1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/footer/a3faf4373242d1.png -------------------------------------------------------------------------------- /src/assets/footer/ff1a003aa731b0d4e2dd3d39687c8a54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/footer/ff1a003aa731b0d4e2dd3d39687c8a54.png -------------------------------------------------------------------------------- /src/assets/index/02d9a1cfacacfd02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/index/02d9a1cfacacfd02.png -------------------------------------------------------------------------------- /src/assets/index/2fb5c91b6d9c4f02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/index/2fb5c91b6d9c4f02.png -------------------------------------------------------------------------------- /src/assets/index/33bfeb065105c002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/index/33bfeb065105c002.png -------------------------------------------------------------------------------- /src/assets/index/4d709e27fa18aa02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/index/4d709e27fa18aa02.png -------------------------------------------------------------------------------- /src/assets/index/536fa0d98d69d102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/index/536fa0d98d69d102.png -------------------------------------------------------------------------------- /src/assets/index/6ea30b7cd9472a02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/index/6ea30b7cd9472a02.png -------------------------------------------------------------------------------- /src/assets/index/9fd2391a3cf49702.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/index/9fd2391a3cf49702.png -------------------------------------------------------------------------------- /src/assets/index/a4ae5891171faf02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/index/a4ae5891171faf02.png -------------------------------------------------------------------------------- /src/assets/index/c463d610f0e32702.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/index/c463d610f0e32702.png -------------------------------------------------------------------------------- /src/assets/index/f883b0fcd93f602.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/index/f883b0fcd93f602.png -------------------------------------------------------------------------------- /src/assets/jstree/303b0c915e984e02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/jstree/303b0c915e984e02.png -------------------------------------------------------------------------------- /src/assets/jstree/79273a9754b54002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/jstree/79273a9754b54002.png -------------------------------------------------------------------------------- /src/assets/jstree/8bde275d8233602.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/assets/jstree/8bde275d8233602.gif -------------------------------------------------------------------------------- /src/nodeuii/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-05 14:54:15 5 | * @LastEditTime: 2019-08-19 10:24:27 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import Koa from 'koa' 9 | import compress from 'koa-compress' 10 | import views from 'koa-views' 11 | import serve from 'koa-static' 12 | import router from './routes/index' 13 | import path from 'path' 14 | import './dotenv' 15 | import log4js from 'log4js' 16 | import errorHandler from './middlewares/errorHandler.js' 17 | import proxyMiddleware from './middlewares/proxyMiddleware' 18 | import { getNowDate, insertStr } from './utils/formatter' 19 | import request from 'request' 20 | 21 | // development webpack-dev-middleware 22 | let webpack, webpackConfig, devMiddleware, hotMiddleware, compiler 23 | if (process.env.NODE_ENV === 'development') { 24 | webpack = require('webpack') 25 | const RawModule = require('webpack/lib/RawModule') 26 | webpackConfig = require('../webpack.config.js') 27 | devMiddleware = require('./middlewares/devMiddleware') 28 | hotMiddleware = require('./middlewares/hotMiddleware') 29 | 30 | compiler = webpack(webpackConfig) 31 | 32 | compiler.plugin('emit', (compilation, callback) => { 33 | const assets = compilation.assets 34 | let data 35 | 36 | Object.keys(assets).forEach(key => { 37 | if (key.match(/\.html$/)) { 38 | data = assets[key].source() 39 | data = data.replace('<%=navConfig%>', JSON.stringify(global.startalkNavConfig)) 40 | data = data.replace('<%=keys%>', JSON.stringify(global.startalkKeys)) 41 | assets[key] = (new RawModule(data)).source() 42 | } 43 | }) 44 | callback() 45 | }) 46 | } 47 | 48 | const app = new Koa() 49 | const { PORT, IP, BASEURL, NAVIGATION } = process.env 50 | global.startalkNavConfig = {} 51 | global.startalkKeys = {} 52 | 53 | request(`${BASEURL}/${NAVIGATION}`, (error, response, body) => { 54 | if (!error && response.statusCode == 200) { 55 | global.startalkNavConfig = JSON.parse(body) 56 | 57 | request(`${global.startalkNavConfig.baseaddess.javaurl}/qtapi/nck/rsa/get_public_key.do`, (error, response, body) => { 58 | if (!error && response.statusCode == 200) { 59 | global.startalkKeys = JSON.parse(body).data 60 | } 61 | }) 62 | } 63 | }) 64 | 65 | app.use(compress({ 66 | threshold: 2048 67 | })) 68 | 69 | // development webpack-dev-middleware 70 | if (process.env.NODE_ENV === 'development') { 71 | app.use(devMiddleware(compiler, { 72 | publicPath: webpackConfig.output.publicPath, 73 | quiet: true 74 | })) 75 | app.use(hotMiddleware(compiler)) 76 | } 77 | 78 | app.use(views(path.join(__dirname , './views'), { 79 | map: {html: 'ejs' } 80 | })) 81 | app.use(serve(path.join(__dirname , './assets'))) 82 | 83 | // 错误日志处理 84 | log4js.configure({ 85 | appenders: { cheese: { type: 'file', filename: path.join(__dirname, `logs/${getNowDate()}.log`) } }, 86 | categories: { default: { appenders: ['cheese'], level: 'error' } } 87 | }) 88 | const logger = log4js.getLogger('cheese') 89 | 90 | errorHandler.error(app, logger) 91 | 92 | // 代理 93 | app.use(proxyMiddleware()) 94 | 95 | //路由 96 | app.use(router.routes()) 97 | 98 | app.listen(PORT, IP, () => { 99 | console.log(`请访问端口:${PORT}`) 100 | }) -------------------------------------------------------------------------------- /src/nodeuii/middlewares/devMiddleware.js: -------------------------------------------------------------------------------- 1 | // 改造成koa中间件 2 | import webpackDev from 'webpack-dev-middleware' 3 | 4 | const devMiddleware = (compiler, opts) => { 5 | const middleware = webpackDev(compiler, opts) 6 | return async (ctx, next) => { 7 | await middleware(ctx.req, { 8 | end: (content) => { 9 | ctx.body = content 10 | }, 11 | setHeader: (name, value) => { 12 | ctx.set(name, value) 13 | } 14 | }, next) 15 | } 16 | } 17 | 18 | module.exports = devMiddleware -------------------------------------------------------------------------------- /src/nodeuii/middlewares/errorHandler.js: -------------------------------------------------------------------------------- 1 | const errorHandler = { 2 | error(app, logger){ 3 | app.use(async (ctx, next) => { 4 | try { 5 | await next() 6 | } catch (error) { 7 | logger.error(error) 8 | ctx.status = error.status || 500 9 | ctx.body = "error page" 10 | } 11 | }) 12 | app.use(async (ctx, next) => { 13 | await next() 14 | if(404 != ctx.status) return 15 | ctx.status = 404 16 | ctx.response.redirect('/') 17 | }) 18 | } 19 | } 20 | // 21 | export default errorHandler -------------------------------------------------------------------------------- /src/nodeuii/middlewares/hotMiddleware.js: -------------------------------------------------------------------------------- 1 | // 改造成koa中间件 2 | import webpackHot from 'webpack-hot-middleware' 3 | import stream from 'stream' 4 | 5 | const PassThrough = stream.PassThrough 6 | 7 | const hotMiddleware = (compiler, opts) => { 8 | const middleware = webpackHot(compiler, opts) 9 | return async (ctx, next) => { 10 | let stream = new PassThrough() 11 | ctx.body = stream 12 | await middleware(ctx.req, { 13 | write: stream.write.bind(stream), 14 | writeHead: (status, headers) => { 15 | ctx.status = status 16 | ctx.set(headers) 17 | } 18 | }, next) 19 | } 20 | } 21 | 22 | 23 | module.exports = hotMiddleware -------------------------------------------------------------------------------- /src/nodeuii/middlewares/proxyMiddleware.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-05 14:54:15 5 | * @LastEditTime: 2019-08-13 14:33:20 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import k2c from 'koa2-connect' 9 | import httpProxy from 'http-proxy-middleware' 10 | import url from 'url' 11 | 12 | const proxyMap = { 13 | '/package': { 14 | url: ['baseaddess', 'javaurl'] 15 | }, 16 | '/py/search': { 17 | url: ['ability', 'searchurl'] 18 | }, 19 | '/newapi': { 20 | url: ['baseaddess', 'httpurl'] 21 | }, 22 | '/api': { 23 | url: ['baseaddess', 'httpurl'] 24 | }, 25 | '/file': { 26 | url: ['baseaddess', 'fileurl'] 27 | } 28 | } 29 | 30 | const getIn = (obj, arr = []) => { 31 | return arr.reduce((accumulator, currentValue) => { 32 | if (typeof accumulator === 'object') { 33 | return accumulator[currentValue] 34 | } 35 | }, obj) 36 | } 37 | 38 | 39 | const proxyMiddleware = () => { 40 | return async (ctx, next) => { 41 | for(var proxyKey in proxyMap) { 42 | if (ctx.url.startsWith(proxyKey)) { 43 | ctx.respond = false 44 | const { body } = ctx.request 45 | const contentType = ctx.request.header['content-type'] 46 | const urlObj = url.parse(global.startalkNavConfig && getIn(global.startalkNavConfig, proxyMap[proxyKey].url)) 47 | const defaultOpt = {} 48 | 49 | if (proxyMap[proxyKey].pathRewrite) { 50 | defaultOpt.pathRewrite = { 51 | pathRewrite: { 52 | [proxyMap.proxyKey.pathRewrite]: urlObj.pathname 53 | } 54 | } 55 | } 56 | 57 | await k2c(httpProxy(proxyKey, Object.assign({ 58 | target: `${urlObj.protocol}//${urlObj.host}`, 59 | changeOrigin: true, 60 | onProxyReq: (proxyReq) => { 61 | if (body && contentType.indexOf('application/json') > -1) { 62 | const bodyData = JSON.stringify(body) 63 | 64 | proxyReq.setHeader('Content-Type', 'application/json') 65 | proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)) 66 | proxyReq.write(bodyData) 67 | } 68 | else if (body && contentType.indexOf('application/x-www-form-urlencoded') > -1) { 69 | const bodyData = Object.keys(body).map(key => `${key}=${body[key]}`).join('&') 70 | 71 | proxyReq.setHeader('Content-Type', 'application/x-www-form-urlencoded') 72 | proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)) 73 | proxyReq.write(bodyData) 74 | } 75 | } 76 | }, defaultOpt)))(ctx, next) 77 | } 78 | } 79 | 80 | await next() 81 | } 82 | } 83 | 84 | module.exports = proxyMiddleware -------------------------------------------------------------------------------- /src/nodeuii/routes/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: your name 4 | * @Date: 2019-08-05 14:54:15 5 | * @LastEditTime: 2019-08-13 17:38:00 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import Router from 'koa-router' 9 | 10 | const router = new Router() 11 | 12 | 13 | router.get('/reterievepassword', async (ctx, next) => { 14 | await ctx.render('reterievepassword') 15 | }) 16 | 17 | router.get('/', async (ctx, next) => { 18 | const navConfig = JSON.stringify(global.startalkNavConfig) 19 | const keys = JSON.stringify(global.startalkKeys) 20 | await ctx.render('index', { 21 | navConfig, 22 | keys 23 | }) 24 | }) 25 | 26 | export default router 27 | -------------------------------------------------------------------------------- /src/nodeuii/utils/formatter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: your name 4 | * @Date: 2019-08-05 14:54:15 5 | * @LastEditTime: 2019-08-12 19:55:02 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | export const getNowDate = () => { 9 | const time = new Date() 10 | const year = time.getFullYear() 11 | const month = time.getMonth() + 1 12 | const day = time.getDate() 13 | 14 | return `${year}-${month}-${day}` 15 | } 16 | 17 | export const insertStr = (soure,start, newStr) => { 18 | return soure.slice(0, start) + newStr + soure.slice(start) 19 | } 20 | -------------------------------------------------------------------------------- /src/web/app/common/components/message-box/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import Modal from '../modal'; 4 | 5 | const noop = () => { }; 6 | 7 | const contentWrapper = content => ( 8 |
12 | ); 13 | 14 | class MessageBox extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | show: false, 19 | title: null, 20 | content: null, 21 | isConfirm: false, 22 | okText: '确定', 23 | cancelText: '取消' 24 | }; 25 | this.confirmCancel = noop; 26 | this.confirmOk = noop; 27 | } 28 | 29 | toggle = (show) => { 30 | this.setState({ 31 | show 32 | }); 33 | }; 34 | 35 | cancel = () => { 36 | if (this.state.isConfirm) { 37 | this.confirmCancel(); 38 | this.confirmCancel = noop; 39 | } 40 | }; 41 | 42 | ok = () => { 43 | this.confirmOk(); 44 | this.confirmOk = noop; 45 | }; 46 | 47 | alert(content, title, okText = '确定') { 48 | this.setState({ 49 | content: contentWrapper(content), 50 | isConfirm: false, 51 | show: true, 52 | title, 53 | okText 54 | }); 55 | return { 56 | ok: (fn) => { 57 | this.confirmOk = fn; 58 | } 59 | }; 60 | } 61 | 62 | confirm(content, title, okText = '确定', cancelText = '取消') { 63 | this.setState({ 64 | content: contentWrapper(content), 65 | isConfirm: true, 66 | show: true, 67 | title, 68 | okText, 69 | cancelText 70 | }); 71 | return this.confirmBind(); 72 | } 73 | 74 | confirmBind() { 75 | const result = { 76 | ok: (fn) => { 77 | this.confirmOk = fn; 78 | return result; 79 | }, 80 | cancel: (fn) => { 81 | this.confirmCancel = fn; 82 | return result; 83 | } 84 | }; 85 | return result; 86 | } 87 | 88 | render() { 89 | const modalProps = { 90 | defaultFooter: true, 91 | show: this.state.show, 92 | onToggle: this.toggle, 93 | title: this.state.title, 94 | content: this.state.content, 95 | cancel: this.cancel, 96 | ok: this.ok, 97 | noCancel: !this.state.isConfirm, 98 | okText: this.state.okText, 99 | cancelText: this.state.cancelText 100 | }; 101 | return ( 102 | 103 | ); 104 | } 105 | } 106 | 107 | const container = document.createElement('div'); 108 | document.body.appendChild(container); 109 | 110 | export default ReactDom.render( 111 | , 112 | container 113 | ); 114 | -------------------------------------------------------------------------------- /src/web/app/common/components/modal/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cx from 'classnames'; 3 | import './index.less'; 4 | 5 | class Modal extends Component { 6 | onCancel = () => { 7 | const { cancel, hideAfterCancel, onToggle } = this.props; 8 | if (typeof cancel === 'function') { 9 | cancel(); 10 | } 11 | if (hideAfterCancel) { 12 | onToggle(false); 13 | } 14 | }; 15 | 16 | onOk = () => { 17 | const { ok, hideAfterOk, onToggle } = this.props; 18 | if (typeof ok === 'function') { 19 | ok(); 20 | } 21 | if (hideAfterOk) { 22 | onToggle(false); 23 | } 24 | }; 25 | 26 | hide = (e) => { 27 | if (e.target.classList.value.indexOf('modals') < 0) { 28 | return; 29 | } 30 | this.props.onToggle(false); 31 | }; 32 | 33 | renderFooter() { 34 | if (this.props.footer) { 35 | return ( 36 |
37 | {this.props.footer} 38 |
39 | ); 40 | } 41 | if (this.props.defaultFooter) { 42 | return ( 43 |
44 | { 45 | this.props.noCancel ? null : ( 46 |
47 | {this.props.cancelText} 48 |
49 | ) 50 | } 51 | { 52 | this.props.noOk ? null : ( 53 |
54 | {this.props.okText} 55 |
56 | ) 57 | } 58 |
59 | ); 60 | } 61 | return null; 62 | } 63 | 64 | renderBody() { 65 | const { title, content } = this.props; 66 | return ( 67 |
68 | { 69 | title ? ( 70 |
71 | {title} 72 |
73 | ) : null 74 | } 75 |
76 | {content} 77 |
78 | {this.renderFooter()} 79 |
80 | ); 81 | } 82 | 83 | render() { 84 | const { show, className, children } = this.props; 85 | return ( 86 |
87 |
{ 90 | if (dom && dom.style) { 91 | // 先 top:0, left: 0 触发渲染,计算宽度 92 | dom.style.cssText = 'top: 0; left: 0'; 93 | dom.style.cssText = `top: 50%; left: 50%; margin: -${Math.floor(dom.offsetHeight / 2)}px 0 0 -${Math.floor(dom.offsetWidth / 2)}px`; 94 | } 95 | }} 96 | > 97 | {children || this.renderBody()} 98 |
99 |
100 | ); 101 | } 102 | } 103 | 104 | Modal.defaultProps = { 105 | className: '', 106 | show: false, 107 | hideAfterCancel: true, 108 | hideAfterOk: true, 109 | defaultFooter: true, 110 | noCancel: false, 111 | noOk: false, 112 | okText: '确定', 113 | cancelText: '取消' 114 | }; 115 | 116 | export default Modal; 117 | -------------------------------------------------------------------------------- /src/web/app/common/components/modal/index.less: -------------------------------------------------------------------------------- 1 | .modals { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | background: rgba(0,0,0, 0.5); 10 | z-index: 100; 11 | text-align: left; 12 | &.hide { 13 | display: none; 14 | } 15 | .modal { 16 | margin: 0 auto; 17 | border-radius: 5px; 18 | background: #fff; 19 | min-width: 300px; 20 | // max-width: 90vw; 21 | position: fixed; 22 | overflow: hidden; 23 | .title { 24 | line-height: 50px; 25 | background: #f9f9f9; 26 | position: relative; 27 | padding-left: 15px; 28 | .close { 29 | position: absolute; 30 | right: 16px; 31 | top: 14px; 32 | } 33 | } 34 | .content { 35 | text-align: left; 36 | padding: 10px; 37 | } 38 | .footer { 39 | border-top: 1px solid #f1f1f1; 40 | padding: 15px; 41 | text-align: right; 42 | .btn { 43 | margin-left: 15px; 44 | &:first-child { 45 | margin-left: 0; 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/web/app/common/components/select2-one/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cx from 'classnames'; 3 | import './index.less'; 4 | 5 | class SelectOne extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | show: false, 10 | inputValue: '', 11 | renderData: [] 12 | }; 13 | } 14 | 15 | componentWillReceiveProps(nextProps) { 16 | if (nextProps.show !== this.props.show) { 17 | this.setState({ show: nextProps.show }); 18 | } 19 | if (nextProps.value !== this.props.value) { 20 | this.setState({ 21 | inputValue: nextProps.value[nextProps.mapKey.text] 22 | }); 23 | } 24 | this.setState({ renderData: this.props.data }); 25 | } 26 | 27 | onChange(val) { 28 | let renderData = this.props.data.filter(item => 29 | item[this.props.mapKey.text].indexOf(val) > -1); 30 | if (!val) { 31 | renderData = this.props.data; 32 | } 33 | this.setState({ 34 | inputValue: val, 35 | renderData 36 | }); 37 | this.props.onChange(val); 38 | } 39 | 40 | onClick(val) { 41 | this.setState({ 42 | inputValue: val[this.props.mapKey.text] 43 | }); 44 | this.props.onSelect(val); 45 | } 46 | 47 | render() { 48 | const { mapKey, value } = this.props; 49 | return ( 50 |
51 | 52 | this.setState({ show: true })} 56 | onBlur={() => setTimeout(() => this.setState({ show: false }), 200)} 57 | value={this.state.inputValue} 58 | onChange={e => this.onChange(e.target.value.trim())} 59 | /> 60 |
    61 | { 62 | this.state.renderData.map(item => ( 63 |
  • this.onClick(item)} 67 | > 68 | {item[mapKey.text]} 69 |
  • 70 | )) 71 | } 72 |
73 |
74 | ); 75 | } 76 | } 77 | 78 | SelectOne.defaultProps = { 79 | className: '', 80 | show: false, 81 | data: [], 82 | onChange: () => {}, 83 | onSelect: () => {}, 84 | value: {}, 85 | mapKey: { text: 'text', value: 'value' } 86 | }; 87 | 88 | export default SelectOne; 89 | -------------------------------------------------------------------------------- /src/web/app/common/components/select2-one/index.less: -------------------------------------------------------------------------------- 1 | .select { 2 | cursor: pointer; 3 | word-wrap: break-word; 4 | position: relative; 5 | line-height: 1em; 6 | white-space: normal; 7 | outline: 0; 8 | display: inline-block; 9 | color: rgba(0,0,0,.87); 10 | box-shadow: none; 11 | padding: 5px 0; 12 | transition: box-shadow .1s ease, width .1s ease, -webkit-box-shadow .1s ease; 13 | text-align: left; 14 | .icon { 15 | position: absolute; 16 | right: 10px; 17 | top: 10px; 18 | cursor: pointer; 19 | } 20 | .s-search { 21 | border: none; 22 | border-bottom: 1px solid rgba(34,36,38,.15); 23 | } 24 | .s-list { 25 | width: 100%; 26 | position: absolute; 27 | top: 25px; 28 | z-index: 3; 29 | background: #fff; 30 | &.hide { 31 | display: none; 32 | } 33 | &-item { 34 | padding: 5px 10px; 35 | width: auto; 36 | &.active { 37 | background: #07b5e9; 38 | color: #fff; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/web/app/common/styles/icon.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'iconfont'; 3 | src: url('//s.qunarzz.com/qtalk-web/0.0.8/qtalk_web.eot'); /* IE9*/ 4 | src: url('//s.qunarzz.com/qtalk-web/0.0.8/qtalk_web.woff') format('woff'), /* chrome、firefox */ 5 | url('//s.qunarzz.com/qtalk-web/0.0.8/qtalk_web.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ 6 | url('//s.qunarzz.com/qtalk-web/0.0.8/qtalk_web.svg#iconfont') format('svg'); /* iOS 4.1- */ 7 | } 8 | // ../../../../assets/images/ 9 | .icon { 10 | background: url(../../../../assets/index/c463d610f0e32702.png) no-repeat; 11 | background-size: 487px 462px; 12 | display: inline-block; 13 | vertical-align: middle; 14 | 15 | // 全局 16 | &.close { 17 | background-position: -150px -432px; 18 | width: 25px; 19 | height: 25px; 20 | } 21 | 22 | &.close-1 { 23 | background-position: -249px -436px; 24 | width: 14px; 25 | height: 14px; 26 | } 27 | 28 | &.sex-1, &.sex-2 { 29 | width: 16px; 30 | height: 16px; 31 | background-position: -384px -304px; 32 | } 33 | &.sex-2 { 34 | background-position: -368px -304px; 35 | } 36 | 37 | &.checked { 38 | background-position: -331px -432px; 39 | width: 20px; 40 | height: 20px; 41 | &.on { 42 | background-position: -467px -330px; 43 | } 44 | } 45 | 46 | &.arrow-down { 47 | background-position: -477px -65px; 48 | width: 10px; 49 | height: 10px; 50 | } 51 | 52 | &.arrow-up { 53 | background-position: -477px -55px; 54 | width: 10px; 55 | height: 10px; 56 | } 57 | 58 | // #panel 59 | &.panel-menu { 60 | width: 30px; 61 | height: 30px; 62 | background-position: -434px -398px; 63 | } 64 | 65 | &.panel-search { 66 | position: absolute; 67 | top: 1px; 68 | width: 30px; 69 | height: 30px; 70 | background-position: -60px -432px; 71 | } 72 | 73 | &.panel-chat { 74 | width: 35px; 75 | height: 35px; 76 | background-position: -150px -96px; 77 | 78 | &.active { 79 | background-position: -185px -96px; 80 | } 81 | } 82 | 83 | &.panel-friends { 84 | width: 35px; 85 | height: 35px; 86 | background-position: -220px -96px; 87 | 88 | &.active { 89 | background-position: -304px -246px; 90 | } 91 | } 92 | 93 | &.panel-reddot { 94 | position: absolute; 95 | top: -6px; 96 | right: -6px; 97 | color: #fff; 98 | font-style: normal; 99 | font-size: 12px; 100 | text-align: center; 101 | background-position: -451px -380px; 102 | width: 22px; 103 | height: 16px; 104 | } 105 | 106 | // #chat 107 | &.chat-header-members-add { 108 | background-position: -422px -55px; 109 | width: 55px; 110 | height: 55px; 111 | } 112 | 113 | &.chat-message-empty-logo { 114 | background-position: -96px -150px; 115 | width: 100px; 116 | height: 94px; 117 | opacity: 0.5; 118 | } 119 | 120 | &.chat-footer-face { 121 | background-position: -404px -398px; 122 | width: 30px; 123 | height: 30px; 124 | } 125 | 126 | &.chat-footer-file { 127 | background-position: -120px -432px; 128 | width: 30px; 129 | height: 30px; 130 | } 131 | 132 | &.add-user-search { 133 | background-position: -90px -432px; 134 | width: 30px; 135 | height: 30px; 136 | } 137 | &.owner { 138 | background-position: -372px -433px; 139 | width: 15px; 140 | height: 24px; 141 | } 142 | &.admin { 143 | background-position: -357px -433px; 144 | width: 15px; 145 | height: 24px; 146 | } 147 | &.arrow-d { 148 | background-position: -393px -440px; 149 | width: 10px; 150 | height: 10px; 151 | } 152 | &.arrow-r { 153 | background-position: -407px -440px; 154 | width: 10px; 155 | height: 10px; 156 | } 157 | &.card-chat { 158 | width: 22px; 159 | height: 22px; 160 | background-position: -223px -432px; 161 | } 162 | &.male { 163 | width: 24px; 164 | height: 24px; 165 | margin-left: 5px; 166 | background-position: -246px -356px; 167 | } 168 | &.female { 169 | width: 24px; 170 | height: 24px; 171 | margin-left: 5px; 172 | background-position: -246px -380px; 173 | } 174 | &.people3 { 175 | width: 32px; 176 | height: 24px; 177 | margin-right: 5px; 178 | background-position: -243px -333px; 179 | } 180 | } 181 | .iconfont{ 182 | font-family: "iconfont" !important; 183 | font-size: 20px; 184 | font-style: normal; 185 | -webkit-font-smoothing: antialiased; 186 | -webkit-text-stroke-width: 0.2px; 187 | -moz-osx-font-smoothing: grayscale; 188 | 189 | &.small { 190 | font-size: 16px; 191 | } 192 | &.large { 193 | font-size: 24px; 194 | } 195 | &.x-large { 196 | font-size: 28px; 197 | } 198 | &.huge { 199 | font-size: 32px; 200 | } 201 | 202 | &.camel:before { 203 | content: '\f3eb'; 204 | } 205 | &.double-arrow-up:before { 206 | content: '\e044'; 207 | } 208 | &.double-arrow-down:before { 209 | content: '\e043'; 210 | } 211 | &.nike:before { 212 | content: '\f3f6'; 213 | } 214 | &.nike2:before { 215 | content: '\f472'; 216 | } 217 | &.circle:before { 218 | content: '\f438'; 219 | } 220 | &.plus:before { 221 | content: '\f016'; 222 | } 223 | &.more:before { 224 | content: '\e1ba'; 225 | } 226 | &.search:before { 227 | content: '\f3b9'; 228 | } 229 | &.message:before { 230 | content: '\f3e4'; 231 | } 232 | &.message-empty:before { 233 | content: '\f0f3'; 234 | } 235 | &.card:before { 236 | content: '\f4ce'; 237 | } 238 | &.card-empty:before { 239 | content: '\f0de'; 240 | } 241 | &.logout:before { 242 | content: '\f1bb'; 243 | } 244 | &.people:before { 245 | content: '\f4c6'; 246 | } 247 | &.smile:before { 248 | content: '\f3be'; 249 | } 250 | &.folder:before { 251 | content: '\e211'; 252 | } 253 | &.people3:before { 254 | content: '\f10f'; 255 | } 256 | &.arrow-up:before { 257 | content: '\f3ca'; 258 | } 259 | &.arrow-down:before { 260 | content: '\f3cb'; 261 | } 262 | &.arrow-left:before { 263 | content: '\f3cd'; 264 | } 265 | &.arrow-right:before { 266 | content: '\f3cc'; 267 | } 268 | &.mail:before { 269 | content: '\f493'; 270 | } 271 | &.female:before { 272 | content: '\f478'; 273 | } 274 | &.male:before { 275 | content: '\f47c'; 276 | } 277 | &.edit:before { 278 | content: '\f4cb'; 279 | } 280 | &.empty:before { 281 | content: '\f0f4'; 282 | } 283 | &.clock:before { 284 | content: '\e158'; 285 | } 286 | &.tebu:before { 287 | content: '\f093'; 288 | } 289 | &.minus:before { 290 | content: '\f436'; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/web/app/common/styles/index.less: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | height: 100%; 4 | } 5 | // ../../../../assets/index/ 6 | body { 7 | font-family: Helvetica Neue, Helvetica, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 8 | background: url(../../../../assets/index/02d9a1cfacacfd02.png) no-repeat 50%; 9 | background-size: cover; 10 | } 11 | 12 | a.btn { 13 | text-decoration: none; 14 | } 15 | 16 | .red { 17 | color: red; 18 | } 19 | 20 | .btn { 21 | display: inline-block; 22 | border: 1px solid #c1c1c1; 23 | border-radius: 4px; 24 | padding: 3px 20px; 25 | font-size: 14px; 26 | cursor: pointer; 27 | } 28 | 29 | .btn-default { 30 | background-color: #c9c9c9; 31 | cursor: default; 32 | } 33 | 34 | .btn-default, 35 | .btn-primary { 36 | color: #fff; 37 | border: 1px solid #fff; 38 | } 39 | 40 | .btn-primary { 41 | background: #3caf36; 42 | border: 1px solid #3caf36; 43 | } 44 | 45 | .btn-smart { 46 | border-radius: 20px; 47 | } 48 | 49 | .btn-send { 50 | border: 0; 51 | background-color: #45CF8E; 52 | color: #fff; 53 | padding-left: 20px; 54 | padding-right: 20px; 55 | } 56 | 57 | .btn-send:hover { 58 | opacity:0.5; 59 | } 60 | 61 | .clearfix::after { 62 | content: " "; 63 | visibility: hidden; 64 | display: block; 65 | height: 0; 66 | clear: both; 67 | } 68 | 69 | #app { 70 | position: relative; 71 | top: 50%; 72 | margin-top: -300px; 73 | } 74 | 75 | #main { 76 | width: 976px; 77 | height: 580px; 78 | margin: 0 auto; 79 | border-radius: 3px; 80 | overflow: hidden; 81 | position: relative; 82 | border: 1px solid #e6e6e6; 83 | } 84 | 85 | #phone_main { 86 | width: 100vw; 87 | height: 100vh; 88 | margin: 0 auto; 89 | overflow: hidden; 90 | position: fixed; 91 | top: 0px; 92 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 93 | } 94 | 95 | @media (max-height: 813px) { 96 | #app { 97 | position: static; 98 | top: 0; 99 | margin-top: 0; 100 | } 101 | } 102 | 103 | .bg { 104 | &.b0 { 105 | background-image: url(../../../../assets/index/33bfeb065105c002.png); 106 | } 107 | &.b1 { 108 | background-image: url(../../../../assets/index/536fa0d98d69d102.png); 109 | } 110 | &.b2 { 111 | background-image: url(../../../../assets/index/2fb5c91b6d9c4f02.png); 112 | } 113 | &.b3 { 114 | background-image: url(../../../../assets/index/6ea30b7cd9472a02.png); 115 | } 116 | &.b4 { 117 | background-image: url(../../../../assets/index/a4ae5891171faf02.png); 118 | } 119 | &.b5 { 120 | background-image: url(../../../../assets/index/f883b0fcd93f602.png); 121 | } 122 | &.b6 { 123 | background-image: url(../../../../assets/index/9fd2391a3cf49702.png); 124 | } 125 | &.b7 { 126 | background-image: url(../../../../assets/index/4d709e27fa18aa02.png); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/web/app/common/styles/login.less: -------------------------------------------------------------------------------- 1 | @mainColor: #45CF8E; 2 | 3 | .login-wrap { 4 | position: fixed; 5 | top: 50%; 6 | left: 50%; 7 | width: 304px; 8 | margin: -200px 0 0 -152px; 9 | border-radius: 4px; 10 | background: #fff; 11 | .login { 12 | width: 304px; 13 | height: 360px; 14 | border-radius: 4px; 15 | background: #fff; 16 | box-shadow: 0 4px 5px 0 rgba(0,0,0,0.20); 17 | text-align: center; 18 | padding-top: 40px; 19 | position: relative; 20 | overflow: hidden; 21 | 22 | .status { 23 | padding-top: 10px; 24 | font-size: 12px; 25 | color: #616161; 26 | } 27 | .cancel { 28 | width: 208px; 29 | height: 36px; 30 | line-height: 36px; 31 | margin: 26px auto 0 auto; 32 | border: 1px solid #E6E6E6; 33 | border-radius: 2px; 34 | font-size: 16px; 35 | color: #616161; 36 | cursor: pointer; 37 | } 38 | .avatar { 39 | width: 100px; 40 | height: 100px; 41 | border-radius: 50%; 42 | margin: auto; 43 | position: relative; 44 | &.green { 45 | background: @mainColor; 46 | } 47 | .iconfont.camel { 48 | font-size: 60px; 49 | position: absolute; 50 | top: 50%; 51 | left: 50%; 52 | margin-left: -30px; 53 | margin-top: -48px; 54 | color: #fff; 55 | } 56 | img { 57 | width: 100px; 58 | height: 100px; 59 | border-radius: 50%; 60 | } 61 | } 62 | .form { 63 | margin-top: 30px; 64 | .account-info { 65 | width: 208px; 66 | margin: 0 auto; 67 | border: 1px solid #E6E6E6; 68 | border-radius: 2px; 69 | .item { 70 | background: rgba(255, 255, 255, 0.3); 71 | width: 208px; 72 | margin: 0 auto; 73 | 74 | &.bb { 75 | border-bottom: 1px solid #E6E6E6; 76 | } 77 | .input { 78 | width: 208px; 79 | height: 36px; 80 | line-height: 36px; 81 | border: 0; 82 | font-size: 14px; 83 | color: #333; 84 | background: none; 85 | } 86 | .domain { 87 | width: 80px; 88 | height: 36px; 89 | line-height: 36px; 90 | color: #BDBDBD; 91 | font-size: 14px; 92 | float: right; 93 | margin-right: 10px; 94 | overflow: hidden; 95 | text-overflow: ellipsis; 96 | white-space: nowrap; 97 | word-wrap: normal; 98 | } 99 | } 100 | } 101 | 102 | .btn-login { 103 | border-radius: 2px; 104 | display: block; 105 | width: 208px; 106 | background: @mainColor; 107 | height: 36px; 108 | line-height: 36px; 109 | margin: 16px auto 0 auto; 110 | color: #fff; 111 | font-size: 16px; 112 | cursor: pointer; 113 | 114 | &.disabled { 115 | background: #9c9c9c; 116 | color: #d4d4d4; 117 | cursor: default; 118 | } 119 | } 120 | 121 | } 122 | .domain-choose { 123 | position: absolute; 124 | bottom: 16px; 125 | left: 50%; 126 | margin-left: -9px; 127 | cursor: pointer; 128 | .iconfont.double-arrow-down{ 129 | font-size: 18px; 130 | color: #9E9E9E; 131 | } 132 | .iconfont.double-arrow-up{ 133 | font-size: 18px; 134 | color: #9E9E9E; 135 | } 136 | } 137 | } 138 | .domain-block { 139 | padding: 17px 0 43px 48px; 140 | font-size: 12px; 141 | .domain-list { 142 | max-height: 60px; 143 | overflow-y: scroll; 144 | .item { 145 | margin-bottom: 7px; 146 | cursor: pointer; 147 | .iconfont.circle { 148 | display: inline-block; 149 | font-size: 16px; 150 | color: #9E9E9E; 151 | vertical-align: middle; 152 | } 153 | .iconfont.nike { 154 | display: inline-block; 155 | font-size: 16px; 156 | color: @mainColor; 157 | vertical-align: middle; 158 | } 159 | span { 160 | display: inline-block; 161 | margin-left: 10px; 162 | height: 16px; 163 | } 164 | 165 | .input { 166 | position: relative; 167 | display: inline-block; 168 | width: 148px; 169 | .check { 170 | position: absolute; 171 | right: 0; 172 | top: 0; 173 | width: 26px; 174 | height: 26px; 175 | background: #E6E6E6; 176 | border-radius: 2px; 177 | &.right { 178 | background: @mainColor; 179 | } 180 | .iconfont.nike2 { 181 | font-size: 14px; 182 | color: #fff; 183 | position: absolute; 184 | top: 50%; 185 | left: 50%; 186 | margin-left: -7px; 187 | margin-top: -11px; 188 | } 189 | } 190 | input { 191 | width: 107px; 192 | height: 24px; 193 | border: 1px solid #E6E6E6; 194 | border-radius: 2px; 195 | padding-left: 5px; 196 | margin-left: 10px; 197 | } 198 | } 199 | 200 | } 201 | } 202 | .domain-add-btn { 203 | width: 24px; 204 | padding-left: 17px; 205 | position: absolute; 206 | cursor: pointer; 207 | right: 48px; 208 | bottom: 16px; 209 | font-size: 12px; 210 | height: 22px; 211 | line-height: 22px; 212 | color: #39B87C; 213 | .iconfont.plus { 214 | font-size: 14px; 215 | color: #39B87C; 216 | position: absolute; 217 | left: 0; 218 | } 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/web/app/common/styles/reset.less: -------------------------------------------------------------------------------- 1 | html { 2 | -ms-text-size-adjust: 100%; 3 | -webkit-text-size-adjust: 100%; 4 | } 5 | 6 | body { 7 | -webkit-font-smoothing: antialiased; 8 | line-height: 1.6; 9 | } 10 | 11 | a, 12 | button, 13 | input, 14 | textarea { 15 | outline: 0; 16 | } 17 | 18 | body, 19 | dd, 20 | dl, 21 | fieldset, 22 | h1, 23 | h2, 24 | h3, 25 | h4, 26 | h5, 27 | h6, 28 | ol, 29 | p, 30 | textarea, 31 | ul { 32 | margin: 0; 33 | } 34 | 35 | fieldset, 36 | input, 37 | legend, 38 | textarea { 39 | padding: 0; 40 | } 41 | 42 | ol, 43 | ul { 44 | padding-left: 0; 45 | list-style-type: none; 46 | } 47 | 48 | a img, 49 | fieldset { 50 | border: 0; 51 | } 52 | 53 | article, 54 | aside, 55 | details, 56 | figcaption, 57 | figure, 58 | footer, 59 | header, 60 | hgroup, 61 | main, 62 | nav, 63 | section, 64 | summary { 65 | display: block; 66 | } 67 | 68 | audio, 69 | canvas, 70 | video { 71 | display: inline-block; 72 | } 73 | 74 | audio:not([controls]) { 75 | display: none; 76 | height: 0; 77 | } 78 | 79 | [hidden] { 80 | display: none; 81 | } 82 | 83 | svg:not(:root) { 84 | overflow: hidden; 85 | } 86 | 87 | figure { 88 | margin: 0; 89 | } 90 | 91 | button, 92 | input, 93 | select, 94 | textarea { 95 | font-family: inherit; 96 | font-size: 100%; 97 | margin: 0; 98 | } 99 | 100 | button, 101 | select { 102 | text-transform: none; 103 | } 104 | 105 | button, 106 | html input[type=button], 107 | input[type=reset], 108 | input[type=submit] { 109 | cursor: pointer; 110 | -webkit-appearance: button; 111 | } 112 | 113 | button[disabled], 114 | html input[disabled] { 115 | cursor: default; 116 | } 117 | 118 | input[type=checkbox], 119 | input[type=radio] { 120 | box-sizing: border-box; 121 | padding: 0; 122 | } 123 | 124 | input[type=search] { 125 | box-sizing: content-box; 126 | -moz-box-sizing: content-box; 127 | -webkit-appearance: textfield; 128 | -webkit-box-sizing: content-box; 129 | } 130 | 131 | // input[type=search]::-webkit-search-cancel-button, 132 | // input[type=search]::-webkit-search-decoration { 133 | // -webkit-appearance: none; 134 | // } 135 | 136 | button::-moz-focus-inner, 137 | input::-moz-focus-inner { 138 | border: 0; 139 | padding: 0; 140 | } 141 | 142 | textarea { 143 | overflow: auto; 144 | vertical-align: top; 145 | resize: none; 146 | } 147 | 148 | input:-webkit-autofill, 149 | select:-webkit-autofill, 150 | textarea:-webkit-autofill { 151 | box-shadow: inset 0 0 0 1000px #fff; 152 | -moz-box-shadow: inset 0 0 0 1000px #fff; 153 | -webkit-box-shadow: inset 0 0 0 1000px #fff; 154 | } 155 | 156 | select { 157 | border-radius: 0; 158 | -webkit-border-radius: 0; 159 | } 160 | -------------------------------------------------------------------------------- /src/web/app/common/utils/namespace-actions.js: -------------------------------------------------------------------------------- 1 | import { createActions } from 'redux-actions'; 2 | 3 | function appendNs(namespace, ...args) { 4 | const actions = createActions.apply(this, args); 5 | Object.keys(actions).forEach((key) => { 6 | const fn = actions[key]; 7 | actions[key] = (...actionArgs) => { 8 | const action = fn.apply(this, actionArgs); 9 | action.type = `${namespace}/${action.type}`; 10 | return action; 11 | }; 12 | }); 13 | args.forEach((name) => { 14 | actions[name] = `${namespace}/${name}`; 15 | }); 16 | return actions; 17 | } 18 | 19 | export default namespace => (...args) => (appendNs(namespace, ...args)); 20 | -------------------------------------------------------------------------------- /src/web/app/common/utils/namespace-reducers.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | 3 | function appendNs(namespace, reducerMap, initialState) { 4 | const reducerMapWithNs = Object.keys(reducerMap).reduce((result, key) => { 5 | const reducer = reducerMap[key]; 6 | result[`${namespace}/${key}`] = reducer; 7 | return result; 8 | }, {}); 9 | return handleActions(reducerMapWithNs, initialState); 10 | } 11 | 12 | export default namespace => (...args) => (appendNs(namespace, ...args)); 13 | -------------------------------------------------------------------------------- /src/web/app/common/utils/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Prompt, 4 | Link, 5 | BrowserRouter, 6 | HashRouter, 7 | Route 8 | } from 'react-router-dom'; 9 | 10 | const MyRoute = ({ 11 | component: Component, 12 | exact, 13 | path, 14 | strict, 15 | ...rest 16 | }) => ( 17 | () 21 | } 22 | /> 23 | ); 24 | 25 | export default { 26 | Prompt, 27 | Link, 28 | BrowserRouter, 29 | HashRouter, 30 | Route: MyRoute 31 | }; 32 | -------------------------------------------------------------------------------- /src/web/app/index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | startalk 16 | 17 | 18 |
19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/web/app/pages/index/actions.js: -------------------------------------------------------------------------------- 1 | import nsActions from '../../common/utils/namespace-actions'; 2 | 3 | const createActions = nsActions('INDEX'); 4 | 5 | export default createActions( 6 | 'CHANGE_CHAT_FIELD', 7 | 'SET_CHAT_FIELD', 8 | 'SET_USER_INFO', 9 | // 会话 10 | 'SET_SESSION_LIST', 11 | 'SET_SESSION_TOP', 12 | 'MOVE_SESSION', 13 | 'CLEAR_SESSION_CNT', 14 | 'SET_LAST_SESSION_MESSAGE', 15 | 'SET_CURRENT_SESSION_USERS', 16 | 'REMOVE_CURRENT_SESSION_USER', 17 | // 'MERGE_CURRENT_SESSION_USER', 18 | 'SET_CURRENT_SESSION', 19 | 'REMOVE_SESSION', 20 | // 右键菜单 21 | 'SET_CONTENT_MENU', 22 | // 消息 23 | 'SET_MESSAGE', 24 | 'APPEND_MESSAGE', 25 | 'CLEAR_MESSAGE', 26 | 'SET_MESSAGE_READ', 27 | 'REVOKE_MESSAGE', 28 | // 卡片弹层信息 29 | 'SET_MODAL_USER_CARD', 30 | 'SET_MODAL_GROUP_CARD', 31 | // 通讯录 32 | 'SET_FRIENDS_MUCS', 33 | 'SET_FRIENDS_USERS', 34 | // 发起会话 35 | 'SET_MEMBERS_INFO', 36 | // 导航 37 | 'SET_STARTALK_NAV', 38 | 'SET_PUBLIC_KEY' 39 | ); 40 | -------------------------------------------------------------------------------- /src/web/app/pages/index/consts.js: -------------------------------------------------------------------------------- 1 | const atTips = { 2 | single: '你被@了一次', 3 | all: '@全体成员' 4 | }; 5 | 6 | const treeKey = 'struct'; 7 | export { 8 | atTips, 9 | treeKey 10 | }; 11 | -------------------------------------------------------------------------------- /src/web/app/pages/index/entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | // import { createStore, applyMiddleware } from 'redux'; 5 | import { createStore } from 'redux'; 6 | // import createSagaMiddleware from 'redux-saga'; 7 | import reducer from './reducer'; 8 | // import saga from './saga'; 9 | import Page from './'; 10 | import './import-less'; 11 | 12 | const root = document.getElementById('app'); 13 | // const sagaMiddleware = createSagaMiddleware(); 14 | // const store = createStore(reducer, applyMiddleware(sagaMiddleware)); 15 | const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); 16 | // sagaMiddleware.run(saga); 17 | 18 | const render = (Entry) => { 19 | ReactDOM.render( 20 | 21 | 22 | , 23 | root 24 | ); 25 | }; 26 | 27 | render(Page); 28 | 29 | if (module.hot) { 30 | module.hot.accept(['.'], () => { 31 | eslint-disable-next-line 32 | render(require('.')); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/web/app/pages/index/entry.settings.js: -------------------------------------------------------------------------------- 1 | const { QTALK_SDK_URL } = process.env; 2 | export default { 3 | QTALK_SDK_URL 4 | }; 5 | -------------------------------------------------------------------------------- /src/web/app/pages/index/import-less.js: -------------------------------------------------------------------------------- 1 | import '../../common/styles/reset.less'; 2 | import '../../common/styles/icon.less'; 3 | import '../../common/styles/animate.less'; 4 | 5 | import '../../common/styles/index.less'; 6 | import '../../common/styles/panel.less'; 7 | import '../../common/styles/chat.less'; 8 | import '../../common/styles/login.less'; 9 | import '../../common/styles/modals.less'; 10 | import '../../common/styles/jstree.css'; 11 | 12 | import '../../common/styles/phone/panel.less'; 13 | import '../../common/styles/phone/modals.less'; 14 | import '../../common/styles/phone/chat.less'; 15 | 16 | const styleContent = ` 17 | ::-webkit-scrollbar{ 18 | width: 6px; 19 | background-color: #F5F5F5; 20 | } 21 | ::-webkit-scrollbar-thumb{ 22 | background-color: rgba(50,50,50,.3); 23 | } 24 | ::-webkit-scrollbar-track{ 25 | background-color: rgba(50,50,50,.5); 26 | } 27 | `; 28 | 29 | const doc = window.document; 30 | const platform = window.navigator.platform.toLowerCase(); 31 | if (platform.indexOf('mac') === -1) { 32 | if (doc.all) { 33 | window.qtalkWebStyle = styleContent; 34 | // eslint-disable-next-line 35 | doc.createStyleSheet('javascript:qtalkWebStyle'); 36 | } else { 37 | const style = doc.createElement('style'); 38 | style.type = 'text/css'; 39 | style.innerHTML = styleContent; 40 | doc.getElementsByTagName('HEAD').item(0).appendChild(style); 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/web/app/pages/index/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: your name 4 | * @Date: 2019-08-06 11:24:02 5 | * @LastEditTime: 2019-08-20 17:25:02 6 | * @LastEditors: chaos.dong 7 | */ 8 | import React, { Component } from 'react'; 9 | import { connect } from 'react-redux'; 10 | import axios from 'axios'; 11 | // import moment from 'moment'; 12 | import actions from './actions'; 13 | import Login from './ui/login'; 14 | import Chat from './ui/chat'; 15 | import Panel from './ui/panel'; 16 | import UserCard from './ui/modal/userCard'; 17 | import GroupCard from './ui/modal/groupCard'; 18 | import ContentMenu from './ui/modal/contentmenu'; 19 | import Members from './ui/modal/members'; 20 | 21 | import PhonePanel from './phone-ui/panel'; 22 | import PhoneUserCard from './phone-ui/modal/userCard'; 23 | import PhoneGroupCard from './phone-ui/modal/groupCard'; 24 | import PhoneMembers from './phone-ui/modal/members'; 25 | import PhoneChat from './phone-ui/chat'; 26 | import PhoneContentMenu from './phone-ui/modal/contentmenu'; 27 | 28 | import { treeKey } from './consts'; 29 | import sdk from './sdk'; 30 | 31 | const webConfig = { 32 | fileurl: startalkNav.baseaddess && startalkNav.baseaddess.fileurl 33 | } 34 | const users = {}; 35 | const usersName = {}; 36 | const bu = []; 37 | 38 | @connect( 39 | state => ({ 40 | connectStatus: state.getIn(['chat', 'connectStatus']), 41 | nav: state.getIn(['nav']) 42 | }), 43 | actions 44 | ) 45 | export default class Page extends Component { 46 | constructor(props) { 47 | super(props) 48 | } 49 | 50 | componentDidMount() { 51 | sdk.ready(async () => { 52 | const res = await sdk.getCompanyStruct(); 53 | if (res.ret) { 54 | const treeData = this.createTree(res.data); 55 | // res.data 处理成jstree结构 56 | this.props.setChatField({ 57 | companyStruct: this.genTreeData(treeData || [], treeKey), 58 | companyUsers: users, 59 | companyUsersName: usersName 60 | }); 61 | } 62 | }); 63 | } 64 | 65 | // 把平级数组转化为树状结构 66 | createTree(data) { 67 | // 控制每个人的可视数据 68 | const visible = []; 69 | data.forEach((item) => { 70 | if (item.visibleFlag === true) { 71 | visible.push(item); 72 | } 73 | }); 74 | const treeArray = []; 75 | visible.forEach((item) => { 76 | const d = item.D; 77 | let floor = []; 78 | floor = d.split('/').slice(1); 79 | let nowArray = treeArray; 80 | for (let i = 0; i < floor.length; i++) { 81 | const index = this.checkIfExist(nowArray, floor[i]); 82 | const vote = {}; 83 | vote.N = item.N; 84 | vote.U = item.U; 85 | vote.S = item.S; 86 | if (index !== false && i !== floor.length - 1) { 87 | nowArray = nowArray[index].SD; 88 | } else if (index !== false && i === floor.length - 1) { 89 | nowArray[index].UL.push(vote); 90 | } else if (index === false && i === floor.length - 1) { 91 | nowArray.push({ 92 | D: floor[i], 93 | UL: [vote], 94 | SD: [] 95 | }); 96 | } else { 97 | nowArray.push({ 98 | D: floor[i], 99 | UL: [], 100 | SD: [] 101 | }); 102 | nowArray = nowArray[nowArray.length - 1].SD; 103 | } 104 | } 105 | }); 106 | return treeArray; 107 | } 108 | 109 | // 判断当前数组中有没有传进去的name的这个部门 110 | checkIfExist(array = [], name = '') { 111 | let flag = false; 112 | if (array.length === 0) { 113 | flag = false; 114 | } else { 115 | for (let i = 0; i < array.length; i++) { 116 | if (array[i].D === name) { 117 | flag = i; 118 | break; 119 | } 120 | } 121 | } 122 | return flag; 123 | } 124 | 125 | genTreeData(data, id) { 126 | const ret = []; 127 | data.length && data.forEach((item, idx) => { 128 | const key = `${id}-${idx}`; 129 | bu.push(item.D); 130 | const ul = []; 131 | item.UL.forEach((u) => { 132 | users[u.U] = { 133 | bu: bu.slice(0), 134 | U: u.U, 135 | N: u.N, 136 | text: `${u.N}[${u.U}]`, 137 | icon: webConfig.fileurl + '/file/v2/download/8c9d42532be9316e2202ffef8fcfeba5.png', 138 | key: `${key}-${u.U}` 139 | }; 140 | usersName[u.N] = { 141 | bu: bu.slice(0), 142 | U: u.U, 143 | N: u.N, 144 | text: `${u.N}[${u.U}]`, 145 | icon: webConfig.fileurl + '/file/v2/download/8c9d42532be9316e2202ffef8fcfeba5.png', 146 | key: `${key}-${u.U}` 147 | }; 148 | usersName[u.U] = { 149 | bu: bu.slice(0), 150 | U: u.U, 151 | N: u.N, 152 | text: `${u.N}[${u.U}]`, 153 | icon: webConfig.fileurl + '/file/v2/download/8c9d42532be9316e2202ffef8fcfeba5.png', 154 | key: `${key}-${u.U}` 155 | }; 156 | ul.push(users[u.U]); 157 | }); 158 | ret.push({ 159 | text: item.D, 160 | children: [].concat(ul, this.genTreeData(item.SD, key)), 161 | key 162 | }); 163 | bu.pop(); 164 | }); 165 | return ret; 166 | } 167 | 168 | render() { 169 | const { connectStatus } = this.props; 170 | const phone = /Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent); 171 | 172 | if (connectStatus === 'success' && !phone) { 173 | Notification.requestPermission();//用户是否同意显示通知 174 | return ( 175 |
176 | 177 | 178 | 179 | 180 | 181 | 182 |
183 | ); 184 | } else if(connectStatus === 'success' && phone) { 185 | return ( 186 |
187 | 188 | 189 | 190 | 191 | 192 | 193 |
194 | ) 195 | } 196 | return ( 197 | 198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/chat/emotions.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cls from 'classnames'; 3 | 4 | export default class Emotions extends Component { 5 | constructor() { 6 | super(); 7 | const emotions = Object.assign({}, window.QtalkSDK.emotions[0]); 8 | const { faces } = emotions; 9 | delete emotions.faces; 10 | const pageSize = 32; 11 | this.state = { 12 | showMenu: false, 13 | emotions, 14 | faces, 15 | pageCount: Math.ceil(faces.length / pageSize), 16 | pageSize, 17 | page: 0 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | } 23 | 24 | onChangePage = (e) => { 25 | this.setState({ page: parseInt(e.target.getAttribute('data-page'), 10) }); 26 | }; 27 | 28 | time = null; 29 | 30 | showMenu = (b) => { 31 | clearTimeout(this.time); 32 | this.time = setTimeout(() => { 33 | this.setState({ showMenu: b }); 34 | }, 100); 35 | }; 36 | 37 | render() { 38 | const { 39 | showMenu, faces, emotions, pageCount, page, pageSize 40 | } = this.state; 41 | let i = 0; 42 | const pageEl = []; 43 | let data = []; 44 | if (showMenu) { 45 | data = faces.slice(page * pageSize, (page * pageSize) + pageSize); 46 | while (i < pageCount) { 47 | pageEl.push(); 53 | i += 1; 54 | } 55 | } 56 | return ( 57 |
{ this.showMenu(false); }} 60 | onClick={() => { this.showMenu(true); }} 61 | > 62 | 63 |
68 |
    69 | { 70 | data.map((face, index) => ( 71 |
  • { this.props.selected(emotions, face); }} 73 | key={`face-${index + 1}`} 74 | > 75 | 76 |
  • 77 | )) 78 | } 79 |
80 |
{pageEl}
81 |
82 |
83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/chat/empty.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Empty extends Component { 4 | componentDidMount() { 5 | } 6 | 7 | render() { 8 | return ( 9 |
10 | 21 |
22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/chat/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import actions from '../../actions'; 4 | import Header from './header'; 5 | import Message from './message'; 6 | import Footer from './footer'; 7 | import Empty from './empty'; 8 | import GroupCard from './groupCard'; 9 | import UserCard from './userCard'; 10 | // import sdk from '../../sdk'; 11 | 12 | @connect( 13 | state => ({ 14 | currentSession: state.getIn(['chat', 'currentSession']), 15 | switchIndex: state.getIn(['chat', 'switchIndex']), 16 | currentFriend: state.getIn(['chat', 'currentFriend']), 17 | isChat: state.getIn(['chat', 'isChat']), 18 | isCard: state.getIn(['chat', 'isCard']) 19 | }), 20 | actions 21 | ) 22 | export default class Chat extends Component { 23 | constructor() { 24 | super(); 25 | this.state = { 26 | messageScrollToBottom: false 27 | }; 28 | } 29 | 30 | messageScrollToBottom = () => { 31 | this.setState({ 32 | messageScrollToBottom: true 33 | }, () => { 34 | this.setState({ 35 | messageScrollToBottom: false 36 | }); 37 | }); 38 | }; 39 | 40 | render() { 41 | const { 42 | currentSession, 43 | switchIndex, 44 | currentFriend, 45 | isChat, 46 | isCard 47 | } = this.props; 48 | const currId = currentSession.get('user') || currentSession.get('groupname'); 49 | const currFri = currentFriend.get('user'); 50 | if (switchIndex === 'chat' && currId && isChat) { 51 | return ( 52 |
53 |
54 | 55 |
56 |
57 | ); 58 | } else if (switchIndex === 'friends' && currFri && isCard) { 59 | return ( 60 |
61 | { 62 | currentFriend.get('mFlag') === '2' ? 63 | : 64 | 65 | } 66 |
67 | ); 68 | } 69 | 70 | return (); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/modal/addFriends.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Cookies from 'js-cookie'; 4 | import LazyLoad from 'react-lazyload'; 5 | import Modal from '../../../../common/components/modal'; 6 | import Select from '../../../../common/components/select2-one'; 7 | import actions from '../../actions'; 8 | import sdk from '../../sdk'; 9 | import webConfig from '../../../../../../web_config'; 10 | 11 | @connect( 12 | state => ({ 13 | userInfo: state.get('userInfo'), 14 | companyStruct: state.getIn(['chat', 'companyStruct']), 15 | currentSession: state.getIn(['chat', 'currentSession']) 16 | }), 17 | actions 18 | ) 19 | export default class AddFriends extends Component { 20 | constructor() { 21 | super(); 22 | this.state = { 23 | domainList: [], // 域列表 24 | selectDomain: {}, // 选中的域 25 | users: [] 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | sdk.ready(async () => { 31 | const res = await sdk.getDomainList(); 32 | if (res.ret) { 33 | this.setState({ domainList: res.data.domains, selectDomain: res.data.domains[0] || {} }); 34 | } 35 | }); 36 | } 37 | 38 | // componentWillReceiveProps(nextProps) { 39 | // // if (nextProps.userList.length > 0) { 40 | // // this.initTree(nextProps); 41 | // // } 42 | // } 43 | 44 | onSearch = (e) => { 45 | const val = e.target.value.trim(); 46 | clearTimeout(this.time); 47 | this.time = setTimeout(async () => { 48 | const res = await sdk.searchSbuddy({ 49 | id: this.state.selectDomain.id, 50 | key: val, 51 | ckey: Cookies.get('q_ckey'), 52 | limit: 12, 53 | offset: 0 54 | }); 55 | if (res.ret) { 56 | this.setState({ 57 | users: res.data.users 58 | }); 59 | } 60 | }, 250); 61 | }; 62 | 63 | render() { 64 | return ( 65 | 66 |
67 | 查找联系人 68 | { this.props.hide(); }} className="icon close" /> 69 |
70 |
71 |
72 |

查找范围:

73 | 89 |
90 |
    91 | { 92 | this.state.users.map(item => ( 93 |
  • 94 |
    95 | 96 | 97 | 98 |
    99 |
    100 | 添加好友 101 |
    102 |

    103 | {item.label || item.uri} 104 | {item.content} 105 |

    106 |
  • 107 | )) 108 | } 109 |
110 |
111 |
112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/modal/addUser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-06 11:24:03 5 | * @LastEditTime: 2019-08-13 12:01:48 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import React, { Component } from 'react'; 9 | import { connect } from 'react-redux'; 10 | import cls from 'classnames'; 11 | import Modal from '../../../../common/components/modal'; 12 | import actions from '../../actions'; 13 | import $ from '../../../../common/lib/jstree'; 14 | import sdk from '../../sdk'; 15 | 16 | const webConfig = { 17 | domain: startalkNav.baseaddess && startalkNav.baseaddess.domain 18 | } 19 | 20 | @connect( 21 | state => ({ 22 | userInfo: state.get('userInfo'), 23 | companyStruct: state.getIn(['chat', 'companyStruct']), 24 | currentSession: state.getIn(['chat', 'currentSession']) 25 | }), 26 | actions 27 | ) 28 | export default class AddUser extends Component { 29 | constructor() { 30 | super(); 31 | this.state = { 32 | selected: [] 33 | }; 34 | } 35 | 36 | componentDidMount() { 37 | this.initTree(); 38 | } 39 | 40 | onSearch = (e) => { 41 | const val = e.target.value.trim(); 42 | clearTimeout(this.time); 43 | this.time = setTimeout(() => { 44 | $('#addUserTree').jstree(true).search(val); 45 | }, 300); 46 | }; 47 | 48 | time = null; 49 | 50 | initTree(nextProps) { 51 | const { companyStruct, currentSession, userInfo } = nextProps || this.props; 52 | $('#addUserTree') 53 | .on('changed.jstree', (e, data) => { 54 | const ret = []; 55 | data.selected.forEach((id, index) => { 56 | const json = data.instance.get_node(data.selected[index]).original; 57 | if (json.U) { 58 | ret.push(json); 59 | } 60 | }); 61 | // 单聊,要把会话对象加入 62 | if (ret.length > 0 && currentSession.get('mFlag') === '1') { 63 | const u = userInfo.get(currentSession.get('user')); 64 | ret.push({ 65 | U: u.get('username') || window.QtalkSDK.env.Strophe.getNodeFromJid(currentSession.get('user')), 66 | N: u.get('nickname') || '' 67 | }); 68 | } 69 | this.setState({ 70 | selected: ret 71 | }); 72 | }) 73 | .jstree({ 74 | core: { 75 | data: [ 76 | { 77 | text: 'Staff', 78 | state: { 79 | opened: true 80 | }, 81 | children: companyStruct 82 | } 83 | ] 84 | }, 85 | plugins: [ 86 | 'checkbox', 87 | 'search' 88 | ] 89 | }); 90 | // this.initTree = () => { }; 91 | } 92 | 93 | addUser = async () => { 94 | const { selected } = this.state; 95 | const { changeChatField, currentSession, clearSessionCnt } = this.props; 96 | const users = selected.map(u => ({ jid: `${u.U}@${webConfig.domain}`, nick: u.N })); 97 | if (users.length > 0) { 98 | const res = await sdk.addUser(users); 99 | if (res.ret) { 100 | this.props.hide(); 101 | // 单聊创建新群后,激活会话 102 | if (currentSession.get('mFlag') === '1') { 103 | setTimeout(() => { 104 | changeChatField({ 105 | currentSession: { 106 | cnt: 0, 107 | sdk_msg: '', 108 | simpmsg: '', 109 | user: res.data, 110 | mFlag: '2' 111 | } 112 | }); 113 | clearSessionCnt(); 114 | }, 0); 115 | } 116 | } else { 117 | alert(res.errmsg); 118 | } 119 | } 120 | }; 121 | 122 | render() { 123 | const { selected } = this.state; 124 | return ( 125 | 126 |
127 | 添加会话成员 128 | { this.props.hide(); }} className="icon close" /> 129 |
130 |
131 |
132 | 133 | 139 |
140 |
141 |
142 |
143 |
144 | 155 | 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/panel/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Info from './info'; 3 | import Search from './search'; 4 | import Tab from './tab'; 5 | // import sdk from '../../sdk'; 6 | 7 | export default class Panel extends Component { 8 | componentDidMount() { 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/panel/info.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-06 11:24:03 5 | * @LastEditTime: 2019-08-13 12:08:26 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import React, { Component } from 'react'; 9 | import { connect } from 'react-redux'; 10 | import cls from 'classnames'; 11 | import Cookies from 'js-cookie'; 12 | import MessageBox from '../../../../common/components/message-box'; 13 | import actions from '../../actions'; 14 | import sdk from '../../sdk'; 15 | 16 | const webConfig = { 17 | fileurl: startalkNav.baseaddess && startalkNav.baseaddess.fileurl 18 | } 19 | 20 | @connect( 21 | state => ({ 22 | userInfo: state.get('userInfo') 23 | }), 24 | actions 25 | ) 26 | export default class Info extends Component { 27 | constructor() { 28 | super(); 29 | this.state = { 30 | showMenu: false 31 | }; 32 | } 33 | 34 | componentDidMount() { 35 | const { setUserInfo } = this.props; 36 | // 可以正常收发消息了 37 | sdk.ready(async () => { 38 | const res = await sdk.getUserCard([sdk.bareJid]); 39 | if (res.ret) { 40 | setUserInfo(res.data); 41 | } 42 | }); 43 | } 44 | 45 | onShowUserCard = (e) => { 46 | const { setModalUserCard } = this.props; 47 | setModalUserCard({ 48 | show: true, 49 | pos: { 50 | left: `${e.clientX}px`, 51 | top: `${e.clientY + 50}px` 52 | }, 53 | user: sdk.bareJid 54 | }); 55 | } 56 | 57 | onShowMembers = () => { 58 | const { setMembersInfo } = this.props; 59 | setMembersInfo({ 60 | show: true, 61 | isNew: true 62 | }); 63 | this.showMenu(false); 64 | } 65 | 66 | time = null; 67 | 68 | showMenu = (b) => { 69 | clearTimeout(this.time); 70 | this.time = setTimeout(() => { 71 | this.setState({ showMenu: b }); 72 | }, 100); 73 | }; 74 | 75 | logout = () => { 76 | MessageBox.confirm( 77 | '确认退出?', 78 | '提示' 79 | ).ok(() => { 80 | let img = webConfig.fileurl+'/file/v2/download/8c9d42532be9316e2202ffef8fcfeba5.png?w=80&h=80'; 81 | const info = this.props.userInfo.get(sdk.bareJid); 82 | if (info) { 83 | img = info.get('imageurl') || ''; 84 | if (!/^(https:|http:|\/\/)/g.test(img)) { 85 | img = `${webConfig.fileurl}/${img}`; 86 | } 87 | } 88 | Cookies.set('qt_avatar', img, { expires: 1 }); 89 | Cookies.remove('qt_username'); 90 | Cookies.remove('qt_password'); 91 | sdk.connection.disConnection(); 92 | this.props.changeChatField({ connectStatus: '' }); 93 | window.location.reload(); 94 | }); 95 | } 96 | 97 | render() { 98 | const { userInfo } = this.props; 99 | let img = webConfig.fileurl+'/file/v2/download/8c9d42532be9316e2202ffef8fcfeba5.png?w=80&h=80'; 100 | let name = ''; 101 | const info = userInfo.get(sdk.bareJid); 102 | if (info) { 103 | name = info.get('nickname') || ''; 104 | img = info.get('imageurl') || ''; 105 | if (!/^(https:|http:|\/\/)/g.test(img)) { 106 | img = `${webConfig.fileurl}/${img}`; 107 | } 108 | } 109 | return ( 110 |
111 |
112 | 113 |
114 |
115 |

116 | {name} 117 | { this.showMenu(false); }} 120 | onClick={() => { this.showMenu(true); }} 121 | > 122 | 123 | 137 | 138 |

139 |
140 |
141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/panel/search.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import LazyLoad from 'react-lazyload'; 4 | import Cookies from 'js-cookie'; 5 | // import cls from 'classnames'; 6 | import actions from '../../actions'; 7 | import sdk from '../../sdk'; 8 | 9 | @connect( 10 | state => ({ 11 | currentSession: state.getIn(['chat', 'currentSession']) 12 | }), 13 | actions 14 | ) 15 | export default class Search extends Component { 16 | constructor() { 17 | super(); 18 | this.state = { 19 | showModal: false, 20 | value: '', 21 | result: [] 22 | }; 23 | } 24 | 25 | componentDidMount() { 26 | } 27 | 28 | onClearClick() { 29 | this.setState({ 30 | value: '', 31 | showModal: false 32 | }); 33 | } 34 | 35 | onSessionClick = async (data) => { 36 | const { 37 | setUserInfo, 38 | setCurrentSession, 39 | changeChatField 40 | } = this.props; 41 | this.setState({ 42 | value: '', 43 | showModal: false 44 | }); 45 | if (data.mFlag === '2') { 46 | const res = await sdk.getGroupCard([data.user]); 47 | if (res.ret) { 48 | setUserInfo(res.data); 49 | } 50 | } 51 | setCurrentSession(data); 52 | changeChatField({ switchIndex: 'chat' }); 53 | window.QtalkSDK.$('.session').scrollTop(0); 54 | } 55 | 56 | onChange = (e) => { 57 | const val = e.target.value.trim(); 58 | this.setState({ value: val }); 59 | clearTimeout(this.timer); 60 | this.timer = setTimeout(async () => { 61 | const state = {}; 62 | if (val.length > 1) { 63 | state.showModal = true; 64 | // 获取查询结果 65 | const res = await sdk.searchUser(val); 66 | if (res.data) { 67 | state.result = res.data; 68 | } 69 | } else { 70 | state.showModal = false; 71 | } 72 | this.setState(state); 73 | }, 250); 74 | } 75 | 76 | timer = null; 77 | renderResult = () => { 78 | const { result } = this.state; 79 | if (result && result.length < 1) { 80 | return null; 81 | } 82 | return ( 83 |
    84 | { 85 | result.map((group) => { 86 | const arr = []; 87 | if (result.length > 1) { 88 | arr.push(
  • {group.groupLabel}
  • ); 89 | } 90 | const lis = group.info.map(item => ( 91 |
  • this.onSessionClick({ user: item.uri, mFlag: `${group.groupPriority + 1}` })}> 92 |
    93 | 94 | 95 | 96 |
    97 |

    98 | {item.label} 99 | {item.content} 100 |

    101 |
  • 102 | )); 103 | return arr.concat(lis); 104 | }) 105 | } 106 |
107 | ); 108 | } 109 | render() { 110 | return ( 111 |
112 | 113 | { setTimeout(() => { this.setState({ showModal: false }); }, 300); }} 120 | /> 121 | { 122 | this.state.showModal && 123 |
124 | {this.renderResult()} 125 | {/*

找不到?尝试打开会话

126 |

this.onSessionClick({ user: this.state.value, mFlag: '1' })}>打开ID为[{this.state.value}]的对话

*/} 127 |
128 | } 129 |
130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/panel/tab.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import cls from 'classnames'; 4 | import Session from './session'; 5 | import Friends from './friends'; 6 | import actions from '../../actions'; 7 | // import sdk from '../../sdk'; 8 | 9 | @connect( 10 | state => ({ 11 | switchIndex: state.getIn(['chat', 'switchIndex']) 12 | }), 13 | actions 14 | ) 15 | export default class Tab extends Component { 16 | onSwitch(switchIndex) { 17 | this.props.changeChatField({ switchIndex }); 18 | } 19 | 20 | render() { 21 | const { switchIndex } = this.props; 22 | return ( 23 |
24 |
25 |
{ this.onSwitch('chat'); }}> 26 | 27 | 28 | 29 |
30 |
{ this.onSwitch('friends'); }}> 31 | 32 | 33 | 34 |
35 |
36 | 37 | { this.onSwitch('chat'); }} 39 | show={switchIndex === 'friends'} 40 | /> 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/tree/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-06 11:24:03 5 | * @LastEditTime: 2019-08-13 12:10:29 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import React, { Component } from 'react'; 9 | import { connect } from 'react-redux'; 10 | import cls from 'classnames'; 11 | import actions from '../../actions'; 12 | import './index.less'; 13 | import sdk from '../../sdk'; 14 | 15 | const webConfig = { 16 | fileurl: startalkNav.baseaddess && startalkNav.baseaddess.fileurl, 17 | domain: startalkNav.baseaddess && startalkNav.baseaddess.domain 18 | } 19 | 20 | @connect( 21 | state => ({ 22 | userInfo: state.get('userInfo') 23 | }), 24 | actions 25 | ) 26 | class Tree extends Component { 27 | constructor(props) { 28 | super(props); 29 | this.state = { 30 | tree: {}, // { key: true } key=true 展开 false 闭合 31 | selected: {}, // { key: true } key=true 被选择的 32 | searchTree: {} // { key: true } key=true 搜索的结果 33 | }; 34 | } 35 | 36 | componentWillReceiveProps(nextProps) { 37 | const { selected, tree, searchTree } = this.state; 38 | if (nextProps.data.length === 0) { 39 | return null; 40 | } 41 | this.setState({ 42 | selected: nextProps.selected ? nextProps.selected : selected, 43 | tree: nextProps.tree ? nextProps.tree : tree, 44 | searchTree: nextProps.searchTree ? nextProps.searchTree : searchTree 45 | }); 46 | return true; 47 | } 48 | 49 | onTreeClick(e, data, disable) { 50 | e.stopPropagation(); 51 | if (disable) { 52 | return null; 53 | } 54 | const { key } = data; 55 | const { tree } = this.state; 56 | const newTree = Object.assign({}, tree, { 57 | [key]: !tree[key] 58 | }); 59 | this.setState({ 60 | tree: newTree 61 | }); 62 | let u = ''; 63 | if (data.U && this.props.onClick) { 64 | const { changeChatField } = this.props; 65 | changeChatField({ isCard: true }); 66 | u = `${data.U}@${webConfig.domain}`; 67 | } 68 | if (data.children) { 69 | const users = []; 70 | data.children.forEach((item) => { 71 | if (item.U) { 72 | users.push(`${item.U}@${webConfig.domain}`); 73 | } 74 | }); 75 | if (users.length > 0) { 76 | this.cacheUserCard(users); 77 | } 78 | } 79 | this.props.onClick(u, newTree); 80 | if (this.props.showSelect && !data.children) { 81 | this.onSelect(data); 82 | } 83 | return true; 84 | } 85 | 86 | onSelect(item) { 87 | const { selected } = this.state; 88 | const newSelected = Object.assign( 89 | {}, 90 | selected, 91 | { 92 | [item.key]: { 93 | flag: !(selected[item.key] && selected[item.key].flag), 94 | value: item 95 | } 96 | } 97 | ); 98 | this.setState({ 99 | selected: newSelected 100 | }); 101 | 102 | this.props.onSelect(newSelected); 103 | } 104 | 105 | async cacheUserCard(users) { 106 | const { setUserInfo } = this.props; 107 | const res = await sdk.getUserCard(users); 108 | if (res.ret) { 109 | setUserInfo(res.data); 110 | } 111 | return res; 112 | } 113 | 114 | imgError(e) { 115 | e.target.src = webConfig.fileurl+'/file/v2/download/8c9d42532be9316e2202ffef8fcfeba5.png';//darlyn' 116 | } 117 | 118 | renderItems(data) { 119 | const { selected, searchTree } = this.state; 120 | const { showSelect, noSelected } = this.props; 121 | return ( 122 |
    123 | { 124 | data.map((item) => { 125 | const tree = this.state.tree[item.key]; 126 | const active = !item.children && selected[item.key] && selected[item.key].flag; 127 | const disable = showSelect && !item.children && noSelected[item.U]; 128 | if (Object.keys(searchTree).length > 0 && tree === undefined) { 129 | return null; 130 | } 131 | return ( 132 |
  • this.onTreeClick(e, item, disable)} 135 | className={cls('tree-list-item', { 136 | deep: !item.children, 137 | disabled: showSelect && !item.children && noSelected[item.U] 138 | })} 139 | > 140 | { 141 | item.children ? 142 | : 148 | 153 | } 154 |

    {item.text}

    155 | { 156 | showSelect && !item.children && !noSelected[item.U] && 157 | 160 | } 161 | { 162 | tree && item.children && this.renderItems(item.children) 163 | } 164 |
  • 165 | ); 166 | }) 167 | } 168 |
169 | ); 170 | } 171 | 172 | render() { 173 | const { data } = this.props; 174 | return ( 175 |
176 | {this.renderItems(data)} 177 |
178 | ); 179 | } 180 | } 181 | 182 | const noob = () => {}; 183 | Tree.defaultProps = { 184 | className: '', 185 | data: [], 186 | showSelect: false, 187 | onSelect: noob, 188 | onClick: '', 189 | selected: [], 190 | noSelected: {} 191 | }; 192 | 193 | export default Tree; 194 | -------------------------------------------------------------------------------- /src/web/app/pages/index/phone-ui/tree/index.less: -------------------------------------------------------------------------------- 1 | .tree { 2 | min-height: 22px; 3 | padding: 15px 0; 4 | cursor: pointer; 5 | &-list { 6 | width: 248px; 7 | min-height: 40px; 8 | z-index: 1; 9 | &-item { 10 | width: 248px; 11 | min-height: 40px; 12 | float: left; 13 | position: relative; 14 | &.deep { 15 | padding: 9px 0; 16 | } 17 | &.search { 18 | .text { 19 | color: #E92F2F; 20 | } 21 | } 22 | &.disabled { 23 | // pointer-events: none 24 | .text { 25 | color: #9E9E9E; 26 | } 27 | } 28 | .iconfont { 29 | float: left; 30 | color: #45CF8E; 31 | font-size: 14px; 32 | vertical-align: middle; 33 | margin-top: 9px; 34 | } 35 | .iconfont.nike { 36 | font-size: 24px; 37 | color: #BDBDBD; 38 | position: absolute; 39 | right: 10px; 40 | top: 50%; 41 | margin-top: -24px; 42 | cursor: pointer; 43 | &.active { 44 | color: #45CF8E; 45 | } 46 | } 47 | .text { 48 | width: 166px; 49 | height: 40px; 50 | line-height: 40px; 51 | float: left; 52 | font-weight: 400; 53 | font-size: 14px; 54 | margin-left: 6px; 55 | overflow: hidden; 56 | text-overflow:ellipsis; 57 | white-space: nowrap; 58 | word-wrap: normal; 59 | user-select:none; 60 | &.text-active{ 61 | color: #45CF8E; 62 | } 63 | } 64 | img { 65 | width: 45px; 66 | height: 45px; 67 | border-radius: 50%; 68 | float: left; 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/web/app/pages/index/sdk.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: your name 4 | * @Date: 2019-08-06 11:24:02 5 | * @LastEditTime: 2019-08-12 16:54:37 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | 9 | /** 10 | * qtlk sdk 11 | */ 12 | const { baseaddess: {domain, xmpp, xmppmport, fileurl, javaurl, socketurl} = {} } = startalkNav 13 | 14 | 15 | const sdk = new window.QtalkSDK({ 16 | // 调试 17 | debug: true, 18 | xmpp: xmpp, 19 | // 链接配置 20 | connect: { 21 | host: socketurl 22 | }, 23 | maType: 6 // 平台类型web端:6 24 | }); 25 | 26 | export default sdk; 27 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/chat/emotions.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cls from 'classnames'; 3 | 4 | export default class Emotions extends Component { 5 | constructor() { 6 | super(); 7 | const emotions = Object.assign({}, window.QtalkSDK.emotions[0]); 8 | const { faces } = emotions; 9 | delete emotions.faces; 10 | const pageSize = 32; 11 | this.state = { 12 | showMenu: false, 13 | emotions, 14 | faces, 15 | pageCount: Math.ceil(faces.length / pageSize), 16 | pageSize, 17 | page: 0 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | } 23 | 24 | onChangePage = (e) => { 25 | this.setState({ page: parseInt(e.target.getAttribute('data-page'), 10) }); 26 | }; 27 | 28 | time = null; 29 | 30 | showMenu = (b) => { 31 | clearTimeout(this.time); 32 | this.time = setTimeout(() => { 33 | this.setState({ showMenu: b }); 34 | }, 100); 35 | }; 36 | 37 | render() { 38 | const { 39 | showMenu, faces, emotions, pageCount, page, pageSize 40 | } = this.state; 41 | let i = 0; 42 | const pageEl = []; 43 | let data = []; 44 | if (showMenu) { 45 | data = faces.slice(page * pageSize, (page * pageSize) + pageSize); 46 | while (i < pageCount) { 47 | pageEl.push(); 53 | i += 1; 54 | } 55 | } 56 | return ( 57 | { this.showMenu(false); }} 60 | onMouseEnter={() => { this.showMenu(true); }} 61 | > 62 | 63 |
68 |
    69 | { 70 | data.map((face, index) => ( 71 |
  • { this.props.selected(emotions, face); }} 73 | key={`face-${index + 1}`} 74 | > 75 | 76 |
  • 77 | )) 78 | } 79 |
80 |
{pageEl}
81 |
82 |
83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/chat/empty.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Empty extends Component { 4 | componentDidMount() { 5 | } 6 | 7 | render() { 8 | return ( 9 |
10 | 21 |
22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/chat/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import actions from '../../actions'; 4 | import Header from './header'; 5 | import Message from './message'; 6 | import Footer from './footer'; 7 | import Empty from './empty'; 8 | import GroupCard from './groupCard'; 9 | import UserCard from './userCard'; 10 | // import sdk from '../../sdk'; 11 | 12 | @connect( 13 | state => ({ 14 | currentSession: state.getIn(['chat', 'currentSession']), 15 | switchIndex: state.getIn(['chat', 'switchIndex']), 16 | currentFriend: state.getIn(['chat', 'currentFriend']) 17 | }), 18 | actions 19 | ) 20 | export default class Chat extends Component { 21 | constructor() { 22 | super(); 23 | this.state = { 24 | messageScrollToBottom: false 25 | }; 26 | } 27 | 28 | messageScrollToBottom = () => { 29 | this.setState({ 30 | messageScrollToBottom: true 31 | }, () => { 32 | this.setState({ 33 | messageScrollToBottom: false 34 | }); 35 | }); 36 | }; 37 | 38 | render() { 39 | const { 40 | currentSession, 41 | switchIndex, 42 | currentFriend 43 | } = this.props; 44 | 45 | const currId = currentSession.get('user') || currentSession.get('groupname'); 46 | const currFri = currentFriend.get('user'); 47 | if (switchIndex === 'chat' && currId) { 48 | return ( 49 |
50 |
51 | 52 |
53 |
54 | ); 55 | } else if (switchIndex === 'friends' && currFri) { 56 | return ( 57 |
58 | { 59 | currentFriend.get('mFlag') === '2' ? 60 | : 61 | 62 | } 63 |
64 | ); 65 | } 66 | 67 | return (); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/chat/members.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-06 11:24:02 5 | * @LastEditTime: 2019-08-13 19:47:39 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import React, { Component } from 'react'; 9 | import { connect } from 'react-redux'; 10 | import cls from 'classnames'; 11 | import LazyLoad from 'react-lazyload'; 12 | import MessageBox from '../../../../common/components/message-box'; 13 | import actions from '../../actions'; 14 | import sdk from '../../sdk'; 15 | import webConfig from '../../../../../../web_config'; 16 | import footera3faf4373242d1 from '../../../../../../assets/footer/a3faf4373242d1.png'; 17 | 18 | const { $ } = window.QtalkSDK; 19 | 20 | @connect( 21 | state => ({ 22 | userInfo: state.get('userInfo'), 23 | currentSession: state.getIn(['chat', 'currentSession']) 24 | }), 25 | actions 26 | ) 27 | export default class Members extends Component { 28 | constructor() { 29 | super(); 30 | this.state = { 31 | showRemove: false, 32 | owner: false, 33 | admin: false 34 | }; 35 | } 36 | 37 | componentDidMount() { 38 | this.getUserInfo(); 39 | $(window) 40 | .on('keydown', (e) => { 41 | // ctrl 显示 踢人按钮 42 | if (e.keyCode === 17 43 | && this.props.show 44 | && (this.state.owner || this.state.admin) 45 | ) { 46 | this.setState({ showRemove: true }); 47 | } 48 | }) 49 | .on('keyup', (e) => { 50 | if (e.keyCode === 17 51 | && this.props.show 52 | && (this.state.owner || this.state.admin) 53 | ) { 54 | this.setState({ showRemove: false }); 55 | } 56 | }); 57 | } 58 | 59 | componentWillReceiveProps(nextProps) { 60 | const { show } = this.props; 61 | // 隐藏的时候重置 62 | if (show !== nextProps.show && !nextProps.show) { 63 | this.setState({ 64 | showRemove: false, 65 | owner: false, 66 | admin: false 67 | }); 68 | } 69 | if (nextProps.show) { 70 | this.getUserInfo(nextProps); 71 | } 72 | } 73 | 74 | onShowUserCard = (e, user) => { 75 | const { setModalUserCard } = this.props; 76 | const pos = { 77 | left: `${e.clientX}px`, 78 | top: `${e.clientY + 50}px` 79 | }; 80 | if (e.clientX + 280 > $(window).width()) { 81 | pos.left = `${e.clientX - 280}px`; 82 | } 83 | setModalUserCard({ 84 | show: true, 85 | pos, 86 | user 87 | }); 88 | } 89 | 90 | onRemoveUser(jid) { 91 | const { currentSession } = this.props; 92 | if (jid === sdk.bareJid) { 93 | MessageBox.confirm( 94 | '即将从该群退出,如果继续且需要恢复此操作,
你需要让当前群成员重新邀请。是否继续?', 95 | '警告' 96 | ).ok(() => { 97 | sdk.groupExit(jid); 98 | }); 99 | } else { 100 | sdk.groupRemoveUser(jid, currentSession.get('user')); 101 | } 102 | } 103 | 104 | onContextMenu(e, jid, role) { 105 | e.preventDefault(); 106 | const { setContentMenu, currentSession } = this.props; 107 | const { owner, admin } = this.state; 108 | setContentMenu({ 109 | show: true, 110 | type: 'member', 111 | data: { 112 | user: jid, 113 | userRole: role, 114 | owner, 115 | admin, 116 | groupId: currentSession.get('user') 117 | }, 118 | pos: { 119 | left: `${e.clientX}px`, 120 | top: `${e.clientY}px` 121 | } 122 | }); 123 | } 124 | 125 | async getUserInfo(nextProps) { 126 | const { setUserInfo, userList, userInfo } = nextProps || this.props; 127 | if (userList && userList.size > 0) { 128 | const users = []; 129 | const usersCheck = {}; 130 | userList.forEach((item) => { 131 | // 是否有权限踢人,跟升级管理员 132 | const jid = item.get('jid'); 133 | const affiliation = item.get('affiliation'); 134 | if (jid === sdk.bareJid && affiliation) { 135 | this.setState({ 136 | [affiliation]: true 137 | }); 138 | } 139 | if (!userInfo.get(jid) && !usersCheck[jid]) { 140 | users.push(jid); 141 | usersCheck[jid] = true; 142 | } 143 | }); 144 | if (users.length > 0) { 145 | const res = await sdk.getUserCard(users); 146 | if (res.ret) { 147 | setUserInfo(res.data); 148 | } 149 | } 150 | } 151 | } 152 | 153 | addUser = () => { 154 | this.props.addUser(); 155 | }; 156 | 157 | imgError(e) { 158 | e.target.src = webConfig.fileurl+'/file/v2/download/8c9d42532be9316e2202ffef8fcfeba5.png'; 159 | } 160 | 161 | render() { 162 | const { show, userList, userInfo } = this.props; 163 | const { showRemove, owner, admin } = this.state; 164 | if (!show) { 165 | return null; 166 | } 167 | return ( 168 |
169 |
170 |
171 | 172 |
173 | { 174 | userList.map((item, index) => { 175 | let img = footera3faf4373242d1; 176 | const jid = item.get('jid'); 177 | const affiliation = item.get('affiliation'); 178 | let name = jid; 179 | const info = userInfo.get(jid); 180 | if (info) { 181 | name = info.get('nickname') || jid; 182 | img = info.get('imageurl') || ''; 183 | if (!/^(https:|http:|\/\/)/g.test(img)) { 184 | img = webConfig.fileurl+`/${img}`; 185 | } 186 | } 187 | return ( 188 |
{ this.onContextMenu(e, jid, affiliation); }} 192 | > 193 | { 194 | showRemove 195 | && ((admin && affiliation !== 'owner') || owner) 196 | ? ( 197 | { this.onRemoveUser(jid); }} 199 | className="opt animation animating fadeIn" 200 | > 201 | 202 | 203 | ) 204 | : null 205 | } 206 | 207 | 208 | 209 | 210 | { this.onShowUserCard(e, jid); }} 212 | className="avatar user-card" 213 | src={img} 214 | alt="" 215 | onError={this.imgError} 216 | /> 217 | 218 |

{name}

219 |
220 | ); 221 | }) 222 | } 223 |
224 | { 225 | (owner || admin) 226 | ?
群人员管理[右键]菜单,按住[ctrl]快捷操作
227 | : null 228 | } 229 |
230 | ); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/modal/addFriends.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Cookies from 'js-cookie'; 4 | import LazyLoad from 'react-lazyload'; 5 | import Modal from '../../../../common/components/modal'; 6 | import Select from '../../../../common/components/select2-one'; 7 | import actions from '../../actions'; 8 | import sdk from '../../sdk'; 9 | import webConfig from '../../../../../../web_config'; 10 | 11 | @connect( 12 | state => ({ 13 | userInfo: state.get('userInfo'), 14 | companyStruct: state.getIn(['chat', 'companyStruct']), 15 | currentSession: state.getIn(['chat', 'currentSession']) 16 | }), 17 | actions 18 | ) 19 | export default class AddFriends extends Component { 20 | constructor() { 21 | super(); 22 | this.state = { 23 | domainList: [], // 域列表 24 | selectDomain: {}, // 选中的域 25 | users: [] 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | sdk.ready(async () => { 31 | const res = await sdk.getDomainList(); 32 | if (res.ret) { 33 | this.setState({ domainList: res.data.domains, selectDomain: res.data.domains[0] || {} }); 34 | } 35 | }); 36 | } 37 | 38 | // componentWillReceiveProps(nextProps) { 39 | // // if (nextProps.userList.length > 0) { 40 | // // this.initTree(nextProps); 41 | // // } 42 | // } 43 | 44 | onSearch = (e) => { 45 | const val = e.target.value.trim(); 46 | clearTimeout(this.time); 47 | this.time = setTimeout(async () => { 48 | const res = await sdk.searchSbuddy({ 49 | id: this.state.selectDomain.id, 50 | key: val, 51 | ckey: Cookies.get('q_ckey'), 52 | limit: 12, 53 | offset: 0 54 | }); 55 | if (res.ret) { 56 | this.setState({ 57 | users: res.data.users 58 | }); 59 | } 60 | }, 250); 61 | }; 62 | 63 | render() { 64 | return ( 65 | 66 |
67 | 查找联系人 68 | { this.props.hide(); }} className="icon close" /> 69 |
70 |
71 |
72 |

查找范围:

73 | 89 |
90 |
    91 | { 92 | this.state.users.map(item => ( 93 |
  • 94 |
    95 | 96 | 97 | 98 |
    99 |
    100 | 添加好友 101 |
    102 |

    103 | {item.label || item.uri} 104 | {item.content} 105 |

    106 |
  • 107 | )) 108 | } 109 |
110 |
111 |
112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/modal/addUser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-06 11:24:03 5 | * @LastEditTime: 2019-08-13 12:01:48 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import React, { Component } from 'react'; 9 | import { connect } from 'react-redux'; 10 | import cls from 'classnames'; 11 | import Modal from '../../../../common/components/modal'; 12 | import actions from '../../actions'; 13 | import $ from '../../../../common/lib/jstree'; 14 | import sdk from '../../sdk'; 15 | 16 | const webConfig = { 17 | domain: startalkNav.baseaddess && startalkNav.baseaddess.domain 18 | } 19 | 20 | @connect( 21 | state => ({ 22 | userInfo: state.get('userInfo'), 23 | companyStruct: state.getIn(['chat', 'companyStruct']), 24 | currentSession: state.getIn(['chat', 'currentSession']) 25 | }), 26 | actions 27 | ) 28 | export default class AddUser extends Component { 29 | constructor() { 30 | super(); 31 | this.state = { 32 | selected: [] 33 | }; 34 | } 35 | 36 | componentDidMount() { 37 | this.initTree(); 38 | } 39 | 40 | onSearch = (e) => { 41 | const val = e.target.value.trim(); 42 | clearTimeout(this.time); 43 | this.time = setTimeout(() => { 44 | $('#addUserTree').jstree(true).search(val); 45 | }, 300); 46 | }; 47 | 48 | time = null; 49 | 50 | initTree(nextProps) { 51 | const { companyStruct, currentSession, userInfo } = nextProps || this.props; 52 | $('#addUserTree') 53 | .on('changed.jstree', (e, data) => { 54 | const ret = []; 55 | data.selected.forEach((id, index) => { 56 | const json = data.instance.get_node(data.selected[index]).original; 57 | if (json.U) { 58 | ret.push(json); 59 | } 60 | }); 61 | // 单聊,要把会话对象加入 62 | if (ret.length > 0 && currentSession.get('mFlag') === '1') { 63 | const u = userInfo.get(currentSession.get('user')); 64 | ret.push({ 65 | U: u.get('username') || window.QtalkSDK.env.Strophe.getNodeFromJid(currentSession.get('user')), 66 | N: u.get('nickname') || '' 67 | }); 68 | } 69 | this.setState({ 70 | selected: ret 71 | }); 72 | }) 73 | .jstree({ 74 | core: { 75 | data: [ 76 | { 77 | text: 'Staff', 78 | state: { 79 | opened: true 80 | }, 81 | children: companyStruct 82 | } 83 | ] 84 | }, 85 | plugins: [ 86 | 'checkbox', 87 | 'search' 88 | ] 89 | }); 90 | // this.initTree = () => { }; 91 | } 92 | 93 | addUser = async () => { 94 | const { selected } = this.state; 95 | const { changeChatField, currentSession, clearSessionCnt } = this.props; 96 | const users = selected.map(u => ({ jid: `${u.U}@${webConfig.domain}`, nick: u.N })); 97 | if (users.length > 0) { 98 | const res = await sdk.addUser(users); 99 | if (res.ret) { 100 | this.props.hide(); 101 | // 单聊创建新群后,激活会话 102 | if (currentSession.get('mFlag') === '1') { 103 | setTimeout(() => { 104 | changeChatField({ 105 | currentSession: { 106 | cnt: 0, 107 | sdk_msg: '', 108 | simpmsg: '', 109 | user: res.data, 110 | mFlag: '2' 111 | } 112 | }); 113 | clearSessionCnt(); 114 | }, 0); 115 | } 116 | } else { 117 | alert(res.errmsg); 118 | } 119 | } 120 | }; 121 | 122 | render() { 123 | const { selected } = this.state; 124 | return ( 125 | 126 |
127 | 添加会话成员 128 | { this.props.hide(); }} className="icon close" /> 129 |
130 |
131 |
132 | 133 | 139 |
140 |
141 |
142 |
143 |
144 | 155 | 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/panel/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Info from './info'; 3 | import Search from './search'; 4 | import Tab from './tab'; 5 | // import sdk from '../../sdk'; 6 | 7 | export default class Panel extends Component { 8 | componentDidMount() { 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/panel/info.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-06 11:24:03 5 | * @LastEditTime: 2019-08-13 12:08:26 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import React, { Component } from 'react'; 9 | import { connect } from 'react-redux'; 10 | import cls from 'classnames'; 11 | import Cookies from 'js-cookie'; 12 | import MessageBox from '../../../../common/components/message-box'; 13 | import actions from '../../actions'; 14 | import sdk from '../../sdk'; 15 | 16 | const webConfig = { 17 | fileurl: startalkNav.baseaddess && startalkNav.baseaddess.fileurl 18 | } 19 | 20 | @connect( 21 | state => ({ 22 | userInfo: state.get('userInfo') 23 | }), 24 | actions 25 | ) 26 | export default class Info extends Component { 27 | constructor() { 28 | super(); 29 | this.state = { 30 | showMenu: false 31 | }; 32 | } 33 | 34 | componentDidMount() { 35 | const { setUserInfo } = this.props; 36 | // 可以正常收发消息了 37 | sdk.ready(async () => { 38 | const res = await sdk.getUserCard([sdk.bareJid]); 39 | if (res.ret) { 40 | setUserInfo(res.data); 41 | } 42 | }); 43 | } 44 | 45 | onShowUserCard = (e) => { 46 | const { setModalUserCard } = this.props; 47 | setModalUserCard({ 48 | show: true, 49 | pos: { 50 | left: `${e.clientX}px`, 51 | top: `${e.clientY + 50}px` 52 | }, 53 | user: sdk.bareJid 54 | }); 55 | } 56 | 57 | onShowMembers = () => { 58 | const { setMembersInfo } = this.props; 59 | setMembersInfo({ 60 | show: true, 61 | isNew: true 62 | }); 63 | this.showMenu(false); 64 | } 65 | 66 | time = null; 67 | 68 | showMenu = (b) => { 69 | clearTimeout(this.time); 70 | this.time = setTimeout(() => { 71 | this.setState({ showMenu: b }); 72 | }, 100); 73 | }; 74 | 75 | logout = () => { 76 | MessageBox.confirm( 77 | '确认退出?', 78 | '提示' 79 | ).ok(() => { 80 | let img = webConfig.fileurl+'/file/v2/download/8c9d42532be9316e2202ffef8fcfeba5.png?w=80&h=80'; 81 | const info = this.props.userInfo.get(sdk.bareJid); 82 | if (info) { 83 | img = info.get('imageurl') || ''; 84 | if (!/^(https:|http:|\/\/)/g.test(img)) { 85 | img = `${webConfig.fileurl}/${img}`; 86 | } 87 | } 88 | Cookies.set('qt_avatar', img, { expires: 1 }); 89 | Cookies.remove('qt_username'); 90 | Cookies.remove('qt_password'); 91 | sdk.connection.disConnection(); 92 | this.props.changeChatField({ connectStatus: '' }); 93 | window.location.reload(); 94 | }); 95 | } 96 | 97 | render() { 98 | const { userInfo } = this.props; 99 | let img = webConfig.fileurl+'/file/v2/download/8c9d42532be9316e2202ffef8fcfeba5.png?w=80&h=80'; 100 | let name = ''; 101 | const info = userInfo.get(sdk.bareJid); 102 | if (info) { 103 | name = info.get('nickname') || ''; 104 | img = info.get('imageurl') || ''; 105 | if (!/^(https:|http:|\/\/)/g.test(img)) { 106 | img = `${webConfig.fileurl}/${img}`; 107 | } 108 | } 109 | return ( 110 |
111 |
112 | 113 |
114 |
115 |

116 | {name} 117 | { this.showMenu(false); }} 120 | onMouseEnter={() => { this.showMenu(true); }} 121 | > 122 | 123 | 137 | 138 |

139 |
140 |
141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/panel/search.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import LazyLoad from 'react-lazyload'; 4 | import Cookies from 'js-cookie'; 5 | // import cls from 'classnames'; 6 | import actions from '../../actions'; 7 | import sdk from '../../sdk'; 8 | 9 | @connect( 10 | state => ({ 11 | currentSession: state.getIn(['chat', 'currentSession']) 12 | }), 13 | actions 14 | ) 15 | export default class Search extends Component { 16 | constructor() { 17 | super(); 18 | this.state = { 19 | showModal: false, 20 | value: '', 21 | result: [] 22 | }; 23 | } 24 | 25 | componentDidMount() { 26 | } 27 | 28 | onClearClick() { 29 | this.setState({ 30 | value: '', 31 | showModal: false 32 | }); 33 | } 34 | 35 | onSessionClick = async (data) => { 36 | const { 37 | setUserInfo, 38 | setCurrentSession, 39 | changeChatField 40 | } = this.props; 41 | this.setState({ 42 | value: '', 43 | showModal: false 44 | }); 45 | if (data.mFlag === '2') { 46 | const res = await sdk.getGroupCard([data.user]); 47 | if (res.ret) { 48 | setUserInfo(res.data); 49 | } 50 | } 51 | setCurrentSession(data); 52 | changeChatField({ switchIndex: 'chat' }); 53 | window.QtalkSDK.$('.session').scrollTop(0); 54 | } 55 | 56 | onChange = (e) => { 57 | const val = e.target.value.trim(); 58 | this.setState({ value: val }); 59 | clearTimeout(this.timer); 60 | this.timer = setTimeout(async () => { 61 | const state = {}; 62 | if (val.length > 1) { 63 | state.showModal = true; 64 | // 获取查询结果 65 | const res = await sdk.searchUser(val); 66 | if (res.data) { 67 | state.result = res.data; 68 | } 69 | } else { 70 | state.showModal = false; 71 | } 72 | this.setState(state); 73 | }, 250); 74 | } 75 | 76 | timer = null; 77 | renderResult = () => { 78 | const { result } = this.state; 79 | if (result && result.length < 1) { 80 | return null; 81 | } 82 | return ( 83 |
    84 | { 85 | result.map((group) => { 86 | const arr = []; 87 | if (result.length > 1) { 88 | arr.push(
  • {group.groupLabel}
  • ); 89 | } 90 | const lis = group.info.map(item => ( 91 |
  • this.onSessionClick({ user: item.uri, mFlag: `${group.groupPriority + 1}` })}> 92 |
    93 | 94 | 95 | 96 |
    97 |

    98 | {item.label} 99 | {item.content} 100 |

    101 |
  • 102 | )); 103 | return arr.concat(lis); 104 | }) 105 | } 106 |
107 | ); 108 | } 109 | render() { 110 | return ( 111 |
112 | 113 | { setTimeout(() => { this.setState({ showModal: false }); }, 300); }} 120 | /> 121 | { 122 | this.state.showModal && 123 |
124 | {this.renderResult()} 125 | {/*

找不到?尝试打开会话

126 |

this.onSessionClick({ user: this.state.value, mFlag: '1' })}>打开ID为[{this.state.value}]的对话

*/} 127 |
128 | } 129 |
130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/panel/tab.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import cls from 'classnames'; 4 | import Session from './session'; 5 | import Friends from './friends'; 6 | import actions from '../../actions'; 7 | // import sdk from '../../sdk'; 8 | 9 | @connect( 10 | state => ({ 11 | switchIndex: state.getIn(['chat', 'switchIndex']) 12 | }), 13 | actions 14 | ) 15 | export default class Tab extends Component { 16 | onSwitch(switchIndex) { 17 | this.props.changeChatField({ switchIndex }); 18 | } 19 | 20 | render() { 21 | const { switchIndex } = this.props; 22 | return ( 23 |
24 |
25 |
{ this.onSwitch('chat'); }}> 26 | 27 | 28 | 29 |
30 |
{ this.onSwitch('friends'); }}> 31 | 32 | 33 | 34 |
35 |
36 | 37 | { this.onSwitch('chat'); }} 39 | show={switchIndex === 'friends'} 40 | /> 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/tree/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-06 11:24:03 5 | * @LastEditTime: 2019-08-13 12:10:29 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import React, { Component } from 'react'; 9 | import { connect } from 'react-redux'; 10 | import cls from 'classnames'; 11 | import actions from '../../actions'; 12 | import './index.less'; 13 | import sdk from '../../sdk'; 14 | 15 | const webConfig = { 16 | fileurl: startalkNav.baseaddess && startalkNav.baseaddess.fileurl, 17 | domain: startalkNav.baseaddess && startalkNav.baseaddess.domain 18 | } 19 | 20 | @connect( 21 | state => ({ 22 | userInfo: state.get('userInfo') 23 | }), 24 | actions 25 | ) 26 | class Tree extends Component { 27 | constructor(props) { 28 | super(props); 29 | this.state = { 30 | tree: {}, // { key: true } key=true 展开 false 闭合 31 | selected: {}, // { key: true } key=true 被选择的 32 | searchTree: {} // { key: true } key=true 搜索的结果 33 | }; 34 | } 35 | 36 | componentWillReceiveProps(nextProps) { 37 | const { selected, tree, searchTree } = this.state; 38 | if (nextProps.data.length === 0) { 39 | return null; 40 | } 41 | this.setState({ 42 | selected: nextProps.selected ? nextProps.selected : selected, 43 | tree: nextProps.tree ? nextProps.tree : tree, 44 | searchTree: nextProps.searchTree ? nextProps.searchTree : searchTree 45 | }); 46 | return true; 47 | } 48 | 49 | onTreeClick(e, data, disable) { 50 | e.stopPropagation(); 51 | if (disable) { 52 | return null; 53 | } 54 | const { key } = data; 55 | const { tree } = this.state; 56 | const newTree = Object.assign({}, tree, { 57 | [key]: !tree[key] 58 | }); 59 | this.setState({ 60 | tree: newTree 61 | }); 62 | let u = ''; 63 | if (data.U && this.props.onClick) { 64 | u = `${data.U}@${webConfig.domain}`; 65 | } 66 | if (data.children) { 67 | const users = []; 68 | data.children.forEach((item) => { 69 | if (item.U) { 70 | users.push(`${item.U}@${webConfig.domain}`); 71 | } 72 | }); 73 | if (users.length > 0) { 74 | this.cacheUserCard(users); 75 | } 76 | } 77 | this.props.onClick(u, newTree); 78 | if (this.props.showSelect && !data.children) { 79 | this.onSelect(data); 80 | } 81 | return true; 82 | } 83 | 84 | onSelect(item) { 85 | const { selected } = this.state; 86 | const newSelected = Object.assign( 87 | {}, 88 | selected, 89 | { 90 | [item.key]: { 91 | flag: !(selected[item.key] && selected[item.key].flag), 92 | value: item 93 | } 94 | } 95 | ); 96 | this.setState({ 97 | selected: newSelected 98 | }); 99 | 100 | this.props.onSelect(newSelected); 101 | } 102 | 103 | async cacheUserCard(users) { 104 | const { setUserInfo } = this.props; 105 | const res = await sdk.getUserCard(users); 106 | if (res.ret) { 107 | setUserInfo(res.data); 108 | } 109 | return res; 110 | } 111 | 112 | imgError(e) { 113 | e.target.src = webConfig.fileurl+'/file/v2/download/8c9d42532be9316e2202ffef8fcfeba5.png';//darlyn' 114 | } 115 | 116 | renderItems(data) { 117 | const { selected, searchTree } = this.state; 118 | const { showSelect, noSelected } = this.props; 119 | return ( 120 |
    121 | { 122 | data.map((item) => { 123 | const tree = this.state.tree[item.key]; 124 | const active = !item.children && selected[item.key] && selected[item.key].flag; 125 | const disable = showSelect && !item.children && noSelected[item.U]; 126 | if (Object.keys(searchTree).length > 0 && tree === undefined) { 127 | return null; 128 | } 129 | return ( 130 |
  • this.onTreeClick(e, item, disable)} 133 | className={cls('tree-list-item', { 134 | deep: !item.children, 135 | disabled: showSelect && !item.children && noSelected[item.U] 136 | })} 137 | > 138 | { 139 | item.children ? 140 | : 146 | 151 | } 152 |

    {item.text}

    153 | { 154 | showSelect && !item.children && !noSelected[item.U] && 155 | 158 | } 159 | { 160 | tree && item.children && this.renderItems(item.children) 161 | } 162 |
  • 163 | ); 164 | }) 165 | } 166 |
167 | ); 168 | } 169 | 170 | render() { 171 | const { data } = this.props; 172 | return ( 173 |
174 | {this.renderItems(data)} 175 |
176 | ); 177 | } 178 | } 179 | 180 | const noob = () => {}; 181 | Tree.defaultProps = { 182 | className: '', 183 | data: [], 184 | showSelect: false, 185 | onSelect: noob, 186 | onClick: '', 187 | selected: [], 188 | noSelected: {} 189 | }; 190 | 191 | export default Tree; 192 | -------------------------------------------------------------------------------- /src/web/app/pages/index/ui/tree/index.less: -------------------------------------------------------------------------------- 1 | .tree { 2 | min-height: 22px; 3 | padding: 15px 0; 4 | cursor: pointer; 5 | &-list { 6 | width: 248px; 7 | min-height: 40px; 8 | z-index: 1; 9 | &-item { 10 | width: 248px; 11 | min-height: 40px; 12 | float: left; 13 | position: relative; 14 | &.deep { 15 | padding: 9px 0; 16 | } 17 | &.search { 18 | .text { 19 | color: #E92F2F; 20 | } 21 | } 22 | &.disabled { 23 | // pointer-events: none 24 | .text { 25 | color: #9E9E9E; 26 | } 27 | } 28 | .iconfont { 29 | float: left; 30 | color: #45CF8E; 31 | font-size: 14px; 32 | vertical-align: middle; 33 | margin-top: 9px; 34 | } 35 | .iconfont.nike { 36 | font-size: 24px; 37 | color: #BDBDBD; 38 | position: absolute; 39 | right: 10px; 40 | top: 50%; 41 | margin-top: -24px; 42 | cursor: pointer; 43 | &.active { 44 | color: #45CF8E; 45 | } 46 | } 47 | .text { 48 | width: 166px; 49 | height: 40px; 50 | line-height: 40px; 51 | float: left; 52 | font-weight: 400; 53 | font-size: 14px; 54 | margin-left: 6px; 55 | overflow: hidden; 56 | text-overflow:ellipsis; 57 | white-space: nowrap; 58 | word-wrap: normal; 59 | user-select:none; 60 | &.text-active{ 61 | color: #45CF8E; 62 | } 63 | } 64 | img { 65 | width: 45px; 66 | height: 45px; 67 | border-radius: 50%; 68 | float: left; 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/web/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './styles/index.css' 4 | 5 | ReactDOM.render( 6 |
hello
, 7 | document.getElementById('app') 8 | ) 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/web/sdk/common/assets/20180423_qtalk_msg.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/startalk_web/2278d146bd4220f45639391d9e8ac3e9f0268407/src/web/sdk/common/assets/20180423_qtalk_msg.mp3 -------------------------------------------------------------------------------- /src/web/sdk/common/lib/strophejs-plugin-iexdomain.js: -------------------------------------------------------------------------------- 1 | import { Strophe } from 'strophe.js'; 2 | 3 | Strophe.addConnectionPlugin('iexdomain', { 4 | init: function(conn) { 5 | // replace Strophe.Request._newXHR with new IE CrossDomain version 6 | var nativeXHR = new XMLHttpRequest(); 7 | if (window.XDomainRequest && ! ("withCredentials" in nativeXHR)) { 8 | Strophe.Request.prototype._newXHR = function() { 9 | var xhr = new XDomainRequest(); 10 | xhr.setRequestHeader = function () {} ; 11 | xhr.readyState = 0; 12 | 13 | xhr.onreadystatechange = this.func.bind(undefined, this); 14 | xhr.onerror = function() { 15 | xhr.readyState = 4; 16 | xhr.status = 500; 17 | xhr.onreadystatechange(xhr.responseText); 18 | }; 19 | xhr.ontimeout = function() { 20 | xhr.readyState = 4; 21 | xhr.status = 0; 22 | xhr.onreadystatechange(xhr.responseText); 23 | }; 24 | xhr.onload = function() { 25 | xhr.readyState = 4; 26 | xhr.status = 200; 27 | var _response = xhr.responseText; 28 | var _xml = new ActiveXObject('Microsoft.XMLDOM'); 29 | _xml.async = 'false'; 30 | _xml.loadXML(_response); 31 | xhr.responseXML = _xml; 32 | xhr.onreadystatechange(_response); 33 | }; 34 | return xhr; 35 | }; 36 | } 37 | // else { 38 | // console.info("Browser doesnt support XDomainRequest." + " Falling back to native XHR implementation."); 39 | // } 40 | } 41 | }); -------------------------------------------------------------------------------- /src/web/sdk/common/utils/messageHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: your name 4 | * @Date: 2019-08-05 21:14:24 5 | * @LastEditTime: 2019-08-12 17:02:36 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import { getEmoticonsUrl, createUUID } from './utils'; 9 | import $ from 'jquery'; 10 | 11 | const sdkConfig = { 12 | fileurl: startalkNav.baseaddess && startalkNav.baseaddess.fileurl 13 | } 14 | /** 15 | * 消息转换处理 16 | */ 17 | 18 | // 取出 type value width height 19 | const OBJ_RE = /\[obj type=\"(.*?)\" value=\"\[?(.*?)\]?\"( width=(.*?) height=(.*?))?.*?\]/g; 20 | const URL_RE = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<]{2,200}\b/g; 21 | const IMG_RE = /<(img|IMG) src=\"(.*?)\" data-emoticon=\"(.*?)\".*?>/g; 22 | 23 | /** 24 | * 编码 25 | * [obj type="img" value="src"] 26 | * url => [obj type="url" value="url"] 27 | *   => ' ' 28 | * 29 | */ 30 | const encode = (content) => { 31 | const objcache = {}; 32 | content = content.replace(/ /g, ' '); // 空格 33 | content = content.replace(/\n\s*\n/g, '\n'); // 回车 34 | content = content.replace(/\n/g, ''); 35 | content = content.replace(/]*>/ig, '\n'); // 回车 36 | // 只保留 img 37 | content = content.replace(/(<[^>]*>)/g, ($0, $1) => { 38 | // 因为要替换 url ,图片地址也属于 url,所以先把img替换掉并缓存起来 39 | if (/]*>/gi.test($1)) { 40 | const uuid = createUUID(); 41 | let src = $1.match(/src="(.*?)"/); 42 | let emoticon = $1.match(/data-emoticon="(.*?)"/); 43 | let categery = $1.match(/data-categery="(.*?)"/); 44 | let type = $1.match(/data-type="(.*?)"/); 45 | if (!src && !src[1]) { 46 | return ''; 47 | } 48 | src = src[1]; 49 | if (emoticon && emoticon[1]) { 50 | emoticon = `[${emoticon[1]}]`; 51 | } 52 | if (categery && categery[1]) { 53 | categery = categery[1]; 54 | } 55 | if (type && type[1]) { 56 | type = type[1]; 57 | } 58 | // 表情 59 | if (emoticon && categery) { 60 | objcache[uuid] = `[obj type="emoticon" value="${emoticon}" width=${categery} height=0]`; 61 | } else if (type === 'base64') { 62 | objcache[uuid] = `[obj type="base64" value="${src}"]`; 63 | } else { 64 | objcache[uuid] = `[obj type="image" value="${src}"]`; 65 | } 66 | return uuid; 67 | } else { 68 | return ''; 69 | } 70 | }); 71 | // 兼容客户端,需要转回来 72 | content = content.replace(/</g, '<'); 73 | content = content.replace(/>/g, '>'); 74 | content = content.replace(/"/g, '\''); 75 | content = content.replace(/&/g, '&'); 76 | // 编码URL 77 | const list = content.match(URL_RE); 78 | if (list) { 79 | for (let i = 0; i < list.length;) { 80 | const prot = list[i].indexOf('http://') === 0 || list[i].indexOf('https://') === 0 ? '' : 'http://'; 81 | const escapedUrl = encodeURI(decodeURI(list[i])).replace(/[!'()]/g, escape).replace(/\*/g, '%2A'); 82 | const uuid = createUUID(); 83 | objcache[uuid] = `[obj type="url" value="${prot}${escapedUrl}"]`; 84 | content = content.replace(list[i], uuid); 85 | i += 1; 86 | } 87 | } 88 | // 把img替换回来 89 | Object.keys(objcache).forEach((key) => { 90 | content = content.replace(key, objcache[key]); 91 | }); 92 | return $.trim(content); 93 | }; 94 | 95 | // 解码 96 | const decode = (content, msgType) => { 97 | if (!content) { 98 | return ''; 99 | } 100 | if (msgType.toString() === '5') { 101 | let file; 102 | try { 103 | file = JSON.parse(content); 104 | } catch (e) { 105 | return content; 106 | } 107 | const { FileName, HttpUrl } = file; 108 | let url = HttpUrl; 109 | if (url.indexOf('http') === -1) { 110 | url = sdkConfig.fileurl + url; 111 | } 112 | // 文件都是单行数据 113 | return `${FileName}`; 114 | } 115 | content = content.replace(//g, '>'); 116 | content = content.replace(OBJ_RE, (...args) => { 117 | if (args && args.length > 2) { 118 | let ret = args[0]; 119 | const type = args[1]; 120 | let val = args[2]; 121 | const width = args[4]; 122 | switch (type) { 123 | case 'image': 124 | if (val.indexOf('http') === -1) { 125 | val = sdkConfig.fileurl + val; 126 | } 127 | ret = ``; 128 | break; 129 | case 'emoticon': 130 | ret = ``; 131 | break; 132 | case 'url': 133 | ret = `${val}`; 134 | break; 135 | } 136 | return ret; 137 | } 138 | }); 139 | return content; 140 | }; 141 | 142 | /** 143 | * 过滤 144 | * [obj type="image" ...] => [图片] 145 | * [obj type="file" ...] => [文件] 146 | * [obj type="emoticon" ...] => [表情] 147 | */ 148 | const filter = (content, msgType = '') => { 149 | if (!content) { 150 | return ''; 151 | } 152 | if (msgType.toString() === '5') { 153 | return '[文件]'; 154 | } else if (msgType.toString() === '666') { 155 | return '[分享]'; 156 | }else if (content.indexOf('[') > -1) { 157 | content = content.replace(OBJ_RE, (...args) => { 158 | if (args && args.length > 2) { 159 | let ret = args[0]; 160 | const type = args[1]; 161 | if (type === 'image') { 162 | ret = '[图片]'; 163 | } else if (type === 'emoticon') { 164 | ret = '[表情]'; 165 | } else if (type === 'url') { 166 | ret = '[url]'; 167 | } 168 | return ret; 169 | } 170 | }); 171 | } 172 | return content; 173 | }; 174 | 175 | export default { 176 | encode, 177 | decode, 178 | filter 179 | }; 180 | -------------------------------------------------------------------------------- /src/web/sdk/common/utils/publicEncrypt.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: your name 4 | * @Date: 2019-08-05 21:14:24 5 | * @LastEditTime: 2019-08-12 13:22:46 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import parseKeys from 'parse-asn1'; 9 | import createHash from 'create-hash'; 10 | import bn from 'bn.js'; 11 | import crt from 'browserify-rsa'; 12 | import randomBytes from './randomBytes'; 13 | 14 | function withPublic(paddedMsg, key) { 15 | return new Buffer(paddedMsg 16 | .toRed(bn.mont(key.modulus)) 17 | .redPow(new bn(key.publicExponent)) 18 | .fromRed() 19 | .toArray()); 20 | } 21 | 22 | function mgf(seed, len) { 23 | var t = new Buffer(''); 24 | var i = 0, c; 25 | while (t.length < len) { 26 | c = i2ops(i++); 27 | t = Buffer.concat([t, createHash('sha1').update(seed).update(c).digest()]); 28 | } 29 | return t.slice(0, len); 30 | } 31 | 32 | function i2ops(c) { 33 | var out = new Buffer(4); 34 | out.writeUInt32BE(c,0); 35 | return out; 36 | } 37 | 38 | function xor(a, b) { 39 | var len = a.length; 40 | var i = -1; 41 | while (++i < len) { 42 | a[i] ^= b[i]; 43 | } 44 | return a 45 | } 46 | 47 | 48 | function oaep(key, msg){ 49 | var k = key.modulus.byteLength(); 50 | var mLen = msg.length; 51 | var iHash = createHash('sha1').update(new Buffer('')).digest(); 52 | var hLen = iHash.length; 53 | var hLen2 = 2 * hLen; 54 | if (mLen > k - hLen2 - 2) { 55 | throw new Error('message too long'); 56 | } 57 | var ps = new Buffer(k - mLen - hLen2 - 2); 58 | ps.fill(0); 59 | var dblen = k - hLen - 1; 60 | var seed = randomBytes(hLen); 61 | var maskedDb = xor(Buffer.concat([iHash, ps, new Buffer([1]), msg], dblen), mgf(seed, dblen)); 62 | var maskedSeed = xor(seed, mgf(maskedDb, hLen)); 63 | return new bn(Buffer.concat([new Buffer([0]), maskedSeed, maskedDb], k)); 64 | } 65 | function pkcs1(key, msg, reverse){ 66 | var mLen = msg.length; 67 | var k = key.modulus.byteLength(); 68 | if (mLen > k - 11) { 69 | throw new Error('message too long'); 70 | } 71 | var ps; 72 | if (reverse) { 73 | ps = new Buffer(k - mLen - 3); 74 | ps.fill(0xff); 75 | } else { 76 | ps = nonZero(k - mLen - 3); 77 | } 78 | return new bn(Buffer.concat([new Buffer([0, reverse?1:2]), ps, new Buffer([0]), msg], k)); 79 | } 80 | function nonZero(len, crypto) { 81 | var out = new Buffer(len); 82 | var i = 0; 83 | var cache = randomBytes(len*2); 84 | var cur = 0; 85 | var num; 86 | while (i < len) { 87 | if (cur === cache.length) { 88 | cache = randomBytes(len*2); 89 | cur = 0; 90 | } 91 | num = cache[cur++]; 92 | if (num) { 93 | out[i++] = num; 94 | } 95 | } 96 | return out; 97 | } 98 | 99 | export default (public_key, msg, reverse) => { 100 | var padding; 101 | if (public_key.padding) { 102 | padding = public_key.padding; 103 | } else if (reverse) { 104 | padding = 1; 105 | } else { 106 | padding = 4; 107 | } 108 | var key = parseKeys(public_key); 109 | var paddedMsg; 110 | if (padding === 4) { 111 | paddedMsg = oaep(key, msg); 112 | } else if (padding === 1) { 113 | paddedMsg = pkcs1(key, msg, reverse); 114 | } else if (padding === 3) { 115 | paddedMsg = new bn(msg); 116 | if (paddedMsg.cmp(key.modulus) >= 0) { 117 | throw new Error('data too long for modulus'); 118 | } 119 | } else { 120 | throw new Error('unknown padding'); 121 | } 122 | if (reverse) { 123 | return crt(paddedMsg, key); 124 | } else { 125 | return withPublic(paddedMsg, key); 126 | } 127 | }; 128 | 129 | -------------------------------------------------------------------------------- /src/web/sdk/common/utils/randomBytes.js: -------------------------------------------------------------------------------- 1 | import getRandomValues from 'polyfill-crypto.getrandomvalues'; 2 | // if (!window.crypto) { 3 | // window.crypto = { getRandomValues }; 4 | // } 5 | 6 | 7 | // function oldBrowser () { 8 | // throw new Error('Secure random number generation is not supported by this browser.\nUse Chrome, Firefox or Internet Explorer 11') 9 | // } 10 | 11 | var Buffer = require('safe-buffer').Buffer 12 | var crypto = global.crypto || global.msCrypto 13 | 14 | // if (crypto && crypto.getRandomValues) { 15 | export default randomBytes 16 | // } else { 17 | // module.exports = oldBrowser 18 | // } 19 | 20 | function randomBytes (size, cb) { 21 | // phantomjs needs to throw 22 | if (size > 65536) throw new Error('requested too many random bytes') 23 | // in case browserify isn't using the Uint8Array version 24 | var rawBytes = new global.Uint8Array(size) 25 | 26 | // This will not work in older browsers. 27 | // See https://developer.mozilla.org/en-US/docs/Web/API/window.crypto.getRandomValues 28 | if (size > 0) { // getRandomValues fails on IE if size == 0 29 | if (crypto && crypto.getRandomValues) { 30 | crypto.getRandomValues(rawBytes) 31 | } else { 32 | getRandomValues(rawBytes); 33 | } 34 | } 35 | 36 | // XXX: phantomjs doesn't like a buffer being passed here 37 | var bytes = Buffer.from(rawBytes.buffer) 38 | 39 | if (typeof cb === 'function') { 40 | return process.nextTick(function () { 41 | cb(null, bytes) 42 | }) 43 | } 44 | 45 | return bytes 46 | } 47 | -------------------------------------------------------------------------------- /src/web/sdk/common/utils/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: your name 4 | * @Date: 2019-08-05 21:14:24 5 | * @LastEditTime: 2019-08-12 17:03:03 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import $ from 'jquery'; 9 | 10 | const sdkConfig = { 11 | fileurl: startalkNav.baseaddess && startalkNav.baseaddess.fileurl 12 | } 13 | 14 | const checkUpLoadFileExist = (url) => { 15 | return $.ajax({ 16 | url, 17 | type: 'GET', 18 | dataType: 'json', 19 | data: {}, 20 | jsonp: 'callback' 21 | }); 22 | }; 23 | 24 | const isObject = (o) => { 25 | return Object.prototype.toString.call(o) === '[object Object]'; 26 | }; 27 | /** 28 | * 配置混入 29 | * originObject => { a: 1, b: { c: 1, d: 2}} 30 | * newObject = { a: 2, b: {c: 2} } 31 | * return => {a: 1, b: { c:2, d: 2}} 32 | */ 33 | const configMix = (originObject, newObject) => { 34 | const ret = Object.assign({}, originObject); 35 | Object.keys(newObject).forEach((key) => { 36 | if (ret[key] === undefined) { 37 | return; 38 | } 39 | if (isObject(ret[key])) { 40 | ret[key] = configMix(ret[key], newObject[key]); 41 | } else { 42 | ret[key] = newObject[key]; 43 | } 44 | }); 45 | return ret; 46 | }; 47 | 48 | const isSupportWebSocket = !!(window.WebSocket && window.WebSocket.prototype.send); 49 | 50 | /** 51 | * 反转枚举 52 | */ 53 | const reverseEnum = (o) => { 54 | const reverse = {}; 55 | Object.keys(o).forEach((key) => { 56 | reverse[o[key]] = key; 57 | }); 58 | return reverse; 59 | }; 60 | 61 | const createUUID = () => { 62 | let d = new Date().getTime(); 63 | const uuid = 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 64 | var r = (d + Math.random() * 16) % 16 | 0; 65 | d = Math.floor(d / 16); 66 | return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16); 67 | }); 68 | return uuid.toUpperCase(); 69 | }; 70 | 71 | const getEmoticonsUrl = (shortcut, category = 'EmojiOne') => { 72 | if (!shortcut || shortcut.length == 0) { 73 | return ''; 74 | } 75 | 76 | return sdkConfig.fileurl+`/file/v1/emo/d/e/${category}/${shortcut.replace('/', '')}/org`; 77 | }; 78 | 79 | const bytesToMB = (bytes) => { 80 | if (bytes === 0) { 81 | return '1'; 82 | } 83 | const m = 1024 * 1024; 84 | let result = Math.floor(bytes / m); 85 | if (result < 1) { // 如果小于1都为1 86 | result = 1; 87 | } 88 | return result; 89 | }; 90 | 91 | const bytesToSize = (bytes) => { 92 | if (bytes === 0) { 93 | return '0 B'; 94 | } 95 | const k = 1024; 96 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 97 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 98 | return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; 99 | }; 100 | 101 | const audioPlayer = (() => { 102 | let player; 103 | const id = createUUID(); 104 | // const file = sdkConfig.fileurl+'/zhuanti/20180423_qtalk_msg.mp3'; 105 | const file = "../assets/20180423_qtalk_msg.mp3"; 106 | const init = () => { 107 | if (!player) { 108 | player = window.document.createElement('audio'); 109 | player.id = id; 110 | const mp3 = document.createElement('source'); 111 | mp3.src = file; 112 | mp3.type = 'audio/mpeg'; 113 | player.appendChild(mp3); 114 | window.document.body.appendChild(player); 115 | } 116 | }; 117 | return () => { 118 | init(); 119 | player.play(); 120 | }; 121 | })(); 122 | 123 | const getCookie = (name) => { 124 | const reg = new RegExp(`(^| )${name}=([^;]*)(;|$)`); 125 | const arr = window.document.cookie.match(reg); 126 | if (arr) { 127 | return unescape(arr[2]); 128 | } 129 | return null; 130 | }; 131 | 132 | // 将base64转换为文件 133 | const dataURLtoFile = (dataurl, filename) => { 134 | const arr = dataurl.split(','); 135 | const mime = arr[0].match(/:(.*?);/)[1]; 136 | const bstr = atob(arr[1]); 137 | let n = bstr.length; 138 | const u8arr = new Uint8Array(n); 139 | // eslint-disable-next-line 140 | while (n--) { 141 | u8arr[n] = bstr.charCodeAt(n); 142 | } 143 | return new window.File([u8arr], filename, { type: mime }); 144 | }; 145 | 146 | const utils = { 147 | configMix, 148 | isObject, 149 | isSupportWebSocket, 150 | reverseEnum, 151 | createUUID, 152 | getEmoticonsUrl, 153 | bytesToMB, 154 | bytesToSize, 155 | checkUpLoadFileExist, 156 | audioPlayer, 157 | getCookie, 158 | dataURLtoFile 159 | } 160 | 161 | export { 162 | configMix, 163 | isObject, 164 | isSupportWebSocket, 165 | reverseEnum, 166 | createUUID, 167 | getEmoticonsUrl, 168 | bytesToMB, 169 | bytesToSize, 170 | checkUpLoadFileExist, 171 | audioPlayer, 172 | getCookie, 173 | dataURLtoFile 174 | }; 175 | 176 | export default utils 177 | -------------------------------------------------------------------------------- /src/web/sdk/core/auth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: chaos.dong 4 | * @Date: 2019-10-25 21:14:24 5 | * @LastEditTime: 2019-08-13 17:32:30 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import dayjs from 'dayjs'; 9 | import publicEncrypt from '../common/utils/publicEncrypt'; 10 | import axios from 'axios'; 11 | 12 | const pubKeyFullkey = startalkKeys.pub_key_fullkey 13 | const webConfig = { 14 | loginType: startalkNav.Login && startalkNav.Login.loginType, 15 | domain: startalkNav.baseaddess && startalkNav.baseaddess.domain, 16 | } 17 | 18 | /** 19 | * 密码加密 20 | */ 21 | 22 | // 公钥 sdkConfig.pub_key_fullkey 23 | 24 | const encrypt = raw => ( 25 | publicEncrypt({ 26 | key: pubKeyFullkey, 27 | padding: 1 28 | }, raw).toString('base64') 29 | ); 30 | 31 | function generateUUID() { 32 | var d = new Date().getTime(); 33 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 34 | var r = (d + Math.random() * 16) % 16 | 0; 35 | d = Math.floor(d / 16); 36 | return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); 37 | }); 38 | return uuid; 39 | }; 40 | 41 | export default (username, password) => { 42 | if (webConfig.loginType === 'password') { 43 | const uinfo = { 44 | p: password, 45 | a: 'testapp', 46 | u: username, 47 | d: dayjs().format('YYYY-MM-DD HH:mm:ss') 48 | }; 49 | // eslint-disable-next-line 50 | const encrypted = encrypt(new Buffer(JSON.stringify(uinfo))); 51 | return encrypted.toString('base64'); 52 | 53 | } else if (webConfig.loginType === 'newpassword') { 54 | const newEncrypted = encrypt(new Buffer(password)); 55 | const requestData = { 56 | p: newEncrypted, 57 | h: webConfig.domain, 58 | u: username, 59 | mk: generateUUID(), 60 | plat: "web" 61 | }; 62 | 63 | return new Promise((resolve, reject) => { 64 | axios({ 65 | url: '/newapi/nck/qtlogin.qunar', 66 | method: 'POST', 67 | data: requestData 68 | }).then(res => { 69 | const data = [res.data.data.t,requestData.mk] 70 | resolve(data); 71 | }).catch(e => { 72 | console.info(e) 73 | }) 74 | }) 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/web/sdk/core/buildMessage.js: -------------------------------------------------------------------------------- 1 | import { Strophe } from './strophe'; 2 | import messageHelper from '../common/utils/messageHelper'; 3 | 4 | /** 5 | * 生成公共消息内容 6 | */ 7 | export default (data, myId) => { 8 | let isMe = false; 9 | const { message, body, from, t, muc, carbonMessage, readType } = data; // read_flag 10 | const { type, sendjid } = message; 11 | const { id, msgType, content, backupinfo, extendInfo } = body; 12 | const fromName = Strophe.getBareJidFromJid(sendjid || ''); 13 | let sj = ''; 14 | let ids = []; 15 | if (type === 'chat') { 16 | // carbonMessage --> 抄送消息,from to 是反得,实际上是自己发的 17 | if (from === myId || carbonMessage) { 18 | isMe = true; 19 | } 20 | sj = from; 21 | } else if (type === 'groupchat') { 22 | if (fromName === myId) { 23 | isMe = true; 24 | } 25 | sj = fromName; 26 | } else if (type === 'readmark') { 27 | try { 28 | ids = JSON.parse(content); 29 | } catch (e) { 30 | ids = []; 31 | } 32 | ids = ids.map(item => item.id); 33 | } else if (type === 'revoke') { 34 | if (muc) { 35 | sj = message.from; 36 | } else { 37 | sj = from; 38 | } 39 | } 40 | return { 41 | // 公共 42 | id, // 消息ID, 43 | msgType, // 消息类型 44 | content: messageHelper.decode(content, msgType), // 消息内容 45 | simpcontent: messageHelper.filter(content, msgType), 46 | time: t * 1000, // 消息时间 47 | isMe, // 是否自己 48 | type, // 消息类型 groupchat 群消息, chat 单人消息 49 | sendjid: sj, // 发送者的jid 50 | backupinfo, 51 | extendInfo, 52 | carbonMessage, // 是否抄送 53 | 54 | // 单聊属性 55 | isRead: Math.floor(data.read_flag / 2) % 2 === 1, 56 | 57 | // 群属性 58 | muc, // 群ID 59 | 60 | // readmark 61 | readType, 62 | ids 63 | 64 | // revoke 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /src/web/sdk/core/connection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-05 21:14:24 5 | * @LastEditTime: 2019-08-13 17:33:34 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import EventEmitter from 'events'; 9 | import auth from './auth'; 10 | import { 11 | isSupportWebSocket, 12 | reverseEnum 13 | } from '../common/utils/utils'; 14 | import { Strophe } from './strophe'; 15 | import defaultOptions from '../options'; 16 | 17 | const { config: sdkConfig } = defaultOptions 18 | const webConfig = { 19 | loginType: startalkNav.Login && startalkNav.Login.loginType 20 | } 21 | 22 | /** 23 | * 链接websocket或者http-bind 24 | */ 25 | class Connection extends EventEmitter { 26 | STATUS = { 27 | 0: 'ERROR', // 连接错误 28 | 1: 'CONNECTING', // 连接中 29 | 2: 'CONNFAIL', // 连接失败 30 | 3: 'AUTHENTICATING', // 认证中 31 | 4: 'AUTHFAIL', // 认证失败 32 | 5: 'CONNECTED', // 已连接 33 | 6: 'DISCONNECTED', // 断开连接 34 | 7: 'DISCONNECTING', // 断开连接中 35 | 8: 'ATTACHED', // 已连接 36 | 9: 'REDIRECT' // 连接转发 37 | }; 38 | 39 | constructor(options) { 40 | super(); 41 | this.options = options; 42 | this.STATUS_REVERSE = reverseEnum(this.STATUS); 43 | const { host, path, delay } = options; 44 | this.reconnectCount = 0; 45 | this.delay = delay; 46 | // let { protocol } = window.location; 47 | // let bind = 'http-bind'; 48 | // if (isSupportWebSocket) { 49 | // protocol = protocol === 'https:' ? 'wss:' : 'ws:'; 50 | // bind = 'websocket'; 51 | // } 52 | // sdkConfig.httpurl 53 | // this.stropheConnection = new Strophe.Connection(`${protocol}//${host}${path}/${bind}`); 54 | this.stropheConnection = new Strophe.Connection(host || ''); 55 | } 56 | 57 | connect(user, pwd, domain, autologin) { 58 | if (webConfig.loginType === 'password') { 59 | if (!autologin) { 60 | const buildPwd = auth(user, pwd); 61 | pwd = buildPwd; 62 | } 63 | if (!domain) { 64 | domain = sdkConfig.domain; 65 | } 66 | this.auth = { user, pwd }; 67 | this.stropheConnection.connect(`${user}@${domain}`, pwd, this.onConnectStatusChange); 68 | } else if (webConfig.loginType === 'newpassword') { 69 | if (!domain) { 70 | domain = sdkConfig.domain; 71 | } 72 | if (!autologin) { 73 | auth(user, pwd).then(res => { 74 | const token = res[0]; 75 | const uinfo = { 76 | nauth: { 77 | p: token, 78 | u: `${user}@${domain}`, 79 | mk: res[1] 80 | } 81 | }; 82 | pwd = JSON.stringify(uinfo); 83 | this.auth = { user, pwd }; 84 | this.stropheConnection.connect(`${user}@${domain}`, pwd, this.onConnectStatusChange); 85 | }) 86 | } else { 87 | this.auth = { user, pwd }; 88 | this.stropheConnection.connect(`${user}@${domain}`, pwd, this.onConnectStatusChange); 89 | } 90 | } 91 | } 92 | 93 | reconnect() { 94 | const { jid, pass, wait, hold, route } = this.stropheConnection; 95 | if (this.reconnectCount >= this.options.reconnectCount) { 96 | return; 97 | } 98 | setTimeout(() => { 99 | this.reconnectCount += 1; 100 | this.emit('connect:reconnect'); 101 | this.stropheConnection.connect( 102 | jid, 103 | pass, 104 | this.onConnectStatusChange, 105 | wait, 106 | hold, 107 | route 108 | ); 109 | this.userDisConnection = false; 110 | }, this.delay); 111 | } 112 | 113 | /** 114 | * 主动断开链接 115 | */ 116 | disConnection() { 117 | this.userDisConnection = true; 118 | if (this.stropheConnection.connected) { 119 | this.stropheConnection.disconnect(); 120 | } 121 | } 122 | 123 | onConnectStatusChange = (status, condition) => { 124 | status = status.toString(); 125 | if (status === this.STATUS_REVERSE.CONNECTED || status === this.STATUS_REVERSE.ATTACHED) { 126 | delete this.disconnection_cause; 127 | // 已连接 128 | this.emit('connect:success', this.stropheConnection); 129 | } else if (status === this.STATUS_REVERSE.DISCONNECTED) { 130 | if (this.disconnection_cause === this.STATUS_REVERSE.CONNFAIL) { 131 | // 因为连接失败而断开连接,则 重新连接 132 | this.reconnect(); 133 | } else { 134 | // 已断开连接 135 | this.emit('connect:disconnected', this.stropheConnection); 136 | } 137 | } else if (status === this.STATUS_REVERSE.AUTHFAIL) { 138 | // 认证失败 139 | this.emit('connect:authfail', status, condition); 140 | } else if (status === this.STATUS_REVERSE.CONNFAIL) { 141 | // 连接失败 142 | if (!this.userDisConnection) { 143 | this.disconnection_cause = status; // 记录连接失败原因 144 | } 145 | this.emit('connect:fail', status, condition); 146 | } 147 | this.emit('connect', status, condition); 148 | }; 149 | } 150 | 151 | export default Connection; 152 | -------------------------------------------------------------------------------- /src/web/sdk/core/emotions/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-05 21:14:24 5 | * @LastEditTime: 2019-08-13 11:55:01 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | import { oneEmotions } from './oneEmotions'; 9 | 10 | const sdkConfig = { 11 | fileurl: startalkNav.baseaddess && startalkNav.baseaddess.fileurl 12 | } 13 | 14 | const emotions = [ 15 | oneEmotions 16 | ]; 17 | 18 | function getEmoticons() { 19 | const ret = []; 20 | emotions.forEach((item) => { 21 | const df = item.FACESETTING.DEFAULTFACE; 22 | const ns = df.categoryNew || df['-categery']; 23 | const child = { 24 | name: df['-name'], 25 | width: df['-width'], 26 | height: df['-height'], 27 | categery: ns, 28 | faces: [] 29 | }; 30 | df.FACE.forEach((f) => { 31 | const shortcut = f['-shortcut']; 32 | child.faces.push({ 33 | url: sdkConfig.fileurl+`/file/v1/emo/d/e/${ns}/${shortcut.replace('/', '')}/fixed`, 34 | shortcut, 35 | tip: f['-tip'] 36 | }); 37 | }); 38 | ret.push(child); 39 | }); 40 | return ret; 41 | } 42 | 43 | export default getEmoticons(); 44 | -------------------------------------------------------------------------------- /src/web/sdk/core/index.js: -------------------------------------------------------------------------------- 1 | import Connection from './connection'; 2 | import Ping from './ping'; 3 | import Message from './message'; 4 | import buildMessage from './buildMessage'; 5 | import upload from './upload'; 6 | import emotions from './emotions'; 7 | 8 | export { 9 | Connection, 10 | Ping, 11 | Message, 12 | buildMessage, 13 | upload, 14 | emotions 15 | }; 16 | -------------------------------------------------------------------------------- /src/web/sdk/core/ping.js: -------------------------------------------------------------------------------- 1 | import { Strophe } from './strophe'; 2 | 3 | class Ping { 4 | constructor(stropheConnection, pingInterval = 20) { 5 | this.stropheConnection = stropheConnection; 6 | this.pingInterval = pingInterval; 7 | } 8 | 9 | register() { 10 | const self = this; 11 | const { disco, ping } = self.stropheConnection; 12 | 13 | disco.addFeature(Strophe.NS.PING); 14 | ping.addPingHandler((pi) => { 15 | self.lastStanzaDate = new Date(); 16 | ping.pong(pi); 17 | return true; 18 | }); 19 | 20 | if (self.pingInterval > 0) { 21 | this.stropheConnection.addHandler(() => { 22 | self.lastStanzaDate = new Date(); 23 | return true; 24 | }); 25 | this.stropheConnection.addTimedHandler(1000, () => { 26 | const now = new Date(); 27 | if (!self.lastStanzaDate) { 28 | self.lastStanzaDate = now; 29 | } 30 | const interval = (now - self.lastStanzaDate) / 1000; 31 | if (interval > self.pingInterval) { 32 | return self.ping(); 33 | } 34 | return true; 35 | }); 36 | } 37 | } 38 | 39 | ping(jid) { 40 | this.lastStanzaDate = new Date(); 41 | if (jid === undefined) { 42 | const bareJid = Strophe.getBareJidFromJid(this.stropheConnection.jid); 43 | jid = Strophe.getDomainFromJid(bareJid); 44 | } 45 | this.stropheConnection.ping.ping(jid, null, null, null); 46 | return true; 47 | } 48 | } 49 | 50 | export default Ping; 51 | -------------------------------------------------------------------------------- /src/web/sdk/core/strophe.js: -------------------------------------------------------------------------------- 1 | import strophe, { Strophe } from 'strophe.js'; 2 | import 'strophejs-plugin-disco'; 3 | import 'strophejs-plugin-ping'; 4 | import 'strophejs-plugin-vcard'; 5 | // ie <= 8 使用 6 | // import '../common/lib/strophejs-plugin-iexdomain'; 7 | 8 | // Strophe.js => export default => 9 | // root.Strophe = wrapper.Strophe; 10 | // root.$build = wrapper.$build; 11 | // root.$iq = wrapper.$iq; 12 | // root.$msg = wrapper.$msg; 13 | // root.$pres = wrapper.$pres; 14 | // root.SHA1 = wrapper.SHA1; 15 | // root.MD5 = wrapper.MD5; 16 | // root.b64_hmac_sha1 = wrapper.b64_hmac_sha1; 17 | // root.b64_sha1 = wrapper.b64_sha1; 18 | // root.str_hmac_sha1 = wrapper.str_hmac_sha1; 19 | // root.str_sha1 = wrapper.str_sha1; 20 | 21 | // const { Strophe } = strophe; 22 | 23 | Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates'); 24 | Strophe.addNamespace('REGISTER', 'jabber:iq:register'); 25 | Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx'); 26 | Strophe.addNamespace('XFORM', 'jabber:x:data'); 27 | Strophe.addNamespace('CSI', 'urn:xmpp:csi:0'); 28 | 29 | const { $msg, $iq, MD5, $pres } = strophe 30 | 31 | export { Strophe, $msg, $iq, MD5, $pres }; 32 | export default strophe; 33 | -------------------------------------------------------------------------------- /src/web/sdk/core/upload.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import '../common/lib/jquery.iframe.transport'; 3 | import '../common/lib/jquery.fileupload'; 4 | import '../common/lib/jquery.md5'; 5 | import { 6 | createUUID, 7 | bytesToMB, 8 | bytesToSize, 9 | checkUpLoadFileExist 10 | } from '../common/utils/utils'; 11 | 12 | const limitFileSize = 1024 * 1024 * 50; // 50M; 13 | const iframe = (() => { 14 | let ret = false; 15 | const browser = window.navigator.appName; 16 | const version = (window.navigator.appVersion || '').split(';'); 17 | if (browser === 'Microsoft Internet Explorer' 18 | && version.length > 1 19 | && parseInt(version[1].replace(/[ ]/g, '').replace(/MSIE/g, ''), 10) < 10 20 | ) { 21 | ret = true; 22 | } 23 | return ret; 24 | })(); 25 | 26 | // $inputImg.fileupload({ 27 | // drop: function (e, data) { 28 | // $.each(data.files, function (index, file) { 29 | // alert('Dropped file: ' + file.name); 30 | // }); 31 | // } 32 | // }); 33 | 34 | function image(beforeFn, successFn, progressFn, filesList, onlyUrl) { 35 | const { myId, key } = this; 36 | const url = `/file/v2/upload/img?size=48&u=${myId}&k=${key}`; 37 | const $inputImg = $(''); 38 | $(window.document.body).append($inputImg); 39 | $inputImg.fileupload({ 40 | url, 41 | dataType: 'json', 42 | autoUpload: false, 43 | forceIframeTransport: iframe, 44 | limitMultiFileUploadSize: 1024 * 1024 * 50, // 50M 45 | add: (e, data) => { 46 | data.process().done(() => { 47 | $.each(data.files, (index, { size, name }) => { 48 | const uuid = $.md5(createUUID()); // $.md5(file.name); 49 | const sizeMB = bytesToMB(size); 50 | const paramLink = `name=${name}&size=${sizeMB}&u=${myId}&k=${key}&key=${uuid}&p=qim_web`; 51 | const newUrl = `/file/v2/upload/img?${paramLink}`; 52 | if (size > limitFileSize) { 53 | alert('图片大小不能超过50M'); 54 | return; 55 | } 56 | // 设置新的提交地址 57 | data.setSubmitURL(newUrl); 58 | // 校验上传的图片是否存在 59 | // 如果图片存在了就不上传了,直接显示为和上传成功的效果 60 | const checkFileUrl = `/file/v2/inspection/img?${paramLink}`; 61 | checkUpLoadFileExist(checkFileUrl) 62 | .done(async (res) => { 63 | beforeFn(); 64 | if (res.ret) { 65 | data.submit(); 66 | } else { 67 | let result = res.data; // 存在的图片URL地址 68 | if (iframe) { 69 | result = $('pre', result).text(); 70 | } 71 | const msg = ``; 72 | if (onlyUrl) { 73 | successFn(result); 74 | } else { 75 | // 发送消息 76 | const ret = await this.sendMessage(msg); 77 | successFn(ret); 78 | } 79 | progressFn(100); 80 | $inputImg.remove(); 81 | } 82 | }); 83 | }); 84 | }); 85 | }, 86 | done: async (e, data) => { 87 | let result = data.result.data; 88 | if (iframe) { 89 | result = $('pre', result).text(); 90 | } 91 | const msg = ``; 92 | if (onlyUrl) { 93 | successFn(result); 94 | } else { 95 | // 发送消息 96 | const ret = await this.sendMessage(msg); 97 | successFn(ret); 98 | } 99 | $inputImg.remove(); 100 | }, 101 | progress: (e, data) => { 102 | const progress = parseInt((data.loaded / data.total) * 100, 10); 103 | progressFn(progress); 104 | } 105 | }); 106 | if (filesList && filesList.length > 0) { 107 | $inputImg.fileupload('add', { files: filesList }); 108 | } else { 109 | $inputImg.trigger('click'); 110 | } 111 | } 112 | 113 | function file(beforeFn, successFn, progressFn, filesList, onlyUrl) { 114 | const { myId, key } = this; 115 | const url = `/file/v2/upload/file?size=46&u=${myId}&k=${key}`; 116 | const $inputFile = $(''); 117 | $(window.document.body).append($inputFile); 118 | $inputFile.fileupload({ 119 | url, 120 | dataType: 'json', 121 | forceIframeTransport: iframe, 122 | add: (e, data) => { 123 | data.process().done(() => { 124 | $.each(data.files, (index, { size, name }) => { 125 | const uuid = $.md5(createUUID()); // $.md5(file.name); 126 | const sizeMB = bytesToMB(size); 127 | const paramLink = `name=${name}&size=${sizeMB}&u=${myId}&k=${key}&key=${uuid}&p=qim_web`; 128 | const newUrl = `/file/v2/upload/file?${paramLink}`; 129 | if (size > limitFileSize) { 130 | alert('图片大小不能超过50M'); 131 | return; 132 | } 133 | // 设置新的提交地址 134 | data.setSubmitURL(newUrl); 135 | // 校验上传的文件是否存在 136 | // 如果文件存在了就不上传了,直接显示为和上传成功的效果 137 | const checkFileUrl = `/file/v2/inspection/file?${paramLink}`; 138 | checkUpLoadFileExist(checkFileUrl) 139 | .done(async (res) => { 140 | beforeFn(); 141 | if (res.ret) { 142 | data.submit(); 143 | } else { 144 | let result = res.data; // 存在的文件URL地址 145 | if (iframe) { 146 | result = $('pre', result).text(); 147 | } 148 | const msg = { 149 | FILEID: new Date().getTime(), 150 | FILEMD5: '123', 151 | FileName: name, 152 | FileSize: bytesToSize(size), 153 | HttpUrl: result 154 | }; 155 | if (onlyUrl) { 156 | successFn(result); 157 | } else { 158 | // 发送消息 159 | const ret = await this.sendMessage(JSON.stringify(msg), 5); 160 | successFn(ret); 161 | } 162 | progressFn(100); 163 | $inputFile.remove(); 164 | } 165 | }); 166 | }); 167 | }); 168 | }, 169 | done: async (e, data) => { 170 | let result = data.result.data; 171 | if (iframe) { 172 | result = $('pre', result).text(); 173 | } 174 | if (data && data.files && data.files.length > 0) { 175 | const msg = { 176 | FILEID: new Date().getTime(), 177 | FILEMD5: '123', 178 | FileName: data.files[0].name, 179 | FileSize: bytesToSize(data.files[0].size), 180 | HttpUrl: result 181 | }; 182 | if (onlyUrl) { 183 | successFn(result); 184 | } else { 185 | // 发送消息 186 | const ret = await this.sendMessage(JSON.stringify(msg), 5); 187 | successFn(ret); 188 | } 189 | } 190 | $inputFile.remove(); 191 | }, 192 | progress: (e, data) => { 193 | const progress = parseInt((data.loaded / data.total) * 100, 10); 194 | progressFn(progress); 195 | } 196 | }); 197 | if (filesList && filesList.length > 0) { 198 | $inputFile.fileupload('add', { files: filesList }); 199 | } else { 200 | $inputFile.trigger('click'); 201 | } 202 | } 203 | 204 | export default { 205 | image, 206 | file 207 | }; 208 | -------------------------------------------------------------------------------- /src/web/sdk/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: your name 4 | * @Date: 2019-08-05 21:14:24 5 | * @LastEditTime: 2019-08-13 11:47:33 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | export default { 9 | debug: false, 10 | xmpp:"", 11 | // 链接配置 12 | connect: { 13 | reconnectCount: 10, // 最多重连10次 14 | delay: 5000, // 重新连接间隔 15 | host: '', // 主机名 16 | // port: 80, // 端口 17 | path: '' // 路径 18 | }, 19 | maType: 6, // 平台类型web端:6 20 | // 20 秒ping一次 21 | pingInterval: 20 22 | }; 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: In User Settings Edit 3 | * @Author: xi.guo 4 | * @Date: 2019-08-05 14:54:15 5 | * @LastEditTime: 2019-08-13 19:54:57 6 | * @LastEditors: Please set LastEditors 7 | */ 8 | 9 | const MiniCssExtractPlugin = require("mini-css-extract-plugin") 10 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') 11 | const CopyPlugin = require('copy-webpack-plugin') 12 | const merge = require('webpack-merge') 13 | const path = require('path') 14 | const dotenv = require('dotenv') 15 | dotenv.config() 16 | process.env.NODE_ENV === 'production' && require('./dotenv') 17 | const _mode = process.env.NODE_ENV || 'development' 18 | const _mergeConfig = require(`./config/webpack.${_mode}.js`) 19 | const HtmlWebpackPlugin = require('html-webpack-plugin') 20 | const htmlAfterWebpackPlugin = require('./config/htmlAfterWebpackPlugin') 21 | const ASSETS = path.join(__dirname, '/dist/assets/') 22 | 23 | // 用户可配置 config 24 | const devPaths ={ 25 | COMMON: path.join(__dirname , '/src/web/common'), 26 | COMPONENTS: path.join(__dirname , '/src/web/components'), 27 | CONTAINERS: path.join(__dirname , '/src/web/containers'), 28 | HOC: path.join(__dirname , '/src/web/hoc'), 29 | IMAGES: path.join(__dirname , '/src/web/images'), 30 | MODULES: path.join(__dirname , '/src/web/modules'), 31 | STORE: path.join(__dirname , '/src/web/store'), 32 | CONFIG: path.join(__dirname , '/src/web/config'), 33 | strophe: 'strophe.js' 34 | } 35 | 36 | // 用户可配置 config 37 | const entry = () => { 38 | return { 39 | _startalk_sdk: './src/web/sdk/entry.js', 40 | index: './src/web/app/pages/index/entry.js' 41 | } 42 | } 43 | 44 | let webpackConfig = { 45 | mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', 46 | entry: entry(), 47 | output: { 48 | filename: 'scripts/[name].js', 49 | // 静态资源输出路径 50 | path: ASSETS, 51 | // 所有资源的基础路径 52 | publicPath: process.env.PUBLICPATH 53 | }, 54 | module: { 55 | rules: [ 56 | { 57 | test: /\.js$/, 58 | exclude: /node_modules/, 59 | use: { 60 | loader: 'babel-loader?cacheDirectory', 61 | options: { 62 | 'presets': [ '@babel/preset-env', '@babel/preset-react'], 63 | plugins: [ 64 | ['@babel/plugin-proposal-decorators',{ 'legacy': true }], 65 | '@babel/plugin-proposal-class-properties', 66 | // 配合路由懒加载 67 | '@babel/plugin-syntax-dynamic-import', 68 | '@babel/plugin-transform-runtime' 69 | ] 70 | } 71 | } 72 | }, 73 | { 74 | test: /\.css$/, 75 | use: [ 76 | MiniCssExtractPlugin.loader, 77 | 'css-loader', 78 | { loader: 'postcss-loader', options: { plugins: [ require('autoprefixer') ]}} 79 | ] 80 | }, 81 | { 82 | test:/\.less$/, 83 | use:[ 84 | MiniCssExtractPlugin.loader, 85 | 'css-loader', 86 | { loader: 'postcss-loader', options: { plugins: [ require('autoprefixer') ] }}, 87 | 'less-loader' 88 | ] 89 | }, 90 | { 91 | test: /\.(png|jpg|jpeg|gif)$/, 92 | use: [ 93 | { 94 | loader: 'url-loader', 95 | options: { 96 | name: "[name]-[hash:5].min.[ext]", 97 | limit: 1024, 98 | publicPath: "../images/", 99 | outputPath: "images/" 100 | } 101 | }, 102 | { 103 | loader: 'img-loader' 104 | } 105 | ] 106 | }, 107 | { 108 | test: /\.html$/, 109 | loader: 'html-loader', 110 | } 111 | ] 112 | }, 113 | resolve: { 114 | extensions: ["*",".jsx",".js"], 115 | alias: devPaths 116 | }, 117 | optimization: { 118 | splitChunks: { 119 | cacheGroups: { 120 | common: { 121 | test: /[\\/]node_modules[\\/]/, 122 | name: 'common', 123 | chunks: 'all', 124 | priority: 2, 125 | minChunks: 2, 126 | }, 127 | } 128 | } 129 | }, 130 | plugins: [ 131 | new OptimizeCssAssetsPlugin(), 132 | new CopyPlugin([{ 133 | from: path.join(__dirname, './package.json'), 134 | to: path.join(__dirname, '/dist/package.json') 135 | }]), 136 | new HtmlWebpackPlugin({ 137 | template: __dirname + '/src/web/app/index.html', 138 | //development 环境使用 webpack-dev-middleware 插件打包资源到内存 139 | //production 环境打包到 dist/view 作为 node 的 html 模板 140 | filename: _mode === 'production' ? '../views/index.html' : 'index.html', 141 | inject: false, 142 | minify: false, 143 | favicon: __dirname + '/src/assets/favicon.ico' 144 | }), 145 | new htmlAfterWebpackPlugin() 146 | ] 147 | } 148 | 149 | module.exports = merge(webpackConfig, _mergeConfig) -------------------------------------------------------------------------------- /开发人员使用手册.md: -------------------------------------------------------------------------------- 1 | # 开发人员使用手册 2 | 3 | ## 代码各部分简介 4 | 5 | 一共分为四个文件夹:config,profiles,src,dist 6 | 7 | ### config文件夹 8 | 对webpack打包过程进行了针对性配置 9 | 10 | ### profiles文件夹 11 | 对开发和生产两种不同环境来配置不同的项目启动端口,IP,后台接口地址和公共路径 12 | 13 | ### src文件夹 14 | 15 | * assets 16 | 存放静态文件的目录,如图片、字体等,不存放代码类文件,如CSS、JavaScript 17 | * nodeuii 18 | node部分的代码,配置路由和中间件 19 | * web 20 | 为主要不断完善维护部分,app文件夹为页面逻辑,sdk文件夹为与后台交互逻辑 21 | 22 | ### dist文件夹 23 | 运行打包命令之后会打包进该文件夹,进行dev打包时静态文件会在本机内存中进行运行,线上环境打包时会将静态文件同时打包进该文件夹 24 | 25 | # 功能清单 26 | 27 | | **功能** | **功能说明** | 28 | | ------------ | ------------------------------------------------------------ | 29 | | 单聊 | 两人聊天 | 30 | | 群聊 | 多人聊天,群个数无上限,单个群的群人数无上限;支持创建群、添加群成员、退出群聊支持修改群名及群公告 | 31 | | 消息类型 | 支持文本、语音、图片、视频、音视频、表情、文件等 | 32 | | 已读未读标识 | 消息已读未读提示 | 33 | | 会话加密 | 除会话双方客服端,会话消息不会在任何地方留存 | 34 | | @ | 群组中,当@某人时,被@的人会收到提示 | 35 | | 置顶 | 重要群组及联系人会话置顶,被置顶的会话展示在列表最上方 | 36 | | 消息提醒 | 未读消息数提示 | 37 | | 引用 | 引用提问回复,避免群内消息量大时上下文连不上 | 38 | | Push | 支持通过push提示新消息 | 39 | | 多端同步 | 支持消息在移动端、PC端同步 | 40 | | 全局搜索 | 支持搜索联系人、群组、共同群组 | 41 | | 组织架构 | 支持多级树形组织架构 | 42 | | 好友 | 支持查看好友 | 43 | | 群组 | 展示所有群组,快速进入群组 | 44 | 45 | # 接口汇总 46 | 47 | | | 接口 | 接口路径 | 48 | | :--- | :--------------------- | :----------------------------------------------- | 49 | | 1 | 获取用户名片信息 | /newapi/domain/get_vcard_info.qunar | 50 | | 2 | 获取用户个性签名 | /newapi/domain/get_vcard_info.qunar | 51 | | 3 | 更新用户个性签名 | /newapi/profile/set_profile.qunar | 52 | | 4 | 获取群名片 | /newapi/muc/get_muc_vcard.qunar | 53 | | 5 | 获取组织架构 | /newapi/getUpdateUsers.qunar | 54 | | 6 | 更新群名片 | /newapi/muc/set_muc_vcard.qunar | 55 | | 7 | 获取置顶信息 | /newapi/configuration/getincreclientconfig.qunar | 56 | | 8 | 设置置顶 | /newapi/configuration/setclientconfig.qunar | 57 | | 9 | 群列表 | newapi/muc/get_increment_mucs.qunar | 58 | | 10 | 获取状态码 | newapi/domain/get_user_status.qunar | 59 | | 11 | 获取直属领导,员工编号 | 需使用者自己实现 | 60 | | 12 | 查询用户电话 | 需使用者自己实现 | 61 | | 13 | 获取会话列表 | /package/qtapi/getrbl.qunar | 62 | | 14 | 获取单人历史消息 | /package/qtapi/getmsgs.qunar | 63 | | 15 | 获取群历史消息 | /package/qtapi/getmucmsgs.qunar | 64 | | 17 | 查询用户和群组 | /py/search | 65 | | 18 | php获取域列表 | /newapi/domain/get_domain_list.qunar?t=qtalk | 66 | 67 | --------------------------------------------------------------------------------