Commit f958b975 by keith

into client,admin public

parent d9c0acfd
Showing with 11205 additions and 22 deletions
......@@ -2,4 +2,5 @@ attach
mimc.log
kefu_server
kefu_server.tar.gz
conf/app.back.conf
\ No newline at end of file
conf/app.back.conf
node_modules
\ No newline at end of file
File mode changed
......@@ -36,8 +36,8 @@ mimc_appSecret = "wjLFWivIORCFsi3tHr9wHQ=="
# IM数据库信息
kf_alias_name = "default"
kf_driver_name= "mysql"
kf_mysql_host = "192.168.31.72"
kf_mysql_port = "3306"
kf_mysql_host = "aissz.com"
kf_mysql_port = "3636"
kf_mysql_user = "root"
kf_mysql_db = "kefu_server_dev"
kf_mysql_pwd = "chenxianqi"
......
File mode changed
File mode changed
......@@ -142,8 +142,8 @@ func (c *WorkOrderController) DeleteWorkType() {
c.JSON(configs.ResponseSucess, "删除成功!", nil)
}
// GetType get work order type
func (c *WorkOrderController) GetType() {
// GetWorkType get work order type
func (c *WorkOrderController) GetWorkType() {
// id
id, _ := strconv.ParseInt(c.Ctx.Input.Param(":id"), 10, 64)
......@@ -155,17 +155,8 @@ func (c *WorkOrderController) GetType() {
}
// GetTypes get work order types
func (c *WorkOrderController) GetTypes() {
}
// PutType update work order type
func (c *WorkOrderController) PutType() {
}
// DeleteType delete work order type
func (c *WorkOrderController) DeleteType() {
// GetWorkTypes get work order types
func (c *WorkOrderController) GetWorkTypes() {
workOrderTypes := c.WorkOrderTypeRepository.GetWorkOrderTypes()
c.JSON(configs.ResponseSucess, "查询成功!", workOrderTypes)
}
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
No preview for this file type
No preview for this file type
No preview for this file type

4.35 KB | W: | H:

4.35 KB | W: | H:

public/admin/img/kefu_logo.9c308a55.png
public/admin/img/kefu_logo.9c308a55.png
public/admin/img/kefu_logo.9c308a55.png
public/admin/img/kefu_logo.9c308a55.png
  • 2-up
  • Swipe
  • Onion skin

237 KB | W: | H:

237 KB | W: | H:

public/admin/img/login_bg.8ba760be.jpg
public/admin/img/login_bg.8ba760be.jpg
public/admin/img/login_bg.8ba760be.jpg
public/admin/img/login_bg.8ba760be.jpg
  • 2-up
  • Swipe
  • Onion skin

23.4 KB | W: | H:

23.4 KB | W: | H:

public/admin/img/login_bg1.531e0c1c.jpg
public/admin/img/login_bg1.531e0c1c.jpg
public/admin/img/login_bg1.531e0c1c.jpg
public/admin/img/login_bg1.531e0c1c.jpg
  • 2-up
  • Swipe
  • Onion skin
File mode changed
File mode changed
File mode changed
File mode changed
No preview for this file type

8.64 KB | W: | H:

8.64 KB | W: | H:

public/client/img/expression.73c98a16.png
public/client/img/expression.73c98a16.png
public/client/img/expression.73c98a16.png
public/client/img/expression.73c98a16.png
  • 2-up
  • Swipe
  • Onion skin

5.59 KB | W: | H:

5.59 KB | W: | H:

public/client/img/photo_btn.c337b681.png
public/client/img/photo_btn.c337b681.png
public/client/img/photo_btn.c337b681.png
public/client/img/photo_btn.c337b681.png
  • 2-up
  • Swipe
  • Onion skin
File mode changed
File mode changed
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: 'babel-eslint'
}
}
node_modules
dist
build
\ No newline at end of file
客服系统开发者QQ交流群: 623661658
# 欢迎使用本客服系统 - 客服端-工作台
![客服系统](http://qiniu.cmp520.com/kefuxitonh.jpg)
## 本项目关联GIT项目资源连接
- **[服务端][1]**
- **[客服端Flutter][5]**
- **[客服端][2]**
- **[客户端H5][3]**
- **[客户端Flutter][4]**
**本系统** 是基于小米消息云实现的一款简单实用的面向多终端的客服系统,本系统简单易用,易扩展,易整合现有的业务系统,无缝对接自有业务。
## 安装
```
npm install
npm run serve
npm run build
npm run test
npm run lint
```
## 打包基于Electron的二进制包
```
npm run build # 先执行
npm run win32
npm run win64
npm run mac
npm run linux32
npm run linux64
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
### TypeError: Cannot destructure property `createHash` of 'undefined' or 'null'.
npm add webpack@latest OK
[1]: https://github.com/chenxianqi/kefu_server
[2]: https://github.com/chenxianqi/kefu_admin
[3]: https://github.com/chenxianqi/kefu_client
[4]: https://github.com/chenxianqi/kefu_flutter
[5]: https://github.com/chenxianqi/kefu_workbench
module.exports = {
presets: [
'@vue/app'
]
}
No preview for this file type
const electron = require('electron')
const app = electron.app
const dialog = electron.dialog;
const BrowserWindow = electron.BrowserWindow
const Menu = electron.Menu
const path = require('path')
if (process.mas) app.setName('客服系统')
let mainWindow
function createWindow () {
let windowOptions = {
width: 1330,
height: 780,
title: app.getName()
}
if (process.platform === 'linux') {
windowOptions.icon = path.join(__dirname, '/logo.ico')
}
mainWindow = new BrowserWindow(windowOptions);
mainWindow.loadURL(path.join('file://', __dirname, '/dist/index.html'))
mainWindow.on('closed', function () {
mainWindow = null
})
mainWindow.on('close', function (e) {
const options = {
type: 'warning',
title: '温馨提示!',
message: "您确定关闭应用程序吗?",
buttons: ['我点错了', '窗口最小化', '确定关闭']
}
dialog.showMessageBox(options, function (index) {
if(index == 2) app.exit();
if(index == 1) mainWindow.minimize()
})
e.preventDefault()
})
}
app.on('ready', function() {
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
createWindow()
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', function () {
if (mainWindow === null) createWindow()
})
app.on('browser-window-created', function () {
let reopenMenuItem = findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled = false
})
app.on('window-all-closed', function () {
let reopenMenuItem = findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled = true
app.quit()
})
/**
* 注册键盘快捷键
*/
let template = [
{
label: '操作',
submenu: [{
label: '重新加载',
accelerator: 'CmdOrCtrl+R',
click: function (item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow.id === 1) {
BrowserWindow.getAllWindows().forEach(function (win) {
if (win.id > 1) {
win.close()
}
})
}
focusedWindow.loadURL(path.join('file://', __dirname, '/dist/index.html'))
}
}
},
{
label: "选项",
submenu: [
{ label: "复制", accelerator: "CmdOrCtrl+C", selector: "copy:" },
{ label: "粘贴", accelerator: "CmdOrCtrl+V", selector: "paste:" },
]
}]
},
{
label: '窗口设置',
role: 'window',
submenu: [{
label: '最小化',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
},{
label: '切换开发者工具',
accelerator: (function () {
if (process.platform === 'darwin') {
return 'Alt+Command+I'
} else {
return 'Ctrl+Shift+I'
}
})(),
click: function (item, focusedWindow) {
if (focusedWindow) {
focusedWindow.toggleDevTools()
}
}
}]
},
{
label: '关于',
role: 'help',
submenu: [{
label: '客服',
click: function () {
electron.shell.openExternal('http://kf.aissz.com:666')
}
}]
}
]
/**
* 增加更新相关的菜单选项
*/
function addUpdateMenuItems (items, position) {
if (process.mas) return
const version = electron.app.getVersion()
let updateItems = [{
label: `当前版本 ${version}`,
enabled: false
}]
items.splice.apply(items, [position, 0].concat(updateItems))
}
function findReopenMenuItem () {
const menu = Menu.getApplicationMenu()
if (!menu) return
let reopenMenuItem
menu.items.forEach(function (item) {
if (item.submenu) {
item.submenu.items.forEach(function (item) {
if (item.key === 'reopenMenuItem') {
reopenMenuItem = item
}
})
}
})
return reopenMenuItem
}
// 针对Mac端的一些配置
if (process.platform === 'darwin') {
const name = electron.app.getName()
template.unshift({
label: name,
submenu: [{
label: '退出应用',
accelerator: 'Command+Q',
click: function () {
app.exit()
}
}]
})
// Window menu.
template[3].submenu.push({
type: 'separator'
})
addUpdateMenuItems(template[0].submenu, 1)
}
// 针对Windows端的一些配置
if (process.platform === 'win32') {
const helpMenu = template[template.length - 1].submenu
addUpdateMenuItems(helpMenu, 0)
}
This diff could not be displayed because it is too large.
{
"name": "kefu_admin",
"version": "0.0.1",
"private": true,
"main": "main.js",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"open": "electron .",
"win32": "electron-builder --win --ia32",
"win64": "electron-builder --win --x64",
"mac": "electron-builder --mac --x64",
"linux32": "electron-builder --linux --ia32",
"linux64": "electron-builder --linux --x64"
},
"build": {
"appId": "im.aissz.com:666",
"productName": "客服系统",
"win": {
"icon": "logo.ico"
},
"mac": {
"icon": "mac.png"
},
"directories": {
"output": "build"
}
},
"dependencies": {
"axios": "^0.19.0",
"core-js": "^2.6.5",
"echarts": "^4.2.1",
"element-ui": "^2.10.1",
"moment": "^2.24.0",
"push.js": "^1.0.12",
"qiniu-js": "^2.5.5",
"v-charts": "^1.19.0",
"vue": "^2.6.10",
"vue-photo-preview": "git+https://github.com/chenxianqi/vue-photo-preview.git",
"vue-router": "^3.0.3",
"vuex": "^3.1.1",
"webpack": "^4.41.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.9.0",
"@vue/cli-plugin-eslint": "^3.9.0",
"@vue/cli-service": "^3.9.0",
"babel-eslint": "^10.0.1",
"electron": "^6.0.7",
"electron-builder": "^21.2.0",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"html-webpack-plugin": "^4.0.0-beta.8",
"mini-css-extract-plugin": "^0.8.0",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"vue-loader": "^15.7.1",
"vue-template-compiler": "^2.6.10",
"webpack-cli": "^3.3.8"
}
}
module.exports = {
plugins: {
autoprefixer: {}
}
}
(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory(global):typeof define==="function"&&define.amd?define(factory):factory(global)})(typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:this,function(global){"use strict";global=global||{};var _Base64=global.Base64;var version="2.5.1";var buffer;if(typeof module!=="undefined"&&module.exports){try{buffer=eval("require('buffer').Buffer")}catch(err){buffer=undefined}}var b64chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var b64tab=function(bin){var t={};for(var i=0,l=bin.length;i<l;i++)t[bin.charAt(i)]=i;return t}(b64chars);var fromCharCode=String.fromCharCode;var cb_utob=function(c){if(c.length<2){var cc=c.charCodeAt(0);return cc<128?c:cc<2048?fromCharCode(192|cc>>>6)+fromCharCode(128|cc&63):fromCharCode(224|cc>>>12&15)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}else{var cc=65536+(c.charCodeAt(0)-55296)*1024+(c.charCodeAt(1)-56320);return fromCharCode(240|cc>>>18&7)+fromCharCode(128|cc>>>12&63)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}};var re_utob=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;var utob=function(u){return u.replace(re_utob,cb_utob)};var cb_encode=function(ccc){var padlen=[0,2,1][ccc.length%3],ord=ccc.charCodeAt(0)<<16|(ccc.length>1?ccc.charCodeAt(1):0)<<8|(ccc.length>2?ccc.charCodeAt(2):0),chars=[b64chars.charAt(ord>>>18),b64chars.charAt(ord>>>12&63),padlen>=2?"=":b64chars.charAt(ord>>>6&63),padlen>=1?"=":b64chars.charAt(ord&63)];return chars.join("")};var btoa=global.btoa?function(b){return global.btoa(b)}:function(b){return b.replace(/[\s\S]{1,3}/g,cb_encode)};var _encode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(u){return(u.constructor===buffer.constructor?u:buffer.from(u)).toString("base64")}:function(u){return(u.constructor===buffer.constructor?u:new buffer(u)).toString("base64")}:function(u){return btoa(utob(u))};var encode=function(u,urisafe){return!urisafe?_encode(String(u)):_encode(String(u)).replace(/[+\/]/g,function(m0){return m0=="+"?"-":"_"}).replace(/=/g,"")};var encodeURI=function(u){return encode(u,true)};var re_btou=new RegExp(["[À-ß][€-¿]","[à-ï][€-¿]{2}","[ð-÷][€-¿]{3}"].join("|"),"g");var cb_btou=function(cccc){switch(cccc.length){case 4:var cp=(7&cccc.charCodeAt(0))<<18|(63&cccc.charCodeAt(1))<<12|(63&cccc.charCodeAt(2))<<6|63&cccc.charCodeAt(3),offset=cp-65536;return fromCharCode((offset>>>10)+55296)+fromCharCode((offset&1023)+56320);case 3:return fromCharCode((15&cccc.charCodeAt(0))<<12|(63&cccc.charCodeAt(1))<<6|63&cccc.charCodeAt(2));default:return fromCharCode((31&cccc.charCodeAt(0))<<6|63&cccc.charCodeAt(1))}};var btou=function(b){return b.replace(re_btou,cb_btou)};var cb_decode=function(cccc){var len=cccc.length,padlen=len%4,n=(len>0?b64tab[cccc.charAt(0)]<<18:0)|(len>1?b64tab[cccc.charAt(1)]<<12:0)|(len>2?b64tab[cccc.charAt(2)]<<6:0)|(len>3?b64tab[cccc.charAt(3)]:0),chars=[fromCharCode(n>>>16),fromCharCode(n>>>8&255),fromCharCode(n&255)];chars.length-=[0,0,2,1][padlen];return chars.join("")};var _atob=global.atob?function(a){return global.atob(a)}:function(a){return a.replace(/\S{1,4}/g,cb_decode)};var atob=function(a){return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g,""))};var _decode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(a){return(a.constructor===buffer.constructor?a:buffer.from(a,"base64")).toString()}:function(a){return(a.constructor===buffer.constructor?a:new buffer(a,"base64")).toString()}:function(a){return btou(_atob(a))};var decode=function(a){return _decode(String(a).replace(/[-_]/g,function(m0){return m0=="-"?"+":"/"}).replace(/[^A-Za-z0-9\+\/]/g,""))};var noConflict=function(){var Base64=global.Base64;global.Base64=_Base64;return Base64};global.Base64={VERSION:version,atob:atob,btoa:btoa,fromBase64:decode,toBase64:encode,utob:utob,encode:encode,encodeURI:encodeURI,btou:btou,decode:decode,noConflict:noConflict,__buffer__:buffer};if(typeof Object.defineProperty==="function"){var noEnum=function(v){return{value:v,enumerable:false,writable:true,configurable:true}};global.Base64.extendString=function(){Object.defineProperty(String.prototype,"fromBase64",noEnum(function(){return decode(this)}));Object.defineProperty(String.prototype,"toBase64",noEnum(function(urisafe){return encode(this,urisafe)}));Object.defineProperty(String.prototype,"toBase64URI",noEnum(function(){return encode(this,true)}))}}if(global["Meteor"]){Base64=global.Base64}if(typeof module!=="undefined"&&module.exports){module.exports.Base64=global.Base64}else if(typeof define==="function"&&define.amd){define([],function(){return global.Base64})}return{Base64:global.Base64}});
\ No newline at end of file
No preview for this file type
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script type="text/javascript" src="<%= BASE_URL %>mimc-min_1_0_2.js"></script>
<script type="text/javascript" src="<%= BASE_URL %>base64.min.js"></script>
<title>客服系统</title>
<style>
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, dl, dt, dd, ul, ol, li, pre, form, fieldset, legend, button, input, textarea, th, td { margin:0; padding:0; }
body, button, input, select, textarea { font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; }
h1, h2, h3, h4, h5, h6{ font-size:100%; }
address, cite, dfn, em, var { font-style:normal; }
code, kbd, pre, samp { font-family:couriernew, courier, monospace; }
small{ font-size:12px; }
ul, ol { list-style:none; }
a { text-decoration:none;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
-webkit-user-select: none;
-moz-user-focus: none;
-moz-user-select: none;
}
a:hover { text-decoration:underline; }
sup { vertical-align:text-top; }
sub{ vertical-align:text-bottom; }
legend { color:#000; }
fieldset, img { border:0; }
button, input, select, textarea { font-size:100%; outline: none;}
table { border-collapse:collapse; border-spacing:0; }
input{
border:0;
outline: none;
}
body{
height: 100vh;
overflow: hidden;
overflow-y: auto;
-webkit-overflow-scrolling:touch;
background-color: #fff;
}
.lx-load-box{
width: 2rem !important;
height: 2rem !important;
top:0 !important;
min-height: inherit!important;
left:0 !important; right:0 !important; bottom:0 !important; margin: auto !important;
}
input::-webkit-input-placeholder{
color:#ccc;
}
input::-moz-placeholder{ /* Mozilla Firefox 19+ */
color:#ccc;
}
input:-moz-placeholder{ /* Mozilla Firefox 4 to 18 */
color:#ccc;
}
input:-ms-input-placeholder{ /* Internet Explorer 10-11 */
color:#ccc;
}
</style>
</head>
<body>
<noscript>
<strong>We're sorry but m doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
This diff could not be displayed because it is too large.
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
import axios from "axios";
import Push from "push.js";
export default {
created(){
this.$store.dispatch('ON_GET_ME')
},
methods: {
// app init
appInit(){
if(!this.adminInfo){
setTimeout(()=> this.appInit(), 50)
return
}
this.$store.dispatch('ON_GET_UPLOAD_TOKEN')
this.$store.dispatch('ON_GET_PLATFORM_CONFIG')
this.$store.dispatch('ON_GET_SYSTEM')
this.$store.dispatch('ON_GET_COMPANY')
this.$store.dispatch('ON_GET_UPLOADS_CONFIG')
this.$store.dispatch('ON_GET_ROBOTS')
this.$store.dispatch('ON_GET_CONTACTS')
// 一分钟上报一次我的活动时间
this.upLastActivity()
// 获取会话表
this.getContacts()
// Mimc 初始化
this.initMimc()
},
// 获取会话列表
getContacts(){
if(this.adminInfo){
this.$store.dispatch('ON_GET_CONTACTS')
if(this.seviceCurrentUser && this.$store.getters.contacts.length > 0){
this.$store.getters.contacts.map(i => {
if(i.from_account == this.seviceCurrentUser.from_account){
this.$store.commit("onChangeSeviceCurrentUser", i)
}
})
}
}
},
// 上报最后活动时间
upLastActivity(){
this.$store.dispatch('ON_RUN_LAST_ACTiIVITY')
setTimeout(() => this.upLastActivity(), 1000*60)
},
// 初始化Mimc
initMimc(){
var self = this
var adminInfo = this.$store.state.adminInfo
if(!adminInfo){
setTimeout(() => this.initMimc(), 1000)
}else{
self.$mimcInstance.init({
type: 1,
account_id: adminInfo.id
}, (isSuccess) => {
// 初始化完成
if(isSuccess){
// 监听登录状态
this.$mimcInstance.addEventListener("statusChange", (status) => {
if(!status && self.$store.getters.adminInfo.online != 0){
self.watchLogin()
}
})
// 监听连接断开
this.$mimcInstance.addEventListener("disconnect", () => {
console.log("链接断开!")
if(self.$store.getters.adminInfo.online != 0){
self.watchLogin()
}
})
self.watchLogin()
}else{
self.initMimc()
}
})
}
},
// 更新用户状态
changeUserOnlineStatus(online){
// 更新状态
axios.put('/admin/online/' + online)
.then(() => {
this.$store.dispatch('ON_GET_ME')
if(online == 0){
this.$message.info("当前状态为离线")
}
})
.catch(error => {
this.$message.error(error.response.data.message)
});
},
// 监听用户是否有上线登录
watchLogin(){
try{
var self = this
if(self.$store.state.user != null) return;
if(self.$store.getters.adminInfo.online == 1 || self.$store.getters.adminInfo.online == 2){
self.$mimcInstance.login(()=>{
self.changeUserOnlineStatus(self.$store.getters.adminInfo.online)
self.$store.dispatch('ON_RUN_LAST_ACTiIVITY')
self.$store.dispatch('ON_GET_CONTACTS')
self.$store.commit("onChangeMimcUser", self.$mimcInstance.user)
})
}else if(self.$store.getters.adminInfo.online != 0){
setTimeout(() => self.watchLogin(), 1000)
}
}catch(err){
setTimeout(() => this.watchLogin(), 1000)
}
},
},
mounted(){
window.addEventListener("resize", () => {
this.$store.commit("onChangeToggleAside", true)
if(document.body.clientWidth < 1000){
this.$store.commit("onChangeToggleAside", false)
}
}, false)
// 判断通知权限
if(!Push.Permission.has()){
Push.Permission.request(function(){}, function(){})
}
},
computed: {
adminInfo(){
return this.$store.getters.adminInfo
},
seviceCurrentUser(){
return this.$store.getters.seviceCurrentUser
},
messageRecord(){
return this.$store.getters.messageRecord
},
isLogin(){
return this.$store.getters.isLogin
}
},
watch: {
"$route"(){
if(!/^\/workbench(\/\d+)?$/i.test(this.$route.path)){
// 监听消息
this.$mimcInstance.addEventListener("receiveP2PMsg", (message) => {
var nowTime = parseInt((new Date().getTime() +"").substr(0, 10))
message.timestamp = parseInt((message.timestamp +"").substr(0, 10))
if(nowTime - message.timestamp >= 60) return
// 处理用户列表
if(message.biz_type == "contacts"){
var contacts = JSON.parse(message.payload)
// console.log(contacts)
this.$store.commit('onChangeContacts', contacts)
return
}
// 判断是否是握手消息
if(message.biz_type == "handshake"){
this.$mimcInstance.sendMessage("text", message.from_account, this.adminInfo.auto_reply)
return
}
var newMessageRecord = JSON.parse(JSON.stringify(this.messageRecord))
newMessageRecord.list.push(message)
this.$store.commit("onChangeMessageRecord", newMessageRecord)
// 推送消息
if(message.biz_type == "contacts" || message.biz_type == "pong" || message.biz_type == "welcome" || message.biz_type == "cancel" || message.biz_type == "handshake" || message.biz_type == "end" || message.biz_type == "timeout") return
if(!Push.Permission.has()) return
Push.create("收到一条新消息", {
body: message.payload,
icon: this.$store.state.pushIcon,
timeout: 5000,
onClick: () => {
this.$router.push({ path: '/workbench?uid=' + message.from_account})
window.focus();
}
});
})
}
},
isLogin(){
console.log("当前是登录状态")
this.appInit()
}
}
}
</script>
<style lang="stylus">
#app{
display flex
height 100vh
}
.el-tabs__content,.el-tab-pane{
height 100%
padding 0
}
.el-tabs__content{
padding 0 !important
}
button{
background-color #fff
}
.pswp{
z-index 3000!important
}
</style>
import store from '../store'
import * as qiniu from 'qiniu-js'
import axios from 'axios'
var subscription;
export default function({file, progress, success, error}){
if(!file) return
const uploadToken = store.getters.uploadToken || {}
const fileName = parseInt(Math.random() * 10000 * new Date().getTime()) + file.name.substr(file.name.lastIndexOf('.'))
// 系统内置
if(uploadToken.mode == 1){
var CancelToken = axios.CancelToken;
subscription = CancelToken.source();
let fd = new FormData();
fd.append('file',file);
fd.append('file_name', fileName);
axios.post('/public/upload', fd, {
cancelToken: subscription.token
})
.then((res) => success(res.data.data))
.catch((err) => error(err.message))
progress(100)
// 七牛云
}else if(uploadToken.mode == 2){
var observer = {
next: res => progress(Math.ceil(res.total.percent)),
error: err => error(err.message),
complete: res => success(res.key)
};
const observable = qiniu.upload(file, fileName, uploadToken.secret, {}, {})
subscription = observable.subscribe(observer)
}else{
alert("上传配置错误")
}
return subscription
}
\ No newline at end of file
<template>
<el-aside width="200px" class="mini-im-aside">
<div class="mini-im-logo" @click="$router.push({ path: '/index'})">
<div v-if="$store.getters.systemInfo.logo"><img :src="$store.getters.systemInfo.logo + '?id=' + Date.now()" alt=""></div>
<div v-else><img src="../assets/kefu_logo.png" alt=""></div>
</div>
<el-menu
:default-active="menuActive"
class="el-menu-vertical-demo"
background-color="#3e444a"
text-color="#fff"
:router="true"
active-text-color="#ffd04b"
>
<el-menu-item index="/index">
<i class="el-icon-s-home"></i>
<span slot="title">首页</span>
</el-menu-item>
<el-menu-item index="/workbench">
<el-badge :hidden="$store.getters.readCount == 0" :value="$store.getters.readCount" :max="99" style="width: 100%;">
<div>
<i class="el-icon-s-platform"></i>
<span slot="title">工作台</span>
</div>
</el-badge>
</el-menu-item>
<el-menu-item index="/knowledge">
<i class="el-icon-reading"></i>
<span slot="title">知识库</span>
</el-menu-item>
<el-menu-item index="/robot">
<i class="el-icon-picture-outline-round"></i>
<span slot="title">机器人</span>
</el-menu-item>
<el-menu-item index="/customer">
<i class="el-icon-headset"></i>
<span slot="title">客服管理</span>
</el-menu-item>
<el-menu-item index="/users">
<i class="el-icon-user"></i>
<span slot="title">用户管理</span>
</el-menu-item>
<el-menu-item index="/chat_record">
<i class="el-icon-time"></i>
<span slot="title">服务记录</span>
</el-menu-item>
<el-menu-item index="/system">
<i class="el-icon-setting"></i>
<span slot="title">系统设置</span>
</el-menu-item>
</el-menu>
<div class="fix-bottom">
<a title="去给作者Star" target="_blank" href="https://github.com/chenxianqi/kefu_server.git">
<svg class="github-logo" height="23" viewBox="0 0 16 16" version="1.1" width="23" aria-hidden="true"><path fill="#fff" fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
<span> Github</span>
</a>
</div>
</el-aside>
</template>
<script>
export default {
name: 'mini-im-aside',
data(){
return {
menuActive: "/index"
}
},
mounted(){
this.setHeaderTitle()
},
methods: {
setHeaderTitle(){
this.menuActive = this.$route.path
var title
switch(this.menuActive){
case "/index":
title = "首页"
break
case "/workbench":
title = "工作台"
break
case "/knowledge":
title = "知识库"
break
case "/robot":
title = "机器人"
break
case "/customer":
title = "客服管理"
break
case "/users":
title = "用户管理"
break
case "/system":
title = "系统设置"
break
}
this.$store.commit("onChangeHeaserTitle", title)
}
},
watch: {
"$route"(){
this.setHeaderTitle()
}
}
}
</script>
<style lang="stylus">
.mini-im-aside{
background-color: #3e444a
display flex
flex-direction column
.mini-im-logo{
width 100%;
height: 100px;
display flex
justify-content center
flex-direction column
align-items center
border-bottom 1px solid #ddd
img{
height : 30px
}
}
.el-menu{
border-right 0
}
.el-badge__content{
border 0
top 30px
}
.fix-bottom{
flex-grow 1
display flex
flex-direction column
justify-content flex-end
padding-bottom 30px
a{
cursor pointer
padding 0 30px
text-align center
align-items center
color #fff
display flex
align-content center
.github-log{
width 50px
}
span{
margin-top 3px
margin-left 5px
}
}
}
}
</style>
<template>
<el-dialog width="500px" title="修改密码" :show-close="false" :visible.sync="$store.state.editPasswordDialogFormVisible" :close-on-click-modal="false">
<el-form :model="form">
<el-form-item label="旧密码" :label-width="formLabelWidth">
<el-input v-model="form.old_password" placeholder="请输入旧密码" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="新密码" :label-width="formLabelWidth">
<el-input v-model="form.new_password" placeholder="请输入新密码" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="确认密码" :label-width="formLabelWidth">
<el-input v-model="form.enter_password" placeholder="请再次输入新密码" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from 'axios'
export default {
name: 'mini-im-create-knowledge',
data(){
return {
form: {
old_password: "",
new_password: "",
enter_password: ""
},
robotSwitch: true,
formLabelWidth: "80px"
}
},
props:{
dialogFormVisible: Boolean
},
mounted(){
},
methods: {
// 关闭
closeModal(){
this.resize()
this.$store.commit("onChangeEditPasswordDialogFormVisible", false)
},
// 保存
save(){
// 验证字段 !! 算了前端不验证了
const loading = this.$loading({
lock: true,
text: '保存中...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.5)'
});
axios.put('/admin/password', this.form)
.then(response => {
console.log(response)
loading.close();
this.$message.success("资料修改成功")
this.closeModal()
this.resize()
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message)
});
},
resize(){
this.form = {
old_password: "",
new_password: "",
enter_password: ""
}
}
}
}
</script>
<style scoped lang="stylus">
</style>
<template>
<el-dialog width="500px" title="修改资料" :show-close="false" :visible.sync="$store.state.editDialogFormVisible" :close-on-click-modal="false">
<el-form :model="form">
<el-form-item label="头像" :label-width="formLabelWidth">
<el-row :gutter="10">
<el-col :span="3">
<div class="mini-im-file-button" title="点击上传图片">
<el-avatar :size="50" :src="form.avatar || $store.state.avatar"></el-avatar>
<input onClick="this.value = null" @change="changeFile" type="file" accept="image/*">
<div v-show="isUploading" class="mini-im-file-percent">
<span>{{uploadPercent}}</span>
</div>
</div>
</el-col>
<el-col :span="6">
</el-col>
</el-row>
</el-form-item>
<el-form-item label="账号" :label-width="formLabelWidth">
{{form.username}}
</el-form-item>
<el-form-item label="昵称" :label-width="formLabelWidth">
<el-input v-model="form.nickname" placeholder="请输入昵称" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="联系方式" :label-width="formLabelWidth">
<el-input v-model="form.phone" placeholder="请输入联系方式" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="自动回复语" :label-width="formLabelWidth">
<el-input v-model="form.auto_reply" type="textarea" placeholder="请输入自动回复语,不支持emoji,请使用简单语句描述" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from 'axios'
import upload from '../common/upload'
export default {
name: 'mini-im-edit-profile',
data(){
return {
form: {
id: "",
avatar: "",
username: "",
nickname: '',
phone: '',
auto_reply: ''
},
formLabelWidth: "90px",
isUploading: false,
uploadPercent: ""
}
},
computed: {
adminInfo(){
return this.$store.state.adminInfo
}
},
methods: {
// 关闭窗口
closeModal(){
this.$store.commit("onChangeEditDialogFormVisible", false)
},
// 保存
save(){
const loading = this.$loading({
lock: true,
text: '保存中...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.5)'
});
axios.put('/admin', this.form)
.then(response => {
console.log(response)
loading.close();
this.$message.success("资料修改成功")
this.closeModal()
this.$store.dispatch('ON_GET_ME')
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message)
});
},
// 上传头像
changeFile(file){
upload({
file: file.target.files[0],
progress: (percent) => {
this.isUploading = true
this.uploadPercent = percent + "%"
},
success: (url) => {
this.isUploading = false
this.uploadPercent = ""
this.$message.success("上传成功")
var imgUrl = this.$store.getters.uploadToken.host +"/"+ url
this.form.avatar = imgUrl
},
error: (err)=>{
this.isUploading = false
this.uploadPercent = ""
this.$message.error(err.message)
}
});
}
},
watch: {
adminInfo(){
if(!this.adminInfo) return
const {avatar,username, nickname, phone, id, auto_reply } = this.$store.state.adminInfo
this.form = {avatar,username, nickname, phone, id, auto_reply }
}
}
}
</script>
<style scoped lang="stylus">
.mini-im-file-button{
width 50px
height 50px
border-radius 50%
position relative
overflow hidden
input{
font-size 100px
position absolute
top 0px
left 0px
cursor pointer
opacity 0
}
cursor pointer
.mini-im-file-percent{
position absolute
top 0px
left 0px
width 100%
height 100%
display flex
align-items center
justify-content center
border-radius 50%
background-color rgba(0,0,0, .5)
color #fff
font-size 12px
}
}
</style>
<template>
<el-row type="flex" justify="end" :gutter="20">
<el-col :span="5">
<el-button
@click="$store.commit('onChangeToggleAside', !$store.state.isShowAside)"
class="mini-im-button"
type="info"
:icon="$store.state.isShowAside ? 'el-icon-s-fold' : 'el-icon-s-unfold'"
>
</el-button>
</el-col>
<el-col :span="16" >
<div class="mini-im-title">{{$store.state.heaserTitle}}</div>
</el-col>
<el-col :span="5">
<el-row type="flex" justify="end" class="mini-im-dropdown">
<el-dropdown @command="handleCommand" trigger="click">
<div class="el-dropdown-link">
<el-avatar :size="25" class="mini-im-avatar">
<img :src="$store.getters.avatar"/>
</el-avatar>
<span style="padding:0 5px;"> {{$store.getters.nickname}} </span>
<i class="el-icon-arrow-down el-icon--right"></i>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="a">
<i class="el-icon-user icon"></i>
修改资料
</el-dropdown-item>
<el-dropdown-item command="b">
<i class="el-icon-unlock icon"></i>
修改密码
</el-dropdown-item>
<el-dropdown-item command="c" divided>
<i class="el-icon-caret-right icon"></i>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-row>
</el-col>
</el-row>
</template>
<script>
import axios from 'axios'
export default {
name: "mini-im-aside",
data(){
return {
bgColor: "#ffffff"
}
},
props: {
title: String
},
methods: {
// 退出登录
logout(){
axios.put('/admin/online/0')
axios.get('/auth/logout')
.then(() => {
this.$store.commit("onReset")
this.$store.commit("onChangeAdminInfo", null)
this.$router.push({ path: '/login'})
this.$mimcInstance.logout()
this.$store.commit("onIsLogin", false)
localStorage.clear()
})
.catch(error => {
this.$message.error('退出失败')
console.log(error)
})
},
handleCommand(command){
switch(command){
case 'a':
this.$store.commit("onChangeEditDialogFormVisible", true)
break
case 'b':
this.$store.commit("onChangeEditPasswordDialogFormVisible", true)
break
case 'c':
this.$confirm('您确定要退出登录吗? ', '温馨提示!', {
confirmButtonText: '确定',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() => this.logout())
break
}
}
}
}
</script>
<style scoped lang="stylus">
.mini-im-header {
background-color: #545c64;
border-bottom: 1px solid #545c64;
.mini-im-dropdown{
height 100%
}
.mini-im-button{
border 0
font-size 35px
display block
background 0
padding-left 0
}
.mini-im-title{
color #fff
font-size 16px
text-align center
line-height 60px
}
.icon {
color: #fff;
}
.el-dropdown-link {
cursor: pointer;
display flex
height 100%
line-height 60px
align-items center
color: #fff;
}
.el-icon-arrow-down {
font-size: 12px;
}
}
</style>
import Vue from 'vue'
import ElementUI from 'element-ui'
import App from './App.vue'
import router from './router'
import store from './store'
import 'element-ui/lib/theme-chalk/index.css'
import preview from 'vue-photo-preview'
import 'vue-photo-preview/dist/skin.css'
import Helps from "./plugins/help"
import MimcPlugin from "./plugins/mimc"
import momentLocal from './resource/moment_locale'
var moment = require('moment');
moment.locale("zh-cn", momentLocal)
import axios from 'axios'
axios.defaults.baseURL = '/v1'
// 添加请求拦截器
axios.interceptors.request.use((config) => {
var token = localStorage.getItem("Authorization")
config.headers['Authorization'] = token || ""
return config;
}, (error) => {
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
// 登录失效了
if(error.response.status == 401) {
localStorage.clear()
store.commit("onChangeAdminInfo", null)
if(store.state.mimcUser) store.state.mimcUser.logout()
router.push("/login")
}
return Promise.reject(error);
});
var options={
fullscreenEl:false, //关闭全屏按钮
}
Vue.use(preview, options)
Vue.use(ElementUI)
Vue.use(Helps)
Vue.use(MimcPlugin)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
var moment = require('moment');
// eslint-disable-next-line no-undef
var Helps = {};
Helps.install = function (Vue, options) {
Vue.prototype.$myMethod = function(){
console.log(options)
}
// 获取单个平台数据
Vue.prototype.$getPlatformItem = function(index){
var platformConfigItem
var platformConfig = this.$store.getters.platformConfig
for(let i = 0; i< platformConfig.length; i++){
if(platformConfig[i].id == index){
platformConfigItem = platformConfig[i]
}
}
return platformConfigItem || {title: "未知"}
}
// 格式化日期
Vue.prototype.$formatUnixDate = function(unix, format = "YYYY-MM-DD HH:mm:ss"){
return moment(parseInt(unix + '000')).format(format)
}
// 格式化日期(相对日期)
Vue.prototype.$formatFromNowDate = function(unix){
if(moment().format("YYYYMMDD") == moment(parseInt(unix + '000')).format("YYYYMMDD")){
return moment(parseInt(unix + '000')).format("HH:mm")
}
return moment(parseInt(unix + '000')).format("YYYY-MM-DD HH:mm")
}
Vue.prototype.$robotNickname = function(id){
var nickname
var robots = this.$store.getters.robots
for(let i = 0; i< robots.length; i++){
if(robots[i].id == id){
nickname = robots[i].nickname
}
}
return nickname
}
}
export default Helps;
\ No newline at end of file
import axios from "axios";
import { Message } from 'element-ui';
var MimcPlugin = {};
MimcPlugin.install = function (Vue, options) {
console.log(options)
// 获取单个平台数据
Vue.MimcInstance = Vue.prototype.$mimcInstance = {
user: null,
robot: null,
fetchMIMCTokenResult: null,
// 初始化
init(request, callback){
this.getRobot()
this.fetchMIMCToken(request, callback)
},
_receiveP2PMsgCallback: null,
_statusChangeCallback: null,
_serverAckCallback: null,
_disconnectCallback: null,
// 获取token
// request 登录参数
// 登录回调 callback bool 是否成功
fetchMIMCToken(request, callback){
axios.post('/public/register', request)
.then(response => {
this.fetchMIMCTokenResult = response.data.data.token
if(callback) callback(true)
console.log("MIMC初始化成功")
})
.catch((error)=>{
if(callback) callback(false)
Message.error("mimc初始化失败,请刷新重试," + error.response.data.message)
})
},
// 获取机器人
getRobot(){
axios.get('/public/robot/1')
.then(response => {
this.robot = response.data.data
})
.catch((error)=>{
Message.error("mimc初始化失败,请刷新重试" + error.response.data.message)
})
},
// pushMessage
pushMessage(payload){
axios.post('/public/message/push', {
"msgType": "NORMAL_MSG",
"payload": payload
})
.then(response => {
console.log(response.data)
if(response.data['code'] != 200){
setTimeout(()=> this.pushMessage(payload), 300)
}
})
.catch(()=>{
setTimeout(()=> this.pushMessage(payload), 300)
})
},
// 登录
login(callback){
try{
var fetchMIMCTokenResult = this.fetchMIMCTokenResult
if(!fetchMIMCTokenResult) return
// eslint-disable-next-line no-undef
this.user = new MIMCUser(fetchMIMCTokenResult.data.appId, fetchMIMCTokenResult.data.appAccount, "666");
this.user.registerP2PMsgHandler((message)=>{
var msg = JSON.parse(window.Base64.decode(message.getPayload()));
if(this._receiveP2PMsgCallback) this._receiveP2PMsgCallback(msg)
});
this.user.registerFetchToken(() => {
return fetchMIMCTokenResult;
});
this.user.registerStatusChange((bindResult, errType, errReason, errDesc)=>{
if(this._statusChangeCallback) this._statusChangeCallback(bindResult, errType, errReason, errDesc)
});
this.user.registerServerAckHandler((packetId, sequence, timeStamp, errMsg)=>{
if(this._serverAckCallback) this._serverAckCallback(packetId, sequence, timeStamp, errMsg)
});
this.user.registerDisconnHandler(() => {
if(this._disconnectCallback) this._disconnectCallback()
});
this.user.login();
window.mimcInstance = this
if(callback) callback()
console.log("MIMC登录成功")
}catch(e){
console.log(e)
console.log("MIMC登录失败")
// 重新尝试
setTimeout(()=>{
this.login()
}, 1000)
}
},
// 退出
logout(){
if(this.user){
this.user.logout()
this.user = null
}
},
// 注册监听器
addEventListener(type, callback){
switch(type){
case "receiveP2PMsg":
this._receiveP2PMsgCallback = callback
break
case "statusChange":
this._statusChangeCallback = callback
break
case "serverAck":
this._serverAckCallback = callback
break
case "disconnect":
this._disconnectCallback = callback
break
}
},
// 发送消息
sendMessage(type, toAccount, payload = "", transferAccount = 0){
if(!this.user){
Message.error("服务异常,请刷新重试!")
return
}
const messageJson = {
"from_account": parseInt(this.fetchMIMCTokenResult.data.appAccount),
"to_account": parseInt(toAccount),
"biz_type": type,
"version": "0",
"timestamp": parseInt((new Date().getTime() + " ").substr(0, 10)),
"key": new Date().getTime(),
"read": 0,
"transfer_account": parseInt(transferAccount),
"payload": payload + ''
}
// console.log("发送消息")
// console.log(messageJson)
const jsonBase64Msg = window.Base64.encode(JSON.stringify(messageJson))
try {
// 过滤不入库
if(!(type == "contacts" || type == "pong" || type == "welcome" || type == "handshake")){
// 发送给机器人入库
// const intoMessageJson = {
// "biz_type": "into",
// "payload": jsonBase64Msg
// }
// const intoJsonBase64Msg = window.Base64.encode(JSON.stringify(intoMessageJson))
// this.user.sendMessage(this.robot.id.toString(), intoJsonBase64Msg);
// 消息入库
this.pushMessage(window.Base64.encode(jsonBase64Msg))
}
setTimeout(()=>{
// 发送给对方
this.user.sendMessage(toAccount.toString(), jsonBase64Msg);
},200)
} catch (err) {
console.log("sendMessage fail, err=" + err);
}
return messageJson
},
// 创建本地消息
createLocalMessage(type, toAccount, payload = "", transferAccount = 0){
const messageJson = {
"from_account": parseInt(this.fetchMIMCTokenResult.data.appAccount),
"to_account": parseInt(toAccount),
"biz_type": type,
"version": "0",
"timestamp": parseInt((new Date().getTime() + " ").substr(0, 10)),
"key": new Date().getTime(),
"read": 0,
"transfer_account": parseInt(transferAccount),
"payload": payload + ''
}
return messageJson
}
}
}
export default MimcPlugin;
\ No newline at end of file
const emojiData = ["😀","😁","😂","🤣","😃","😄","😅","😆","😉","😊","😋","😎","😍","😘","😗","😙","😚","🙂","🤗","🤩","🤔","🤨","😐","😑","😶","🙄","😏","😣","😥","😮","🤐","😯","😪","😫","😴","😌","😛","😜","😝","🤤","😒","😓","😔","😕","🙃","🤑","😲","🙁","😖","😞","😟","😤","😢","😭","😦","😧","😨","😩","🤯","😬","😰","😱","😳","🤪","😵","😡","😠","🤬","😷","🤒","🤕","🤢","🤮","🤧","😇","🤠","🤡","🤥","🤫","🤭","🧐","🤓","😈","👿","👹","👺","💀","👻","👽","🤖","💩","😺","😸","😹","😻","😼","😽","🙀","😿","😾","🤲","👐","🙌","👏","🤝","👍","👎","👊","✊","🤛","🤜","🤞","✌️","🤟","🤘","👌","👈","👉","👆","👇","☝️","✋","🤚","🖐","🖖","👋","🤙","💪","🖕","✍️","🙏"]
exports.emojiData = emojiData
\ No newline at end of file
export default {
months: '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split('_'),
monthsShort: '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'),
weekdays: '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'),
weekdaysShort: '周日_周一_周二_周三_周四_周五_周六'.split('_'),
weekdaysMin: '日_一_二_三_四_五_六'.split('_'),
longDateFormat: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'YYYY-MM-DD',
LL: 'YYYY年MM月DD日',
LLL: 'YYYY年MM月DD日Ah点mm分',
LLLL: 'YYYY年MM月DD日ddddAh点mm分',
l: 'YYYY-M-D',
ll: 'YYYY年M月D日',
lll: 'YYYY年M月D日 HH:mm',
llll: 'YYYY年M月D日dddd HH:mm'
},
meridiemParse: /凌晨|早上|上午|中午|下午|晚上/,
meridiemHour: function (hour, meridiem) {
if (hour === 12) {
hour = 0;
}
if (meridiem === '凌晨' || meridiem === '早上' ||
meridiem === '上午') {
return hour;
} else if (meridiem === '下午' || meridiem === '晚上') {
return hour + 12;
} else {
// '中午'
return hour >= 11 ? hour : hour + 12;
}
},
meridiem: function (hour, minute) {
const hm = hour * 100 + minute;
if (hm < 600) {
return '凌晨';
} else if (hm < 900) {
return '早上';
} else if (hm < 1130) {
return '上午';
} else if (hm < 1230) {
return '中午';
} else if (hm < 1800) {
return '下午';
} else {
return '晚上';
}
},
calendar: {
sameDay: '[今天]LT',
nextDay: '[明天]LT',
nextWeek: '[下]ddddLT',
lastDay: '[昨天]LT',
lastWeek: '[上]ddddLT',
sameElse: 'L'
},
dayOfMonthOrdinalParse: /\d{1,2}(日|月|周)/,
ordinal: function (number, period) {
switch (period) {
case 'd':
case 'D':
case 'DDD':
return number + '日';
case 'M':
return number + '月';
case 'w':
case 'W':
return number + '周';
default:
return number;
}
},
relativeTime: {
future: '%s内',
past: '%s前',
s: '几秒',
ss: '%d秒',
m: '1分钟',
mm: '%d分钟',
h: '1小时',
hh: '%d小时',
d: '1天',
dd: '%d天',
M: '1个月',
MM: '%d个月',
y: '1年',
yy: '%d年'
},
week: {
// GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效
dow: 1, // Monday is the first day of the week.
doy: 4 // The week that contains Jan 4th is the first week of the year.
}
}
\ No newline at end of file
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
// mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: () => import('./views/index.vue'),
redirect: "/index",
children: [
{
path: 'index',
component: () => import('./views/home/index.vue'),
},
{
path: 'workbench',
component: () => import('./views/workbench/index.vue'),
},
{
path: 'knowledge',
component: () => import('./views/knowledge/index.vue'),
},
{
path: 'robot',
component: () => import('./views/robot/index.vue'),
},
{
path: 'customer',
component: () => import('./views/customer/index.vue'),
},
{
path: 'users',
component: () => import('./views/users/index.vue'),
},
{
path: 'system',
component: () => import('./views/system/index.vue'),
},
{
path: 'chat_record',
component: () => import('./views/record/index.vue')
},
]
},
{
path: '/login',
name: 'login',
component: () => import('./views/auth/login.vue')
},
{path:'*',redirect: "/index"},
]
})
import axios from 'axios'
import router from '../router'
export default {
// 获取用户信息
ON_GET_ME(context){
var pathname = location.pathname
axios.get('/admin/me')
.then(response => {
context.commit("onIsLogin", true)
context.commit("onChangeAdminInfo", response.data.data)
if(location.pathname == '/login' || location.hash.indexOf("#/login") != -1){
router.push({ path: '/index'})
}
})
.catch(error => {
console.log(error.response)
context.commit("onIsLogin", false)
if(pathname != '/login'){
router.push({ path: '/login'})
}
});
},
// 获取上传配置
ON_GET_UPLOAD_TOKEN(context){
axios.get('/public/secret')
.then(response => {
context.commit('onChangeUploadToken', response.data.data)
})
},
// 获取平台配置数据
ON_GET_PLATFORM_CONFIG(context){
axios.get('/platform/list')
.then(response => {
context.commit('onChangePlatformConfig', response.data.data)
})
},
// 获取systemInfo
ON_GET_SYSTEM(context){
axios.get('/system')
.then(response => {
context.commit('onChangeSystemInfo', response.data.data)
document.title = response.data.data.title
})
},
// 获取companyInfo
ON_GET_COMPANY(context){
axios.get('/public/company')
.then(response => {
context.commit('onChangeCompanyInfo', response.data.data)
})
},
// 获取uploads/config
ON_GET_UPLOADS_CONFIG(context){
axios.get('/uploads/config')
.then(response => {
context.commit('onChangeUploadsConfigs', response.data.data)
})
},
// 获取会话列表
ON_GET_CONTACTS(context){
axios.get('/contact/list')
.then(response => {
context.commit('onChangeContacts', response.data.data)
})
},
// 一分钟上报一次我的活动
ON_RUN_LAST_ACTiIVITY(){
axios.get('/public/activity/')
},
// 获取机器人列表
ON_GET_ROBOTS(context){
axios.get('/robot/list')
.then(response => {
context.commit('onChangeRobos', response.data.data)
})
.catch(() => {
this.loading = false
});
}
}
\ No newline at end of file
export default {
// 获取个人信息
adminInfo(state){
return state.adminInfo || {}
},
// 是否是登录状态
isLogin(state){
return state.isLogin
},
// 获取头像
avatar(state){
if(state.adminInfo && state.adminInfo.avatar != ""){
return state.adminInfo.avatar
}else{
return ''
}
},
// 获取上传mode
uploadMod(state){
return state.uploadToken.mode || -1
},
// 获取昵称
nickname(state){
if(state.adminInfo && state.adminInfo.nickname != ""){
return state.adminInfo.nickname
}else{
return '未设置昵称'
}
},
// 获取上传配置文件
uploadToken(state){
return state.uploadToken
},
// 获取平台配置数据
platformConfig(state){
return state.platformConfig
},
// 获取systemInfo
systemInfo(state){
return state.systemInfo
},
// 获取companyInfo
companyInfo(state){
return state.companyInfo
},
// 获取uploadsConfigs
uploadsConfigs(state){
return state.uploadsConfigs
},
// 获取会话列表
contacts(state){
return state.contacts|| []
},
// 获取当前窗口服务谁
seviceCurrentUser(state){
return state.seviceCurrentUser || {}
},
// 获取机器人
robots(state){
return state.robots || []
},
// 聊天信息
messageRecord(state){
return state.messageRecord || {list:[]}
},
// 新消息总数
readCount(state){
var count = 0
for(let i =0; i<state.contacts.length; i++) {
count = count + state.contacts[i].read
}
return count
},
// 工作台背景颜色
workbenchBgColor(state){
return state.workbenchBgColor
},
}
\ No newline at end of file
import Vue from 'vue'
import Vuex from 'vuex'
import actions from './actions'
import mutations from './mutations'
import getters from './getters'
import state from './state'
Vue.use(Vuex)
export default new Vuex.Store({
state: state,
getters: getters,
mutations: mutations,
actions: actions
})
\ No newline at end of file
export default {
// 更新标题
onChangeHeaserTitle(state, title){
state.heaserTitle = title
},
// 更新平台配置数据
onChangePlatformConfig(state, platformConfig){
state.platformConfig = platformConfig
},
// 展开隐藏
onChangeToggleAside(state, isShow){
state.isShowAside = isShow
},
// 更新个人资料modal状态
onChangeEditDialogFormVisible(state, isShow){
state.editDialogFormVisible = isShow
},
// 更新个人密码modal状态
onChangeEditPasswordDialogFormVisible(state, isShow){
state.editPasswordDialogFormVisible = isShow
},
// 更新个人资料
onChangeAdminInfo(state, adminInfo){
state.adminInfo = adminInfo
},
// 更新上传token
onChangeUploadToken(state, uploadToken){
state.uploadToken = uploadToken
},
// 更新systemInfo
onChangeSystemInfo(state, systemInfo){
state.systemInfo = systemInfo
},
// 更新companyInfo
onChangeCompanyInfo(state, companyInfo){
state.companyInfo = companyInfo
},
// 更新uploadsConfigs
onChangeUploadsConfigs(state, uploadsConfigs){
state.uploadsConfigs = uploadsConfigs
},
// 更新mimcUser
onChangeMimcUser(state, mimcUser){
state.mimcUser = mimcUser
},
// 更新contacts
onChangeContacts(state, contacts){
state.contacts = contacts
for(let index in contacts){
var contact = contacts[index]
if(state.seviceCurrentUser && contact.from_account == state.seviceCurrentUser.from_account){
state.seviceCurrentUser = contact
break
}
}
},
// 更新当前窗口服务谁
onChangeSeviceCurrentUser(state, seviceCurrentUser){
state.seviceCurrentUser = seviceCurrentUser
},
// 更新机器人列表
onChangeRobos(state, robots){
state.robots = robots
},
// 重置某些值
onReset(state){
state.seviceCurrentUser = null
state.contacts = []
state.mimcUser = null
},
// 更新聊天记录
onChangeMessageRecord(state, messageRecord){
state.messageRecord = messageRecord
},
// 是否是登陆状态
onIsLogin(state, isLogin){
state.isLogin = isLogin
}
}
\ No newline at end of file
export default {
adminInfo: null, // 个人信息
heaserTitle: "首页", // header标题
isShowAside: true, // 控制左侧栏显示隐藏
editDialogFormVisible: false, // 控制修改个人资料modal
isLogin: false, // 是否已登录状态
editPasswordDialogFormVisible: false, // 控制修改密码modal
uploadToken: null, // 上传签名数据
platformConfig: [], // 平台数据
systemInfo: {}, // 系统信息
companyInfo: {}, // 公司信息
uploadsConfigs: [], // 可配置上传参数
mimcUser: null, // mimc用户对象
contacts: [], // 会话列表
robots: [], // 机器人列表
seviceCurrentUser: null,// 当前窗口服务谁
messageRecord: { // 当前聊天面板聊天消息记录
list: []
},
avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAAAXNSR0IArs4c6QAAD7tJREFUeAHtXV1sHMUdn9m7s50P59NxbBI7McH5IHz5A8g5OHFTJECUIlWFVkIFCdI+8dYHqOARBKjqG08tIIHEA7QvlCJAShPHxnYAO6aiKE0s5DZpcBJiFOqEOLZvp//f3q1zd7673b2dnZ21b6TT7u3O/L9+O/Of7+FsEYTx8fGa0xOTrVwYOwQT9GM7OGP1pFotY6JWMG5duXWlJ0xMMcaneOZK8aYozQVKc5LinBTcPNncuH6spaVlOurmIZ2iF44OjewilA7Q70cETAcp0SyEMGRqwjk3ifZpoj3CODtCv8P7kx0nZPJQQSsSAA8PD9ddmTEeZqZ5gIx+gAzToMI4BXicI4MdZoZxeEWV+V5nZ+fFAnG0eqQtwGNjY9UTF6Yeopz5OOPifiFYQifLcc5mmeAfUU5/q7G+9v3W1tZrOslny6IdwL0Do3dyYT5FOfUX5C3X2ILqfeWXyJDvCG683rO37XOdZNUG4P6h0Z5UynyOQL1XJwN5l4UfisWMF7uTbb3e08pPETrAvUPHH6DqzPNUFHfJVy88ilR0DwqDv9CTbP8wPCmorRAW8/6h4btTJn+VCdEZlgxK+HI+HDPE093Jzk+V8MtjohzgwcGv1s2I6ZeJ8UHKtcr55+mv5C/lZqojsteqeM2zXV27v1PCNMNEmYEBZt/Q6JNCmK9Q+3W9SiW14cXZJOfGM/uSbW8AdBVyKQH46GdfNrHZmbcJ5G4VSunOg8DtZ4mqx/bfdeuZoGUNHOC+geEHTcbeXLK5thiClJup6+2JfXs7PygWRcZzqd172QJRbo0f/WTk99QP/H4F3GzLZO7JTcE2lo3IVgViSHkUSA4eHPxi06xI/ZlATkqRcpEToSJ7KMFjj3R13XFWtqrSAbYGAlLiY6pBNMkWdjHTIyDOsBi/T/aAhtQi+pNjI3uEKfor4Hr/FGEz2A429J66eAppAKNHKpUSf6/42+LGdnxDfhk2hC0d47qMIAXgvoHjv2Qp86/Uml/ukm8lWhELWDYkW1o2LRLHy2PfPtj62kggYhpYTdCLQoso7hyLGT/125ftC2D4CxQplZwbzGdFY84/xGL8x/fs6ThWLoeyAUZtGZWCis8t1/Qu06F70+Dd5dauy/LBaOcyagpVwHUJkp9o6LcnW1s2L4OOZ4DRQ2V1YlTauWWYu7wkaEJlOo4813M8A0y1u5cqPVTlAeUnFWwO23ul4ckHY+AA/afEzFM6r0LJjL+qdiWrW7+G1a5czqqqEqyafgjXZmbZDP2mLv/ALk5eYv+buiyTbSC0MMRIc7kf8jJA4RooDPmJ2WujUfC7ZAi2+YZ6+m20QHVjbYD932/O0+8CTTKhQlHXgEpXorrN7VCjqyLayrE0nhsFcJFb7+q4hd24dbNrcIElcjfSIC1oaBtQ6UqPrbvKnK4iHR08/pQwzde0VToj2NbmG9iWpkYpYv7nzAT79+lvpNAKggg3jIP7u9pfd6LtCHB6DtXVU7rn3l3bW1j9hnVO+np6f+Hb79iJU+Oe0iiLTEV1FV+23WmOl2MRjQlyuoOLnCsbXAAFmqCtZaCi2sLGQbiSORhTW02TD+lca4a/3L1zm4Oa/l5/9a+vrZq2PyryU6NWbRgiWWpKbskcjHnLOoOL2vK2luDnFYAHeOkWgI01t7yEYEUBtkaJNJ+UjqZQTXVVCfXkvAIP8NIyEEalxo+LAozlJFoqlCUU2rmqgkpeXnWibExrugqHggBjIRhlf63XCqGHCm1XVQG8wFPHQN0ye4FZIdkKApxe5Vcouj7PwuiMCIOnW4sXw2wBwFifG4UlnOhbVh3C4OleR3FvGrvcFAsA5sw8mBtFz38qi2fbAmHwtHm7uRbCLgdgbJtA/eyPuiEWdhx7VEilHGHw9KIfsAOG2WlyAMaeGNHZNiFbDTX3Og8ypS0g1qQxvG6PHICp5vz49Vd632E8V3WYmVXP06uO+RjOA4ytirCbjVeCYcXH+K3qEAZPzzoShhaWmYTzAGMfKiqC1DUsPUuemwAzMVSHqctXVLP0zA8YWnuKZVLOA4xNxjxTCzEBptmoDhcnv1fNsjx+WVjOA0y9IZECGHOoVBaZ4BWFeVv4IrKxtAC2lnyGtz1geV8ppcIcKlVBJS8JOjVkMGXpHIyNPSMYMEFu+tpM4JKDB3hFKmQwTQNsRhNgahKwr8cD38fE4gFekQq0Ey/ktQCmybbtkRI+S1hUtjBBLqgA2mFU6PzqQ59jB2gY2Eyb5io0+yUYZnrMfsQEOdkBNHWeWVlKX2AKbOPYKZ1Kn3RRXSqF5u8w+/Hq9LUlM23WCQ5yKQawjae3waedrBZBQG67fOUHa55WuVN5UKGCX49isZwPIbCN44yD/BdR/g9gJr/7fvEuXfEADrAlgNmiAhj6o8Z75ux567eYFp95wNaKCmzjtFptY8QaAJ70RO9TVHqgPCnmIjKwpcoVjpyphMVpAV5LAAs9pwouTosr1kqsNGhBdyUHKza7KnbAFjk4kgAbBmcNG+tYxx272No1qwKz2fLlNSwWiwVGP1jCopYqWZyOfotONQszGzc1bmCNGzewRCK9J8nO7VvZyBcnpA8fgtftt2xnBjfY2YkL1ujV3FwqWEwkUge2nndtkcjfEynDMKxeqqZNGxcsBKtKJBjWB//jn6c80XSKDJqgjYCF5VifdHbiW2p+nWNRAZp8MA5q1DugCO5su5k1b25YAK4t+ZrVtVLX8mJdMGhmBxTVkOHOtt1s3drV2a+0vAe2aCZpC3A8Hmc7W7ey23a3smU11Y5GhPFl+GPQAK1iAUX3rTffxLZva6ZjDMmE2gY+RVUVPXPwsmU1rP32nWxjvfsDWrCGF/7YzwoEpAUNN+uBGxs2WCWLrktagK2WOXj1qpWs7bYdrnJtfuax/XH+c7f/s/2umzQoWVARyy/O3aQNPg7lYGKiVRG9oW6tVSQnqHguN5Trjwv5XTcywDejyK5bp932S1NUyWLaTDZCuxY5SIZf8+qPnfyuE9CQ+eadN3pyKU40/b4HtuSD2Um/hGSkR65DpcWN73PDz4s/9uJ3S/EGzx03bWHrNcnJwJYA5qEDjArVbvr6ZYFrg+DWH3v1uzb9Qlfrw6Ka/3LSKewAbA3BzVABRlMI/gvXIIKTPy7X75aSNR6Psd27toXexQlsjebG9WP01YU2Z2cXNUnctHFLGdTpXTF/7NfvluKLHIySIawATIGt0dLSMk3O+HQYgqDGrKJHyCo289rHsvxuKbutX7eaNXhox5ei5fUdMAW2VjcMOeMRrwT8xqdj0Nk22t1VVcj3xzL9bikdWrZsoqLaMnOpaNLf2ZimOXN2RDoHB4JbmzexagWbmGWLYfvjIPxuNp/se5QUW5pC2O8yg2m6ZsPZ4Wyhgr5fsXyZNeQXNJ9C9Ev1MReKL+PZpsZ6NnH+Irt6dVoGOXc0MphaOThzZMs5dyn9x9pcYMjPP1V3FOCP8VMZMDmhhUanFIZz9jE8886BVFaSi9GEqKfK1VIL6PywJygErXs2lvMAU/+gEoAbqTtSRldk0EaSTd+aYlRfJ5tsYXpZWM4DvKLKfI9KrsB3NrmBhtiWamhsCB5gYAgsbRvPA9zZ2XmRCf6R/SKIKzoWalwM3AfBWwea6NBBTT7QQBhaWGaYzAOM/1T5eCtI5mvXBKxckMJLoh30QEQ+hjkAN9bXvk8wB7Z9zepVFYAxmSG4wC+lMbzOIQfg1tbWa1SGv3v9tbw7VKx0ndoiT0tnSitXLAuskkm153eAYbYUOQDjBa0FD+R8JHy5qtuf2Yrqcg8brK5dEYg4ghsLzlFaAHDP3rbPqZg+JFuCYIsm2dIGS29VIMU0P5TGLlf2BQDjNXWOv5gbzf8/1f3O/iUOjkIQw6PFMCsIcHeyrZeKkkGZKvqZRCdTDh1oye7RAlbArJBuBQFGRGHwFwolKPeZbKXKlUOHdLJtUQqrogD3JNs/pFrRsCyDyFZKllxh0JFamhFGFlZFFCkKMOLHDPE0ZX+aHOA/SFXKvzihUpD1sQMbYFRKGcdxs96BkT/Sria/LkXEzbul3EVZyD7TtKeX78D5n3r2dvymFB3HqYxVvObZGXb1Z35PIJWiUClNlto763jZmmed1C5ZRCMxzqfl3HjGiVDlvVoLABOns4MhkWMRjUi07xTvGzx+lK7d+F8J4VqAfG//vq72/W7qR445GKpYhBJVj9HnMBmuahXuFgaEhRtwYS1XACPi/rtuPUORn3BLGGkqQa4FYHtgACzcUnYNMAju29v5AVW2/uCWeCWeZAuQ7S0MPJB15YOz6ZEfjpM/7qNrMvt55T5YC1DuHSK/u4+uc144ecrBIAwGCR57hL4M18WEF4EqcRdaALa2bO4RXFDyDDASdXXdcZbF+H2VShesEXBAxZZsbdm8DFZlAQw+mFgdj/Gf0Pi1+iPIylA0iklgW9jYnsRejg5lAwxm9+zpOCYM4+d068kvlCPoEkwzB9vCxn509wUwGGMkg7b6+xXdVkD2g0Ru2jnYtNQoUW704v8816KLkeodOv4AN82/0Gbr6s9eLyZUBJ+jWEbOlQEu1JcGMIh9cmxkz1xK/M3vwARoLclAFSr4XL/FcrbtpAIMwtaZeSnxMQ0iN2UzqtyXtgCaQqgt+6lQFeLg2wfnE4WACSOeRMM8/13lf2ELwFawmWxwwU16DrZVsHq8Bo6/RBx+S/eB8bH5RfFKwGKrbup+bP8dOpCC0CFww/cNDD9IW/i8WfHLefCRv6Xi8wmvfct5VBz/Bg4wJDj62ZdNbHbmbcrJlfFksgfl1n5GQ35eRoUckSwSQQnA4I1ium9o9EkhzFeWbG6mXIuZGPuSbW9YxXMRUGQ+VgawLfTg4FfrZsT0y8T44FLxzQCTWhWvYX6bm2k2tq1kXJUDbAvdPzR8d8rkr1LW7rSfLcorzVvG1NbuZOenYegXGsC2sukeMPE85eYu+9liuFKuHcSKA1k9UuXaJHSAbcH7h0Z7UinzOfLW99rPonnlh7AQrNhaIdU6aQOwrXjvwOidnJnkn9mjBLZ2W6jbcuZe+SXqQ34Xa6sLLeHMjav2n3YA2+qPjY1VT1yYeoiK7scZF/cT4OkDjOwIIV8J0FlsWkNF8VvYNiF/ZX3I4s2z1xbgeQnpZnh4uO7KjPEwM80DVBs9QI+Kn3mTnVD+/Tky2GHsKYatirJ3s5HPSg7FSACcr6o1oCEIaJMdoE7QdlKimXK61H51ypkmfUynifYIdbceod/hIPqK83WT/T+SAOcbYXx8vOb0xGQrzqynzl36sR04HJn6jGhbH7EyfcKqqMVZfkibPu2NT6XPjOKX6ckUxTlPxjiJbfCxUzo2026h/ZbzeUXt//8ByJ7Ze9EGCQYAAAAASUVORK5CYII=",
pushIcon: "data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMfaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0MCA3OS4xNjA0NTEsIDIwMTcvMDUvMDYtMDE6MDg6MjEgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkJEMURFODg4MTlDRjExRUFBQjY5RTZDMUM4OUFGOUNDIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkJEMURFODg3MTlDRjExRUFBQjY5RTZDMUM4OUFGOUNDIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE4IE1hY2ludG9zaCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJFRUE0ODZCQ0Q5QzUzN0E5MkNBQzNDRDdGODRCMUE4NSIgc3RSZWY6ZG9jdW1lbnRJRD0iRUVBNDg2QkNEOUM1MzdBOTJDQUMzQ0Q3Rjg0QjFBODUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwM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",
workbenchBgColor: "#646b6f" // 工作台背景颜色
}
\ No newline at end of file
<template>
<div class="login">
<div class="form">
<div class="left">
<div class="tit">欢迎回来, 请登录!</div>
<span>客服系统-工作台</span>
<div class="client">
<p>客户端下载</p>
<el-button @click="dmac" type="primary" size="mini" icon="el-icon-download">Mac 版下载</el-button>
<el-button @click="dwin" type="primary" size="mini" icon="el-icon-download">Windows 版下载</el-button>
</div>
</div>
<div class="right">
<el-form ref="form" :model="form" onsubmit="return false" label-width="80px">
<span class="lable">用户登录</span>
<el-input
class="input"
placeholder="请输入用户名"
prefix-icon="el-icon-user"
v-model="form.username">
</el-input>
<el-input
class="input"
type="password"
placeholder="请输入密码"
prefix-icon="el-icon-unlock"
show-password
v-model="form.password">
</el-input>
<el-row type="flex" class="btn-group">
<el-button native-type="submit" @click="login" size="small" type="primary">登录</el-button>
<el-button size="small" type="info">重置</el-button>
</el-row>
</el-form>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'login',
data(){
return{
form: {
auth_type: 1,
username: "",
password: ""
}
}
},
mounted(){
document.title = "用户登录"
},
methods: {
// login
login(){
// valid
if(this.form.username.trim() == ""){
this.$message.error('用户名不能为空!')
return
}
if(this.form.password.trim() == ""){
this.$message.error('密码不能为空!')
return
}
axios.post('/auth/login', this.form)
.then(response => {
this.$store.commit("onChangeAdminInfo", response.data.data)
this.$store.commit("onIsLogin", true)
localStorage.setItem("Authorization", response.data.data.token)
this.$message({
message: '登录成功!',
type: 'success'
});
this.$router.push({ path: '/index'})
})
.catch(error => {
console.log(error)
this.$message.error(error.response.data.message)
});
},
dmac(){
window.open("http://kf.aissz.com:666/static/app/mac-0.0.1.dmg")
},
dwin(){
window.open("http://kf.aissz.com:666/static/app/win-0.0.1.exe")
}
}
}
</script>
<style lang="stylus" scoped>
.login{
display flex
width 100%
height 100%
background url('../../assets/login_bg.jpg') center bottom no-repeat;
background-size cover
.form{
display flex
overflow hidden
width 600px;
height 300px;
background-color #fff
margin auto
border-radius 5px;
.left{
width 350px
height 100%
padding 20px;
box-sizing border-box
background url('../../assets/login_bg1.jpg') center bottom no-repeat;
background-size cover
font-size 18px;
color #fff
display flex
flex-direction column
justify-content center
.tit{
margin-top: 50px;
border-bottom 1px solid #fff
}
div{
width 300px
padding-bottom 10px;
margin-bottom 10px;
}
span{
font-size 14px;
}
.client{
margin-top: 80px;
button{
margin-top 10px
}
}
}
.right{
padding 20px;
padding-top: 50px;
.input{
margin-bottom 20px;
}
.btn-group{
margin-top: 15px;
}
.lable{
font-size 18px;
color: #606266;
margin-bottom 15px;
display block
}
}
}
}
</style>
<template>
<el-dialog width="600px" title="添加客服" :show-close="false" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
<el-form :model="form">
<el-form-item label="客服账号" :label-width="formLabelWidth">
<el-input v-model="form.username" placeholder="请输入客服账号" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="客服昵称" :label-width="formLabelWidth">
<el-input v-model="form.nickname" placeholder="请输入客服昵称" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="登录密码" :label-width="formLabelWidth">
<el-input v-model="form.password" placeholder="请输入登录密码" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="确认密码" :label-width="formLabelWidth">
<el-input v-model="cCassword" placeholder="请输入确认密码" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from "axios";
export default {
name: 'mini-im-create-admin',
data(){
return {
form: {
username: '',
nickname: '',
password: ''
},
cCassword: '',
formLabelWidth: "80px"
}
},
props:{
dialogFormVisible: Boolean,
complete: Function
},
mounted(){
},
methods: {
// 关闭
closeModal(){
this.$emit('update:dialogFormVisible', false);
},
// 保存
save() {
// 验证一下密码字段
if(this.form.username.trim() == ""){
this.$message.error("账号不能为空!")
return;
}
if(this.form.nickname.trim() == ""){
this.$message.error("昵称不能为空!")
return;
}
if(this.form.password.trim() == ""){
this.$message.error("密码不能为空!")
return;
}
if(this.form.password.trim() != this.cCassword.trim()){
this.$message.error("两次密码不一致!")
return;
}
// 验证字段 !! 算了其它前端不验证了
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
axios
.post("/admin", this.form)
.then(response => {
try {
console.log(response);
loading.close();
this.$message.success("添加成功");
this.closeModal();
this.resize();
this.complete(1);
} catch (e) {
console.log(e);
}
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message);
});
},
resize() {
this.cCassword = '';
this.form = {
username: '',
nickname: '',
password: ''
};
}
}
}
</script>
<style scoped lang="stylus">
</style>
<template>
<el-dialog
width="600px"
title="修改客服资料"
:show-close="false"
:visible.sync="dialogFormVisible"
:close-on-click-modal="false"
>
<el-form :model="form">
<el-form-item label="头像" :label-width="formLabelWidth">
<el-row :gutter="10">
<el-col :span="3">
<div class="mini-im-file-button" title="点击上传图片">
<el-avatar
:size="50"
:src="form.avatar || $store.state.avatar"
></el-avatar>
<input onClick="this.value = null" @change="changeFile" type="file" accept="image/*" />
<div v-show="isUploading" class="mini-im-file-percent">
<span>{{uploadPercent}}</span>
</div>
</div>
</el-col>
<el-col :span="6"></el-col>
</el-row>
</el-form-item>
<el-form-item label="客服账号" :label-width="formLabelWidth">
<span>{{form.username}}</span>
</el-form-item>
<el-form-item label="客服昵称" :label-width="formLabelWidth">
<el-input v-model="form.nickname" placeholder="请输入客服昵称" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="手机号" :label-width="formLabelWidth">
<el-input v-model="form.phone" placeholder="请输入登录密码" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="自动回复语" :label-width="formLabelWidth">
<el-input v-model="form.auto_reply" type="textarea" placeholder="请输入自动回复语,不支持emoji,请使用简单语句描述" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from "axios";
import upload from '../../common/upload'
export default {
name: "mini-im-create-admin",
data() {
return {
form: {
phone: "",
nickname: "",
avatar: "",
auto_reply: ""
},
formLabelWidth: "90px",
isUploading: false,
uploadPercent: ""
};
},
props: {
dialogFormVisible: Boolean,
complete: Function,
formData: Object
},
methods: {
// 关闭
closeModal() {
this.$emit("update:dialogFormVisible", false);
},
// 上传
changeFile(file) {
var fileData = file.target.files[0];
upload({
file: fileData,
progress: (percent) => {
this.isUploading = true;
this.uploadPercent = percent + "%";
},
success: (url) => {
this.isUploading = false;
this.uploadPercent = "";
this.$message.success("上传成功");
var imgUrl = this.$store.getters.uploadToken.host + "/" + url
this.form.avatar = imgUrl;
},
error: (err)=>{
this.isUploading = false;
this.uploadPercent = "";
this.$message.error(err.message);
}
});
},
// 保存
save() {
// 验证字段 !! 算了其它前端不验证了
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
axios
.put("/admin", this.form)
.then(response => {
try {
loading.close();
this.$message.success("修改成功");
this.closeModal();
this.complete(1);
} catch (e) {
console.log(e, response);
}
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message);
});
}
},
watch: {
formData() {
this.form = Object.assign({}, this.form, this.formData);
}
}
};
</script>
<style scoped lang="stylus">
.mini-im-file-button {
width: 50px;
height: 50px;
border-radius: 50%;
position: relative;
overflow: hidden;
input {
font-size: 100px;
position: absolute;
top: 0px;
left: 0px;
cursor: pointer;
opacity 0
}
cursor: pointer;
.mini-im-file-percent {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 12px;
}
}
</style>
<template>
<div>
<div class="mini-im-head">
<span>
<i class="el-icon-headset"></i>
<span slot="title">客服管理</span>
</span>
<el-button v-if="adminInfo.root == 1" @click="createDialogFormVisible = true" size="mini">添 加</el-button>
</div>
<el-divider />
<div class="search">
<el-row :gutter="20">
<el-col :span="2.1">
<el-form ref="form" label-width="120px">
<el-form-item label="按关键字:"></el-form-item>
</el-form>
</el-col>
<el-col :span="5">
<el-input @change="changeInput" @clear="clearKeyword" placeholder="请输入关键词" v-model="tableData.keyword" clearable prefix-icon="el-icon-search"></el-input>
</el-col>
<el-col :span="3">
<el-button @click="search">查 找</el-button>
</el-col>
</el-row>
</div>
<el-table
:data="tableData.list"
style="width: 100%"
v-loading="loading"
>
<el-table-column
type="index"
:index="indexMethod"
width="60">
</el-table-column>
<el-table-column
prop="avatar"
label="头像"
width="120">
<template slot-scope="scope">
<el-avatar :size="40" :src="scope.row.avatar || $store.state.avatar"></el-avatar>
</template>
</el-table-column>
<el-table-column
prop="username"
label="客服账号">
</el-table-column>
<el-table-column
prop="nickname"
label="客服昵称">
</el-table-column>
<el-table-column
prop="online"
align="center"
label="在线状态">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.online == 1">在线</el-tag>
<el-tag type="warning" v-if="scope.row.online == 2">繁忙</el-tag>
<el-tag type="info" v-if="scope.row.online == 0">离线</el-tag>
</template>
</el-table-column>
<el-table-column
prop="root"
align="center"
label="角色">
<template slot-scope="scope">
<el-tag effect="dark" type="warning" v-if="scope.row.root == 1">超级管理</el-tag>
<el-tag v-if="scope.row.root == 0">客服人员</el-tag>
</template>
</el-table-column>
<el-table-column
prop="last_activity"
label="最后在线时间">
<template slot-scope="scope">
{{$formatUnixDate(scope.row.last_activity, "YYYY/MM/DD HH:mm")}}
</template>
</el-table-column>
<el-table-column
prop="create_at"
label="创建时间">
<template slot-scope="scope">
{{$formatUnixDate(scope.row.create_at, "YYYY/MM/DD")}}
</template>
</el-table-column>
<el-table-column
prop="operating"
align="center"
width="150"
v-if="adminInfo.root == 1"
label="操作">
<template slot-scope="scope">
<el-button
size="mini"
v-if="scope.row.root == 0"
@click="edit(scope.row)">编 辑</el-button>
<el-button
size="mini"
type="danger"
v-if="scope.row.root == 0"
@click="deleteAdmin(scope.row)">删 除</el-button>
</template>
</el-table-column>
</el-table>
<el-row type="flex" style="margin-top: 20px;" justify="space-between">
<span style="color:#666;font-size: 14px;">共找到{{tableData.total}}条数据</span>
<el-pagination
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
layout="sizes, prev, pager, next"
:current-page="tableData.page_on"
:page-sizes="[5, 10, 15, 20]"
:total="tableData.total">
</el-pagination>
</el-row>
<CreateDialog :complete="getAdmins" :dialogFormVisible.sync="createDialogFormVisible" />
<EditDialog :formData="editItem" :complete="getAdmins" :dialogFormVisible.sync="editDialogFormVisible" />
</div>
</template>
<script>
import CreateDialog from "./create"
import EditDialog from "./edit"
import axios from 'axios'
export default {
name: "admins",
components: {
CreateDialog,
EditDialog
},
data() {
return {
tableData: {
list: [],
page_on: 1,
page_size: 10,
keyword: "",
total: 0,
},
createDialogFormVisible: false,
editDialogFormVisible: false,
loading: true,
editItem: {}
}
},
computed: {
adminInfo(){
return this.$store.getters.adminInfo
}
},
created(){
setTimeout(()=> this.getAdmins(1), 500)
},
methods: {
// 行号
indexMethod(index) {
return (this.tableData.page_on - 1) * this.tableData.page_size + index +1;
},
// 删除
deleteAdmin(item){
this.$confirm('您确定要删除该客服吗? 删除后不可恢复!', '温馨提示!', {
confirmButtonText: '确定',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() => {
axios.delete('/admin/' + item.id)
.then(response => {
console.log(response.data)
this.$message.success("删除成功")
this.getAdmins(1)
})
.catch(error => {
this.$message.error(error.response.data.message)
});
})
},
// 编辑
edit(item){
this.editItem = item
this.editDialogFormVisible = true
},
// 改变每页条数
handleSizeChange(val) {
this.tableData.page_size = val
this.getAdmins()
},
// 分页
handleCurrentChange(val) {
this.tableData.page_on = val
this.getAdmins()
},
// 清空关键字
clearKeyword(){
this.getAdmins(1)
},
// 关键字input变动
changeInput(){
if(this.tableData.keyword == ""){
this.getAdmins(1)
}
},
// 查找
search(){
this.tableData.keyword = this.tableData.keyword.trim()
if(!this.tableData.keyword) return
this.getAdmins(1)
},
// 获取数据
getAdmins(index){
if(index) this.tableData.page_on = index
const {page_on, page_size, keyword} = this.tableData
axios.post('/admin/list', {page_on, page_size, keyword, "online": 3})
.then(response => {
this.loading = false
this.tableData = response.data.data
})
.catch(error => {
this.loading = false
this.$message.error(error.response.data.message)
});
}
}
};
</script>
<style lang="stylus" scoped>
.mini-im-head{
height 30px
display flex
align-items center
font-size 20px
justify-content space-between
color #666
i{
margin-right 5px
}
}
.el-select .el-input {
width: 130px;
}
.input-with-select .el-input-group__prepend {
background-color: #fff;
}
</style>
<template>
<div>
<el-row :gutter="20">
<el-col :span="2">
<el-form label-width="120px">
<el-form-item label="按日期:">
</el-form-item>
</el-form>
</el-col>
<el-col :span="6.5">
<el-date-picker
@change="changeDate"
v-model="selectDate"
type="daterange"
align="right"
:editable="false"
:clearable="false"
unlink-panels
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-col>
<el-col :span="5">
<el-select @change="changeSelectd" v-model="selectDateValue" placeholder="快捷选择日期">
<el-option
v-for="item in optionsDate"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-col>
<el-col :span="6.5">
<div class="online-count">
当天累积访问用户<span>{{todayStatisticalTableDataCount}}</span> 当前在线用户<span>{{onlines}}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="16">
<div class="mini-im-home-title">各渠道<span style="color: #f44336">{{optionsDate[selectDateValue].label}}</span>服务量统计</div>
<ve-line :data="statisticalChartData"></ve-line>
</el-col>
<el-col :span="8">
<div class="mini-im-home-title">客服<span style="color: #f44336">{{optionsDate[selectDateValue].label}}</span>接入量统计</div>
<ve-funnel :data="membersChartData"></ve-funnel>
</el-col>
</el-row>
<div>
<div class="mini-im-home-title" style="padding-bottom: 10px;">各渠道<span style="color: #f44336;">当天独立用户</span>访问量</div>
<el-table
:data="todayStatisticalTableData"
style="width: 100%">
<el-table-column
prop="platform"
label="#序号(ID)"
width="180">
</el-table-column>
<el-table-column
prop="title"
label="平台名称"
width="250">
</el-table-column>
<el-table-column
prop="count"
align="center"
width="180"
label="当天访问人次">
</el-table-column>
<el-table-column
label="">
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import VeLine from "v-charts/lib/line.common";
import VeFunnel from "v-charts/lib/funnel.common";
import axios from 'axios'
var moment = require('moment');
export default {
name: "home",
components: { VeLine, VeFunnel },
data() {
return {
todayStatisticalTableData: [],
selectDate: [],
optionsDate: [
{
value: 0,
label: '今天'
},
{
value: 1,
label: '昨天'
},
{
value: 2,
label: '近7天'
},
{
value: 3,
label: '近30天'
},
{
value: 4,
label: '本月'
},
{
value: 5,
label: '上月'
},
],
selectDateValue: 2,
statisticalChartData: {
columns: [],
rows: []
},
membersChartData: {
columns: ["状态", "数值"],
rows: []
},
onlines: 0
};
},
created(){
this.selectDate = this.getDate(2)
this.getStatistical()
this.getTodayStatistical()
this.getOnlines()
},
computed:{
todayStatisticalTableDataCount(){
var count = 0
var self = this
self.todayStatisticalTableData.map((item) => {
count = count + parseInt(item.count)
})
if(self.todayStatisticalTableData.length > 0){
var totalCount = self.todayStatisticalTableData[0].count
self.todayStatisticalTableData[0].count = parseInt(totalCount) + count
}
return count
}
},
methods: {
// 获取数据
getStatistical(){
axios.post('/home/statistical', {
"date_start": moment(this.selectDate[0]).format("YYYY-MM-DD"),
"date_end": moment(this.selectDate[1]).format("YYYY-MM-DD")
})
.then(response => {
const {members, statistical} = response.data.data
// 处理客服
var membersRows = []
for(let i = 0; i<members.length; i++){
membersRows.push({
"状态": members[i].nickname || members[i].username,
"数值": members[i].count
})
}
this.membersChartData.rows = membersRows
// 处理statisticalChartData
var statisticalChartDataColumns = ["日期"]
var statisticalChartDataRows = []
for(let i = 0; i< statistical[0]['list'].length; i++){
statisticalChartDataColumns.push(statistical[0]['list'][i].title)
}
this.statisticalChartData.columns = statisticalChartDataColumns
for(let i = 0; i< statistical.length; i++){
var jsonData = {}
jsonData["日期"] = statistical[i].date
for(let b = 0;b < statistical[i].list.length; b++){
var item = statistical[i].list[b]
jsonData[item["title"]] = item["count"]
}
statisticalChartDataRows.push(jsonData)
}
this.statisticalChartData.rows = statisticalChartDataRows
})
.catch(error => {
this.$message.error(error.response.data.message)
});
},
// 获取今天访问数据
getTodayStatistical(){
axios.post('/home/today_statistical', {
"date_start": moment(new Date()).format("YYYY-MM-DD"),
"date_end": moment(new Date()).format("YYYY-MM-DD")
})
.then(response => {
this.todayStatisticalTableData = response.data.data
})
.catch(error => {
this.$message.error(error.response.data.message)
});
},
// 快捷日期变化
changeSelectd(){
this.selectDate = this.getDate(this.selectDateValue)
this.changeDate()
},
// 获取时间
getDate(index){
var start,end
switch(index){
case 0:
start = new Date()
end = new Date()
break
case 1:
var yesterday = moment().dayOfYear(moment().dayOfYear() -1);
end = yesterday;
start = yesterday;
break
case 2:
end = new Date();
start = moment().dayOfYear(moment().dayOfYear() -6);
break
case 3:
end = new Date();
start = moment().dayOfYear(moment().dayOfYear() -29);
break
case 4:
end = new Date();
start = moment().dayOfYear(moment().dayOfYear() - new Date().getDate() + 1);
break
case 5:
end = moment(moment().subtract(1, 'months').startOf('month')).endOf('month')
start = moment().subtract(1, 'months').startOf('month')
break
}
return [start, end]
},
// 时间变更
changeDate(){
this.getStatistical()
},
// 获取在线用户数
getOnlines(){
axios.get('/user/onlines')
.then(response => {
this.onlines = response.data.data
})
.catch(error => {
this.$message.error(error.response.data.message)
});
}
}
};
</script>
<style lang="stylus" scoped>
.mini-im-home-title {
text-align: center;
font-size: 18px;
color: #666;
padding: 15px 0 50px;
}
.mini-im-home-copyright {
text-align: center;
color: #666;
font-size: 14px;
padding-top: 50px;
}
.online-count{
text-align center
color #666
margin-top 10px
span{
color #8bc34a
margin 0 5px
}
}
</style>
<template>
<el-container>
<me-aside v-if="$store.state.isShowAside"></me-aside>
<el-container style="min-width: 800px;">
<el-header class="mini-im-header">
<me-heaser title="sdfsd"></me-heaser>
</el-header>
<el-main :style="'background-color:' + workbenchBgColor">
<router-view></router-view>
</el-main>
</el-container>
<EditProfile />
<EditPassword />
</el-container>
</template>
<script>
import MeAside from "@/components/me-aside.vue";
import MeHeaser from "@/components/me-header.vue";
import EditProfile from "@/components/me-edit-profile.vue";
import EditPassword from "@/components/me-edit-password.vue";
export default {
name: "home",
components: {
MeAside,MeHeaser,EditProfile,EditPassword
},
computed: {
workbenchBgColor(){
if(this.$route.path == "/workbench"){
return this.$store.getters.workbenchBgColor
}
return "#ffffff"
}
}
};
</script>
<style lang="stylus" scoped>
.mini-im-header{
background-color #545c64
border-bottom 1px solid #545c64
}
</style>
<template>
<el-dialog
width="600px"
title="添加新的知识"
:show-close="false"
:visible.sync="dialogFormVisible"
:close-on-click-modal="false"
>
<el-form :model="form">
<el-form-item label="主标题" :label-width="formLabelWidth">
<el-input v-model="form.title" placeholder="请输入主标题" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="子标题" :label-width="formLabelWidth">
<el-tag
:key="tag"
v-for="tag in dynamicTags"
closable
:disable-transitions="false"
@close="handleDel(tag)"
>{{tag}}</el-tag>
<el-input
class="input-new-tag"
v-if="inputVisible"
v-model="inputValue"
ref="saveTagInput"
size="small"
@keyup.enter.native="handleInputConfirm"
@blur="handleInputConfirm"
></el-input>
<el-button v-else class="button-new-tag" size="small" @click="showInput">+ 新增</el-button>
</el-form-item>
<el-form-item label="内容" :label-width="formLabelWidth">
<el-input rows="5" type="textarea" v-model="form.content" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="匹配平台" :label-width="formLabelWidth">
<el-select v-model="form.platform" placeholder="请选择匹配平台">
<el-option
:label="item.title"
:value="item.id"
:key="index"
v-for="(item, index) in $store.getters.platformConfig"
></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from "axios";
export default {
name: "mini-im-create-knowledge",
data() {
return {
dynamicTags: [],
inputVisible: false,
inputValue: "",
form: {
uid: "",
platform: 1,
title: "",
sub_title: "",
content: ""
},
formLabelWidth: "80px"
};
},
props: {
dialogFormVisible: Boolean,
complete: Function
},
methods: {
// 关闭
closeModal() {
this.$emit("update:dialogFormVisible", false);
},
// 删除标签
handleDel(tag) {
this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1);
},
// 显示子标题输入框
showInput() {
this.inputVisible = true;
this.$nextTick(() => {
this.$refs.saveTagInput.$refs.input.focus();
});
},
// 标签确定
handleInputConfirm() {
let inputValue = this.inputValue;
if (inputValue) {
this.dynamicTags.push(inputValue);
}
this.inputVisible = false;
this.inputValue = "";
console.log(this.dynamicTags.join("|"));
},
// 保存
save() {
// 验证字段 !! 算了前端不验证了
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
this.form.uid = this.$store.state.adminInfo.id;
this.form.sub_title = this.dynamicTags.join("|");
axios
.post("/knowledge", this.form)
.then(response => {
try {
console.log(response);
loading.close();
this.$message.success("添加成功");
this.closeModal();
this.resize();
this.complete(1);
} catch (e) {
console.log(e);
}
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message);
});
},
resize() {
this.dynamicTags = [];
this.form = {
uid: "",
platform: 1,
title: "",
sub_title: "",
content: ""
};
}
}
};
</script>
<style scoped lang="stylus">
.el-tag + .el-tag {
margin-left: 10px;
}
.button-new-tag {
margin-left: 10px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
}
.input-new-tag {
width: 150px;
margin-left: 10px;
vertical-align: bottom;
}
</style>
<template>
<el-dialog width="600px" title="编辑的知识" :show-close="false" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
<el-form :model="form">
<el-form-item label="主标题" :label-width="formLabelWidth">
<el-input v-model="form.title" placeholder="请输入主标题" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="子标题" :label-width="formLabelWidth">
<el-tag
:key="tag"
v-for="tag in dynamicTags"
closable
:disable-transitions="false"
@close="handleDel(tag)">
{{tag}}
</el-tag>
<el-input
class="input-new-tag"
v-if="inputVisible"
v-model="inputValue"
ref="saveTagInput"
size="small"
@keyup.enter.native="handleInputConfirm"
@blur="handleInputConfirm"
>
</el-input>
<el-button v-else class="button-new-tag" size="small" @click="showInput">+ 新增子标题</el-button>
</el-form-item>
<el-form-item label="内容" :label-width="formLabelWidth">
<el-input rows="5" type="textarea" v-model="form.content" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="匹配平台" :label-width="formLabelWidth">
<el-select v-model="form.platform" placeholder="请选择匹配平台">
<el-option :label="item.title" :value="item.id" :key="index" v-for="(item, index) in platformConfig"></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from 'axios'
export default {
name: 'mini-im-create-knowledge',
data(){
return {
dynamicTags: [],
inputVisible: false,
inputValue: '',
form: {
uid: "",
platform: 1,
title: '',
sub_title: '',
content: '',
},
platformConfig: [],
formLabelWidth: "80px"
}
},
props:{
dialogFormVisible: Boolean,
complete: Function,
formData: Object
},
methods: {
// 关闭
closeModal(){
this.$emit('update:dialogFormVisible', false);
},
// 删除标签
handleDel(tag) {
this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1);
},
// 显示子标题输入框
showInput() {
this.inputVisible = true;
this.$nextTick(() => {
this.$refs.saveTagInput.$refs.input.focus();
});
},
// 确定
handleInputConfirm() {
let inputValue = this.inputValue;
if (inputValue) {
this.dynamicTags.push(inputValue);
}
this.inputVisible = false;
this.inputValue = '';
},
// 保存
save(){
// 验证字段 !! 算了前端不验证了
const loading = this.$loading({
lock: true,
text: '保存中...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.5)'
});
this.form.uid = this.$store.state.adminInfo.id
this.form.sub_title = this.dynamicTags.join("|")
axios.put('/knowledge', this.form)
.then(response => {
try{
console.log(response)
loading.close();
this.$message.success("修改成功")
this.closeModal()
this.resize()
this.complete(1)
}catch(e){
console.log(e)
}
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message)
});
},
resize(){
this.dynamicTags = []
this.form = {
uid: "",
platform: 1,
title: '',
sub_title: '',
content: '',
};
}
},
watch:{
formData(){
this.platformConfig = this.$store.getters.platformConfig
this.form = Object.assign({},this.form, this.formData)
if(this.formData.sub_title != "") this.dynamicTags = this.formData.sub_title.split("|")
}
}
}
</script>
<style scoped lang="stylus">
.el-tag + .el-tag {
margin-left: 10px;
}
.button-new-tag {
margin-left: 10px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
}
.input-new-tag {
width: 150px;
margin-left: 10px;
vertical-align: bottom;
}
</style>
<template>
<div>
<div class="me-head">
<span>
<i class="el-icon-reading"></i>
<span slot="title">知识库管理 </span>
</span>
<el-button-group>
<template v-for="item in total" >
<el-button :class="{'el-button--primary': item.id + '' == tableData.platform + ''}" @click="onTogglePlatform(item.id)" :key="item.id" size="mini">
{{item.title}} ({{item.count}})
</el-button>
</template>
</el-button-group>
<el-col :span="5">
<el-input @change="onRefresh" placeholder="请输入关键词" prefix-icon="el-icon-search" v-model="keyword" clearable></el-input>
</el-col>
<el-button @click="createDialogFormVisible = true" size="mini">添 加</el-button>
</div>
<el-divider />
<el-table
:data="tableData.list"
style="width: 100%"
v-loading="loading"
>
<el-table-column
type="index"
:index="indexMethod"
width="60">
</el-table-column>
<el-table-column
prop="title"
label="主标题">
</el-table-column>
<el-table-column
prop="sub_title"
label="子标题">
<template slot-scope="scope">
<div v-if="scope.row.sub_title != ''">
<div style="font-size: 13px;" :key="key" v-for="(item, key) in scope.row.sub_title.split('|')">
{{key+1}}.{{item}}
</div>
</div>
<div v-else>-----</div>
</template>
</el-table-column>
<el-table-column
prop="content"
label="内容">
<template slot-scope="scope">
<div style="font-size: 13px;" :key="key" v-for='(item, key) in scope.row.content.split("\n")'>
{{item}}
</div>
</template>
</el-table-column>
<el-table-column
prop="platform"
align="center"
label="匹配平台">
<template slot-scope="scope">
<el-tag>{{$getPlatformItem(scope.row.platform).title}}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="create_at"
label="创建时间">
<template slot-scope="scope">
{{$formatUnixDate(scope.row.create_at, "YYYY/MM/DD")}}
</template>
</el-table-column>
<el-table-column
prop="operating"
align="center"
width="150"
label="操作">
<template slot-scope="scope">
<el-button
size="mini"
@click="edit(scope.row)">编 辑</el-button>
<el-button
size="mini"
type="danger"
@click="deleteKnowledge(scope.row)">删 除</el-button>
</template>
</el-table-column>
</el-table>
<el-row type="flex" style="margin-top: 20px;" justify="space-between">
<span style="color:#666;font-size: 14px;">共找到{{tableData.total}}条数据</span>
<el-pagination
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
layout="sizes, prev, pager, next"
:current-page="tableData.page_on"
:page-sizes="[5, 10, 15, 20]"
:total="tableData.total">
</el-pagination>
</el-row>
<CreateDialog :complete="onRefresh" :dialogFormVisible.sync="createDialogFormVisible" />
<EditDialog :formData="editItem" :complete="onRefresh" :dialogFormVisible.sync="editDialogFormVisible" />
</div>
</template>
<script>
import CreateDialog from "./create"
import EditDialog from "./edit"
import axios from 'axios'
export default {
name: "knowledge",
components: {
CreateDialog,
EditDialog
},
data() {
return {
keyword: "",
tableData: {
list: [],
page_on: 1,
page_size: 10,
total: 0,
keyword: "",
platform: 1,
},
total: [],
createDialogFormVisible: false,
editDialogFormVisible: false,
loading: true,
editItem: null
}
},
computed: {
platformConfig(){
return this.$store.state.platformConfig || []
}
},
created(){
setTimeout( ()=> {
this.getKnowledgeList()
this.getTotal()
}, 500)
},
methods: {
onRefresh(){
this.getTotal()
this.getKnowledgeList()
},
// 行号
indexMethod(index) {
return (this.tableData.page_on - 1) * this.tableData.page_size + index +1;
},
// 删除
deleteKnowledge(item){
this.$confirm('您确定要删除该知识库吗? 删除后不可恢复!', '温馨提示!', {
confirmButtonText: '确定',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() => {
axios.delete('/knowledge/' + item.id)
.then(response => {
console.log(response.data)
this.$message.success("删除成功")
this.getKnowledgeList(1)
})
.catch(error => {
this.$message.error(error.response.data.message)
});
})
},
// 切换显示平台
onTogglePlatform(pid){
this.tableData.platform = parseInt(pid)
this.getKnowledgeList(1)
},
// 编辑
edit(item){
this.editItem = item
this.editDialogFormVisible = true
},
// 改变每页条数
handleSizeChange(val) {
this.tableData.page_size = val
this.getKnowledgeList()
},
// 分页
handleCurrentChange(val) {
this.tableData.page_on = val
this.getKnowledgeList()
},
// 获取数据
getKnowledgeList(index){
if(index) this.tableData.page_on = index
const {page_on, page_size, platform} = this.tableData
const keyword = this.keyword
axios.post('/knowledge/list', {page_on, page_size, platform, keyword})
.then(response => {
this.loading = false
this.tableData = response.data.data
})
.catch(error => {
this.loading = false
this.$message.error(error.response.data.message)
});
},
// 获取统计数据
getTotal(){
axios.get('/knowledge/total')
.then(response => {
this.total = response.data.data
})
},
},
};
</script>
<style lang="stylus" scoped>
.me-head{
height 30px
display flex
align-items center
font-size 20px
justify-content space-between
color #666
i{
margin-right 5px
}
}
</style>
<template>
<div class="mini-im-chat-list">
<div class="mini-im-chat-message-box">
<div class="loading" v-show="loading">
<i class="el-icon-loading"></i><span>消息加载中...</span>
</div>
<el-button v-show="isMessageEnd" type="text" disabled icon="el-icon-refresh-right">无更多聊天记录...</el-button>
<el-button v-if="!isMessageEnd && !loading" type="text" @click="onLoadMor" icon="el-icon-refresh">点击加载更多聊天记录</el-button>
</div>
<div class="mini-im-chat-message-box">
<div class="loading" v-show="messages.length <= 0 && !loading">
<i class="el-icon-time"></i><span>暂无聊天记录...</span>
</div>
</div>
<div class="mini-im-chat-message-box" :class="{'self': item.from_account != userId}" v-for="(item, index) in messages" :key="index">
<!-- 用户信息 -->
<template v-if="item.biz_type == 'text' || item.biz_type == 'photo' || item.biz_type == 'knowledge' || item.biz_type == 'knowledge_list'">
<div class="user-date">
<span v-if="item.from_account == seviceId">
{{seviceNickname}}
</span>
<span v-else-if="item.from_account == userId">
{{userNickname}}
</span>
<span v-else>
<span style="font-size:12px;color: #666;">(机器人)</span>{{$robotNickname(item.from_account)}}
</span>
<em>{{$formatFromNowDate(item.timestamp)}}</em>
</div>
</template>
<!-- 文本消息 -->
<template v-if="item.biz_type == 'text'">
<div class="text">
<span v-html="item.payload.replace(/\n/ig, '<br />')"></span>
</div>
</template>
<!-- 图片 -->
<template v-if="item.biz_type == 'photo'">
<div class="photo">
<div class="loading" v-if="item.percent && item.percent != 100">
<i class="el-icon-loading"></i>
<span>{{item.percent}}%</span>
</div>
<div class="img-content">
<img :src="item.payload" preview="1" />
</div>
</div>
</template>
<!-- 转接 -->
<template v-if="item.biz_type == 'transfer'">
<div class="system">
<span>{{item.payload}}</span>
<em>{{$formatFromNowDate(item.timestamp)}}</em>
</div>
</template>
<!-- 结束聊天 -->
<template v-if="item.biz_type == 'end'">
<div class="system">
<span v-if="item.to_account != adminInfo.id">你结束了会话</span>
<span v-else>对方结束了会话</span>
<em>{{$formatFromNowDate(item.timestamp)}}</em>
</div>
</template>
<!-- 聊天超时 -->
<template v-if="item.biz_type == 'timeout'">
<div class="system">
<span>用户长时间无应答,会话结束</span>
<em>{{$formatFromNowDate(item.timestamp)}}</em>
</div>
</template>
<!-- 撤回消息 -->
<template v-if="item.biz_type == 'cancel'">
<div class="system">
<span v-if="item.from_account == adminInfo.id">您撤回了一条消息</span>
<span v-else>对方撤回了一条消息</span>
<em>{{$formatFromNowDate(item.timestamp)}}</em>
</div>
</template>
<!-- 知识库列表 -->
<template v-if="item.biz_type == 'knowledge'">
<div class="knowledge">
<div class="content">
<div class="title">以下是否是您关心的相关问题呢?</div>
<div class="item" :key="index" v-for="(item, index) in JSON.parse(item.payload)">
{{index+1}}.{{item.title}}
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<script>
export default {
name: "mini-im-contact",
data() {
return {};
},
computed: {
seviceCurrentUser(){
return this.$store.getters.seviceCurrentUser || {}
},
adminInfo(){
return this.$store.getters.adminInfo || {}
}
},
props: {
loading: Boolean,
isMessageEnd: Boolean,
messages: Array,
userId: String,
userNickname: String,
seviceId: String,
seviceNickname: String,
onLoadMor: Function
},
watch:{
messages(){
setTimeout(()=>{
this.$previewRefresh()
}, 1000)
}
}
};
</script>
<style scoped lang="stylus">
.mini-im-chat-list {
display: flex;
flex-direction: column;
.mini-im-chat-message-box {
width: 100%;
display: flex;
flex-direction: column;
margin-bottom: 15px;
.user-date {
display: flex;
align-items: center;
color: #999;
font-size: 14px;
span {
color: #666;
font-weight: 500;
font-size: 14px;
padding: 0 5px;
}
em {
font-style: normal;
font-size 12px
}
}
.loading{
color #666
display: flex;
margin-top: 5px;
align-items center
align-content center
justify-content center
span{
margin-left 5px
font-size 13px
}
}
.text {
display: flex;
margin-top: 5px;
span {
max-width: 40%;
display: inline;
padding: 5px 10px;
border-radius: 5px;
background-color: #eef4f9;
font-size: 14px;
color: #666;
}
}
.photo {
display: flex;
margin-top: 5px;
.loading{
align-self flex-end
padding 0 5px
span{
background none !important
color: #999 !important
}
}
.img-content{
border-radius: 5px;
width: 200px;
overflow hidden
}
img {
cursor: pointer;
width: 100%;
height 100%
display: inline;
}
}
.knowledge {
display: flex;
margin-top: 5px;
justify-content: flex-end;
.content {
display: flex;
flex-direction: column;
padding: 5px;
border-radius: 5px;
color: #666;
text-align: left;
background-color: #eef4f9;
.title {
font-size: 13px;
font-weight: 500;
}
.item {
font-size: 13px;
line-height: 22px;
}
}
}
.system {
display: flex;
margin-top: 5px;
flex-direction: column;
align-items: center;
justify-content: center;
em{
margin-top: 5px;
font-size: 12px;
color: #999;
}
span {
font-size: 12px;
max-width: 50%;
min-width: 100px;
display: inline;
padding: 3px 20px;
border-radius: 5px;
text-align: center;
background-color: #f2f2f2;
color: #999;
}
}
&.self {
text-align: right;
.user-date {
display: flex;
justify-content: flex-end;
span {
order: -2;
}
em {
order: -3;
}
}
.text, .photo {
justify-content: flex-end;
align-items flex-end
.cancel-btn{
color #26a2ff
font-size 12px
margin-right 5px
cursor pointer
}
span {
background-color: rgba(33, 150, 243, 0.72);
color: #fff;
text-align left
}
}
.knowledge>.content {
background-color: rgba(33, 150, 243, 0.72);
color: #fff;
}
}
}
}
</style>
<template>
<div class="record-page">
<div class="record-mini-im-head">
<span>
<i class="el-icon-time"></i>
<span slot="title">服务记录</span>
</span>
</div>
<el-divider />
<div class="search">
<el-row :gutter="20">
<el-col style="width: 120px">
<el-form ref="form" label-width="120px">
<el-form-item :label="adminInfo.root == 1 ? '按客服:' : '按日期:'"></el-form-item>
</el-form>
</el-col>
<el-col v-if="adminInfo.root == 1" :span="3">
<el-select v-model="selectCustomerId" @change="refreshRecord" placeholder="请选择客服">
<el-option
v-for="item in customerData"
:key="item.id"
:label="item.nickname"
:value="item.id"
></el-option>
</el-select>
</el-col>
<el-col :span="5.5">
<el-date-picker
v-model="selectDate"
align="right"
type="date"
@change="refreshRecord"
placeholder="选择日期"
:picker-options="pickerOptions">
</el-date-picker>
</el-col>
<el-col :span="5.5">
<el-checkbox v-model="isDeWeighting" label="去重目标客户" @change="refreshRecord" border></el-checkbox>
</el-col>
<el-col :span="5.5">
<el-checkbox v-model="isReception" label="只显示未接待客户" @change="refreshRecord" border></el-checkbox>
</el-col>
</el-row>
</div>
<el-table :data="tableData.list" v-loading="loading" style="width: 100%">
<el-table-column
type="index"
:index="indexMethod"
width="60">
</el-table-column>
<el-table-column prop="service_account" label="接待客服">
<template slot-scope="scope">
<span>{{serviceNickname(scope.row.service_account)}}</span>
</template>
</el-table-column>
<el-table-column prop="nickname" label="目标客户">
<template slot-scope="scope">
<el-tag type="success">{{scope.row.nickname}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="is_reception" label="是否已接待">
<template slot-scope="scope">
<el-tag v-show="scope.row.is_reception == 0" type="danger">未接待</el-tag>
<el-tag v-show="scope.row.is_reception == 1" type="success">已接待</el-tag>
</template>
</el-table-column>
<el-table-column prop="transfer_account" label="满意度">
<template>
<span>-----</span>
</template>
</el-table-column>
<el-table-column prop="platform" label="客户端平台">
<template slot-scope="scope">
<el-tag>{{$getPlatformItem(scope.row.platform).title}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_at" label="服务时间">
<template slot-scope="scope">
{{$formatUnixDate(scope.row.create_at, "YYYY/MM/DD HH:mm:ss")}}
</template>
</el-table-column>
<el-table-column prop="operating" align="center" label="操作" width="150">
<template slot-scope="scope">
<el-button size="mini" @click="openModal(scope)">聊天记录</el-button>
</template>
</el-table-column>
</el-table>
<el-row type="flex" style="margin-top: 20px;" justify="space-between">
<span style="color:#666;font-size: 14px;">共找到{{tableData.total}}条数据</span>
<el-pagination
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
layout="sizes, prev, pager, next"
:current-page="tableData.page_on"
:page-sizes="[5, 10, 15, 20]"
:total="tableData.total">
</el-pagination>
</el-row>
<!-- 聊天数据模态框 -->
<el-dialog :visible.sync="dialogFormVisible">
<div slot="title" class="dialog-title">
<div style="color: #666">
<span style="color: #e6a23c">{{serviceNickname(selectUser.service_account)}}</span>
<span style="color: #67c23a">{{selectUser.nickname}}</span>
的聊天记录
</div>
</div>
<div class="record-modal-chat-box" ref="chatBody" id="chatBody">
<ChatsComponent
:isMessageEnd="isMessageEnd"
:seviceId="selectCustomerId+''"
:seviceNickname="serviceNickname(selectUser.service_account)"
:messages="messageRecord.list"
:userId="selectUser.user_account"
:userNickname="selectUser.nickname"
:onLoadMor="onLoadMor"
:loading="getMessageRecordLoading"/>
</div>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios'
var moment = require('moment');
import ChatsComponent from "./chats"
export default {
name: "robot",
components:{
ChatsComponent
},
data() {
return {
loading: true,
isDeWeighting: false,
isReception: false,
selectDate: Date.now(),
tableData: {
list: [],
page_on: 1,
page_size: 10,
cid: 0,
total: 0,
is_de_weighting: false,
date: "",
},
customerData: [],
selectCustomerId: null,
selectUser: {},
pickerOptions: {
disabledDate(time) {
return time.getTime() > Date.now();
},
shortcuts: [{
text: '今天',
onClick(picker) {
picker.$emit('pick', new Date());
}
}, {
text: '昨天',
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', date);
}
}, {
text: '一周前',
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', date);
}
}]
},
isMessageEnd: false,
// 模态框数据
getMessageRecordLoading: false,
getMessageRecordPageSize: 20,
dialogFormVisible: false,
messageRecord: {
list: []
}
};
},
computed: {
platformConfig(){
return this.$store.getters.platformConfig
},
adminInfo(){
return this.$store.getters.adminInfo
}
},
created() {
this.getAdmins()
},
mounted(){
setTimeout(() =>{
this.selectCustomerId = this.adminInfo.id
this.getRecord(1)
}, 1000)
},
methods: {
// 行号
indexMethod(index) {
return (this.tableData.page_on - 1) * this.tableData.page_size + index +1;
},
// 改变每页条数
handleSizeChange(val) {
this.tableData.page_size = val
this.getRecord()
},
// 分页
handleCurrentChange(val) {
this.tableData.page_on = val
this.getRecord()
},
// 获取客服昵称
serviceNickname(id){
let nickname = ""
for(let i =0; i< this.customerData.length; i++){
if(this.customerData[i].id == id){
nickname = this.customerData[i].nickname
break
}
}
return nickname
},
// 获取数据
getAdmins(){
axios.post('/admin/list', {page_on: 1, page_size: 100, "online": 3})
.then(response => {
this.customerData = response.data.data.list
})
.catch(error => {
this.$message.error(error.response.data.message)
});
},
// 获取数据
getRecord(index){
this.loading = true
if(index) this.tableData.page_on = index
const {page_on, page_size} = this.tableData
axios.post('/services_statistical/list', {
page_on,
page_size,
cid: this.selectCustomerId,
date: moment(this.selectDate).format("YYYY-MM-DD"),
is_de_weighting: this.isDeWeighting,
is_reception: this.isReception
})
.then(response => {
this.loading = false
this.tableData = response.data.data
})
.catch(error => {
this.loading = false
this.$message.error(error.response.data.message)
});
},
// 刷新记录
refreshRecord(){
this.getRecord()
},
// 打开模态框
openModal(scope){
this.selectUser = scope.row
this.isMessageEnd = false
this.dialogFormVisible = true
this.messageRecord = {
list: []
}
this.getMessageRecord()
},
// 获取聊天记录
getMessageRecord(timestamp){
this.getMessageRecordLoading = true
if(timestamp == undefined){
timestamp = 0
}
axios.post('/message/list', {
"timestamp": timestamp,
"page_size": this.getMessageRecordPageSize,
"service": parseInt(this.selectCustomerId),
"account": parseInt(this.selectUser.user_account)
})
.then(response => {
this.getMessageRecordLoading = false
if(response.data.data.list.length < this.getMessageRecordPageSize){
this.isMessageEnd = true
}
if(this.messageRecord.list.length == 0 || timestamp == 0){
this.messageRecord = response.data.data
this.scrollIntoBottom()
}else{
response.data.data.list = response.data.data.list.concat(this.messageRecord.list)
this.messageRecord = response.data.data
}
setTimeout(()=>this.$previewRefresh(), 500)
})
.catch(error => {
console.log(error)
this.getMessageRecordLoading = false
});
},
// 加载更多数据
onLoadMor(){
if(this.getMessageRecordLoading) return
if(this.messageRecord.list.length >= this.messageRecord.total || this.messageRecord.total <= this.getMessageRecordPageSize){
this.isMessageEnd = true
return
}
this.getMessageRecord(this.messageRecord.list[0].timestamp)
setTimeout(()=>{
var chatBody = document.getElementById("chatBody")
chatBody.scrollTop = 500
}, 50)
},
// 滚动条置底
scrollIntoBottom(){
try{
setTimeout(()=>{
var chatBody = document.getElementById("chatBody")
if(!chatBody) return
var height = chatBody.clientHeight
var scrollHeight = chatBody.scrollHeight
chatBody.scrollTop = scrollHeight-height
}, 50)
}catch(e){
console.log(e)
}
},
}
};
</script>
<style lang="stylus">
.record-page .record-mini-im-head {
height: 30px;
display: flex;
align-items: center;
font-size: 20px;
justify-content: space-between;
color: #666;
i {
margin-right: 5px;
}
}
.record-page .record-modal-chat-box{
height 600px;
padding 0 20px
overflow hidden
overflow-y auto
}
.record-page .el-dialog__body{
padding: 0px;
border-top: 1px solid #f7f7f7
}
</style>
<template>
<el-dialog
width="600px"
title="添加机器人"
:show-close="false"
:visible.sync="dialogFormVisible"
:close-on-click-modal="false"
>
<el-form :model="form">
<el-form-item label="头像" :label-width="formLabelWidth">
<el-row :gutter="10">
<el-col :span="3">
<div class="mini-im-file-button" title="点击上传图片">
<el-avatar
:size="50"
:src="form.avatar"
></el-avatar>
<input onClick="this.value = null" @change="changeFile" type="file" accept="image/*" />
<div v-show="isUploading" class="mini-im-file-percent">
<span>{{uploadPercent}}</span>
</div>
</div>
</el-col>
<el-col :span="6"></el-col>
</el-row>
</el-form-item>
<el-form-item label="机器人昵称" :label-width="formLabelWidth">
<el-input v-model="form.nickname" placeholder="请输入机器人昵称" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="机器人欢迎语" :label-width="formLabelWidth">
<el-input type="textarea" :rows="2" v-model="form.welcome" placeholder="请输入机器人欢迎语" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="无匹配知识库语" :label-width="formLabelWidth">
<el-input type="textarea" :rows="2" v-model="form.understand" placeholder="请输入无法识别回复语" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="超时结束提示" :label-width="formLabelWidth">
<el-input type="textarea" :rows="2" v-model="form.timeout_text" placeholder="请输入会话超时结束提示" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="无人工在线提示" :label-width="formLabelWidth">
<el-input type="textarea" :rows="2" v-model="form.no_services" placeholder="请输入无人工在线提示" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="长时间等待提示" :label-width="formLabelWidth">
<el-input type="textarea" :rows="2" v-model="form.loog_time_wait_text" placeholder="请输入长时间等待提示语" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="检索知识库热词" :label-width="formLabelWidth">
<el-tag
:key="tag"
v-for="tag in keyWordTags"
closable
:disable-transitions="false"
@close="handleKeyWordDel(tag, 'keyWordTagsInput')"
>{{tag}}</el-tag>
<el-input
class="input-new-tag"
v-if="showkeyWordTagsInput"
v-model="inputkeyWordTagValue"
ref="keyWordTagsInput"
size="small"
@keyup.enter.native="handleInputConfirm"
@blur="handleInputConfirm"
></el-input>
<el-button v-else class="button-new-tag" size="small" @click="showTagInput('keyWordTagsInput')">+ 新增</el-button>
<div style="font-size:12px;">* 该词库会在用户输入的时候去匹配检索提示</div>
</el-form-item>
<el-form-item label="转人工关键词" :label-width="formLabelWidth">
<el-tag
:key="tag"
v-for="tag in dynamicTags"
closable
:disable-transitions="false"
@close="handleKeyWordDel(tag, 'dynamicTagsInput')"
>{{tag}}</el-tag>
<el-input
class="input-new-tag"
v-if="showDynamicTagsInput"
v-model="inputDynamicTagValue"
ref="dynamicTagsInput"
size="small"
@keyup.enter.native="handleInputConfirm"
@blur="handleInputConfirm"
></el-input>
<el-button v-else class="button-new-tag" size="small" @click="showTagInput('dynamicTagsInput')">+ 新增</el-button>
<div style="font-size:12px;">* 匹配该关键词进入人工,系统已内置: "人工"</div>
</el-form-item>
<el-form-item label="运行状态" :label-width="formLabelWidth">
<el-switch v-model="robotSwitch" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</el-form-item>
<el-form-item label="匹配平台" :label-width="formLabelWidth">
<el-select v-model="form.platform" placeholder="请选择匹配平台">
<el-option
:label="item.title"
:value="item.id"
:key="index"
v-for="(item, index) in $store.getters.platformConfig"
></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from 'axios'
import upload from '../../common/upload'
export default {
name: "mini-im-create-robot",
data() {
return {
dynamicTags: [],
keyWordTags: [],
showkeyWordTagsInput: false,
inputkeyWordTagValue: "",
showDynamicTagsInput: false,
inputDynamicTagValue: "",
form: {
nickname: "",
avatar: "",
welcome: "",
understand: "",
artificial: "",
keyword: "",
timeout_text: "",
no_services: "",
loog_time_wait_text: "",
platform: 1,
switch: 1
},
robotSwitch: true,
formLabelWidth: "120px",
isUploading: false,
uploadPercent: ""
};
},
props: {
dialogFormVisible: Boolean,
complete: Function
},
methods: {
// 关闭
closeModal() {
this.resize();
this.$emit("update:dialogFormVisible", false);
},
// 上传
changeFile(file) {
var fileData = file.target.files[0];
upload({
file: fileData,
progress: (percent) => {
this.isUploading = true;
this.uploadPercent = percent + "%";
},
success: (url) => {
this.isUploading = false;
this.uploadPercent = "";
this.$message.success("上传成功");
var imgUrl = this.$store.getters.uploadToken.host + "/" + url
this.form.avatar = imgUrl;
},
error: (err)=>{
this.isUploading = false;
this.uploadPercent = "";
this.$message.error(err.message);
}
});
},
// 删除标签
handleKeyWordDel(tag, type) {
if(type == "dynamicTagsInput"){
this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1);
}
else if(type == "keyWordTagsInput"){
this.keyWordTags.splice(this.keyWordTags.indexOf(tag), 1);
}
},
// 显示子标题输入框
showTagInput(type) {
if(type == "dynamicTagsInput"){
this.showDynamicTagsInput = true;
this.$nextTick(() => {
this.$refs.dynamicTagsInput.$refs.input.focus();
});
}else if(type == "keyWordTagsInput"){
this.showkeyWordTagsInput = true;
this.$nextTick(() => {
this.$refs.keyWordTagsInput.$refs.input.focus();
});
}
},
// 标签确定
handleInputConfirm() {
let inputDynamicTagValue = this.inputDynamicTagValue;
let inputkeyWordTagValue = this.inputkeyWordTagValue;
if (inputDynamicTagValue) {
this.dynamicTags.push(inputDynamicTagValue);
}
if (inputkeyWordTagValue) {
this.keyWordTags.push(inputkeyWordTagValue);
}
this.showkeyWordTagsInput = false;
this.showDynamicTagsInput = false;
this.inputDynamicTagValue = "";
this.inputkeyWordTagValue = "";
},
// 保存
save(){
// 验证字段 !! 算了前端不验证了
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
this.form.artificial = this.dynamicTags.join("|");
this.form.keyword = this.keyWordTags.join("|");
this.form.switch = this.robotSwitch ? 1 : 0
axios
.post("/robot", this.form)
.then(response => {
try {
console.log(response);
loading.close();
this.$message.success("添加成功");
this.closeModal();
this.resize();
this.complete();
} catch (e) {
console.log(e);
}
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message);
});
},
// 重置
resize(){
this.dynamicTags = []
this.inputVisible = false
this.inputValue = ""
this.form = {
nickname: "",
avatar: "",
welcome: "",
timeout_text: "",
no_services: "",
loog_time_wait_text: "",
understand: "",
artificial: "",
platform: 1,
switch: 1
}
}
}
};
</script>
<style scoped lang="stylus">
.mini-im-file-button {
width: 50px;
height: 50px;
border-radius: 50%;
position: relative;
overflow: hidden;
input {
font-size: 100px;
position: absolute;
top: 0px;
left: 0px;
cursor: pointer;
opacity 0
}
cursor: pointer;
.mini-im-file-percent {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 12px;
}
}
.el-tag + .el-tag {
margin-left: 10px;
}
.button-new-tag {
margin-left: 10px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
}
.input-new-tag {
width: 150px;
margin-left: 10px;
vertical-align: bottom;
}
</style>
<template>
<el-dialog
width="600px"
title="编辑机器人"
:show-close="false"
:visible.sync="dialogFormVisible"
:close-on-click-modal="false"
>
<el-form :model="form">
<el-form-item label="头像" :label-width="formLabelWidth">
<el-row :gutter="10">
<el-col :span="3">
<div class="mini-im-file-button" title="点击上传图片">
<el-avatar
:size="50"
:src="form.avatar"
></el-avatar>
<input onClick="this.value = null" @change="changeFile" type="file" accept="image/*" />
<div v-show="isUploading" class="mini-im-file-percent">
<span>{{uploadPercent}}</span>
</div>
</div>
</el-col>
<el-col :span="6"></el-col>
</el-row>
</el-form-item>
<el-form-item label="机器人昵称" :label-width="formLabelWidth">
<el-input v-model="form.nickname" placeholder="请输入机器人昵称" autocomplete="off"></el-input>
</el-form-item>
<el-form-item type="textarea" :rows="2" label="机器人欢迎语" :label-width="formLabelWidth">
<el-input v-model="form.welcome" placeholder="请输入机器人欢迎语" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="无匹配知识库语" :label-width="formLabelWidth">
<el-input type="textarea" :rows="2" v-model="form.understand" placeholder="请输入无法识别回复语" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="超时结束提示" :label-width="formLabelWidth">
<el-input type="textarea" :rows="2" v-model="form.timeout_text" placeholder="请输入会话超时结束提示" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="无人工在线提示" :label-width="formLabelWidth">
<el-input type="textarea" :rows="2" v-model="form.no_services" placeholder="请输入无人工在线提示" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="长时间等待提示" :label-width="formLabelWidth">
<el-input type="textarea" :rows="2" v-model="form.loog_time_wait_text" placeholder="请输入长时间等待提示语" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="检索知识库热词" :label-width="formLabelWidth">
<el-tag
:key="tag"
v-for="tag in keyWordTags"
closable
:disable-transitions="false"
@close="handleKeyWordDel(tag, 'keyWordTagsInput')"
>{{tag}}</el-tag>
<el-input
class="input-new-tag"
v-if="showkeyWordTagsInput"
v-model="inputkeyWordTagValue"
ref="keyWordTagsInput"
size="small"
@keyup.enter.native="handleInputConfirm"
@blur="handleInputConfirm"
></el-input>
<el-button v-else class="button-new-tag" size="small" @click="showTagInput('keyWordTagsInput')">+ 新增</el-button>
<div style="font-size:12px;">* 该词库会在用户输入的时候去匹配检索提示</div>
</el-form-item>
<el-form-item label="转人工关键词" :label-width="formLabelWidth">
<el-tag
:key="tag"
v-for="tag in dynamicTags"
closable
:disable-transitions="false"
@close="handleKeyWordDel(tag, 'dynamicTagsInput')"
>{{tag}}</el-tag>
<el-input
class="input-new-tag"
v-if="showDynamicTagsInput"
v-model="inputDynamicTagValue"
ref="dynamicTagsInput"
size="small"
@keyup.enter.native="handleInputConfirm"
@blur="handleInputConfirm"
></el-input>
<el-button v-else class="button-new-tag" size="small" @click="showTagInput('dynamicTagsInput')">+ 新增</el-button>
<div style="font-size:12px;">* 匹配该关键词进入人工,系统已内置: "人工"</div>
</el-form-item>
<el-form-item v-show="formData.system != 1" label="运行状态" :label-width="formLabelWidth">
<el-switch v-model="robotSwitch" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</el-form-item>
<el-form-item v-show="formData.system != 1" label="匹配平台" :label-width="formLabelWidth">
<el-select v-model="form.platform" placeholder="请选择匹配平台">
<el-option
:label="item.title"
:value="item.id"
:key="index"
v-for="(item, index) in $store.getters.platformConfig"
></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from 'axios'
import upload from '../../common/upload'
export default {
name: "mini-im-edit-robot",
data() {
return {
dynamicTags: [],
keyWordTags: [],
showkeyWordTagsInput: false,
inputkeyWordTagValue: "",
showDynamicTagsInput: false,
inputDynamicTagValue: "",
form: {
nickname: "",
avatar: "",
welcome: "",
understand: "",
artificial: "",
keyword: "",
timeout_text: "",
no_services: "",
loog_time_wait_text: "",
platform: 1,
switch: 1
},
robotSwitch: true,
formLabelWidth: "120px",
isUploading: false,
uploadPercent: ""
};
},
props: {
dialogFormVisible: Boolean,
complete: Function,
formData: Object
},
methods: {
// 关闭
closeModal() {
this.$emit("update:dialogFormVisible", false);
},
// 上传
changeFile(file) {
var fileData = file.target.files[0];
upload({
file: fileData,
progress: (percent) => {
this.isUploading = true;
this.uploadPercent = percent + "%";
},
success: (url) => {
this.isUploading = false;
this.uploadPercent = "";
this.$message.success("上传成功");
var imgUrl = this.$store.getters.uploadToken.host + "/" + url
this.form.avatar = imgUrl;
},
error: (err)=>{
this.isUploading = false;
this.uploadPercent = "";
this.$message.error(err.message);
}
});
},
// 删除标签
handleKeyWordDel(tag, type) {
if(type == "dynamicTagsInput"){
this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1);
}
else if(type == "keyWordTagsInput"){
this.keyWordTags.splice(this.keyWordTags.indexOf(tag), 1);
}
},
// 显示子标题输入框
showTagInput(type) {
if(type == "dynamicTagsInput"){
this.showDynamicTagsInput = true;
this.$nextTick(() => {
this.$refs.dynamicTagsInput.$refs.input.focus();
});
}else if(type == "keyWordTagsInput"){
this.showkeyWordTagsInput = true;
this.$nextTick(() => {
this.$refs.keyWordTagsInput.$refs.input.focus();
});
}
},
// 标签确定
handleInputConfirm() {
let inputDynamicTagValue = this.inputDynamicTagValue;
let inputkeyWordTagValue = this.inputkeyWordTagValue;
if (inputDynamicTagValue) {
this.dynamicTags.push(inputDynamicTagValue);
}
if (inputkeyWordTagValue) {
this.keyWordTags.push(inputkeyWordTagValue);
}
this.showkeyWordTagsInput = false;
this.showDynamicTagsInput = false;
this.inputDynamicTagValue = "";
this.inputkeyWordTagValue = "";
},
// 保存
save(){
// 验证字段 !! 算了前端不验证了
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
this.form.artificial = this.dynamicTags.join("|");
this.form.keyword = this.keyWordTags.join("|");
this.form.switch = this.robotSwitch ? 1 : 0
axios
.put("/robot", this.form)
.then(response => {
try {
console.log(response);
loading.close();
this.$message.success("修改成功");
this.closeModal();
this.complete();
} catch (e) {
console.log(e);
}
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message);
});
},
},
watch:{
formData(){
this.platformConfig = this.$store.getters.platformConfig
this.form = Object.assign({},this.form, this.formData)
if(this.formData.artificial != "") this.dynamicTags = this.formData.artificial.split("|")
if(this.formData.keyword != "") this.keyWordTags = this.formData.keyword.split("|")
this.robotSwitch = this.form.switch == 1 ? true : false
}
}
};
</script>
<style scoped lang="stylus">
.mini-im-file-button {
width: 50px;
height: 50px;
border-radius: 50%;
position: relative;
overflow: hidden;
input {
font-size: 100px;
position: absolute;
top: 0px;
left: 0px;
cursor: pointer;
opacity 0
}
cursor: pointer;
.mini-im-file-percent {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 12px;
}
}
.el-tag + .el-tag {
margin-left: 10px;
}
.button-new-tag {
margin-left: 10px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
}
.input-new-tag {
width: 150px;
margin-left: 10px;
vertical-align: bottom;
}
</style>
<template>
<div>
<div class="mini-im-head">
<span>
<i class="el-icon-picture-outline-round"></i>
<span slot="title">机器人管理</span>
</span>
<el-button v-if="adminInfo.root == 1" @click="createDialogFormVisible = true" size="mini">添 加</el-button>
</div>
<el-divider />
<el-table
:data="tableData"
style="width: 100%"
v-loading="loading"
>
<el-table-column
type="index"
width="60">
</el-table-column>
<el-table-column
prop="avatar"
label="头像"
width="80">
<template slot-scope="scope">
<el-avatar :size="40" :src="scope.row.avatar"></el-avatar>
</template>
</el-table-column>
<el-table-column
prop="nickname"
label="机器人昵称">
</el-table-column>
<el-table-column
prop="welcome"
label="欢迎语">
</el-table-column>
<el-table-column
prop="understand"
label="无匹配知识库语">
</el-table-column>
<el-table-column
prop="timeout_text"
label="超时结束提示">
</el-table-column>
<el-table-column
prop="no_services"
label="无人工在线提示">
</el-table-column>
<el-table-column
prop="loog_time_wait_text"
label="长时间等待提示">
</el-table-column>
<el-table-column
prop="keyword"
label="检索知识库热词">
<template slot-scope="scope">
<span>{{scope.row.keyword.replace(/\|/g, " , ")}}</span>
</template>
</el-table-column>
<el-table-column
prop="artificial"
label="转人工关键词">
<template slot-scope="scope">
<span>{{scope.row.artificial.replace(/\|/g, " , ")}}</span>
</template>
</el-table-column>
<el-table-column
prop="switch"
align="center"
label="运行状态">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.switch == 1">服务中</el-tag>
<el-tag type="danger" v-if="scope.row.switch == 0">服务暂停</el-tag>
</template>
</el-table-column>
<el-table-column
align="center"
prop="platform"
label="服务平台">
<template slot-scope="scope">
<el-tag>{{$getPlatformItem(scope.row.platform).title}}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="create_at"
label="创建时间">
<template slot-scope="scope">
{{$formatUnixDate(scope.row.create_at, "YYYY/MM/DD")}}
</template>
</el-table-column>
<el-table-column
v-if="adminInfo.root == 1"
prop="operating"
align="center"
label="操作"
width="150"
>
<template slot-scope="scope">
<el-button
size="mini"
@click="edit(scope.row)">编 辑</el-button>
<el-button
size="mini"
type="danger"
@click="deleteRobot(scope.row)">删 除</el-button>
</template>
</el-table-column>
</el-table>
<el-row type="flex" style="margin-top: 20px;" justify="space-between">
<span style="color:#666;font-size: 14px;">当前有{{tableData.length}}个机器人</span>
</el-row>
<CreateDialog :complete="getRobotList" :dialogFormVisible.sync="createDialogFormVisible" />
<EditDialog :complete="getRobotList" :formData="editItem" :dialogFormVisible.sync="editDialogFormVisible" />
</div>
</template>
<script>
import CreateDialog from "./create"
import EditDialog from "./edit"
import axios from 'axios'
export default {
name: "robot",
components: {
CreateDialog,
EditDialog
},
data() {
return {
createDialogFormVisible: false,
editDialogFormVisible: false,
loading: true,
editItem: {}
}
},
created(){
setTimeout( ()=> this.getRobotList(), 500)
},
computed: {
tableData(){
return this.$store.getters.robots || []
},
adminInfo(){
return this.$store.getters.adminInfo || {}
}
},
methods: {
// 删除
deleteRobot(item){
this.$confirm('您确定要删除该机器人吗? 删除后不可恢复!', '温馨提示!', {
confirmButtonText: '确定',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() => {
axios.delete('/robot/' + item.id)
.then(response => {
console.log(response.data)
this.$message.success("删除成功")
this.getRobotList()
})
.catch(error => {
this.$message.error(error.response.data.message)
});
})
},
// 编辑
edit(item){
this.editItem = item
this.editDialogFormVisible = true
},
// 获取数据
getRobotList(){
axios.get('/robot/list')
.then(response => {
this.loading = false
this.$store.commit('onChangeRobos', response.data.data)
})
.catch(error => {
this.loading = false
this.$message.error(error.response.data.message)
});
},
}
};
</script>
<style lang="stylus" scoped>
.mini-im-head{
height 30px
display flex
align-items center
font-size 20px
justify-content space-between
color #666
i{
margin-right 5px
}
}
</style>
<template>
<el-dialog title="添加平台" :show-close="false" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
<el-form :model="form">
<el-form-item label="平台名称" :label-width="formLabelWidth">
<el-input v-model="form.title" placeholder="请输入平台名称" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="平台别名" :label-width="formLabelWidth">
<el-input v-model="form.alias" placeholder="请输入平台别名" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from "axios";
export default {
name: 'mini-im-create-admin',
data(){
return {
form: {
title: '',
alias: '',
},
formLabelWidth: "80px"
}
},
props:{
dialogFormVisible: Boolean,
complete: Function
},
methods: {
// 关闭
closeModal(){
this.$emit('update:dialogFormVisible', false);
},
// 保存
save() {
// 验证字段 !! 算了其它前端不验证了
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
axios
.post("/platform", this.form)
.then(response => {
console.log(response);
loading.close();
this.$message.success("添加成功");
this.closeModal();
this.resize();
this.$store.dispatch('ON_GET_PLATFORM_CONFIG')
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message);
});
},
resize() {
this.form = {
title: '',
alias: '',
};
}
}
}
</script>
<style scoped lang="stylus">
</style>
<template>
<el-dialog title="修改平台" :show-close="false" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
<el-form :model="form">
<el-form-item label="平台名称" :label-width="formLabelWidth">
<el-input v-model="form.title" placeholder="请输入平台名称" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="平台别名" :label-width="formLabelWidth">
<el-input v-model="form.alias" placeholder="请输入平台别名" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from "axios";
export default {
name: 'me-create-admin',
data(){
return {
form: {
title: '',
alias: '',
},
formLabelWidth: "80px"
}
},
props:{
dialogFormVisible: Boolean,
complete: Function,
formData: Object
},
methods: {
// 关闭
closeModal(){
this.$emit('update:dialogFormVisible', false);
},
// 保存
save() {
// 验证字段 !! 算了其它前端不验证了
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
axios
.put("/platform", this.form)
.then(response => {
console.log(response);
loading.close();
this.$message.success("添加成功");
this.closeModal();
this.$store.dispatch('ON_GET_PLATFORM_CONFIG')
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message);
});
}
},
watch:{
formData(){
this.form = Object.assign({},this.form, this.formData)
}
}
}
</script>
<style scoped lang="stylus">
</style>
<template>
<div>
<div class="mini-im-head">
<span>
<i class="el-icon-setting"></i>
<span slot="title">系统设置</span>
</span>
</div>
<el-tabs v-model="activeName">
<el-tab-pane label="基本设置" name="first">
<el-form style="width:500px" ref="form" label-width="100px">
<el-form-item label="系统LOGO" label-width="120px">
<el-row :gutter="10">
<el-col :span="3">
<div class="mini-im-file-button" title="点击上传图片">
<img :src="systemInfo.logo" alt="点击上传图片">
<input
:disabled="!isRoot"
onClick="this.value = null"
@change="systemLogoUpload"
type="file"
accept="image/*"
/>
<div v-show="isUploadingSysLogo" class="mini-im-file-percent">
<span>{{uploadysLogoPercent}}</span>
</div>
</div>
</el-col>
<el-col :span="6"></el-col>
</el-row>
</el-form-item>
<el-form-item label="系统名称">
<el-input :readonly="!isRoot" v-model="systemInfo.title" placeholder="请输入系统名称"></el-input>
</el-form-item>
<el-form-item label="版权信息">
<el-input :readonly="!isRoot" v-model="systemInfo.copy_right" placeholder="请输入版权信息"></el-input>
</el-form-item>
<el-divider content-position="left">选择资源存储空间服务商(上传的,图片,文件)</el-divider>
<el-form-item label="上传选项">
<el-select v-model="systemInfo.upload_mode">
<el-option :label="item.name" :value="item.id" :key="item.id" v-for="item in $store.getters.uploadsConfigs"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button v-if="isRoot" @click="saveSystem" size="mini">保存设置</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="公司信息" name="second">
<el-divider content-position="left">该配置信息会展示在各个前台给客户</el-divider>
<el-form style="width:500px" ref="form" label-width="100px">
<el-form-item label="公司LOGO" label-width="120px">
<el-row :gutter="10">
<el-col :span="3">
<div class="mini-im-file-button" title="点击上传图片">
<img :src="companyInfo.logo" alt="点击上传图片">
<input
:disabled="!isRoot"
onClick="this.value = null"
@change="companyLogoUpload"
type="file"
accept="image/*"
/>
<div v-show="isUploadingCompany" class="mini-im-file-percent">
<span>{{uploadCompanyPercent}}</span>
</div>
</div>
</el-col>
<el-col :span="6"></el-col>
</el-row>
</el-form-item>
<el-form-item label="公司名称">
<el-input :readonly="!isRoot" v-model="companyInfo.title" placeholder="请输入公司名称"></el-input>
</el-form-item>
<el-form-item label="服务时间">
<el-input :readonly="!isRoot" v-model="companyInfo.service" placeholder="请输入在线客服服务时间"></el-input>
</el-form-item>
<el-form-item label="公司邮箱">
<el-input :readonly="!isRoot" v-model="companyInfo.email" placeholder="请输入公司邮箱"></el-input>
</el-form-item>
<el-form-item label="公司电话">
<el-input :readonly="!isRoot" v-model="companyInfo.tel" placeholder="请输入公司电话"></el-input>
</el-form-item>
<el-form-item label="公司地址">
<el-input :readonly="!isRoot" type="textarea" rows="5" v-model="companyInfo.address" placeholder="请输入公司地址"></el-input>
</el-form-item>
<el-form-item>
<el-button v-if="isRoot" @click="saveCompany" size="mini">保存设置</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane v-if="isRoot" label="七牛云存储配置" name="three">
<el-form style="width:500px" ref="form" label-width="100px">
<el-divider content-position="left">请不要随意修改该选项,可能会导致客户端上传不了文件或图片</el-divider>
<el-form-item label="Bucket">
<el-input v-model="qiniuSecret.bucket" placeholder="请输入bucket"></el-input>
</el-form-item>
<el-form-item label="accessKey">
<el-input v-model="qiniuSecret.access_key" placeholder="请输入accessKey" show-password></el-input>
</el-form-item>
<el-form-item label="secretKey">
<el-input v-model="qiniuSecret.secret_key" placeholder="请输入secretKey" show-password></el-input>
</el-form-item>
<el-form-item label="Host">
<el-input v-model="qiniuSecret.host" placeholder="请输入host"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="saveQiniu" size="mini">保存设置</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="客户端平台" name="fives">
<el-divider content-position="left">通过该配置,对接的平台,机器人,知识库匹配等 (系统默认项不可修改)</el-divider>
<el-table :data="$store.getters.platformConfig" style="width: 100%">
<el-table-column prop="id" label="#ID" width="80"></el-table-column>
<el-table-column prop="title" label="名称" align="center">
<template slot-scope="scope">
<el-tag type="danger" v-if="scope.row.system == 1">{{scope.row.title}}</el-tag>
<el-tag v-if="scope.row.system == 0">{{scope.row.title}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="alias" label="别名" align="center"></el-table-column>
<el-table-column v-if="isRoot" label="操作" align="center">
<template slot-scope="scope">
<template v-if="scope.row.system == 0">
<el-button @click="editPlatform(scope.row)" size="mini">编 辑</el-button>
<el-button @click="deletePlatform(scope.row)" size="mini" type="danger">删 除</el-button>
</template>
<span v-if="scope.row.system == 1" style="font-size: 12px;color: #999;">系统内置,不可操作</span>
</template>
</el-table-column>
<el-table-column></el-table-column>
</el-table>
<el-button v-if="isRoot" style="margin-top:20px;" @click="createDialogFormVisible = true" size="mini">添加新平台</el-button>
</el-tab-pane>
</el-tabs>
<CreatePlatformDialog :dialogFormVisible.sync="createDialogFormVisible" />
<EditPlatformDialog :formData="editPlatformItem" :dialogFormVisible.sync="editDialogFormVisible" />
</div>
</template>
<script>
import axios from 'axios'
import upload from '../../common/upload'
import CreatePlatformDialog from "./create_platform"
import EditPlatformDialog from "./edit_platform"
export default {
name: "system",
components: {
CreatePlatformDialog,
EditPlatformDialog,
},
data() {
return {
activeName: "first",
systemInfo: {},
companyInfo: {},
isUploadingSysLogo: false,
uploadysLogoPercent: "",
isUploadingCompany: false,
uploadCompanyPercent: "",
qiniuSecret: {},
createDialogFormVisible: false,
editDialogFormVisible: false,
editPlatformItem: {}
}
},
computed:{
isRoot(){
if(this.$store.getters.adminInfo){
return this.$store.getters.adminInfo.root == 1
}else{
return false
}
}
},
updated: function () {
this.$nextTick(function () {
this.systemInfo = this.$store.getters.systemInfo
this.companyInfo = this.$store.getters.companyInfo
})
},
mounted(){
this.systemInfo = this.$store.getters.systemInfo
this.companyInfo = this.$store.getters.companyInfo
if(this.isRoot) this.getQiniu()
},
methods: {
onSubmit() {
this.$confirm("您确定要保存修改后的配置吗?", "温馨提示!", {
confirmButtonText: "保存",
cancelButtonText: "取消",
center: true,
type: "warning"
});
},
// 系统logo上传
systemLogoUpload(file) {
var fileData = file.target.files[0];
upload({
file: fileData,
progress: (percent) => {
this.isUploadingSysLogo = true;
this.uploadysLogoPercent = percent + "%";
},
success: (url) => {
this.isUploadingSysLogo = false;
this.uploadysLogoPercent = "";
this.$message.success("上传成功");
var imgUrl = this.$store.getters.uploadToken.host + "/" + url;
this.systemInfo.logo = imgUrl;
},
error: (err)=>{
this.isUploadingSysLogo = false;
this.uploadysLogoPercent = "";
this.$message.error(err.message);
}
});
},
// 保存系统配置
saveSystem(){
this.$confirm("您确定要保存修改后的系统配置吗?", "温馨提示!", {
confirmButtonText: "保存",
cancelButtonText: "取消",
center: true,
type: "warning"
}).then(()=>{
axios
.put("/system", this.systemInfo)
.then(response => {
this.$store.commit('onChangeSystemInfo', response.data.data)
this.$message.success("保存成功");
this.$store.dispatch('ON_GET_SYSTEM')
this.$store.dispatch('ON_GET_UPLOADS_CONFIG')
})
.catch(error => {
this.$message.error(error.response.data.message);
});
})
},
// 公司logo上传
companyLogoUpload(file) {
var fileData = file.target.files[0];
upload({
file: fileData,
progress: (percent) => {
this.isUploadingCompany = true;
this.uploadCompanyPercent = percent + "%";
},
success: (url) => {
this.isUploadingCompany = false;
this.uploadCompanyPercent = "";
this.$message.success("上传成功");
var imgUrl = this.$store.getters.uploadToken.host + "/" + url;
this.companyInfo.logo = imgUrl;
},
error: (err)=>{
this.isUploadingCompany = false;
this.uploadCompanyPercent = "";
this.$message.error(err.message);
}
});
},
// 保存公司配置
saveCompany(){
this.$confirm("您确定要保存修改后的公司信息吗?", "温馨提示!", {
confirmButtonText: "保存",
cancelButtonText: "取消",
center: true,
type: "warning"
}).then(()=>{
axios
.put("/company", this.companyInfo)
.then(response => {
this.$store.commit('onChangeCompanyInfo', response.data.data)
this.$message.success("保存成功");
})
.catch(error => {
this.$message.error(error.response.data.message);
});
})
},
// 获取七牛配置
getQiniu(){
axios.get('/qiniu')
.then(response => {
this.qiniuSecret = response.data.data
})
.catch(error => {
this.$message.error(error.response.data.message)
});
},
// 保存七牛配置
saveQiniu(){
this.$confirm("您确定要保存修改后的七牛配置信息吗?如配置信息错误会导致客户端无法上传图片文件", "警告!", {
confirmButtonText: "保存",
cancelButtonText: "取消",
center: true,
type: "warning"
}).then(()=>{
axios
.put("/qiniu", this.qiniuSecret)
.then(response => {
console.log(response.data.data)
this.$message.success("保存成功");
})
.catch(error => {
this.$message.error(error.response.data.message);
});
})
},
// 删除平台
deletePlatform(item){
console.log(item)
this.$confirm('您确定要删除该平台配置吗? 删除后不可恢复!', '温馨提示!', {
confirmButtonText: '确定',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() => {
axios.delete('/platform/' + item.id)
.then(response => {
console.log(response.data)
this.$message.success("删除成功")
this.$store.dispatch('ON_GET_PLATFORM_CONFIG')
})
.catch(error => {
this.$message.error(error.response.data.message)
});
})
},
// 编辑平台
editPlatform(item){
this.editPlatformItem = item
this.editDialogFormVisible = true
},
}
};
</script>
<style lang="stylus" scoped>
.mini-im-head {
height: 60px;
display: flex;
align-items: center;
font-size: 20px;
justify-content: space-between;
color: #666;
i {
margin-right: 5px;
}
}
.mini-im-file-button {
width: 180px;
position: relative;
overflow: hidden;
// background-color #f3f3f3
border-radius 3px
padding 5px
box-shadow 1px 1px 7px 0px #ccc
input {
width: 180px;
font-size: 100px;
position: absolute;
top: 0px;
left: 0px;
opacity 0
cursor: pointer;
opacity 0
}
img{
width 100%
display block
}
cursor: pointer;
.mini-im-file-percent {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 12px;
}
}
</style>
<template>
<div></div>
</template>
<script>
export default {
name: 'mini-im-template',
data(){
return {
}
},
mounted(){
},
methods: {
}
}
</script>
<style scoped lang="stylus">
</style>
<template>
<el-dialog
title="编辑用户"
:show-close="false"
:visible.sync="dialogFormVisible"
:close-on-click-modal="false"
>
<el-form :model="form">
<el-form-item label="头像" :label-width="formLabelWidth">
<el-row :gutter="10">
<el-col :span="3">
<div class="mini-im-file-button" title="点击上传图片">
<el-avatar
:size="50"
:src="form.avatar || $store.state.avatar"
></el-avatar>
<input onClick="this.value = null" @change="changeFile" type="file" accept="image/*" />
<div v-show="isUploading" class="mini-im-file-percent">
<span>{{uploadPercent}}</span>
</div>
</div>
</el-col>
<el-col :span="6"></el-col>
</el-row>
</el-form-item>
<el-form-item label="用户昵称" :label-width="formLabelWidth">
<el-input v-model="form.nickname" placeholder="请输入用户昵称" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="所在地区" :label-width="formLabelWidth">
<el-input v-model="form.address" placeholder="请输入所在地区" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="联系方式" :label-width="formLabelWidth">
<el-input v-model="form.phone" placeholder="请输入联系方式" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="备注" :label-width="formLabelWidth">
<el-input v-model="form.remarks" type="textarea" placeholder="请输入备注" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from 'axios'
import upload from '../../common/upload'
export default {
name: "mini-im-edit-user",
data() {
return {
form: {
id: "",
avatar: "",
phone: "",
address: "",
nickname: "",
remarks: ""
},
formLabelWidth: "120px",
isUploading: false,
uploadPercent: ""
};
},
props: {
dialogFormVisible: Boolean,
complete: Function,
formData: Object
},
methods: {
// 关闭
closeModal() {
this.$emit("update:dialogFormVisible", false);
},
// 上传
changeFile(file) {
var fileData = file.target.files[0];
upload({
file: fileData,
progress: (percent) => {
this.isUploading = true;
this.uploadPercent = percent + "%";
},
success: (url) => {
this.isUploading = false;
this.uploadPercent = "";
this.$message.success("上传成功");
var imgUrl = this.$store.getters.uploadToken.host + "/" + url
this.form.avatar = imgUrl;
},
error: (err)=>{
this.isUploading = false;
this.uploadPercent = "";
this.$message.error(err.message);
}
});
},
// 保存
save(){
// 验证字段 !! 算了前端不验证了
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
axios
.put("/user", this.form)
.then(response => {
try {
console.log(response);
loading.close();
this.$message.success("修改成功");
this.closeModal();
this.complete();
} catch (e) {
console.log(e);
}
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message);
});
},
},
watch:{
formData(){
this.form = Object.assign({},this.form, this.formData)
}
}
};
</script>
<style scoped lang="stylus">
.mini-im-file-button {
width: 50px;
height: 50px;
border-radius: 50%;
position: relative;
overflow: hidden;
input {
font-size: 100px;
position: absolute;
top: 0px;
left: 0px;
cursor: pointer;
opacity 0
}
cursor: pointer;
.mini-im-file-percent {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 12px;
}
}
</style>
<template>
<div>
<div class="mini-im-head">
<span>
<i class="el-icon-user"></i>
<span slot="title">用户管理</span>
</span>
</div>
<el-divider />
<div class="search">
<el-row :gutter="20">
<el-col style="width: 120px">
<el-form ref="form" label-width="120px">
<el-form-item label="按条件查找:"></el-form-item>
</el-form>
</el-col>
<el-col :span="3">
<el-select v-model="tableData.platform" placeholder="请选择平台">
<el-option
v-for="item in platformConfig"
:key="item.id"
:label="item.title"
:value="item.id"
></el-option>
</el-select>
</el-col>
<el-col :span="4">
<el-input placeholder="请输入关键词" prefix-icon="el-icon-search" v-model="tableData.keyword" clearable></el-input>
</el-col>
<el-col :span="6.5">
<el-date-picker
v-model="selectDate"
type="daterange"
align="right"
unlink-panels
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-col>
<el-col :span="3">
<el-button @click="search">查 找</el-button>
</el-col>
</el-row>
</div>
<el-table :data="tableData.list" v-loading="loading" style="width: 100%">
<el-table-column
type="index"
:index="indexMethod"
width="60">
</el-table-column>
<el-table-column prop="avatar" label="头像" width="80">
<template slot-scope="scope">
<el-avatar :size="40" :src="scope.row.avatar || $store.state.avatar"></el-avatar>
</template>
</el-table-column>
<el-table-column prop="nickname" label="用户昵称">
<template slot-scope="scope">
<span v-if="scope.row.nickname != ''">{{scope.row.nickname}}</span>
<span v-else>------</span>
</template>
</el-table-column>
<el-table-column prop="uid" label="业务平台ID">
<template slot-scope="scope">
<span v-if="scope.row.uid != ''">{{scope.row.uid}}</span>
<span v-else>------</span>
</template>
</el-table-column>
<el-table-column prop="address" label="所在地区">
<template slot-scope="scope">
<span v-if="scope.row.address != ''">{{scope.row.address}}</span>
<span v-else>------</span>
</template>
</el-table-column>
<el-table-column prop="phone" label="联系方式">
<template slot-scope="scope">
<span v-if="scope.row.phone != ''">{{scope.row.phone}}</span>
<span v-else>------</span>
</template>
</el-table-column>
<el-table-column prop="online" align="center" label="在线状态">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.online == 1">在线</el-tag>
<el-tag type="info" v-if="scope.row.online == 0">离线</el-tag>
</template>
</el-table-column>
<el-table-column prop="platform" align="center" label="服务平台">
<template slot-scope="scope">
<el-tag>{{$getPlatformItem(scope.row.platform).title}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="remarks" label="备注">
<template slot-scope="scope">
{{scope.row.remarks || '------'}}
</template>
</el-table-column>
<el-table-column prop="create_at" label="注册时间">
<template slot-scope="scope">
{{$formatUnixDate(scope.row.create_at, "YYYY/MM/DD")}}
</template>
</el-table-column>
<el-table-column prop="operating" align="center" label="操作" width="150">
<template slot-scope="scope">
<el-button size="mini" @click="edit(scope.row)">编 辑</el-button>
<el-button v-if="$store.getters.adminInfo.root == 1" size="mini" type="danger" @click="deleteUser(scope.row)">删 除</el-button>
</template>
</el-table-column>
</el-table>
<el-row type="flex" style="margin-top: 20px;" justify="space-between">
<span style="color:#666;font-size: 14px;">共找到{{tableData.total}}条数据</span>
<el-pagination
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
layout="sizes, prev, pager, next"
:current-page="tableData.page_on"
:page-sizes="[5, 10, 15, 20]"
:total="tableData.total">
</el-pagination>
</el-row>
<EditDialog :complete="getUsers" :formData="editItem" :dialogFormVisible.sync="editDialogFormVisible" />
</div>
</template>
<script>
import EditDialog from "./edit";
import axios from 'axios'
var moment = require('moment');
export default {
name: "robot",
components: {
EditDialog
},
data() {
return {
loading: true,
selectDate: [],
tableData: {
list: [],
page_on: 1,
page_size: 10,
keyword: "",
total: 0,
platform: 1,
date_start: "",
date_end: "",
},
editItem: {},
editDialogFormVisible: false,
};
},
computed: {
platformConfig(){
return this.$store.getters.platformConfig
}
},
created() {
setTimeout(()=> this.getUsers(1), 500)
},
methods: {
// 行号
indexMethod(index) {
return (this.tableData.page_on - 1) * this.tableData.page_size + index +1;
},
// 编辑
edit(item){
this.editItem = item
this.editDialogFormVisible = true
},
// 删除
deleteUser(item) {
this.$confirm('您确定要删除该用户吗? 删除后不可恢复!', '温馨提示!', {
confirmButtonText: '确定',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() => {
axios.delete('/user/' + item.id)
.then(response => {
console.log(response.data)
this.$message.success("删除成功")
this.getUsers(1)
})
.catch(error => {
this.$message.error(error.response.data.message)
});
})
},
// 改变每页条数
handleSizeChange(val) {
this.tableData.page_size = val
this.getUsers()
},
// 分页
handleCurrentChange(val) {
this.tableData.page_on = val
this.getUsers()
},
// 查询
search(){
if(this.selectDate.length == 2){
this.tableData.date_start = moment(this.selectDate[0]).format("YYYY-MM-DD")
this.tableData.date_end = moment(this.selectDate[1]).format("YYYY-MM-DD")
}else{
this.tableData.date_start = ""
this.tableData.date_end = ""
}
this.getUsers(1)
},
// 获取数据
getUsers(index){
if(index) this.tableData.page_on = index
const {page_on, page_size, keyword, date_start, date_end, platform} = this.tableData
axios.post('/user/list', {page_on, page_size, keyword, date_start, date_end, platform})
.then(response => {
this.loading = false
this.tableData = response.data.data
})
.catch(error => {
this.loading = false
this.$message.error(error.response.data.message)
});
},
}
};
</script>
<style lang="stylus" scoped>
.mini-im-head {
height: 30px;
display: flex;
align-items: center;
font-size: 20px;
justify-content: space-between;
color: #666;
i {
margin-right: 5px;
}
}
</style>
<template>
<div class="mini-im-chat-list">
<div class="mini-im-chat-message-box">
<div class="loading" v-show="loading">
<i class="el-icon-loading"></i><span>消息加载中...</span>
</div>
<el-button v-show="isMessageEnd" type="text" disabled icon="el-icon-refresh-right">无更多聊天记录...</el-button>
<el-button v-if="!isMessageEnd && !loading" type="text" @click="onLoadMor" icon="el-icon-refresh">点击加载更多聊天记录</el-button>
</div>
<div class="mini-im-chat-message-box">
<div class="loading" v-show="messages.length <= 0 && !loading">
<i class="el-icon-time"></i><span>暂无聊天记录...</span>
</div>
</div>
<div class="mini-im-chat-message-box" :class="{'self': item.from_account != seviceCurrentUser.id}" v-for="(item, index) in messages" :key="index">
<!-- 用户信息 -->
<template v-if="item.biz_type == 'text' || item.biz_type == 'photo' || item.biz_type == 'knowledge' || item.biz_type == 'knowledge_list'">
<div class="user-date">
<span v-if="item.from_account == adminInfo.id">
{{adminInfo.nickname || adminInfo.username}}
</span>
<span v-else-if="item.from_account == seviceCurrentUser.from_account">
{{seviceCurrentUser.nickname}}
</span>
<span v-else>
<span style="font-size:12px;color: #666;">(机器人)</span>{{$robotNickname(item.from_account)}}
</span>
<em>{{$formatFromNowDate(item.timestamp)}}</em>
</div>
</template>
<!-- 文本消息 -->
<template v-if="item.biz_type == 'text'">
<div class="text">
<div @click="()=>onCancelMessage(item.key)" title="撤回本条消息" class="cancel-btn" v-if="item.from_account == adminInfo.id && item.isShowCancel">
撤回
</div>
<span v-html="item.payload.replace(/\n/ig, '<br />')"></span>
</div>
</template>
<!-- 图片 -->
<template v-if="item.biz_type == 'photo'">
<div class="photo">
<div class="loading" v-if="item.percent && item.percent != 100">
<i class="el-icon-loading"></i>
<span>{{item.percent}}%</span>
</div>
<div @click="()=>onCancelMessage(item.key)" title="撤回本条消息" class="cancel-btn" v-if="item.from_account == adminInfo.id && item.isShowCancel">
撤回
</div>
<div class="img-content">
<img :src="item.payload" preview="1" />
</div>
</div>
</template>
<!-- 转接 -->
<template v-if="item.biz_type == 'transfer'">
<div class="system">
<em>{{$formatFromNowDate(item.timestamp)}}</em>
<span>{{item.payload}}</span>
</div>
</template>
<!-- 结束聊天 -->
<template v-if="item.biz_type == 'end'">
<div class="system">
<em>{{$formatFromNowDate(item.timestamp)}}</em>
<span v-if="item.to_account != adminInfo.id">你结束了会话</span>
<span v-else>对方结束了会话</span>
<em>{{$formatFromNowDate(item.timestamp)}}</em>
</div>
</template>
<!-- 聊天超时 -->
<template v-if="item.biz_type == 'timeout'">
<div class="system">
<em>{{$formatFromNowDate(item.timestamp)}}</em>
<span>{{item.payload}}</span>
</div>
</template>
<!-- 撤回消息 -->
<template v-if="item.biz_type == 'cancel'">
<div class="system">
<em>{{$formatFromNowDate(item.timestamp)}}</em>
<span v-if="item.from_account == adminInfo.id">您撤回了一条消息</span>
<span v-else>对方撤回了一条消息</span>
</div>
</template>
<!-- 知识库列表 -->
<template v-if="item.biz_type == 'knowledge'">
<div class="knowledge">
<div class="content">
<div class="title">以下是否是您关心的相关问题呢?</div>
<div class="item" :key="index" v-for="(item, index) in JSON.parse(item.payload)">
{{index+1}}.{{item.title}}
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<script>
export default {
name: "mini-im-contact",
data() {
return {};
},
computed: {
seviceCurrentUser(){
return this.$store.getters.seviceCurrentUser || {}
},
adminInfo(){
return this.$store.getters.adminInfo || {}
}
},
props: {
loading: Boolean,
onCancelMessage: Function,
messages: Array,
onLoadMor: Function,
isMessageEnd: Boolean
},
watch:{
messages(){
setTimeout(()=>{
this.$previewRefresh()
}, 1000)
}
}
};
</script>
<style scoped lang="stylus">
.mini-im-chat-list {
display: flex;
flex-direction: column;
.mini-im-chat-message-box {
width: 100%;
display: flex;
flex-direction: column;
margin-bottom: 15px;
.user-date {
display: flex;
align-items: center;
color: #999;
font-size: 14px;
span {
color: #666;
font-weight: 500;
font-size: 14px;
padding: 0 5px;
}
em {
font-style: normal;
font-size 12px
}
}
.loading{
color #666
display: flex;
margin-top: 5px;
align-items center
align-content center
justify-content center
span{
margin-left 5px
font-size 13px
}
}
.text {
display: flex;
margin-top: 5px;
word-break:break-all;
span {
max-width: 40%;
display: inline;
padding: 5px 10px;
border-radius: 5px;
background-color: #eef4f9;
font-size: 14px;
color: #666;
}
}
.photo {
display: flex;
margin-top: 5px;
.loading{
align-self flex-end
padding 0 5px
span{
background none !important
color: #999 !important
}
}
.img-content{
border-radius: 5px;
width: 200px;
overflow hidden
}
img {
cursor: pointer;
width: 100%;
height 100%
display: inline;
}
}
.knowledge {
display: flex;
margin-top: 5px;
justify-content: flex-end;
.content {
display: flex;
flex-direction: column;
padding: 5px;
border-radius: 5px;
color: #666;
text-align: left;
background-color: #eef4f9;
.title {
font-size: 13px;
font-weight: 500;
}
.item {
font-size: 13px;
line-height: 22px;
}
}
}
.system {
display: flex;
margin-top: 5px;
flex-direction: column;
align-items: center;
justify-content: center;
em{
margin-top: 5px;
font-size: 12px;
color: #999;
}
span {
font-size: 12px;
max-width: 50%;
min-width: 100px;
display: inline;
padding: 3px 20px;
border-radius: 5px;
text-align: center;
background-color: #f2f2f2;
color: #999;
}
}
&.self {
text-align: right;
.user-date {
display: flex;
justify-content: flex-end;
span {
order: -2;
}
em {
order: -3;
}
}
.text, .photo {
justify-content: flex-end;
align-items flex-end
word-break:break-all;
.cancel-btn{
color #26a2ff
font-size 12px
margin-right 5px
cursor pointer
}
span {
background-color: rgba(33, 150, 243, 0.72);
color: #fff;
text-align left
}
}
.knowledge>.content {
background-color: rgba(33, 150, 243, 0.72);
color: #fff;
}
}
}
}
</style>
<template>
<div class="mini-im-chat-item">
<span @click.capture="deleteContact(item)" class="delete_contact" title="删除该记录"> <i class="el-icon-close"></i></span>
<el-avatar @click="clickItem(item)" class="mini-im-avatar">
<img v-if="item.avatar != ''" :src="item.avatar"/>
<template v-else>访</template>
</el-avatar>
<div @click="clickItem(item)" class="mini-im-message-box">
<div class="mini-im-user-date">
<div class="mini-im-nickname">
<span class="mini-im-online-status" :class="{'success': item.online == 1}"></span> {{item.nickname}}
</div>
<div class="mini-im-date">
{{$formatFromNowDate(item.contact_create_at)}}
</div>
</div>
<div class="mini-im-message-badge">
<div v-if="item.last_message_type == 'text'" class="mini-im-message">{{item.last_message}}</div>
<div v-if="item.last_message_type == 'photo'" class="mini-im-message">收到图片文件</div>
<div v-if="item.last_message_type == 'video'" class="mini-im-message">收到视频文件</div>
<div v-if="item.last_message_type == 'end'" class="mini-im-message">会话结束</div>
<div v-if="item.last_message_type == 'cancel'" class="mini-im-message">对方撤回了一条消息</div>
<div v-if="item.last_message_type == 'timeout'" class="mini-im-message">会话超时,结束对话</div>
<div v-if="item.last_message_type == 'transfer' || item.last_message_type == 'handshake'" class="mini-im-message">客服转接...</div>
<div v-if="item.read > 0" class="mini-im-badge">{{item.read}}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'mini-im-contact',
data(){
return {
}
},
props: {
item: Object,
clickItem: Function,
deleteContact: Function,
},
}
</script>
<style scoped lang="stylus">
.mini-im-chat-item{
padding 10px
cursor pointer
display flex
border-left 3px solid #fff
border-bottom 1px solid rgba(175, 175, 175, 0.11)
position relative
&:hover{
border-left 3px solid #ff5722
background-color #f3f3f3
.delete_contact{
display block
}
}
.mini-im-avatar{
align-self center
flex-shrink 0
}
&::last-child{
border-bottom 0
}
.delete_contact{
position absolute
left 0
top 0
color #999
display none
}
.mini-im-message-box{
width 180px
flex-grow 1
padding 8px 0
padding-left 10px
display flex
flex-direction column
justify-content space-around
box-sizing border-box
font-size 14px
.mini-im-nickname{
font-size 14px
color #666
font-weight 600
margin-bottom 5px
.mini-im-online-status{
font-size 12px
color #9e9e9e
&.success{
color #aadc97
}
}
}
.mini-im-user-date,.mini-im-message-badge{
display flex
justify-content space-between
}
.mini-im-badge{
width 20px
height 20px
border-radius 100%
background-color #f56c6c
text-align center
color #ffffff
line-height 20px
font-size 12px
flex-shrink 0
}
.mini-im-message{
font-size 13px
color #999
text-overflow: ellipsis
white-space: nowrap
overflow:hidden
padding-right 5px
}
.mini-im-date{
font-size 12px
color #999
}
}
}
.mini-im-chat-item-active{
border-left 3px solid #ff5722
background-color #f4f5f7
}
</style>
<template>
<el-dialog width="600px" title="添加快捷语" :show-close="false" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
<el-form :model="form">
<el-form-item label="标题" :label-width="formLabelWidth">
<el-input maxlength="50" show-word-limit v-model="form.title" type="text" placeholder="请输入标题" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="内容" :label-width="formLabelWidth">
<el-input rows="8" resize="none" :autosize="false" maxlength="200" show-word-limit v-model="form.content" type="textarea" placeholder="请输入快捷语" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from "axios";
export default {
name: 'mini-im-create-shortcuts',
data(){
return {
form: {
content: '',
title: ""
},
formLabelWidth: "40px"
}
},
props:{
dialogFormVisible: Boolean,
complete: Function
},
mounted(){
},
methods: {
// 关闭
closeModal(){
this.$emit('update:dialogFormVisible', false);
},
// 保存
save() {
if(this.form.title.trim() == "") return
if(this.form.content.trim() == "") return
// 验证字段 !! 算了其它前端不验证了
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
axios
.post("/shortcut", this.form)
.then(response => {
try {
console.log(response);
loading.close();
this.$message.success("添加成功");
this.closeModal();
this.complete();
this.form = {content: "", title: ""}
} catch (e) {
console.log(e);
}
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message);
});
},
}
}
</script>
<style scoped lang="stylus">
</style>
<template>
<el-dialog width="600px" title="编辑快捷语" :show-close="false" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
<el-form :model="form">
<el-form-item label="标题" :label-width="formLabelWidth">
<el-input maxlength="50" show-word-limit v-model="form.title" type="text" placeholder="请输入标题" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="内容" :label-width="formLabelWidth">
<el-input rows="8" resize="none" :autosize="false" maxlength="200" show-word-limit v-model="form.content" type="textarea" placeholder="请输入快捷语" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
import axios from "axios";
export default {
name: 'mini-im-edit-shortcuts',
data(){
return {
form: {
content: '',
title: ""
},
formLabelWidth: "40px"
}
},
props:{
dialogFormVisible: Boolean,
complete: Function,
formData: Object
},
methods: {
// 关闭
closeModal(){
this.$emit('update:dialogFormVisible', false);
},
// 保存
save() {
if(this.form.title.trim() == "") return
if(this.form.content.trim() == "") return
// 验证字段 !! 算了其它前端不验证了
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
axios
.put("/shortcut", this.form)
.then(response => {
try {
console.log(response);
loading.close();
this.$message.success("添加成功");
this.closeModal();
this.complete();
} catch (e) {
console.log(e);
}
})
.catch(error => {
loading.close();
this.$message.error(error.response.data.message);
});
},
},
watch:{
formData(){
this.form = Object.assign({},this.form, this.formData)
}
}
}
</script>
<style scoped lang="stylus">
</style>
<template>
<el-popover
placement="top-start"
width="400"
trigger="hover">
<div class="emoji-box">
<span @click="clickEmoji(item)" v-for="(item, index) in emojis" :key="index">{{item}}</span>
</div>
<button slot="reference" style="font-size: 20px" title="选择表情" class="mini-im-button">😊</button>
</el-popover>
</template>
<script>
var emojiService = require("../../resource/emoji")
export default {
name: 'mini-im-emoji',
data(){
return {
emojis: emojiService.emojiData
}
},
props: {
clickEmoji: Function
},
}
</script>
<style scoped lang="stylus">
.mini-im-button{
height 30px
padding 0 5px
border 0
background-color #fff
cursor pointer
color #666
font-size 12px
i{
color #666
}
}
.emoji-box{
font-size 20px
span{
cursor pointer
padding 2px
}
}
</style>
<template>
<div class="mini-im-workbench">
<div class="mini-im-session-list">
<div class="title">
<el-row type="flex" justify="space-between" :gutter="20">
<el-col :span="16">
<span>
<i class="el-icon-s-custom"></i>
会话列表 ( {{contacts.length}}人 )
</span>
<i style="cursor: pointer;" @click="clearContact" title="清空列表" class="el-icon-delete"></i>
</el-col>
<el-col :span="9">
<el-popover
placement="bottom"
width="100">
<div class="mini-im-online-setting">
<div v-if="adminInfo.online != 1" class="item" @click="()=>online(1)">
<i style="color: rgb(135, 208, 104)" class="el-icon-circle-check"></i>
我要上线
</div>
<div v-if="adminInfo.online != 0" class="item" @click="()=>online(0)">
<i style="color: #ccc" class="el-icon-switch-button"></i>
我要下线
</div>
<div v-if="adminInfo.online != 2" class="item" @click="()=>online(2)">
<i style="color: #e6a23c" class="el-icon-remove-outline"></i>
繁忙状态
</div>
</div>
<el-button v-if="adminInfo.online == 0" size="mini" slot="reference">
<i class="el-icon-switch-button"></i >
<em>离 线 </em>
<i class="el-icon-arrow-right el-icon--right"></i>
</el-button>
<el-button v-else-if="adminInfo.online == 1" size="mini" slot="reference">
<span style="color: rgb(135, 208, 104)">
<i class="el-icon-circle-check"></i>
<em>在 线</em>
<i class="el-icon-arrow-right el-icon--right"></i>
</span>
</el-button>
<el-button v-else size="mini" slot="reference">
<span style="color: #e6a23c">
<i class="el-icon-circle-check"></i>
<em>繁 忙</em>
<i class="el-icon-arrow-right el-icon--right"></i>
</span>
</el-button>
</el-popover>
</el-col>
</el-row>
</div>
<div class="mini-im-session-content">
<div class="mini-im-flex">
<div class="mini-im-no-data" v-if="contacts.length <= 0">暂无会话数据</div>
<ContactComponent :deleteContact="deleteContact" :clickItem="selectUser" :item="item" :class="{'mini-im-chat-item-active': seviceCurrentUser.from_account == item.from_account}" :key="index" v-for="(item, index) in contacts" />
</div>
</div>
</div>
<div class="mini-im-chat-view no-window" v-if="!$store.getters.seviceCurrentUser.id" >
<div><i class="el-icon-service"></i></div>
<span>当前无对话</span>
<div class="mini-im-right-window-loading" v-show="chatWindowLoading">
<i class="el-icon-loading"></i><span>数据加载中...</span>
</div>
</div>
<div v-else class="mini-im-chat-view">
<div class="mini-im-chat-view-content-header">
<div class="mini-im-header-user-box">
<el-avatar :size="35" class="mini-im-avatar">
<img v-if="seviceCurrentUser.avatar != ''" :src="seviceCurrentUser.avatar"/>
<template v-else>访</template>
</el-avatar>
<div class="mini-im-header-user-info">
<div>
{{seviceCurrentUser.nickname}}
<span style="color: rgb(135, 208, 104)" v-if="seviceCurrentUser.online == 1">● 在线</span>
<span v-else>● 离线</span>
<template v-if="isInputPongIng">
<span class="input-pong">{{inputPongIngString}} <i class="el-icon-edit"></i></span>
</template>
</div>
<span>用户来至:{{$getPlatformItem(seviceCurrentUser.platform).title}}客户端,所在地:{{seviceCurrentUser.address || '未知'}}</span>
</div>
</div>
<el-row class="mini-im-buttons">
<el-popover
placement="bottom"
width="200"
trigger="click">
<div class="mini-im-customer-list">
<div class="mini-im-customer-title">请选择转接的客服 ({{filterAdmins.length}}人在线)</div>
<div class="mini-im-customer-item" :key="item.id" v-for="item in filterAdmins" @click="()=>transferCustomer(item)">
<el-avatar :size="30" class="mini-im-avatar">
<img :src="item.avatar"/>
</el-avatar>
<span>{{item.nickname || item.username}}</span>
</div>
<div style="background: none;border:0;" v-if="filterAdmins.length == 0" class="mini-im-customer-item">
<span>当前没有其他客服在线</span>
</div>
</div>
<el-button v-show="seviceCurrentUser.is_session_end == 0 && adminInfo.online != 0" @click="getAdmins" slot="reference" icon="el-icon-refresh" size="small">
转接客服
</el-button>
</el-popover>
<el-button v-if="seviceCurrentUser.is_session_end == 0 && adminInfo.online != 0" @click="closeSession" icon="el-icon-close" size="small">结束会话</el-button>
</el-row>
</div>
<div ref="miniImChatViewBontentBody" class="mini-im-chat-view-content-body">
<div class="mini-im-chat-view-content">
<div v-show="advanceText.trim() != ''" class="advance">
<div>正在输入:</div>
<span>
{{advanceText}}
<template v-if="isInputPongIng">
<span class="input-pong">{{inputPongIngString.replace("对方正在输入", "")}} <i class="el-icon-edit"></i></span>
</template>
</span>
</div>
<div ref="chatBody" id="chatBody" class="mini-im-chat-body">
<ChatWindowComponent :onLoadMor="onLoadMorMessage" :isMessageEnd="isMessageEnd" :onCancelMessage="onCancelMessage" :messages="messageRecord.list" :loading="getMessageRecordLoading"/>
</div>
<div class="mini-im-chat-input">
<div class="mini-im-chat-input-bar">
<el-row>
<EmojiComponent :clickEmoji="clickEmoji" />
<span title="选择图片" class="mini-im-button">
🌁
<input
onClick="this.value = null"
@change="sendPhotoMessageEvent"
type="file"
accept="image/*"
/>
</span>
</el-row>
<el-row>
<el-popover
placement="top-start"
width="350"
trigger="hover"
>
<div class="mini-im-shortcut">
<div class="mini-im-shortcut-head">
<span><i class="el-icon-tickets"></i>快捷语回复列表</span>
<div>
<button @click="createShortcutDialogFormVisible = true" title="添加"><i class="el-icon-plus"></i></button>
<button @click="shortcutEditVisible = !shortcutEditVisible" title="编辑"><i class="el-icon-edit"></i></button>
</div>
</div>
<div class="mini-im-shortcut-body">
<el-input clearable style="margin-bottom: 10px;" v-model="shortcutKey" type="text" placeholder="请输入关键词" autocomplete="off"></el-input>
<div style="background: none;" class="mini-im-shortcut-item" v-if="filterShortcuts.length == 0">
<span style="text-align: center;margin-top: 20px;">暂无快捷语</span>
</div>
<div :title="item.content" class="mini-im-shortcut-item" :key="item.id" v-for="item in filterShortcuts">
<span @click.capture="()=>checkShortcut(item.content)" v-html='item.title.replace(/\n/g, "<br>")'></span>
<button v-show="shortcutEditVisible" @click.capture="()=>editShortcut(item)" title="修改"><i class="el-icon-edit"></i></button>
<button v-show="shortcutEditVisible" @click.capture="()=>deleteShortcut(item)" title="删除"><i class="el-icon-delete"></i></button>
</div>
</div>
</div>
<button class="mini-im-button" slot="reference">
<i style="font-size: 15px" class="el-icon-tickets"></i>
<span style="font-size: 14px"> 快捷语</span>
</button>
</el-popover>
</el-row>
</div>
<div class="mini-im-chat-input-edit" @keyup.exact="keyUpEvent" @keyup.enter.13.shift="enterShift" @keyup.enter.exact="sendMessage">
<el-input
type="textarea"
ref="chatValueDom"
rows="3" resize="none"
:autosize="false"
:disabled="seviceCurrentUser.is_session_end == 1 || adminInfo.online == 0"
class="mini-im-chat-text-input"
maxlength="200"
show-word-limit
v-model="chatValue"
:placeholder="seviceCurrentUser.is_session_end == 1 ? '当前会话已结束' : '请输入内容'">
</el-input>
</div>
</div>
</div>
<div class="mini-im-chat-view-user">
<el-tabs type="border-card">
<el-tab-pane label="用户信息">
<UserInfoComponent />
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
<CreateShortcutComponent :complete="getShortcuts" :dialogFormVisible.sync="createShortcutDialogFormVisible" />
<EditShortcutComponent :formData="editShortcutItem" :complete="getShortcuts" :dialogFormVisible.sync="editShortcutDialogFormVisible" />
</div>
</template>
<script>
import EmojiComponent from "./emoji"
import ContactComponent from "./contact"
import UserInfoComponent from "./user_info"
import CreateShortcutComponent from "./create_shortcut"
import EditShortcutComponent from "./edit_shortcut"
import ChatWindowComponent from "./chat_window"
import upload from '../../common/upload'
import axios from "axios";
import Push from "push.js";
import { mapGetters } from 'vuex'
export default {
name: "workbench",
components: {
EmojiComponent,
ContactComponent,
UserInfoComponent,
ChatWindowComponent,
CreateShortcutComponent,
EditShortcutComponent
},
data(){
return {
chatValue: "",
advanceText: "",
admins: [],
shortcuts: [],
shortcutKey: "",
createShortcutDialogFormVisible: false,
editShortcutDialogFormVisible: false,
editShortcutItem: null,
shortcutEditVisible: false,
getMessageRecordLoading: true,
chatWindowLoading: false,
currentSessionIsEnd: false,
getMessageRecordPageSize: 20,
isInputPongIng: false,
isSendPong: false,
inputPongIngString: "对方正在输入...",
isPush: false, // 是否可以推送消息
isMessageEnd: false,
mousemoveTimerout: null
}
},
computed: {
filterShortcuts(){
var shortcutKey = this.shortcutKey.trim()
if(shortcutKey != ""){
return this.shortcuts.filter(i => i.title.indexOf(shortcutKey) != -1)
}else{
return this.shortcuts
}
},
filterAdmins(){
return this.admins.filter((i) => i.id != this.adminInfo.id && (i.online == 1 || i.online == 2))
},
...mapGetters([
"contacts",
"adminInfo",
"seviceCurrentUser",
"messageRecord"
])
},
mounted(){
// 关闭快捷语面板
document.ondblclick = () => {
this.shortcutEditVisible = false
}
this.init()
// 刷新鼠标动态
document.addEventListener("mousemove", this.onMousemoveEvent, false)
// 粘贴事件
document.addEventListener("paste", this.inputPaste, false)
},
beforeDestroy(){
document.removeEventListener("mousemove", this.onMousemoveEvent, false)
document.removeEventListener("paste", this.inputPaste, false)
this.changeCurrentUser();
},
methods: {
// 初始化
init(){
if(!this.adminInfo){
this.$store.dispatch('ON_GET_ME')
setTimeout(() => this.init(), 100)
return
}
// 获取快捷语
this.getShortcuts()
// 判断当前和谁聊天
this.chatWindowLoading = true
setTimeout(()=>{
this.chatWindowLoading = false
}, 1500)
setTimeout(()=>{
var uid = this.$store.getters.seviceCurrentUser.id || this.$route.query.uid
this.changeCurrentUser(uid || 0)
if(uid){
var user
this.contacts.map(i => {
if(i.from_account == uid){
user = i
}
})
history.replaceState(null, null, location.href.replace(/uid=\d+/i, "uid=" + uid))
this.$store.commit("onChangeSeviceCurrentUser", user)
if(user) this.selectUser(user)
// 获取聊天记录
this.getMessageRecord()
this.scrollIntoBottom()
}
}, 1000)
// 监听登录状态
this.$mimcInstance.addEventListener("statusChange", (status) => {
if(this.adminInfo.online == 1){
this.$message.success("您当前状态为在线")
}else if(this.adminInfo.online == 2){
this.$message.warning("您当前状态为繁忙")
}
if(!status){
this.$store.dispatch('ON_GET_ME').then(()=>{
if(this.adminInfo.online != 0){
this.init();
}
})
}
})
// 监听消息
this.$mimcInstance.addEventListener("receiveP2PMsg", this.receiveP2PMsg)
// 监听连接断开
this.$mimcInstance.addEventListener("disconnect", () => {
console.log("链接断开!")
var adminInfo = this.adminInfo
if(adminInfo.online != 0){
this.adminInfo = null;
this.init();
}else{
adminInfo.online = 0
this.$store.commit("onChangeAdminInfo", adminInfo)
}
})
},
// 刷新鼠标动态 mousemove
onMousemoveEvent(){
// 以下其他浏览器的聊天高度
if(this.$refs.miniImChatViewBontentBody){
this.$refs.miniImChatViewBontentBody.style.height = document.body.clientHeight - 155 + "px"
}
this.isPush = false;
if(this.mousemoveTimerout) clearTimeout(this.mousemoveTimerout);
this.mousemoveTimerout = setTimeout(()=>{
this.isPush = true;
}, 30000)
},
// 快捷键换行
enterShift(event){
if(event.code == "Enter") return
this.chatValue = this.chatValue + "\n"
},
// 滚动条置底
scrollIntoBottom(){
try{
setTimeout(()=>{
var chatBody = document.getElementById("chatBody")
if(!chatBody) return
var height = chatBody.clientHeight
var scrollHeight = chatBody.scrollHeight
chatBody.scrollTop = scrollHeight-height
}, 50)
}catch(e){
console.log(e)
}
},
// 删除单个会话记录(聊天数据不会删除)
deleteContact(item){
if(!item)return
if(!item.cid)return
axios.delete('/contact/' + item.cid)
.then(() => {
this.$message.success("删除成功")
this.$store.dispatch('ON_GET_CONTACTS')
if(this.seviceCurrentUser.id == item.id){
this.changeCurrentUser();
this.$store.commit("onChangeSeviceCurrentUser", null)
}
})
.catch(error => {
this.$message.error(error.response.data.message)
});
},
// 清空会话记录(聊天数据不会删除)
clearContact(){
this.$confirm('您确定要清空列表吗? ', '温馨提示!', {
confirmButtonText: '确定',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() => {
axios.delete('/contact/clear')
.then(() => {
this.$message.success("清空成功")
this.$store.dispatch('ON_GET_CONTACTS')
this.changeCurrentUser();
this.$store.commit("onChangeSeviceCurrentUser", null)
})
.catch(error => {
this.$message.error(error.response.data.message)
});
})
},
// emoji
clickEmoji(emoji){
// 当前用户是否上线
if(this.adminInfo.online == 0){
this.$message.info("您当前为离线状态!")
return
}
if(this.seviceCurrentUser.is_session_end == 1){
this.$message.info("当前会话已结束!")
return
}
this.chatValue = this.chatValue + emoji
this.$refs.chatValueDom.focus()
},
// 选择快捷语
checkShortcut(value){
// 当前用户是否上线
if(this.adminInfo.online == 0){
this.$message.info("您当前为离线状态!")
return
}
if(this.seviceCurrentUser.is_session_end == 1){
this.$message.info("当前会话已结束!")
return
}
this.shortcutKey = ""
this.chatValue = value
this.$refs.chatValueDom.focus()
},
// 获取快捷语
getShortcuts(){
axios.get('/shortcut/list')
.then((res) => {
this.shortcuts = res.data.data
})
.catch((error)=>{
this.$message.error(error.response.data.message);
})
},
// 编辑快捷语
editShortcut(item){
this.editShortcutItem= item
this.editShortcutDialogFormVisible = true
},
// 获取在线客服
getAdmins(){
axios.post('/admin/list', {page_on: 1, page_size: 10000, online: 3})
.then(response => {
this.loading = false
this.admins = response.data.data.list
})
.catch(error => {
this.$message.error(error.response.data.message)
});
},
// 删除快捷语
deleteShortcut(item){
this.$confirm('您确定要删除该快捷语吗?', '温馨提示!', {
confirmButtonText: '确定',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() => {
axios.delete('/shortcut/' + item.id)
.then(() => {
this.$message.success("删除成功")
this.getShortcuts()
})
.catch(error => {
this.$message.error(error.response.data.message)
});
})
},
// 撤回消息
onCancelMessage(key){
const message = this.$mimcInstance.sendMessage("cancel", this.seviceCurrentUser.from_account, key)
this.messageRecord.list.push(message)
this.removeMessage(this.adminInfo.id, key)
if(this.qiniuObservable) this.qiniuObservable.unsubscribe()
},
// 转接客服
transferCustomer(item){
this.$confirm('您确定将该客户转接给 ' +(item.nickname || item.username)+' 吗?', '温馨提示!', {
confirmButtonText: '转接',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() => {
axios.post('/contact/transfer', {to_account: item.id, user_account: this.seviceCurrentUser.from_account})
.then(() => {
var seviceCurrentUser = this.seviceCurrentUser
seviceCurrentUser.is_session_end = 1
this.$store.commit("onChangeSeviceCurrentUser", seviceCurrentUser)
})
.catch(error => {
this.$message.error(error.response.data.message)
});
})
},
// 结束当前会话
closeSession(){
this.$confirm("您确定结束当前会话吗?强制结束可能会被客户投诉!", '温馨提示!', {
confirmButtonText: '结束',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() => {
const localMessage = this.$mimcInstance.sendMessage("end", this.seviceCurrentUser.from_account, "")
this.messageRecord.list.push(localMessage)
var seviceCurrentUser = this.seviceCurrentUser
seviceCurrentUser.is_session_end = 1
this.$store.commit("onChangeSeviceCurrentUser", seviceCurrentUser)
})
},
// 更新用户状态
changeUserOnlineStatus(online){
// 更新状态
axios.put('/admin/online/' + online)
.then(() => {
this.$store.dispatch('ON_GET_ME')
if(online == 0){
this.$message.info("当前状态为离线")
}
})
.catch(error => {
this.$message.error(error.response.data.message)
});
},
// 上下线
online(online){
var self = this
if(self.adminInfo.online == online) return
self.$confirm("您确定" + (online == 0 ? "下线" : online == 1 ? "上线": "设置繁忙") +"吗!", '温馨提示!', {
confirmButtonText: '确定',
cancelButtonText: '取消',
center: true,
type: 'warning'
}).then(() =>{
if(online == 0){
self.$mimcInstance.logout()
self.changeUserOnlineStatus(online)
self.$store.commit("onChangeMimcUser", null)
}else{
self.$mimcInstance.login(()=>{
self.changeUserOnlineStatus(online)
self.$store.dispatch('ON_RUN_LAST_ACTiIVITY')
self.$store.dispatch('ON_GET_CONTACTS')
self.$store.commit("onChangeMimcUser", self.$mimcInstance.user)
})
}
})
},
// 接收消息
receiveP2PMsg(message){
console.log(message)
var nowTime = parseInt((new Date().getTime() +"").substr(0, 10))
message.timestamp = parseInt((message.timestamp +"").substr(0, 10))
if(message.from_account == this.adminInfo.id && message.biz_type == "pong") return;
if(message.biz_type == "into") return;
if(message.from_account == this.adminInfo.id && this.seviceCurrentUser.from_account == message.to_account){
this.messageRecord.list.push(message)
if(message.biz_type == "cancel"){
this.removeMessage(message.from_account, message.payload)
}
this.scrollIntoBottom()
this.$previewRefresh()
return;
}
// 文本消息
if(message.biz_type == "text" && message.from_account == this.seviceCurrentUser.from_account){
this.advanceText = ""
}
// 处理用户列表
if(message.biz_type == "contacts"){
var contacts = JSON.parse(message.payload)
this.$store.commit('onChangeContacts', contacts)
return
}
if(nowTime - message.timestamp >= 60) return
// 是否是撤回消息
if(message.biz_type == "cancel"){
this.removeMessage(message.from_account, message.payload)
}
// 判断是否是握手消息
if(message.biz_type == "handshake"){
setTimeout(() => {
this.$mimcInstance.sendMessage("text", message.from_account, this.adminInfo.auto_reply)
if(this.seviceCurrentUser == undefined || this.seviceCurrentUser == null) return
setTimeout(() => this.getMessageRecord(), 1000)
}, 500)
return
}
// 对方正在输入
if(message.biz_type == "pong" && message.from_account == this.seviceCurrentUser.from_account){
this.advanceText = message.payload
this.inputPongIng()
return
}
// 推送消息
if(!(message.biz_type == "contacts" || message.biz_type == "pong" || message.biz_type == "welcome" || message.biz_type == "cancel" || message.biz_type == "handshake" || message.biz_type == "end" || message.biz_type == "timeout") && this.isPush && Push.Permission.has()){
Push.create("收到一条新消息", {
body: message.payload,
icon: this.$store.state.pushIcon,
timeout: 5000,
onClick: () => {
this.$router.push({ path: '/workbench?uid=' + message.from_account})
window.focus();
setTimeout(() => this.getMessageRecord(), 1000)
}
});
}
// 是否是否当前会话消息
if(message.from_account != this.seviceCurrentUser.from_account) return
if(message.biz_type == 'end'){
var seviceCurrentUser = this.seviceCurrentUser
seviceCurrentUser.is_session_end = 1
this.$store.commit("onChangeSeviceCurrentUser", seviceCurrentUser)
this.advanceText = ""
}
this.messageRecord.list.push(message)
let messageRecord = JSON.stringify(this.messageRecord)
this.$store.commit("onChangeMessageRecord", JSON.parse(messageRecord))
this.scrollIntoBottom()
this.$previewRefresh()
},
// 更新当前和谁聊天
changeCurrentUser(uid = 0){
if(JSON.stringify(this.adminInfo) == "{}") return;
axios.get('/admin/current/user/' + uid)
},
// 发送文本消息
sendMessage(){
// 当前用户是否上线
if(this.adminInfo.online == 0){
this.$message.info("您当前为离线状态!")
return
}
if(this.seviceCurrentUser.is_session_end == 1) return
// 当前用户是否已经结束会话
if(this.seviceCurrentUser.is_session_end == 1){
this.$message.info("当前会话已结束!")
return
}
var chatValue= this.chatValue.trim()
if(chatValue == ""){
this.chatValue = ""
return
}
this.shortcutEditVisible = false
this.scrollIntoBottom()
const msg = this.$mimcInstance.sendMessage("text", this.seviceCurrentUser.from_account, this.chatValue.trim("\n"))
msg.isShowCancel = true
setTimeout(() => msg.isShowCancel = false, 10000)
this.messageRecord.list.push(msg)
this.chatValue = ""
},
// 发送图片消息
sendPhotoMessageEvent(e){
var fileDom = e.target;
var file = fileDom.files[0]
this.sendPhotoMessage(file)
},
sendPhotoMessage(file){
// 当前用户是否上线
if(this.adminInfo.online == 0){
this.$message.info("您当前为离线状态!")
return
}
if(this.seviceCurrentUser.is_session_end == 1){
this.$message.info("当前会话已结束!")
return
}
var imgFile = new FileReader();
imgFile.readAsDataURL(file)
var self = this
var localMessage
imgFile.onload = function(){
localMessage = self.$mimcInstance.createLocalMessage("photo", self.seviceCurrentUser.from_account, this.result)
localMessage["percent"] = 0
localMessage.isShowCancel = true
self.messageRecord.list.push(localMessage)
setTimeout(() => localMessage.isShowCancel = false, 10000)
self.$previewRefresh()
self.scrollIntoBottom()
upload({ file,
progress: (percent) => {
localMessage.percent = percent
},
success: (url) => {
localMessage.percent = 100
var imgUrl = self.$store.getters.uploadToken.host + "/" + url;
self.$mimcInstance.sendMessage("photo", self.seviceCurrentUser.from_account, imgUrl)
},
error: (err)=>{
localMessage.percent = 0
self.$message.error(err.message);
}
});
}
},
// 选择用户
selectUser(user){
let href = location.href
let index = href.indexOf("#")
href = href.substr(0, index != -1 ? index : href.length)
history.replaceState(null, null, href + '#/workbench?uid=' + user.from_account)
this.isMessageEnd = false
if(this.seviceCurrentUser.from_account != user.from_account){
this.messageRecord.list = []
this.$store.commit("onChangeSeviceCurrentUser", user)
this.changeCurrentUser(user.from_account)
}
// 获取聊天记录
this.timestamp = undefined
this.getMessageRecord()
this.advanceText = ""
},
// 获取聊天记录
getMessageRecord(timestamp){
this.getMessageRecordLoading = true
if(timestamp == undefined){
timestamp = 0
}
var account = parseInt(this.seviceCurrentUser.from_account)
if(!account) return
axios.post('/message/list', {
"timestamp": timestamp,
"page_size": this.getMessageRecordPageSize,
"account": account
})
.then(response => {
this.getMessageRecordLoading = false
if(response.data.data.list.length < this.getMessageRecordPageSize){
this.isMessageEnd = true
}
if(this.messageRecord.list.length == 0 || timestamp == 0){
this.$store.commit("onChangeMessageRecord", response.data.data)
this.scrollIntoBottom()
}else{
response.data.data.list = response.data.data.list.concat(this.messageRecord.list)
this.$store.commit("onChangeMessageRecord", response.data.data)
}
setTimeout(()=>this.$previewRefresh(), 1000)
this.$store.dispatch('ON_GET_CONTACTS')
})
.catch(() => {
this.getMessageRecordLoading = false
});
},
//获取更多消息
onLoadMorMessage(){
if(this.getMessageRecordLoading) return
if(this.messageRecord.list.length >= this.messageRecord.total || this.messageRecord.total <= this.getMessageRecordPageSize){
this.isMessageEnd = true
return
}
this.getMessageRecord(this.messageRecord.list[0].timestamp)
setTimeout(()=>{
var chatBody = document.getElementById("chatBody")
chatBody.scrollTop = 500
}, 50)
},
// 显示正在输入
inputPongIng(){
if(this.isInputPongIng)return
this.isInputPongIng = true
setTimeout(()=>{
this.inputPongIngString = "对方正在输入."
}, 500)
setTimeout(()=>{
this.inputPongIngString = "对方正在输入.."
}, 1500)
setTimeout(()=>{
this.inputPongIngString = "对方正在输入..."
this.isInputPongIng = false
}, 3000)
},
// 敲键盘发送pong事件消息
keyUpEvent(){
if(this.isSendPong) return
this.isSendPong = true
setTimeout(() => this.isSendPong = false, 500)
this.$mimcInstance.sendMessage("pong", this.seviceCurrentUser.from_account, "")
},
// 删除消息
removeMessage(accountId, key){
var newMessages = []
var list = this.messageRecord.list
for(let i =0; i<list.length; i++){
if(list[i].key == key && list[i].from_account == accountId) continue
newMessages.push(list[i])
}
this.messageRecord.list = newMessages
this.$store.commit("onChangeMessageRecord", this.messageRecord)
},
// 输入框粘贴事件
inputPaste(e){
if(!this.seviceCurrentUser.id) return
if(this.seviceCurrentUser.is_session_end == 1) return
let self = this
var cbd = e.clipboardData;
var ua = window.navigator.userAgent;
// Safari return
if ( !(e.clipboardData && e.clipboardData.items) ) {
return;
}
// Mac平台下Chrome49版本以下 复制Finder中的文件的Bug Hack掉
if(cbd.items && cbd.items.length === 2 && cbd.items[0].kind === "string" && cbd.items[1].kind === "file" &&
cbd.types && cbd.types.length === 2 && cbd.types[0] === "text/plain" && cbd.types[1] === "Files" &&
ua.match(/Macintosh/i) && Number(ua.match(/Chrome\/(\d{2})/i)[1]) < 49){
return;
}
for(var i = 0; i < cbd.items.length; i++) {
var item = cbd.items[i];
if(item.kind == "file"){
var file = item.getAsFile();
if (file.size === 0) {
return;
}
var imgFile = new FileReader();
imgFile.readAsDataURL(file);
imgFile.onload = function () {
var imgData = this.result;
self.$alert(
'<img preview="1" style="width:100%;max-height: 500px;" src="'+imgData+'" />',
'检测到图片是否要发送?', {
dangerouslyUseHTMLString: true,
showCancelButton: true,
confirmButtonText: "发送"
}).then(() => {
self.sendPhotoMessage(file)
});
self.$previewRefresh()
}
}
}
}
},
watch:{
messageRecord(){
this.$previewRefresh()
}
}
};
</script>
<style lang="stylus" scoped>
.mini-im-workbench{
height 100%
display flex
flex-direction row
min-width: 1100px;
}
.mini-im-session-list{
width 280px
border-radius 5px
box-sizing border-box
display flex
flex-direction column
height 100%
.mini-im-no-data{
text-align center
padding-top 15px
font-size 14px
color #666
}
.title{
height 35px
border-radius 5px 5px 0 0
background-color #f4f5f7
line-height 35px
padding-left 10px
color #666
font-size 14px
border 1px solid #edf1f5
button{
border 0
background none
text-align right
span>span{
display flex
align-items center
align-content center
i{
font-size 15px
}
em{
margin-left 3px
}
.el-icon--right{
font-size 12px
}
}
}
}
.mini-im-session-content{
flex-grow 1
height 100%
border 1px solid #edf1f5
overflow hidden
overflow-y auto
width 278px
background-color #fff
border-radius 0 0 5px 5px
}
}
.mini-im-chat-view{
flex-grow 1
border 1px solid #edf1f5
margin-left 20px
border-radius 3px
overflow hidden
display flex
flex-direction column
.mini-im-chat-view-content-header{
width 100%
flex-shrink 0
height 55px
border-bottom 1px solid #edf1f5
display flex
justify-content space-between
background-color #f4f5f7
align-items center
padding 0 10px
box-sizing border-box
.mini-im-header-user-box{
display flex
flex-direction row
align-items center
.mini-im-header-user-info{
padding-left 10px
font-size 14px
display flex
flex-direction column
justify-content space-around
.input-pong{
margin-left 10px
font-size 12px
}
div{
font-weight 600
color #666
span{
font-size 10px
}
}
span{
color #999
font-size 12px
}
}
}
.mini-im-buttons{
width: 230px;
display: flex;
justify-content: space-around;
}
}
.mini-im-chat-view-content-body{
display flex
flex-direction row
flex-grow 1
overflow hidden
background-color #fff
.mini-im-chat-view-content{
flex-grow 1
height 100%
display flex
flex-direction column
.mini-im-chat-body{
background-color #fff
flex-grow 1
padding 10px
padding-bottom 20px
overflow: hidden;
overflow-y: auto;
min-width: 400px;
}
.mini-im-chat-input{
height 115px
border-top 1px solid #edf1f5
position relative
flex-grow 0
background-color #fff
flex-shrink 0
.mini-im-chat-input-bar{
height 30px
display flex
justify-content space-between
padding 0 15px
box-sizing border-box
.mini-im-button{
height 30px
padding 0 5px
border 0
font-size 18px
cursor pointer
background-color #fff
color #666
position relative
overflow hidden
input{
position absolute
top 0
cursor pointer
left 0
width 100%
opacity 0
height 100%
font-size 100px
}
i{
color #666
}
}
}
.mini-im-chat-input-edit{
height 100%
.mini-im-chat-text-input{
width 100%
border: 0px solid #DCDFE6;
resize none
font-size 14px
color #666
box-sizing border-box
padding 5px;
}
}
}
}
.mini-im-chat-view-user{
width 350px
height 100%
border-left 1px solid #edf1f5
box-sizing border-box
background-color #fff
flex-shrink 0
flex-grow 0
.el-tabs--border-card{
height 100%
border 0
box-shadow none
}
}
}
.mini-im-chat-view-content{
position relative
.advance{
position absolute
box-sizing: border-box;
width 100%
left 0
bottom 115px
font-size: 14px;
color: #999;
display flex
padding 5px 3px
background-color: #f5f7fa;
border-top 1px solid #f3f3f3
div{
width 70px
flex-shrink: 0;
}
span{
font-size 12px
}
}
}
}
.no-window{
display flex
background-color #fff
text-align center
flex-direction column
align-items center
justify-content center
position relative
i{
font-size 130px
color #999
}
span{
color #999
font-size 20px
margin-top 10px
}
.mini-im-right-window-loading{
width 100%
height 100%
background-color #fff
display flex
align-items center
justify-content center
position absolute
left 0
top 0
i{
font-size 25px
}
span{
margin-left 5px
font-size 15px
margin-top 0
}
}
}
.mini-im-user-info{
width 300px
}
.mini-im-online-setting{
font-size 14px
color #666
.item{
padding 5px
cursor pointer
border-radius 3px
&:hover{
background #f2f2f2
}
}
}
.mini-im-shortcut{
display flex
height 500px
flex-direction column
.mini-im-shortcut-head{
height 30px
width 100%
display flex
border-bottom 1px solid #f4f5f7
justify-content space-between
align-items center
padding-bottom 5px
button{
width 25px
height 25px
flex-grow 0
flex-shrink 0
border 0
i{
font-size 15px
color #999
cursor pointer
}
}
}
.mini-im-shortcut-body{
flex-grow 1
display block
width 100%
overflow hidden
overflow-y auto
}
.mini-im-shortcut-item{
display flex
width 100%
min-height 30px
padding 5px
box-sizing border-box
cursor pointer
font-size 13px
span{
flex-grow 1
padding-right 10px
}
button{
width 15px
height 30px
flex-grow 0
flex-shrink 0
margin-right: 5px;
border 0
background none
i{
font-size 15px
color #999
cursor pointer
}
}
&:hover{
opacity .9
background #f2f2f2
border-radius 3px
}
}
}
.mini-im-customer-list{
overflow hidden
min-height 150px
max-height 500px
overflow-y auto
.mini-im-customer-title{
padding-bottom 10px
border-bottom 1px solid #f2f2f2
}
.mini-im-customer-item{
display flex
cursor pointer
align-items center
padding 5px
border-bottom 1px solid #f7f5f5
border-radius 3px
&:hover{
background #f2f2f2
}
span{
margin-left 10px
}
}
}
.mini-im-avatar{
flex-grow 0
flex-shrink 0
}
</style>
<template>
<div class="mini-im-username-component-box">
<div class="mini-im-username-component">
<button @click="isUserReadonly = !isUserReadonly" title="编辑用户信息"><i class="el-icon-edit"></i></button>
</div>
<el-form ref="form" :class="{'form-item-readonly': isUserReadonly}" label-width="80px">
<el-form-item class="form-item" label="用户昵称">
<el-input v-model="form.nickname" placeholder="游客" :readonly="isUserReadonly" type="text"></el-input>
</el-form-item>
<el-form-item class="form-item" label="所在地区">
<el-input :readonly="isUserReadonly" placeholder="无" v-model="form.address" type="text"></el-input>
</el-form-item>
<el-form-item class="form-item" label="联系方式">
<el-input :readonly="isUserReadonly" placeholder="无联系方式" v-model="form.phone" type="text"></el-input>
</el-form-item>
<el-form-item class="form-item no-border" label="所在平台">
<el-input readonly :value="$getPlatformItem(user.platform).title" resize="none" type="text"></el-input>
</el-form-item>
<el-form-item class="form-item no-border" label="创建时间">
<el-input readonly :value='$formatUnixDate(user.create_at, "YYYY/MM/DD")' resize="none" type="text"></el-input>
</el-form-item>
<el-form-item class="form-item" label="备注信息">
<el-input rows="4" :readonly="isUserReadonly" placeholder="无备注" v-model="form.remarks" resize="none" type="textarea"></el-input>
</el-form-item>
<el-row type="flex" justify="center" v-if="!isUserReadonly">
<el-button @click="isUserReadonly = true">取消</el-button>
<el-button @click="save" type="primary">保存</el-button>
</el-row>
<div v-if="!isUserReadonly" style="text-align: center;font-size: 12px; color: #666;margin-top: 15px;">当前为编辑模式</div>
</el-form>
</div>
</template>
<script>
import axios from "axios";
export default {
name: 'mini-im-user-info',
data(){
return {
form:{
id: "",
nickname: "",
address: "",
phone: "",
remarks: ""
},
isUserReadonly: true, // 用户信息是否是编辑模式
}
},
computed: {
user(){
return this.$store.getters.seviceCurrentUser || {}
}
},
methods:{
// 保存
save() {
axios
.put("/user", this.form)
.then(() => {
this.isUserReadonly = true
})
.catch(error => {
this.$message.error(error.response.data.message);
});
},
},
watch: {
user(newUser){
if(newUser.id != this.form.id) this.isUserReadonly = true
if(!this.isUserReadonly && newUser.id == this.form.id) return
this.form = this.user
}
}
}
</script>
<style lang="stylus">
.mini-im-username-component-box{
height 100%
overflow hidden
overflow-y auto
padding: 10px 10px 10px 5px;
.form-item{
margin-top 25px
}
.form-item-readonly input.el-input__inner,
.form-item-readonly textarea.el-textarea__inner {
border 0
}
.no-border input.el-input__inner{
border 0
}
}
.mini-im-username-component{
position absolute
top 10px
right 25px
span{
font-size 18px
color #666
i{
font-size 20px
}
}
button{
border 0
cursor pointer
}
}
</style>
module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
outputDir: '../admin/',
devServer: {
proxy: 'http://localhost:8080',
}
}
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: 'babel-eslint'
}
}
node_modules
dist
\ No newline at end of file
客服系统开发者QQ交流群: 623661658
# 欢迎使用本客服系统 - 客户端H5
![客服系统](http://qiniu.cmp520.com/kefuxitonh.jpg)
## 本项目关联GIT项目资源连接
- **[服务端][1]**
- **[客服端-APP工作台][6]** 客服端APP工作台flutter源码
- **[客服端-网页工作台][2]**
- **[客户端H5][3]**
- **[客户端Flutter][4]**
**本系统** 是基于小米消息云实现的一款简单实用的面向多终端的客服系统,本系统简单易用,易扩展,易整合现有的业务系统,无缝对接自有业务。
## 安装与打包
```
npm install
npm run serve
npm run build
npm run test
npm run lint
```
## 连接管控关键字
通过连接控制页面的样式以及一些基本配置
``` html
// url query 介绍
// h == header 0 不显示 1显示 默认值显示,PC端不显示
// m == mobile 0 不是移动端 1是移动端
// p == platform 平台ID(渠道)
// r == robot 0 当前为为客服 1机器人(对应的账号为a)
// a == account 当前提供对话服务的账号,即客服账号,或机器人
// u == userAccount 会话用户账号
// uid == userId 业务平台的ID
// c = 1 清除本地缓存
```
## mini_im.js 工具
GO》》》》》[Example][5]
mini_im 工具是帮助其在PC端以及其他小程序,创建IM账户连接提供的相关函数,具体使用见example目录demo
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
### TypeError: Cannot destructure property `createHash` of 'undefined' or 'null'.
npm add webpack@latest OK
[1]: https://github.com/chenxianqi/kefu_server
[2]: https://github.com/chenxianqi/kefu_admin
[3]: https://github.com/chenxianqi/kefu_client
[4]: https://github.com/chenxianqi/kefu_flutter
[5]: http://kf.aissz.com:666/example/
[6]: https://github.com/chenxianqi/kefu_workbench
module.exports = {
presets: [
'@vue/app'
]
}
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>在线客服 demo</title>
<style>
body{
overflow: hidden;
background: url(./bg.jpg) center center;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.title{
position: absolute;
top: 30px;
left:0; right:0;
margin: 0 auto;
color: #d9d4f2;
text-align: center;
font-size: 40px;
}
.mini-im{
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-content: center;
align-items: center;
padding-bottom: 20px;
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
right: 0;
margin: 0 auto;
}
.mini-im div{
margin-bottom: 10px;
display: flex;
}
button{
width: 120px;
height: 35px;
border-radius: 3px;
margin: 0 10px;
cursor: pointer;
outline: none;
border: 1px solid #ece9fd;
background-color:#ece9fd;
}
button:hover{
border: 1px solid #1898fc;
}
.mini-im-m{
width: 350px;
height: 550px;
position: absolute;
top: 120px;
left:0; right:0;
margin: 0 auto;
z-index: 9999;
box-shadow: 1px 1px 20px 2px rgb(9, 102, 178);
}
.fix-bottom{
position: absolute;
top: 20px;
right: 30px;
}
.fix-bottom a{
display: flex;
align-items: center;
}
.fix-bottom a span{
margin-left: 10px;
color: #ffffff;
text-decoration: none;
}
</style>
</head>
<body>
<canvas id="nokey" width="946" height="946"></canvas>
<script>
var canvas = document.getElementById('nokey'),
can_w = parseInt(canvas.getAttribute('width')),
can_h = parseInt(canvas.getAttribute('height')),
ctx = canvas.getContext('2d');
// console.log(typeof can_w);
var ball = {
x: 0,
y: 0,
vx: 0,
vy: 0,
r: 0,
alpha: 1,
phase: 0
},
ball_color = {
r: 207,
g: 255,
b: 4
},
R = 2,
balls = [],
alpha_f = 0.03,
alpha_phase = 0,
// Line
link_line_width = 0.8,
dis_limit = 260,
add_mouse_point = true,
mouse_in = false,
mouse_ball = {
x: 0,
y: 0,
vx: 0,
vy: 0,
r: 0,
type: 'mouse'
};
// Random speed
function getRandomSpeed(pos){
var min = -1,
max = 1;
switch(pos){
case 'top':
return [randomNumFrom(min, max), randomNumFrom(0.1, max)];
break;
case 'right':
return [randomNumFrom(min, -0.1), randomNumFrom(min, max)];
break;
case 'bottom':
return [randomNumFrom(min, max), randomNumFrom(min, -0.1)];
break;
case 'left':
return [randomNumFrom(0.1, max), randomNumFrom(min, max)];
break;
default:
return;
break;
}
}
function randomArrayItem(arr){
return arr[Math.floor(Math.random() * arr.length)];
}
function randomNumFrom(min, max){
return Math.random()*(max - min) + min;
}
console.log(randomNumFrom(0, 10));
// Random Ball
function getRandomBall(){
var pos = randomArrayItem(['top', 'right', 'bottom', 'left']);
switch(pos){
case 'top':
return {
x: randomSidePos(can_w),
y: -R,
vx: getRandomSpeed('top')[0],
vy: getRandomSpeed('top')[1],
r: R,
alpha: 1,
phase: randomNumFrom(0, 10)
}
break;
case 'right':
return {
x: can_w + R,
y: randomSidePos(can_h),
vx: getRandomSpeed('right')[0],
vy: getRandomSpeed('right')[1],
r: R,
alpha: 1,
phase: randomNumFrom(0, 10)
}
break;
case 'bottom':
return {
x: randomSidePos(can_w),
y: can_h + R,
vx: getRandomSpeed('bottom')[0],
vy: getRandomSpeed('bottom')[1],
r: R,
alpha: 1,
phase: randomNumFrom(0, 10)
}
break;
case 'left':
return {
x: -R,
y: randomSidePos(can_h),
vx: getRandomSpeed('left')[0],
vy: getRandomSpeed('left')[1],
r: R,
alpha: 1,
phase: randomNumFrom(0, 10)
}
break;
}
}
function randomSidePos(length){
return Math.ceil(Math.random() * length);
}
// Draw Ball
function renderBalls(){
Array.prototype.forEach.call(balls, function(b){
if(!b.hasOwnProperty('type')){
ctx.fillStyle = 'rgba('+ball_color.r+','+ball_color.g+','+ball_color.b+','+b.alpha+')';
ctx.beginPath();
ctx.arc(b.x, b.y, R, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}
});
}
// Update balls
function updateBalls(){
var new_balls = [];
Array.prototype.forEach.call(balls, function(b){
b.x += b.vx;
b.y += b.vy;
if(b.x > -(50) && b.x < (can_w+50) && b.y > -(50) && b.y < (can_h+50)){
new_balls.push(b);
}
// alpha change
b.phase += alpha_f;
b.alpha = Math.abs(Math.cos(b.phase));
// console.log(b.alpha);
});
balls = new_balls.slice(0);
}
// loop alpha
function loopAlphaInf(){
}
// Draw lines
function renderLines(){
var fraction, alpha;
for (var i = 0; i < balls.length; i++) {
for (var j = i + 1; j < balls.length; j++) {
fraction = getDisOf(balls[i], balls[j]) / dis_limit;
if(fraction < 1){
alpha = (1 - fraction).toString();
ctx.strokeStyle = 'rgba(150,150,150,'+alpha+')';
ctx.lineWidth = link_line_width;
ctx.beginPath();
ctx.moveTo(balls[i].x, balls[i].y);
ctx.lineTo(balls[j].x, balls[j].y);
ctx.stroke();
ctx.closePath();
}
}
}
}
// calculate distance between two points
function getDisOf(b1, b2){
var delta_x = Math.abs(b1.x - b2.x),
delta_y = Math.abs(b1.y - b2.y);
return Math.sqrt(delta_x*delta_x + delta_y*delta_y);
}
// add balls if there a little balls
function addBallIfy(){
if(balls.length < 20){
balls.push(getRandomBall());
}
}
// Render
function render(){
ctx.clearRect(0, 0, can_w, can_h);
renderBalls();
renderLines();
updateBalls();
addBallIfy();
window.requestAnimationFrame(render);
}
// Init Balls
function initBalls(num){
for(var i = 1; i <= num; i++){
balls.push({
x: randomSidePos(can_w),
y: randomSidePos(can_h),
vx: getRandomSpeed('top')[0],
vy: getRandomSpeed('top')[1],
r: R,
alpha: 1,
phase: randomNumFrom(0, 10)
});
}
}
// Init Canvas
function initCanvas(){
canvas.setAttribute('width', window.innerWidth);
canvas.setAttribute('height', window.innerHeight);
can_w = parseInt(canvas.getAttribute('width'));
can_h = parseInt(canvas.getAttribute('height'));
}
window.addEventListener('resize', function(e){
console.log('Window Resize...');
initCanvas();
});
function goMovie(){
initCanvas();
initBalls(30);
window.requestAnimationFrame(render);
}
goMovie();
// Mouse effect
canvas.addEventListener('mouseenter', function(){
console.log('mouseenter');
mouse_in = true;
balls.push(mouse_ball);
});
canvas.addEventListener('mouseleave', function(){
console.log('mouseleave');
mouse_in = false;
var new_balls = [];
Array.prototype.forEach.call(balls, function(b){
if(!b.hasOwnProperty('type')){
new_balls.push(b);
}
});
balls = new_balls.slice(0);
});
canvas.addEventListener('mousemove', function(e){
var e = e || window.event;
mouse_ball.x = e.pageX;
mouse_ball.y = e.pageY;
// console.log(mouse_ball);
});</script>
<div class="title">在线客服 DEMO</div>
<div class="mini-im-m" id="miniIMiFrameBox">
<iframe id="miniIMiFrame" height="100%" width="100%" frameborder="0"></iframe>
</div>
<div class="mini-im">
<div>
<button onclick="location.href = 'http://kf.aissz.com:666/static/app/kefu_workbench.apk'">工作台(Android)</button>
<button onclick="location.href = 'http://kf.aissz.com:666/static/app/linux-0.0.1.AppImage'">工作台(linux)</button>
<button onclick="location.href = 'http://kf.aissz.com:666/static/app/mac-0.0.1.dmg'">工作台(mac)</button>
<button onclick="location.href = 'http://kf.aissz.com:666/static/app/win-0.0.1.exe'">工作台(window)</button>
<button onclick="goToClinet()">工作台(网页)</button>
<button style="color: red" onclick="location.href = 'http://kf.aissz.com:666/static/app/app-release.apk'">客户端demo(Android)</button>
</div>
<div>
<button onclick="hiddenHeadder()">隐藏Header</button>
<button onclick="showHeadder()">显示Header</button>
<button onclick="verticalScreen()">移动端坚屏</button>
<button onclick="horizontalScreen()">横屏PAD</button>
<button onclick="show()">展开PC客服</button>
<button onclick="hide()">收起PC客服</button>
</div>
</div>
<div class="fix-bottom">
<a title="去给作者Star" target="_blank" href="https://github.com/chenxianqi/kefu_server.git">
<svg class="github-logo" height="23" viewBox="0 0 16 16" version="1.1" width="23" aria-hidden="true"><path fill="#ffffff" fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
<span> Github</span>
</a>
</div>
</body>
</html>
<script src="./mini_im.js"></script>
<script>
/** ---------------------------PC端---------------------- **/
// 初始化PC 在线客服
var MiniIMPC = MiniIM.init({
url: "http://kf.aissz.com:666",
isShowPcBar: true,// 是否显示PC右下角的bar
platform: 3, // 平台
isHeader: 0, // 移动端是否显示header(PC端强制不显示)
isMobile: 0, // 是否是移动端
userAccount: 10256, // 会话用户账号
uid: 0, // 业务平台的ID
})
// 显示窗口
function show(){
MiniIMPC.showChatWindow()
}
// 隐藏窗口
function hide(){
MiniIMPC.hideChatWindow()
}
/** ---------------------------M端---------------------- **/
// 初始化M端 在线客服
var MiniIMMB = MiniIM.init({
url: "http://aissz.com:666",
platform: 5, // 平台
isHeader: 1, // 移动端是否显示header
isMobile: 1, // 是否是移动端
userAccount: 10255, // 会话用户账号
uid: 0, // 业务平台的ID
})
// iFrame
var miniIMiFrame = document.getElementById("miniIMiFrame")
miniIMiFrame.src = MiniIMMB.createLink()
var miniIMiFrameBox = document.getElementById("miniIMiFrameBox")
// 坚屏
function verticalScreen(){
miniIMiFrameBox.style.width = "350px"
miniIMiFrameBox.style.height = "550px"
}
// 横屏
function horizontalScreen(){
miniIMiFrameBox.style.height = "400px"
miniIMiFrameBox.style.width = "700px"
}
// 隐藏Header
function hiddenHeadder(){
miniIMiFrame.src = miniIMiFrame.src.replace("h=1", "h=0")
miniIMiFrame.reload()
}
// 显示Header
function showHeadder(){
miniIMiFrame.src = miniIMiFrame.src.replace("h=0", "h=1")
miniIMiFrame.reload()
}
// 去工作台
function goToClinet(){
window.open("http://aissz.com:666/admin/")
}
</script>
\ No newline at end of file
// eslint-disable-next-line no-unused-vars
var MiniIM = {
showChat: false,
miniImClientBar: null,
miniImclientIframe: null,
miniImclientIframeBox: null,
miniImClientFixed: null,
miniImClientNewMessageTag: null,
isShowMiniIM: false,
isFlashTag: true,
// 配置信息
// query 介绍
// h == header 0 不显示 1显示 默认值显示,PC端不显示
// m == mobile 0 不是移动端 1是移动端
// p == platform 平台ID(渠道)
// r == robot 0 当前为客服 1机器人
// a == account 当前提供对话服务的账号,客服账号,或机器人账号
// u == userAccount 会话用户账号
// uid == userId 业务平台的ID
options: {
isShowPcBar: true,// 是否显示PC右下角的bar
url: "localhost", // 项目地址
platform: 5, // 平台
isHeader: 1, // 移动端是否显示header
isMobile: 1, // 是否是移动端
isRobot: 1, // 当前是否机器人提供服务(默认即可)
account: 0, // 当前提供对话服务的账号,即客服账号,或机器人默认即可)
userAccount: 0, // 会话用户账号
uid: 0, // 业务平台的ID
isFlashTag: true, // 有新消息是否闪烁
},
// 初始化
init: function(options){
if(!options.url){
alert("URL不能为空!")
return
}
if(options.url != undefined) this.options.url = options.url
if(options.platform != undefined) this.options.platform = options.platform
if(options.isHeader != undefined) this.options.isHeader = options.isHeader
if(options.isMobile != undefined) this.options.isMobile = options.isMobile
if(options.isRobot != undefined) this.options.isRobot = options.isRobot
if(options.account != undefined) this.options.account = options.account
if(options.isShowPcBar != undefined) this.options.isShowPcBar = options.isShowPcBar
if(options.userAccount != undefined) this.options.userAccount = options.userAccount
if(options.uid != undefined) this.options.uid = options.uid
if(options.isFlashTag != undefined) this.isFlashTag= options.isFlashTag
var self = this
if(options.isMobile == 0){
this.createStyle()
this.createElement()
window.addEventListener('message',function(event){
if(event.data.clickCloseWindow) self.hideChatWindow()
if(event.data.newMessage > 0 && !this.isShowMiniIM){
self.flashTag()
}else{
if(this.flashTagInterval) clearInterval(this.flashTagInterval)
this.miniImClientNewMessageTag.innerHTML = "在线客服"
}
}, false)
}
return this
},
// flash tag
flashTagInterval: null,
flashTag(){
if(!this.isFlashTag) return
if(this.flashTagInterval) clearInterval(this.flashTagInterval)
var self = this
var timer = 0
this.flashTagInterval = setInterval(function(){
timer ++
if(timer%2 == 0){
self.miniImClientNewMessageTag.innerHTML = " 您有新消息"
}else{
self.miniImClientNewMessageTag.innerHTML = ""
}
}, 500)
},
// showChat
showChatWindow(){
if(this.flashTagInterval) clearInterval(this.flashTagInterval)
this.miniImClientNewMessageTag.innerHTML = "在线客服"
if(this.isShowMiniIM) return
this.isShowMiniIM = true
this.miniImClientBar.style.display = "none"
this.miniImclientIframeBox.style.display = "block"
// 加点动画效果
var right = -360;
this.miniImClientFixed.style.right = right + 'px'
var interval = setInterval(function(){
right = right + 5
this.miniImClientFixed.style.right = right + 'px'
if(right == 10) clearInterval(interval)
}, 1);
},
// hideChat
hideChatWindow(){
if(this.flashTagInterval) clearInterval(this.flashTagInterval)
this.miniImClientNewMessageTag.innerHTML = "在线客服"
if(!this.isShowMiniIM) return
this.isShowMiniIM = false
// 加点动画效果
var right = 10;
var interval = setInterval(function(){
right = right - 5
this.miniImClientFixed.style.right = right + 'px'
if(right <= -360){
this.miniImClientBar.style.display = "flex"
this.miniImClientBar.style.display = "flex"
this.miniImclientIframeBox.style.display = "none"
this.miniImClientFixed.style.right = 10 + 'px'
clearInterval(interval)
}
}, 1);
},
// 创建连接
createLink(){
var options = this.options
var host = options.url +'/'
var r = options.isRobot
var a = options.account
var m = options.isMobile
var h = options.isHeader
var p = options.platform
var u = options.userAccount
var uid = options.uid
var query = "?h=" + h + "&m=" + m + "&p=" + p + "&r=" + r + "&a=" + a + "&u=" + u + "&uid=" + uid
return host + query
},
// PC 创建一个element悬浮在右下角
createElement: function(){
var htmlString = '<div class="mini-im-client-iframe"id="miniImclientIframeBox"><iframe id="miniImclientIframe"height="500"width="360"frameborder="0"></iframe></div><div class="mini-im-client-bar"id="miniImClientBar"><div class="mini-im-client-bar-icon"><img src="http://qiniu.cmp520.com/kefu_icon_2000.png"alt=""></div><div class="mini-im-client-bar-title" id="miniImClientNewMessageTag">在线客服</div></div>'
var miniImClientFixedBox = document.createElement("div")
miniImClientFixedBox.setAttribute("class", "mini-im-client-fixed")
miniImClientFixedBox.setAttribute("id", "miniImClientFixed")
miniImClientFixedBox.innerHTML = htmlString
document.body.append(miniImClientFixedBox)
this.miniImClientFixed = document.getElementById("miniImClientFixed")
this.miniImClientNewMessageTag = document.getElementById("miniImClientNewMessageTag")
this.miniImClientBar = document.getElementById("miniImClientBar")
this.miniImclientIframe = document.getElementById("miniImclientIframe")
this.miniImclientIframeBox = document.getElementById("miniImclientIframeBox")
var link = this.createLink()
this.miniImclientIframe.src = link
var self = this
if(!this.options.isShowPcBar){
this.miniImClientFixed.style.background = "none";
this.miniImClientFixed.style.minHeight = "1px"
this.miniImClientBar.style.height = "1px"
this.miniImClientBar.style.opacity = 0;
}
this.options.isShowPcBar && this.miniImClientBar.addEventListener("click", function(){
self.showChatWindow()
}, false)
},
// PC 页面写入样式
createStyle: function(){
var style = '.mini-im-client-fixed{position:fixed;z-index: 9999999;bottom:10px;right:10px;min-width:200px;max-width:360px;min-height:40px}.mini-im-client-bar{width:200px;height:40px;border-radius:3px;overflow:hidden;display:flex;cursor:pointer}.mini-im-client-bar-icon{width:40px;height:40px;background-color:#1779ca;display:flex;justify-content:center;align-items:center}.mini-im-client-bar-icon img{width:30px;height:30px}.mini-im-client-bar-title{width:160px;height:40px;background-color:#1898fc;display:flex;justify-content:center;align-items:center;color:#fff;font-size:15px;text-align:center}.mini-im-client-iframe{width:360px;height:500px;box-shadow:1px 1px 8px 2px rgba(0, 0, 0, 0.16);display:none}'
var styleElement = document.createElement("style")
styleElement.innerHTML = style
document.head.append(styleElement)
}
}
\ No newline at end of file
// eslint-disable-next-line no-unused-vars
var MiniIM = {
showChat: false,
miniImClientBar: null,
miniImclientIframe: null,
miniImclientIframeBox: null,
miniImClientFixed: null,
miniImClientNewMessageTag: null,
isShowMiniIM: false,
isFlashTag: true,
// 配置信息
// query 介绍
// h == header 0 不显示 1显示 默认值显示,PC端不显示
// m == mobile 0 不是移动端 1是移动端
// p == platform 平台ID(渠道)
// r == robot 0 当前为客服 1机器人
// a == account 当前提供对话服务的账号,客服账号,或机器人账号
// u == userAccount 会话用户账号
// uid == userId 业务平台的ID
options: {
isShowPcBar: true,// 是否显示PC右下角的bar
url: "localhost", // 项目地址
platform: 5, // 平台
isHeader: 1, // 移动端是否显示header
isMobile: 1, // 是否是移动端
isRobot: 1, // 当前是否机器人提供服务(默认即可)
account: 0, // 当前提供对话服务的账号,即客服账号,或机器人默认即可)
userAccount: 0, // 会话用户账号
uid: 0, // 业务平台的ID
isFlashTag: true, // 有新消息是否闪烁
},
// 初始化
init: function(options){
if(!options.url){
alert("URL不能为空!")
return
}
if(options.url != undefined) this.options.url = options.url
if(options.platform != undefined) this.options.platform = options.platform
if(options.isHeader != undefined) this.options.isHeader = options.isHeader
if(options.isMobile != undefined) this.options.isMobile = options.isMobile
if(options.isRobot != undefined) this.options.isRobot = options.isRobot
if(options.account != undefined) this.options.account = options.account
if(options.isShowPcBar != undefined) this.options.isShowPcBar = options.isShowPcBar
if(options.userAccount != undefined) this.options.userAccount = options.userAccount
if(options.uid != undefined) this.options.uid = options.uid
if(options.isFlashTag != undefined) this.isFlashTag= options.isFlashTag
var self = this
if(options.isMobile == 0){
this.createStyle()
this.createElement()
window.addEventListener('message',function(event){
if(event.data.clickCloseWindow) self.hideChatWindow()
if(event.data.newMessage > 0 && !this.isShowMiniIM){
self.flashTag()
}else{
if(this.flashTagInterval) clearInterval(this.flashTagInterval)
this.miniImClientNewMessageTag.innerHTML = "在线客服"
}
}, false)
}
return this
},
// flash tag
flashTagInterval: null,
flashTag(){
if(!this.isFlashTag) return
if(this.flashTagInterval) clearInterval(this.flashTagInterval)
var self = this
var timer = 0
this.flashTagInterval = setInterval(function(){
timer ++
if(timer%2 == 0){
self.miniImClientNewMessageTag.innerHTML = " 您有新消息"
}else{
self.miniImClientNewMessageTag.innerHTML = ""
}
}, 500)
},
// showChat
showChatWindow(){
if(this.flashTagInterval) clearInterval(this.flashTagInterval)
this.miniImClientNewMessageTag.innerHTML = "在线客服"
if(this.isShowMiniIM) return
this.isShowMiniIM = true
this.miniImClientBar.style.display = "none"
this.miniImclientIframeBox.style.display = "block"
// 加点动画效果
var right = -360;
this.miniImClientFixed.style.right = right + 'px'
var interval = setInterval(function(){
right = right + 5
this.miniImClientFixed.style.right = right + 'px'
if(right == 10) clearInterval(interval)
}, 1);
},
// hideChat
hideChatWindow(){
if(this.flashTagInterval) clearInterval(this.flashTagInterval)
this.miniImClientNewMessageTag.innerHTML = "在线客服"
if(!this.isShowMiniIM) return
this.isShowMiniIM = false
// 加点动画效果
var right = 10;
var interval = setInterval(function(){
right = right - 5
this.miniImClientFixed.style.right = right + 'px'
if(right <= -360){
this.miniImClientBar.style.display = "flex"
this.miniImClientBar.style.display = "flex"
this.miniImclientIframeBox.style.display = "none"
this.miniImClientFixed.style.right = 10 + 'px'
clearInterval(interval)
}
}, 1);
},
// 创建连接
createLink(){
var options = this.options
var host = options.url +'/'
var r = options.isRobot
var a = options.account
var m = options.isMobile
var h = options.isHeader
var p = options.platform
var u = options.userAccount
var uid = options.uid
var query = "?h=" + h + "&m=" + m + "&p=" + p + "&r=" + r + "&a=" + a + "&u=" + u + "&uid=" + uid
return host + query
},
// PC 创建一个element悬浮在右下角
createElement: function(){
var htmlString = '<div class="mini-im-client-iframe"id="miniImclientIframeBox"><iframe id="miniImclientIframe"height="500"width="360"frameborder="0"></iframe></div><div class="mini-im-client-bar"id="miniImClientBar"><div class="mini-im-client-bar-icon"><img src="http://qiniu.cmp520.com/kefu_icon_2000.png"alt=""></div><div class="mini-im-client-bar-title" id="miniImClientNewMessageTag">在线客服</div></div>'
var miniImClientFixedBox = document.createElement("div")
miniImClientFixedBox.setAttribute("class", "mini-im-client-fixed")
miniImClientFixedBox.setAttribute("id", "miniImClientFixed")
miniImClientFixedBox.innerHTML = htmlString
document.body.append(miniImClientFixedBox)
this.miniImClientFixed = document.getElementById("miniImClientFixed")
this.miniImClientNewMessageTag = document.getElementById("miniImClientNewMessageTag")
this.miniImClientBar = document.getElementById("miniImClientBar")
this.miniImclientIframe = document.getElementById("miniImclientIframe")
this.miniImclientIframeBox = document.getElementById("miniImclientIframeBox")
var link = this.createLink()
this.miniImclientIframe.src = link
var self = this
if(!this.options.isShowPcBar){
this.miniImClientFixed.style.background = "none";
this.miniImClientFixed.style.minHeight = "1px"
this.miniImClientBar.style.height = "1px"
this.miniImClientBar.style.opacity = 0;
}
this.options.isShowPcBar && this.miniImClientBar.addEventListener("click", function(){
self.showChatWindow()
}, false)
},
// PC 页面写入样式
createStyle: function(){
var style = '.mini-im-client-fixed{position:fixed;z-index: 9999999;bottom:10px;right:10px;min-width:200px;max-width:360px;min-height:40px}.mini-im-client-bar{width:200px;height:40px;border-radius:3px;overflow:hidden;display:flex;cursor:pointer}.mini-im-client-bar-icon{width:40px;height:40px;background-color:#1779ca;display:flex;justify-content:center;align-items:center}.mini-im-client-bar-icon img{width:30px;height:30px}.mini-im-client-bar-title{width:160px;height:40px;background-color:#1898fc;display:flex;justify-content:center;align-items:center;color:#fff;font-size:15px;text-align:center}.mini-im-client-iframe{width:360px;height:500px;box-shadow:1px 1px 8px 2px rgba(0, 0, 0, 0.16);display:none}'
var styleElement = document.createElement("style")
styleElement.innerHTML = style
document.head.append(styleElement)
}
}
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Mini IM UI</title>
<style>
.mini-im-client-fixed{
position: fixed;
bottom: 10px;
right: 10px;
min-width: 200px;
max-width: 360px;
min-height: 40px;
}
.mini-im-client-bar{
width: 200px;
height: 40px;
border-radius: 3px;
overflow: hidden;
display: flex;
cursor: pointer;
}
.mini-im-client-bar-icon{
width: 40px;
height: 40px;
background-color: #1779ca;
display: flex;
justify-content: center;
align-items: center;
}
.mini-im-client-bar-icon img{
width: 30px;
height: 30px;
}
.mini-im-client-bar-title{
width: 160px;
height: 40px;
background-color: #1898fc;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 15px;
text-align: center;
}
.mini-im-client-iframe{
width: 360px;
height: 500px;
box-shadow: 1px 1px 8px 2px rgba(204, 204, 204, 0.61);
display: none;
}
</style>
</head>
<body>
<!-- PC Bar UI -->
<div class="mini-im-client-fixed" id="miniImClientFixed">
<div class="mini-im-client-iframe" id="miniImclientIframeBox">
<iframe id="miniImclientIframe" height="500" width="360" frameborder="0"></iframe>
</div>
<div class="mini-im-client-bar" id="miniImClientBar">
<div class="mini-im-client-bar-icon">
<img src="http://qiniu.cmp520.com/kefu_icon_2000.png" alt="">
</div>
<div class="mini-im-client-bar-title" id="miniImClientNewMessageTag">
在线客服
</div>
</div>
</div>
</body>
</html>
\ No newline at end of file
{
"name": "kefu_client",
"version": "0.0.1",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.19.0",
"better-scroll": "^1.15.2",
"core-js": "^2.6.5",
"mint-ui": "^2.2.13",
"moment": "^2.24.0",
"qiniu-js": "^2.5.5",
"vue": "^2.6.10",
"vue-photo-preview": "git+https://github.com/chenxianqi/vue-photo-preview.git",
"webpack": "^4.41.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.9.0",
"@vue/cli-plugin-eslint": "^3.9.0",
"@vue/cli-service": "^3.9.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"eslint-plugin-vue": "^5.0.0",
"vue-template-compiler": "^2.6.10",
"html-webpack-plugin": "^4.0.0-beta.8",
"mini-css-extract-plugin": "^0.8.0",
"vue-loader": "^15.7.1",
"webpack-cli": "^3.3.8"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}
var moment = require('moment');
// eslint-disable-next-line no-undef
var Helps = {};
Helps.install = function (Vue, options) {
Vue.prototype.$myMethod = function(){
console.log(options)
}
// 格式化日期
Vue.prototype.$formatUnixDate = function(unix, format){
return moment(parseInt(unix + '000')).format(format)
}
// 格式化日期(相对日期)
Vue.prototype.$formatFromNowDate = function(unix, format = "YYYY-MM-DD HH:mm"){
if(moment().format("YYYYMMDD") == moment(parseInt(unix + '000')).format("YYYYMMDD")){
return "今天 " + moment(parseInt(unix + '000')).format("HH:mm")
}
return moment(parseInt(unix + '000')).format(format)
}
Vue.prototype.$robotNickname = function(id){
var nickname
var robots = this.$store.getters.robots
for(let i = 0; i< robots.length; i++){
if(robots[i].id == id){
nickname = robots[i].nickname
}
}
return nickname
}
// 判断是否是全面屏
Vue.prototype.$judgeBigScreen = function(){
let yes = false;
const rate = window.screen.height / window.screen.width;
let limit = window.screen.height == window.screen.availHeight ? 1.8 : 1.65;
if (rate > limit) yes = true;
return yes;
}
}
export default Helps;
\ No newline at end of file
import axios from "axios";
import { Toast } from 'mint-ui';
var MimcPlugin = {};
MimcPlugin.install = function (Vue, options) {
console.log(options)
// 获取单个平台数据
Vue.MimcInstance = Vue.prototype.$mimcInstance = {
user: null,
robot: null,
platform: 5,
fetchMIMCTokenResult: null,
_receiveP2PMsgCallback: null,
_statusChangeCallback: null,
_serverAckCallback: null,
_disconnectCallback: null,
// 初始化
init(request, callback){
this.platform = request.platform
this.fetchMIMCToken(request, callback)
this.getRobot()
},
// 获取本地已经登录过的User
getLocalCacheUser(uid){
const userString = localStorage.getItem("miniImAppUser_" + uid)
if(userString) return JSON.parse(userString)
return null
},
// 获取token
// request 登录参数
// 登录回调 callback bool 是否成功
fetchMIMCToken(request, callback){
axios.post('/public/register', request)
.then(response => {
this.fetchMIMCTokenResult = response.data.data.token
localStorage.setItem("miniImAppUser_" + response.data.data.user.id, JSON.stringify(response.data.data.user))
console.log("MIMC初始化成功")
if(callback) callback(response.data.data.user)
})
.catch((error)=>{
if(callback) callback(null)
console.log(error.response)
Toast({
message: error.response.data.message
})
})
},
// 获取机器人
getRobot(){
axios.get('/public/robot/1')
.then(response => {
this.robot = response.data.data
})
.catch((error)=>{
Toast({
message: "mimc初始化失败,请刷新重试" + error.response.data.message
})
})
},
// pushMessage
pushMessage(payload){
axios.post('/public/message/push', {
"msgType": "NORMAL_MSG",
"payload": payload
})
.then(response => {
console.log(response.data)
if(response.data['code'] != 200){
setTimeout(()=> this.pushMessage(payload), 300)
}
})
.catch(()=>{
setTimeout(()=> this.pushMessage(payload), 300)
})
},
// 登录
login(callback){
try{
if(this.user) return
var fetchMIMCTokenResult = this.fetchMIMCTokenResult
// eslint-disable-next-line no-undef
this.user = new MIMCUser(fetchMIMCTokenResult.data.appId, fetchMIMCTokenResult.data.appAccount, "666");
this.user.registerP2PMsgHandler((message)=>{
var msg = JSON.parse(window.Base64.decode(message.getPayload()));
if(this._receiveP2PMsgCallback) this._receiveP2PMsgCallback(msg)
});
this.user.registerFetchToken(() => {
return fetchMIMCTokenResult;
});
this.user.registerStatusChange((bindResult, errType, errReason, errDesc)=>{
if(this._statusChangeCallback) this._statusChangeCallback(bindResult, errType, errReason, errDesc)
});
this.user.registerServerAckHandler((packetId, sequence, timeStamp, errMsg)=>{
if(this._serverAckCallback) this._serverAckCallback(packetId, sequence, timeStamp, errMsg)
});
this.user.registerDisconnHandler(() => {
if(this._disconnectCallback) this._disconnectCallback()
});
this.user.login();
window.mimcInstance = this
if(callback) callback()
console.log("MIMC登录成功")
}catch(e){
console.log("MIMC登录失败")
// 重新尝试
setTimeout(()=>{
this.login()
}, 1000)
}
},
// 退出
logout(){
if(this.user){
this.user.logout()
this.user = null
}
},
// 注册监听器
addEventListener(type, callback){
switch(type){
case "receiveP2PMsg":
this._receiveP2PMsgCallback = callback
break
case "statusChange":
this._statusChangeCallback = callback
break
case "serverAck":
this._serverAckCallback = callback
break
case "disconnect":
this._disconnectCallback = callback
break
}
},
// 发送消息
sendMessage(type, toAccount, payload = ""){
if(!this.user){
Toast({
message: "服务异常,请刷新重试!"
})
return
}
var messageJson = {
"from_account": parseInt(this.fetchMIMCTokenResult.data.appAccount),
"to_account": parseInt(toAccount),
"biz_type": type,
"version": "0",
"timestamp": parseInt((new Date().getTime() + " ").substr(0, 10)),
"key": new Date().getTime(),
"read": 0,
"platform": this.platform,
"transfer_account": 0,
"payload": payload + ''
}
var jsonBase64Msg = window.Base64.encode(JSON.stringify(messageJson))
// 过滤不入库
if(!(type == "contacts" || type == "pong" || type == "welcome" || type == "handshake" || type == "search_knowledge")){
// 发送给机器人中专入库
// const intoMessageJson = {
// "biz_type": "into",
// "payload": jsonBase64Msg
// }
// const intoJsonBase64Msg = window.Base64.encode(JSON.stringify(intoMessageJson))
// this.user.sendMessage(this.robot.id.toString(), intoJsonBase64Msg);
// 消息入库
this.pushMessage(window.Base64.encode(jsonBase64Msg))
}
setTimeout(()=>{
// 发送给对方
this.user.sendMessage(toAccount.toString(), jsonBase64Msg);
// console.log("发送给对方", jsonBase64Msg)
},150)
return messageJson
},
// 创建本地消息
createLocalMessage(type, toAccount, payload = "", transferAccount = 0){
const messageJson = {
"from_account": parseInt(this.fetchMIMCTokenResult.data.appAccount),
"to_account": parseInt(toAccount),
"biz_type": type,
"version": "0",
"platform": this.platform,
"timestamp": parseInt((new Date().getTime() + " ").substr(0, 10)),
"key": new Date().getTime(),
"read": 0,
"transfer_account": parseInt(transferAccount),
"payload": payload + ''
}
return messageJson
}
}
}
export default MimcPlugin;
\ No newline at end of file
module.exports = {
plugins: {
autoprefixer: {}
}
}
(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory(global):typeof define==="function"&&define.amd?define(factory):factory(global)})(typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:this,function(global){"use strict";global=global||{};var _Base64=global.Base64;var version="2.5.1";var buffer;if(typeof module!=="undefined"&&module.exports){try{buffer=eval("require('buffer').Buffer")}catch(err){buffer=undefined}}var b64chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var b64tab=function(bin){var t={};for(var i=0,l=bin.length;i<l;i++)t[bin.charAt(i)]=i;return t}(b64chars);var fromCharCode=String.fromCharCode;var cb_utob=function(c){if(c.length<2){var cc=c.charCodeAt(0);return cc<128?c:cc<2048?fromCharCode(192|cc>>>6)+fromCharCode(128|cc&63):fromCharCode(224|cc>>>12&15)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}else{var cc=65536+(c.charCodeAt(0)-55296)*1024+(c.charCodeAt(1)-56320);return fromCharCode(240|cc>>>18&7)+fromCharCode(128|cc>>>12&63)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}};var re_utob=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;var utob=function(u){return u.replace(re_utob,cb_utob)};var cb_encode=function(ccc){var padlen=[0,2,1][ccc.length%3],ord=ccc.charCodeAt(0)<<16|(ccc.length>1?ccc.charCodeAt(1):0)<<8|(ccc.length>2?ccc.charCodeAt(2):0),chars=[b64chars.charAt(ord>>>18),b64chars.charAt(ord>>>12&63),padlen>=2?"=":b64chars.charAt(ord>>>6&63),padlen>=1?"=":b64chars.charAt(ord&63)];return chars.join("")};var btoa=global.btoa?function(b){return global.btoa(b)}:function(b){return b.replace(/[\s\S]{1,3}/g,cb_encode)};var _encode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(u){return(u.constructor===buffer.constructor?u:buffer.from(u)).toString("base64")}:function(u){return(u.constructor===buffer.constructor?u:new buffer(u)).toString("base64")}:function(u){return btoa(utob(u))};var encode=function(u,urisafe){return!urisafe?_encode(String(u)):_encode(String(u)).replace(/[+\/]/g,function(m0){return m0=="+"?"-":"_"}).replace(/=/g,"")};var encodeURI=function(u){return encode(u,true)};var re_btou=new RegExp(["[À-ß][€-¿]","[à-ï][€-¿]{2}","[ð-÷][€-¿]{3}"].join("|"),"g");var cb_btou=function(cccc){switch(cccc.length){case 4:var cp=(7&cccc.charCodeAt(0))<<18|(63&cccc.charCodeAt(1))<<12|(63&cccc.charCodeAt(2))<<6|63&cccc.charCodeAt(3),offset=cp-65536;return fromCharCode((offset>>>10)+55296)+fromCharCode((offset&1023)+56320);case 3:return fromCharCode((15&cccc.charCodeAt(0))<<12|(63&cccc.charCodeAt(1))<<6|63&cccc.charCodeAt(2));default:return fromCharCode((31&cccc.charCodeAt(0))<<6|63&cccc.charCodeAt(1))}};var btou=function(b){return b.replace(re_btou,cb_btou)};var cb_decode=function(cccc){var len=cccc.length,padlen=len%4,n=(len>0?b64tab[cccc.charAt(0)]<<18:0)|(len>1?b64tab[cccc.charAt(1)]<<12:0)|(len>2?b64tab[cccc.charAt(2)]<<6:0)|(len>3?b64tab[cccc.charAt(3)]:0),chars=[fromCharCode(n>>>16),fromCharCode(n>>>8&255),fromCharCode(n&255)];chars.length-=[0,0,2,1][padlen];return chars.join("")};var _atob=global.atob?function(a){return global.atob(a)}:function(a){return a.replace(/\S{1,4}/g,cb_decode)};var atob=function(a){return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g,""))};var _decode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(a){return(a.constructor===buffer.constructor?a:buffer.from(a,"base64")).toString()}:function(a){return(a.constructor===buffer.constructor?a:new buffer(a,"base64")).toString()}:function(a){return btou(_atob(a))};var decode=function(a){return _decode(String(a).replace(/[-_]/g,function(m0){return m0=="-"?"+":"/"}).replace(/[^A-Za-z0-9\+\/]/g,""))};var noConflict=function(){var Base64=global.Base64;global.Base64=_Base64;return Base64};global.Base64={VERSION:version,atob:atob,btoa:btoa,fromBase64:decode,toBase64:encode,utob:utob,encode:encode,encodeURI:encodeURI,btou:btou,decode:decode,noConflict:noConflict,__buffer__:buffer};if(typeof Object.defineProperty==="function"){var noEnum=function(v){return{value:v,enumerable:false,writable:true,configurable:true}};global.Base64.extendString=function(){Object.defineProperty(String.prototype,"fromBase64",noEnum(function(){return decode(this)}));Object.defineProperty(String.prototype,"toBase64",noEnum(function(urisafe){return encode(this,urisafe)}));Object.defineProperty(String.prototype,"toBase64URI",noEnum(function(){return encode(this,true)}))}}if(global["Meteor"]){Base64=global.Base64}if(typeof module!=="undefined"&&module.exports){module.exports.Base64=global.Base64}else if(typeof define==="function"&&define.amd){define([],function(){return global.Base64})}return{Base64:global.Base64}});
\ No newline at end of file
No preview for this file type
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
<meta content="yes" name="apple-mobile-web-app-capable">
<meta content="black" name="apple-mobile-web-app-status-bar-style">
<meta content="telephone=no" name="format-detection">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script type="text/javascript" src="<%= BASE_URL %>mimc-min_1_0_2.js"></script>
<script type="text/javascript" src="<%= BASE_URL %>base64.min.js"></script>
<title>在线客服</title>
<style>
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, dl, dt, dd, ul, ol, li, pre, form, fieldset, legend, button, input, textarea, th, td { margin:0; padding:0; }
body, button, input, select, textarea { font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; }
h1, h2, h3, h4, h5, h6{ font-size:100%; }
address, cite, dfn, em, var { font-style:normal; }
code, kbd, pre, samp { font-family:couriernew, courier, monospace; }
small{ font-size:12px; }
ul, ol { list-style:none; }
a { text-decoration:none;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
-webkit-user-select: none;
-moz-user-focus: none;
-moz-user-select: none;
}
a:hover { text-decoration:underline; }
sup { vertical-align:text-top; }
sub{ vertical-align:text-bottom; }
legend { color:#000; }
fieldset, img { border:0; }
button, input, select, textarea { font-size:100%; outline: none;}
table { border-collapse:collapse; border-spacing:0; }
input{
border:0;
outline: none;
}
html{
overflow: hidden;
height: 100vh;
}
body{
height: 100vh;
-webkit-overflow-scrolling:touch;
background-color: #f3f3f3;
}
.lx-load-box{
width: 2rem !important;
height: 2rem !important;
top:0 !important;
min-height: inherit!important;
left:0 !important; right:0 !important; bottom:0 !important; margin: auto !important;
}
</style>
</head>
<body>
<noscript>
<strong>We're sorry but m doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
\ No newline at end of file
This diff could not be displayed because it is too large.
const emojiData = ["😀","😁","😂","🤣","😃","😄","😅","😆","😉","😊","😋","😎","😍","😘","😗","😙","😚","🙂","🤗","🤩","🤔","🤨","😐","😑","😶","🙄","😏","😣","😥","😮","🤐","😯","😪","😫","😴","😌","😛","😜","😝","🤤","😒","😓","😔","😕","🙃","🤑","😲","🙁","😖","😞","😟","😤","😢","😭","😦","😧","😨","😩","🤯","😬","😰","😱","😳","🤪","😵","😡","😠","🤬","😷","🤒","🤕","🤢","🤮","🤧","😇","🤠","🤡","🤥","🤫","🤭","🧐","🤓","😈","👿","👹","👺","💀","👻","👽","🤖","💩","😺","😸","😹","😻","😼","😽","🙀","😿","😾","🤲","👐","🙌","👏","🤝","👍","👎","👊","✊","🤛","🤜","🤞","✌️","🤟","🤘","👌","👈","👉","👆","👇","☝️","✋","🤚","🖐","🖖","👋","🤙","💪","🖕","✍️","🙏"]
exports.emojiData = emojiData
\ No newline at end of file
export default {
months: '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split('_'),
monthsShort: '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'),
weekdays: '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'),
weekdaysShort: '周日_周一_周二_周三_周四_周五_周六'.split('_'),
weekdaysMin: '日_一_二_三_四_五_六'.split('_'),
longDateFormat: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'YYYY-MM-DD',
LL: 'YYYY年MM月DD日',
LLL: 'YYYY年MM月DD日Ah点mm分',
LLLL: 'YYYY年MM月DD日ddddAh点mm分',
l: 'YYYY-M-D',
ll: 'YYYY年M月D日',
lll: 'YYYY年M月D日 HH:mm',
llll: 'YYYY年M月D日dddd HH:mm'
},
meridiemParse: /凌晨|早上|上午|中午|下午|晚上/,
meridiemHour: function (hour, meridiem) {
if (hour === 12) {
hour = 0;
}
if (meridiem === '凌晨' || meridiem === '早上' ||
meridiem === '上午') {
return hour;
} else if (meridiem === '下午' || meridiem === '晚上') {
return hour + 12;
} else {
// '中午'
return hour >= 11 ? hour : hour + 12;
}
},
meridiem: function (hour, minute) {
const hm = hour * 100 + minute;
if (hm < 600) {
return '凌晨';
} else if (hm < 900) {
return '早上';
} else if (hm < 1130) {
return '上午';
} else if (hm < 1230) {
return '中午';
} else if (hm < 1800) {
return '下午';
} else {
return '晚上';
}
},
calendar: {
sameDay: '[今天]LT',
nextDay: '[明天]LT',
nextWeek: '[下]ddddLT',
lastDay: '[昨天]LT',
lastWeek: '[上]ddddLT',
sameElse: 'L'
},
dayOfMonthOrdinalParse: /\d{1,2}(日|月|周)/,
ordinal: function (number, period) {
switch (period) {
case 'd':
case 'D':
case 'DDD':
return number + '日';
case 'M':
return number + '月';
case 'w':
case 'W':
return number + '周';
default:
return number;
}
},
relativeTime: {
future: '%s内',
past: '%s前',
s: '几秒',
ss: '%d秒',
m: '1分钟',
mm: '%d分钟',
h: '1小时',
hh: '%d小时',
d: '1天',
dd: '%d天',
M: '1个月',
MM: '%d个月',
y: '1年',
yy: '%d年'
},
week: {
dow: 1,
doy: 4
}
}
\ No newline at end of file
<template>
<div class="mini-im-container" :class="{'mini-im-pc-container': !isMobile, 'mini-im-container-no-pto': !isShowHeader}">
<span class="input-ing" v-show="isMobile && (isInputPongIng && !isShowHeader)">{{inputPongIngString}}</span>
<mt-header v-if="isShowHeader" fixed :title="isInputPongIng ? inputPongIngString : '在线客服'">
<div slot="left">
<mt-button @click="back" icon="back"></mt-button>
</div>
<mt-button @click="headRightBtn" slot="right">
<img title="人工客服" v-if="!isArtificial" src="http://qiniu.cmp520.com/kefu_icon_2000.png" alt="">
<span v-else>结束会话</span>
</mt-button>
</mt-header>
<div v-if="!isMobile" class="mini-im-pc-header">
<div class="title">
<img src="http://qiniu.cmp520.com/kefu_icon_2000.png" alt="">
<span>在线客服</span>
</div>
<span v-show="isInputPongIng">{{inputPongIngString}}</span>
<div class="right">
<img title="人工客服" @click="headRightBtn" v-if="!isArtificial" src="http://qiniu.cmp520.com/kefu_icon_2000.png" alt="">
<span v-if="isArtificial" @click="headRightBtn">结束会话</span>
<div @click="clickCloseWindow" class="close-btn"></div>
</div>
</div>
<div class="mini-im-body" ref="miniImBody">
<ul class="mini-im-chat-list">
<li class="message-loading" v-if="isLoadMorLoading">
<mt-spinner color="#26a2ff" :size="20" type="triple-bounce"></mt-spinner>
</li>
<li :key="index" v-for="(item,index) in viewMessage">
<template v-if="item.isShowDate">
<!-- 日期 -->
<div class="mini-im-chat-item">
<div class="chat-content">
<div class="chat-body">
<template>
<div class="system">
<div class="content">
<span>{{$formatFromNowDate(item.timestamp, "YYYY年MM月DD日 HH:mm")}}</span>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<div class="mini-im-chat-item" :class="{'self': item.from_account == userInfo.id}">
<!-- 头像 -->
<div class="chat-avatar" v-if="isShowInfo(item.biz_type)">
<img :src="item.avatar" >
</div>
<!-- 消息主体 -->
<div class="chat-content">
<div class="chat-body">
<!-- 撤回按钮 -->
<template v-if="item.isShowCancel">
<span @click="()=>cancelMessage(item.key)" v-if="item.from_account == userInfo.id && isShowInfo(item.biz_type)" class="cancel-btn">撤回</span>
</template>
<!-- 文本消息 -->
<template v-if="item.biz_type == 'text' || item.biz_type == 'welcome'">
<div class="text">
<span v-html="item.payload.replace(/\n/ig, '<br />')"></span>
</div>
</template>
<!-- 图片消息 -->
<template v-if="item.biz_type == 'photo'">
<div class="photo">
<span v-if="item.percent && item.percent != 100">上传中{{item.percent}}%</span>
<img v-if="isMobile" :src="item.payload" preview="1" />
<img v-else @click="clickPhoto(item.payload)" :src="item.payload" />
</div>
</template>
<!-- 知识消息 -->
<template v-if="item.biz_type == 'knowledge'">
<div class="knowledge">
<div class="title">以下是您关心的相关问题?</div>
<a @click="()=>sendKnowledgeMessage(item.title)" href="javascript:void(0);" :key="index" v-for="(item, index) in JSON.parse(item.payload)">
<span>{{item.title}}</span>
</a>
<a @click="headRightBtn">• 以上都不是?我要找人工</a>
</div>
</template>
<!-- 会话结束 -->
<template v-if="item.biz_type == 'end'">
<div class="system">
<div class="content">
<span>本次会话结束,感谢您的支持!</span>
</div>
</div>
</template>
<!-- 会话超时-->
<template v-if="item.biz_type == 'timeout'">
<div class="system">
<div class="content">
<span>{{item.payload}}</span>
</div>
</div>
</template>
<!-- 系统消息-->
<template v-if="item.biz_type == 'system'">
<div class="system">
<div class="content">
<span v-html="item.payload"></span>
</div>
</div>
</template>
<!-- 撤回消息 -->
<template v-if="item.biz_type == 'cancel'">
<div class="system">
<div class="content">
<span v-if="item.from_account == userInfo.id">您撤回了一条消息</span>
<span v-else>对方撤回了一条消息</span>
</div>
</div>
</template>
<!-- 客服转接 -->
<template v-if="item.biz_type == 'transfer'">
<div class="system">
<div class="content">
<span>已为您转接{{item.transfer_account}}号客服</span>
</div>
</div>
</template>
</div>
</div>
</div>
</li>
</ul>
<div class="no-network" v-if="isNotNetWork">
<img src="./assets/network.png" alt="">
<span>网络连接已断开,请重新加载尝试~</span>
<button @click="resetLoad">重新加载</button>
</div>
</div>
<div class="mini-im-loading" v-if="isLoading">
<mt-spinner type="triple-bounce" color="#26a2ff"></mt-spinner>
</div>
<div class="mini-im-emoji" v-show="showEmoji">
<div class="mini-im-emoji-content">
<span @click="()=>clickEmoji(item)" v-for="(item, index) in emojis" :key="index">{{item}}</span>
</div>
</div>
<div class="mini-im-knowledge" v-show="handshakeKeywordList.length > 0">
<div class="mask" @click="handshakeKeywordList = []"></div>
<span>以下是您关心的相关问题?</span>
<ul>
<li :data="item.title" class="sendKnowledgeMessage" @click="!isIOS && sendKnowledgeMessage(item.title)" v-for="(item, index) in handshakeKeywordList" :key="index">• {{item.title}}</li>
</ul>
</div>
<div class="mini-im-tabbar-input">
<span class="photo-btn">
<img src="./assets/photo_btn.png" alt="">
<input onClick="this.value = null" @change="sendPhotoMessageEvent" type="file" accept="image/*" />
</span>
<span class="expression-btn" @click="showEmoji = !showEmoji">
<img src="./assets/expression.png" alt="">
</span>
<span v-show="isMobile && !isShowHeader" @click="headRightBtn" class="serverci" :class="{'on': !isArtificial}">
<img title="人工客服" v-if="!isArtificial" src="http://qiniu.cmp520.com/bfbfbf.png" alt="">
<span v-else>结束会话</span>
</span>
<textarea
ref="textarea"
maxlength="200"
@keyup.exact="keyUpEvent"
@keyup.enter.13.shift="enterShift"
@keyup.enter.exact="enterSendMessage"
@submit="sendTextMessage"
@focus="chatInputFocus"
@blur="chatInputBlur"
placeholder="请用一句话描述您的问题~"
v-model="chatValue"
style="vertical-align:top;outline:none;"
></textarea>
<button ref="sendButton" type="button" class="mini-input-send">发送</button>
</div>
</div>
</template>
<script>
import axios from 'axios'
import { Toast,MessageBox } from 'mint-ui';
import * as qiniu from 'qiniu-js'
var emojiService = require("../resource/emoji")
import BScroll from 'better-scroll'
export default {
name: 'app',
data(){
return {
messages: [],
isLoading: true,
isNotNetWork: false,
userLocal: "", // 用户地理位置
isFirstGetMessage: true, // 第一次获取本地消息
platform: 5, // 平台(渠道)
uid: null, // 业务平台的ID
chatValue: "", // 发送消息的内容
emojis: emojiService.emojiData, // emoji数据
showEmoji: false, // 是否显示emoji面板
userInfo: {}, // 用户信息
userAccount: null, // 用户账号
companyInfo: null, // 公司信息
uploadToken: null, // 上传token
isArtificial: false, // 是否是人工服务
artificialAccount: null, // 客服账号ID
robotInfo: null, // 机器人信息
robotAccount: null, // 机器人账号ID
isLoadMorEnd: false,
isUserSendLongTimeSystemMessage: false, // 本次用户会话超时了是否发送了结束前提示语
isAdminSendLongTimeSystemMessage: false, // 本次客服会话超时了是否发送了结束前提示语
isInputPongIng: false,
isLoadMorLoading: false,
isSendPong: false,
qiniuObservable: null,
inputPongIngString: "对方正在输入...",
scroll: null, // 滚动控制器
isShowHeader: true, // 是否显示header
isMobile: true, // 是否是移动端
handshakeKeywordList: [], // 检索关键词
searchHandshakeTimer: null
}
},
created(){
// run
this.getLocal()
this.run()
},
computed: {
account(){
return this.isArtificial ? this.artificialAccount : this.robotAccount
},
isIOS(){
return !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
},
isSafari(){
return navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") < 1
},
isJudgeBigScreen(){
return this.$judgeBigScreen();
},
viewMessage(){
var messages = this.messages
for(let i = 0; i< messages.length; i++){
if(i == 0) messages[i].isShowDate = true
if(i < messages.length-1){
messages[i+1].isShowDate = false
if(messages[i+1].timestamp-120 > messages[i].timestamp) messages[i+1].isShowDate = true
}
}
return messages
}
},
mounted(){
// url query 介绍
// h == header 0 不显示 1显示 默认值显示,PC端不显示
// m == mobile 0 不是移动端 1是移动端
// p == platform 平台ID(渠道)
// r == robot 0 当前为为客服 1机器人(对应的账号为a)
// a == account 当前提供对话服务的账号,即客服账号,或机器人
// u == userAccount 会话用户账号
// uid == userId 业务平台的ID
// c = 1 清除本地缓存
var query = this.queryToJson(location.search)
if(query && query.c) localStorage.clear()
// 获取本地缓存
var urlQuery = this.queryToJson(localStorage.getItem("urlQuery"))
if(urlQuery){
query = Object.assign({}, urlQuery, query)
query.u = urlQuery.u
}
if(query){
if(query.h == "0") this.isShowHeader = false
if(query.m == "0"){
this.isMobile = false
this.isShowHeader = false
}
if(query.u) this.userAccount = parseInt(query.u)
if(query.p) this.platform = parseInt(query.p)
if(query.uid) this.uid = parseInt(query.uid)
if(query.r == "0"){
this.isArtificial = true
this.artificialAccount = parseInt(query.a)
}else{
this.robotAccount = parseInt(query.a)
}
}
var isArtificial = localStorage.getItem("isArtificial_" + this.userAccount)
var artificialAccount = localStorage.getItem("artificialAccount_" + this.userAccount)
if(isArtificial == "true"){
this.isArtificial = true
this.artificialAccount = parseInt(artificialAccount)
}
setTimeout(() =>{
this.isLoading = false
this.scroll = new BScroll(this.$refs.miniImBody, {
click: true,
tab: true,
scrollY: true,
scrollbar: true,
bounceTime: 400,
preventDefaultException: {className:/(^|\s)text(\s|$)/},
mouseWheel: true
})
this.scroll.on('touchEnd', (pos) => {
if (pos.y > 30) {
this.loadMorData()
}
})
// 监听发送按钮触摸事件
this.addSendButtonTouchEventListener()
this.createLinkQuery()
this.scrollIntoBottom()
}, 500)
// 判断是否被踢出对话
this.onCheckIsOutSession()
// 粘贴事件
document.addEventListener("paste", this.inputPaste, false)
},
beforeDestroy(){
this.toggleWindow(this.userAccount, 0)
},
methods: {
// run
run(){
// 发起请求
this.getAllhttp()
// 上报活动时间
this.upLastActivity()
// 监听消息
this.$mimcInstance.addEventListener("receiveP2PMsg", this.receiveP2PMsg)
// 监听连接断开
this.$mimcInstance.addEventListener("disconnect", () => {
console.log("链接断开!")
this.isNotNetWork = true;
})
// 状态发生变化
this.$mimcInstance.addEventListener("statusChange", (bindResult, errType, errReason, errDesc) => {
console.log("状态发生变化", bindResult, errType, errReason, errDesc)
})
// 发送消息服务器ack
this.$mimcInstance.addEventListener("serverAck", (packetId, sequence, timeStamp, errMsg) => {
console.log("发送消息服务器ack", packetId, sequence, timeStamp, errMsg)
localStorage.setItem("userLastCallBackMessageTime_" + this.userAccount, Date.now())
this.isUserSendLongTimeSystemMessage = false
})
// 计算用户是否长时间未回复弹出给出提示
this.onCheckIsloogTimeNotCallBack()
},
// 根据IP获取用户地理位置
getLocal(){
var APPKey = "" // 高德地图web应用key
axios.get("https://restapi.amap.com/v3/ip?key=" + APPKey)
.then(response => {
if(response.data.province){
console.log(response.data.province + response.data.city)
this.userLocal = response.data.province + response.data.city
}
}).catch((error)=>{
console.error(error)
})
},
// 初始化IM
initMimc(){
const IM = this.$mimcInstance
const user = IM.getLocalCacheUser(this.userAccount)
this.userInfo = user
let userAccount = this.userAccount ? this.userAccount : user ? user.id : 0
IM.init({
type: 0, // 默认0
address: this.userLocal,
uid: this.uid, // 预留字段扩展自己平台业务
platform: this.platform, // 渠道(平台)
account_id: userAccount // 用户ID
// 初始化完成这里返回一个user
}, (user) => {
if(!user){
setTimeout(()=> this.initMimc(), 1000)
}else{
this.userInfo = user
this.userAccount = user.id
// 清除未读消息
this.cleanRead(user.id)
// 更换toggle
this.toggleWindow(user.id, 1)
// 登录完成发送一条握手消息给机器人
IM.login(() => {
setTimeout(()=> {
// 获取消息记录
this.getMessageRecord()
if(!this.artificialAccount){
console.log("握手消息")
IM.sendMessage("handshake", this.robotAccount, "")
}
this.scrollIntoBottom()
}, 500)
})
}
})
// 计算客服最后回复时间
this.onServciceLastMessageTimeNotCallBack()
},
// 刷新页面
resetLoad(){
window.location.reload()
},
// 快捷键换行
enterShift(event){
if(this.isMobile) return
if(event.code == "Enter") return
this.chatValue = this.chatValue + "\n"
},
// 监听发送按钮触摸事件
addSendButtonTouchEventListener(){
var self = this
if(this.isIOS){
document.addEventListener('touchstart', function(e) {
if(e.target.getAttribute("class") == "mini-input-send"){
self.sendTextMessage()
}
if(e.target.getAttribute("class") == "sendKnowledgeMessage"){
console.log("监听发送按钮触摸事件", e.target.getAttribute("data"))
self.sendKnowledgeMessage(e.target.getAttribute("data"))
}
}, false);
}else{
this.$refs.sendButton.addEventListener('click', this.sendTextMessage, false);
}
},
// 清除未读消息
cleanRead(id){
axios.get('/public/clean_read/' + id)
},
// 用户是否在当前聊天页面
toggleWindow(id, window){
axios.put('/public/window/' + id,{window: window})
},
// query 转json
queryToJson(str){
if(!str || str == '') return null
var query = str.substr(1, str.length).split("&")
if(!query) return null
var mapData = {}
for(let i= 0; i<query.length; i++){
var temArr = query[i].split("=")
mapData[temArr[0]] = temArr[1]
}
return mapData
},
// 返回上一页按钮
back(){
history.go(-1)
},
// 是否显示用户头像信息(系统消息隐藏)
isShowInfo(biz_type){
return ['end', 'transfer', 'cancel', 'timeout', "system"].indexOf(biz_type) == -1
},
// 点击图片
clickPhoto(url){
if(url.indexOf("http") == -1){
let img = new Image();
img.src = url;
const newWin = window.open("", "_blank");
newWin.document.write(img.outerHTML);
newWin.document.title = "图片"
newWin.document.close();
}else{
window.open(url);
}
},
// 上报最后活动时间
upLastActivity(){
this.onCheckIsOutSession()
const user = this.$mimcInstance.getLocalCacheUser(this.userAccount)
if(user) axios.get('/public/activity/' + user.id)
if(this.isArtificial){
localStorage.setItem("artificialTime_" + this.userAccount,Date.now())
}
setTimeout(() => this.upLastActivity(), 1000*60)
},
// 判断是否被踢出对话
onCheckIsOutSession(){
var artificialTime = localStorage.getItem("artificialTime_" + this.userAccount)
if(artificialTime){
artificialTime = parseInt(artificialTime)
if(Date.now() > artificialTime + 60*1000 * 10){
this.isArtificial = false
this.artificialAccount = null
}
}
},
// 获取本地更多数据
loadMorData(){
if(this.isLoadMorLoading) return
if(this.isLoadMorEnd) return
this.isLoadMorLoading = true
setTimeout(()=> {
// 获取消息记录
this.getMessageRecord()
this.isLoadMorLoading = false
}, 1000)
},
// 获取本地缓存的客服信息
localAdmin(id){
var adminString = localStorage.getItem("admin_" + id)
if(!adminString) return null
return JSON.parse(adminString)
},
// 获取本地缓存的robot
localRobot(id){
var adminString = localStorage.getItem("robot_" + id)
if(!adminString) return null
return JSON.parse(adminString)
},
// emoji
clickEmoji(emoji){
this.showEmoji = false
this.chatValue = this.chatValue + emoji
this.scrollIntoBottom()
},
// 发送图片消息
sendPhotoMessageEvent(e){
var fileDom = e.target;
var file = fileDom.files[0]
this.sendPhotoMessage(file)
},
sendPhotoMessage(file) {
var imgFile = new FileReader();
imgFile.readAsDataURL(file)
var self = this
var localMessage
const fileName = parseInt(Math.random() * 10000 * new Date().getTime()) + file.name.substr(file.name.lastIndexOf('.'))
imgFile.onload = function(){
// 上传失败
let uploadError = function(){
localMessage.percent = 0
self.qiniuObservable= null
self.removeMessage(self.userInfo.id, localMessage.key)
Toast({
message: "上传失败,请重新上传!"
})
const IM = self.$mimcInstance
var message = IM.createLocalMessage("system", self.account, "您刚刚上传的图片失败了,请重新上传!")
self.messages.push(self.handlerMessage(message))
self.scrollIntoBottom()
}
// 上传成功
let uploadSuccess = function(url){
self.qiniuObservable= null
localMessage.percent = 100
var imgUrl = self.uploadToken.host + "/" + url
self.$mimcInstance.sendMessage("photo", self.account, imgUrl)
}
// 创建本地消息
localMessage = self.$mimcInstance.createLocalMessage("photo", self.account, this.result)
localMessage["percent"] = 0
localMessage.isShowCancel = true
setTimeout(() => {
localMessage.isShowCancel = false
}, 10000)
self.messages.push(self.handlerMessage(localMessage))
var cacheMsg = Object.assign({}, localMessage)
cacheMsg.payload = self.uploadToken.host + "/" + fileName
self.$previewRefresh()
self.scrollIntoBottom()
// 系统内置
if(self.uploadToken.mode == 1) {
let fd = new FormData();
fd.append('file',file);
fd.append('file_name', fileName);
axios.post('/public/upload', fd)
.then((res) => {
uploadSuccess(res.data.data)
})
.catch(()=>{
uploadError()
})
}
// 七牛云
else if(self.uploadToken.mode == 2){
let options = {
quality: 0.92,
noCompressIfLarger: true,
maxWidth: 1500,
}
qiniu.compressImage(file, options).then(data => {
const observable = qiniu.upload(data.dist, fileName, self.uploadToken.secret, {}, {
mimeType: null
})
self.qiniuObservable = observable.subscribe({
next: function(res){
localMessage.percent = Math.ceil(res.total.percent);
if(res.total.size < 1){
self.qiniuObservable.unsubscribe()
self.cancelMessage(localMessage.key);
Toast({
message: "上传失败,该图片已损坏!"
})
}
},
error: function(){
// 失败后再次使用FormData上传
var formData = new FormData()
formData.append("fileType", "image")
formData.append("fileName", "file")
formData.append("key", fileName)
formData.append("token", self.uploadToken.secret)
formData.append("file", file)
axios.post("https://upload.qiniup.com", formData)
.then(()=>{
uploadSuccess(fileName)
}).catch(()=>{
uploadError()
})
},
complete: function(res){
uploadSuccess(res.key)
}
})
})
}
}
},
// 滚动条置底
scrollIntoBottom(){
setTimeout(()=>{
var lis = this.$refs.miniImBody.querySelectorAll("li")
this.scroll && this.scroll.scrollToElement(lis[lis.length-1])
}, 50)
},
// input获得焦点
chatInputFocus(){
this.scrollIntoBottom()
this.showEmoji = false
},
// input 失去焦点
chatInputBlur(){
window.chatInputInterval = null
window.scroll(0, 0)
},
// 获取机器人
getRobot(){
return axios.get('/public/robot/1')
.then((response)=>{
var robot = response.data.data
localStorage.setItem("robot_" + robot.id, JSON.stringify(robot))
this.robotAccount = robot.id
this.robotInfo = robot
}).catch((error)=>{
Toast({
message: error.response.data.message
})
})
},
// 获取上传配置
getUploadSecret(){
return axios.get('/public/secret')
.then(response => {
this.uploadToken = response.data.data
})
},
// 获取公司信息
getCompanyInfo(){
return axios.get('/public/company')
.then(response => {
this.companyInfo = response.data.data
}).catch((error)=>{
Toast({
message: error.response.data.message
})
})
},
// 发起并发请求
getAllhttp(){
axios.all([this.getRobot(), this.getCompanyInfo(), this.getUploadSecret()])
.then(axios.spread(() => {
// 初始化MIMC
this.initMimc()
})).catch(()=> setTimeout(() => this.getAllhttp(), 1000));
},
// 接收消息
receiveP2PMsg(message){
console.log(message)
// 是否是转接客服消息
if(message.biz_type == "transfer"){
this.isArtificial = true
this.artificialAccount = message.transfer_account
var admin = JSON.parse(message.payload)
localStorage.setItem("admin_" + admin.id, JSON.stringify(admin))
localStorage.setItem("adminLastCallBackMessageTime_" + admin.id, Date.now())
this.isAdminSendLongTimeSystemMessage = false
}
// 计算客服最后回复时间
if(this.isArtificial && (message.biz_type == "text" || message.biz_type == "photo" || message.biz_type == 'cancel')){
localStorage.setItem("adminLastCallBackMessageTime_" + this.account, Date.now())
this.isAdminSendLongTimeSystemMessage = false
}
// 是否是撤回消息
if(message.biz_type == "cancel"){
this.removeMessage(message.from_account, message.payload)
}
// 是否是结束或超时消息
if(message.biz_type == "end" || message.biz_type == "timeout"){
this.isArtificial = false
this.artificialAccount = null
}
// 对方正在输入
if(message.biz_type == "pong"){
this.inputPongIng()
return
}
// 检索关键词知识库消息
if(message.biz_type == "search_knowledge"){
this.handshakeKeywordList = []
if(message.payload!=""){
this.handshakeKeywordList = JSON.parse(message.payload)
}
return
}
this.messagesPushMemory(message)
this.scrollIntoBottom()
this.$previewRefresh()
window.parent.postMessage({newMessage: 1},'*')
},
// 显示正在输入
inputPongIng(){
if(this.isInputPongIng)return
this.isInputPongIng = true
setTimeout(()=>{
this.inputPongIngString = "对方正在输入."
}, 500)
setTimeout(()=>{
this.inputPongIngString = "对方正在输入.."
}, 1500)
setTimeout(()=>{
this.inputPongIngString = "对方正在输入..."
this.isInputPongIng = false
}, 3000)
},
// enterSendMessage
enterSendMessage(){
if(this.isMobile) return
this.sendTextMessage()
this.$refs.textarea.focus()
},
// 发送文本消息
sendTextMessage(){
// 当前用户是否上线
if(this.userInfo.online == 0){
Toast({
message: "您貌似掉线了"
})
return
}
var chatValue = this.chatValue.trim()
if(chatValue == "") return
const IM = this.$mimcInstance
const message = IM.sendMessage("text", this.account, chatValue)
message.isShowCancel = true
setTimeout(() => message.isShowCancel = false, 10000)
this.messagesPushMemory(message)
this.chatValue = ""
this.handshakeKeywordList = []
},
// 撤回消息
cancelMessage(key){
const IM = this.$mimcInstance
const message = IM.sendMessage("cancel", this.account, key)
this.messagesPushMemory(message)
this.removeMessage(this.userInfo.id, key)
if(this.qiniuObservable) this.qiniuObservable.unsubscribe()
},
// 点击知识库消息
sendKnowledgeMessage(content){
this.handshakeKeywordList = []
const IM = this.$mimcInstance
const message = IM.sendMessage("text", this.account, content)
this.messagesPushMemory(message)
this.chatValue = ""
},
// 点击head右边按钮
headRightBtn(){
if(window.isClickHeadRightBtn) return;
window.isClickHeadRightBtn = true
const IM = this.$mimcInstance
if(this.isArtificial){
MessageBox.confirm('您确定关闭此次会话吗?', "温馨提示! ")
.then(() => {
const message = IM.sendMessage("end", this.account, "")
this.messagesPushMemory(message)
this.isArtificial = false
this.artificialAccount = null
})
setTimeout( () => window.isClickHeadRightBtn = false, 3000)
return
}
const message = IM.sendMessage("text", this.account, "人工")
this.messagesPushMemory(message)
setTimeout( () => window.isClickHeadRightBtn = false, 3000)
},
// 消息处理Memory storage
messagesPushMemory(msg){
if(msg.biz_type == 'pong' || msg.biz_type == "handshake" || msg.biz_type == "into") return;
this.messages.push(this.handlerMessage(msg))
this.scrollIntoBottom()
},
// 处理头像昵称
handlerMessage(msg){
const defaultAvatar = "http://qiniu.cmp520.com/avatar_degault_3.png"
var admin = this.localAdmin(msg.from_account)
var robot = this.localRobot(msg.from_account)
if(admin && msg.from_account == admin.id){
msg.nickname = admin.nickname
msg.avatar = admin.avatar == "" ? defaultAvatar : admin.avatar
}else if(robot && msg.from_account == robot.id){
msg.nickname = robot.nickname
msg.avatar = robot.avatar == "" ? defaultAvatar : robot.avatar
}else if(msg.from_account == this.userInfo.id){
msg.nickname = this.userInfo.nickname
if(this.userInfo.nickname.indexOf(this.userInfo.id) != -1) msg.nickname = "我"
msg.avatar =this.userInfo.avatar == "" ? defaultAvatar : this.userInfo.avatar
}
return msg
},
// 获取服务器消息列表
getMessageRecord(){
const pageSize = 20
let uid = this.userInfo.id
let token = this.userInfo.token
let timestamp = this.messages.length == 0 ? parseInt((new Date().getTime() + " ").substr(0, 10)) : this.messages[0].timestamp
axios.post('/public/messages',{
"timestamp": timestamp,
"page_size": pageSize,
"account": uid
},{
"headers": {
'token': token
}
})
.then(response => {
let messages = response.data.data.list || []
if(messages.length < pageSize) this.isLoadMorEnd = true;
if(this.messages.length == 0 && messages.length > 0){
this.messages = response.data.data.list.map((i) => this.handlerMessage(i))
this.scrollIntoBottom()
}else if(messages.length > 0){
messages = messages.map((i) => this.handlerMessage(i))
this.messages = messages.concat(this.messages)
}
}).catch((error)=>{
console.log(error)
})
},
// 敲键盘发送pong事件消息
keyUpEvent(){
if(!this.isArtificial) return
if(this.isSendPong) return
this.isSendPong = true
setTimeout(() => this.isSendPong = false, 100)
this.$mimcInstance.sendMessage("pong", this.account, this.chatValue)
},
// 删除本地消息
removeMessage(accountId, key){
var newMessages = []
for(let i =0; i<this.messages.length; i++){
if(this.messages[i].key == key && this.messages[i].from_account == accountId) continue
newMessages.push(this.messages[i])
}
this.messages = newMessages
},
// 生成query
createLinkQuery(){
let r = this.isArtificial ? 0 : 1
let a = r == 0 ? this.artificialAccount : this.robotAccount
let m = this.isMobile ? 1 : 0
let h = this.isShowHeader ? 1 : 0
let p = this.platform ? this.platform : 1
let u = this.userAccount ? "&u=" + this.userAccount : ''
let uid = this.uid ? "&uid=" + this.uid : ''
let query = "?h=" + h + "&m=" + m + "&p=" + p + "&r=" + r + "&a=" + a + u + uid
history.replaceState(null, null, query)
if(this.userAccount != null && this.userAccount != 'null' && this.userAccount != ""){
localStorage.setItem("urlQuery", query)
}
},
// 关闭窗口
clickCloseWindow(){
window.parent.postMessage({clickCloseWindow: true},'*')
},
// 计算用户是否长时间未回复弹出给出提示
onCheckIsloogTimeNotCallBack(){
var lastCallBackMessageTime = localStorage.getItem("userLastCallBackMessageTime_" + this.userAccount) || Date.now()
if(this.isArtificial && !this.isUserSendLongTimeSystemMessage && Date.now() - lastCallBackMessageTime >= (1000*60)*5){
const IM = this.$mimcInstance
var message = IM.createLocalMessage("system", this.account, "您已超过5分钟未回复消息,系统3分钟后将结束对话")
this.messages.push(this.handlerMessage(message))
this.isUserSendLongTimeSystemMessage = true
this.scrollIntoBottom()
}
setTimeout(()=> this.onCheckIsloogTimeNotCallBack(), 10000)
},
// 计算客服最后回复时间(超过3分钟没回复给出提示)
onServciceLastMessageTimeNotCallBack(){
if(!this.robotInfo) return
var loogTimeWaitText = this.robotInfo.loog_time_wait_text
var lastCallBackMessageTime = localStorage.getItem("adminLastCallBackMessageTime_" + this.account) || Date.now()
if(this.isArtificial && !this.isAdminSendLongTimeSystemMessage && loogTimeWaitText.trim() != "" && Date.now() - lastCallBackMessageTime >= (1000*60)*2){
const IM = this.$mimcInstance
var message = IM.createLocalMessage("text", this.account, loogTimeWaitText)
message.from_account = this.robotAccount
this.messages.push(this.handlerMessage(message))
this.isAdminSendLongTimeSystemMessage = true
this.scrollIntoBottom()
}
setTimeout(()=> this.onServciceLastMessageTimeNotCallBack(), 10000)
},
// 检索知识库消息
onSearchHandshake(){
if(!this.chatValue || this.isArtificial){
this.handshakeKeywordList = []
return
}
if(this.searchHandshakeTimer) clearTimeout(this.searchHandshakeTimer)
const IM = this.$mimcInstance
this.searchHandshakeTimer = setTimeout(()=>{
IM.sendMessage("search_knowledge", this.robotAccount, this.chatValue)
this.searchHandshakeTimer = null
},500)
},
// 输入框粘贴事件
inputPaste(e){
if(this.isMobile) return
let self = this
var cbd = e.clipboardData;
var ua = window.navigator.userAgent;
// Safari return
if ( !(e.clipboardData && e.clipboardData.items) ) {
return;
}
// Mac平台下Chrome49版本以下 复制Finder中的文件的Bug Hack掉
if(cbd.items && cbd.items.length === 2 && cbd.items[0].kind === "string" && cbd.items[1].kind === "file" &&
cbd.types && cbd.types.length === 2 && cbd.types[0] === "text/plain" && cbd.types[1] === "Files" &&
ua.match(/Macintosh/i) && Number(ua.match(/Chrome\/(\d{2})/i)[1]) < 49){
return;
}
for(var i = 0; i < cbd.items.length; i++) {
var item = cbd.items[i];
if(item.kind == "file"){
var file = item.getAsFile();
if (file.size === 0) {
return;
}
self.sendPhotoMessage(file)
}
}
}
},
watch: {
messages(){
setTimeout(()=>{
this.scroll && this.scroll.refresh()
this.$previewRefresh()
}, 50)
},
isArtificial(isArtificial){
this.createLinkQuery()
localStorage.setItem("isArtificial_" + this.userAccount, isArtificial)
localStorage.setItem("artificialTime_" + this.userAccount,Date.now())
if(!isArtificial){
localStorage.removeItem("artificialTime_" + this.userAccount)
}
},
artificialAccount(){
localStorage.setItem("artificialAccount_" + this.userAccount, this.artificialAccount)
},
userInfo(){
this.createLinkQuery()
},
chatValue(){
this.onSearchHandshake()
}
}
}
</script>
<style lang="stylus">
body{
min-width 240px
overflow: hidden;
height 100vh
background-color #f3f3f3
}
.mint-header.is-fixed{
height 50px!important;
background: -webkit-linear-gradient(to right,#26a2ff, #736cde);
background: -o-linear-gradient(to right,#26a2ff, #736cde);
background: -moz-linear-gradient(to right,#26a2ff, #736cde);
background: linear-gradient(to right,#26a2ff, #736cde);
.mint-header-title{
font-size 15px
}
}
.mint-header,.mint-tabbar{
min-width 240px
z-index: 999999999!important;
}
.mint-header .is-right{
img{
width 25px
}
}
.mint-header .mint-button .mintui{
font-size 23px!important;
}
.mint-tabbar{
z-index: 999999999!important;
background-color #fff!important;
}
.mint-loadmore-spinner{
width: 15px !important;
height: 15px !important;
}
.mini-im-container{
margin: 0 auto;
padding 50px 0 100px
overflow: hidden;
height 100vh
box-sizing:border-box;
-moz-box-sizing:border-box;
-webkit-box-sizing:border-box;
box-sizing: border-box;
.input-ing{
width 100vw
height 25px
position fixed
top 0
left 0
right 0
background-color #26a2ff!important;
z-index 9
color #fff
margin auto
text-align: center;
font-size 14px
line-height 25px
}
.mini-im-loading{
display flex
min-width 240px
width 100%;
position fixed
top 0
left 0
right 0
background-color #fff!important;
margin auto
align-items center
justify-content center
}
}
.mini-im-container-no-pto{
padding-top 0px!important;
}
.mini-im-tabbar-input{
width 100%
padding 5px 10px
overflow hidden
height 100px
display flex
align-items flex-end
justify-content space-between
position fixed
bottom 0
z-index 9
background-color #fff!important;
border-top 1px solid #f2f2f2
left 0
right 0
margin 0 auto
-moz-box-sizing:border-box;
-webkit-box-sizing:border-box;
box-sizing: border-box;
textarea{
outline: none;
-webkit-appearance none
-webkit-tap-highlight-color: rgba(0, 0, 0, 0)
border none
border-radius 5px
height 65px
flex-grow 1
padding 8px 0
font-size 14px
color #666
background-color #ffffff
display block
box-sizing border-box
resize none
flex-shrink 1
flex-grow 1
width 100px
}
span{
width 25px
height 25px
display flex
align-items center
justify-content center
img{
width 28px
}
&.expression-btn{
position absolute
left 40px
top 6px
z-index 99
}
&.photo-btn{
position absolute
left 10px
top 5px
overflow hidden
z-index 99
img{
width 22px
}
input{
width 100%
height 100%
position absolute
top 0
left 0
opacity 0
}
}
&.serverci{
width 70px
position absolute
flex-direction: row;
justify-content: flex-end;
top 5px
right 10px
img{
width 26px
}
span{
width 70px
background-color #f3f3f370
color #999
font-size 14px
}
&.on{
left 75px
justify-content: flex-start;
right initial
}
}
}
.mini-input-send{
width 55px
height 30px
color #fff
line-height 30px
text-align center
border-radius 3px
border none
font-size 14px
background: linear-gradient(to right, #26a2ff, #736cde);
flex-shrink 0
&:active{
opacity 0.8
}
}
}
.mini-im-emoji{
width 100%
height 100vh
position fixed
top 0
left 0
right 0
padding 5px 0
z-index: 9;
margin 0 auto
background-color #fff
.mini-im-emoji-content{
width 100%
height 100vh
padding 50px 0 5px
position absolute
box-sizing:border-box;
-moz-box-sizing:border-box;
-webkit-box-sizing:border-box;
overflow: hidden;
bottom 0
left 0
right 0
margin 0 auto
background-color #fff
text-align center
box-shadow 0px 2px 2px 1px rgba(0, 0, 0, 0.1)
span{
display inline-block
width 28px
height 28px
padding 2px
text-align center
font-size 23px
}
}
}
.mini-im-body{
position: relative
height 100%;
box-sizing:border-box;
-moz-box-sizing:border-box;
-webkit-box-sizing:border-box;
background-color #f3f3f3
margin 0 auto
overflow: hidden;
z-index: 1;
ul{
position: absolute;
z-index: 1;
-webkit-tap-highlight-color: rgba(0,0,0,0);
width: 100%;
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-text-size-adjust: none;
-moz-text-size-adjust: none;
-ms-text-size-adjust: none;
-o-text-size-adjust: none;
text-size-adjust: none;
}
.loading{
height 100%
position fixed
top 0
left 0
right 0
bottom 0
margin auto
display flex
justify-content center
align-items center
}
.no-network{
width 100%;
height 100%;
background-color #fff
position absolute
top 0
left 0
right 0
margin 0 auto
display flex
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9;
span{
color #999
font-size 13px
margin 20px 0
}
button{
width: 100px;
height: 30px;
color: #fff;
line-height: 30px;
text-align: center;
border-radius: 3px;
border: none;
font-size: 14px;
background: -webkit-gradient(linear, left top, right top, from(#26a2ff), to(#736cde));
background: linear-gradient(to right, #26a2ff, #736cde);
-ms-flex-negative: 0;
flex-shrink: 0;
}
}
}
.mini-im-knowledge{
width 100vw
height 100vh
background-color rgba(0,0,0, .2)
position fixed
z-index 8
top 0
left 0
right 0
margin 0 auto
box-sizing:border-box;
padding-bottom 100px
display flex
flex-direction column
justify-content flex-end
.mask{
flex-grow: 1;
width 100vw
height 100px
}
span{
background-color #fff
font-size 14px
color #666
padding 10px
}
ul{
background-color white
li{
font-size 13px
cursor: pointer;
color #56b7ff
padding 6px 10px
border-top 1px solid #f2f2f2
}
}
}
.mint-loadmore{
height 100%
}
.mint-loadmore-text{
color #666
font-size 14px
}
.mini-im-chat-list{
padding 20px 10px
box-sizing:border-box;
-moz-box-sizing:border-box;
-webkit-box-sizing:border-box;
.message-loading{
padding-bottom 20px
display flex
align-items center
justify-content center
}
.mini-im-chat-item{
display flex
margin-bottom 15px
.chat-avatar{
width 30px
height 30px
flex-grow 0
flex-shrink 0
overflow hidden
margin-top 2px
box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.3)
border-radius 100%
img{
width 100%
height 100%
border-radius 100%
}
}
.chat-content{
width 100%
padding-left 10px
.chat-username{
display flex
align-items center
padding-bottom 5px
span{
font-size 12px
color #666
font-weight 500
}
em{
color #666
font-size 12px
margin-left 8px
}
}
.chat-body{
display flex
align-items flex-end
.cancel-btn{
font-size 12px
color #26a2ff !important;
margin-right 5px
}
.text{
padding 5px 8px
background-color #fff
border-radius 3px
font-size 14px
color #333
max-width 85%
position relative
box-shadow 1px 2px 2px 0px rgba(0, 0, 0, 0.1)
-webkit-user-select:text;
-moz-user-select:text;
-o-user-select:text;
user-select:text;
word-break break-all
&:before{
content ''
display block
position absolute
top 5px
left -9px
width 0
height 0
overflow hidden
font-size 0
line-height 0
border 5px
border-radius 2px
border-style dashed solid dashed dashed
border-color transparent #fff transparent transparent
}
}
.photo{
display flex
align-items flex-end
img{
width 120px
display block
border-radius 5px
cursor: pointer;
}
span{
font-size 12px
color #999
padding-right 5px
}
}
.system{
width 100%
display flex
flex-direction column
justify-content center
span{
text-align center
font-size 12px
color #999
}
.content{
margin-top 1.5px
height 25px
text-align center
span{
padding 0 10px
text-align center
font-size 12px
border-radius 5px
display inline-block
line-height 22px
height 22px
min-width 80px
color #949393
}
}
}
.knowledge{
padding 5px 8px
background-color #fff
border-radius 3px
font-size 13px
color #333
max-width 80%
position relative
box-shadow 1px 2px 2px 0px rgba(0, 0, 0, 0.1)
display flex
flex-direction column
align-items flex-start
.title{
min-height 25px
font-size 14px
}
a{
font-size 13px
color #26a2ff
text-decoration none
width 100%
display flex
min-height 25px
}
}
}
}
&.self{
justify-content flex-end
.chat-content{
padding-right 10px
}
.chat-body{
justify-content flex-end
.text{
box-shadow -1px 1px 3px 0px rgba(0,0,0,0.1)
background-color #26a2ff
color #fff
-webkit-user-select:text;
-moz-user-select:text;
-o-user-select:text;
user-select:text;
word-break:break-all;
&:before{
left inherit
right -9px
border-style dashed dashed dashed solid
border-color transparent transparent transparent #26a2ff
}
}
}
.chat-avatar{
order 1
}
.chat-username{
justify-content flex-end
em{
order -2
margin-right 5px
}
}
}
}
}
// PC端兼容样式
.mini-im-pc-container{
width 360px;
height 500px
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin auto
display flex
flex-direction column
padding 0!important
overflow hidden
box-shadow 1px 1px 8px 2px #ccc
.mini-im-loading,.mini-im-emoji{
width 360px!important
height 500px!important
bottom 0
margin auto !important
}
.mini-im-emoji{
box-sizing:border-box;
-moz-box-sizing:border-box;
-webkit-box-sizing:border-box;
}
.cancel-btn{
cursor pointer
}
.mini-im-emoji-content{
padding 8px!important
height 465px!important
box-sizing:border-box;
-moz-box-sizing:border-box;
-webkit-box-sizing:border-box;
span{
width 26px
height 26px
cursor pointer
}
}
.mini-im-body{
width 360px;
height 500px
position static !important
.mini-im-chat-list{
padding 15px!important
}
}
.mini-im-pc-header{
z-index: 999999999!important;
height 45px
background: linear-gradient(to right, #26a2ff, #736cde);
flex-shrink 0
display flex
justify-content space-between
align-items center
padding 0 10px
color #fff
.right{
display flex
align-items center
cursor pointer
img{
width 20px
margin-right 5px
}
}
.title{
font-size 14px
display flex
align-items center
img{
width 20px
margin-right 5px
}
}
span{
font-size 14px
}
.close-btn{
width 20px
height 35px
text-align right
line-height 35px
cursor pointer
}
}
.mini-im-tabbar-input{
height 130px
overflow hidden
padding 5px
position relative
box-sizing:border-box;
-moz-box-sizing:border-box;
-webkit-box-sizing:border-box;
z-index 9
textarea{
height 65px
padding-right 5px
margin 0
}
.mini-input-send{
height 70px
width 60px
background: linear-gradient(to right, #26a2ff, #736cde);
color #fff
border 0
cursor pointer
border-radius 2px
}
span.photo-btn{
left 3px
}
span.expression-btn{
left 30px
}
}
}
.bscroll-vertical-scrollbar{
right 0px!important
height 100%!important
.bscroll-indicator{
width 4px !important
border: 0 !important;
background: rgba(0, 0, 0, 0.2) !important;
right 0!important;
}
}
</style>
import Vue from 'vue'
import App from './App.vue'
import preview from 'vue-photo-preview'
import 'vue-photo-preview/dist/skin.css'
import MintUI from 'mint-ui'
import 'mint-ui/lib/style.css'
import Helps from "../plugins/help"
import MimcPlugin from "../plugins/mimc"
import momentLocal from '../resource/moment_locale'
var moment = require('moment');
moment.locale("zh-cn", momentLocal)
import axios from 'axios'
axios.defaults.baseURL = '/v1'
var options={
clickToCloseNonZoomable: false,
fullscreenEl:false, //关闭全屏按钮
}
Vue.use(preview, options)
Vue.use(Helps)
Vue.use(MimcPlugin)
Vue.use(MintUI)
Vue.config.productionTip = false
new Vue({
render: h => h(App)
}).$mount('#app')
module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
outputDir: '../client/',
devServer: {
proxy: 'http://localhost:8080',
}
}
\ No newline at end of file
......@@ -158,6 +158,8 @@ func init() {
beego.NSRouter("/type", &controllers.WorkOrderController{}, "post:PostWorkType"),
beego.NSRouter("/type", &controllers.WorkOrderController{}, "put:UpdateWorkType"),
beego.NSRouter("/type/:id", &controllers.WorkOrderController{}, "delete:DeleteWorkType"),
beego.NSRouter("/type/:id", &controllers.WorkOrderController{}, "get:GetWorkType"),
beego.NSRouter("/types", &controllers.WorkOrderController{}, "get:GetWorkTypes"),
),
)
beego.AddNamespace(ns)
......
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
......@@ -2,11 +2,14 @@ package services
import (
"kefu_server/models"
"github.com/astaxie/beego/logs"
)
// WorkOrderCommentRepositoryInterface interface
type WorkOrderCommentRepositoryInterface interface {
GetWorkOrder() *models.WorkOrderComment
DeleteAll(wid int64) (int64, error)
}
// WorkOrderCommentRepository struct
......@@ -21,3 +24,11 @@ func GetWorkOrderCommentRepositoryInstance() *WorkOrderCommentRepository {
return instance
}
// DeleteAll delete all WorkOrderComment
func (r *WorkOrderRepository) DeleteAll(wid int64) (int64, error) {
index, err := r.q.Filter("wid", wid).Delete()
if err != nil {
logs.Warn(" DeleteAll delete all WorkOrderComment------------", err)
}
return index, err
}
......@@ -9,26 +9,49 @@ import (
// WorkOrderRepositoryInterface interface
type WorkOrderRepositoryInterface interface {
GetWorkOrder() *models.WorkOrder
Update(id int64,params *orm.Params) (int64, error)
GetWorkOrder() models.WorkOrder
Update(id int64, params *orm.Params) (int64, error)
Add(workOrder models.WorkOrder) (int64, error)
Delete(id int64) (int64, error)
}
// WorkOrderRepository struct
type WorkOrderRepository struct {
BaseRepository
CommentRepository *WorkOrderCommentRepository
}
// GetWorkOrderRepositoryInstance get instance
func GetWorkOrderRepositoryInstance() *WorkOrderRepository {
instance := new(WorkOrderRepository)
instance.Init(new(models.WorkOrder))
instance.CommentRepository = GetWorkOrderCommentRepositoryInstance()
return instance
}
// Delete delete WorkOrder
func (r *WorkOrderRepository) Delete(id int64) (int64, error) {
index, err := r.q.Filter("id", id).Delete()
if err != nil {
logs.Warn("Delete delete WorkOrder------------", err)
}
if index > 0 {
GetWorkOrderCommentRepositoryInstance()
}
return index, err
}
// Add add WorkOrder
func (r *WorkOrderRepository) Add(workOrder models.WorkOrder) (int64, error) {
index, err := r.o.Insert(workOrder)
if err != nil {
logs.Warn("Add add WorkOrder------------", err)
}
return index, err
}
// Update WorkOrder Info
func (r *WorkOrderRepository) Update(id int64,params orm.Params) (int64, error) {
func (r *WorkOrderRepository) Update(id int64, params orm.Params) (int64, error) {
index, err := r.q.Filter("id", id).Update(params)
if err != nil {
logs.Warn("Update WorkOrder Info------------", err)
......

246 KB | W: | H:

246 KB | W: | H:

static/uploads/images/1024564602991303.png
static/uploads/images/1024564602991303.png
static/uploads/images/1024564602991303.png
static/uploads/images/1024564602991303.png
  • 2-up
  • Swipe
  • Onion skin

13.2 KB | W: | H:

13.2 KB | W: | H:

static/uploads/images/10595706961116000.jpg
static/uploads/images/10595706961116000.jpg
static/uploads/images/10595706961116000.jpg
static/uploads/images/10595706961116000.jpg
  • 2-up
  • Swipe
  • Onion skin

246 KB | W: | H:

246 KB | W: | H:

static/uploads/images/10739162322084292.png
static/uploads/images/10739162322084292.png
static/uploads/images/10739162322084292.png
static/uploads/images/10739162322084292.png
  • 2-up
  • Swipe
  • Onion skin

1.7 KB | W: | H:

1.7 KB | W: | H:

static/uploads/images/11162360010345286.png
static/uploads/images/11162360010345286.png
static/uploads/images/11162360010345286.png
static/uploads/images/11162360010345286.png
  • 2-up
  • Swipe
  • Onion skin

13.2 KB | W: | H:

13.2 KB | W: | H:

static/uploads/images/15465341792953788.jpg
static/uploads/images/15465341792953788.jpg
static/uploads/images/15465341792953788.jpg
static/uploads/images/15465341792953788.jpg
  • 2-up
  • Swipe
  • Onion skin

4.35 KB | W: | H:

4.35 KB | W: | H:

static/uploads/images/1895426542557186.png
static/uploads/images/1895426542557186.png
static/uploads/images/1895426542557186.png
static/uploads/images/1895426542557186.png
  • 2-up
  • Swipe
  • Onion skin

4.81 MB | W: | H:

4.81 MB | W: | H:

static/uploads/images/2020-03-07/sdfsdfs1df.jpg
static/uploads/images/2020-03-07/sdfsdfs1df.jpg
static/uploads/images/2020-03-07/sdfsdfs1df.jpg
static/uploads/images/2020-03-07/sdfsdfs1df.jpg
  • 2-up
  • Swipe
  • Onion skin

4.35 KB | W: | H:

4.35 KB | W: | H:

static/uploads/images/2020-03-14/1059705541630756.png
static/uploads/images/2020-03-14/1059705541630756.png
static/uploads/images/2020-03-14/1059705541630756.png
static/uploads/images/2020-03-14/1059705541630756.png
  • 2-up
  • Swipe
  • Onion skin

4.35 KB | W: | H:

4.35 KB | W: | H:

static/uploads/images/2020-03-14/10748561963710498.png
static/uploads/images/2020-03-14/10748561963710498.png
static/uploads/images/2020-03-14/10748561963710498.png
static/uploads/images/2020-03-14/10748561963710498.png
  • 2-up
  • Swipe
  • Onion skin

4.35 KB | W: | H:

4.35 KB | W: | H:

static/uploads/images/2020-03-14/3868620975118566.png
static/uploads/images/2020-03-14/3868620975118566.png
static/uploads/images/2020-03-14/3868620975118566.png
static/uploads/images/2020-03-14/3868620975118566.png
  • 2-up
  • Swipe
  • Onion skin

4.35 KB | W: | H:

4.35 KB | W: | H:

static/uploads/images/2020-03-14/5319691654085043.png
static/uploads/images/2020-03-14/5319691654085043.png
static/uploads/images/2020-03-14/5319691654085043.png
static/uploads/images/2020-03-14/5319691654085043.png
  • 2-up
  • Swipe
  • Onion skin

4.35 KB | W: | H:

4.35 KB | W: | H:

static/uploads/images/2020-03-14/7805661278802885.png
static/uploads/images/2020-03-14/7805661278802885.png
static/uploads/images/2020-03-14/7805661278802885.png
static/uploads/images/2020-03-14/7805661278802885.png
  • 2-up
  • Swipe
  • Onion skin

4.35 KB | W: | H:

4.35 KB | W: | H:

static/uploads/images/2020-03-14/8569570733363921.png
static/uploads/images/2020-03-14/8569570733363921.png
static/uploads/images/2020-03-14/8569570733363921.png
static/uploads/images/2020-03-14/8569570733363921.png
  • 2-up
  • Swipe
  • Onion skin

4.35 KB | W: | H:

4.35 KB | W: | H:

static/uploads/images/2020-03-14/9610469172281572.png
static/uploads/images/2020-03-14/9610469172281572.png
static/uploads/images/2020-03-14/9610469172281572.png
static/uploads/images/2020-03-14/9610469172281572.png
  • 2-up
  • Swipe
  • Onion skin

246 KB | W: | H:

246 KB | W: | H:

static/uploads/images/2550071827788813.png
static/uploads/images/2550071827788813.png
static/uploads/images/2550071827788813.png
static/uploads/images/2550071827788813.png
  • 2-up
  • Swipe
  • Onion skin

4.17 KB | W: | H:

4.17 KB | W: | H:

static/uploads/images/3001017273673196.png
static/uploads/images/3001017273673196.png
static/uploads/images/3001017273673196.png
static/uploads/images/3001017273673196.png
  • 2-up
  • Swipe
  • Onion skin

31.2 KB | W: | H:

31.2 KB | W: | H:

static/uploads/images/4191721475264322.jpeg
static/uploads/images/4191721475264322.jpeg
static/uploads/images/4191721475264322.jpeg
static/uploads/images/4191721475264322.jpeg
  • 2-up
  • Swipe
  • Onion skin

965 Bytes | W: | H:

965 Bytes | W: | H:

static/uploads/images/5516437697233784.png
static/uploads/images/5516437697233784.png
static/uploads/images/5516437697233784.png
static/uploads/images/5516437697233784.png
  • 2-up
  • Swipe
  • Onion skin

4.35 KB | W: | H:

4.35 KB | W: | H:

static/uploads/images/6359931358190748.png
static/uploads/images/6359931358190748.png
static/uploads/images/6359931358190748.png
static/uploads/images/6359931358190748.png
  • 2-up
  • Swipe
  • Onion skin

551 Bytes | W: | H:

551 Bytes | W: | H:

static/uploads/images/6417803415722617.png
static/uploads/images/6417803415722617.png
static/uploads/images/6417803415722617.png
static/uploads/images/6417803415722617.png
  • 2-up
  • Swipe
  • Onion skin

4.17 KB | W: | H:

4.17 KB | W: | H:

static/uploads/images/6800660106241034.png
static/uploads/images/6800660106241034.png
static/uploads/images/6800660106241034.png
static/uploads/images/6800660106241034.png
  • 2-up
  • Swipe
  • Onion skin

4.17 KB | W: | H:

4.17 KB | W: | H:

static/uploads/images/7485103156563738.png
static/uploads/images/7485103156563738.png
static/uploads/images/7485103156563738.png
static/uploads/images/7485103156563738.png
  • 2-up
  • Swipe
  • Onion skin

246 KB | W: | H:

246 KB | W: | H:

static/uploads/images/9050831048854968.png
static/uploads/images/9050831048854968.png
static/uploads/images/9050831048854968.png
static/uploads/images/9050831048854968.png
  • 2-up
  • Swipe
  • Onion skin

551 Bytes | W: | H:

551 Bytes | W: | H:

static/uploads/images/9111169004028260.png
static/uploads/images/9111169004028260.png
static/uploads/images/9111169004028260.png
static/uploads/images/9111169004028260.png
  • 2-up
  • Swipe
  • Onion skin

4 KB | W: | H:

4 KB | W: | H:

static/uploads/images/sdfsdfsdf.jpg
static/uploads/images/sdfsdfsdf.jpg
static/uploads/images/sdfsdfsdf.jpg
static/uploads/images/sdfsdfsdf.jpg
  • 2-up
  • Swipe
  • Onion skin
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
File mode changed
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment