This commit is contained in:
feng0207
2026-08-11 22:22:10 +08:00
commit 5e312c1266
6985 changed files with 1061169 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
build.sh
.idea
.hbuilderx/
unpackage
+217
View File
@@ -0,0 +1,217 @@
<script>
import {
navigateTo,
Modal
} from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
import config from './pages/config/app.js';
import store from '@/store';
export default {
globalData: {
http: store.state.http,
kefuInfo: store.state.kefuInfo,
openLogin: false
},
onLaunch: function() {
console.log('App Launch');
this.init();
this.addEventListener();
if (this.globalData.http && store.state.token) {
this.scoket.startConnect();
this.socketEvent();
}
},
onShow: function() {
if (this.globalData.http) {
http(api.setKefubackstage, {
value: 1
}).then(res => {
let kefuInfo = JSON.parse(JSON.stringify(this.globalData.kefuInfo));
kefuInfo.is_backstage = 1;
this.globalData.kefuInfo.is_backstage = 1;
store.commit('setkefuInfo', {
one: true,
is_backstage: 1
});
});
}
console.log('App Show');
//#ifdef APP-PLUS
this.cheackSingleLogin();
//#endif
},
onHide: function() {
//#ifdef APP-PLUS
if (this.globalData.http) {
http(api.setKefubackstage, {
value: 0
}).then(res => {
let kefuInfo = JSON.parse(JSON.stringify(this.globalData.kefuInfo));
kefuInfo.is_backstage = 0;
store.commit('setkefuInfo', kefuInfo);
this.globalData.kefuInfo.is_backstage = 0;
});
}
//#endif
//#ifndef APP-PLUS
if (this.globalData.http) {
http(api.setKefubackstage, {
value: 1
}).then(res => {
let kefuInfo = JSON.parse(JSON.stringify(this.globalData.kefuInfo));
kefuInfo.is_backstage = 1;
this.globalData.kefuInfo.is_backstage = 1;
store.commit('setkefuInfo', kefuInfo);
});
}
//#endif
console.log('App Hide');
},
methods: {
//单点登录检测
cheackSingleLogin() {
let that = this;
if (store.state.token && this.globalData.http && !that.globalData.openLogin) {
let info = plus.push.getClientInfo();
let cid = info.clientid;
this.$store.dispatch('getKeufuInfo').then(res => {
if (res.client_id && res.client_id !== cid) {
that.globalData.openLogin = true;
//需要强制退出登录
uni.showModal({
title: '强制退出登录',
content: '您的账号已在另一台设备上登录',
showCancel: false,
confirmText: '确认退出',
success() {
//关闭长连接
that.scoket.clearPing();
that.$store.commit('clearChat');
that.$store.commit('logout');
uni.reLaunch({
url: '/pages/view/login/index'
});
},
complete: () => {
that.globalData.openLogin = false;
}
});
} else if (!res.client_id) {
this.$store.dispatch('saveUserClient', cid);
}
});
}
},
addEventListener() {
//#ifdef APP-PLUS
/* 5+ push 消息推送 ps:使用:H5+的方式监听,实现推送*/
var info = plus.push.getClientInfo();
var cid = info.clientid;
plus.push.addEventListener(
'click',
function(msg) {
console.log('点击事件:', JSON.parse(JSON.parse(msg.payload)));
navigateTo(1, JSON.parse(JSON.parse(msg.payload)).url);
},
false
);
plus.push.addEventListener(
'receive',
function(msg) {
console.log('透传消息来了:', msg);
if (msg.aps) {
try {
let options = {
cover: false,
sound: 'system',
title: msg.payload.title
};
plus.push.createMessage(msg.payload.body, 'LocalMSG', options);
} catch (e) {
console.log(e);
}
console.log(msg);
} else if (msg.payload === 'LocalMSG') {} else if (msg.payload.type === 'url') {
navigateTo(1, msg.payload.url);
} else {
try {
let options = {
cover: false,
sound: 'system',
title: msg.payload.title
};
plus.push.createMessage(msg.payload.body, 'LocalMSG', options);
} catch (e) {
console.log(e);
}
console.log(msg);
}
},
false
);
//#endif
if (!store.state.token && this.$cache.get('pages_login')) {
this.$cache.remove('pages_login');
}
},
// 获取域名, 若没有域名,自动跳转到填写域名界面
getDomainName() {
let dominName = this.$cache.get('dominName');
if (!dominName && !this.$cache.get('pages_dominName')) {
navigateTo(2, '/pages/view/dominName/index');
}
},
init() {
if (!this.$cache.get('wsFile')) {
this.$cache.set('wsFile', config.defaultWsType);
}
if (!this.$cache.get('dominName') && config.defaultDomainName) {
this.$cache.set('dominName', config.defaultDomainName);
this.$cache.set('requestFile', config.defaultRequestType);
this.$cache.set('wsFile', config.defaultWsType);
this.globalData.http = config.defaultRequestType + '://' + config.defaultDomainName;
store.commit('setHttp', {
wsFile: config.defaultWsType,
dominName: config.defaultDomainName,
requestFile: config.defaultRequestType
});
}
if (!this.$cache.get('requestFile')) {
this.$cache.set('requestFile', config.defaultRequestType);
}
// 如果开启了域名配置且没有缓存域名,跳转到域名设置页
this.getDomainName();
},
socketEvent() {
uni.$on('success', data => {
console.log('success');
});
uni.$on('mssage_num', data => {
if (data.num > 0) {
this.$onlineAudio.play();
uni.setTabBarBadge({
index: 0,
text: data.num ? String(data.num) : 0 // 设置为数值,右下角则不在显示
});
}
this.scoket.setBadgeNumber(data.num);
});
}
}
};
</script>
<style lang="less">
/*每个页面公共less */
@import url('./static/globalLess/index.less');
@import url('./static/globalLess/iconfont.css');
// 表情包
@import url('./static/emoji/emojione.css');
</style>
+3
View File
@@ -0,0 +1,3 @@
{
"prompt": "template"
}
+65
View File
@@ -0,0 +1,65 @@
<template>
<view class="download-popup">
<view class="mask"></view>
<view class="content">
<view class="title">正在下载升级包中</view>
<progress :percent="percent" show-info stroke-width="3" />
<view class="button-wrap" @click='stop'>
<button type="primary" size="mini">取消</button>
</view>
</view>
</view>
</template>
<script>
export default {
name:"DownloadProgress",
props: {
percent: {
type: Number,
default: 0
}
},
data() {
return {
};
},
methods: {
stop(){
this.$emit('clearAbort')
},
}
}
</script>
<style scoped>
.mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
background-color: rgba(0, 0, 0, .3);
}
.content {
position: fixed;
top: 50%;
left: 50%;
z-index: 1000;
width: 600rpx;
padding: 60rpx 60rpx 30rpx;
border-radius: 20rpx;
box-sizing: border-box;
background-color: #FFFFFF;
transform: translate(-50%, -50%);
}
.button-wrap {
margin-top: 30rpx;
font-size: 0;
text-align: center;
}
</style>
@@ -0,0 +1,511 @@
<!--
* @插件日期时间选择器
* @作者陈万照
* @公司山东标梵互动信息技术有限公司
* @官网http://biaofun.com/
* @微信C207668802
* @QQ207668802
* @邮箱cwz@biaofun.com || 207668802@qq.com
* @版本v1.0.8
-->
<template>
<view class="datatime">
<picker mode="multiSelector" :range="range" range-key="text" @change="change" @columnchange="columnchange" :value="value" :disabled="disabled">
<view class="content" :class="{ placeholder: !dateStr }">
<text>{{ dateStr ? dateStr : placeholder }}</text>
</view>
</picker>
</view>
</template>
<script>
import utils from '../utils.js'; // 封装的工具集
export default {
/**
* 数据
*/
props: {
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 占位符
placeholder: {
type: String,
default: '请选择日期时间'
},
// 表示有效日期时间范围的开始,
// 字符串格式为 "YYYY-MM-DD hh:mm"
start: {
type: String,
default: '1970-1-1 00:00'
},
// 表示有效日期时间范围的结束
// 字符串格式为 "YYYY-MM-DD hh:mm"
end: {
type: String,
default: '2300-1-1 00:00'
},
// 表示选择器的粒度,有效值:year | month | day | hour | minute
fields: {
type: String,
default: 'minute'
},
// 默认值
// 字符串格式为 "YYYY-MM-DD hh:mm"
defaultValue: {
type: String,
default: ''
}
},
/**
* 数据
*/
data() {
return {
range: [],
value: [],
dateStr: '', // 最终显示的字符串
dtStart: null, // 有效范围开始
dtEnd: null, // 有效范围结束
};
},
/**
* 监听数据
*/
watch: {
// 默认值
defaultValue() {
// 设置默认值
this.setDefaultValue();
}
},
/**
* 组件初次加载完成
*/
mounted() {
// 有效日期开始和结束
let start = this.start;
let end = this.end;
// 验证是否是有效的开始和结束日期
if(!utils.isString(this.start)) {
console.log('开始日期需为String类型,格式为 "YYYY-MM-DD hh:mm"');
start = '1970-1-1 00:00';
}
if(!utils.isString(this.start)) {
console.log('结束日期需为String类型,格式为 "YYYY-MM-DD hh:mm"');
start = '2300-1-1 00:00';
}
// 将开始日期和结束日期转为 Date
let dtStart = utils.formatDate(start).dt;
let dtEnd = utils.formatDate(end).dt;
// 判断有效日期结束是否大于有效日期开始,如果不是,则将有效日期结束修改为有效日期开始往后300年
if (dtEnd <= dtStart) {
dtEnd = utils.formatDate(start).dt;
dtEnd.setFullYear(dtStart.getFullYear() + 300);
dtEnd.setDate(dtEnd.getDate() - 1);
}
// 更新开始日期和结束日期
this.dtStart = dtStart;
this.dtEnd = dtEnd;
// 设置默认值
this.setDefaultValue();
},
/**
* 方法
*/
methods: {
/**
* 确认选择
*/
change(event) {
let year, month, day, hour, minute;
if(this.fields == 'year') {
year = this.range[0][this.value[0]].number; // 年
let dtStr = `${year}`;
this.setDateStr(dtStr);
this.$emit('change', utils.formatDate(dtStr));
return;
}
else if(this.fields == 'month') {
year = this.range[0][this.value[0]].number; // 年
month = this.range[1][this.value[1]].number; // 月
let dtStr = `${year}-${month}`;
this.setDateStr(dtStr);
this.$emit('change', utils.formatDate(dtStr));
return;
}
else if(this.fields == 'day') {
year = this.range[0][this.value[0]].number; // 年
month = this.range[1][this.value[1]].number; // 月
day = this.range[2][this.value[2]].number; // 日
let dtStr = `${year}-${month}-${day}`;
this.setDateStr(dtStr);
this.$emit('change', utils.formatDate(dtStr));
return;
}
else if(this.fields == 'hour') {
year = this.range[0][this.value[0]].number; // 年
month = this.range[1][this.value[1]].number; // 月
day = this.range[2][this.value[2]].number; // 日
hour = this.range[3][this.value[3]].number; // 时
day = this.range[2][this.value[2]].number; // 日
let dtStr = `${year}-${month}-${day} ${hour}`;
this.setDateStr(dtStr);
this.$emit('change', utils.formatDate(dtStr));
return;
}
else if(this.fields == 'minute') {
year = this.range[0][this.value[0]].number; // 年
month = this.range[1][this.value[1]].number; // 月
day = this.range[2][this.value[2]].number; // 日
hour = this.range[3][this.value[3]].number; // 时
minute = this.range[4][this.value[4]].number; // 分
let dtStr = `${year}-${month}-${day} ${hour}:${minute}`;
this.setDateStr(dtStr);
this.$emit('change', utils.formatDate(dtStr));
return;
}
},
/**
* 设置显示的值
* @param {Date|String} date 日期字符串或日期对象
*/
setDateStr(date) {
let dt = utils.formatDate(date);
if(this.fields == 'year') {
this.dateStr = `${dt.YYYY}`;
return;
}
if(this.fields == 'month') {
this.dateStr = `${dt.YYYY}${dt.M}`;
return;
}
if(this.fields == 'day') {
this.dateStr = `${dt.YYYY}${dt.M}${dt.D}`;
return;
}
if(this.fields == 'hour') {
this.dateStr = `${dt.YYYY}${dt.M}${dt.D}${dt.h}`;
return;
}
this.dateStr = `${dt.YYYY}${dt.M}${dt.D}${dt.h}${dt.m}`;
},
/**
* 设置年数据
*/
setYearData() {
// 有效日期
let yearStart = this.dtStart.getFullYear();
let yearEnd = this.dtEnd.getFullYear();
// 年
let years = [];
for (let year = yearStart; year <= yearEnd; year++) {
let item = {
number: year,
text: `${year}`,
};
years.push(item);
}
this.range.splice(0, 1, years);
},
/**
* 设置月数据
* @param {Number} year 年
*/
setMonthData(year) {
// 有效日期
let yearStart = this.dtStart.getFullYear();
let monthStart = this.dtStart.getMonth() + 1;
let yearEnd = this.dtEnd.getFullYear();
let monthEnd = this.dtEnd.getMonth() + 1;
// 月
let months = [];
let monthStartIndex = year == yearStart ? monthStart : 1;
let monthEndIndex = year == yearEnd ? monthEnd : 12;
for (let month = monthStartIndex; month <= monthEndIndex; month++) {
let item = {
number: month,
text: `${month}`,
};
months.push(item);
}
this.range.splice(1, 1, months);
},
/**
* 设置日数据
* @param {Number} year 年
* @param {Number} month 月
*/
setDayData(year, month) {
// 有效日期
let yearStart = this.dtStart.getFullYear();
let monthStart = this.dtStart.getMonth() + 1;
let dayStart = this.dtStart.getDate();
let yearEnd = this.dtEnd.getFullYear();
let monthEnd = this.dtEnd.getMonth() + 1;
let dayEnd = this.dtEnd.getDate();
// 日
let days = [];
let dayStartIndex = year == yearStart && month == monthStart ? dayStart : 1;
let dayEndIndex;
if(year == yearEnd && month == monthEnd) {
dayEndIndex = dayEnd;
} else {
dayEndIndex = (new Date(year, month, 0)).getDate();
}
for (let day = dayStartIndex; day <= dayEndIndex; day++) {
let item = {
number: day,
text: `${day}`,
};
days.push(item);
}
this.range.splice(2, 1, days);
},
/**
* 设置时数据
* @param {Number} year 年
* @param {Number} month 月
* @param {Number} day 日
*/
setHourData(year, month, day) {
// 有效日期
let yearStart = this.dtStart.getFullYear();
let monthStart = this.dtStart.getMonth() + 1;
let dayStart = this.dtStart.getDate();
let hourStart = this.dtStart.getHours();
let yearEnd = this.dtEnd.getFullYear();
let monthEnd = this.dtEnd.getMonth() + 1;
let dayEnd = this.dtEnd.getDate();
let hourEnd = this.dtEnd.getHours();
// 时
let hours = [];
let hourStartIndex = year == yearStart && month == monthStart && day == dayStart ? hourStart : 0;
let hourEndIndex = year == yearEnd && month == monthEnd && day == dayEnd ? hourEnd : 23;
for (let hour = hourStartIndex; hour <= hourEndIndex; hour++) {
let item = {
number: hour,
text: `${hour}`,
};
hours.push(item);
}
this.range.splice(3, 1, hours);
},
/**
* 设置分数据
* @param {Number} year 年
* @param {Number} month 月
* @param {Number} day 日
* @param {Number} hour 时
*/
setMinuteData(year, month, day, hour) {
// 有效日期
let yearStart = this.dtStart.getFullYear();
let monthStart = this.dtStart.getMonth() + 1;
let dayStart = this.dtStart.getDate();
let hourStart = this.dtStart.getHours();
let minuteStart = this.dtStart.getMinutes();
let yearEnd = this.dtEnd.getFullYear();
let monthEnd = this.dtEnd.getMonth() + 1;
let dayEnd = this.dtEnd.getDate();
let hourEnd = this.dtEnd.getHours();
let minuteEnd = this.dtEnd.getMinutes();
// 分
let minutes = [];
let minuteStartIndex = year == yearStart && month == monthStart && day == dayStart && hour == hourStart ? minuteStart : 0;
let minuteEndIndex = year == yearEnd && month == monthEnd && day == dayEnd && hour == hourEnd ? minuteEnd : 59;
for(let minute = minuteStartIndex; minute <= minuteEndIndex; minute++) {
let item = {
number: minute,
text: `${minute}`,
}
minutes.push(item);
}
this.range.splice(4, 1, minutes);
},
/**
* 设置默认值
*/
setDefaultValue() {
// 默认日期
let dtDefault;
// 开始日期和结束日期
let dtStart = this.dtStart;
let dtEnd = this.dtEnd;
// 判断是否传了默认日期
// 传了默认日期,格式化默认日期为日期对象
if(this.defaultValue) {
dtDefault = utils.formatDate(this.defaultValue).dt;
}
// 如果没有传默认日期,将默认日期设置为当前日期
else {
dtDefault = new Date();
}
// 如果默认日期不在有效日期范围内,设置默认日期为有效日期开始值
if (dtDefault < dtStart || dtDefault > dtEnd) {
dtDefault = dtStart;
}
// 更新 dateStr
if(this.defaultValue) this.setDateStr(dtDefault);
// 默认值相关数据
let dfYear = dtDefault.getFullYear();
let dfMonth = dtDefault.getMonth() + 1;
let dfDay = dtDefault.getDate();
let dfHour = dtDefault.getHours();
let dfMinute = dtDefault.getMinutes();
// 设置年数据
this.setYearData();
// 设置 Year 这一列的 value 值
let yearIndex = this.range[0].findIndex(year => {
return dfYear == year.number;
});
this.value.splice(0, 1, yearIndex >= 0 ? yearIndex : 0);
// 设置月数据
if(this.fields == 'year') return;
this.setMonthData(dfYear);
// 设置 Month 这一列的 value 值
let monthIndex = this.range[1].findIndex(month => {
return dfMonth == month.number;
});
this.value.splice(1, 1, monthIndex >=0 ? monthIndex : 0);
// 设置日数据
if(this.fields == 'month') return;
this.setDayData(dfYear, dfMonth);
// 设置 Day 这一列的 value 值
let dayIndex = this.range[2].findIndex(day => {
return dfDay == day.number;
});
this.value.splice(2, 1, dayIndex >=0 ? dayIndex : 0);
// 设置时数据
if(this.fields == 'day') return;
this.setHourData(dfYear, dfMonth, dfDay);
// 设置 Hour 这一列的 value 值
let hourIndex = this.range[3].findIndex(hour => {
return dfHour == hour.number;
});
this.value.splice(3, 1, hourIndex >=0 ? hourIndex : 0);
// 设置分数据
if(this.fields == 'hour') return;
this.setMinuteData(dfYear, dfMonth, dfDay, dfHour);
// 设置 Minute 这一列的 value 值
let minuteIndex = this.range[4].findIndex(minute => {
return dfMinute == minute.number;
});
this.value.splice(4, 1, minuteIndex >=0 ? minuteIndex : 0);
},
/**
* 某一列的值改变时触发
* @param {Number} event.detail.column 表示改变了第几列(下标从0开始)
* @param {Number} event.detail.value 表示变更值的下标
*/
columnchange(event) {
let columnIndex = event.detail.column; // 改变的列的下标
let valueIndex = event.detail.value; // 变更值的下标
// 更新改变列的 value
this.value.splice(columnIndex, 1, valueIndex);
// 改变年要更新月数据
if(this.fields == 'year') return;
if (columnIndex == 0) {
// 当前选择的月
let monthBeforeUpdate = this.range[1][this.value[1]];
// 更新月数据
this.setMonthData(this.range[0][this.value[0]].number);
// 更新 Month Value
let monthIndex = this.range[1].findIndex(month => {
return month.number == monthBeforeUpdate.number;
});
this.value.splice(1, 1, monthIndex >= 0 ? monthIndex : 0);
}
// 改变年、月都要更新日数据
if(this.fields == 'month') return;
if (columnIndex == 0 || columnIndex == 1) {
// 当前选择的日
let dayBeforeUpdate = this.range[2][this.value[2]];
// 更新日数据
this.setDayData(this.range[0][this.value[0]].number, this.range[1][this.value[1]].number);
// 更新 Day Value
let dayIndex = this.range[2].findIndex(day => {
return day.number == dayBeforeUpdate.number;
});
this.value.splice(2, 1, dayIndex >= 0 ? dayIndex : 0);
}
// 改变年、月、日都要更新时数据
if(this.fields == 'day') return;
if (columnIndex == 0 || columnIndex == 1 || columnIndex == 2) {
// 当前选择的时
let hourBeforeUpdate = this.range[3][this.value[3]];
// 更新时数据
this.setHourData(this.range[0][this.value[0]].number, this.range[1][this.value[1]].number, this.range[2][this.value[2]].number);
// 更新 Hour Value
let hourIndex = this.range[3].findIndex(hour => {
return hour.number == hourBeforeUpdate.number;
});
this.value.splice(3, 1, hourIndex >= 0 ? hourIndex : 0);
}
// 当前选择的分
if(this.fields == 'hour') return;
let minuteBeforeUpdate = this.range[4][this.value[4]];
// 更新分数据
this.setMinuteData(this.range[0][this.value[0]].number, this.range[1][this.value[1]].number, this.range[2][this.value[2]].number, this.range[3][this.value[3]].number);
// 更新 Minute Value
let minuteIndex = this.range[4].findIndex(minute => {
return minute.number == minuteBeforeUpdate.number;
});
this.value.splice(4, 1, minuteIndex >= 0 ? minuteIndex : 0);
},
}
};
</script>
<style lang="scss" scoped>
.content {
text-align: right;
}
.placeholder {
color: #949596;
}
</style>
@@ -0,0 +1,30 @@
### 组件说明
* 日期时间选择器
* 组件的默认日期有效期范围为:"1970-01-01 00:00" - "2300-01-01 00:00"。
* 注意:如果您传递的日期有效范围的结束日期小于开始日期,则日期有效范围的结束日期会自动修正为开始日期+300年,比如,您传递的日期有效范围的开始日期为 "2020-11-11 18:30"
* 结束日期为 "2018-11-11 18:30",则此时将会自动修正结束日期为 "2320-11-11 18:30"。
* 注意:如果您传递的默认值不在日期有效范围内,则会自动修正默认值为当前日期时间,如果当前日期时间也不在日期有效范围内,则会再次修正为日期有效范围的开始日期。
* 注意:该组件用到了我自己封装的 utils.js,位置在 '@/common/js/utils.js'。
### 插件 props 属性
* disabled: 是否禁用该组件?Boolean类型;
* placeholder: 组件没有选中值时显示的内容,String类型;
* start: 表示有效日期时间范围的开始,String类型,格式为 "YYYY-MM-DD hh:mm"
* end: 表示有效日期时间范围的结束,String类型,格式必为 "YYYY-MM-DD hh:mm"
* fields: 选择器的粒度,String类型,有效值为 year、month、day、hour、minute
* defaultValue: 默认值,String类型,格式为 "YYYY-MM-DD hh:mm"
### 插件事件
- change(date):选择日期时间后的回调事件。
* date.YYYY: 年;
* date.M: 月;
* date.MM: 月(补0);
* date.D: 日;
* date.DD: 日(补0);
* date.h: 时;
* date.hh: 时(补0);
* date.m: 分;
* date.mm: 分(补0);
* date.dt: Date对象;
* ... 还有一些其他的字段,具体看返回值吧!
+348
View File
@@ -0,0 +1,348 @@
/**
* @说明:工具集
* @作者:陈万照
* @公司:山东标梵互动技术有限公司
* @官网:http://biaofun.com/
* @版本:v1.0.0
* @时间:2020年4月28日11:28:13
*/
export default {
/**
* 同步 try catch 的进一步封装处理
* 使用方法:
* let [err, res] = await this.$utils.asyncTasks(Promise函数);
* if(res) 成功
* if(err) 失败
*/
asyncTasks(promise) {
return promise.then(data => {
return [null, data];
}).catch(err => [err]);
},
/**
* 精确判断数据是否是 Object 类型
* @param {Any} val 要判断的数据
* @returns {Boolean} true:是;false:不是;
*/
isObject(val) {
return Object.prototype.toString.call(val) === '[object Object]' && val !== null && val !== undefined;
},
/**
* 判断数据是否是 Array 类型
* @param {Any} val 要判断的数据
* @returns {Boolean} true:是;false:不是;
*/
isArray(val) {
return Object.prototype.toString.call(val) === '[object Array]';
},
/**
* 判断数据是否是 String 类型
* @param {Any} val 要判断的数据
* @returns {Boolean} true:是;false:不是;
*/
isString(val) {
return Object.prototype.toString.call(val) === '[object String]';
},
/**
* 精确判断数据是否是 Date 类型
* @param {Any} val 要判断的数据
* @returns {Boolean} true:是;false:不是;
*/
isDate(val) {
return Object.prototype.toString.call(val) === '[object Date]';
},
/**
* 精确判断数据是否是 Function 类型
* @param {Any} val 要判断的数据
* @returns {Boolean} true:是;false:不是;
*/
isFunction(val) {
return Object.prototype.toString.call(val) === '[object Function]';
},
/**
* 精确判断数据是否是 Number 类型
* @param {Any} val 要判断的数据
* @returns {Boolean} true:是;false:不是;
*/
isNumber(val) {
return Object.prototype.toString.call(val) === '[object Number]';
},
/**
* 精确判断数据是否是 Boolean 类型
* @param {Any} val 要判断的数据
* @returns {Boolean} true:是;false:不是;
*/
isBoolean(val) {
return Object.prototype.toString.call(val) === '[object Boolean]';
},
/**
* 判断 URL 是否是绝对 URL。
* @param {String} url 要判断的 URL
* @return {Boolean} true:是绝对URLfalse:不是绝对URL
*/
isAbsoluteURL(url) {
// 如果 URL 以 “<scheme>//” 或 “//”(协议相对URL)开头,则认为它是绝对的
// RFC 3986 将方案名称定义为以字母开头的字符序列,然后是字母,数字,加号,句点或连字符的任意组合
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
},
/**
* 合并 baseURL 和相对 URL 成一个完整的 URL
* @param {String} baseURL baseURL
* @param {String} relativeURL 相对 URL
* @returns {String} 返回组合后的完整 URL
*/
combineURLs(baseURL, relativeURL) {
return relativeURL && this.isString(relativeURL) && this.isString(baseURL) ? baseURL.replace(/\/+$/, '') + '/' +
relativeURL.replace(/^\/+/, '') : baseURL;
},
/**
* 深度合并对象,只支持合并两个对象,该方法不会改变原有的对象
* @param {Object} FirstOBJ 第一个对象
* @param {Object} SecondOBJ 第二个对象
* @return {Object} 返回深度合并后的对象
*/
deepMargeObject(FirstOBJ, SecondOBJ) {
let ResultOBJ = {};
for (let key in FirstOBJ) {
ResultOBJ[key] = ResultOBJ[key] && ResultOBJ[key].toString() === "[object Object]" ? this.deepMargeObject(ResultOBJ[
key], FirstOBJ[key]) : ResultOBJ[key] = FirstOBJ[key];
}
for (let key in SecondOBJ) {
ResultOBJ[key] = ResultOBJ[key] && ResultOBJ[key].toString() === "[object Object]" ? this.deepMargeObject(ResultOBJ[
key], SecondOBJ[key]) : ResultOBJ[key] = SecondOBJ[key];
}
return ResultOBJ;
},
/**
* 生成指定长度的随机字符串
* @param {Number} min 最小程度
* @param {Number} max 最大长度
* @return {String} 返回生成的字符串
*/
randomString(min, max) {
let returnStr = "",
range = (max ? Math.round(Math.random() * (max - min)) + min : min),
arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
];
for (let i = 0; i < range; i++) {
let index = Math.round(Math.random() * (arr.length - 1));
returnStr += arr[index];
}
return returnStr;
},
/**
* 格式化日期
* @param {Date|String} date 日期或日期字符串
*/
formatDate(date) {
let YYYY = null;
let M = null;
let MM = null;
let D = null;
let DD = null;
let h = null;
let hh = null;
let m = null;
let mm = null;
let s = null;
let ss = null;
let ms = null;
let ms2 = null;
let ms3 = null;
let ms4 = null;
let dt = null;
// 如果 date 是 String 类型
if (date && this.isString(date)) {
// 真机运行时,如果直接用 new Date('YYYY-MM-DD hh:mm:ss') 会报 Invalid Date 错误,所以采用下面的方式创建日期
let dtArr = date.replace(/\//g, '.').replace(/-/g, '.').replace(/:/g, '.').replace(/T/g, ' ').replace(' ', '.').replace(
'Z', '').split('.');
let year = 2020;
let month = 12;
let day = 18;
let hour = 0;
let minute = 0;
let second = 0;
let millisecond = 0;
// 年
if (dtArr.length > 0 && !isNaN(dtArr[0])) {
year = parseInt(dtArr[0]);
}
// 月
if (dtArr.length > 1 && !isNaN(dtArr[1])) {
month = parseInt(dtArr[1]);
}
// 日
if (dtArr.length > 2 && !isNaN(dtArr[2])) {
day = parseInt(dtArr[2]);
}
// 时
if (dtArr.length > 3 && !isNaN(dtArr[3])) {
hour = parseInt(dtArr[3]);
}
// 分
if (dtArr.length > 4 && !isNaN(dtArr[4])) {
minute = parseInt(dtArr[4]);
}
// 秒
if (dtArr.length > 5 && !isNaN(dtArr[5])) {
second = parseInt(dtArr[5]);
}
// 毫秒
if (dtArr.length > 6 && !isNaN(dtArr[6])) {
millisecond = parseInt(dtArr[6]);
}
date = new Date(year, month - 1, day, hour, minute, second, millisecond);
}
// 如果 date 是 Date 类型
if (date && this.isDate(date)) {
YYYY = date.getFullYear();
M = date.getMonth() + 1;
MM = M >= 10 ? M : '0' + M;
D = date.getDate();
DD = D >= 10 ? D : '0' + D;
h = date.getHours();
hh = h >= 10 ? h : '0' + h;
m = date.getMinutes();
mm = m >= 10 ? m : '0' + m;
s = date.getSeconds();
ss = s >= 10 ? s : '0' + s;
ms = date.getMilliseconds();
ms2 = ms;
ms3 = ms;
ms4 = ms;
if (ms < 10) {
ms2 = '0' + ms;
ms3 = '00' + ms;
ms4 = '000' + ms;
} else if (ms < 100) {
ms3 = '0' + ms;
ms4 = '00' + ms;
} else {
ms4 = '0' + ms;
}
}
// 返回的数据对象
let result = {
YYYY: YYYY,
MM: MM,
M: M,
DD: DD,
D: D,
hh: hh,
h: h,
mm: mm,
m: m,
ss: ss,
s: s,
ms: ms,
ms2: ms2,
ms3: ms3,
ms4: ms4,
dt: date,
f1: `${YYYY}-${MM}-${DD}`,
f2: `${YYYY}${M}${D}`,
f3: `${YYYY}-${M}-${D} ${hh}:${mm}`,
f4: `${h}:${m}:${s}`,
f5: `${MM}-${DD}`,
f6: `${YYYY}-${MM}`,
f7: `${YYYY}${M}`,
f8: `${h}:${m}`,
f9: `${M}${D}`,
notes: 'YYYY(年),MM(月,补0),M(月,不补0),DD(日,补0),D(日,不补0),hh(时,补0),h(时,不补0),mm(分,补0),m(分,不补0),ss(秒,补0),s(秒,不补0),ms(毫秒,不补0),ms2(毫秒,补0到2位),ms3(毫秒,补0到3位),ms4(毫秒,补0到4位),其余的f1,f2,... 看格式就知道了!'
};
return result;
},
/**
* 数字转中文
* @param {Number} num 数字
*/
numberToChinese(num) {
if (!/^\d*(\.\d*)?$/.test(num)) return "Number is wrong!";
let AA = new Array("零", "一", "二", "三", "四", "五", "六", "七", "八", "九");
let BB = new Array("", "十", "百", "千", "万", "亿", "点", "");
let a = ("" + num).replace(/(^0*)/g, "").split("."),
k = 0,
re = "";
for (let i = a[0].length - 1; i >= 0; i--) {
switch (k) {
case 0:
re = BB[7] + re;
break;
case 4:
if (!new RegExp("0{4}\\d{" + (a[0].length - i - 1) + "}$").test(a[0]))
re = BB[4] + re;
break;
case 8:
re = BB[5] + re;
BB[7] = BB[5];
k = 0;
break;
}
if (k % 4 == 2 && a[0].charAt(i + 2) != 0 && a[0].charAt(i + 1) == 0) re = AA[0] + re;
if (a[0].charAt(i) != 0) re = AA[a[0].charAt(i)] + BB[k % 4] + re;
k++;
}
if (a.length > 1) //加上小数部分(如果有小数部分)
{
re += BB[6];
for (let i = 0; i < a[1].length; i++) re += AA[a[1].charAt(i)];
}
return re;
},
/**
* 计算两个经纬度点之间的距离
* @param {Number} lng1 第一个点的经度
* @param {Number} lat1 第一个点的纬度
* @param {Number} lng2 第二个点的经度
* @param {Number} lat2 第二个点的纬度
*/
calcDistance(lng1, lat1, lng2, lat2) {
var radLat1 = lat1 * Math.PI / 180.0;
var radLat2 = lat2 * Math.PI / 180.0;
var a = radLat1 - radLat2;
var b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +
Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
s = s * 6378.137; // EARTH_RADIUS;
s = Math.round(s * 10000) / 10000;
return s;
},
/**
* 获取数组最小值的下标
* @param {Array} arr 数组
*/
getArrayMixValueIndex(arrar) {
let min = arrar[0];
let index = 0;
for (let i = 0; i < arrar.length; i++) {
if (min > arrar[i]) {
min = arrar[i];
index = i;
}
}
return index;
}
}
+32
View File
@@ -0,0 +1,32 @@
import Vue from 'vue'
import App from './App'
import layOut from 'pages/components/layout.vue';
import store from './store'
import Socket from 'pages/api/socket.js';
import {
cache
} from './pages/utils/uniApi.js'
Vue.prototype.$store = store
Vue.prototype.$cache = cache
// 挂载 socket
Vue.prototype.scoket = new Socket();
// 挂载音频文件
Vue.prototype.$onlineAudio = uni.createInnerAudioContext();
Vue.prototype.$onlineAudio.src = './static/video/notice.wav'; // 上线通知
Vue.component('layOut', layOut);
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App,
store
})
app.$mount()
+173
View File
@@ -0,0 +1,173 @@
{
"name" : "CRMChat",
"appid" : "__UNI__5EA211F",
"description" : "CRM客服",
"versionName" : "1.2",
"versionCode" : 170,
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {
"Push" : {}
},
"privacy" : {
"prompt" : "template",
"template" : {
"title" : "服务协议和隐私政策",
"message" : "请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/><a href='https://chat.crmeb.net/chat.html'>《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
"buttonAccept" : "同意",
"buttonRefuse" : "拒绝并退出App",
"second" : {
"title" : "温馨提示",
"message" : "进入应用前,你需先同意<a href='https://chat.crmeb.net/chat.html'>《隐私政策》</a>,否则将退出应用。",
"buttonAccept" : "同意并继续",
"buttonRefuse" : "退出应用"
}
}
},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.READ_SMS\"/>",
"<uses-permission android:name=\"android.permission.SEND_SMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SMS\"/>"
],
"autoSdkPermissions" : false
},
/* ios */
"ios" : {
"idfa" : false,
"privacyDescription" : {
"NSPhotoLibraryUsageDescription" : "为了保证您在使用本客户端时,能够正常加载H5页面、启动页视频以及在线升级客户端新版本,我们需要您授权我们接收您手机终端的图片、声音、视频内容,并获取终端存储功能",
"NSPhotoLibraryAddUsageDescription" : "为了保证您在使用本客户端时,能够正常加载H5页面、启动页视频以及在线升级客户端新版本,我们需要您授权我们接收您手机终端的图片、声音、视频内容,并获取终端存储功能",
"NSCameraUsageDescription" : "为了保证您能够正常使用本客户端的扫一扫、与拍像相关的功能,我们会在您授权后获取您的相机权限",
"NSLocationAlwaysUsageDescription" : "根据客户地理位置推荐最近门店",
"NSLocalNetworkUsageDescription" : "为了更好的用户消息提现,我们需要访问本地网络"
},
"capabilities" : {
"entitlements" : {
"com.apple.developer.associated-domains" : [ "applinks:static-b7af47aa-37fd-4e93-be96-e14553f3f3e2.bspapp.com" ]
}
}
},
/* SDK */
"sdkConfigs" : {
"push" : {
"unipush" : {}
},
"ad" : {},
"oauth" : {
"weixin" : {
"appid" : "",
"appsecret" : "",
"UniversalLinks" : "https://static-b7af47aa-37fd-4e93-be96-e14553f3f3e2.bspapp.com/uni-universallinks/__UNI__8D7FD81"
}
},
"payment" : {
"alipay" : {
"__platform__" : [ "ios", "android" ]
},
"weixin" : {
"__platform__" : [ "ios", "android" ],
"appid" : "",
"UniversalLinks" : "https://static-b7af47aa-37fd-4e93-be96-e14553f3f3e2.bspapp.com/uni-universallinks/__UNI__8D7FD81"
}
}
},
"icons" : {
"android" : {
"hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
}
},
"nativePlugins" : {}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"networkTimeout" : {
"connectSocket" : 1000
},
"h5" : {
"router" : {
"mode" : "history"
}
},
"_spaceID" : "b7af47aa-37fd-4e93-be96-e14553f3f3e2"
}
+277
View File
@@ -0,0 +1,277 @@
{
"easycom": {
"autoscan": true
},
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/view/messageList/index",
"style": {
"navigationBarTitleText": "加载中...",
"app-plus": {
"navigationStyle": "default",
"titleView": true,
"bounce": "none",
"navigationBarTextStyle": "write",
"disableScroll": true,
"navigationBarBackgroundColor": "#1890FC",
"titleNView": {
"buttons": [{
"fontSize": "20px",
"float": "right",
"fontSrc":"/static/font/iconfont.ttf",
"text": "\ue732"
}]
}
}
}
},
{
"path": "pages/view/customerServer/index",
"style": {
"navigationBarTitleText": "客服工作台",
"app-plus": {
"navigationStyle": "default",
"titleView": false,
"navigationBarTextStyle": "write",
"disableScroll": true,
"navigationBarBackgroundColor": "#1890FC",
"titleNView": {
"buttons": [{
"float": "right",
"type": "menu"
}]
}
}
}
},
{
"path": "pages/view/list/addbook",
"style": {
"navigationBarTitleText": "客户列表",
"app-plus": {
"navigationStyle": "default",
"navigationBarTextStyle": "write",
"disableScroll": true,
"navigationBarBackgroundColor": "#1890FC",
"titleNView": {
"buttons": [{
"fontSize": "14px",
"float": "right",
// "fontSrc":"/static/font/iconfont.ttf",
"text": "筛选",
"width": "50px"
// "fontWeight": "bold"
}]
}
}
}
},
{
"path": "pages/view/authReply/index",
"style": {
"navigationBarTitleText": "自动回复",
"app-plus": {
"navigationStyle": "default",
"navigationBarTextStyle": "write",
"navigationBarBackgroundColor": "#1890FC"
}
}
},
{
"path": "pages/view/setTags/index",
"style": {
"navigationBarTitleText": "标签",
"app-plus": {
"navigationStyle": "default",
"titleView": false,
"navigationBarTextStyle": "black",
"disableScroll": true,
"navigationBarBackgroundColor": "#EEEEEE"
}
}
},
{
"path": "pages/view/setTags/addTag",
"style": {
"navigationBarTitleText": "标签详情",
"app-plus": {
"navigationStyle": "default",
"titleView": false,
"navigationBarTextStyle": "black",
"disableScroll": true,
"navigationBarBackgroundColor": "#EEEEEE"
}
}
},
{
"path": "pages/view/statistics/index",
"style": {
"navigationBarTitleText": "统计",
"app-plus": {
"navigationStyle": "default",
"titleView": false,
"navigationBarTextStyle": "write",
"disableScroll": true,
"navigationBarBackgroundColor": "#1890FC"
}
}
},
{
"path": "pages/view/user/index",
"style": {
"navigationBarTitleText": "我的",
"app-plus": {
"navigationStyle": "default",
"titleView": false,
"navigationBarTextStyle": "write",
"disableScroll": true,
"navigationBarBackgroundColor": "#1890FC"
}
}
},
{
"path": "pages/view/login/index",
"style": {
"navigationBarTitleText": "登录页",
"app-plus": {
"navigationStyle": "custom",
"titleView": false,
"navigationBarTextStyle": "write"
}
}
},
{
"path": "pages/view/authorizedLogin/index",
"style": {
"navigationBarTitleText": "授权登录",
"app-plus": {
"navigationStyle": "default",
"titleView": false,
"navigationBarTextStyle": "black"
}
}
},
{
"path": "pages/view/dominName/index",
"style": {
"navigationBarTitleText": "填写域名",
"app-plus": {
"navigationStyle": "custom",
"titleView": false,
"navigationBarTextStyle": "black"
}
}
},
{
"path": "pages/view/customerServer/script",
"style": {
"navigationBarTitleText": "常用语",
"app-plus": {
"navigationStyle": "custom",
"titleView": false,
"navigationBarTextStyle": "black"
}
}
},
{
"path": "pages/view/user/setting/setting",
"style": {
"navigationBarTitleText": "设置",
"app-plus": {
"navigationStyle": "default",
"navigationBarTextStyle": "write",
"disableScroll": true,
"navigationBarBackgroundColor": "#1890FC",
"titleNView": {
"buttons": [{
"type":"none",
"fontSize": "14px",
"float": "right",
// "fontSrc":"/static/font/iconfont.ttf",
"text": "保存",
"width": "50px"
}]
}
}
}
},
{
"path": "pages/view/customerServer/customerMessage",
"style": {
"navigationBarTitleText": "客户信息",
"app-plus": {
"navigationStyle": "default",
"navigationBarTextStyle": "write",
"navigationBarBackgroundColor": "#1890FC",
"titleNView": {
"buttons": [{
"type":"none",
"fontSize": "14px",
"float": "right",
// "fontSrc":"/static/font/iconfont.ttf",
"text": "保存",
"width": "50px"
}]
}
}
}
},
{
"path": "pages/view/customerServer/complaint",
"style": {
"navigationBarTitleText": "投诉",
"app-plus": {
"navigationStyle": "default",
"titleView": false,
"navigationBarTextStyle": "black"
}
}
},
{
"path": "pages/view/feedback/index",
"style": {
"navigationBarTitleText": "客户反馈",
"app-plus": {
"navigationStyle": "default",
"titleView": false,
"navigationBarTextStyle": "black"
}
}
},
{
"path": "pages/view/privacyAgreement/index",
"style": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "隐私协议"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#f7f7f7",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#707070",
"selectedColor": "#707070",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"height": "50px",
"fontSize": "12px",
"iconWidth": "24px",
"spacing": "3px",
"list": [{
"pagePath": "pages/view/messageList/index",
"iconPath": "static/image/footer/chatUnSelcect.png",
"selectedIconPath": "static/image/footer/chatSelect.png",
"text": "聊天"
}, {
"pagePath": "pages/view/user/index",
"iconPath": "static/image/footer/myUnSelect.png",
"selectedIconPath": "static/image/footer/mySelect.png",
"text": "我的"
}]
}
}
+41
View File
@@ -0,0 +1,41 @@
import apiSetting from './apiSetting.js';
import login from './apiModel/login.js';
import message from './apiModel/message.js';
import userProgAdministration from "./apiModel/userProgAdministration.js";
import statistics from './apiModel/statistics.js';
import personalCenter from './apiModel/personalCenter.js';
const api = {
...login,
// 聊天列表
...message,
// 客户管理
...userProgAdministration,
// 统计
...statistics,
// 个人中心
...personalCenter
}
if (process.env.NODE_ENV === 'production') {
Object.keys(api).forEach((item) => {
// api[item]['url'] = apiSetting.productionApiUrl + '/api/' + api[item]['url'];
api[item]['url'] = '/api/' + api[item]['url'];
});
} else {
Object.keys(api).forEach((item) => {
// api[item]['url'] = apiSetting.developeApiUrl + '/api/'+ api[item]['url'];
if (api[item]['url'].indexOf('/api/') === -1) {
api[item]['url'] = '/api/' + api[item]['url'];
}
});
}
export default api;
+27
View File
@@ -0,0 +1,27 @@
const api = {
// 客服登录
login: {
url: 'kefu/login',
method: 'post',
loading: true,
noAuth: true
},
// 退出登录
userLogout: {
url: 'kefu/user/logout',
method: 'post'
},
// 发送CID
userClient: {
url: 'kefu/user/client',
method: 'put'
},
// 授权登录
serviceCode: {
url: 'kefu/service/code',
method: 'post'
}
}
export default api;
+106
View File
@@ -0,0 +1,106 @@
// 聊天界面接口
const customerServer = {
pingNetWork:{
url:'kefu/service/ping',
noAuth:true,
method:'get',
timeout:1000
},
kefuServiceList: {
url: 'kefu/service/list',
method: 'get'
},
getSendId: {
url: 'kefu/service/get_send_id',
method: 'get'
},
sendMessage:{
url: 'kefu/service/send_message',
method: 'post'
},
// 获取转接客服列表
serviceTransferList: {
url: 'kefu/service/transfer_list',
method: 'get'
},
// 确认转接
serviceTransfer: {
url: 'kefu/service/transfer',
method: 'post'
},
// 查询客服话术
serviceSpeechcraft: {
url: 'kefu/service/speechcraft',
method: 'get'
},
// 获取话术分类
kefuServiceCate: {
url: 'kefu/service/cate',
method: 'get'
},
// 添加话术
postServiceSpeechcraft: {
url: 'kefu/service/speechcraft',
method: 'post'
},
// 修改话术
putServiceSpeechcraft: {
url: 'kefu/service/speechcraft',
method: 'put',
queryKey: ['id']
},
// 删除话术
deleteServiceSpeechcraft: {
url: 'kefu/service/speechcraft',
method: 'delete',
queryKey: ['id'],
canNotInputQuery: true
},
// 修改分组
putServiceCate: {
url: 'kefu/service/cate',
method: 'put',
queryKey: ['id']
},
// 删除分组
deleteServiceCate: {
url: 'kefu/service/cate',
method: 'delete',
queryKey: ['id']
}
}
const api = {
// 获取客户列表
userList: {
url: 'kefu/user/list',
method: 'get'
},
// 获取聊过天的用户
userRecord: {
url: 'kefu/user/record',
method: 'get'
},
// 获取客服未读条数
userCount: {
url: 'kefu/user/count',
method: 'get'
},
// 获取所有聊天消息id
userRecordAll: {
url: 'kefu/user/recordAll',
method: 'get'
},
// 删除聊天列表用户
deleteUserRecord: {
url: 'kefu/user/record',
method: 'delete',
queryKey: ['id']
},
...customerServer
};
export default api;
@@ -0,0 +1,44 @@
const api = {
// 获取当前登录客服信息
userUserInfo: {
url: 'kefu/user/userInfo',
method: 'get'
},
// 上传头像
kefuUploadAvatar: {
url: 'kefu/upload',
method: 'post',
form: true
},
// 确认信息修改
putKefuUserUserInfo: {
url: 'kefu/user/userInfo',
method: 'put'
},
// 客服信息反馈
userFeedback: {
url: 'kefu/user/feedback',
method: 'post'
},
// 获取投诉列表
userComplain: {
url: 'kefu/user/complain',
method: 'get'
},
// 保存客户投诉
postUserComplain: {
url: 'kefu/user/complain',
method: 'post'
},
// 拉黑用户
putUserStatus: {
url: 'kefu/user/status',
method: 'put',
queryKey: ['userId']
},
appVersion: {
url: 'kefu/version',
method:'post'
}
};
export default api;
+13
View File
@@ -0,0 +1,13 @@
const api = {
// 客户统计
statisticsAll: {
url: 'kefu/statistics/all',
method: 'get'
},
// 获取当前客服统计
statisticsIndex: {
url: 'kefu/statistics/index',
method: 'get'
}
};
export default api;
@@ -0,0 +1,105 @@
const api = {
// 客户列表
userList: {
url: 'kefu/user/list',
method: 'get'
},
// 客户全部标签
userLabe: {
url: 'kefu/user/label/all',
method: 'get'
},
// 客户已有标签,带有选中标识
userLabelSelect: {
url: 'kefu/user/label',
method: 'get'
},
// 设置用户标签
putUserLabel: {
url: 'kefu/user/label',
method: 'put',
queryKey: ['userId']
},
// 客户详细信息
userInfo: {
url: 'kefu/user/info',
method: 'get',
queryKey: ['user_id'],
canInputQuery: true // 有此选项时,并且值为true,代表可以在url上不可以携带参数
},
// 用户分组
UserGroup: {
url: 'kefu/user/group',
method: 'get'
},
// 设置用户分组
setUserGroup: {
url: 'kefu/user/group',
method: 'put',
queryKey: ['userId', 'id'],
canInputQuery: true
},
// 修改用户信息
userUpdateUser: {
url: 'kefu/user/updateUser',
method: 'put',
queryKey: ['userId']
},
// 删除单个用户标签
deleteUserLabel: {
url: 'kefu/user/label',
method: 'delete',
queryKey: ['userId', 'labelId'],
canInputQuery: true
},
// 添加分组
serviceCate: {
url: 'kefu/service/cate',
method: 'post'
},
userAgreement: {
url: 'kefu/agreement',
method: 'get',
noAuth: true
},
getAuthReply: {
url: 'kefu/service/auth_reply',
method: 'get'
},
saveAuthReply: {
url: 'kefu/service/auth_reply',
method: 'post',
queryKey: ['id'],
},
deleteAuthReply: {
url: 'kefu/service/auth_reply',
method: 'delete',
queryKey: ['id'],
canInputQuery: true
},
putAuthReply: {
url: 'kefu/service/auth_reply',
method: 'put',
queryKey: ['value'],
},
// 获取当前登录客服信息
kefuInfoApi: {
url: 'kefu/user/userInfo',
method: 'get',
},
savelog: {
url: 'kefu/user/savelog',
method: 'post'
},
setKefubackstage: {
url: 'kefu/service/backstage',
method: 'put',
queryKey: ['value'],
}
}
export default api;
+13
View File
@@ -0,0 +1,13 @@
import { cache } from 'pages/utils/uniApi.js';
const setting = {
// developeApiUrl: 'http://chat.lfmn.fun',
developeApiUrl: `${cache.get('requestFile')}://${cache.get('dominName')}`,
productionApiUrl: `${cache.get('requestFile')}://${cache.get('dominName')}`,
// productionApiUrl: location.origin + '/api' : '',
wsUrlDevelop: `${cache.get('wsFile')}://${cache.get('dominName')}/`,
wsUrlProduction: `${cache.get('wsFile')}://${cache.get('dominName')}/`,
// wsUrlProduction: `ws:${location.hostname}`
}
export default setting;
+136
View File
@@ -0,0 +1,136 @@
import {
Loading,
hideLoading,
Toast,
navigateTo,
cache
} from 'pages/utils/uniApi.js';
import apiHttp from 'pages/api/api.js';
import store from '../../store/index.js';
//检测是否去登录
async function toLogin() {
if (!store.state.token) {
if (cache.get('accountData')) {
let res = await http(apiHttp.login, cache.get('accountData'));
if (res.token) {
cache.set('userData', res);
cache.remove('pages_login');
store.commit('setToken', res.token);
return false;
}
}
if (!cache.get('pages_login')) {
navigateTo(3, '/pages/view/login/index');
}
return true;
}
return false;
}
function getHttpUrl() {
let requestFile = cache.get('requestFile');
let dominName = cache.get('dominName');
return `${cache.get('requestFile')}://${cache.get('dominName')}`;
}
//检测并执行去设置域名
function checkHttpUrl() {
let requestFile = cache.get('requestFile');
let dominName = cache.get('dominName');
let res = !!requestFile && !!dominName;
if (!res) {
cache.remove('pages_dominName');
if (!cache.get('pages_dominName')) {
navigateTo(1, '/pages/view/dominName/index');
}
}
return res;
}
function http(api, data, base) {
let baseUrl = getHttpUrl();
let url = (base ? base : baseUrl) + api.url;
// 是否在url上挂载单独参数时,也可以传输数据 eg: /api/{12}/ false 代表可传输,true 代表不可传输
let canNotInputQuery = api.canNotInputQuery ? api.canNotInputQuery : false
if (api.queryKey instanceof Array && api.queryKey.length) {
url += api.queryKey.map(item => `/${data[item]}`).join('')
}
let post_data = {
url: url,
data: api.canNotInputQuery ? '' : data,
header: {
'content-type': api.form ? 'application/x-www-form-urlencoded' : 'application/json',
'Authori-zation': 'Bearer ' + store.state.token
},
method: api.method.toUpperCase(),
timeout: api.timeout || 3000,
}
return new Promise(async (reslove, reject) => {
//检测域名是否存在
if (!checkHttpUrl()) {
return reject({
msg: '域名填写错误, 请核实并修改域名'
})
}
if (api.loading) {
Loading();
}
//检测是否登录
if (api.noAuth === undefined && await toLogin()) {
return reject({
msg: '请登录'
})
}
uni.request({
...post_data,
success: (data) => {
switch (data.data.status) {
case 200:
reslove(data.data.data);
break;
case 400:
Toast(data.data.msg)
reject(data.data);
break;
case 410003:
// token失效后,自动登录
if (cache.get('accountData')) {
http(apiHttp.login, cache.get('accountData')).then(res => {
cache.set('userData', res);
store.commit('setToken', res.token);
}).catch(res => {
if (!cache.get('pages_login')) {
navigateTo(2, '/pages/view/login/index');
}
})
} else {
if (!cache.get('pages_login')) {
navigateTo(2, '/pages/view/login/index');
}
}
break;
default:
Toast(data.data.msg)
reject(data.data);
break;
}
},
fail: (rej) => {
reject(rej);
},
complete: () => {
if (api.loading) {
hideLoading();
}
}
});
})
}
export default http;
+59
View File
@@ -0,0 +1,59 @@
import http from '@/pages/api/index.js'
import api from '@/pages/api/api.js'
export function onNetworkStatusChange(onlineFun, offlineFun) {
//#ifdef H5
function addHandler(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
}
addHandler(window, 'online', () => {
onlineFun()
})
addHandler(window, 'offline', () => {
offlineFun()
});
//#endif
//#ifdef APP-PLUS
uni.onNetworkStatusChange(res => {
if (res.networkType != 'none') {
onlineFun(res);
} else {
offlineFun(res);
}
});
//#endif
}
let NetWork = null;
export function onNetworkStatusChangeV2(onlineFun, offlineFun) {
if (NetWork) {
clearInterval(NetWork);
NetWork = null;
}
let online = null,
offline = null;
NetWork = setInterval(() => {
http(api.pingNetWork).then(() => {
if (online === null) {
onlineFun();
online = true;
}
offline = null;
}).catch(() => {
if (offline === null) {
offlineFun();
offline = true;
}
online = null;
})
}, 1000);
}
+259
View File
@@ -0,0 +1,259 @@
import apiSetting from './apiSetting.js';
import {
cache
} from 'pages/utils/uniApi.js';
import store from '@/store';
import {
onNetworkStatusChange,
onNetworkStatusChangeV2
} from './network.js'
class Socket {
constructor() {
this.socketTask = null; //socket 任务
this.timer = null; //心跳定时器
this.connectStatus = false; //连接状态
this.wsUrl = ''; //ws地址
this.reconnectTimer = 2000; //重连
this.handClse = false; //手动关闭
this.reconnetime = null; //重连 定时器
this.networkStatus = true;
this.connectLing = false; //连接是否进行中
this.defaultEvenv(); //执行默认事件
}
//网络状态变化监听
networkEvent() {
onNetworkStatusChange(() => {
console.log('有网了')
this.networkStatus = true;
uni.$on('timeout', this.timeoutEvent.bind(this))
}, () => {
console.log('断网了')
this.networkStatus = false;
this.connectStatus = false;
clearInterval(this.timer);
this.timer = null;
this.socketTask && this.socketTask.close();
});
}
//开始连接
startConnect() {
console.log('开始链接')
this.networkEvent();
this.handClse = false;
if (!this.connectStatus) {
this.init();
this.connect();
}
}
//默认事件
defaultEvenv() {
uni.$off('success', this.successEvent);
uni.$off('timeout', this.timeoutEvent)
uni.$off('recored', this.recoredEvent);
uni.$off('online', this.onlineEvent);
uni.$on('success', this.successEvent.bind(this));
uni.$on('timeout', this.timeoutEvent.bind(this))
uni.$on('recored', this.recoredEvent.bind(this));
uni.$on('online', this.onlineEvent.bind(this));
}
messageListEvent() {
}
timeoutEvent() {
this.reconne();
}
recoredEvent(data) {
store.commit('setChatInfo', data);
}
successEvent() {
console.log('success默认事件');
this.changOnline();
this.openLaterEvent()
}
onlineEvent(data) {
store.commit('setChatOnline', data);
}
//发送用户状态
changOnline() {
let online = cache.get('kefu_online')
if (online !== undefined && online !== '') {
this.send({
data: {
online: online
},
type: 'online'
});
}
}
//打开连接后的事件
openLaterEvent() {
if (store.state.chatList) {
Object.keys(store.state.chatList).map(item => {
store.state.chatList[item].map(value => {
this.send({
type: 'chat',
data: {
msn: value.msn,
type: value.msn_type,
to_user_id: value.to_user_id,
guid: value.guid
}
}).then(() => {
store.commit('delChatList', {
id: item,
guid: value.guid
})
})
})
})
}
}
//连接websocket
connect() {
this.connectLing = true;
this.socketTask = uni.connectSocket({
url: this.wsUrl,
complete: () => {}
});
this.socketTask.onOpen(this.onOpen.bind(this))
this.socketTask.onError(this.onError.bind(this));
this.socketTask.onMessage(this.onMessage.bind(this))
this.socketTask.onClose(this.onClose.bind(this));
}
init() {
let wsUrl = `${cache.get('wsFile')}://${cache.get('dominName')}/ws`
wsUrl += `?type=kefu&app=1`
this.wsUrl = `${wsUrl}&token=${store.state.token}&form=app&client_id=${cache.get('cid')}`
}
onOpen() {
clearInterval(this.reconnetime);
this.reconnetime = null;
this.connectLing = false;
this.connectStatus = true;
this.ping();
}
onError(error) {
console.log('连接发生错误', error)
this.connectStatus = false;
this.connectLing = false;
this.reconne();
}
//断线重连
reconne() {
if (this.reconnetime || this.connectStatus) {
return;
}
this.reconnetime = setInterval(() => {
if (this.connectStatus) {
return;
}
this.connectLing || this.connect();
}, this.reconnectTimer);
}
onClose() {
console.log('关闭连接')
uni.$emit('close');
//手动关闭不用重新连接
if (this.handClse) {
return;
}
clearInterval(this.timer);
this.timer = null;
this.connectStatus = false;
this.connectLing = false;
this.reconne();
}
ping() {
this.timer = setInterval(() => {
this.send({
type: 'ping'
})
}, 10000)
}
onMessage(response) {
let {
type,
data
} = JSON.parse(response.data);
// console.log(type, data);
uni.$emit(type, data);
}
send(data) {
let that = this;
//没有网络,或者没有连接
if (!this.connectStatus || !this.networkStatus) {
this.reconne();
}
return new Promise((reslove, reject) => {
this.socketTask.send({
data: JSON.stringify(data),
success() {
reslove();
},
fail(res) {
console.log(res)
if (res.errMsg ==
'sendSocketMessage:fail WebSocket is not connected' ||
res.errMsg ==
'sendSocketMessage:fail Error: SocketTask.readyState is not OPEN'
) {
that.reconne();
}
reject(res);
},
complete(res) {
console.log(res)
}
})
});
}
guid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
clearPing() {
clearInterval(this.timer);
this.timer = null;
if (this.connectStatus) {
this.socketTask.close();
}
this.handClse = true;
this.connectStatus = false;
this.connectLing = false;
}
setBadgeNumber(count) {
//#ifdef APP-PLUS
plus.runtime.setBadgeNumber(Number(count));
//#endif
}
}
export default Socket;
+286
View File
@@ -0,0 +1,286 @@
<template>
<view class="layout" :class="{ noBottomHeight: noBottomHeight }">
<view class="layout_header">
<view v-if="isShowHeader" class="global_title" :style="{ 'background-color': headerColor, color: headerTextColor }">
<view class="goBack" @click="goBack"><span class="iconfont">&#xe6c4;</span></view>
<view class="title_message">{{ titleName ? titleName : getCurRoute.$holder.navigationBarTitleText }}</view>
<view><slot name="hander_handle"></slot></view>
</view>
<slot v-else name="header"></slot>
</view>
<view class="layout_content">
<!-- <slot name="content"></slot> -->
<scroll-view
:key="refresherTriggered"
:scroll-with-animation="scrollWithAnimation"
scroll-anchoring
:scroll-y="true"
:refresher-enabled="refresherEnabled"
@refresherpulling="onPulling"
@refresherrefresh="refresherrefresh"
:refresher-triggered="refresherTriggered"
:refresher-threshold="refresherThreshold"
@refresherrestore="refresherrestore"
@refresherabort="refresherabort"
class="layout_content_scroll"
:scroll-top="scrollTop"
@scrolltolower="scrolltolower"
@scrolltoupper="scrolltoupper"
>
<slot name="content" class="slot-content"></slot>
</scroll-view>
</view>
<view class="layout_bottom">
<view v-if="isShowTap" class="footer">
<view class="footer_item" v-for="(item, index) in footerData" :key="index" @click="toRouter(item)">
<view class="footer_item_image"><image :src="getCurRoute.route == item.path ? item.selectImage : item.unSelectImage" mode="widthFix"></image></view>
<view class="footer_item_tip">
<span>{{ item.title }}</span>
</view>
<view class="footer_message" v-if="item.id == 1 && unreadMessage">{{ unreadMessage }}</view>
</view>
</view>
<slot v-else name="bottom"></slot>
</view>
</view>
</template>
<script>
import { navigateTo, navigateBack } from '../utils/uniApi.js';
export default {
props: {
// 是否减去底部高度
noBottomHeight: {
type: Boolean,
default: false
},
scrollWithAnimation:{
type: Boolean,
default: true
},
// 标题名称
titleName: {
type: String,
default: '设置'
},
// 下拉刷新阈值
refresherThreshold: {
type: Number,
default: 90
},
// 是否开启自定义下拉
refresherEnabled: {
type: Boolean,
default: false
},
// 下拉刷新状态
refresherTriggered: {
type: Boolean | String,
default: false
},
unreadMessage: {
type: Number | String,
default: 0
},
headerTextColor: {
type: String,
default: ''
},
headerColor: {
type: String,
default: ''
},
isShowHeader: {
type: Boolean,
default: false
},
isShowTap: {
type: Boolean,
default: false
},
scrollTop: {
type: Number,
default: 0
}
},
data() {
return {
selectId: 1,
footerData: [
{
title: '聊天',
selectImage: require('../../static/image/footer/chatSelect.png'),
unSelectImage: require('../../static/image/footer/chatUnSelcect.png'),
id: 1,
path: 'pages/view/messageList/index'
},
{
title: '客户',
selectImage: require('../../static/image/footer/userselect.png'),
unSelectImage: require('../../static/image/footer/userUnselect.png'),
id: 2,
path: 'pages/view/list/addbook'
},
{
title: '统计',
selectImage: require('../../static/image/footer/staticSelect.png'),
unSelectImage: require('../../static/image/footer/staticUnSelect.png'),
id: 3,
path: 'pages/view/statistics/index'
},
{
title: '我的',
selectImage: require('../../static/image/footer/mySelect.png'),
unSelectImage: require('../../static/image/footer/myUnSelect.png'),
id: 4,
path: 'pages/view/user/index'
}
]
};
},
created() {},
computed: {
getCurRoute() {
let routes = getCurrentPages(); // 获取当前打开过的页面路由数组
let curRoute = routes[routes.length - 1]; // 获取当前路由
return curRoute;
}
},
methods: {
// 滑动到底部
scrolltolower() {
this.$emit('goBottom');
},
// 滑动到顶部
scrolltoupper() {
this.$emit('goTop');
},
// 自定义下拉控件被下拉
onPulling(e) {
// console.log(e);
},
// 自定义下拉被触发
refresherrefresh() {
this.$emit('refresherrefresh');
},
// 自定义下拉复位
refresherrestore() {
this.$emit('refresherrestore');
},
// 自定义下拉被终止
refresherabort() {
this.$emit('refresherabort');
},
toRouter(item) {
navigateTo(3, '/' + item.path);
},
// 返回上一页
goBack() {
navigateBack(1);
}
}
};
</script>
<style lang="less" scoped>
.layout {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.noBottomHeight {
// height: calc(100% - 100rpx);
position: fixed;
top: 0;
left: 0;
}
.layout_header {
// padding-top: 10rpx;
}
.layout_content {
flex: 1;
overflow: hidden;
.layout_content_scroll {
height: 100%;
}
}
.global_title {
height: 96rpx;
display: flex;
align-items: center;
padding: 0 30rpx;
position: relative;
font-size: 16px;
justify-content: space-between;
/* #ifdef APP-PLUS */
padding-top: 64rpx;
// border-bottom: 1px solid rgba(0, 0, 0, 0.1);
/* #endif */
// font-weight: 600;
.title_message {
position: absolute;
top: 50%;
/* #ifdef APP-PLUS */
top: 70%;
/* #endif */
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
}
.goBack {
.iconfont {
}
}
}
.footer {
height: 98rpx;
padding: 0 65rpx;
/* #ifdef APP-PLUS */
padding-bottom: 65rpx;
/* #endif */
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0px -3px 16px rgba(0, 33, 96, 0.04);
background: #fff;
position: relative;
&_item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&_image {
width: 44rpx;
image {
width: 100%;
}
}
&_tip {
color: #707070;
font-size: 22rpx;
}
}
&_message {
position: absolute;
top: 0;
left: 90rpx;
height: 30rpx;
padding: 4rpx 10rpx;
background: #f74c31;
opacity: 1;
border-radius: 16rpx;
color: #fff;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
//默认域名,域名格式例如:chat.crmeb.net
// 修改下面这行为你的实际域名即可
defaultDomainName: 'kefu.c1cscs2026.com',
//默认请求方式为https或者http
defaultRequestType: 'http',
//默认ws链接方式,连接方式wss或者ws
defaultWsType: 'ws',
//是否打开自定义配置域名,可选值:true|false;关闭后必须要配置默认域名(defaultDomainName)选项
isDomainName: false,
}
+167
View File
@@ -0,0 +1,167 @@
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2021 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
export default [
'em-tlj-1',
'em-tlj-3',
'em-tlj-4',
'em-tlj-5',
'em-tlj-6',
'em-tlj-7',
'em-tlj-8',
'em-tlj-9',
'em-tlj-10',
'em-tlj-11',
'em-tlj-12',
'em-tlj-13',
'em-tlj-14',
'em-tlj-15',
'em-tlj-16',
'em-tlj-17',
'em-tlj-18',
'em-tlj-19',
'em-tlj-20',
'em-tlj-21',
'em-tlj-22',
'em-tlj-23',
'em-tlj-24',
'em-tlj-25',
'em-tlj-26',
'em-tlj-27',
'em-tlj-28',
'em-tlj-29',
'em-tlj-30',
'em-tlj-31',
'em-tlj-32',
'em-tlj-33',
'em-tlj-34',
'em-tlj-35',
'em-tlj-36',
'em-tlj-37',
'em-tlj-38',
'em-tlj-39',
'em-tlj-40',
'em-tlj-41',
'em-tlj-42',
'em-tlj-43',
'em-tlj-44',
'em-tlj-45',
'em-tlj-46',
'em-tlj-47',
'em-tlj-48',
'em-tlj-49',
'em-tlj-50',
'em-tlj-51',
'em-tlj-52',
'em-tlj-53',
'em-tlj-54',
'em-tlj-55',
'em-tlj-56',
'em-tlj-57',
'em-tlj-58',
'em-tlj-59',
'em-tlj-60',
'em-tlj-61',
'em-tlj-62',
'em-tlj-63',
'em-tlj-64',
'em-tlj-65',
'em-tlj-66',
'em-tlj-67',
'em-tlj-68',
'em-tlj-69',
'em-tlj-70',
'em-tlj-71',
'em-tlj-72',
'em-tlj-73',
'em-tlj-74',
'em-tlj-75',
'em-tlj-76',
'em-tlj-77',
'em-tlj-78',
'em-tlj-79',
'em-tlj-80',
'em-tlj-81',
'em-tlj-82',
'em-tlj-83',
'em-tlj-84',
'em-tlj-85',
'em-tlj-86',
'em-tlj-87',
'em-tlj-88',
'em-tlj-89',
'em-tlj-90',
'em-tlj-91',
'em-tlj-92',
'em-tlj-93',
'em-tlj-94',
'em-tlj-95',
'em-tlj-96',
"em-smile",
"em-laughing",
"em-blush",
"em-smiley",
"em-relaxed",
"em-smirk",
"em-heart_eyes",
"em-kissing_heart",
"em-kissing_closed_eyes",
"em-flushed",
"em-relieved",
"em-satisfied",
"em-grin",
"em-wink",
"em-stuck_out_tongue_winking_eye",
"em-stuck_out_tongue_closed_eyes",
"em-grinning",
"em-kissing",
"em-kissing_smiling_eyes",
"em-stuck_out_tongue",
"em-sleeping",
"em-worried",
"em-frowning",
"em-anguished",
"em-open_mouth",
"em-grimacing",
"em-confused",
"em-hushed",
"em-expressionless",
"em-unamused",
"em-sweat_smile",
"em-sweat",
"em-disappointed_relieved",
"em-weary",
"em-pensive",
"em-disappointed",
"em-confounded",
"em-fearful",
"em-cold_sweat",
"em-persevere",
"em-cry",
"em-sob",
"em-joy",
"em-astonished",
"em-scream",
"em-tired_face",
"em-angry",
"em-rage",
"em-triumph",
"em-sleepy",
"em-yum",
"em-mask",
"em-sunglasses",
"em-dizzy_face",
"em-imp",
"em-smiling_imp",
"em-neutral_face",
"em-no_mouth",
"em-innocent",
"em-alien"
];
+509
View File
@@ -0,0 +1,509 @@
/*
参数说明
@url
要跳转的目标地址
@opt
要传给目标地址的参数
可在目标页面的onLoad生命周期函数的第一个参数中获取
*/
// 压栈跳转页面
export function navigateTo(type, url, opt) {
// H5端页面跳转目前不支持动画 (浏览器性能限制)
let toUrl = url;
let api = 'navigateTo';
toUrl = opt ? toUrl + '?' + convertObj(opt) : toUrl;
switch (type) {
case 1:
api = 'navigateTo';
break;
case 2:
api = 'redirectTo'; // 关闭当前页,跳转应用内某个页面
break;
case 3:
api = 'reLaunch'; // 关闭所有页面,打开到应用内某个页面
break;
case 4:
api = 'switchTab'; //跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。
break;
default:
api = 'navigateTo'
break;
}
uni[api]({
url: toUrl,
animationType: 'slide-in-right',
animationDuration: 200
});
}
// 关闭当前页面并返回上一页面 delta 标识返回几层
export function navigateBack(delta) {
uni.navigateBack({
delta: delta
});
}
// setStorage 将数据存入缓存
export function setStorage(key, val) {
if (typeof val == 'string') {
uni.setStorageSync(key, val);
return val
}
uni.setStorageSync(key, JSON.stringify(val));
}
// getStorage 从缓存中读取数据
export function getStorage(key) {
let uu = uni.getStorageSync(key);
try {
if (typeof JSON.parse(uu) != 'number') {
uu = JSON.parse(uu);
}
} catch (e) {}
return uu;
}
// 删除缓存中的数据
export function removeStorage(key) {
if (key) {
uni.removeStorageSync(key);
}
}
const prefixCache = 'kefu-'
export const cache = {
remove(key) {
removeStorage(prefixCache + key)
},
get(key) {
return getStorage(prefixCache + key)
},
set(key, val) {
setStorage(prefixCache + key, val)
}
}
// 将缓存中的数据清空
export function clearStorage() {
try {
uni.clearStorageSync();
} catch (e) {
throw new Error('处理失败');
}
}
// 显示Toast
/*
@title 最多汉字数量7个
@icon success loading none
*/
export function Toast(title, icon = 'none', obj = {}, duration = 2000) {
let toastData = {
title: title,
duration: duration,
position: 'center',
mask: true,
icon: icon ? icon : 'none',
...obj
};
uni.showToast(toastData);
}
/*
显示loading提示框,需要手动隐藏
*/
export function Loading(title = '正在处理...', obj = {}) {
uni.showLoading({
title: title,
mask: true,
...obj
});
}
// 隐藏loading
export function hideLoading() {
try {
uni.hideLoading();
} catch (e) {
//TODO handle the exception
throw new Error('处理失败');
}
}
// 模态框
/*
确定取消按钮的文字颜色可修改
obj 对象中传入 cancelColor : rgb 即可修改取消按钮颜色
obj 对象中传入 confirmColor : rgb 即可修改确认按钮颜色
*/
export function Modal(title = '提示', content = '这是一个模态弹窗!', obj = {
showCancel: true,
cancelText: '取消',
confirmText: '确定'
}) {
return new Promise((reslove, reject) => {
uni.showModal({
title: title,
content: content,
...obj,
success: (res) => {
if (res.confirm) {
reslove()
}
if (res.cancel) {
reject()
}
}
});
})
}
/*
显示操作菜单
@itemList 操作菜单数组
@itemColor 文字颜色
*/
export function ActionSheet(itemList, itemColor = "#000000") {
return new Promise((reslove, reject) => {
uni.showActionSheet({
itemList: itemList,
itemColor: itemColor,
success: (res) => {
reslove(res.tapIndex);
},
fail: function(res) {
reject(res.errMsg);
}
});
})
}
//将页面滚动到目标位置。
export function ScrollTo(ScrollTop) {
uni.pageScrollTo({
scrollTop: ScrollTop,
duration: 300
})
}
// 获取用户信息
export function GetUserInfo() {
return new Promise((reslove, reject) => {
uni.getUserInfo({
success(res) {
console.log(res);
reslove(res);
},
fail(rej) {
reject(rej);
}
})
})
}
// 获取用户授权信息
export function Authorize(scoped = 'scope.userInfo') {
return new Promise((reslove, reject) => {
uni.authorize({
scope: scoped,
success(res) {
reslove(res);
},
fail(rej) {
reject(rej);
}
})
})
}
// 将对象转换成使用 & 连接的字符串
export function convertObj(opt) {
let str = '';
let arr = [];
Object.keys(opt).forEach(item => {
arr.push(`${item}=${opt[item]}`);
})
str = arr.join('&');
return str;
}
// 节流函数
// 节流函数
function throttle(fn, delay) {
var lastTime;
var timer;
var delay = delay || 200;
return function() {
var args = arguments;
// 记录当前函数触发的时间
var nowTime = Date.now();
if (lastTime && nowTime - lastTime < delay) {
clearTimeout(timer);
timer = setTimeout(function() {
// 记录上一次函数触发的时间
lastTime = nowTime;
// 修正this指向问题
fn.apply(this, args);
}, delay);
} else {
lastTime = nowTime;
fn.apply(this, args);
}
}
}
// 调起相机
export function chooseImage(count) {
return new Promise((reslove, reject) => {
uni.chooseImage({
count: count, //默认9
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], //从相册选择
success: (res) => {
console.log(res)
reslove(res);
// const tempFilePaths = res.tempFilePaths;
// let tempPathList = [];
// for (let i = 0; i < tempFilePaths.length; i++) {
// const path = tempFilePaths[i];
// const src = await compressImageHandler(path)
// tempPathList.push(src);
// }
// reslove(tempPathList);
},
fail: (rej) => {
reject(rej);
}
});
})
}
// function compressImageHandler(src) {
// // console.log('platform===' + device.platform)
// const tempPath = compressImage(src, device.platform);
// // console.log('tempPath-----' + tempPath);
// return tempPath
// }
//序列化对象和数组
export function serialize(data) {
if (data != null && data != '') {
try {
return JSON.parse(JSON.stringify(data));
} catch (e) {
if (data instanceof Array) {
return [];
}
return {};
}
}
return data;
}
Date.prototype.format = function(fmt) {
let o = {
'M+': this.getMonth() + 1, //月份
'd+': this.getDate(), //日
'h+': this.getHours(), //小时
'm+': this.getMinutes(), //分
's+': this.getSeconds(), //秒
'q+': Math.floor((this.getMonth() + 3) / 3), //季度
S: this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, String(this.getFullYear()).substr(4 - RegExp.$1.length));
}
for (let k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(String(o[k]).length));
}
}
return fmt;
};
//格式化日期
export function formatDate(nS, format) {
//日期格式化
if (!nS) {
return '';
}
format = format || 'yyyy-MM-dd hh:mm:ss';
return new Date(nS).format(format);
}
// 图片转base64
export function pathToBase64(path) {
return new Promise(function(resolve, reject) {
if (typeof window === 'object' && 'document' in window) {
if (typeof FileReader === 'function') {
var xhr = new XMLHttpRequest()
xhr.open('GET', path, true)
xhr.responseType = 'blob'
xhr.onload = function() {
if (this.status === 200) {
let fileReader = new FileReader()
fileReader.onload = function(e) {
resolve(e.target.result)
}
fileReader.onerror = reject
fileReader.readAsDataURL(this.response)
}
}
xhr.onerror = reject
xhr.send()
return
}
var canvas = document.createElement('canvas')
var c2x = canvas.getContext('2d')
var img = new Image
img.onload = function() {
canvas.width = img.width
canvas.height = img.height
c2x.drawImage(img, 0, 0)
resolve(canvas.toDataURL())
canvas.height = canvas.width = 0
}
img.onerror = reject
img.src = path
return
}
if (typeof plus === 'object') {
plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), function(entry) {
entry.file(function(file) {
var fileReader = new plus.io.FileReader()
fileReader.onload = function(data) {
resolve(data.target.result)
}
fileReader.onerror = function(error) {
reject(error)
}
fileReader.readAsDataURL(file)
}, function(error) {
reject(error)
})
}, function(error) {
reject(error)
})
return
}
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
wx.getFileSystemManager().readFile({
filePath: path,
encoding: 'base64',
success: function(res) {
resolve('data:image/png;base64,' + res.data)
},
fail: function(error) {
reject(error)
}
})
return
}
reject(new Error('not support'))
})
}
/*
@value 要拷贝的内容
*/
export function copyText(value) {
// 条件编译,以下代码仅在H5出现
//#ifdef H5
return new Promise((reslove, reject) => {
uniCopy({
content: value,
success: (res) => {
reslove(res);
},
error: (e) => {
reject(res)
}
})
})
//#endif
// 以下代码在除H5以外的平台出现
//#ifndef H5
//#endif
}
// 获取本周的第一天
export function showWeekFirstDay() {
var date = new Date();
var weekday = date.getDay() || 7; //获取星期几,getDay()返回值是 0(周日) 到 6(周六) 之间的一个整数。0||7为7,即weekday的值为1-7
date.setDate(date.getDate() - weekday + 1); //往前算(weekday-1)天,年份、月份会自动变化
return formatDate(date, 'yyyy-MM-dd');
}
// 获取本月第一天
export function showMonthFirstDay() {
var MonthFirstDay = new Date().setDate(1);
return formatDate(new Date(MonthFirstDay).getTime(), 'yyyy-MM-dd');
}
var now = new Date(); //当前日期
// var nowDayOfWeek = now.getDay(); //今天本周的第几天
// var nowDay = now.getDate(); //当前日
var nowMonth = now.getMonth(); //当前月
var nowYear = now.getYear(); //当前年
nowYear += (nowYear < 2000) ? 1900 : 0; //
//获得本季度的开始月份
function getQuarterStartMonth() {
var quarterStartMonth = 0;
if (nowMonth < 3) {
quarterStartMonth = 0;
}
if (2 < nowMonth && nowMonth < 6) {
quarterStartMonth = 3;
}
if (5 < nowMonth && nowMonth < 9) {
quarterStartMonth = 6;
}
if (nowMonth > 8) {
quarterStartMonth = 9;
}
return quarterStartMonth;
}
//或的本季度的结束日期
//获得本季度的开始日期
export function getQuarterStartDate() {
var quarterStartDate = new Date(nowYear, getQuarterStartMonth(), 1);
return formatDate(quarterStartDate, 'yyyy-MM-dd');
}
// 删除数组中重复数据
export function unique(data) {
data = data || [];
var n = {}; //存放新的数据
for (var i = 0; i < data.length; i++) {
var v = JSON.stringify(data[i]);
if (typeof(v) == "undefined") {
n[v] = 1;
}
}
data.length = 0;
for (var i in n) {
data[data.length] = i;
}
return data;
}
// 将&符号连接的字符串转换成对象
export function parseToObject(queryString) {
// 使用 & 符号将查询字符串分割开
var parts = queryString.split("&");
// 遍历所有的 "key=value" 字符串结构
var obj = {};
for (var i = 0, len = parts.length; i < len; i++) {
// 使用 = 将 "key=value" 字符串分割开
var arr = parts[i].split("=");
// = 号前的内容是对象的属性名,= 号后的内容是对应属性的值
var
name = arr.shift(),
value = arr.join("=");
// 将属性添加到对象中
obj[name] = value;
}
// 返回创建好的对象
return obj;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,329 @@
<!-- 话数库 -->
<template>
<div class="scriptLibary_container" :style="{ 'max-height': maxHeight, height: height }">
<div class="script_list">
<scroll-view scroll-y="true" class="script_list_scroll">
<div class="add_script" v-if="speechArtType == 1" @click="addSpeech">
<div><span class="iconfont">&#xe6b9;</span></div>
<div><span>添加自动回复</span></div>
</div>
<div class="script_list_item" v-for="(item, index) in authReply" :key="index" @click="selectScript(item)">
<div class="script_list_item_label">{{ item.keyword }}</div>
<div class="script_list_item_value">{{ item.content }}</div>
<div class="edit_box" v-if="speechArtType == 1">
<div @click.stop="editSpeech(item)"><span class="iconfont">&#xe6b2;</span></div>
<div @click.stop="handleDeleteServiceSpeechcraft(item)"><span class="iconfont">&#xe6b3;</span></div>
</div>
</div>
</scroll-view>
</div>
<uni-popup ref="addEdGroup" animation type="center">
<div class="addGroup_container">
<div class="addGroup_container_title">
<div></div>
<div v-if="speechData.id">编辑自动回复</div>
<div v-else>添加自动回复</div>
<div @click="closeAddGroup"><span class="iconfont">&#xe6b5;</span></div>
</div>
<div class="addGroup_form">
<div class="input_box"><input v-model="speechData.keyword" type="text" placeholder="请输入关键词,多个关键字请用逗号隔开" /></div>
<div class="input_box"><textarea v-model="speechData.content" type="text" placeholder="请输入自动回复内容"></textarea></div>
<div class="input_box" style='width: 40%;'><input v-model="speechData.sort" type="number" placeholder='排序' /></div>
<div class="button_box" :class="{ canInput: speechData.keyword && speechData.content }" @click="handleSubmiteditAdd"><div>确定</div></div>
</div>
</div>
</uni-popup>
</div>
</template>
<script>
import { Toast, throttle, serialize, Modal, ActionSheet } from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
props: {
// 话术库分类
speechArtType: {
type: Number | String,
default: '1'
},
// 话术检索
title: {
type: String,
default: ''
},
// 是否展示添加话术
icCanEdit: {
type: Boolean,
default: false
},
maxHeight: {
type: String,
default: ''
},
height: {
type: String,
default: ''
}
},
data() {
return {
authReply: [],
where: {
page: 1,
limit: 5
},
scriptList: [],
pageData: {
page: 1,
limit: '200'
},
cateId: '', // 选中的侧边栏ID
speechData: {
keyword: '',
content: '',
sort:0
},
userGroupData: {
name: '',
sort: ''
},
handleAdEdType: 1, // 1新增 2修改
handleGroupTitle: '新增分组',
groupType: 1 // 1新增 2修改
};
},
watch: {
authReply: {
handler(val) {
console.log(val);
},
deep: true
},
title: {
handler(val) {}
}
},
created() {
this.getAuthReplyList();
},
methods: {
// 查询话术类
getAuthReplyList() {
http(api.getAuthReply, this.where).then(res => {
this.authReply = res;
});
},
// 添加话术
addSpeech() {
this.handleAdEdType = 1;
this.speechData = {};
this.$refs.addEdGroup.open();
},
// 修改自动回复
editSpeech(item) {
this.handleAdEdType = 2;
this.speechData = serialize(item);
this.$refs.addEdGroup.open();
},
// 确认新增,修改自动回复
handleSubmiteditAdd() {
this.speechData.id = this.speechData.id ? this.speechData.id : 0;
http(api.saveAuthReply, this.speechData).then(res => {
Toast(this.speechData.id ? '修改成功' : '添加成功');
this.getAuthReplyList();
this.closeAddGroup();
});
},
// 删除自动回复
handleDeleteServiceSpeechcraft(item) {
Modal('温馨提示', `自动回复"${item.content}"将被删除,请问是否继续?`).then(res => {
http(api.deleteAuthReply,item).then(res => {
Toast('删除成功');
this.getAuthReplyList();
});
});
},
closeAddGroup() {
this.$refs.addEdGroup.close();
}
}
};
</script>
<style lang="less" scoped>
.scriptLibary_container {
display: flex;
overflow: hidden;
.script_group {
width: 176rpx;
background: #f5f6f9;
.script_group_scroll {
height: 100%;
.tag_list {
width: 100%;
height: 109rpx;
.iconfont {
color: #9f9f9f;
font-size: 11px;
margin-right: 4rpx;
}
.title_value {
color: #9f9f9f;
font-size: 13px;
position: relative;
top: 2rpx;
}
.tag_list_tag {
width: 6rpx;
height: 46rpx;
background: @primaryColor;
opacity: 1;
border-radius: 0px 4px 4px 0px;
}
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.tag_list_group {
display: flex;
align-items: center;
justify-content: flex-start;
position: relative;
.tag_list_label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
color: #282828;
font-size: 13px;
font-weight: bold;
}
}
}
}
.script_list {
flex: 1;
.add_script {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 30rpx;
height: 109rpx;
background-color: #ffffff;
}
.script_list_scroll {
height: 100%;
.script_list_item {
padding: 25rpx 30rpx;
background-color: #ffffff;
&_label {
color: #282828;
font-size: 14px;
font-weight: bold;
}
&_value {
font-size: 14px;
color: #9f9f9f;
}
}
.edit_box {
display: flex;
justify-content: flex-end;
> div {
margin-left: 48rpx;
}
.iconfont {
font-size: 16px;
}
}
}
}
}
.selectEdTag {
background: #fff !important;
.tag_list_label {
color: @primaryColor !important;
}
}
.addGroup_container {
width: 690rpx;
background: #fff;
border-radius: 12px;
&_title {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
padding: 30rpx 30rpx 50rpx 0;
.iconfont {
font-size: 22px;
color: #c8cad0;
}
> div {
flex: 1;
text-align: center;
}
> div:nth-child(3) {
text-align: right;
}
}
.addGroup_form {
padding: 0 30rpx;
padding-bottom: 69rpx;
.input_box {
margin-bottom: 32rpx;
background: #f5f5f5;
border-radius: 14rpx;
align-items: center;
padding: 0 20rpx;
input {
font-size: 14px;
height: 68rpx;
}
textarea {
padding: 8px 0;
font-size: 14px;
height: 192rpx;
}
}
.button_box {
margin-top: 60rpx;
> div {
height: 86rpx;
background: #c8cad0;
opacity: 1;
border-radius: 43px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
}
}
.canInput {
> div {
height: 86rpx;
background: @primaryColor;
opacity: 1;
border-radius: 43px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
}
}
}
}
</style>
+159
View File
@@ -0,0 +1,159 @@
<template>
<div class="container">
<lay-out>
<!-- <div slot="header" class="header"><div class="header_title">自动回复</div></div> -->
<div slot="content" class="content">
<div class="caption">
<div class="">自动回复开关:</div>
<div class="right"><switch @change="changeSwitch" :checked="authReply" /></div>
</div>
<div class="scriptLibary_content">
<scripts-libary :speechArtType="speechArtType" :maxHeight="maxHeight" icCanEdit :tagList="speechTagList" :title="propstitle"></scripts-libary>
</div>
</div>
</lay-out>
</div>
</template>
<script>
import scriptsLibary from './component/scriptLibary.vue';
import { navigateTo, navigateBack } from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
components: {
scriptsLibary
},
data() {
return {
authReply: false,
maxHeight: '',
speechArtType: '',
speechArtTypeConcat: {
1: '个人库',
0: '公共库'
},
speechTagList: [], // 话术分类
propstitle: ''
};
},
onLoad(opt) {
this.speechArtType = opt.speechArtType;
this.selectScriptLibary();
console.log(this.speechArtType);
this.$nextTick(() => {
let info = uni.createSelectorQuery().select('.content');
info.boundingClientRect(data => {
this.maxHeight = Math.ceil(data.height) * 1.85 + 'rpx';
}).exec();
});
this.getKefuInfo();
},
methods: {
getKefuInfo() {
this.$store.dispatch('getKeufuInfo').then(res => {
this.authReply = !!res.auto_reply;
});
},
changeSwitch(e) {
console.log(e);
http(api.putAuthReply, {
value: e.detail.value ? 1 : 0
}).then(res => {
this.speechTagList = res.data;
});
},
// 查询话术类
selectScriptLibary() {
http(api.kefuServiceCate, {
type: this.speechArtType
}).then(res => {
this.speechTagList = res.data;
});
},
finish() {
navigateBack(1);
}
}
};
</script>
<style lang="less" scoped>
.header {
height: 96rpx;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 30rpx;
position: relative;
background: #fff;
&_title {
position: absolute;
top: 70%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 16px;
font-weight: bold;
}
> div:nth-child(2) {
font-size: 14px;
color: @primaryColor;
}
}
.scriptLibary_search {
background: #fff;
padding-bottom: 44rpx;
border-bottom: 1px solid #f0f3fa;
> div {
display: flex;
background: #f5f6f9;
width: 690rpx;
height: 68rpx;
margin: auto;
display: flex;
align-items: center;
padding: 0 20rpx;
box-sizing: border-box;
border-radius: 39px;
> input {
flex: 1;
font-size: 14px;
margin-left: 8rpx;
}
.iconfont {
font-size: 22px;
color: #c4c4c4;
}
}
}
.content {
overflow: hidden;
height: 100%;
.scriptLibary_content {
}
.caption {
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 25rpx 30rpx;
div:first-of-type {
margin-left: 5px;
}
div {
width: 50%;
}
.right {
text-align: right;
}
/deep/ .uni-switch-input {
height: 24px;
}
/deep/ .uni-switch-input:after,
/deep/ .uni-switch-input:before {
height: 22px;
}
}
}
</style>
+108
View File
@@ -0,0 +1,108 @@
<template>
<div class="auth_login container">
<div class="auth_login_content">
<div class="userData">
<div class="avater"><image :src="userData.avatar" mode=""></image></div>
<div class="userName">
<span>{{ userData.nickname }}</span>
</div>
<div class="tipMessage">点击授权登录您的客服工作台</div>
</div>
<div class="handle">
<div class="handle_login" @click="handle_login">授权登录</div>
<div class="handle_cancel" @click="handleCancel">取消</div>
</div>
</div>
<div class="bottom"></div>
</div>
</template>
<script>
import http from 'pages/api/index';
import api from 'pages/api/api.js';
import { navigateTo, Modal, navigateBack } from 'pages/utils/uniApi.js';
export default {
data() {
return {
key: '',
userData: {}
};
},
onLoad(opt) {
this.key = opt.key;
this.userData = this.$store.state.kefuInfo;
},
methods: {
handle_login() {
http(api.serviceCode, { code: this.key }).then(res => {
Modal('温馨提示', '登录成功', { showCancel: false }).then(() => {
navigateBack(1);
});
});
},
handleCancel() {
navigateBack(1);
}
}
};
</script>
<style lang="less" scoped>
.auth_login {
display: flex;
flex-direction: column;
justify-content: space-between;
&_content {
flex: 1;
.userData {
.avater {
width: 178rpx;
height: 178rpx;
border-radius: 50%;
overflow: hidden;
margin: 112rpx auto 20rpx auto;
image {
width: 100%;
height: 100%;
}
}
.userName {
text-align: center;
font-weight: bold;
margin-bottom: 10rpx;
}
.tipMessage {
text-align: center;
color: #9F9F9F;
font-size: 14px;
}
}
.handle {
margin-top: 96rpx;
> div {
width: 594rpx;
height: 86rpx;
border-radius: 43rpx;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
&_login {
background: linear-gradient(90deg, #3875ea 0%, #1890fc 100%);
color: #fff;
}
&_cancel {
margin-top: 28rpx !important;
border: 1px solid #3875EA;
color: #3875EA;
}
}
}
}
</style>
@@ -0,0 +1,129 @@
<template>
<div class="container">
<div class="header_banner">
<div class="header_banner_content">
<div class="header_banner_content_title"><span>提建议或举报不良内容</span></div>
<div class="header_banner_content_message">
<span>若您收到不良信息请及时反馈我们非常重视您给我们提出宝贵的建议帮助我们不断完善产品谢谢</span></div>
</div>
<div class="header_banner_image">
<div class="header_banner_image_imageBox">
<image src="~static/image/feedback/bg.png" mode="widthFix"></image>
</div>
</div>
</div>
<!-- <div class="organization_work">
<div class="organization_work_item">
<div class="organization_work_item_label">
<span>09:00~22:00</span>
</div>
<div class="organization_work_item_value">
<span>我们的工作时间</span>
</div>
</div>
<div class="tip"></div>
<div class="organization_work_item">
<div class="organization_work_item_label">
<span>400-8888-794</span>
</div>
<div class="organization_work_item_value">
<span>售前客服电话</span>
</div>
</div>
</div> -->
<div class="feedback">
<div class="feedback_title"><span>我要投诉</span></div>
<div class="feedback_list">
<div class="feedback_list_item" v-for="(item, index) in feedbackList" :key="index"
@click="selectItem(item)">
<div>
<div :class="{ feedback_list_item_selected: item.selected }"></div>
</div>
<div>{{ item.name }}</div>
</div>
</div>
<!-- <div class="feedback_input">
<input v-model="feedBackData.rela_name" type="text" value="" placeholder="请输入您的姓名"/>
</div>
<div class="feedback_input">
<input v-model="feedBackData.phone" type="text" value="" placeholder="请输入您的联系电话" />
</div> -->
<div class="feedback_textarea"><textarea v-model="feedBackData.content" value="" placeholder="请填写留言内容" />
</div>
<div class="handle_button" @click="handleSubmit"><span>提交</span></div>
</div>
<uni-popup ref="feedbackReslove" animation>
<submit-response @handleSubmitRes="handleSubmitRes"></submit-response>
</uni-popup>
</div>
</template>
<script>
import {
navigateBack
} from 'pages/utils/uniApi.js';
import submitResponse from '../feedback/component/submitResponse.vue';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
components: {
submitResponse
},
data() {
return {
feedBackData: {
content: '',
user_id: '',
cate_id: []
},
feedbackList: [],
userId: 0,
};
},
onLoad(option) {
this.userId = option.id || 0;
},
onShow() {
this.getUserComplain();
},
methods: {
// 获取投诉内容列表
getUserComplain() {
http(api.userComplain).then(res => {
this.feedbackList = res;
this.feedbackList.forEach(item => {
this.$set(item, 'selected', false);
});
});
},
// 选择投诉项目
selectItem(item) {
item.selected = !item.selected;
},
handleSubmit() {
this.feedBackData.cate_id = this.feedbackList.filter(item => item.selected).map(val => val.id);
this.feedBackData.user_id = this.userId;
http(api.postUserComplain, this.feedBackData).then(res => {
this.$refs.feedbackReslove.open('center');
});
},
handleSubmitRes() {
navigateBack(1);
this.$refs.feedbackReslove.close();
}
}
};
</script>
<style lang="less" scoped>
@import url('../feedback/less/feedback.less');
</style>
@@ -0,0 +1,536 @@
<!-- 话数库 -->
<template>
<div class="scriptLibary_container" :style="{ 'max-height': maxHeight, height: height,'min-height': minWidth, width: width }">
<div class="script_group">
<scroll-view scroll-y="true" class="script_group_scroll">
<div class="tag_list flex-center" @click="addGroup" v-if="speechArtType == 1">
<span class="iconfont">&#xe6c1;</span>
<span class="title_value">分组</span>
</div>
<div class="tag_list tag_list_group" v-for="(item, index) in tagList" :key="index" :class="{ selectEdTag: cateId == item.id }" @click="selectTags(item)">
<div class="tag_list_tag" v-if="cateId == item.id"></div>
<div class="tag_list_label">{{ item.name }}</div>
</div>
</scroll-view>
</div>
<div class="script_list">
<scroll-view scroll-y="true" class="script_list_scroll">
<div class="add_script" v-if="speechArtType == 1" @click="addSpeech">
<div><span class="iconfont">&#xe6b9;</span></div>
<div><span>添加话术</span></div>
</div>
<div class="script_list_item" v-for="(item, index) in scriptList" :key="index" @click="selectScript(item)">
<div class="script_list_item_label">{{ item.title }}</div>
<div class="script_list_item_value line2">{{ item.message }}</div>
<div class="edit_box" v-if="speechArtType == 1">
<div @click.stop="editSpeech(item)"><span class="iconfont">&#xe6b2;</span></div>
<div @click.stop="handleDeleteServiceSpeechcraft(item)"><span class="iconfont">&#xe6b3;</span></div>
</div>
</div>
</scroll-view>
</div>
<uni-popup ref="addEdGroup" animation type="center">
<div class="addGroup_container">
<div class="addGroup_container_title">
<div></div>
<div>添加话术</div>
<div @click="closeAddGroup"><span class="iconfont">&#xe6b5;</span></div>
</div>
<div class="addGroup_form">
<div class="input_box"><input class="inp_title" maxlength="20" v-model="speechData.title" type="text" placeholder="请输入标题名称" /></div>
<div class="input_box"><textarea v-model="speechData.message" type="text" placeholder="请输入您的话术"></textarea></div>
<div class="button_box" :class="{ canInput: speechData.title && speechData.message }" @click="handleSubmiteditAdd"><div>确定</div></div>
</div>
</div>
</uni-popup>
<uni-popup ref="addEdUserGroups" animation type="center">
<div class="addGroup_container">
<div class="addGroup_container_title">
<div></div>
<div>{{handleGroupTitle}}</div>
<div @click="closeUserGroups"><span class="iconfont">&#xe6b5;</span></div>
</div>
<div class="addGroup_form">
<div class="input_box"><input v-model.trim="userGroupData.name" maxlength="4" type="text" placeholder="请输入分组名称" /></div>
<div class="input_box"><input v-model="userGroupData.sort" type="text" placeholder="请输入分组排序"></input></div>
<div class="button_box" :class="{ canInput: userGroupData.name }" @click="handleSubmitUserGroup"><div>确定</div></div>
</div>
</div>
</uni-popup>
</div>
</template>
<script>
import { Toast, throttle, serialize, Modal, ActionSheet } from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
props: {
// 话术库分类
speechArtType: {
type: Number | String,
default: '1'
},
// 话术检索
title: {
type: String,
default: ''
},
// 是否展示添加话术
icCanEdit: {
type: Boolean,
default: false
},
maxHeight: {
type: String,
default: ''
},
height: {
type: String,
default: ''
},
minWidth: {
type: String,
default: ''
},
width: {
type: String,
default: ''
},
},
data() {
return {
// 话术分类
tagList: {
type: Array,
default() {
return [];
}
},
scriptList: [],
pageData: {
page: 1,
limit: '200'
},
cateId: '', // 选中的侧边栏ID
speechData: {
title: '',
message: ''
},
userGroupData: {
name: '',
sort: ''
},
handleAdEdType: 1, // 1新增 2修改
handleGroupTitle: '新增分组',
groupType: 1, // 1新增 2修改
};
},
watch: {
speechArtType: {
handler(val) {
this.selectScriptLibary();
// this.getSpeechArtList('init');
console.log(this.tagList);
}
},
title: {
handler(val) {
setTimeout(() => {
this.getSpeechArtList();
}, 500);
}
},
tagList: {
handler(val) {
this.getSpeechArtList('init');
}
}
},
created() {
this.selectScriptLibary();
},
methods: {
// 查询话术类
selectScriptLibary() {
http(api.kefuServiceCate, {
type: this.speechArtType
}).then(res => {
this.tagList = res.data;
this.getSpeechArtList('init');
});
},
// 获取话术列表
getSpeechArtList(type = 'select') {
if (this.tagList.length) {
if (type == 'init') {
this.cateId = this.tagList[0].id;
}
http(api.serviceSpeechcraft, {
cate_id: this.cateId,
type: this.speechArtType,
...this.pageData,
title: this.title
}).then(res => {
this.scriptList = res;
this.speechData = {
title: '',
message: ''
}
console.log(this.scriptList);
});
} else {
// setTimeout(() => {
// this.getSpeechArtList(type);
// }, 500);
}
},
// 选择标签
selectTags(item) {
if(this.cateId == item.id) {
console.log(item);
this.userGroupData = serialize(item);
ActionSheet(['编辑', '删除']).then(res => {
console.log(res)
switch(res) {
case 0:
this.groupType = 2;
this.handleGroupTitle = '编辑分组';
this.$refs.addEdUserGroups.open();
break;
case 1:
Modal('温馨提示', `分组 "${item.name}"将被删除, 请问是否继续?`).then(() => {
http(api.deleteServiceCate, item).then(res => {
Toast('删除成功');
this.selectScriptLibary('init');
})
})
break;
default:
break;
}
})
} else {
this.cateId = item.id;
this.getSpeechArtList();
}
},
// 选择话术
selectScript(item) {
this.$emit('sendSpeechMessage', item);
},
// 添加话术
addSpeech() {
this.speechData.title = '';
this.speechData.message = '';
this.handleAdEdType = 1;
this.$refs.addEdGroup.open();
},
// 修改话术
editSpeech(item) {
this.handleAdEdType = 2;
this.speechData = serialize(item);
this.$refs.addEdGroup.open();
},
// 确认新增,修改话术
handleSubmiteditAdd() {
if (this.handleAdEdType == 1) {
this.addSpeechClass();
return;
}
if ((this.handleAdEdType = 2)) {
this.editSpeechClass();
return;
}
},
// 修改话术事件
editSpeechClass() {
let canInput = true;
console.log(this.speechData);
Object.keys(this.speechData).forEach(item => {
if (!this.speechData.title || !this.speechData.message) {
canInput = false;
}
});
if (!canInput) {
Toast('请将内容填写完整');
return;
}
http(api.putServiceSpeechcraft, this.speechData).then(res => {
Toast('修改成功');
this.getSpeechArtList();
this.$refs.addEdGroup.close();
});
},
// 新增话术事件
addSpeechClass() {
let canInput = true;
Object.keys(this.speechData).forEach(item => {
if (!this.speechData[item]) {
canInput = false;
}
});
if (!canInput) {
Toast('请将内容填写完整');
return;
}
http(api.postServiceSpeechcraft, {
...this.speechData,
cate_id: this.cateId
}).then(res => {
Toast('新增成功');
this.getSpeechArtList();
this.$refs.addEdGroup.close();
});
},
// 删除话术
handleDeleteServiceSpeechcraft(item) {
Modal('温馨提示', `话术"${item.title}"将被删除,请问是否继续?`).then(res => {
http(api.deleteServiceSpeechcraft, item).then(res => {
Toast('删除成功');
this.getSpeechArtList();
});
});
},
closeAddGroup() {
this.$refs.addEdGroup.close();
},
// 新增分组
addGroup() {
this.groupType = 1;
this.$refs.addEdUserGroups.open();
},
// 确定新增 或 修改 分组 1增 2改
async handleSubmitUserGroup() {
if(this.groupType == 1) {
await http(api.serviceCate, {
...this.userGroupData
}).then(res => {
Toast('添加成功');
})
}
if(this.groupType == 2) {
console.log(this.userGroupData);
await http(api.putServiceCate, this.userGroupData).then(res => {
Toast('修改成功');
this.selectScriptLibary();
this.$refs.addEdUserGroups.close();
})
}
this.selectScriptLibary();
this.$refs.addEdUserGroups.close();
this.userGroupData = {
name: '',
sort: ''
}
},
// 关闭分组弹框
closeUserGroups() {
this.$refs.addEdUserGroups.close();
}
}
};
</script>
<style lang="less" scoped>
.scriptLibary_container {
display: flex;
overflow: hidden;
.script_group {
// width: 176rpx;
width: 20%;
position: fixed;
background: #f5f6f9;
.script_group_scroll {
width: 100%;
height: 100%;
.tag_list {
width: 100%;
height: 109rpx;
.iconfont {
color: #9f9f9f;
font-size: 11px;
margin-right: 4rpx;
}
.title_value {
color: #9f9f9f;
font-size: 13px;
position: relative;
top: 2rpx;
}
.tag_list_tag {
width: 6rpx;
height: 46rpx;
background: @primaryColor;
opacity: 1;
border-radius: 0px 4px 4px 0px;
}
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.tag_list_group {
display: flex;
align-items: center;
justify-content: flex-start;
position: relative;
.tag_list_label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
color: #282828;
font-size: 13px;
font-weight: bold;
}
}
}
}
.script_list {
// flex: 1;
width: 80%;
position: absolute;
right: 0;
.add_script {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 30rpx;
height: 109rpx;
}
.script_list_scroll {
height: 100%;
.script_list_item {
padding: 25rpx 30rpx;
&_label {
color: #282828;
font-size: 14px;
font-weight: bold;
}
&_value {
font-size: 14px;
color: #9f9f9f;
}
}
.edit_box {
display: flex;
justify-content: flex-end;
> div {
margin-left: 48rpx;
}
.iconfont {
font-size: 16px;
}
}
}
}
}
.selectEdTag {
background: #fff !important;
.tag_list_label {
color: @primaryColor !important;
}
}
.addGroup_container {
width: 690rpx;
background: #fff;
border-radius: 12px;
&_title {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
padding: 30rpx 30rpx 50rpx 0;
.iconfont {
font-size: 22px;
color: #c8cad0;
}
> div {
flex: 1;
text-align: center;
}
> div:nth-child(3) {
text-align: right;
}
}
.addGroup_form {
padding: 0 30rpx;
padding-bottom: 69rpx;
.input_box {
margin-bottom: 32rpx;
background: #f5f5f5;
border-radius: 14rpx;
display: flex;
align-items: center;
padding: 0 20rpx;
input {
font-size: 14px;
height: 68rpx;
}
textarea {
padding: 8px 0;
font-size: 14px;
height: 192rpx;
}
}
.button_box {
margin-top: 60rpx;
> div {
height: 86rpx;
background: #c8cad0;
opacity: 1;
border-radius: 43px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
}
}
.canInput {
> div {
height: 86rpx;
background: @primaryColor;
opacity: 1;
border-radius: 43px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
}
}
}
}
.line2 {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.inp_title {
width: 600rpx;
}
</style>
@@ -0,0 +1,176 @@
<template>
<div class="transfer_container">
<div class="transfer_container_title">
<div class="transfer_container_title_value"><span>转接客服</span></div>
<div class="transfer_container_title_icon" @click="close"><span class="iconfont">&#xe6b5;</span></div>
</div>
<div class="customerServer_list">
<scroll-view scroll-y="true" class="max_height">
<div class="customerServer_list_item" v-for="(item, index) in serverList" :key="index" @click="selectCustomerServer(item)">
<div class="customerServer_list_item_radio">
<div><div v-if="item.select"></div></div>
</div>
<div class="customerServer_list_item_avar"><image :src="item.avatar" mode=""></image></div>
<div class="customerServer_list_item_nickName">
<span>{{ item.nickname }}</span>
</div>
</div>
</scroll-view>
</div>
<div class="handle_box" @click="handleSubmit"><div>确定</div></div>
</div>
</template>
<script>
import { Toast } from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
props:{
userInfo:{
type:Object,
default:{}
}
},
data() {
return {
serverList: []
};
},
created() {
this.initData();
},
methods: {
// 查询其他客服
initData() {
http(api.serviceTransferList).then(res => {
this.serverList = res.list;
this.serverList.forEach(item => {
this.$set(item, 'select', false);
});
console.log(this.serverList);
});
},
// 选择客服
selectCustomerServer(item) {
if (item.select) {
item.select = false;
} else {
this.serverList.forEach(item => {
item.select = false;
});
item.select = true;
}
},
// 确认转接
handleSubmit() {
http(api.serviceTransfer, {
kefuToUserId: this.serverList.find(item => item.select).user_id, // 转接人id
user_id: this.userInfo.id
}).then(res => {
Toast('转接成功');
this.$emit('handleSubmit');
});
},
close() {
this.$emit('close');
}
}
};
</script>
<style lang="less" scoped>
.transfer_container {
padding: 40rpx 0 68rpx 0;
background: #fff;
border-radius: 12rpx 12rpx 0px 0px;
&_title {
position: relative;
display: flex;
justify-content: flex-end;
padding: 0 30rpx;
&_value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #282828;
font-weight: 600;
font-size: 16px;
}
&_icon {
.iconfont {
font-size: 22px;
color: #c8cad0;
}
}
}
.customerServer_list {
&_item {
display: flex;
align-items: center;
padding: 0 30px;
height: 122rpx;
border-bottom: 1px solid #f0f2f7;
&_radio {
margin-right: 38rpx;
> div {
width: 34rpx;
height: 34rpx;
border: 1px solid @primaryColor;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
> div {
width: 18rpx;
height: 18rpx;
background: @primaryColor;
border-radius: 50%;
}
}
}
&_avar {
width: 90rpx;
height: 90rpx;
border-radius: 6px;
overflow: hidden;
margin-right: 28rpx;
image {
width: 100%;
height: 100%;
}
}
&_nickName {
color: #282828;
font-size: 15px;
font-weight: bold;
}
}
}
.handle_box {
display: flex;
justify-content: center;
margin-top: 80rpx;
> div {
width: 690rpx;
height: 86rpx;
background: #3875ea;
opacity: 1;
border-radius: 43px;
color: #ffffff;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.max_height {
max-height: 772rpx;
}
</style>
@@ -0,0 +1,336 @@
<template>
<div class="container">
<lay-out titleName="客户信息" headerColor="#fff">
<!-- <div slot="hander_handle">
<div @click="changeInputRadio"><span class="iconfont" :class="{ selected: inputRadio }">&#xe6b2;</span></div>
</div> -->
<div slot="content" class="content">
<div class="content_header">
<div class="content_header_avar"><image :src="userDetilsData.avatar" mode="widthFix"></image></div>
<div class="content_header_userName">
<span>{{ userDetilsData.nickname }}</span>
</div>
<div class="content_header_tag">
<span>{{ formType[userDetilsData.type] }}</span>
</div>
</div>
<div class="content_message">
<div class="content_message_item">
<div class="content_message_item_label"><span>备注昵称</span></div>
<div class="content_message_item_value">
<input type="text" v-model="userDetilsData.remark_nickname" placeholder="请输入备注昵称" v-if="inputRadio" />
<span v-else>{{ userDetilsData.remark_nickname ? userDetilsData.remark_nickname : '暂无数据' }}</span>
</div>
</div>
<div class="content_message_item">
<div class="content_message_item_label"><span>手机号</span></div>
<div class="content_message_item_value">
<input type="text" v-model="userDetilsData.phone" placeholder="请输入客户联系方式" v-if="inputRadio" />
<span v-else>{{ userDetilsData.phone ? userDetilsData.phone : '暂无数据' }}</span>
</div>
</div>
<div class="content_message_item">
<div class="content_message_item_label"><span>性别</span></div>
<div class="content_message_item_value" v-if="inputRadio">
<picker mode="selector" :range="sexArr" range-key="label" @change="changeSex">
<view>{{ sexType[userDetilsData.sex] }}</view>
</picker>
</div>
<div class="content_message_item_value" v-else>
<span>{{ sexType[userDetilsData.sex] }}</span>
</div>
</div>
<div class="content_message_item">
<div class="content_message_item_label"><span>备注</span></div>
<div class="content_message_item_value">
<input type="text" v-model="userDetilsData.remarks" placeholder="请输入备注" v-if="inputRadio" />
<span v-else>{{ userDetilsData.remarks ? userDetilsData.remarks : '暂无数据' }}</span>
</div>
</div>
<div class="content_message_item" @click="openUserTag">
<div class="content_message_item_label"><span>用户标签</span></div>
<div class="content_message_item_value flex">
<div class="tag_list">
<span v-for="(item, index) in userDetilsData.label" :key="index">{{ item.label }}</span>
</div>
<div class="to_link" v-if="inputRadio"><span class="iconfont font">&#xe6c3;</span></div>
</div>
</div>
<div class="content_message_item" @click="editGroup">
<div class="content_message_item_label"><span>分组</span></div>
<div class="content_message_item_value">
<span>{{ userGroupConcat[userDetilsData.group_id] }}</span>
</div>
<div class="to_link" v-if="inputRadio"><span class="iconfont">&#xe6c3;</span></div>
</div>
<div class="content_message_item">
<div class="content_message_item_label"><span>用户类型</span></div>
<div class="content_message_item_value">
<span>{{ formType[userDetilsData.type] }}</span>
</div>
</div>
<!-- <div class="content_message_item">
<div class="content_message_item_label"><span>生日</span></div>
<div class="content_message_item_value"><span>9月5号</span></div>
</div> -->
<div class="content_message_item marginTop16" @click="handelComplaint">
<div class="content_message_item_label"><span>投诉</span></div>
<div class="content_message_item_value">
<span>{{ formType[userDetilsData.type] }}</span>
</div>
</div>
<div class="content_message_item marginTop16" @click="handelblock">
<div class="content_message_item_label"><span>拉黑</span></div>
<div class="content_message_item_value">
<span>{{ formType[userDetilsData.type] }}</span>
</div>
</div>
</div>
<!-- <div class="userTag_container_handle" v-if="inputRadio" @click="handleEditUserMessage">
<div class="userTag_container_handle_button"><span>确认修改</span></div>
</div> -->
<!-- <div class="userTag_container_handle" @click="toMessage">
<div class="userTag_container_handle_button"><span>发消息</span></div>
</div> -->
</div>
</lay-out>
<uni-popup ref="userTagModel" type="center" animation>
<div class="userTag_container">
<div class="userTag_container_title">
<div class="userTag_container_title_message"><span>用户标签</span></div>
<div class="closeModel" @click="closeUserTagModel"><span class="iconfont">&#xe6c6;</span></div>
</div>
<div class="userTag_container_list">
<div class="userTag_container_list_item" :class="{ mg40: index == 0 }" v-for="(item, index) in userTagList" :key="index">
<div class="userTag_container_list_item_label">
<span>{{ item.name }}</span>
</div>
<div class="userTag_container_list_item_value">
<span :class="{ select: val.disabled }" v-for="(val, i) in item.label" :key="i" @click="selectTags(val)">{{ val.label }}</span>
</div>
</div>
</div>
<div class="userTag_container_handle" @click="handleSetTags">
<div class="userTag_container_handle_button" ><span>确定</span></div>
</div>
</div>
</uni-popup>
<uni-popup ref="userGroupModel" type="center" animation>
<div class="userTag_container">
<div class="userTag_container_title">
<div class="userTag_container_title_message"><span>用户分组</span></div>
<div class="closeModel" @click="closeUserGroupModel"><span class="iconfont">&#xe6c6;</span></div>
</div>
<div class="userTag_container_list">
<div class="userTag_container_list_item">
<div class="userTag_container_list_item_value">
<span :class="{ select: val.id == selectGroupId }" v-for="(val, i) in userGroup" :key="i" @click="selectGroup(val)">{{ val.group_name }}</span>
</div>
</div>
</div>
<div class="userTag_container_handle" @click="handleSetGroup">
<div class="userTag_container_handle_button"><span>确定</span></div>
</div>
</div>
</uni-popup>
</div>
</template>
<script>
import { Toast, navigateTo, Modal } from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
data() {
return {
inputRadio: true,
selectGroupId: '',
userTagList: [],
formType: {
0: 'pc',
1: '微信',
2: '小程序',
3: 'H5'
},
sexType: {
0: '未知',
1: '男',
2: '女'
},
sexArr: [
{
value: 0,
label: '未知'
},
{
value: 1,
label: '男'
},
{
value: 2,
label: '女'
}
],
userGroup: [], // 用户分组
userGroupConcat: {},
queryUserData: {}, // url上挂载的参数
userDetilsData: {} // 用户详细信息
};
},
onLoad(opt) {
this.queryUserData = opt;
this.initUserGroup();
},
onNavigationBarButtonTap() {
this.handleEditUserMessage();
},
methods: {
// 拉黑
handelblock() {
Modal('拉黑用户', `"${this.userDetilsData.nickname}"将被拉黑,请问是否继续?`).then(() => {
http(api.putUserStatus, { userId: this.userDetilsData.user_id }).then(res => {
Toast('拉黑成功');
this.$store.commit('setRefresh', true);
navigateTo(4, '/pages/view/messageList/index');
});
});
},
// 点击投诉按钮,跳转投诉界面
handelComplaint() {
navigateTo(1, '/pages/view/customerServer/complaint?id='+this.userDetilsData.id);
},
// 获取用户详细信息
initData() {
console.log(this.queryUserData);
http(api.userInfo, this.queryUserData).then(res => {
this.userDetilsData = res;
this.selectGroupId = this.userDetilsData.group_id;
});
},
// inputRadio 为 true 时,可以修改客户信息
changeInputRadio() {
this.inputRadio = !this.inputRadio;
},
// 获取所有分组 ---> 获取详细信息
initUserGroup() {
http(api.UserGroup).then(res => {
this.userGroup = res;
this.userGroup.forEach(item => {
this.userGroupConcat[item.id] = item.group_name;
});
this.initData();
});
},
// 打开用户标签选择选项
openUserTag() {
if (!this.inputRadio) {
Toast('请打开修改开关');
return;
}
http(api.userLabelSelect, { id: this.queryUserData.user_id }).then(res => {
this.userTagList = res;
this.$refs.userTagModel.open('center');
});
},
// 选择标签
selectTags(item) {
this.$set(item, 'disabled', !item.disabled);
},
// 选择性别
changeSex(e) {
this.userDetilsData.sex = e.detail.value;
},
// 选择分组弹框
editGroup() {
if (!this.inputRadio) {
Toast('请打开修改开关');
return;
}
this.$refs.userGroupModel.open();
},
// 关闭选择分组
closeUserGroupModel() {
this.$refs.userGroupModel.close();
},
// 选择分组
selectGroup(item) {
if (this.selectGroupId == item.id) {
this.selectGroupId = '';
return;
}
this.selectGroupId = item.id;
},
// 确认选择分组
handleSetGroup() {
this.userDetilsData.group_id = this.selectGroupId;
http(api.setUserGroup, {
id: this.userDetilsData.group_id,
userId: this.userDetilsData.id
}).then(res => {
Toast('用户分组设置完成');
this.$refs.userGroupModel.close();
});
},
// 设置标签
handleSetTags() {
let postData = {
label_ids: [],
un_label_ids: [],
userId: this.queryUserData.user_id
},Label = [];
this.userTagList.forEach(item => {
item.label.forEach(val => {
if (val.disabled) {
postData.label_ids.push(val.id);
Label.push(val)
} else {
postData.un_label_ids.push(val.id);
}
});
});
http(api.putUserLabel, postData).then(res => {
this.$refs.userTagModel.close();
// this.initData();
this.selectGroupId = postData.label_ids
this.userDetilsData.label = Label
});
},
// 关闭用户标签选项
closeUserTagModel() {
this.$refs.userTagModel.close();
},
// 确认修改用户信息
handleEditUserMessage() {
http(api.userUpdateUser, { ...this.userDetilsData, userId: this.userDetilsData.id }).then(res => {
// this.inputRadio = !this.inputRadio;
this.initUserGroup();
Toast('修改成功');
});
},
toMessage() {
navigateTo('2', '/pages/view/customerServer/index', this.queryUserData);
}
}
};
</script>
<style scoped lang="less">
@import url('./less/customerMessage.less');
</style>
+651
View File
@@ -0,0 +1,651 @@
<template>
<view class="container">
<lay-out id='layOut' ref="layOut" :scrollTop="scrollTop" :scrollWithAnimation="false" @goTop="goTop"
@layoutScroll="scroll">
<!-- #ifdef H5 -->
<view slot="header" class="header">
<view class="header_left" @click="goBackToMessageList"><span class="iconfont icon-fanhui"></span></view>
<view class="header_center">
{{ userData.remark_nickname ? userData.remark_nickname : userData.nickname }}
</view>
<view class="header_right" @click="selectCustomerServerOfTopRight(userData)"><span
class="iconfont">&#xe6d3;</span></view>
</view>
<!-- #endif -->
<view slot="content" class="content" id="contentMessage">
<view class="content_list" v-for="(item, index) in messageList" :key="index"
:class="{ right_box: customerServerData.user_ids && customerServerData.user_ids.indexOf(item.user_id) !== -1 }">
<view class="content_list_avar" @click="selectCustomerServer(item)">
<image :src="item.avatar" mode="widthFix"></image>
</view>
<view class="content_list_message">
<view class="content_list_message_value" v-if="item.msn_type <= 2"><text selectable="true"
v-html="replace_em(item.msn)"></text></view>
<view class="content_list_message_product" v-if="item.msn_type == 5">
<view class="content_list_message_orderId">
<span class="content_list_message_orderId_label">商品名称:</span>
<span class="content_list_message_orderId_value">{{ item.other.store_name }}</span>
</view>
<view class="content_list_message_detils">
<view class="content_list_message_detils_image">
<image :src="item.other.image" mode="widthFix"></image>
</view>
<view class="content_list_message_detils_value">
<view class="content_list_message_detils_value_shopMessag">
<view class="content_list_message_detils_value_shopMessag_item">
<view class="content_list_message_detils_value_shopMessag_item_label">
<span>库存:</span>
</view>
<view class="content_list_message_detils_value_shopMessag_item_value">
<span>{{ item.other.stock }}</span>
</view>
</view>
<view class="content_list_message_detils_value_shopMessag_item">
<view class="content_list_message_detils_value_shopMessag_item_label">
<span>销量:</span>
</view>
<view class="content_list_message_detils_value_shopMessag_item_value">
<span>{{ parseInt(item.other.sales) + parseInt(item.other.ficti ? item.other.ficti : 0) }}</span>
</view>
</view>
</view>
<view class="content_list_message_detils_value_shopPrice">
<span>{{ item.other.price }}</span>
</view>
</view>
</view>
</view>
<view class="content_list_message_image" v-if="item.msn_type == 3">
<image @load="imageLoad" :src="item.msn" mode="widthFix" @click="showImage(item.msn)">
</image>
</view>
</view>
<view class="chat_result" v-if="item.user_id == customerServerData.user_id">
<view class="chat_result_ing" v-if="item.is_send == 0">
<!-- 正在发送 -->
<view></view>
</view>
<view class="chat_result_faile" v-if="item.is_send == -1">
<!-- 发送失败 -->
<view>!</view>
</view>
</view>
</view>
</view>
<view slot="bottom" class="footer" :class="{ translateY0: selectModel }">
<view class="footer_input">
<view class="footer_input_icon mr18" @click="selectScriptLibary"><span
class="iconfont">&#xe6b8;</span></view>
<view class="footer_input_content mr18">
<textarea v-model="sendMessage" @input="textareaChange($event)" @focus="textareaFocus"
class="font" value="" @confirm="sendText" auto-height confirm-type="send"
:focus="sendMessageFocus" placeholder="" />
<!-- <p v-html="pCont" class="font"></p> -->
<!-- <span class="iconfont" :class="{ canSend: sendMessage }" @click="sendText">&#xe6bb;</span> -->
</view>
<view class="footer_input_icon">
<span class="iconfont mr18" @click="selectOption(1)">&#xe6cb;</span>
<span class="sendMessage" v-if="sendMessage" @click.stop="sendText">发送</span>
<span class="iconfont" @click="selectOption(2)" v-if="!sendMessage">&#xe6c2;</span>
</view>
</view>
<view class="footer_option" :style="{ height: selectModel ? '440rpx' : '0' }">
<view class="option" v-if="selectModel == 2">
<view @click="uploadImage">
<image src="~static/image/messageList/picture.png" mode="widthFix"></image>
<view>图片</view>
</view>
<view @click="transfer">
<image src="~static/image/messageList/connection.png" mode="widthFix"></image>
<view>转接</view>
</view>
<view @click="authReply">
<image src="~static/images/auth-reply.png" mode="widthFix"></image>
<view>自动回复</view>
</view>
</view>
<scroll-view class="emoji" v-if="selectModel == 1" scroll-y>
<view class="emoji-item" v-for="(item, index) in emoji" :key="index" @click="sendEmoji(item)"><i
class="em" :class="item"></i></view>
</scroll-view>
</view>
</view>
</lay-out>
<uni-popup ref="transferModel" animation type="bottom">
<transfer @close="transferModelClose" :userInfo="userData" @handleSubmit="handleSubmitTransfer"></transfer>
</uni-popup>
<uni-popup ref="scriptLibaryModel" animation type="bottom">
<view class="scriptLibary">
<view class="scriptLibary_header">
<view class="scriptLibary_header_title">
<view class="tit" v-for="(item, index) in speechArtTypeList" :key="index" @click="selectSpeechArtType(item)"
:class="{ speechArtSelectEd: speechArtType == item.id }">
<span>{{ item.label }}</span>
</view>
</view>
<view class="scriptLibary_header_icon">
<view @click="editScript"><span class="iconfont icon_edit">&#xe6b4;</span></view>
<view @click="closeScript"><span class="iconfont">&#xe6b5;</span></view>
</view>
</view>
<view class="scriptLibary_search">
<view class="search">
<span @click="handleSetTitle" class="iconfont icon_search">&#xe6d8;</span>
<input v-model="scriptTitle" type="text" placeholder="搜索快捷回复" />
</view>
</view>
<view class="scriptLibary_content">
<scripts-libary :tagList="speechTagList" :speechArtType="speechArtType" maxHeight="1000rpx"
height="900rpx" :title="scriptTitle" @sendSpeechMessage="sendSpeechMessage"></scripts-libary>
</view>
</view>
</uni-popup>
<uni-popup ref="showImage" animation type="center">
<view class="image_content">
<span class="iconfont" @click="closeImage">&#xe6b5;</span>
<image :src="selectMessageImage" mode="widthFix"></image>
</view>
</uni-popup>
</view>
</template>
<script>
import emoji from 'pages/utils/emoji.js';
import {
navigateBack,
navigateTo,
serialize,
chooseImage,
Toast,
Modal
} from 'pages/utils/uniApi.js';
import transfer from './component/transfer.vue';
import scriptsLibary from './component/scriptLibary.vue';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
import {
mapGetters
} from 'vuex';
const app = getApp();
export default {
components: {
transfer, // 转接客服
scriptsLibary // 话术库
},
computed: mapGetters(['chatList', 'token']),
data() {
return {
selectMessageImage: '', // 选中的图片
sendMessageFocus: false,
pCont: '',
sendMessage: '', // 发送的消息
selectModel: 0, // 1:选择表情 2: 功能选择(转接,上传图片)
emoji: [],
messageList: [],
userData: {},
pageData: {
limit: 20,
upperId: 0
},
speechArtType: 1, // 0 全局 1 私有
speechTagList: [], // 话术分类
scriptTitle: '', // 搜索输入的内容
propstitle: '',
speechArtTypeList: [{
id: 1,
label: '个人库'
},
{
id: 0,
label: '公共库'
}
],
customerServerData: {
user_ids: []
},
scrollTop: 0, // 滚动位置
refresherTriggered: true, // 下拉刷新装状态
audioFun: '', // 音频对象
old: {
scrollTop: 0
},
kefuInfo: {}, //当前客服信息
userId: 0, //当前用户user_id
toChatSend: false //是否发送过to_chat
};
},
watch: {
messageList: {
deep: true,
handler() {
this.messageList.map((item, index) => {
if (index) {
if (item.add_time - this.messageList[index - 1].add_time >= 300) {
item.show = true;
} else {
item.show = false;
}
} else {
item.show = true;
}
return item;
});
}
}
},
onLoad(opt) {
this.emoji = emoji;
this.userId = opt.to_user_id;
if (!this.userId) {
return navigateBack(1);
}
this.customerServerData = this.$store.getters.kefuInfo;
console.log(this.customerServerData)
this.kefuInfo = this.$store.getters.kefuInfo;
this.messageList = this.$store.getters.chatLog(this.userId);
this.userData = this.$store.getters.userInfo(this.userId);
this.getUserInfo();
this.getKefuInfo();
this.initData();
this.sockEventListenr();
this.$store.dispatch('updataUserCount').then(res => {
this.scoket.setBadgeNumber(res.count);
});
},
onUnload() {
console.log('聊天界面卸载');
uni.$off('chat', this.chatEvent);
uni.$off('chat_auth', this.chatAuthEvent);
uni.$off('reply', this.replyEvent);
uni.$off('success', this.successTochat);
uni.$off('close', this.closeEvent);
uni.$off('rm_transfer', this.rmTransferEvent);
this.toChatSend = false;
this.scoket.send({
data: {
id: 0
},
type: 'to_chat'
});
},
// 监听,点击界面右上角的按钮
onNavigationBarButtonTap(e) {
this.selectCustomerServerOfTopRight();
},
methods: {
authReply() {
navigateTo(1, '/pages/view/authReply/index');
},
getKefuInfo() {
this.$store.dispatch('getKeufuInfo').then(res => {
this.kefuInfo = res;
});
},
//获取用户信息
getUserInfo() {
this.$store
.dispatch('getUserInfo', this.userId)
.then(res => {
this.userData = res; // 获取用户信息
uni.setNavigationBarTitle({
title: this.userData.remark_nickname ? this.userData.remark_nickname : this
.userData.nickname
});
})
.catch(err => {
uni.setNavigationBarTitle({
title: this.userData.remark_nickname ? this.userData.remark_nickname : this
.userData.nickname
});
});
},
scroll(value) {
this.old.scrollTop = value;
},
successTochat() {
console.log(this.toChatSend);
//没发过在发送to_chat
if (!this.toChatSend) {
this.scoket.send({
data: {
id: this.userId
},
type: 'to_chat'
});
this.toChatSend = true;
}
},
replyEvent(data) {
this.pushMessageToList(data);
},
chatAuthEvent(data) {
if (data.length) {
data.map(item => {
item.msn = this.replace_em(item.msn);
this.pushMessageToList(item);
})
}
},
chatEvent(data) {
this.messageList.map(item => {
if (item.guid === data.guid) {
item.is_send = 1;
}
});
this.$store.commit('delChatList', {
id: this.userId,
guid: data.guid
});
},
closeEvent() {
this.toChatSend = false;
},
rmTransferEvent(data) {
if (data.recored.to_user_id == this.userId) {
navigateBack(1);
}
},
// socket监听
sockEventListenr() {
if (!this.toChatSend && this.scoket.connectStatus) {
this.scoket.send({
data: {
id: this.userId
},
type: 'to_chat'
});
this.toChatSend = true;
}
uni.$on('success', this.successTochat);
uni.$on('chat', this.chatEvent);
uni.$on('chat_auth', this.chatAuthEvent);
uni.$on('reply', this.replyEvent);
uni.$on('close', this.closeEvent);
uni.$on('rm_transfer', this.rmTransferEvent);
},
// 获取聊天列表
initData() {
this.$store
.dispatch('getChatLogList', {
user_id: this.userId,
pageData: this.pageData
})
.then(res => {
let upperId = this.pageData.upperId;
this.messageList = upperId === 0 ? res : res.concat(this.messageList);
this.pageData.upperId = this.messageList.length ? this.messageList[0].id : 0;
if (this.chatList[this.userId]) {
this.chatList[this.userId].map(item => {
this.messageList.push(item);
});
}
if (upperId === 0) {
this.$nextTick(() => {
this.goBottom();
});
}
})
.catch(err => {
if (this.chatList[this.userId]) {
this.chatList[this.userId].map(item => {
this.messageList.push(item);
});
}
if (upperId === 0) {
this.$nextTick(() => {
this.goBottom();
});
}
});
},
// 滑动到顶部
goTop() {
this.refresherTriggered = true;
this.initData();
},
// 发送文本消息
sendText(e) {
if (this.sendMessage) {
// this.sendMessageFocus = true;
this.sendMsg(this.sendMessage, 1);
this.sendMessage = '';
} else {
return;
// Toast('请输入要发送的消息');
}
},
// 选择表情
sendEmoji(item) {
this.sendMessage += `[${item}]`;
},
// 发送图片
uploadImage() {
let that = this;
chooseImage(1).then(res => {
//#ifndef H5
uni.compressImage({
src: res.tempFilePaths[0],
width: 'auto',
height: 'auto',
success(res) {
that.upload(res.tempFilePath);
},
fail(e) {
console.log(e);
that.upload(res.tempFilePaths[0]);
}
});
//#endif
//#ifdef H5
that.upload(res.tempFilePaths[0]);
//#endif
});
},
upload(tempFilePath) {
uni.uploadFile({
url: app.globalData.http + api.kefuUploadAvatar.url,
filePath: tempFilePath,
name: 'file',
formData: {
// file: res.tempFilePaths[0],
filename: 'file'
},
header: {
// #ifdef MP || APP-PLUS
'Content-Type': 'multipart/form-data',
// #endif
'Authori-zation': 'Bearer ' + this.$store.state.token
},
success: uploadFileRes => {
let unloadResponse = JSON.parse(uploadFileRes.data);
console.log(unloadResponse);
if (unloadResponse.status == 200) {
this.sendMsg(unloadResponse.data.url, 3);
}
},
fail: res => {
Toast('发送图片失败');
},
complete: res => {
console.log(res);
}
});
},
pushMessageToList(data) {
this.messageList.push(data);
this.$nextTick(() => {
this.goBottom();
});
},
imageLoad() {
},
goBottom() {
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.select('#contentMessage')
.boundingClientRect(data => {
this.scrollTop = this.old.scrollTop;
this.scrollTop = data.height + 50;
console.log(data, this.scrollTop);
})
.exec();
});
},
// textarea获取焦点
textareaFocus() {
this.selectModel = 0;
this.$nextTick(() => {
this.goBottom();
});
},
chatOptinos(guid, msn, type, other) {
return {
msn,
msn_type: type,
to_user_id: this.userId,
is_send: 0,
is_tourist: 0,
avatar: this.kefuInfo.avatar,
user_id: this.kefuInfo.user_id,
appid: this.kefuInfo.appid,
other: other || {},
type: 0,
guid
};
},
// 发送消息统一处理
sendMsg(msn, type) {
let guid = this.scoket.guid();
let chat = this.chatOptinos(guid, msn, type);
this.$store.commit('setChatList', {
id: this.userId,
list: chat
});
chat.msn = this.replace_em(msn);
this.pushMessageToList(chat);
http(api.sendMessage, {
guid,
msn,
msn_type: type,
to_user_id: this.userId,
}).then(data => {
this.chatEvent({
guid: data.guid
});
})
},
selectScriptLibary() {
this.$refs.scriptLibaryModel.open();
},
// 选择话术库,个人,工共
selectSpeechArtType(item) {
this.speechArtType = item.id;
this.selectScriptLibary();
},
// 选中话术
sendSpeechMessage(item) {
this.sendMessage = item.message;
this.$refs.scriptLibaryModel.close(); // 关闭话术库弹框
},
// 关闭话术弹框
closeScript() {
this.$refs.scriptLibaryModel.close();
},
// 修改话术库
editScript() {
this.$refs.scriptLibaryModel.close();
navigateTo(1, '/pages/view/customerServer/script', {
speechArtType: this.speechArtType
});
},
// 话术搜索
handleSetTitle() {
this.propstitle = serialize(this.scriptTitle);
},
// 转接
transfer() {
this.selectModel = 0;
this.$refs.transferModel.open();
},
// 确认转接
handleSubmitTransfer() {
this.$refs.transferModel.close();
navigateTo(4, '/pages/view/messageList/index');
},
// 关闭转接弹框
transferModelClose() {
this.$refs.transferModel.close();
},
// 头部方法,回到聊天列表页
goBackToMessageList() {
navigateTo(4, '/pages/view/messageList/index');
},
// 查看用户详情
selectCustomerServer(item) {
if (this.customerServerData.user_ids && this.customerServerData.user_ids.indexOf(item.user_id) === -1) {
navigateTo(1, '/pages/view/customerServer/customerMessage', item);
}
},
selectCustomerServerOfTopRight() {
navigateTo(1, '/pages/view/customerServer/customerMessage', {
user_id: this.userId
});
},
// 选择上传图片,转接
selectOption(item) {
this.selectModel = item;
},
// 聊天表情转换
replace_em(str) {
str = str.replace(/\[em-([\s\S]*)\]/g, "<span class='em em-$1'/></span>");
return str;
},
textareaChange(e) {
let strCont = e.target.value.replace(/\n\s(\s)*/gi, '\n'); // 将多个回车换行合并为 1个
strCont = strCont.replace(/^\s*/gi, ''); // 清除首行的 空格与换行
let strHtml = strCont.replace(/</gi, '&lt;'); // 将所有的 < 转义为 &lt; 防止html标签被转义
strHtml = strCont.replace(/\n(\n)*/gi, '<br>'); // 回车换行替换为 <br>
strHtml = strHtml.replace(/\s+/gi, '&nbsp;'); // 一个或过个空格 替换为 &nbsp;
strCont = strHtml.replace(/&nbsp;/gi, ' '); // 逆向处理
strCont = strCont.replace(/<br>/gi, '\n'); // 逆向处理
strCont = strCont.replace(/&lt;/gi, '<');
/** 如果 p 标签最后的字符为 <br> 并不会单独另起一行, 会导致与 textarea 的高度相差一行,
* 所以在最后添加一个字符, 这样就能保证 P 标签的高度与 textarea 的高度一致
*/
this.pCont = strHtml + '.';
},
// 点击聊天内容
// 点击图片
showImage(item) {
console.log(item);
this.selectMessageImage = item;
this.$refs.showImage.open();
},
closeImage() {
this.$refs.showImage.close();
}
}
};
</script>
<style scoped lang="less">
@import './less/customerServer.less';
@import './less/scriptLibary.less';
</style>
@@ -0,0 +1,159 @@
.selected {
color: @primaryColor !important;
font-weight: bold;
}
.content_header {
height: 150rpx;
display: flex;
align-items: center;
padding: 0 30rpx;
background: #fff;
&_avar {
width: 110rpx;
height: 110rpx;
margin-right: 22rpx;
image {
width: 100%;
}
}
&_userName {
font-weight: 600;
font-size: 16px;
margin-right: 16rpx;
}
&_tag {
span {
display: inline-block;
font-size: 10px;
color: @primaryColor;
background: rgba(56, 117, 234, 0.14);
padding: 3rpx 10rpx;
}
}
}
.content_message {
margin-top: 14rpx;
&_item {
display: flex;
// height: 100rpx;
align-items: center;
background: #fff;
padding: 40rpx 30rpx;
font-size: 14px;
border-bottom: 1px solid #f0f2f7;
&_label {
flex: 0.25;
}
&_value {
flex: 0.75;
font-weight: 600;
input {
font-size: 14px;
}
.tag_list {
flex: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-row-gap: 20rpx;
justify-items: self-start;
span {
font-size: 11px;
font-weight: 500;
color: @primaryColor;
display: inline-block;
padding: 4rpx 14rpx;
border-radius: 16px;
border: 1px solid @primaryColor;
}
}
}
.flex {
display: flex;
align-items: center;
}
}
.marginTop16 {
margin-top: 16rpx;
}
}
.iconfont {
font-weight: 900;
color: #c4c4c4;
font-size: 20px !important;
}
.to_link {
.iconfont {
font-weight: 400;
color: #000;
font-size: 16px !important;
}
}
.userTag_container {
padding: 30rpx 40rpx;
background: #fff;
border-radius: 12px;
width: 690rpx;
box-sizing: border-box;
&_title {
display: flex;
justify-content: flex-end;
position: relative;
&_message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
&_list {
&_item {
margin-top: 78rpx;
&_label {
color: #282828;
font-weight: bold;
margin-bottom: 26rpx;
}
&_value {
display: grid;
grid-template-columns: repeat(3, 178rpx);
grid-row-gap: 26rpx;
grid-column-gap: 26rpx;
span {
font-size: 14px;
display: inline-block;
background: #f0f0f0;
text-align: center;
padding: 12rpx 0;
border-radius: 12rpx;
}
.select {
background: @primaryColor;
color: #fff;
}
}
}
.mg0 {
margin-top: 40rpx;
}
}
&_handle {
width: 610rpx;
height: 76rpx;
background: @primaryColor;
opacity: 1;
border-radius: 43px;
margin: auto;
margin-top: 84rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 600;
font-size: 14px;
}
}
@@ -0,0 +1,358 @@
.header {
height: 116rpx;
background: linear-gradient(90deg, #3875ea 0%, #1890fc 100%);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 30rpx;
color: #fff;
> view {
flex: 1;
text-align: center;
}
.header_left {
text-align: left;
}
&_center {
// position: absolute;
// top: 50%;
// left: 50%;
// transform: translate(-50%, -50%);
font-weight: 600;
font-size: 16px;
white-space: nowrap;
}
.header_right {
text-align: right;
}
}
// 聊天对话框
.content {
padding: 36rpx 30rpx;
&_list {
display: flex;
margin-bottom: 36rpx;
color: #282828;
font-weight: 600;
&_avar {
width: 90rpx;
height: 90rpx;
margin-right: 22rpx;
border-radius: 50%;
// border: 1px solid rgba(0, 0, 0, 0.1);
overflow: hidden;
image {
width: 100%;
}
}
&_message {
max-width: 420rpx;
padding: 15rpx 28rpx;
background: #fff;
border-radius: 7px;
display: flex;
align-items: center;
&_value{
word-break:break-all;
word-wrap:break-word;
}
&_product {
.content_list_message_orderId {
padding-bottom: 15rpx;
border-bottom: 1px solid #eceff8;
}
.content_list_message_detils {
padding-top: 20rpx;
display: flex;
&_image {
width: 204rpx;
height: 204rpx;
border-radius: 6px;
overflow: hidden;
background: #eee;
image {
width: 100%;
height: 100%;
}
}
&_value {
flex: 1;
padding-left: 16rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
&_shopPrice {
color: #f74c31;
margin-top: 10rpx;
}
.content_list_message_detils_value_shopMessag {
}
.content_list_message_detils_value_shopMessag_item {
display: flex;
}
}
}
}
&_image {
max-width: 400rpx;
image {
max-width: 100%;
}
}
}
}
.right_box {
flex-direction: row-reverse;
.content_list_message {
margin-right: 20rpx;
background: #cde0ff;
}
.content_list_avar {
margin-right: 0rpx !important;
}
}
}
.footer {
background: #f3f4f5;
padding-bottom: 20rpx;
&_input {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 30rpx;
border-top: 1px solid #dddddd;
&_icon {
// align-self: flex-start;
}
.iconfont {
font-size: 24px;
}
.mr18 {
margin-right: 18rpx;
}
&_content {
flex: 1;
position: relative;
display: flex;
align-items: center;
background: #fff;
padding: 8rpx;
border-radius: 32px;
overflow: hidden; // 隐藏超出部分的textarea
.font {
padding: 8rpx 16rpx;
}
textarea {
height: 20px;
display: block;
// position: absolute;
// top: 0rpx;
// left: 0rpx;
z-index: 10;
resize: none;
outline: none;
background: none;
color: rgba(0, 0, 0, 1);
font-weight: bold;
border: none;
width: 80%;
font-size: 14px;
}
p {
width: 100%;
display: block;
min-height: 20rpx;
opacity: 0;
}
.iconfont {
color: #e0e0e0;
}
.canSend {
color: @primaryColor;
}
}
.footer_input_icon {
display: flex;
align-items: center;
.iconfont {
font-size: 60rpx;
}
.sendMessage {
display: inline-block;
background: @primaryColor;
color: #fff;
font-size: 14px;
padding: 12rpx 25rpx;
border-radius: 8rpx;
}
}
}
&_option {
background: #fff;
height: 440rpx;
transition: 0.3s;
.option {
padding: 46rpx 65rpx;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 68rpx;
> view {
text-align: center;
image {
width: 90%;
}
> view {
color: #282828;
font-size: 14px;
font-weight: bold;
}
}
}
.emoji {
height: 100%;
.emoji-item {
display: inline-block;
width: 12.5%;
text-align: center;
margin: 20rpx 0;
// display: flex;
// align-items: center;
// justify-content: center;
.em {
width: 56rpx;
height: 56rpx;
box-sizing: border-box;
}
}
}
}
}
.translateY0 {
// transform: translateY(0);
top: 0;
}
.speechArtSelectEd {
color: @primaryColor;
}
.image_content {
max-width: 90%;
margin: auto;
position: relative;
image {
max-width: 100%;
margin: auto;
}
span {
position: absolute;
right: -30rpx;
top: -30rpx;
z-index: 100;
font-size: 60rpx;
opacity: 0.9;
}
}
.chat_result {
display: flex;
align-items: center;
padding: 0 10rpx;
}
.chat_result_ing {
width: 40rpx;
height: 40rpx;
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
border-bottom: 1px solid #ccc;
animation: changeleft 1s linear infinite;
border-radius: 50%;
}
.chat_result_faile {
width: 40rpx;
height: 40rpx;
background: #FF0000;
border-radius: 50%;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
@keyframes changeleft {
0% {
transform: rotate(0deg);
}
50% {
transform: rotate(-180deg);
}
100% {
transform: rotate(-360deg);
}
}
.scriptLibary_header {
display: flex;
padding: 44rpx 32rpx;
position: relative;
justify-content: flex-end;
}
.scriptLibary_header_title[data-v-afab33f6] {
position: absolute;
top: 50%;
display: flex;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.speechArtSelectEd[data-v-afab33f6] {
color: #3875EA;
}
.scriptLibary_header_icon {
display: flex;
}
.tit {
padding: 0 40rpx;
}
.search {
display: flex;
justify-content: center;
text-align: center;
align-items: center;
background: #f5f6f9;
margin: 0 5%;
border-radius: 30rpx;
padding: 10rpx 0;
color: #9f9f9f;
}
.icon_search {
position: absolute;
top: auto;
left: 33%;
}
.uni-input-placeholder {
color: #9f9f9f !important;
}
.icon_edit {
margin-right: 10rpx;
}
@@ -0,0 +1,59 @@
.scriptLibary {
background: #fff;
border-radius: 12rpx 12rpx 0px 0px;
&_header {
display: flex;
justify-content: flex-end;
padding: 40rpx 30rpx;
position: relative;
> div {
display: flex;
}
&_title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
> div:nth-child(1) {
margin-right: 100rpx;
}
}
&_icon {
> div:nth-child(1) {
margin-right: 20rpx;
}
.iconfont {
font-size: 22px;
color: #c8cad0;
}
}
}
&_search {
padding-bottom: 44rpx;
border-bottom: 1px solid #F0F3FA;
> div {
display: flex;
background: #f5f6f9;
width: 690rpx;
height: 68rpx;
margin: auto;
display: flex;
align-items: center;
padding: 0 20rpx;
box-sizing: border-box;
border-radius: 39px;
> input {
flex: 1;
font-size: 14px;
margin-left: 8rpx;
}
.iconfont {
font-size: 22px;
color: #C4C4C4;
}
}
}
}
+134
View File
@@ -0,0 +1,134 @@
<template>
<div class="container">
<lay-out>
<div slot="header" class="header">
<div class="header_title">{{speechArtTypeConcat[speechArtType]}}</div>
<div @click="finish">完成</div>
</div>
<div slot="content" class="content">
<div class="scriptLibary_search">
<div>
<span class="iconfont">&#xe6d8;</span>
<input v-model="propstitle" type="text" placeholder="搜索快捷回复" />
</div>
</div>
<div class="scriptLibary_content">
<scripts-libary
:speechArtType="speechArtType"
:maxHeight="maxHeight"
icCanEdit
:tagList="speechTagList"
:title="propstitle"
></scripts-libary>
</div>
</div>
</lay-out>
</div>
</template>
<script>
import scriptsLibary from './component/scriptLibary.vue';
import { navigateTo, navigateBack } from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
components: {
scriptsLibary
},
data() {
return {
maxHeight: '',
speechArtType: '',
speechArtTypeConcat: {
1: '个人库',
0: '公共库'
},
speechTagList: [], // 话术分类
propstitle: ''
};
},
onLoad(opt) {
this.speechArtType = opt.speechArtType;
this.selectScriptLibary();
console.log(this.speechArtType);
this.$nextTick(() => {
let info = uni.createSelectorQuery().select('.content');
info
.boundingClientRect((data) => {
this.maxHeight = (Math.ceil(data.height) * 1.85) + 'rpx'
})
.exec();
})
},
methods: {
// 查询话术类
selectScriptLibary() {
http(api.kefuServiceCate, {
type: this.speechArtType
}).then(res => {
this.speechTagList = res.data;
});
},
finish() {
navigateBack(1);
}
}
};
</script>
<style lang="less" scoped>
.header {
height: 96rpx;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 30rpx;
position: relative;
background: #fff;
&_title {
position: absolute;
top: 70%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 16px;
font-weight: bold;
}
> div:nth-child(2) {
font-size: 14px;
color: @primaryColor;
}
}
.scriptLibary_search {
background: #fff;
padding-bottom: 44rpx;
border-bottom: 1px solid #f0f3fa;
> div {
display: flex;
background: #f5f6f9;
width: 690rpx;
height: 68rpx;
margin: auto;
display: flex;
align-items: center;
padding: 0 20rpx;
box-sizing: border-box;
border-radius: 39px;
> input {
flex: 1;
font-size: 14px;
margin-left: 8rpx;
}
.iconfont {
font-size: 22px;
color: #c4c4c4;
}
}
}
.content {
overflow: hidden;
height: 100%;
.scriptLibary_content {
}
}
</style>
+184
View File
@@ -0,0 +1,184 @@
<template>
<div class="login_container">
<!-- 头部背景图 -->
<div class="login_container_bg"><!-- <image class="login_container_bg_image" src="~static/dominName/bg.png" mode="widthFix"></image> --></div>
<!-- 头部背景图结束 -->
<div class="login_container_content">
<div class="login_container_content_title"><span>设置请求域名</span></div>
<div class="login_container_content_form">
<div class="login_container_content_form_input">
<picker mode="selector" :range="fileList" @change="chageFile" range-key="label">
<view>
<span>{{ fileListConcat[selectFile] }}</span>
<span>://</span>
</view>
</picker>
<input type="text" v-model="dominName" placeholder="请输入域名" />
<!-- <span class="iconfont">&#xe6f6;</span> -->
</div>
<!-- <div class="login_container_content_form_input">
<span>{{wsFileConcat[selectFile]}}</span>
<span>://</span>
<span>{{dominName}}</span>
</div> -->
</div>
<div class="login_container_content_handle">
<div class="login_button" @click="cancel" v-if="isShowCencel">取消</div>
<div class="login_primary_button" @click="handleSetDominName">确定</div>
</div>
</div>
</div>
</template>
<script>
import { navigateTo, Modal, navigateBack, Toast } from 'pages/utils/uniApi.js';
const app = getApp()
export default {
components: {},
data() {
return {
selectFile: 0,
dominName: '',
fileList: [
{
label: 'http',
value: 0
},
{
label: 'https',
value: 1
}
],
fileListConcat: {
0: 'http',
1: 'https'
},
wsFileConcat: {
0: 'ws',
1: 'wss'
},
isShowCencel: false
};
},
onLoad() {
this.$cache.set('pages_dominName', 1);
this.$cache.remove('accountData');
if (this.$cache.get('dominName')) {
this.dominName = this.$cache.get('dominName');
if (this.$cache.get('requestFile') == 'http') {
this.selectFile = 0;
} else {
this.selectFile = 1;
}
this.isShowCencel = true;
}
},
methods: {
handleSetDominName() {
if (!this.dominName) {
Toast('请设置域名');
return;
}
Modal('温馨提示', `您设置的域名是 "${this.dominName}", 请问是否继续?`).then(res => {
this.$store.commit('setHttp', {
wsFile: this.wsFileConcat[this.selectFile],
dominName: this.dominName,
requestFile: this.fileListConcat[this.selectFile]
});
app.globalData.http = this.$store.state.http
this.scoket.clearPing();
this.$store.commit('clearChat');
this.$store.commit('logout');
navigateTo(1, '/pages/view/login/index');
this.$cache.remove('pages_dominName');
});
},
cancel() {
navigateBack(1);
},
chageFile(e) {
console.log(e);
this.selectFile = e.detail.value;
}
}
};
</script>
<style scoped lang="less">
.login_container {
position: relative;
&_bg {
&_image {
width: 100%;
}
}
&_content {
width: 642rpx;
padding: 54rpx 79rpx 67rpx 79rpx;
// box-shadow: 0px 3px 20px rgba(0, 20, 41, 0.06);
box-sizing: border-box;
position: absolute;
top: 209rpx;
left: 0;
right: 0;
background: #fff;
margin: auto;
border-radius: 20rpx;
&_title {
text-align: center;
font-size: 16px;
font-weight: 600;
margin-bottom: 54rpx;
}
&_form {
&_input {
display: flex;
align-items: center;
height: 96rpx;
// background: #f5f5f5;
padding: 0 28rpx;
// border-radius: 12rpx;
input {
margin-left: 16rpx;
flex: 1;
font-size: 16px;
}
}
> div:nth-child(1) {
border-bottom: 1px solid #ddd;
}
}
&_handle {
margin-top: 57rpx;
display: flex;
.login_primary_button {
font-size: 14px;
width: 100%;
height: 70rpx;
background: @primaryBgColor;
border-radius: 41px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.login_button {
font-size: 14px;
width: 100%;
height: 70rpx;
border-radius: 41px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
margin-right: 32rpx;
}
}
}
}
</style>
+35
View File
@@ -0,0 +1,35 @@
<template>
<div class="container">
<lay-out>
<div slot="content" class="content">
<div v-for="(item, index) in 100" :key="index">{{index}}</div>
</div>
<div slot="bottom">asdad</div>
</lay-out>
</div>
</template>
<script>
import { navigateTo, formatDate, serialize, showWeekFirstDay, showMonthFirstDay, getQuarterStartDate } from 'pages/utils/uniApi.js';
export default {
components: {
},
data() {
return {
}
},
onLoad(opt) {
},
onShow(opt) {
},
methods: {
}
};
</script>
<style lang="less" scoped>
</style>
@@ -0,0 +1,62 @@
<template>
<div class="submit_res_container">
<div class="image_box"><image src="~static/image/feedback/submitSuccess.png" mode="widthFix"></image></div>
<div class="title">
<span>提交成功</span>
</div>
<div class="message">
<div>您的信息提交成功</div>
<div>我们会尽快与您取得联系!</div>
</div>
<div class="handle" @click="handleSubmitRes">
<span>好的</span>
</div>
</div>
</template>
<script>
export default {
methods:{
handleSubmitRes() {
this.$emit('handleSubmitRes')
}
}
}
</script>
<style lang="less" scoped>
.submit_res_container {
padding: 66rpx 111rpx;
background: #fff;
border-radius: 12px;
.image_box {
width: 344rpx;
image {
width: 100%;
}
}
.title {
color: #434343;
font-weight: 600;
text-align: center;
margin: 30rpx 0 12rpx 0;
}
.message {
text-align: center;
font-size: 14px;
color: #9F9F9F;
}
.handle {
width: 426rpx;
height: 76rpx;
background: @primaryBgColor;
opacity: 1;
border-radius: 38px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
margin-top: 54rpx;
}
}
</style>
+87
View File
@@ -0,0 +1,87 @@
<template>
<div class="container">
<lay-out headerColor="#3A3A3A" headerTextColor="#fff">
<div slot="content" class="content">
<div class="header_banner">
<div class="header_banner_content">
<div class="header_banner_content_title">
<span>应用反馈</span>
</div>
<div class="header_banner_content_message">
<span>您有其他对应用的已意见或者您收到不良信息都可以向时反馈我们非常重视您给我们提出宝贵的建议帮助我们不断完善产品谢谢</span>
</div>
</div>
<div class="header_banner_image">
<div class="header_banner_image_imageBox">
<image src="~static/image/feedback/bg.png" mode="widthFix"></image>
</div>
</div>
</div>
<div class="feedback">
<div class="feedback_title">
<span>我要反馈</span>
</div>
<div class="feedback_input">
<input v-model="feedBackData.rela_name" type="text" value="" placeholder="请输入您的姓名"/>
</div>
<div class="feedback_input">
<input v-model="feedBackData.phone" type="text" value="" placeholder="请输入您的联系电话" />
</div>
<div class="feedback_textarea">
<textarea v-model="feedBackData.content" value="" placeholder="请填写留言内容" />
</div>
<div class="handle_button" @click="handleSubmit">
<span>提交</span>
</div>
</div>
</div>
</lay-out>
<uni-popup ref="feedbackReslove" animation>
<submit-response @handleSubmitRes="handleSubmitRes"></submit-response>
</uni-popup>
</div>
</template>
<script>
import {navigateBack} from 'pages/utils/uniApi.js'
import submitResponse from './component/submitResponse.vue';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
components: {
submitResponse
},
data() {
return {
feedBackData: {
rela_name: '',
phone: '',
content: ''
}
}
},
onLoad() {
},
methods: {
handleSubmit() {
http(api.userFeedback, this.feedBackData).then(res => {
this.$refs.feedbackReslove.open('center');
})
},
handleSubmitRes() {
navigateBack(1);
this.$refs.feedbackReslove.close();
}
}
}
</script>
<style lang="less" scoped>
@import url('./less/feedback.less');
</style>
@@ -0,0 +1,135 @@
.container {
background: #f5f5f5;
}
.content {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.header_banner {
display: flex;
align-items: center;
padding: 30rpx;
height: 205rpx;
background: #3a3a3a;
&_content {
flex: 1;
color: #ffffff;
font-size: 16px;
&_title {
margin-bottom: 16rpx;
}
&_message {
font-size: 12px;
line-height: 1.5;
}
}
&_image {
width: 173rpx;
height: 173rpx;
&_imageBox {
width: 100%;
image {
width: 100%;
}
}
}
}
.organization_work {
display: flex;
margin-bottom: 16rpx;
&_item {
flex: 1;
padding: 24rpx 0;
display: flex;
flex-direction: column;
justify-content: space-between;
text-align: center;
background: #fff;
&_label {
font-size: 16px;
margin-bottom: 10rpx;
}
&_value {
color: #9f9f9f;
font-size: 12px;
}
}
.tip {
width: 2px;
height: 100%;
background: #f5f5f5;
}
}
.feedback {
padding: 28rpx 30rpx;
background: #fff;
flex: 1;
&_title {
color: #282828;
font-size: 15px;
font-weight: 600;
margin-bottom: 30rpx;
}
&_input {
height: 78rpx;
background: #f5f5f5;
margin-bottom: 20rpx;
padding-left: 20rpx;
input {
width: 100%;
height: 100%;
font-size: 14px;
}
}
&_textarea {
height: 260rpx;
padding: 19rpx 20rpx;
background: #f5f5f5;
textarea {
font-size: 14px;
}
}
.handle_button {
margin-top: 130rpx;
width: 690rpx;
height: 86rpx;
background: #3875ea;
opacity: 1;
border-radius: 43px;
display: flex;
align-items: center;
justify-content: center;
color: #FFFFFF;
}
.feedback_list {
margin-bottom: 50rpx;
&_item {
padding: 30rpx 15rpx;
display: flex;
border-bottom: 1rpx solid #ccc;
>div:nth-child(1) {
width: 40rpx;
height: 40rpx;
border: 1px solid #ccc;
border-radius: 50%;
margin-right: 15rpx;
display: flex;
align-items: center;
justify-content: center;
}
&_selected {
width: 20rpx;
height: 20rpx;
background: #3875ea;
border-radius: 50%;
}
}
}
}
+339
View File
@@ -0,0 +1,339 @@
<template>
<view class="container">
<lay-out noBottomHeight>
<div slot="header">
<view class="search-content padding">
<view class="uni-input-wrapper"><input class="uni-input" @confirm="initData" v-model="searchData.nickname" placeholder="搜索用户名称" /></view>
</view>
</div>
<div slot="content">
<!-- 通讯录导航 -->
<address-book :bookList="bookList" :letter="letter">
<template v-slot:addressBookList>
<view class="user-content">
<view class="user-list" v-for="(item, index) in Object.keys(bookList)" :key="index">
<view class="number padding">{{ item }}</view>
<view class="user" v-for="(val, i) in bookList[item]" :key="i" @click="selectUser(val)">
<view class="user-list-left"><image :src="val.avatar" mode=""></image></view>
<view class="user-list-right">{{ val.nickname }}</view>
</view>
</view>
</view>
</template>
</address-book>
</div>
</lay-out>
<uni-popup ref="userTagModel" type="bottom" animation>
<div class="userTag_container">
<div class="userTag_container_title">
<div :class="{ on: tabOn == 1 }" class="userTag_container_title_message" @click="tabOn = 1"><span>用户标签</span></div>
<div :class="{ on: tabOn == 2 }" class="userTag_container_title_message" @click="tabOn = 2"><span>分组标签</span></div>
<div class="closeModel" @click="closeUserTagModel"><span class="iconfont">&#xe6b5;</span></div>
</div>
<scroll-view scroll-y="true" class="userTag_container_list">
<div v-if="tabOn == 1" class="userTag_container_list_item" :class="{ mg40: index == 0 }" v-for="(item, index) in userTagList" :key="index">
<div class="userTag_container_list_item_label">
<span>{{ item.name }}</span>
</div>
<div class="userTag_container_list_item_value">
<span :class="{ select: val.disabled }" v-for="(val, i) in item.label" :key="i" @click="selectTags(val)">{{ val.label }}</span>
</div>
</div>
<div v-if="tabOn == 2" class="userTag_container_list_item mg40">
<div class="userTag_container_list_item_value">
<span :class="{ select: val.disabled }" v-for="(val, i) in groupList" :key="val" @click="selectGroups(val)">{{ val.group_name }}</span>
</div>
</div>
</scroll-view>
<div class="userTag_container_handle" @click="handleSetTags">
<div class="userTag_container_handle_button"><span>确定</span></div>
</div>
</div>
</uni-popup>
</view>
</template>
<script>
import AddressBook from './addressBookNavigation/addressBookNavigation';
import { makePy } from '../../utils/utils';
import { navigateTo } from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
components: { AddressBook },
data() {
return {
letter: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
list: [],
search: '',
bookList: {},
customerServerData: {}, // 客服数据
searchData: {
nickname: '',
label_id: '',
group_id: ''
},
userTagList: [],
groupList: [],
tabOn: 1,
};
},
onLoad() {
this.customerServerData = this.$store.state.kefuInfo;
this.bookList = this.$store.state.bookList;
},
onShow() {
this.searchData.label_id = '';
this.searchData.group_id = '';
this.initData();
// { id: this.queryUserData.user_id }
http(api.userLabelSelect).then(res => {
this.userTagList = res;
});
http(api.UserGroup).then(res => {
this.groupList = res;
});
},
onNavigationBarButtonTap() {
// this.toTagList();
this.$refs.userTagModel.open();
},
methods: {
// 关闭弹窗
closeUserTagModel() {
this.$refs.userTagModel.close();
},
// 选择标签
selectTags(item) {
this.$set(item, 'disabled', !item.disabled);
},
selectGroups(item) {
this.$set(item, 'disabled', !item.disabled);
},
// 设置标签
handleSetTags() {
let postData = {
label_ids: [],
group_ids: []
};
console.log(111);
this.userTagList.forEach(item => {
item.label.forEach(val => {
if (val.disabled) {
postData.label_ids.push(val.id);
}
});
});
this.groupList.forEach(item => {
if (item.disabled) {
postData.group_ids.push(item.id);
}
});
console.log(postData);
this.searchData.label_id = postData.label_ids.join(',');
this.searchData.group_id = postData.group_ids.join();
this.closeUserTagModel();
this.initData(this.searchData);
},
// 查询当前客服的客户列表
initData() {
this.$store.dispatch('getBookList', this.searchData).then(res => {
this.bookList = res;
});
},
toTagList() {
navigateTo(1, '/pages/view/setTags/index');
},
selectUser(item) {
navigateTo(1, '/pages/view/customerServer/customerMessage', { user_id: item.to_user_id });
},
// 对通讯录按字母顺序做排序
letterSrot() {
const sortList = [];
const list = this.list;
this.letter.forEach(item => {
const obj = {};
obj.key = item;
obj.list = [];
for (let i = 0; i < list.length; i++) {
if (item == makePy(list[i].username).substr(0, 1)) {
obj.list.push(list[i]);
list.splice(i, 1);
i--;
}
}
if (obj.list.length) sortList.push(obj);
});
this.bookList = sortList;
}
}
};
</script>
<style lang="less" scoped>
@padding-l-r: 30rpx;
@padding-t-p: 20rpx;
@bgc-f: #ffffff;
@fontSize: 28rpx;
@imgSize: 64rpx;
.padding {
padding: @padding-t-p @padding-l-r;
}
.search-content {
position: relative;
background-color: @bgc-f;
input {
border: none;
background-color: #f5f6f9;
color: #c4c4c4;
font-size: @fontSize;
border-radius: 39px;
padding: @padding-t-p 0 @padding-t-p @padding-l-r;
text-align: center;
}
}
.user-content {
background-color: #eeeeee;
.user-list {
.number {
color: #666666;
font-size: @fontSize - 2rpx;
}
.user {
background-color: @bgc-f;
display: flex;
align-items: center;
padding: @padding-t-p @padding-l-r;
border-bottom: 1rpx solid #f0f2f7;
.user-list-left {
width: @imgSize;
height: @imgSize;
padding-right: 20rpx;
image {
width: @imgSize;
height: @imgSize;
border-radius: 50%;
}
}
.user-list-right {
color: #282828;
}
}
}
}
.header {
&_content {
display: flex;
justify-content: flex-end;
align-items: center;
position: relative;
height: 116rpx;
background: #1890fc;
padding: 0 30rpx;
/* #ifdef APP-PLUS */
padding-top: 64rpx;
/* #endif */
color: #fff;
> div {
flex: 1;
}
> div:nth-child(2) {
text-align: center;
font-weight: bold;
}
> div:nth-child(3) {
font-size: 14px;
text-align: right;
}
}
}
.userTag_container {
padding: 30rpx 40rpx;
// padding-bottom: 140rpx;
background: #fff;
width: 100%;
box-sizing: border-box;
&_title {
display: flex;
// justify-content: flex-end;
position: relative;
&_message {
flex: 1;
text-align: center;
// position: absolute;
// top: 50%;
// left: 50%;
// transform: translate(-50%, -50%);
&.on {
font-weight: bold;
color: #3875EA;
}
}
.closeModel {
position: absolute;
top: 0;
right: 0;
color: #c8cad0;
.iconfont {
font-size: 36rpx;
}
}
}
&_list {
height: 600rpx;
&_item {
margin-top: 78rpx;
&_label {
color: #282828;
font-weight: bold;
margin-bottom: 26rpx;
}
&_value {
display: grid;
grid-template-columns: repeat(3, 178rpx);
grid-row-gap: 26rpx;
grid-column-gap: 26rpx;
span {
font-size: 14px;
display: inline-block;
background: #f0f0f0;
text-align: center;
padding: 12rpx 0;
border-radius: 12rpx;
}
.select {
background: @primaryColor;
color: #fff;
}
}
}
.mg0 {
margin-top: 40rpx;
}
}
&_handle {
width: 610rpx;
height: 76rpx;
background: @primaryColor;
opacity: 1;
border-radius: 43px;
margin: auto;
margin-top: 44rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 600;
font-size: 14px;
}
}
</style>
@@ -0,0 +1,265 @@
<template>
<view>
<!-- 右边字母选择导航条 -->
<view class="searchLetter touchClass" :style="{ height: itemH * searchLetter.length + 'px', paddingRight: paddingRight + 'upx', width: searchLetterWidth + 'upx' }">
<view
v-for="(item, index) in searchLetter"
:style="{ height: itemH + 'px' }"
:key="index"
:data-letter="item.name"
@touchstart.stop.prevent="searchStart"
@touchmove.stop.prevent="searchMove"
@touchend.stop.prevent="searchEnd"
>
{{ item.name }}
</view>
</view>
<!-- 字母选择提示框 -->
<block v-if="isShowLetter">
<view class="showSlectedLetter">{{ showLetter }}</view>
</block>
<!-- 通讯录列表 -->
<scroll-view scroll-y :scrollTop="scrollTop"><slot name="addressBookList"></slot></scroll-view>
</view>
</template>
<script>
export default {
name: 'addressBookNavigation',
props: {
//通讯录名字集合
bookList: {
type: Object,
default() {
return {};
},
required: true
},
//索引集合
letter: {
type: Array,
default() {
return ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
}
},
//通讯录索引字母显示高度 upx
bookKeyHeight: {
type: Number,
default() {
return 100;
}
},
//通讯录名字显示高度 upx
bookItemHeight: {
type: Number,
default() {
return 40;
}
},
//右边索引条右边距 upx
paddingRight: {
type: Number,
default() {
return 20;
}
},
//若有搜索框则是搜索框高度 upx
searchHeight: {
type: Number,
default() {
return 20;
}
},
//右边索引条距离搜索框位置 upx
searchLetterTop: {
type: Number,
default() {
return 60;
}
},
//右边索引条宽度 upx
searchLetterWidth: {
type: Number,
default() {
return 40;
}
},
//减去的右边导航条高度 upx
subtractiveSearchLetterHeight: {
type: Number,
default() {
return 80;
}
},
//底部tab高度 使用原生导航栏固定为50px 自定义tab传入自定义tab高度 单位为px
tabHeight: {
type: Number,
default() {
return 100;
}
},
//导航栏高度
navHeight: {
type: Number,
default() {
return 44;
}
}
},
data() {
return {
searchLetter: [],
//右边索引条单个字母元素高度
itemH: 0,
//可滑动区域高度
usableHeight: 0,
screenWidth: 0,
isShowLetter: false,
showLetter: '',
scrollTop: 0,
badPageY: uni.upx2px(this.searchHeight + this.searchLetterTop),
letterPositionTop: 0,
scrollViewPositionTop: 0
};
},
mounted() {
//#ifdef H5
this.letterPositionTop = this.searchHeight + this.searchLetterTop + 88;
this.scrollViewPositionTop = this.searchHeight + 88;
//#endif
//#ifndef H5
this.letterPositionTop = this.searchHeight + this.searchLetterTop;
this.scrollViewPositionTop = this.searchHeight;
//#endif
// #ifdef APP-PLUS
this.letterPositionTop = this.searchHeight + this.searchLetterTop;
this.scrollViewPositionTop = this.searchHeight + 260;
console.log(1);
// #endif
this.countLetterHeight();
},
methods: {
//计算和记录右边每个搜索字母的高度和位置
countLetterHeight() {
const sysInfo = uni.getSystemInfoSync();
//#ifdef H5
const usableHeight = sysInfo.screenHeight - uni.upx2px(this.searchHeight) - this.tabHeight - this.navHeight;
//#endif
//#ifndef H5
const usableHeight = sysInfo.screenHeight - uni.upx2px(this.searchHeight) - this.tabHeight - this.navHeight - sysInfo.statusBarHeight;
//#endif
this.screenWidth = sysInfo.screenWidth;
const letter = this.letter;
const itemH = (usableHeight - uni.upx2px(this.subtractiveSearchLetterHeight) - 20) / letter.length;
const tempObj = [];
for (let i = 0; i < letter.length; i++) {
tempObj.push({
name: letter[i],
tHeight: i * itemH,
bHeight: (i + 1) * itemH
});
}
this.usableHeight = usableHeight;
this.itemH = itemH;
this.searchLetter = tempObj;
},
searchEnd(e) {
const that = this;
setTimeout(() => (that.isShowLetter = false), 500);
},
//点击选择
searchStart(e) {
console.log(e);
const pageY = e.touches[0].pageY - this.badPageY;
this.nowLetter(pageY);
},
//滑动选择
searchMove(e) {
const pageY = e.touches[0].pageY - this.badPageY;
const pageX = e.touches[0].pageX;
if (this.screenWidth - pageX > uni.upx2px(this.searchLetterWidth + this.paddingRight)) {
this.isShowLetter = false;
return;
}
if (pageY < 0 || pageY > this.itemH * this.searchLetter.length) {
this.isShowLetter = false;
return;
}
this.nowLetter(pageY);
},
//计算当前选中的字母并显示
nowLetter(pageY) {
const letterData = this.searchLetter;
for (let i = 0; i < letterData.length; i++) {
if (letterData[i].tHeight <= pageY && pageY <= letterData[i].bHeight) {
this.showLetter = letterData[i].name;
this.isShowLetter = true;
break;
}
}
this.scrollPage(this.showLetter);
},
//计算要滑动的距离
countScrollTop(showLetter) {
let scrollTop = 0;
let cityCount = 0;
let initialCount = 0;
for (let item of Object.keys(this.bookList)) {
if (showLetter == item) {
scrollTop = initialCount * uni.upx2px(this.bookKeyHeight) + cityCount * uni.upx2px(this.bookItemHeight);
break;
} else {
initialCount++;
cityCount += this.bookList[item].length;
}
}
return scrollTop;
},
//页面滑动到与选中字母对应的位置
scrollPage(showLetter) {
const scroll = this.countScrollTop(showLetter);
if (scroll != 0 || showLetter == Object.keys(this.bookList)[0]) this.scrollTop = scroll;
}
}
};
</script>
<style lang="less" scoped>
.searchLetter {
position: fixed;
top: 126rpx;
right: 0;
text-align: center;
display: flex;
flex-direction: column;
color: #666;
z-index: 999;
}
.touchClass {
color: #5a5a5a;
font-size: 28upx;
}
.showSlectedLetter {
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100upx;
height: 100upx;
border-radius: 10upx;
font-size: 44upx;
z-index: 99;
color: #fff;
}
scroll-view {
// position: fixed;
width: 100%;
}
</style>
@@ -0,0 +1,68 @@
<template>
<div class="login_input">
<div class="title_icon">
<slot name="icon"></slot>
</div>
<div class="input">
<input class="input_model" :placeholder="placeholder" :type="type" v-model="value" @blur="$emit('input', $event.target.value)" />
</div>
<div class="clear" v-if="value" @click="clear">
<span class="iconfont">&#xe6b5;</span>
</div>
</div>
</template>
<script>
export default {
model: {
prop: 'value',
event: 'input'
},
props: {
placeholder: {
type: String,
default:''
},
type: {
type: String,
default: 'text'
}
},
data() {
return {
value: ''
};
},
methods:{
clear() {
this.value = '';
this.$emit('input', '');
}
}
};
</script>
<style scoped lang="less">
.login_input {
display: flex;
align-items: center;
padding: 22rpx 0;
border-bottom: 1px solid #EDEDED;
font-size: 14px;
.title_icon {
margin-right: 12rpx;
color: @primaryColor;
}
.input {
flex: 1;
&_model {
border: none !important;
outline: none !important;
}
}
.clear {
opacity: .2;
}
}
</style>
+161
View File
@@ -0,0 +1,161 @@
<template>
<div class="login_container">
<!-- 头部背景图 -->
<div class="login_container_bg"><image class="login_container_bg_image" src="~static/image/login/bg.png" mode="widthFix"></image></div>
<!-- 头部背景图结束 -->
<div class="login_container_content">
<div class="login_container_content_title"><span>欢迎登录</span></div>
<div class="login_container_content_form">
<div class="login_container_content_form_input">
<login-input v-model="loginData.account" type="text" placeholder="请输入账号"><span slot="icon" class="iconfont">&#xe6bc;</span></login-input>
</div>
<div class="login_container_content_form_input">
<login-input v-model="loginData.password" type="password" placeholder="请输入密码"><span slot="icon" class="iconfont">&#xe6bd;</span></login-input>
</div>
<div class="login_container_content_form_input privacy_agreement">
<div class="privacy_agreement_content">
<checkbox-group @change="checkoutPrivaey">
<checkbox class="checkbox" :value="isCheckEd" />
<span>请阅读并勾选</span>
<text @click="showPrivaey">隐私协议</text>
</checkbox-group>
</div>
</div>
</div>
<div class="login_container_content_handle"><div class="login_primary_button" @click="handleLogin">登录</div></div>
<div v-if='isDomainName' class="login-to-set-domin-name"><navigator hover-class="none" url="/pages/view/dominName/index">修改域名</navigator></div>
</div>
</div>
</template>
<script>
import loginInput from './component/loginInput.vue';
import { navigateTo, Modal, Toast } from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
import config from 'pages/config/app.js';
export default {
components: {
loginInput
},
data() {
return {
loginData: {
account: '',
password: ''
},
cid: '',
isCheckEd: '1', // 是否选中隐私协议
isCheckEdArr: [],
isDomainName: config.isDomainName
};
},
onLoad() {
this.$cache.remove('accountData');
this.$cache.set('pages_login', 1);
this.scoket.clearPing()
},
onUnload() {
this.$cache.remove('pages_login');
},
methods: {
async handleLogin() {
if (!this.isCheckEdArr.length) {
Toast('请勾选隐私协议');
return;
}
//#ifdef APP-PLUS
let info = plus.push.getClientInfo();
this.cid = info.clientid;
this.$cache.set('cid', this.cid);
//#endif
let loginRes = await http(api.login, { ...this.loginData, client_id: this.cid, is_app: 1 });
this.$cache.set('userData', loginRes);
this.$store.commit('setToken', loginRes.token);
this.$cache.set('accountData', this.loginData);
//连接长连接
this.scoket.startConnect();
this.$cache.remove('pages_login');
navigateTo(4, '/pages/view/messageList/index');
},
// 勾选隐私协议
checkoutPrivaey(e) {
this.isCheckEdArr = e.detail.value;
},
// 跳转隐私协议
showPrivaey() {
navigateTo(1, '/pages/view/privacyAgreement/index');
}
}
};
</script>
<style scoped lang="less">
.login_container {
position: relative;
&_bg {
&_image {
width: 100%;
}
}
&_content {
width: 642rpx;
padding: 54rpx 79rpx 120rpx 79rpx;
box-shadow: 0px 3px 20px rgba(0, 20, 41, 0.06);
box-sizing: border-box;
position: absolute;
top: 349rpx;
left: 0;
right: 0;
background: #fff;
margin: auto;
border-radius: 20rpx;
&_title {
text-align: center;
font-size: 21px;
font-weight: 600;
margin-bottom: 54rpx;
}
&_handle {
margin-top: 115rpx;
.login_primary_button {
font-size: 15px;
width: 100%;
height: 82rpx;
background: @primaryBgColor;
border-radius: 41px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
}
}
}
.privacy_agreement {
margin-top: 30rpx;
&_content {
font-size: 28rpx;
.checkbox {
transform: scale(0.7);
}
text {
color: @primaryColor;
}
}
}
.login-to-set-domin-name {
font-size: 20rpx;
padding-top: 40rpx;
text-align: center;
}
</style>
+433
View File
@@ -0,0 +1,433 @@
<template>
<div class="container">
<lay-out @goBottom="goBottom" @goTop="goTop" @refresherrefresh="refresherrefresh"
:refresherTriggered="refresherTriggered" @refresherrestore="refresherrestore"
@refresherabort="refresherabort" :scroll-top="scrollTop">
<div slot="header">
<!-- 搜索框 -->
<div class="content_search">
<div class="content_search_box">
<div class="icon" @click="searchUser"><span class="iconfont">&#xe6bf;</span></div>
<div class="input">
<input class="uni-input" @confirm="searchUser" @focus="searchFocus" @blur="searchBlur"
v-model="nickname" type="text" placeholder="搜索用户名称" />
</div>
</div>
</div>
</div>
<div slot="content" class="content">
<!-- 聊天列表 -->
<div class="content_userMessgae">
<uni-swipe-action>
<uni-swipe-action-item v-for="(item, index) in userList" :right-options="options"
@click="deleteUserRecord($event, index)" :key="item.id">
<div :id="`content_userMessgae_item${item.id}`" class="content_userMessgae_item"
@click="connentServerForUser(item)">
<div class="content_userMessgae_item_avar">
<image :src="item.avatar" mode="widthFix"></image>
<span class="online_status" :class="{ off: item.online }"></span>
</div>
<div class="content_userMessgae_item_value">
<div class="content_userMessgae_item_value_user">
<div class="content_userMessgae_item_value_user_nickName">{{ item.nickname }}
</div>
<div class="content_userMessgae_item_value_user_form" v-if="item.type"
:class="formType[item.type].className">
<span>{{ formType[item.type].label }}</span>
</div>
</div>
<!-- 用户消息 -->
<div class="content_userMessgae_item_value_message">
<span>{{ item.message }}</span>
</div>
<!-- 用户消息结束 -->
</div>
<!-- 聊天时间 -->
<div class="content_userMessgae_item_createTime">
<div class="content_userMessgae_item_createTime_value">
<span>{{ item.update_time | toDay }}</span>
</div>
<div class="content_userMessgae_item_createTime_count" v-if="item.mssage_num">
<span>{{ item.mssage_num }}</span>
</div>
</div>
<!-- 聊天时间结束 -->
<div class="content_userMessgae_item_del">删除</div>
</div>
</uni-swipe-action-item>
</uni-swipe-action>
</div>
<!-- <div v-if="!userList.length" class="no_message">
<span>暂无客户!</span>
</div> -->
<!-- 聊天列表结束 -->
</div>
</lay-out>
<div class="fixed_model" v-if="showSearchModel"></div>
<download-progress v-show='download' @clearAbort='clearAbort' :percent='percent'></download-progress>
</div>
</template>
<script>
import {
navigateTo,
Modal,
formatDate,
Toast,
parseToObject
} from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
import {
mapState
} from 'vuex';
import DownloadProgress from '../../../components/DownloadProgress.vue';
export default {
filters: {
toDay(value) {
if (!value) return '';
return formatDate(value * 1000, 'M月d日 hh:mm');
}
},
components: {
DownloadProgress
},
data() {
return {
selectOnlineStatus: 1,
onlineStatus: [{
label: '在线',
value: 1
},
{
label: '下线',
value: 0
}
],
customerServerHandleModel: false, // 客服操作开关
customerServerData: {},
pageData: {
page: 1,
limit: 15
},
formType: {
0: {
className: 'pc',
label: 'pc端'
},
1: {
className: 'gozhonghao',
label: '公众号'
},
2: {
className: 'wchat',
label: '小程序'
},
3: {
className: 'h5',
label: 'H5'
}
},
refresherTriggered: false, // 下拉刷新状态
nickname: '',
is_tourist: '',
userList: [], // 用户列表
audioFun: '', // 音频对象
showSearchModel: false,
lastTapTime: 0,
scrollTop: 0,
count: 0,
userListTemp: {},
options: [{
text: '取消',
style: {
backgroundColor: '#007aff'
}
}, {
text: '删除',
style: {
backgroundColor: '#dd524d'
}
}],
download: false,
percent: 0
};
},
watch: {
chatInfo(data) {
if (data.id) {
let ids = [];
this.userList.map(item => {
ids.push(item.id);
});
if (ids.indexOf(data.id) === -1) {
this.userList.unshift(data);
}
this.$store.commit('setChatInfo', {});
}
}
},
onLoad(opt) {
this.initSocket();
this.userList = this.$store.getters.userRecord.userList;
this.appVersionLevel();
},
onShow(opt) {
this.pageData.page = 1;
// this.initData(this.pageData.page > 1 ? 'concat' : '');
this.initData();
this.getKefuInfo();
this.updataUserCount();
},
onUnload() {
uni.$off('mssage_num', this.mssageNumEvent);
uni.$off('rm_transfer', this.rmTransferEvent);
uni.$off('transfer', this.transferEvent)
},
onTabItemTap(e) {
},
onNavigationBarButtonTap(e) {
this.handleScanCode(); // 吊起相机,扫描二维码
},
computed: {
...mapState(['chatInfo', 'kefuInfo', 'config', 'userRecord', 'userLists'])
},
methods: {
appVersionLevel() {
// #ifdef APP-PLUS
let that = this;
plus.runtime.getProperty(plus.runtime.appid, ({
version,
name
}) => {
//升级过一次就不要在提醒
if (this.$cache.get('update_' + version)) {
return;
}
http(api.appVersion, {
version,
name
}).then(data => {
if (data.update === false) {
return;
}
uni.showModal({
title: '检测到新版本',
content: data.info,
confirmText: '立即升级',
success: (res) => {
if (res.confirm && data.update && data.url) {
that.downloadFile(data.url);
}
},
complete: () => {
this.$cache.set('update_' + version, 1);
}
})
}).catch(res => {})
});
// #endif
},
downloadFile(url) {
this.downloadTask = uni.downloadFile({
url: url,
success: (res) => {
if (res.statusCode === 200) {
plus.runtime.install(
res.tempFilePath, {
force: false
}, () => {
plus.runtime.restart();
}, err => {
console.error(err);
});
}
},
complete: () => {
this.download = false
this.percent = 0;
}
});
this.download = true;
this.downloadTask.onProgressUpdate(res => {
this.percent = res.progress
});
},
clearAbort() {
if (this.downloadTask) {
this.downloadTask.abort();
this.downloadTask = null;
}
},
// 删除聊天列表用户
deleteUserRecord(e, index) {
if (e.index == 1) {
this.$store.dispatch('deleteUserRecord', {
id: this.userList[index].id
}).then(res => {
this.userList.splice(index, 1);
});
}
},
mssageNumEvent(data) {
console.log('mssage_num', data);
this.getMssageNum(data);
},
//被转走事件
rmTransferEvent(data) {
let rmIndex = -1;
this.userList.map((item, index) => {
if (item.id === data.recored.id) {
rmIndex = index;
}
});
if (rmIndex !== -1) {
this.userList.splice(rmIndex, 1);
this.updataUserCount();
}
},
//转接到当前客服事件
transferEvent(data) {
let pushIndex = -1;
this.userList.map((item, index) => {
if (item.id === data.recored.id) {
pushIndex = index;
}
});
if (pushIndex === -1) {
this.userList.unshift(data.recored);
this.updataUserCount();
}
},
initSocket() {
uni.$on('mssage_num', this.mssageNumEvent);
uni.$on('rm_transfer', this.rmTransferEvent);
uni.$on('transfer', this.transferEvent);
},
getKefuInfo() {
this.$store.dispatch('getKeufuInfo').then(res => {
uni.setNavigationBarTitle({
title: res.site_title
});
});
},
// 搜索框获得焦点
searchFocus() {
this.showSearchModel = true;
},
// 搜索框失去焦点
searchBlur() {
this.searchUser();
this.showSearchModel = false;
},
// 搜索用户
searchUser() {
this.pageData.page = 1;
this.initData();
},
// 获取客户列表, 客服信息
initData(type) {
this.pageData.limit = 10;
this.$store.dispatch('getUserRecord', {
nickname: this.nickname,
pageData: this.pageData,
is_tourist: this.is_tourist
}).then(res => {
if (res.length > 0) {
this.pageData.page++;
}
if (type == 'concat') {
this.userList = this.userList.concat(res);
} else {
this.userList = res;
// this.$set(this.userListTemp, this.pageData.page, res);
}
if (this.refresherTriggered) {
this.refresherTriggered = false;
}
});
},
// 下拉刷新被触发
refresherrefresh() {
if (this.refresherTriggered) {
this.refresherTriggered = false;
return;
}
if (!this.refresherTriggered) {
this.refresherTriggered = true;
}
// this.refresherTriggered = true;
this.pageData.page = 1;
this.initData();
},
// 下拉刷新复位
refresherrestore() {
this.refresherTriggered = false;
},
//
refresherabort() {
this.refresherTriggered = false;
},
// 滑动到页面底部
goBottom() {
this.initData('concat');
},
goTop() {
this.pageData.page = 1;
this.initData();
},
updataUserCount() {
this.$store.dispatch('updataUserCount').then(res => {
this.scoket.setBadgeNumber(res.count);
});
},
// 通过sockect连接收取新的消息
getMssageNum(data) {
if (data.num < 1) {
return;
}
this.userList.forEach((item, index) => {
// 如果发送方的user_id 与 列表中的 to_uid 一致
if (item.to_user_id == data.user_id) {
this.userList.splice(index, 1);
}
});
this.userList.unshift(data.recored);
this.updataUserCount();
},
// 扫描二维码
handleScanCode() {
// let str = "http://192.168.31.192:8081/pages/users/scan_login/index?key=463aca66113f65396dc28e3f0041a2f2";
uni.scanCode({
onlyFromCamera: true, // 是否只允许相机扫码,不允许相机选择图片
success(res) {
console.log(res);
navigateTo(1, '/pages/view/authorizedLogin/index', {
key: res.result.split('=')[1]
});
},
fail: () => {}
});
},
connentServerForUser(item) {
item.mssage_num = 0;
navigateTo(1, '/pages/view/customerServer/index', {
to_user_id: item.to_user_id
});
}
}
};
</script>
<style lang="less" scoped>
@import './less/messageList.less';
</style>
@@ -0,0 +1,305 @@
.header {
background: @primaryBgColor;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15rpx;
position: relative;
height: 116rpx;
&_customer {
display: flex;
align-items: center;
&_avar {
margin-right: 10rpx;
border-radius: 50%;
overflow: hidden;
width: 80rpx;
height: 80rpx;
image {
width: 100%;
height: 100%;
}
}
&_status {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
display: flex;
align-items: center;
&_name {
margin-right: 10rpx;
}
&_item {
&_value {
display: flex;
align-items: center;
.tip_primary {
display: inline-block;
width: 12rpx;
height: 12rpx;
background: linear-gradient(143deg, #27f2cb 0%, #14e3b4 100%);
border-radius: 50%;
margin-right: 6rpx;
}
.tip_danger {
background: #f74c31 !important;
}
.tip_type {
font-size: 20rpx;
white-space: nowrap;
}
}
}
}
}
&_handle {
display: flex;
color: rgba(255, 255, 255, 0.8);
align-items: center;
.font {
// display: inline-block;
// width: 38rpx;
// height: 38rpx;
// background: rgba(255, 255, 255, 0.22);
text-align: center;
line-height: 38rpx;
margin-right: 8rpx;
font-size: 16px;
// border-radius: 50%;
}
.loginOut {
font-size: 14px;
}
}
&_checkout_status {
position: absolute;
width: 214rpx;
height: 173rpx;
background: #434343;
top: 140rpx;
left: 43rpx;
z-index: 90;
border-radius: 10px;
&_sanjiao {
background: #434343;
position: absolute;
top: -15rpx;
left: 30rpx;
width: 30rpx;
height: 30rpx;
transform: rotate(45deg);
}
&_select {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
&_item {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
padding: 0 30rpx;
border-bottom: 1px solid #fff;
&_tip {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: linear-gradient(180deg, #bcbcbc 0%, #919191 100%);
}
.tip_select {
background: linear-gradient(143deg, #27f2cb 0%, #14e3b4 100%);
}
.tip_select_outline {
background: #f74c31;
}
&_status {
flex: 1;
padding-left: 15rpx;
font-size: 14px;
}
&_icon {
color: #b9b9b9;
}
}
&_line {
height: 1px;
width: 100%;
background: #f0f1f2;
}
}
}
}
// 内容部分样式
.content {
&_search {
padding: 20rpx 30rpx;
border-bottom: 1px solid #eceff8;
background: #fff;
display: flex;
align-items: center;
&_box {
flex: 1;
// margin-right: 20rpx;
display: flex;
padding: 14rpx 22rpx;
background: #f5f6f9;
border-radius: 39rpx;
.icon {
margin-right: 12rpx;
color: #c4c4c4;
}
.input {
width: 100%;
align-items: center;
input {
font-size: 14px;
}
}
}
.reload {
}
}
// 聊天内容
&_userMessgae {
padding: 0 30rpx;
background: #fff;
&_item {
position: relative;
display: flex;
padding: 28rpx 0;
border-bottom: 1px solid #f6f6f6;
transition: transform 0.3s;
&_avar {
border-radius: 50%;
// overflow: hidden;
width: 96rpx;
height: 96rpx;
margin-right: 22rpx;
// border: 1px solid rgba(0, 0, 0, 0.1);
// background: @primaryBgColor;
position: relative;
image {
width: 100%;
}
.online_status {
position: absolute;
right: 0rpx;
bottom: 0rpx;
z-index: 10;
display: inline-block;
width: 15rpx;
height: 15rpx;
background: #ccc;
border-radius: 50%;
}
.off {
background: linear-gradient(143deg, #27f2cb 0%, #14e3b4 100%);
}
}
&_value {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
padding-top: 8rpx;
&_user {
display: flex;
align-items: center;
&_nickName {
font-size: 14px;
color: rgba(0, 0, 0, 0.7);
font-weight: 600;
margin-right: 16rpx;
}
&_form {
font-size: 26rpx;
padding: 0 10rpx;
}
.wchat {
background: rgba(56, 117, 234, 0.14);
color: rgba(56, 117, 234, 1);
}
.h5 {
background: rgba(255, 162, 0, 0.18);
color: #d08800;
}
.gozhonghao {
background: rgba(0, 186, 100, 0.14);
color: #00a219;
}
.pc {
color: #820adb;
background: rgba(133, 64, 227, 0.14);
}
}
// 用户消息
&_message {
color: rgba(159, 159, 159, 1);
font-size: 24rpx;
width: 100%;
.overText;
}
}
// 聊天时间
&_createTime {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-top: 8rpx;
&_value {
color: rgba(159, 159, 159, 1);
font-size: 22rpx;
}
&_count {
align-self: flex-end;
display: inline-block;
width: 38rpx;
height: 38rpx;
line-height: 38rpx;
text-align: center;
background: rgba(247, 76, 49, 1);
color: #fff;
border-radius: 50%;
font-size: 24rpx;
}
}
&_del {
position: absolute;
top: 0;
right: -30rpx;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100rpx;
background-color: rgba(255, 0, 0, 1);
font-size: 28rpx;
color: rgba(255, 255, 255, 1);
transform: translateX(100%);
}
&.on {
transform: translateX(-100rpx);
}
}
}
}
.no_message {
text-align: center;
padding: 60rpx 0;
font-size: 14px;
color: #ccc;
}
@@ -0,0 +1,33 @@
<template>
<view class="agreement">
<view class="content" v-html="agreement"></view>
</view>
</template>
<script>
import api from 'pages/api/api.js';
import http from 'pages/api/index';
export default {
data() {
return {
agreement: ''
};
},
mounted() {
this.getAgreement();
},
methods: {
getAgreement() {
http(api.userAgreement, {}).then(res => {
console.log(res);
this.agreement = res.content;
});
}
}
};
</script>
<style>
.agreement .content {
margin: 10rpx;
}
</style>
+137
View File
@@ -0,0 +1,137 @@
<template>
<div class="container">
<lay-out>
<div slot="content">
<div class="content">
<div class="content_tag"><span>标签名称</span></div>
<!-- <div class="content_input"><input type="text" placeholder="请输入标签名称" /></div> -->
<div class="content_input">
<span>{{ tagsData.label }}</span>
</div>
<div class="content_tag"><span>标签成员</span></div>
<div class="content_group">
<!-- <div class="content_group_handle">
<span class="iconfont">&#xe6c1;</span>
<span>添加成员</span>
</div> -->
<view class="user-content">
<view class="user-list" v-for="item in canEachUserList" :key="item.key">
<div class="content_tag">
<span>{{ item.key }}</span>
</div>
<uni-swipe-action v-for="items in item.list" :key="items.id">
<uni-swipe-action-item>
<view class="user" @click="toUserMessage(items)">
<view class="user-list-left"><image :src="items.avatar" mode=""></image></view>
<view class="user-list-right">{{ items.nickname }}</view>
</view>
<template v-slot:right>
<div class="delete_tags" @click="deleteUser(items)"><text>删除</text></div>
</template>
</uni-swipe-action-item>
</uni-swipe-action>
</view>
</view>
</div>
</div>
</div>
</lay-out>
</div>
</template>
<script>
import { navigateTo, navigateBack, Modal, Toast } from 'pages/utils/uniApi.js';
import { makePy } from 'pages/utils/utils';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
data() {
return {
userList: [],
canEachUserList: [],
tagsData: {}
};
},
onLoad(opt) {
this.tagsData = this.$cache.get('userTagData');
this.canEachUserList = this.letterSrot(this.tagsData.user);
},
methods: {
// 前往用户详情界面
toUserMessage(item) {
navigateTo(1, '/pages/view/customerServer/customerMessage', { ...item, user_id: item.id });
},
// 删除用户
deleteUser(item) {
Modal('温馨提示', `"${item.nickname}"将被删除,请问是否继续?`).then(() => {
http(api.deleteUserLabel, {
userId: item.id,
labelId: this.tagsData.id
}).then(res => {
this.canEachUserList.forEach(key => {
key.list.forEach((val, index, arr) => {
if (item.id == val.id) {
arr.splice(index, 1);
}
});
});
Toast('删除成功');
});
});
},
cencel() {
navigateBack(1);
},
// 对通讯录按字母顺序做排序
letterSrot(userList) {
const sortList = [];
const list = userList;
const letter = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
letter.forEach(item => {
const obj = {};
obj.key = item;
obj.list = [];
for (let i = 0; i < list.length; i++) {
if (item == makePy(list[i].nickname).substr(0, 1)) {
obj.list.push(list[i]);
list.splice(i, 1);
i--;
}
}
if (obj.list.length) sortList.push(obj);
});
return sortList;
}
}
};
</script>
<style scoped lang="less">
@import url('./less/addTag.less');
.header {
display: flex;
justify-content: space-between;
align-items: center;
height: 96rpx;
padding: 0 30rpx;
font-size: 16px;
&_title {
font-weight: 600;
}
.finish {
width: 92rpx;
height: 56rpx;
background: #dddddd;
opacity: 1;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
color: #aaaaaa;
opacity: 0;
}
}
</style>
+87
View File
@@ -0,0 +1,87 @@
<template>
<div class="container">
<lay-out>
<div slot="content" class="content">
<!-- <div class="handle_title" @click="createdTag">
<span class="iconfont">&#xe6c1;</span>
<span class="handle_title_message">新建标签</span>
</div> -->
<!-- <uni-swipe-action>
<uni-swipe-action-item>
<div class="tag_container">
<div class="tag_container_name">
<span class="name">意向客户</span>
<span class="num">(9)</span>
</div>
<div class="tag_container_user">
<div class="userList">
阿萨大大啊水水水水水水水水水水水水水水水水水水水啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊反反复复反反复复反反复复方法方法分分分3
</div>
</div>
</div>
<template v-slot:right>
<div class="delete_tags">
<text>删除</text>
</div>
</template>
</uni-swipe-action-item>
</uni-swipe-action> -->
<div class="tag_container" v-for="(item, index) in tagList" :key="index" @click="toTagDetils(item)">
<div class="tag_container_name">
<span class="name">{{item.label}}</span>
<span class="num" v-if="item.user.length">({{ item.user.length }})</span>
</div>
<div class="tag_container_user">
<div class="userList" v-if="item.user.length">
{{item.user.map(item=> item.nickname).join('')}}
</div>
</div>
</div>
</div>
</lay-out>
</div>
</template>
<script>
import { navigateTo} from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
data() {
return {
userData: {},
tagList: []
};
},
onLoad(opt) {
this.userData = opt;
},
onShow() {
this.initData();
},
methods: {
initData() {
http(api.userLabe, { id: this.userData.user_id }).then(res => {
console.log(res);
this.tagList = res;
})
},
toTagDetils(item) {
this.$cache.get('userTagData', item);
navigateTo(1, '/pages/view/setTags/addTag');
},
createdTag() {
navigateTo(1, '/pages/view/setTags/addTag');
}
}
};
</script>
<style lang="less" scoped>
@import url('./less/setTags.less');
</style>
@@ -0,0 +1,63 @@
@padding-l-r:30rpx;
@padding-t-p:20rpx;
@bgc-f:#FFFFFF;
@fontSize:28rpx;
@imgSize:64rpx;
.content_tag {
padding: 14rpx 30rpx;
font-size: 12px;
color: #999999;
background: #eeeeee;
}
.content_input {
padding: 24rpx 30rpx;
input {
font-size: 14px;
}
}
.content_group {
&_handle {
color: #3875EA;
height: 88rpx;
display: flex;
align-items: center;
border-bottom: 1px solid #F0F2F7;
padding: 0 30rpx;
}
}
.user-content{
background-color: #EEEEEE;
.user-list{
.number{
color: #666666;
font-size:@fontSize - 2rpx;
}
.user{
background-color: @bgc-f;
display: flex;
align-items: center;
padding:30rpx 28rpx;
border-bottom: 1rpx solid #F0F2F7;
.user-list-left{
width: @imgSize;
height: @imgSize;
padding-right: 20rpx;
image{
width: @imgSize;
height: @imgSize;
border-radius: 50%;
}
}
.user-list-right{
color: #282828;
}
}
}
}
.delete_tags {
padding: 27rpx 37rpx;
background: #F74C31;
color: #fff;
}
@@ -0,0 +1,33 @@
.bg {
background: #fff;
border-bottom: 1px solid #f0f2f7;
}
.handle_title {
color: @primaryColor;
padding: 35rpx 30rpx;
.bg;
}
.tag_container {
.bg;
padding: 15rpx 30rpx;
font-size: 15px;
&_name {
color: #282828;
font-weight: 600;
}
&_user {
color: #999999;
margin-top: 6rpx;
.userList {
.overText;
}
}
}
.delete_tags {
padding: 39rpx;
background: #F74C31;
color: #fff;
font-size: 14px;
}
+256
View File
@@ -0,0 +1,256 @@
<template>
<div class="container">
<lay-out>
<!-- <div slot="header" class="header">
<div class="title_name">统计</div>
</div> -->
<div slot="content">
<div class="statistics"></div>
<div class="statistics_list">
<div class="statistics_list_item">
<div class="statistics_list_item_value">{{ statisticsAll.all }}</div>
<div class="statistics_list_item_label">全部客户</div>
</div>
<div class="statistics_list_item">
<div class="statistics_list_item_value">{{ statisticsAll.toDayKefu }}</div>
<div class="statistics_list_item_label">今日客户</div>
</div>
<div class="statistics_list_item">
<div class="statistics_list_item_value">{{ statisticsAll.month }}</div>
<div class="statistics_list_item_label">本月客户</div>
</div>
<div class="statistics_list_item">
<div class="statistics_list_item_value">{{ statisticsAll.toDayTourist }}</div>
<div class="statistics_list_item_label">今日游客</div>
</div>
</div>
<div class="content">
<div class="content_charts">
<div class="content_charts_title">
<div class="content_charts_title_select">
<scroll-view scroll-x class="content_charts_title_select_scroll">
<view class="scroll_content">
<view class="scroll_content_item" v-for="(item, index) in monthList" :key="index" @click="handleSelectMonth(item)">
<div class="scroll_content_item_label" :class="{ selectEd: selectMonth == item.value }">{{ item.label }}</div>
<div class="tip" v-if="selectMonth == item.value"></div>
</view>
</view>
</scroll-view>
</div>
<div class="content_charts_title_date">
<biaofun-datetime-picker
class="positionPicker"
:class="{ primaryColor: searchChartsData.value }"
placeholder="请选择年月"
:defaultValue="searchChartsData.value"
start="2000"
end="2100"
fields="month"
@change="changeDatetimePicker"
:key="searchChartsData.value"
></biaofun-datetime-picker>
<!-- <span>{{searchChartsData.value}}</span> -->
<span class="iconfont">&#xe6c5;</span>
</div>
</div>
<div class="content_charts_value">
<view class="charts-box">
<div class="title">新增()</div>
<qiun-data-charts type="area" :inScrollView="true" canvasId="scrolllineid" :opts="opts" :chartData="chartData" :ontouch="true" :canvas2d="true" />
</view>
</div>
</div>
</div>
</div>
</lay-out>
</div>
</template>
<script>
import biaofunDatetimePicker from '@/components/biaofun-datetime-picker/biaofun-datetime-picker.vue';
import { formatDate } from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
export default {
components: {
biaofunDatetimePicker
},
data() {
return {
selectMonth: 'yesterday',
chartData: {
categories: [],
series: [
{
name: '客户',
data: [],
color: '#3875EA',
legendShape: 'square' // 长方形
},
{
name: '游客',
data: [],
color: '#3FC7A9',
legendShape: 'square'
}
]
},
opts: {
enableScroll: true,
extra: {
area: { type: 'curve', addLine: true, gradient: true }
},
xAxis: {
type: 'grid',
gridType: 'solid',
itemCount: 5, //x轴单屏显示数据的数量,默认为5个
scrollShow: false, //新增是否显示滚动条,默认false
scrollAlign: 'left', //滚动条初始位置
scrollBackgroundColor: '#F7F7FF', //默认为 #EFEBEF
scrollColor: '#DEE7F7' //默认为 #A6A6A6
},
legend: {
show: true,
position: 'top',
float: 'right',
padding: 2,
margin: 20,
backgroundColor: 'rgba(0,0,0,0)',
borderColor: 'rgba(0,0,0,0)',
borderWidth: 0,
fontSize: 13,
fontColor: '#666666',
lineHeight: 11,
hiddenColor: '#CECECE',
itemGap: 10
}
},
statisticsAll: {}, // 客户总统计数据
searchChartsData: {
year: String(new Date().getFullYear() + '-' + new Date().getMonth() + 1),
month: new Date().getMonth() + 1,
value: ''
}
};
},
computed: {
monthList() {
let arr = [
{
label: '昨天',
value: 'yesterday'
},
{
label: '今天',
value: 'today'
},
{
label: '7天',
value: 'lately7'
},
{
label: '30天',
value: 'lately30'
},
{
label: '半年',
value: 'lately180'
}
];
// for (let i = 1; i < new Date().getMonth(); i++) {
// arr.push({
// month: i,
// label: i + '月'
// });
// }
// let monthArr = [
// {
// label: '当月',
// month: new Date().getMonth() + 1
// },
// {
// label: '上月',
// month: new Date().getMonth()
// }
// ];
// arr.push(...monthArr);
// arr.reverse();
return arr;
}
},
watch: {
// searchChartsData: {
// handler() {
// this.getChartsData();
// },
// deep: true,
// immediate: true
// }
},
onShow() {
// this.searchChartsData.value = formatDate(new Date().getTime(), 'yyyy-MM');
// this.selectMonth = this.searchChartsData.month; // 自动选择本月
this.initData();
this.getChartsData();
},
methods: {
// 获取数据总统计
initData() {
http(api.statisticsAll, {}).then(res => {
this.statisticsAll = res;
});
},
// 获取图标统计数据
getChartsData() {
let categories = [];
http(api.statisticsIndex, { time: this.selectMonth }).then(res => {
// 构建 X 轴数据
// 将两条年份数据拼合在一起, 给对应的条目赋值
// if (res.list.length) {
res.list.forEach(item => {
categories.push(item.time);
});
this.chartData.series[0].data = res.list.map(item => item.number);
// }
// if (res.tourist.length) {
res.tourist.forEach(item => {
categories.push(item.time);
});
this.chartData.series[1].data = res.tourist.map(item => item.number);
// }
// 数组去重
categories = Array.from(new Set(categories));
// 数组从小到大排序
categories.sort((val1, val2) => {
return Number(val1.replace(/-/g, '')) - Number(val2.replace(/-/g, ''));
});
this.chartData.categories = categories;
});
},
// 选择年份
changeDatetimePicker(e) {
this.searchChartsData.value = formatDate(e.dt, 'yyyy/MM');
this.selectMonth = this.searchChartsData.value;
this.getChartsData();
},
// 选择月份
handleSelectMonth(item) {
this.selectMonth = item.value;
this.searchChartsData.value = '';
this.getChartsData();
}
}
};
</script>
<style lang="less" scoped>
@import url('./less/statistics.less');
</style>
@@ -0,0 +1,141 @@
.header {
height: 201rpx;
background: @primaryBgColor;
.title_name {
text-align: center;
padding: 20rpx;
font-size: 16px;
font-weight: 600;
color: #fff;
}
}
.statistics {
background: #1890FC;
padding: 43rpx 30rpx;
position: absolute;
top: 0;
left: 0;
width: 100%;
box-sizing: border-box;
}
.statistics_list {
position: relative;
z-index: 10;
display: flex;
justify-content: space-between;
width: 690rpx;
box-sizing: border-box;
margin: auto;
text-align: center;
padding: 43rpx 30rpx;
background: #fff;
border-radius: 12px;
&_item {
&_value {
font-size: 20px;
font-weight: bold;
margin-bottom: 8rpx;
}
&_label {
color: #999999;
font-size: 12px;
}
}
}
.content {
padding-top: 36.2rpx;
&_charts {
width: 690rpx;
// padding: 34rpx 26rpx;
box-sizing: border-box;
margin: auto;
background: #fff;
// border-radius: 12px;
// overflow: hidden;
&_title {
display: flex;
justify-content: space-between;
padding: 34rpx 26rpx;
padding-bottom: 0;
&_select {
flex: 1;
display: flex;
align-items: center;
&_scroll {
.scroll_content {
display: flex;
// width: 100%;
justify-content: space-around;
&_item {
// margin-right: 52rpx;
&_label {
font-size: 15px;
white-space: nowrap;
color: #999999;
text-align: center;
margin-bottom: 6rpx;
}
.selectEd {
color: @primaryColor;
font-weight: bold;
}
.tip {
width: 40rpx;
height: 4rpx;
background: #3875ea;
opacity: 1;
margin: auto;
}
}
}
}
}
.content_charts_title_date {
// width: 110rpx;
// height: 110rpx;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
border-left: 1px solid #ccc;
padding-left: 30rpx;
// white-space: nowrap;
// position: relative;
.positionPicker {
// position: absolute;
// top: 0;
// left: 0;
// opacity: 0;
}
}
}
&_value {
.charts-box {
position: relative;
.title {
position: absolute;
top: 70rpx;
left: 30rpx;
font-size: 11px;
color: #cccccc;
z-index: 999;
}
}
}
}
}
.content_charts_title_date:before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 1px;
height: 100%;
background: #000000;
opacity: 0.08;
}
.primaryColor {
color: @primaryColor;
}
+387
View File
@@ -0,0 +1,387 @@
<template>
<div class="container">
<lay-out>
<div slot="content">
<div class="header"></div>
<div class="box">
<div class="img" @click="setting">
<image :src="customerServerData.avatar" mode=""></image>
</div>
<div>
<div class="box_font1">
<span>{{ customerServerData.nickname }}</span>
<span class="isOnline" :class="{ online: customerServerData.online == '1' }"
@click="handleChangeOnline">
<span></span>
<span>{{ customerServerData.online == '1' ? '在线' : '离线' }}</span>
<!-- 切换上线下线弹框开始 -->
<span class="onLineModel" v-if="onlineModel">
<span class="sanjiao"></span>
<span class="model_text" @click="online(1)">
<span class="dian online"></span>
<span>在线</span>
<span class="iconfont"
:class="{ opacity0: !(customerServerData.online == '1') }">&#xe6d1;</span>
</span>
<span class="xian"></span>
<span class="model_text" @click="online(0)">
<span class="dian"></span>
<span>离线</span>
<span class="iconfont"
:class="{ opacity0: !(!customerServerData.online || customerServerData.online == '0') }">&#xe6d1;</span>
</span>
</span>
<!-- 切换上线下线弹框结束 -->
</span>
</div>
<div class="box_font2">手机号{{ customerServerData.phone }}</div>
</div>
</div>
<div class="content">
<div class="content_box" @click="setting">
<div class="icon"><span class="iconfont iconPrimaryColor">&#xe6bc;</span></div>
<div class="text">个人信息</div>
<div>
<image src="~static/images/right.png" alt=""></image>
</div>
</div>
<div class="line"></div>
<div class="content_box" @click="authReply">
<div class="icon"><span class="iconfont iconPrimaryColor">&#xe6d9;</span></div>
<div class="text">自动回复</div>
<div>
<image src="~static/images/right.png" alt=""></image>
</div>
</div>
<div class="line"></div>
<div class="content_box" @click="feedback">
<div class="icon"><span class="iconfont iconPrimaryColor">&#xe6d9;</span></div>
<div class="text">意见反馈</div>
<div>
<image src="~static/images/right.png" alt=""></image>
</div>
</div>
<div class="line" v-if='isDomainName'></div>
<div class="content_box" v-if='isDomainName' @click="editDominName">
<div class="icon"><span class="iconfont iconPrimaryColor">&#xe6f9;</span></div>
<div class="text">域名设置</div>
<div class="message">
<span>{{ dominName }}</span>
<image src="~static/images/right.png" alt=""></image>
</div>
</div>
</div>
<div class="content">
<div class="content_box" @click="loginOut">
<div class="icon"><span class="iconfont iconPrimaryColor">&#xe6f8;</span></div>
<div class="text">退出登录</div>
<div>
<image src="~static/images/right.png" alt=""></image>
</div>
</div>
</div>
<div class="copyright"><span class="iconfont">&#xe6f7;</span></div>
</div>
</lay-out>
</div>
</template>
<script>
import {
navigateTo,
Modal
} from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
import config from 'pages/config/app.js';
export default {
components: {},
data() {
return {
customerServerData: {},
dominName: '',
use: false,
onlineModel: false,
isDomainName: config.isDomainName
};
},
onLoad(opt) {
this.dominName = this.$cache.get('dominName');
this.customerServerData = this.$store.getters.kefuInfo;
},
onShow() {
this.initData();
},
onHide() {
this.onlineModel = false;
},
onUnload() {
this.onlineModel = false;
},
methods: {
// 获取当前登录客服信息
initData() {
this.$store.dispatch('getKeufuInfo').then(res => {
this.customerServerData = res;
});
},
// 意见反馈
feedback() {
navigateTo(1, '/pages/view/feedback/index');
},
authReply() {
navigateTo(1, '/pages/view/authReply/index');
},
// 设置
setting() {
navigateTo(1, '/pages/view/user/setting/setting', this.customerServerData);
},
// 修改域名
editDominName() {
navigateTo(1, '/pages/view/dominName/index');
},
// 退出登录
loginOut() {
Modal('温馨提示', '您确定要退出登录吗?').then(() => {
http(api.userLogout)
.then(res => {
//关闭长连接
this.scoket.clearPing();
this.$store.commit('clearChat');
this.$store.commit('logout');
navigateTo(3, '/pages/view/login/index');
})
.catch(res => {
this.scoket.clearPing();
this.$store.commit('clearChat');
this.$store.commit('logout');
navigateTo(3, '/pages/view/login/index');
});
});
},
// 更改在线状态
handleChangeOnline() {
this.onlineModel = !this.onlineModel;
},
online(val) {
this.scoket.send({
data: {
online: val
},
type: 'online'
});
this.customerServerData.online = val;
}
}
};
</script>
<style lang="less" scoped>
.header {
background: #1890fc;
padding: 43rpx 30rpx;
// height: 83rpx;
position: absolute;
top: 0;
left: 0;
width: 100%;
box-sizing: border-box;
}
.box {
width: 690rpx;
height: 180rpx;
background: #ffffff;
border-radius: 12px;
margin: 0 auto;
margin-bottom: 24rpx;
display: flex;
justify-content: left;
align-items: center;
position: relative;
z-index: 10;
image {
width: 100rpx;
height: 100rpx;
margin-left: 34rpx;
margin-right: 20rpx;
border-radius: 50%;
}
.box_font1 {
font-size: 30rpx;
font-weight: 600;
line-height: 42rpx;
color: #282828;
margin-bottom: 8rpx;
display: flex;
align-items: center;
}
.box_font2 {
font-size: 26rpx;
font-weight: 400;
line-height: 37rpx;
color: #999999;
}
}
.content {
width: 690rpx;
margin: 0 auto;
margin-top: 40rpx;
background: #fff;
border-radius: 10rpx;
.line {
width: 90%;
height: 1px;
background: #eeeeee;
position: relative;
left: 10%;
}
.content_box {
padding: 28rpx 30rpx;
// height: 88rpx;
background: #ffffff;
border-radius: 12rpx;
// margin-bottom: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
.icon {
margin-right: 20rpx;
}
.text {
flex: 1;
// width: 84%;
}
image {
width: 33rpx;
height: 33rpx;
}
.message {
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
}
}
}
.copyright {
display: flex;
justify-content: center;
margin-top: 300rpx;
.iconfont {
font-size: 250rpx;
color: #cccccc;
text-align: center;
}
}
.iconPrimaryColor {
// color: @primaryColor;
}
.isOnline {
width: 100rpx;
height: 40rpx;
border-radius: 18rpx;
background: #999999;
display: flex;
margin-left: 9rpx;
color: #fff;
align-items: center;
justify-content: center;
text-align: center;
font-size: 20rpx;
position: relative;
}
.isOnline>span:nth-child(1) {}
.dian {
display: inline-block;
width: 8rpx;
height: 8rpx;
background: #ffffff;
border-radius: 50%;
margin-right: 6rpx;
}
.online {
background: #20c93b;
}
.onLineModel {
position: absolute;
top: 50rpx;
left: 43rpx;
z-index: 999 !important;
background: #434343;
border-radius: 6px;
width: 180rpx;
height: 150rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
.sanjiao {
background: #434343;
position: absolute;
top: -15rpx;
left: 30rpx;
width: 30rpx;
height: 30rpx;
transform: rotate(45deg);
}
.model_text {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
white-space: nowrap;
font-size: 24rpx;
.dian {
margin-right: 8rpx;
}
}
.xian {
display: inline-block;
width: 100%;
height: 1px;
background: #f0f1f2;
}
.opacity0 {
opacity: 0;
}
}
.onlineFlag {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 0;
background: rgba(0, 0, 0, 0.3);
}
</style>
+342
View File
@@ -0,0 +1,342 @@
<template>
<div class="container">
<lay-out titleName="设置">
<div slot="content">
<view class='personal-data'>
<view class='list'>
<view class='item'>
<view>头像</view>
<view class="avatar-box" @click='uploadpic'>
<image :src="customerServerData.avatar"></image>
</view>
</view>
<view class='item'>
<view>昵称</view>
<view class='input'><input v-model="customerServerData.nickname" type='text' placeholder="请输入客服名称"></input></view>
</view>
<view class='item'>
<view>手机号</view>
<view class='input'>
<view class='input'><input v-model="customerServerData.phone" type='number' placeholder="请填写联系方式"></input></view>
</view>
</view>
<view class="item">
<view>密码</view>
<view class='input'>
<view class='input'><input v-model="customerServerData.password" type='number' placeholder="请输入密码"></input></view>
</view>
</view>
</view>
</view>
</div>
</lay-out>
</div>
</template>
<script>
import { navigateTo, chooseImage, pathToBase64 , Toast, Modal, Loading, hideLoading} from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
import { mapState } from 'vuex'
const app = getApp()
export default {
data() {
return {
dominName: '',
customerServerData: {}
}
},
computed:{
...mapState(['token'])
},
onLoad(opt) {
this.initData()
this.dominName = this.$cache.get('dominName');
},
onNavigationBarButtonTap() {
this.handleEdit();
},
methods: {
edit(){
uni.navigateTo({
url:'/pages/view/example'
})
},
// 修改域名
editDominName() {
navigateTo(3, '/pages/view/dominName/index');
},
// 获取当前登录客服信息
initData() {
http(api.userUserInfo).then(res => {
this.customerServerData = res;
console.log(this.customerServerData);
});
},
// 发送图片
uploadpic() {
let that = this;
chooseImage(1).then(res => {
//#ifndef H5
uni.compressImage({
src: res.tempFilePaths[0],
width: 'auto',
height: 'auto',
success(res) {
console.log('ok');
that.upload(res.tempFilePath);
},
fail(e) {
console.log(e,'error');
that.upload(res.tempFilePaths[0]);
}
});
//#endif
//#ifdef H5
that.upload(res.tempFilePaths[0]);
//#endif
});
},
upload(tempFilePath) {
uni.uploadFile({
url: app.globalData.http + api.kefuUploadAvatar.url,
filePath: tempFilePath,
name: 'file',
formData: {
// file: res.tempFilePaths[0],
filename: 'file'
},
header: {
// #ifdef MP || APP-PLUS
'Content-Type': 'multipart/form-data',
// #endif
'Authori-zation': 'Bearer ' + this.token
},
success: uploadFileRes => {
console.log(uploadFileRes)
let unloadResponse = JSON.parse(uploadFileRes.data);
if (unloadResponse.status == 200) {
this.customerServerData.avatar = unloadResponse.data.url;
}
},
fail: res => {
Toast('上传图片失败');
},
complete: res => {
console.log(res);
}
});
},
// 确定修改
handleEdit() {
let postData = this.customerServerData;
if(!postData.password) {
postData.password = '******'
}
http(api.putKefuUserUserInfo, postData).then(res => {
Toast('修改成功');
this.initData();
})
},
// 退出登录
loginOut() {
http(api.userLogout).then(res => {
Modal('温馨提示', '您确定要退出登录吗?').then(() => {
navigateTo(3, '/pages/view/login/index');
});
});
}
}
}
</script>
<style scoped lang="less">
.personal-data {
background-color: #fff;
// padding-bottom: 60rpx;
}
.personal-data .title {
margin-bottom: 30rpx;
font-size: 32rpx;
color: #282828;
}
.personal-data .wrapList .item {
width: 690rpx;
height: 160rpx;
background-color: #f8f8f8;
border-radius: 20rpx;
margin-bottom: 22rpx;
padding: 0 30rpx;
position: relative;
border: 2rpx solid #f8f8f8;
box-sizing: border-box;
}
.personal-data .wrapList .item.on {
border-color: #e93323;
border-radius: 20rpx;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAArIAAACgCAYAAADw+I85AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6M0QzNkY3NzlCNzJCMTFFOTgyNEU4QzhGQTRFRUY2REQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6M0QzNkY3N0FCNzJCMTFFOTgyNEU4QzhGQTRFRUY2REQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozRDM2Rjc3N0I3MkIxMUU5ODI0RThDOEZBNEVFRjZERCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDozRDM2Rjc3OEI3MkIxMUU5ODI0RThDOEZBNEVFRjZERCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pn3rJMAAAArUSURBVHja7N3NXuLIGsDhqigK2Ou+grmEuf/t2fT+bOYKZn9aW5Q6qaQSIoKfoCQ8z29QRBSBzX+q31RiSikAAMDYVF4CAACELAAACFkAABCyAAAIWQAAELIAACBkAQAQsgAAIGQBAEDIAgCAkAUAQMgCAICQBQAAIQsAgJAFAAAhCwAAQhYAACELAABCFgAAhCwAAAhZAACELAAACFkAABCyAAAIWQAAELIAACBkAQAQsgAAIGQBAEDIAgCAkAUAQMgCAICQBQAAIQsAgJAFAAAhCwAAQhYAACELAABCFgAAhCwAAAhZAACELAAACFkAABCyAAAIWQAAELIAACBkAQAQsgAAIGQBAEDIAgCAkAUAQMgCAICQBQAAIQsAgJAFAAAhCwAAQhYAACELAABCFgAAhCwAAAhZAACELAAACFkAABCyAAAIWQAAELIAACBkAQBAyAIAIGQBAEDIAgCAkAUAQMgCAMAJuPQSAABMy79///XaXfJi5qy0YFUuqVzW9eWhvqzK9b1+/vpHyAIAcMjCqxs1tldj/zHl/6oU4rz+ctY2a3tzjO2n0F6tUqobMYZ5fX1V337XBm0MMbX3SuXnvv1peqcBAKYlXl+VSI2lZJuIzSuwi7pUY3/HFPsijYMPcVOps9hG7W19fRVT+50YT6TXvdUAABML2at5V6rdTdfNSmzXquX2FOKTr7trsVvBjeVOISzLyuyfNnNTOIWWFbIAAFNzfd2umjYrsmlWR+i8KuusXbhurudZgTZpU6w/p82Ka0oldJvb47z+cp3HDU5kQVbIAgBMTVwsmzitr1V1ni5C07Pd5EAXtCVlm3BNTfS27dvGbAiDcYPUr9TWvys91jetT2BEVsgCAEwuZOeLJkDr/+Z5sbXdb7UdCIixb9M2WDdjss2n4X274YN2LraJ3fzjeUTh9yk8TyELADC1kM0rsjHVTRpnTYam2I8LNBOuaRO0TbaWbQhidyRYKveLmz0P+vu223ZV8ZWtuYQsAADvD9nlTTMb23/dxelg9TUM4nSzRLvZsSANf274u9uvZnXm/hGyAAAcVHWzzKusl5uDtvq9YtvvpzZJmwGC+GS1tR83iHuGYMuPXtbfF7IAABxWXP7IyVkNT4awGQ/Y7FswHBkIW9e7W1Kfv0/GDKpTeJ5CFgBgapbLPAJQxX5X2DIuEPsdYtsSTak/nKv5Xir7GQxWZNvvlZGC/pReUcgCAHB41c2PnbfHrc+v3bbv61MhZAEAJibmkE1pXRdo9SRDuxXVuJWp3XBsGYDdfL9frx38jub767LVgZAFAOCAIdvsWpBjs5tlHZx4tvmQNhsVdH1bAjYO9pTtrlX9cEJvfQrPU8gCAExMdXOTPz3knQvCk/1iU4iDhO3HCuKT8yK0v6P/mfL9wTFf9W0PpzBvIGQBACYmLm7yOMCqDtB5f6hXak94UFo0lPMklO22ykFfg71mNyu3/ZkUNltz1b+7vYOQBQDgkCG7vMmxmWdkVyGfiWvH3rD9yWeb22O/KVdfuqVy29HZOBwuWKVmbEHIAgBw6JBdLMqKaryLMV3GwRFcqRykVXWt2g0V9KfyimV7rsEEbTkILLbDCXftqIGDvQAAOLTFsjtxwbrOzds6PJcpPT8pQnctlV6N/XlsBwd9lZXcsp/sbZXiuszJClkAAA4rzuclUpsl11UdoXcxxXm709Zg7rUp1fJ13KzKDnbfGhwQFu/qr1fdoGwUsgAAHD5kF32JlhD9E5ots+KiCv0JvAZzr3GzPUGJ235lNo8TpHjbBnF373QSz1PIAgBMLWSvrtoQTf3ga5YP0nqsP89jPgCs7dz2Q4xhu03T5mfuYnNyhTjYzSAE228BALDXv3//9aGf+/mf/5ai3Zy0q4wOrGOIv1NoznEwq0P3sv66yl+XLs0ztfV9wkOO2NieVKFP29SeKqyP2I/+fUIWAIDdZrP+6nDhdDMa0JyZ60+57LvPM9+0CJsfttq6NMetCVkAgIn57pXST0Zr7tOLEqzd552ELAAA3x2u3aV6zw8LWQAAvlKO1Vm5XHzmFwlZAABGE69CFgDgDb5z1vTnr3+m8BLmcL06VnMKWQCAwzRVt9rYHVWf5c2r8g4Bef/WVWi3tZq6WF6L6/DOmVchCwDwdcGWY+0q7N+ZKpa4vSj3y2F7H9ptr9IZvh5CFgDgm+UVx8UHgm0Ye7ehXaUVsEIWAOBLLEq0fTb+lqFdnb0d8WtxXS7fcq4EIQsA8HY5Pmc7bs9jAt0MbJ6HXZe460YLuhna7eDrVjF/j+x1yM9lHo48AytkAQAOY7EnYu9Cu7KadsRtd7DXqtzvqgTgdhTm3z2Gldmq/K0n0ZBCFgDgdd02UkM5UPNK6uMbf0eO2nyQV161XYanq5lX5fZTnpn91jGCfVUNAMB+OdwWOyL2f++I2KHH8rPrrds/cvDYV/XiTWhXkuOp/WEAAOy3axXy944QfY9uNXc7mK9P7Lnnlegf4UT/FV/IAgC8bHukII8HPB7g9z6W3/XSY32nvEK8DKe5SixkAQBecbkVcmlHfH7G9okRYvj+1c/chz9OLKqFLADAO23vUrAKhz0jV7dt10uP+dXhniP2YgxvjpAFANhvO+gejvAYD6885lfJK7D5oK44ljfH9lsAAPttL/o9HuExHl95zK+QdyS4HtubI2QBAPbbXp1cH+Ex1q885rEd4pS7J/F/GQAAvD1sx260EStkAQBelr4gZKtXHvNYlmOOWCELAPCy7X/2P8aBWBevPOYx5JXY2djfHCELALDf9oFYxzi+6PKVxzxGxF5N4c0RsgAA++3a4/WQ4wUxPF8ZfTji85lPJWKFLADAy3JUbp9565DbVF2H52cOWx3puczCCLfYErIAAB93vyM+DzEre7EjLO+P9Bzy+MJyam+MkAUAeNmf8HwngeUnO6raEZapPNYxem85xTdGyAIAvCwH5u2Ohsqnc/3IyuxF+dntDrsNh996K5aIjVN8Y4QsAMDr8tzq/Y6O+hHaA6jeEoqx3PfHjga7D8eZjZ2H42wZdhKcohYA4G1uw+5dBvKc61UJ0XxZh81esFW5zML+HQ9W4fmK7yHMwoR2KBCyAACf8zvs3oc1ltveG473R4rYqvydkyZkAQDeJ4fnQwnFj86ednO3x9pq6zN/m5AFAJiwVYnZbqzgrdGYAzavwu7aCeFQrs6l8YQsAMDH5BC9K5fcVHkmNR9YVQ3CNt8nz8s+DuL3mPJjz8/lDRCyAACf9/AFkfoWZzFSIGQBAF7x89c/Y/pzZ+fWdvaRBQCYhvm5PWEhCwAwftfn2HVCFgBg3GIJ2bMjZAEAxu06nNEBXkIWAGAaujOKnSUhCwAwXme7GitkAQDG66xXY2tJyAIAjNMsnPFqbG0tZAEAxun6zJ+/kAUAGKF8Bq9z77hHIQsAMD5XXoLwIGQBAMYlz8XOzvw1WAcrsgAAo2M1NoRV/iBkAQDGZeYlCPdCFgBgXHK7XYjYZrRAyAIAjMi5r8am+nI3rHoAAITsGNyWmBWyAAAjkncrOOexgjxSsBreIGQBAMbh8oyfew7Y2+0bhSwAgJA9ZQ+7Ivbcyx4AQMietvt9EStkAQDGIc/HntO/pKcSsCtlDwAwbufUbHkV9i4MdifwogAAjNfUdyvIJzhYhcHJDtQ9AMA0TGmsIJVYzZfH0B7M9fiRX/R/AQYA1i4UF+HkevkAAAAASUVORK5CYII=");
background-size: 100% 100%;
background-color: #fff9f9;
background-repeat: no-repeat;
}
.personal-data .wrapList .item .picTxt {
width: 445rpx;
}
.personal-data .wrapList .item .picTxt .pictrue {
width: 96rpx;
height: 96rpx;
position: relative;
}
.personal-data .wrapList .item .picTxt .pictrue image {
width: 100%;
height: 100%;
border-radius: 50%;
}
.personal-data .wrapList .item .picTxt .pictrue .alter {
width: 30rpx;
height: 30rpx;
border-radius: 50%;
position: absolute;
bottom: 0;
right: 0;
}
.personal-data .wrapList .item .picTxt .text {
width: 325rpx;
}
.personal-data .wrapList .item .picTxt .text .name {
width: 100%;
font-size: 30rpx;
color: #282828;
}
.personal-data .wrapList .item .picTxt .text .phone {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.personal-data .wrapList .item .bnt {
font-size: 24rpx;
background-color: #fff;
border-radius: 27rpx;
width: 140rpx;
height: 54rpx;
border: 2rpx solid #e93323;
}
.personal-data .wrapList .item .currentBnt {
position: absolute;
right: 0;
top: 0;
font-size: 26rpx;
background-color: rgba(233, 51, 35, 0.1);
width: 140rpx;
height: 48rpx;
border-radius: 0 20rpx 0 20rpx;
}
.personal-data .list {
// margin-top: 15rpx;
background-color: #fff;
}
.personal-data .list .item {
padding: 30rpx 30rpx 30rpx 0;
border-bottom: 1rpx solid #f2f2f2;
margin-left: 30rpx;
font-size: 32rpx;
color: #282828;
display: flex;
justify-content: space-between;
align-items: center;
}
.personal-data .list .item .phone {
width: 160rpx;
height: 56rpx;
font-size: 24rpx;
color: #fff;
line-height: 56rpx;
border-radius: 32rpx
}
.personal-data .list .item .pictrue {
width: 88rpx;
height: 88rpx;
}
.personal-data .list .item .pictrue image {
width: 100%;
height: 100%;
border-radius: 50%;
}
.personal-data .list .item .input {
width: 415rpx;
text-align: right;
color: #868686;
}
.personal-data .list .item .inputs {
// width: 415rpx;
text-align: right;
color: #868686;
display: flex;
justify-content: right;
align-items: center;
}
.personal-data .list .item .inputs .iconfont {
font-size: 35rpx;
margin-left: 30rpx;
width: 28rpx;
height: 28rpx;
}
.personal-data .modifyBnt {
font-size: 32rpx;
color: #fff;
width: 690rpx;
height: 90rpx;
text-align: center;
line-height: 90rpx;
margin: 76rpx auto 0 auto;
background: #3875EA;
border-radius: 45rpx;
}
.personal-data .logOut {
font-size: 32rpx;
text-align: center;
width: 690rpx;
height: 90rpx;
line-height: 90rpx;
border-radius: 45rpx;
margin: 30rpx auto 0 auto;
border: 1rpx solid #3875EA;
color: #3875EA;
}
.avatar-box {
width: 96rpx;
height: 96rpx;
image {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.
Binary file not shown.
+83
View File
@@ -0,0 +1,83 @@
@font-face {
font-family: 'pingfang';
src: url('~@/static/font/pingfang.ttf');
}
html,
body {
margin: 0;
padding: 0;
font-family: 'pingfang';
width: 100%;
height: 100%;
}
ul,
li {
list-style: none;
}
a {
text-decoration: none;
}
img {
display: block;
}
.cutLine {
height: 10px;
width: 100%;
background: #ededed;
}
.primary_button {
background: rgba(215, 55, 72, 1) !important;
opacity: 1;
color: #fff;
}
.title_identification {
width: 3px;
height: 17px;
background: rgba(215, 55, 72, 1);
opacity: 1;
border-radius: 3px;
display: inline-block;
}
// 父元素拥有相对定位属性时,该样式会将元素置于父元素的底部
.tip_bottom {
position: absolute;
bottom: 5px;
font-size: 11px;
font-weight: 400;
color: rgba(255, 255, 255, 0.6);
opacity: 1;
width: 100%;
text-align: center;
z-index: 100;
}
.parmary_color {
color: #d73748;
font-weight: 600;
}
/* #ifdef APP-PLUS */
.header {
padding-top: 64rpx !important;
}
/* #endif */
.container {
width: 100%;
height: 100%;
background: #f6f6f6;
}
.fixed_model {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
}
+16
View File
@@ -0,0 +1,16 @@
/* #ifdef H5 */
uni-page-head {
display: none;
}
// 改变uni-app高度,该高度可被继承
uni-page-body,
uni-page-wrapper,
uni-page,
#app {
width: 100%;
height: 100% !important;
}
/* #endif */
/* #ifdef MP-WEIXIN */
/* #endif */
+211
View File
@@ -0,0 +1,211 @@
@font-face {
font-family: 'iconfont'; /* Project id 2661080 */
src: url('https://at.alicdn.com/t/font_2661080_l2q37r5cej.woff2?t=1631159043286') format('woff2'),
url('//at.alicdn.com/t/font_2661080_l2q37r5cej.woff?t=1631159043286') format('woff'),
url('//at.alicdn.com/t/font_2661080_l2q37r5cej.ttf?t=1631159043286') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-kehuxinxi:before {
content: "\e6e3";
}
.icon-rili-pc:before {
content: "\e6e2";
}
.icon-Group-pc:before {
content: "\e6e0";
}
.icon-guanbi-pc:before {
content: "\e6dd";
}
.icon-suoxiao-pc:before {
content: "\e6de";
}
.icon-sanjiaobiao-pc:before {
content: "\e6df";
}
.icon-zhankai-pc:before {
content: "\e6e1";
}
.icon-huashusousuo-pc:before {
content: "\e6d8";
}
.icon-sousuo-pc:before {
content: "\e6dc";
}
.icon-shujutongji-pc:before {
content: "\e6d7";
}
.icon-zaixianzixun-pc:before {
content: "\e6d9";
}
.icon-qushitu-pc:before {
content: "\e6da";
}
.icon-xiaoxi-pc:before {
content: "\e6db";
}
.icon-houtui-pc:before {
content: "\e6d5";
}
.icon-qianjin-pc:before {
content: "\e6d6";
}
.icon-gengduoshuzhuang-pc:before {
content: "\e6d2";
}
.icon-gengduo-pc:before {
content: "\e6d3";
}
.icon-duihao-pc:before {
content: "\e6d1";
}
.icon-tijiaochenggong-pc:before {
content: "\e6d4";
}
.icon-zhanghaomima-pc:before {
content: "\e6cf";
}
.icon-erweima:before {
content: "\e6d0";
}
.icon-bianji-pc:before {
content: "\e6cd";
}
.icon-shanchu-pc:before {
content: "\e6ce";
}
.icon-liaotian-pc:before {
content: "\e6cc";
}
.icon-zhuanjie-pc:before {
content: "\e6c9";
}
.icon-tupian-pc:before {
content: "\e6ca";
}
.icon-biaoqing-pc:before {
content: "\e6cb";
}
.icon-rilitubiao:before {
content: "\e6c8";
}
.icon-xuanzhong:before {
content: "\e6c7";
}
.icon-cha:before {
content: "\e6c6";
}
.icon-xiala:before {
content: "\e6c5";
}
.icon-jinru:before {
content: "\e6c3";
}
.icon-fanhui:before {
content: "\e6c4";
}
.icon-tianjia:before {
content: "\e6c1";
}
.icon-tianjiahuashu:before {
content: "\e6c2";
}
.icon-sousuo:before {
content: "\e6bf";
}
.icon-tupian:before {
content: "\e6be";
}
.icon-zhanghao:before {
content: "\e6bc";
}
.icon-mima:before {
content: "\e6bd";
}
.icon-huashu:before {
content: "\e6b8";
}
.icon-gengduozhankai:before {
content: "\e6b9";
}
.icon-biaoqing:before {
content: "\e6ba";
}
.icon-fasong:before {
content: "\e6bb";
}
.icon-biaoqian:before {
content: "\e6b6";
}
.icon-tuichu:before {
content: "\e6b7";
}
.icon-bianji:before {
content: "\e6b4";
}
.icon-cha-shise:before {
content: "\e6b5";
}
.icon-bianji-xianxing:before {
content: "\e6b2";
}
.icon-shanchu:before {
content: "\e6b3";
}
+4
View File
@@ -0,0 +1,4 @@
@import './global.less';
@import './h5.less';
@import './weixin.less';
@import './variable.less';
+7
View File
@@ -0,0 +1,7 @@
@primaryColor: #3875EA;
@primaryBgColor: linear-gradient(90deg, #3875EA 0%, #1890FC 100%);
.overText {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
+3
View File
@@ -0,0 +1,3 @@
/* #ifdef MP-WEIXIN */
/* #endif */
+437
View File
@@ -0,0 +1,437 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
</head>
<body>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">CRMChat是一款由西安众邦网络科技有限公司(注册地为陕西省西安市沣东新城西咸路501号启航时代广场A座1单元1104室。以下亦简称“我们”或“众邦”)提供服务的聊天社交工具产品,我们非常重视您的隐私和个人信息保护,我们将按照法律法规要求,采取相应安全保护措施,尽力保护您的个人信息安全可控。为说明CRMChat会如何收集、使用和存储你的个人信息及你享有何种权利。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">请您在使用本客户端前,认真阅读并充分理解本《隐私政策》的全部内容,特别是以字体加粗方式显著标识的内容。 在阅读过程中如您对本政策内容有任何疑问、意见或建议,请联系我们。您使用或在我们更新本政策后继续使用我们的产品或服务,即表示您同意本政策(含更新版本)内容,并且同意我们按照本政策收集、使用、存储和共享您的相关信息。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">需要特别说明的是,本政策不适用于第三方依托本客户端向您提供的服务,也不适用于本客户端中已另行独立设置隐私政策的产品或服务。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">我们将通过本指引向你阐述相关事宜,其中要点如下:</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>1.我们收集的信息</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">在你使用CRMChat服务的过程中,CRMChat会按照如下方式收集你在使用服务时主动提供或因为使用服务而产生的信息,用以向你提供服务、优化我们的服务以及保障你的帐号安全:</span>
</p>
<p>
<br/>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">1.1 当你使用CRMChat服务时,为保障你正常使用我们的服务,维护我们服务的正常运行,改进及优化我们的服务体验以及保障你的帐号安全,我们会收集你的设备型号、操作系统、唯一设备标识符(指由设备制造商编入到设备中的一串字符,可用于以独有方式标识相应设备)、登录IP地址、软件版本号、接入网络的方式、类型和状态、网络质量数据、)、操作日志、服务日志信息(如你在CRMChat进行对话、快捷回复、服务故障信息)等日志信息,这类信息是为提供服务必须收集的基础信息。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">1.2 <strong>当你使用CRMChat服务时,为保障你能及时收到相关消息提醒,应用集成了com.xiaomi.push(小米;小米推送), com.getui(个推;个数应用统计;个像), com.g.gysdk(个推), com.xiaomi.mipush(小米;小米推送), com.igexin.sdk(个推;个推推送;)等SDK推送服务,你发送的消息将会通过sdk推送服务通知提醒。</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">1.3 当你使用CRMChat聊天功能时,为了保证您能够正常发送本地图片、与拍照像相关的功能,我们会在你授权后获取你的相机权限。你发送的文字、照片等信息会存储在我们的服务器中,因为存储是实现这一功能所必需的。我们会以加密的方式存储,你也可以随时删除这些信息。除非经你自主选择或遵从相关法律法规要求,我们不会对外提供上述信息,或者将其用于该功能以外的其他用途。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">1.4 当你使用CRMChat客户功能时,您添加的用户信息、备注、标签、分组、头像、昵称等信息将被上传并存储在我们的服务器中,以便你在其他设备上登录CRMChat时使用该等信息,这是实现功能所必需的。我们会对你的信息以加密的方式存储,你也可以随时删除自己添加的客户信息。除非经你自主选择或遵从相关法律法规要求,我们不会对外提供上述信息,或者将其用于该功能以外的其他用途。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">1.5 当你使用CRMChat与其他软件或硬件互通功能时,其他软件或硬件的提供方在经你同意后可以获取你主动公开或传输的相关信息,你在选择前应充分了解其他软件或硬件的产品功能及信息保护策略。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">1.6 当你使用CRMChat的自动回复功能时,来储存各类常用回复信息时,该等信息将被上传并存储在我们的服务器中,以便你在其他设备上登录CRMChat时使用该等信息,这是实现功能所必需的。我们会对你的信息以加密的方式存储,你也可以随时删除自己所添加的信息。除非经你自主选择或遵从相关法律法规要求,我们不会对外提供上述信息,或者将其用于该功能以外的其他用途。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">1.7 你可以在你的个人信息栏中填写头像、昵称、密码、域名设置、自动回复等,这些信息将被上传并存储在我们的服务器中,以便你在其他设备上登录微信时使用该等信息。你也可以随时修改、删除该等信息。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>1.8 我们将存储你的聊天记录。你在CRMChat中的聊天记录会储存在我们的服务器中,以便你在其他设备上登录CRMChat时使用该等信息。你也可以随时删除该等信息。</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>另外,根据相关法律法规及国家标准,以下情形中,我们可能会收集、使用你的相关个人信息无需征求你的授权同意:</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>a) 与个人信息控制者履行法律法规规定的义务相关的;</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>b) 与国家安全、国防安全直接相关的;</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>c) 与公共安全、公共卫生、重大公共利益直接相关的;</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>d) 与刑事侦查、起诉、审判和判决执行等直接相关的;</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>e) 出于维护个人信息主体或其他个人的生命、财产等重大合法权益但又很难得到本人授权同意的;</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>f) 所涉及的个人信息是个人信息主体自行向社会公众公开的;</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>g) 根据个人信息主体要求签订和履行合同所必需的;</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>h) 从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道;</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>i) 维护所提供产品或服务的安全稳定运行所必需的,如发现、处置产品或服务的故障;</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>j) 个人信息控制者为新闻单位,且其开展合法的新闻报道所必需的;</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>k) 个人信息控制者为学术研究机构,出于公共利益开展统计或学术研究所必要, 且其对外提供学术研究或描述的结果时,对结果中所包含的个人信息进行去标 识化处理的。</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">请你理解,我们向你提供的功能和服务是不断更新和发展的,如果某一功能或服务未在前述说明中且收集了你的信息,我们会通过页面提示、交互流程、网站公告等方式另行向你说明信息收集的内容、范围和目的,以征得你的同意。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>2. 信息的存储</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">2.1 信息存储的地点</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">我们会按照法律法规规定,将境内收集的用户个人信息存储于中国境内。如果你的个人信息存储地点从中国境内转移到境外的,我们将严格依照法律的规定执行。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">2.2 信息存储的期限</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">一般而言,我们仅在为实现目的所必需的时间内保留你的个人信息,例如:</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">手机号码:无论你是通过手机号码注册还是帐号注册,若你需要使用CRMChat服务,我们需要一直保存你的手机号码(包括初始注册手机号码、绑定的手机号码等),以保证你正常使用该服务。当你注销微信帐号后,我们将删除相应的信息;</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">消息信息:当你发送了消息信息,我们需要保存你的消息信息,以保证你正常使用聊天功能,当你删除你的消息信息后,我们将删除相应的信息。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">当我们的产品或服务发生停止运营的情形时,我们将以推送通知、公告等形式通知你,并在合理的期限内删除你的个人信息或进行匿名化处理(所谓“匿名化处理”,是指通过对个人信息的技术处理,使得个人信息主体无法被识别,且处理后的信息不能被复原的过程。个人信息经匿名化处理后所得的信息不属于个人信息。)。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>3. 信息安全</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">我们努力为用户的信息安全提供保障,以防止信息的丢失、不当使用、未经授权访问或披露。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">我们将在合理的安全水平内使用各种安全保护措施以保障信息的安全。例如,我们会使用加密技术(例如,SSL)、匿名化处理等手段来保护你的个人信息。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">我们通过不断提升的技术手段加强安装在你设备端的软件的安全能力,以防止你的个人信息泄露。我们建立专门的管理制度、流程和组织以保障信息的安全。我们严格限制访问信息的人员范围,要求他们遵守保密义务,并进行审计。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">若发生个人信息泄露等安全事件,我们会启动应急预案,阻止安全事件扩大,并以推送通知、公告等形式告知你。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">我们会尽力保护你的个人信息。我们也请你理解,任何安全措施都无法做到无懈可击。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>4. 我们如何使用信息</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">为了确保服务的安全,帮助我们更好地了解我们应用程序的运行情况,我们可能记录相关信息,例如,你使用应用程序的频率、崩溃数据、总体使用情况、性能数据以及应用程序的来源。我们不会将我们存储在分析软件中的信息与你在应用程序中提供的任何个人身份信息相结合。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">如我们使用你的个人信息,超出了与收集时所声称的目的及具有直接或合理关联的范围,我们将在使用你的个人信息前,再次向你告知并征得你的明示同意。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>5. 对外提供</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">我们可能会接收关联公司、业务合作伙伴等第三方来提供的您的相关信息。例如当您通过我们的关联公司、业务合作伙伴的客户端、网站订购我们的产品,以便我们处理您的订单。我们将在确认接收的个人信息来源的合法性后,依相关法律、法规及第三方约定使用您的信息。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">在收集你的个人信息后,我们将在符合相关法律法规的前提下,基于整体分析和使用的目的,通过技术手段对个人信息进行去标识化处理。去标识化处理的信息将无法识别主体。除此之外,在未征得你的明示同意前,我们不会向该等第三方主体提供你的个人信息。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">随着我们业务的持续发展,我们有可能进行合并、收购、资产转让等交易,我们将告知你相关情形,按照法律法规及不低于本指引所要求的标准继续保护或要求新的控制者继续保护你的个人信息。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">另外,根据相关法律法规及国家标准,以下情形中,我们可能会共享、转让、公开披露个人信息无需事先征得个人信息主体的授权同意:</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">a) 与我们履行法律法规规定的义务相关的 ;</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">b) 与国家安全、国防安全直接相关的;</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">c) 与公共安全、公共卫生、重大公共利益直接相关的;</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">d) 与刑事侦查、起诉、审判和判决执行等直接相关的;</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">e) 出于维护个人信息主体或其他个人的生命、财产等重大合法权益但又很难得到本人授权同意的;</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">f) 个人信息主体自行向社会公众公开的个人信息;</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">g) 从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>6. 你的权利</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">在你使用CRMChat期间,为了你可以更加便捷地访问、更正、删除你的个人信息,同时保障你撤回对个人信息使用的同意及注销帐号的权利,我们在产品设计中为你提供了相应的操作设置,你可以参考下面的指引进行操作。此外,我们还设置了投诉举报渠道,你的意见将会得到及时的处理。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>7. 变更</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">我们可能会适时对本指引进行修订。当指引的条款发生变更时,我们会在你登录及版本更新时以推送通知、弹窗或其他适当形式向你展示变更后的指引。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>8. 未成年人保护</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">(一)我们非常重视保护未成年人个人信息。若您是18周岁以下的未成年人,在使用我们的服务前,应提前取得您的父母或法定监护人的书面同意。我们将根据国家相关法律法规保护未成年人的个人信息。</span>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">(二)对于已经过未成年人的父母或法定监护人同意而收集的未成年人个人信息,我们只会在法律允许、父母或法定监护人明确同意、保护未成年人所必要的情况下使用或公开披露相关信息。</span>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">(三)如果我们发现在未事先获得未成年人的父母或法定监护人同意的情况下收集了未成年人的个人信息,会尽快删除相关数据。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>9. 其他</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">本《隐私政策》是众邦统一适用的一般性隐私条款,其中所规定的用户权利及信息安全保障措施均适用于CRMChat用户。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;"><strong>10. 与我们联系</strong></span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">当你有其他的投诉、建议、未成年人个人信息相关问题时,请通过 http://www.xazbkj.com/ 与我们联系。你也可以将你的问题寄到如下地址:</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">陕西省西安市沣东新城西咸路501号启航时代广场A座1单元1104室(收)</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">邮编:710000</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">我们将尽快审核所涉问题,并在验证你的用户身份后的十五天内予以回复。</span>
</p>
<p>
<br/>
</p>
<p>
<span style="font-family: 微软雅黑, &quot;Microsoft YaHei&quot;;">公司名称:西安众邦网络科技有限公司</span>
</p>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+364
View File
@@ -0,0 +1,364 @@
import Vue from 'vue';
import Vuex from 'vuex';
import {
cache
} from 'pages/utils/uniApi.js';
import http from 'pages/api/index';
import api from 'pages/api/api.js';
import Socket from 'pages/api/socket.js';
Vue.use(Vuex)
let kefuInfo = cache.get('kefuInfo')
const store = new Vuex.Store({
state: {
//请求地址
http: cache.get('requestFile') && cache.get('dominName') ?
`${cache.get('requestFile')}://${cache.get('dominName')}` : '',
//登录token
token: cache.get('token') || '',
//聊天事件信息
chatList: {},
//聊天事件人物
chatInfo: {},
//聊天记录
chatLog: cache.get('chatLog-' + kefuInfo.id) || {},
//聊天分页
chatLogPage: cache.get('chatLogPage-' + kefuInfo.id) || {},
//客服信息
kefuInfo: kefuInfo || {},
//在线事件返回信息
chatOnline: {},
//账号记录
accountData: cache.get('accountData') || {},
//网站配置
config: cache.get('config') || {
site_title: '',
},
//用户列表分页
userRecordPage: cache.get('userPage-' + kefuInfo.id) || 1,
//用户列表
userRecord: cache.get('userList-' + kefuInfo.id) || {},
//客户列表
bookList: cache.get('bookList') || {},
//用户信息所有
userAllInfo: cache.get('userAllInfo') || {},
// 用户列表
userLists: [],
//app升级
percent: 0,
download: false,
downloadTask: null,
},
mutations: {
setPercent(state, data) {
state.percent = data;
},
setDownload(state, data) {
state.download = data;
},
setDownloadTask(state, data) {
state.downloadTask = data;
state.download = true;
},
stopDownloadTask(state) {
if (state.downloadTask) {
state.downloadTask.abort();
state.downloadTask = null
state.download = false;
state.percent = 0;
}
},
setHttp(state, data) {
state.http = data.requestFile + '://' + data.dominName
cache.set('dominName', data.dominName);
cache.set('requestFile', data.requestFile);
cache.set('wsFile', data.wsFile);
},
setRefresh(state, data) {
state.refresh = data
},
setChatList(state, data) {
if (state.chatList[data.id] == undefined) {
state.chatList[data.id] = []
}
state.chatList[data.id].push(data.list);
},
delChatList(state, data) {
state.chatList[data.id] && state.chatList[data.id].map((item, index) => {
if (item.guid === data.guid) {
state.chatList[data.id].splice(index, 1)
}
})
if (state.chatList[data.id].length) {
delete state.chatList[data.id]
}
},
clearChat(state, data) {
if (data === undefined) {
state.chatList = {};
} else if (data.id) {
if (state.chatList[data.id] !== undefined) {
state.chatList[data.id] = [];
}
}
},
setChatInfo(state, data) {
state.chatInfo = data
},
setchatLog(state, data) {
if (state.chatLog[data.user_id] == undefined) {
state.chatLog[data.user_id] = {};
}
if (state.chatLog[data.user_id][data.upperId] == undefined) {
state.chatLog[data.user_id][data.upperId] = [];
}
state.chatLog[data.user_id][data.upperId] = data.list
cache.set('chatLog-' + state.kefuInfo.id, state.chatLog);
if (state.chatLogPage[data.user_id] === undefined) {
state.chatLogPage[data.user_id] = 0
}
if (data.list.length) {
data.upperId = data.list[0].id;
state.chatLogPage[data.user_id] = data.upperId
}
cache.set('chatLogPage-' + state.kefuInfo.id, state.chatLogPage);
},
setkefuInfo(state, data) {
if (data.one === true) {
state.kefuInfo.is_backstage = data.is_backstage;
} else {
state.kefuInfo = data;
}
cache.set('kefuInfo', data)
},
setChatOnline(state, data) {
state.chatOnline = data;
},
setToken(state, data) {
state.token = data;
cache.set('token', data);
},
//退出登录
logout(state) {
state.token = '';
state.kefuInfo = {};
cache.remove('userData');
cache.remove('accountData');
cache.remove('token');
},
setConfig(state, data) {
state.config = data;
cache.set('config', data);
},
setUserRecord(state, data) {
if (state.userRecord[data.page] === undefined) {
state.userRecord[data.page] = [];
}
state.userRecord[data.page] = data.list;
state.userRecordPage = data.page
cache.set('userList-' + state.kefuInfo.id, state.userRecord);
if (data.list.length > 0) {
data.page++;
}
cache.set('userPage-' + state.kefuInfo.id, data.page);
},
setBookList(state, data) {
state.bookList = data;
cache.set('bookList', data)
},
setUserInfo(state, data) {
state.userAllInfo[data.userId] = data.userInfo;
cache.set('userAllInfo', state.userAllInfo);
},
setUserList(state, data) {
state.userLists = data;
}
},
actions: {
//获取客服信息
getKeufuInfo({
commit
}) {
return new Promise((resolve, reject) => {
http(api.kefuInfoApi).then(data => {
commit('setkefuInfo', data)
commit('setConfig', {
site_title: data.site_title
})
resolve(data);
}).catch(res => {
reject(res)
});
})
},
//获取聊天用户列表
getUserRecord({
commit
}, data) {
return new Promise((resolve, reject) => {
http(api.userRecord, {
nickname: data.nickname,
...data.pageData,
is_tourist: data.is_tourist
}).then(res => {
commit('setUserRecord', {
page: data.pageData.page,
list: res
});
resolve(res);
}).catch(err => {
console.log(err)
reject(err)
})
})
},
// 获取所有聊天消息id
getUserRecordAll() {
return new Promise((resolve, reject) => {
http(api.userRecordAll).then(res => {
resolve(res);
}).catch(err => {
reject(err);
});
});
},
// 删除聊天列表用户
deleteUserRecord({
commit
}, data) {
return new Promise((resolve, reject) => {
http(api.deleteUserRecord, {
id: data.id
}).then(res => {
resolve(res);
}).catch(err => {
reject(err);
});
});
},
//获取聊天记录
getChatLogList({
commit
}, data) {
return new Promise((resolve, reject) => {
http(api.kefuServiceList, {
user_id: data.user_id,
...data.pageData
}).then(res => {
commit('setchatLog', {
upperId: data.pageData.upperId,
list: res,
user_id: data.user_id
})
resolve(res)
}).catch(err => {
reject(err)
})
})
},
//获取客服列表
getBookList({
commit
}, data) {
return new Promise((resolve, reject) => {
http(api.userList, data).then(res => {
this.bookList = res;
commit('setBookList', res)
resolve(res)
}).catch(res => {
reject(res)
});
})
},
//设置Client id
saveUserClient({
commit
}, data) {
return new Promise((resolve, reject) => {
http(api.userClient, {
client_id: data
}).then(res => {
cache.set('cid', cid);
resolve(res);
}).catch(res => reject(res));
})
},
updataUserCount({
commit
}) {
return new Promise((resolve, reject) => {
http(api.userCount).then(res => {
if (res.count != 0) {
uni.setTabBarBadge({
index: 0,
text: String(res.count) // 设置为数值,右下角则不在显示
});
} else {
uni.removeTabBarBadge({
index: 0
});
}
resolve(res)
}).catch(err => reject(err));
});
},
login({
commit
}, data) {
},
getUserInfo({
commit
}, userId) {
return new Promise((resolve, reject) => {
http(api.userInfo, {
user_id: userId
}).then(res => {
commit('setUserInfo', {
userId: userId,
userInfo: res
});
resolve(res);
}).catch(err => reject(err));
})
}
},
getters: {
chatList: state => state.chatList,
chatInfo: state => state.chatInfo,
chatLog: state => state.chatLog,
token: state => state.token,
http: state => state.http,
userRecord: state => {
let userList = [];
Object.keys(state.userRecord).map(item => {
state.userRecord[item].map(value => {
userList.push(value)
})
})
return {
userList,
page: state.userRecordPage,
}
},
chatLog: (state) => (user_id) => {
let chatLog = [];
if (state.chatLog[user_id] === undefined) {
return [];
}
Object.keys(state.chatLog[user_id]).map(item => {
state.chatLog[user_id][item].map(value => {
chatLog.push(value)
})
})
return chatLog;
},
config: state => state.config,
kefuInfo: state => state.kefuInfo,
bookList: state => state.bookList,
chatLogPage: (state) => (user_id) => state.chatLogPage[user_id],
userInfo: (state) => (user_id) => state.userAllInfo[user_id] || {},
}
});
export default store

Some files were not shown because too many files have changed in this diff Show More