Browse Source

feat(): 新增客诉统计页面

tags/PMS_Frontend_v1.0.6-develop
fengxiang 1 year ago
parent
commit
581d9ee24d
  1. 563
      src/pages/OperationCenter/CustomerServieMgm/ComplainStat/echartsOptions.jsx
  2. 115
      src/pages/OperationCenter/CustomerServieMgm/ComplainStat/index.scss
  3. 211
      src/pages/OperationCenter/CustomerServieMgm/ComplainStat/loadable.jsx
  4. 19
      src/services/OperationCenter/CustomerManage/index.js

563
src/pages/OperationCenter/CustomerServieMgm/ComplainStat/echartsOptions.jsx

@ -0,0 +1,563 @@
const hexToRgba = (hex, alpha = 1) => {
const color = hex.slice(1);
const rgba = [
parseInt("0x" + color.slice(0, 2)),
parseInt("0x" + color.slice(2, 4)),
parseInt("0x" + color.slice(4, 6)),
alpha,
];
return `rgba(${rgba.toString()})`;
};
const colorList = [
"#9baaff",
"#ff9ba7",
"#c8d6f4",
"#9bfff3",
"#ff9bf3",
"#eeb08e",
"#79d3fb",
];
function linearColors1(color) {
return {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.3,
color: color,
},
{
offset: 1,
color: "#fff",
},
],
global: false,
};
}
function linearColors2(color) {
return {
type: "linear",
x: 0,
y: 0,
x2: 1,
y2: 1,
colorStops: [
{
offset: 0,
color: hexToRgba(color, 1),
},
{
offset: 1,
color: hexToRgba(color, 0.2),
},
],
global: false,
};
}
//
export const barChartOption = (data) => {
const { x_axis = [], y_axis = [] } = data;
// console.log(data);
const len = parseInt(x_axis?.length || 0);
const zoomSpan = 100 / ((len < 8 ? 8 : len) / 8);
return {
backgroundColor: "transparent",
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
backgroundColor: "rgba(0, 0, 0, 0.5)",
borderColor: "rgba(75, 253, 238, 0.4)",
textStyle: { color: "#FFFFFF", fontSize: 12 },
},
grid: {
top: 15,
left: "5%",
right: "3%",
bottom: len > 8 ? 25 : 15,
containLabel: true,
},
// legend: {
// top: 0,
// right: "3%",
// itemWidth: 15,
// itemHeight: 10,
// textStyle: { color: "#999999", fontSize: 12 },
// data: getLegend(y_axis),
// },
dataZoom: [
{
type: "inside",
// span: zoomSpan,
minSpan: zoomSpan,
maxSpan: zoomSpan,
zoomLock: true,
},
{
type: "slider",
backgroundColor: "rgba(255,255,255,.3)",
dataBackground: {
areaStyle: "#fff",
},
height: 9,
bottom: 15,
show: len > 8 ? true : false,
zoomLock: true,
moveHandleSize: 10,
handleIcon:
"path://M512 512m-208 0a6.5 6.5 0 1 0 416 0 6.5 6.5 0 1 0-416 0Z M512 192C335.264 192 192 335.264 192 512c0 176.736 143.264 320 320 320s320-143.264 320-320C832 335.264 688.736 192 512 192zM512 800c-159.072 0-288-128.928-288-288 0-159.072 128.928-288 288-288s288 128.928 288 288C800 671.072 671.072 800 512 800z",
handleColor: "#0260FF",
handleSize: "95%",
// handleStyle: {
// shadowBlur: 3,
// shadowOffsetX: 1,
// shadowOffsetY: 1,
// shadowColor: "rgba(0, 0, 0, 0.6)",
// },
textStyle: {
fontSize: 12,
color: "#9A9A9A",
},
showDetail: false,
},
],
xAxis: [
{
// type: "value",
type: "category",
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { fontSize: 12, color: "#FFFFFF", interval: 0 },
data: x_axis || [],
},
],
yAxis: [
{
axisTick: { show: false },
axisLine: {
lineStyle: {
color: "#9A9A9A",
opacity: 0.6,
},
},
splitLine: {
lineStyle: {
opacity: 0.4,
},
},
axisLabel: { fontSize: 12, color: "#FFFFFF" },
type: "value",
// type: "category",
},
],
series: renderBars(y_axis),
};
};
//
function renderBars(arr) {
let newArr = [];
if (arr?.length) {
newArr = arr.map((v, i) => {
return {
type: "bar",
name: v?.name || "数量",
// yAxisIndex: 0,
// barMaxWidth: "auto",
// barCategoryGap:20,
barWidth: 20,
label: {
show: true,
position: "top",
formatter: function (params) {
let value = params.value || params.value != 0 ? params.value : "";
return value;
},
},
itemStyle: {
color: i % 2 ? linearColors2("#fd9a9a") : linearColors2("#4a70f4"),
},
data: v?.value || [],
};
});
}
return newArr;
}
//
export const pieChartOption = (data) => {
const list = [...data];
const labels = list?.length ? list.map((v) => v.name) : [];
const option = {
color: colorList,
backgroundColor: "transparent",
tooltip: {
trigger: "item",
backgroundColor: "rgba(0, 0, 0, 0.5)",
textStyle: { color: "#FFFFFF", fontSize: 14 },
},
grid: {
top: "middle",
eft: "center",
containLabel: false,
},
legend: {
orient: "horizontal",
top: "bottom",
left: "center",
data: labels,
type: "scroll",
// formatter(name) {
// let num = labels.findIndex((v) => v == name);
// return name + " {a|" + list[num]?.value + "}";
// },
// itemWidth: 10,
// itemHeight: 10,
textStyle: {
color: "#FFFFFF",
// width: 120,
// overflow: "breakAll",
// lineHeight: 16,
// rich: {
// a: { color: "#3f7ff7" },
// },
},
// itemGap: 16,
// selectedMode: false
},
series: [
{
// id: "1",
type: "pie",
center: ["50%", "46%"],
radius: ["50%", "70%"],
data: list,
// z: 0,
// itemStyle: {
// color: ({ dataIndex }: any) => linearColors2[dataIndex % 20],
// },
emphasis: { label: { show: true } },
label: {
show: false,
position: "center",
// borderRadius: 10,
// backgroundColor: "#ffffff",
// width: "120",
// overflow: "breakAll",
formatter(param) {
// return "{num|" + param.value + "}" + "\n {name|" + param.name + "}";
return (
"{num|" + param.percent + "%}" + "\n {name|" + param.name + "}"
);
},
rich: {
num: {
fontSize: 16,
fontWeight: 500,
padding: [0, 0, 5],
color: "#3f7ff7",
},
name: {
fontSize: 14,
color: "#FFFFFF",
},
},
},
},
],
};
return option;
};
// 线
export const lineChartOption = (data) => {
const { x_axis = [], y_axis = [] } = data;
// console.log(data);
const len = parseInt(x_axis?.length || 0);
const zoomSpan = 100 / ((len < 8 ? 8 : len) / 8);
return {
backgroundColor: "transparent",
tooltip: {
trigger: "axis",
backgroundColor: "rgba(0, 0, 0, 0.5)",
borderColor: "rgba(75, 253, 238, 0.4)",
textStyle: { color: "#FFFFFF", fontSize: 12 },
},
grid: {
// top: y_axis?.length > 5 ? 60 : 40,
top: 30,
left: "5%",
right: "3%",
bottom: len > 8 ? 25 : 15,
containLabel: true,
},
legend: {
top: 0,
right: "3%",
// itemWidth: 15,
// itemHeight: 10,
textStyle: { color: "#FFFFFF", fontSize: 12 },
data: getLegend(y_axis),
},
dataZoom: [
{
type: "inside",
// span: zoomSpan,
minSpan: zoomSpan,
maxSpan: zoomSpan,
zoomLock: true,
},
{
type: "slider",
backgroundColor: "rgba(255,255,255,.3)",
dataBackground: {
areaStyle: "#fff",
},
height: 9,
bottom: 15,
show: len > 8 ? true : false,
zoomLock: true,
moveHandleSize: 10,
handleIcon:
"path://M512 512m-208 0a6.5 6.5 0 1 0 416 0 6.5 6.5 0 1 0-416 0Z M512 192C335.264 192 192 335.264 192 512c0 176.736 143.264 320 320 320s320-143.264 320-320C832 335.264 688.736 192 512 192zM512 800c-159.072 0-288-128.928-288-288 0-159.072 128.928-288 288-288s288 128.928 288 288C800 671.072 671.072 800 512 800z",
handleColor: "#0260FF",
handleSize: "95%",
// handleStyle: {
// shadowBlur: 3,
// shadowOffsetX: 1,
// shadowOffsetY: 1,
// shadowColor: "rgba(0, 0, 0, 0.6)",
// },
textStyle: {
fontSize: 12,
color: "#9A9A9A",
},
showDetail: false,
},
],
xAxis: [
{
type: "category",
boundaryGap: false,
axisLabel: { fontSize: 12, color: "#FFFFFF" },
axisLine: {
lineStyle: {
color: "#9A9A9A",
opacity: 0.55,
},
},
data: x_axis || [],
},
],
yAxis: [
{
type: "value",
axisLabel: { fontSize: 12, color: "#FFFFFF" },
axisLine: {
lineStyle: {
color: "#9A9A9A",
opacity: 0.6,
},
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
opacity: 0.4,
},
},
},
],
series: renderLines(y_axis),
// series: {
// name: "",
// type: "line",
// smooth: true,
// lineStyle: {
// width: 2,
// color: "#58b8ff",
// },
// data: y_axis || [],
// },
};
};
//
function getLegend(arr) {
let newArr = [];
if (arr?.length) {
arr.forEach((v, i) => {
newArr.push(v?.name || "数量" + i);
});
}
return newArr;
}
// 线
function renderLines(arr) {
let newArr = [];
if (arr?.length) {
newArr = arr.map((v) => {
return {
name: v?.name || "数量",
type: "line",
// stack: "Total",
yAxisIndex: 0,
// smooth: true,
// lineStyle: {
// width: 2,
// color: `#30b4ea`,
// },
// itemStyle: {
// color: `#30b4ea`,
// },
// areaStyle: {
// opacity: 0.8,
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
// {
// offset: 0,
// color: "rgba(59,255,244,0.3)",
// },
// {
// offset: 1,
// color: "rgba(59,255,244,0)",
// },
// ]),
// },
data: v?.value || [],
};
});
}
return newArr;
}
//
export function waterOption(data) {
let num = (data / 100).toFixed(2);
return {
backgroundColor: "transparent",
// title: [
// {
// text: "",
// x: "22%",
// y: "70%",
// textStyle: {
// fontSize: 14,
// fontWeight: "100",
// color: "#5dc3ea",
// lineHeight: 16,
// textAlign: "center",
// },
// },
// ],
series: [
{
type: "liquidFill",
radius: "85%",
// center: ["25%", "45%"],
color: [
{
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "#7A7BF0",
},
{
offset: 0.25,
color: "#7F76F1",
},
{
offset: 0.5,
color: "#9170F7",
},
{
offset: 0.75,
color: "#BF7EFB",
},
{
offset: 1,
color: "#C89EF9",
},
],
globalCoord: false,
},
],
data: [num, num], // data
backgroundStyle: {
borderWidth: 1,
opacity: 0.5,
color: {
type: "radial",
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{
offset: 0,
color: "#E9EAFB",
},
{
offset: 0.4,
color: "#E9EAFB",
},
{
offset: 1,
color: "#CDCDF9",
},
],
global: false,
},
},
label: {
fontSize: 14,
color: "#fff",
formatter(param) {
return "{num|" + data + "%}" + "\n {name|完好率}";
},
rich: {
num: {
fontSize: 20,
padding: [10, 0, 5],
color: "#ffffff",
},
name: {
fontSize: 10,
color: "#ffffff",
},
},
},
outline: {
// show: false,
borderDistance: 0,
itemStyle: {
borderWidth: 3,
borderColor: "#CCD5F6",
},
},
},
{
type: "pie",
radius: ["85%", "100%"],
silent: true,
labelLine: {
show: false,
},
itemStyle: {
color: "#EDEDFD",
},
data: [{ value: 100 }],
},
],
};
}

115
src/pages/OperationCenter/CustomerServieMgm/ComplainStat/index.scss

@ -17,7 +17,7 @@ $color-primary : var(--color-primary);
align-items: center;
padding: 20px;
background: var(--color-user-list-bg);
border-radius: 20px;
border-radius: 10px;
box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.08);
.ant-select-selector,
@ -85,39 +85,91 @@ $color-primary : var(--color-primary);
width: 100%;
margin-top: 20px;
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
background: var(--color-user-list-bg);
border-radius: 20px;
box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.08);
.paid-summary {
display: flex;
justify-content: space-between;
width: 100%;
// height: 100px;
height: 120px;
>div {
width: calc(25% - 15px);
width: calc(20% - 16px);
height: 100%;
background-color: #ffc0cb;
&.sum-item{
background: var(--color-user-list-bg);
border-radius: 5px;
box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.08);
&.sum-item {
display: flex;
flex-direction: column;
.sum-con{
padding: 6px;
.sum-con {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
// justify-content: space-between;
overflow: hidden;
.num-box {
padding-left: 30px;
.anticon {
color: var(--color-primary);
transform: rotate(95deg);
}
>span {
margin-right: 5px;
font-size: 28px;
font-weight: 600;
}
}
.per-box{
transform: scale(0.5);
.per-box {
transform: scale(0.4);
}
}
.sum-txt{
padding: 3px;
.sum-txt {
height: 34px;
line-height: 34px;
font-size: 16px;
text-align: center;
border-radius: 4px;
// background-color: rgba($color: pink, $alpha: 0.4);
}
@mixin setColor($color) {
.num-box>span {
color: $color;
}
.sum-txt {
background-color: rgba($color: $color, $alpha: 0.2);
}
}
&:nth-child(1) {
@include setColor(#459CFC);
}
&:nth-child(2) {
@include setColor(#F3511D);
}
&:nth-child(3) {
@include setColor(#F8BF4D);
}
&:nth-child(4) {
@include setColor(#9CC811);
}
&:nth-child(5) {
@include setColor(#8182E6);
}
}
}
@ -135,10 +187,39 @@ $color-primary : var(--color-primary);
margin-top: 20px;
}
>div {
.chart-box {
width: 100%;
height: calc(50% - 10px);
background-color: yellow;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px;
background: var(--color-user-list-bg);
border-radius: 10px;
box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.08);
.title {
width: 100%;
height: 32px;
display: flex;
align-items: center;
&::before {
content: "";
display: inline-block;
width: 6px;
height: 20px;
margin-right: 8px;
border-radius: 3px;
background-color: var(--color-primary);
}
}
.wraper {
flex: 1;
width: 100%;
// height: calc(100% - 32px);
}
}
&.col-one {

211
src/pages/OperationCenter/CustomerServieMgm/ComplainStat/loadable.jsx

@ -1,42 +1,106 @@
import React, { useState, useRef, useEffect } from "react";
import React, { useState, useRef, useEffect, useCallback } from "react";
import { message, Button, DatePicker, Progress, Modal } from "antd";
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
import { SearchOutlined, PhoneOutlined } from "@ant-design/icons";
import * as echarts from "echarts";
import ReactEcharts from "echarts-for-react";
// import { dictionary, utils } from "@/config/common";
// import moment from 'moment'
import moment from "moment";
// import { useSessionStorageState, useUpdateEffect, useSize, useUpdate } from 'ahooks';
// import ajax from "@/services"
// import { FormInput, FormSelect, OptionPanel, ResultPanel, FormSliderPicker, AreaCascader, ImgResize, ImgZoom, } from "@/components"
import ajax from "@/services";
import {
pieChartOption,
barChartOption,
lineChartOption,
} from "./echartsOptions";
import "./index.scss";
// import errorImg from "@/assets/images/layout/error.png"
// import { useLocation } from "react-router-dom";
function ComplainStat() {
function renderItem() {
//
const [formData, setFormData] = useState({
s_time: moment().subtract(7, "days").format("YYYY-MM-DD"), //
e_time: moment().format("YYYY-MM-DD"), //
});
//
const [resultData, setResultData] = useState({
total: 0, //
wait_num: 0, //
follow_num: 0, //
deal_num: 0, //
deal_rate: 0, //
});
// 1
const [chartData1, setChartData1] = useState([]);
// 2
const [chartData2, setChartData2] = useState([]);
// 线
const [chartData3, setChartData3] = useState({});
//
const [chartData4, setChartData4] = useState({});
useEffect(() => {
getData();
}, []);
//
const getData = () => {
ajax.getComplainStatistics(formData).then(
(res) => {
if (parseInt(res?.status) === 20000) {
let { base_data, pie1_data, pie2_data, line_data, bar_data } =
res?.data || {};
setResultData(base_data || {});
setChartData1(pie1_data || []);
setChartData2(pie2_data || []);
setChartData3(line_data || {});
setChartData4(bar_data || {});
} else {
message.error(res?.message);
}
},
(err) => {
console.log(err);
message.error("服务器异常");
}
);
};
//
const getPercent = (val) => {
const a = parseInt(resultData?.total);
const b = parseInt(val);
if (a && b) {
return ((b * 100) / a).toFixed(2);
} else {
return 0;
}
};
//
function renderItem(text, html, num, color) {
const bool = typeof num !== "undefined";
return (
<div className="sum-item">
<div className="sum-con">
<div className="num-box">
6<sup></sup>
</div>
<div className="per-box">
<Progress
type="circle"
showInfo={false}
strokeColor={"#ffc0cb"}
strokeWidth={8}
percent={75}
/>
</div>
</div>
<div
className="sum-txt"
style={{
// backgroundColor: `rgba(${"#ffc0cb"}, 0.4)`,
}}
className="sum-con"
style={{ justifyContent: bool ? "space-between" : "center" }}
>
投诉总量
<div className="num-box">
<PhoneOutlined />
{html}
</div>
{bool ? (
<div className="per-box">
<Progress
type="circle"
showInfo={false}
strokeColor={color}
strokeWidth={10}
percent={num}
/>
</div>
) : null}
</div>
<div className="sum-txt">{text}</div>
</div>
);
}
@ -48,8 +112,18 @@ function ComplainStat() {
<DatePicker.RangePicker
className="form-con"
// showTime
// value={formData?.updateTimes || null}
// onChange={(e, str) => setFormData({ ...formData, updateTimes: e })}
value={[
formData?.s_time ? moment(formData.s_time, "YYYY-MM-DD") : null,
formData?.e_time ? moment(formData.e_time, "YYYY-MM-DD") : null,
]}
onChange={(e, str) => {
let bool = str?.length === 2;
setFormData({
...formData,
s_time: bool ? str[0] : "",
e_time: bool ? str[1] : "",
});
}}
/>
</div>
<div className="form-btn">
@ -57,8 +131,7 @@ function ComplainStat() {
className="submit"
type="primary"
icon={<SearchOutlined />}
// onClick={handleSearch}
// loading={loading}
onClick={() => getData()}
>
查询
</Button>
@ -66,19 +139,81 @@ function ComplainStat() {
</div>
<div className="paid-result">
<div className="paid-summary">
{renderItem()}
{renderItem()}
{renderItem()}
{renderItem()}
{renderItem(
"投诉总量",
<>
<span>{resultData?.total || 0}</span>
</>
)}
{renderItem(
"待处理数量",
<>
<span>{resultData?.wait_num || 0}</span>
</>,
getPercent(resultData?.wait_num),
"#F3511D"
)}
{renderItem(
"跟进中数量",
<>
<span>{resultData?.follow_num || 0}</span>
</>,
getPercent(resultData?.follow_num),
"#F8BF4D"
)}
{renderItem(
"已处理数量",
<>
<span>{resultData?.deal_num || 0}</span>
</>,
getPercent(resultData?.deal_num),
"#9CC811"
)}
{renderItem(
"按时处理比率",
<>
<span>{resultData?.deal_rate || 0}</span>%
</>,
resultData?.deal_rate || 0,
"#8182E6"
)}
</div>
<div className="paid-graphic">
<div className="col-con col-one">
<div className="chart-box"></div>
<div className="chart-box"></div>
<div className="chart-box">
<div className="title">投诉订单来源比例</div>
<ReactEcharts
className="wraper"
notMerge={true}
option={pieChartOption(chartData1)}
/>
</div>
<div className="chart-box">
<div className="title">用户满意度占比</div>
<ReactEcharts
className="wraper"
notMerge={true}
option={pieChartOption(chartData2)}
/>
</div>
</div>
<div className="col-con col-two">
<div className="chart-box"></div>
<div className="chart-box"></div>
<div className="chart-box">
<div className="title">投诉订单来源趋势</div>
<ReactEcharts
className="wraper"
notMerge={true}
option={lineChartOption(chartData3)}
/>
</div>
<div className="chart-box">
<div className="title">问题类型统计</div>
<ReactEcharts
className="wraper"
notMerge={true}
option={barChartOption(chartData4)}
/>
</div>
</div>
</div>
</div>

19
src/services/OperationCenter/CustomerManage/index.js

@ -73,6 +73,14 @@ const submitData = (params) => {
data: params,
});
};
//客服管理-反馈建议-撤回
const getRejectData = (params) => {
return ajax({
url: "/api/ope/service/withDraw",
type: "get",
data: params,
});
};
//客服管理-客诉工单-列表
const getComplainList = (params) => {
return ajax({
@ -81,11 +89,11 @@ const getComplainList = (params) => {
data: params,
});
};
//客服管理-反馈建议-撤回
const getRejectData = (params) => {
//客服管理-客诉统计-数据
const getComplainStatistics = (params) => {
return ajax({
url: "/api/ope/service/withDraw",
type: "get",
url: "/api/ope/service/complaint_statistics",
type: "post",
data: params,
});
};
@ -100,5 +108,6 @@ export default{
submitData,
passData,
rejectData,
getComplainList
getComplainList,
getComplainStatistics,
}
Loading…
Cancel
Save