Commit 17b8b15c authored by 李腾's avatar 李腾

feat: 完善消息提醒业务逻辑

parent e2eb48fd
......@@ -146,6 +146,13 @@ export default {
icon: 'smile',
component: './AfterSaleManage/index',
},
{
title: '商户管理后台',
path: '/afterSaleManageNew',
name: 'afterSaleManageNew',
icon: 'smile',
component: './AfterSaleManageNew/index',
},
{
title: '商户管理后台',
path: '/auditPending',
......
......@@ -2,10 +2,10 @@ const isProduction = process.env.NODE_ENV === 'production';
const isPre = process.env.PRE_ENV === 'pre';
const envAPi = {
api: 'https://security-xyqb.liangkebang.net', //'https://security-xyqb.liangkebang.net',
kdspOpApi: 'https://sc-merchant-api-xyqb.liangkebang.net',
kdspApi: 'https://sc-merchant-api-xyqb.liangkebang.net',
goodsApi: 'https://sc-merchant-api-xyqb.liangkebang.net',
api: 'https://security-sc.liangkebang.net', //'https://security-xyqb.liangkebang.net',
kdspOpApi: 'https://sc-merchant-api-sc.liangkebang.net',
kdspApi: 'https://sc-merchant-api-sc.liangkebang.net',
goodsApi: 'https://sc-merchant-api-sc.liangkebang.net',
// kdspOpApi: 'https://kdsp-operation-xyqb.liangkebang.net',
// kdspApi: 'https://sc-op-api-xyqb.liangkebang.net',
// goodsApi: 'https://sc-op-api-xyqb.liangkebang.net',
......@@ -15,6 +15,8 @@ const envAPi = {
// qiniuHost: 'https://appsync.lkbang.net',
qiniuHost: 'https://kdspstatic.q-gp.com/',
opapiHost: 'https://opapi-sc.liangkebang.net',
wsApi: 'ws://ws-sc.liangkebang.net',
msgApi: 'https://msgapi-sc.liangkebang.net',
};
const prodApi = {
......@@ -30,6 +32,8 @@ const prodApi = {
// opapiHost: 'https://opapi.q-gp.com',
// querysApi: 'https://sc-settlement-api.q-gp.com',
querysApi: 'https://sc-merchant-api.q-gp.com/admin/merchant/sc-settlement',
wssApi: '',
msgApi: 'https://msgapi.q-gp.com',
};
const preProdApi = {
......
import React from 'react';
import { Form, Input, Select, DatePicker, Button, Space } from 'antd';
import { moment, Moment } from 'moment';
import style from './index.less';
const { Option } = Select;
const { RangePicker } = DatePicker;
const SEARCH_TYPE = {
SELECT: 'select',
RANGE_PICKER: 'range_picker',
INPUT: 'input',
DATE_PICKER: 'date_picker',
};
const FormSearch = props => {
const {
width = '100%',
form,
initialValues = {},
onFinish = () => {},
formConfig,
formOptions,
btnConfig,
} = props;
const FormItemBox = ({ bindKey, label, column, children, afterRender }) => {
const columnValue = column;
// if (afterRender) {
// columnValue = '';
// }
return (
<Form.Item
className={style['custom-form-item']}
column={columnValue}
name={bindKey}
label={label}
>
{children}
</Form.Item>
);
};
// 下拉框类型
const FormItemSelect = config => {
const {
bindKey,
options: configOptions = [],
originOptions = {},
afterRender = null,
afterOptions = {},
column,
} = config;
const attrs = { placeholder: '请选择', ...originOptions };
// 提取公共部分
const BaseSelectElement = () => (
<FormItemBox {...config}>
<Select name={bindKey} className={style['form-item-tag']} {...attrs}>
{configOptions.map(option => (
<Option key={option.value} value={option.value}>
{option.name}
</Option>
))}
</Select>
</FormItemBox>
);
if (afterRender) {
return (
<>
{/* // <div className={style['custom-form-item-group']}> */}
<BaseSelectElement />
<FormItemBox column={column} {...afterOptions}>
{afterRender()}
</FormItemBox>
{/* // </div> */}
</>
);
}
return <BaseSelectElement />;
};
// 选择日期范围类型
const FormItemRangePicker = config => {
const { originOptions = {} } = config;
const attrs = { placeholder: ['开始日期', '结束日期'], ...originOptions };
return (
<FormItemBox {...config}>
<RangePicker className={style['form-item-tag']} {...attrs} />
</FormItemBox>
);
};
// 选择日期
const FormItemDatePicker = config => {
const { originOptions = {} } = config;
const attrs = { placeholder: '请选择日期', ...originOptions };
return (
<FormItemBox {...config}>
<DatePicker className={style['form-item-tag']} {...attrs} />
</FormItemBox>
);
};
// 多级联动
// 输入框类型
const FormItemInput = config => {
const { originOptions = {} } = config;
const attrs = { placeholder: '请输入', ...originOptions };
return (
<FormItemBox {...config}>
<Input className={style['form-item-tag']} {...attrs} />
</FormItemBox>
);
};
// 表单内容元素
const FormItemElement = () =>
formConfig.map(config => {
const { type } = config;
switch (type) {
case SEARCH_TYPE.SELECT:
return <FormItemSelect key={config.bindKey} {...config} />;
case SEARCH_TYPE.RANGE_PICKER:
return <FormItemRangePicker key={config.bindKey} {...config} />;
case SEARCH_TYPE.INPUT:
return <FormItemInput key={config.bindKey} {...config} />;
case SEARCH_TYPE.DATE_PICKER:
return <FormItemDatePicker key={config.bindKey} {...config} />;
default:
return <></>;
}
});
/**
* @module 按钮操作
*/
const FormItemButton = () => (
<Space size={10}>
{btnConfig.map(config => {
const { label, onClick = () => {}, type = 'primary', clickType = 'search' } = config;
const htmlType = clickType === 'reset' ? 'reset' : 'submit';
const callback = () => {
setTimeout(() => {
onClick({ type: clickType, params: form.getFieldValue() });
});
};
return (
<Button key={clickType} type={type} htmlType={htmlType} onClick={callback}>
{label}
</Button>
);
})}
</Space>
);
return (
<div className={style['form-search']}>
<Form
style={{ width }}
layout="inline"
form={form}
initialValues={initialValues}
onFinish={onFinish}
{...formOptions}
>
<FormItemElement></FormItemElement>
<Form.Item>
<FormItemButton></FormItemButton>
</Form.Item>
</Form>
</div>
);
};
export { FormSearch, SEARCH_TYPE };
.form-search {
padding: 15px;
background: #fff;
}
.custom-form-item {
min-width: 320px;
margin-bottom: 20px !important;
&[column='1'] {
width: 100%;
}
&[column='2'] {
width: calc(50% - 16px);
}
&[column='3'] {
width: calc(33.3333% - 32px);
}
&[column='4'] {
width: calc(25% - 48px);
}
&[column='5'] {
width: calc(20% - 54px);
}
}
.form-item-tag {
width: 100%;
}
// .custom-form-item-group {
// display: flex;
// }
import React, { useState, useEffect } from 'react';
import { Tag, message, Badge } from 'antd';
import { connect } from 'dva';
import groupBy from 'lodash/groupBy';
import moment from 'moment';
import { BellOutlined } from '@ant-design/icons';
import NoticeIcon from '../NoticeIcon';
import styles from './index.less';
const GlobalHeaderRight = props => {
const { messageReminderComplexRef, unReadCount = 0 } = props;
const [count, setCount] = useState(0);
const open = () => {
messageReminderComplexRef.current.open();
};
useEffect(() => {
setCount(unReadCount);
}, [unReadCount]);
return (
<span className={styles['badge-box']} onClick={open}>
<Badge count={count} className={styles['badge-box__self']}>
<BellOutlined className={styles['badge-box__icon']} />
</Badge>
</span>
);
};
export default connect(({ messageReminder }) => ({
unReadCount: messageReminder.unReadCount,
}))(GlobalHeaderRight);
import { Tooltip } from 'antd';
import React from 'react';
import { NotificationOutlined } from '@ant-design/icons';
import { connect } from 'dva';
import Avatar from './AvatarDropdown';
import HeaderSearch from '../HeaderSearch';
import styles from './index.less';
import MessageIcon from './MessageIcon';
const GlobalHeaderRight = props => {
const { theme, layout } = props;
const { theme, layout, messageReminderComplexRef } = props;
let className = styles.right;
if (theme === 'dark' && layout === 'topmenu') {
......@@ -15,6 +17,7 @@ const GlobalHeaderRight = props => {
return (
<div className={className}>
<MessageIcon messageReminderComplexRef={messageReminderComplexRef} />
<Avatar />
</div>
);
......
......@@ -132,3 +132,24 @@
}
}
}
// Badge
.badge-box {
display: inline-block;
height: 100%;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: rgba(0, 0, 0, 0.025);
}
&__self {
font-size: 16px !important;
}
&__icon {
padding: 4px;
vertical-align: middle;
}
}
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Modal, Tabs, Pagination } from 'antd';
import styles from './index.less';
const Complex = (props, ref) => {
const [visible, setVisible] = useState(false);
const [dataTotal, setDataTotal] = useState(10);
const [pagination, setPagination] = useState({
pageSize: 20,
pageNo: 1,
});
// 分页操作
const onPageChange = (page, size) => {
const current = pagination.pageSize !== size ? 1 : page;
setPagination({
pageNo: current,
pageSize: size,
});
};
useEffect(() => {}, [pagination]);
const open = () => {
setVisible(true);
};
const close = () => {
setVisible(false);
};
useImperativeHandle(ref, () => ({
open,
}));
const modalProps = {
width: '1000px',
height: '600px',
visible,
title: '消息提醒',
footer: null,
onCancel: close,
};
return (
<Modal {...modalProps}>
<Tabs tabPosition="left">
<Tabs.TabPane
tab={
<span>
1111<span style={{ background: 'red' }}>abc</span>
</span>
}
key="1"
>
<div className={styles['complex-list']}>
<div className={styles['complex-list__item']}>
<div className={styles['complex-list__item--header']}>
<span className={styles.notice}>新的xxx订单,请查看</span>
<span className={styles.orderNumber}> 订单编号:1562742258351251456</span>
<span className={styles.time}>2022-10-13 12:22:03</span>
</div>
<div className={styles['complex-list__item--body']}>
<div className={styles.good}>
<span className={styles.good__name}>商品名称商品名称商品名称商品名称</span>
<span className={styles.good__count}>x10</span>
</div>
<div className={styles.good}>
<span className={styles.good__name}>商品名称商品名称商品名称商品名称</span>
<span className={styles.good__count}>x10</span>
</div>
<div className={styles.good}>
<span className={styles.good__name}>商品名称商品名称商品名称商品名称</span>
<span className={styles.good__count}>x10</span>
</div>
</div>
<div className={styles['complex-list__item--footer']}>
<div className={styles.actions}>
<a>标记为已读</a>
<a>立即查看</a>
</div>
</div>
</div>
<div className={styles['complex-list__item']}>
<div className={styles['complex-list__item--header']}>
<span className={styles.notice}>新的xxx订单,请查看</span>
<span className={styles.orderNumber}> 订单编号:1562742258351251456</span>
<span className={styles.time}>2022-10-13 12:22:03</span>
</div>
<div className={styles['complex-list__item--body']}>
<div className={styles.good}>
<span className={styles.good__name}>商品名称商品名称商品名称商品名称</span>
<span className={styles.good__count}>x10</span>
</div>
<div className={styles.good}>
<span className={styles.good__name}>商品名称商品名称商品名称商品名称</span>
<span className={styles.good__count}>x10</span>
</div>
<div className={styles.good}>
<span className={styles.good__name}>商品名称商品名称商品名称商品名称</span>
<span className={styles.good__count}>x10</span>
</div>
</div>
<div className={styles['complex-list__item--footer']}>
<div className={styles.actions}>
<a>标记为已读</a>
<a>立即查看</a>
</div>
</div>
</div>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab="售后消息" key="2">
<div className={styles['complex-list']}></div>
</Tabs.TabPane>
</Tabs>
<div className={styles.pagination}>
<Pagination
onChange={onPageChange}
total={dataTotal}
showTotal={(total, range) => `第${range[0]}-${range[1]}条 /总共${total}条`}
pageSize={pagination.pageSize}
current={pagination.pageNo}
/>
</div>
</Modal>
);
};
export default forwardRef(Complex);
.complex-list {
height: 500px;
overflow-y: auto;
border: 1px solid #efefef;
& :first-child {
margin: 0;
}
&__item {
margin-top: 15px;
&--header {
display: flex;
width: 100%;
height: 40px;
padding: 0 20px;
line-height: 40px;
background: #f8f8f8;
.orderNumber {
flex: 1;
padding: 0 20px;
}
.notice {
color: green;
}
}
&--body {
padding: 10px 15px;
.good {
display: flex;
padding: 5px 0;
line-height: 1.5;
&__name {
flex: 1;
}
}
}
&--footer {
display: flex;
justify-content: end;
padding: 0 15px;
border-top: 1px solid #efefef;
border-bottom: 1px solid #efefef;
.actions {
display: flex;
margin-left: 10px;
line-height: 40px;
a {
margin-left: 15px;
}
}
}
}
}
.pagination {
text-align: right;
}
import React from 'react';
import styles from './index.less';
const MessageReminderEmpty = props => {
const { text } = props;
return (
<div className={styles.notFound}>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
alt="not found"
/>
<div>{text}</div>
</div>
);
};
export default MessageReminderEmpty;
@import '~antd/es/style/themes/default.less';
.notFound {
padding: 73px 0 88px;
color: @text-color-secondary;
text-align: center;
img {
display: inline-block;
height: 76px;
margin-bottom: 16px;
}
}
import React, { useState, useEffect } from 'react';
import { Badge, notification } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import { connect } from 'dva';
import { apiGetBussinessMsgList } from '@/services/messageReminder';
import Empty from '../Empty';
import styles from './index.less';
import { CHANNEL_ID } from '@/utils/constants';
const Horn = props => {
const { count, toggle, animationClass } = props;
return (
<div className={classNames(styles.horn, styles[animationClass])} onClick={toggle}>
<Badge count={count}>
<div className={styles['horn--num']}>消息提醒</div>
</Badge>
</div>
);
};
const Message = props => {
const { toggle, animationClass, messageData, openComplex, onMark } = props;
const ReminderItem = args => {
const { item } = args;
const { orderNo, createdAt } = JSON.parse(item.sendContent || '{}');
return (
<div className={styles.item}>
<div className={styles.info}>
<span className={styles['order-number']}>{orderNo}</span>
<span className={styles['mark-read']} onClick={() => onMark([item.id])}>
标记为已读
</span>
</div>
<div className={styles.time}>
<span>{createdAt}</span>
</div>
<div className={styles.notice}>
<a>您有新的xxxxx订单,请查看</a>
</div>
</div>
);
};
return (
<div className={classNames(styles['message-reminder'], styles[animationClass])}>
<div className={styles['message-reminder__header']}>
<div className={styles['message-reminder__header--title']}>
消息提醒
<span className={styles.close} onClick={toggle}>
<CloseOutlined />
</span>
</div>
</div>
<div className={styles['message-reminder__body']}>
<div className={styles['message-reminder__body--list']}>
{messageData.length ? (
messageData
.filter((e, i) => i < 10)
.map(item => <ReminderItem key={item.id} item={item} />)
) : (
<Empty text="暂无数据" />
)}
</div>
</div>
<div className={styles['message-reminder__footer']}>
<div className={styles['message-reminder__footer--actions']}>
<div className={styles.more} onClick={openComplex}>
查看更多
</div>
<div
className={styles['mark-all']}
onClick={() => {
onMark(messageData.filter((e, i) => i < 10).map(item => item.id));
}}
>
全部标记为已读
</div>
</div>
</div>
</div>
);
};
const Simple = props => {
const { dispatch, unReadCount, complexRef, unReadData } = props;
const [visible, setVisible] = useState(false);
const [count, setCount] = useState(0);
const [hornClass, setHornClass] = useState('');
const [messageClass, setMessageClass] = useState('');
const [messageData, setMessageData] = useState([]);
const userInfo = JSON.parse(localStorage.getItem('user') || '{}');
const toggle = () => {
setVisible(!visible);
};
// 初始化获取数据
const getMsgList = async () => {
const params = {
pageNo: 1,
pageSize: 1000,
};
const data = {
channelId: CHANNEL_ID,
bussinessId: userInfo.supplierCode,
readStatus: 0,
};
const res = await apiGetBussinessMsgList(data, params);
if (res.code !== '0000') {
notification.error(res.msg);
return;
}
console.log(res);
const { content } = res.data;
setVisible(!!content.length);
dispatch({
type: 'messageReminder/setUnReadData',
payload: content,
options: {
unReadCount: content.length,
},
});
};
// 打开消息提醒弹框
const openComplex = () => {
complexRef.current.open();
toggle();
};
// 标记已读信息
const onMark = idList => {
const payload = {
channelId: CHANNEL_ID,
bussinessId: userInfo.supplierCode,
idList,
};
dispatch({
type: 'messageReminder/setMarkRead',
payload,
});
};
useEffect(() => {
setCount(unReadCount);
}, [unReadCount]);
useEffect(() => {
setHornClass(visible ? 'hide' : 'show');
setMessageClass(visible ? 'show' : 'hide');
}, [visible]);
useEffect(() => {
getMsgList();
}, []);
useEffect(() => {
setMessageData(unReadData);
}, [unReadData]);
const hornProps = {
animationClass: hornClass,
toggle,
count,
};
const messageProps = {
animationClass: messageClass,
toggle,
messageData,
openComplex,
onMark,
};
// 隐藏消息提醒
return (
<>
<Horn {...hornProps} />
<Message {...messageProps} />
</>
);
};
export default connect(({ messageReminder }) => ({
unReadCount: messageReminder.unReadCount,
unReadData: messageReminder.unReadData,
}))(Simple);
.horn {
position: fixed;
right: 30px;
bottom: 10px;
// width: 112px;
// height: 50px;
color: #fff;
background: #2d8cf0;
transform: translateY(0);
cursor: pointer;
opacity: 1;
transition: 0.2s ease-in;
&.hide {
transform: translateY(100%) scale(0.3);
opacity: 0;
transition: 0.2s ease-out;
}
&--num {
width: 112px;
height: 50px;
color: #fff;
font-size: 16px;
line-height: 50px;
text-align: center;
&::before {
display: inline-block;
width: 20px;
height: 20px;
margin-right: 5px;
vertical-align: -3px;
background: url('https://img.lkbang.net/notice.94d42513.png') no-repeat;
background-size: 100% 100%;
content: '';
}
}
}
.message-reminder {
position: fixed;
right: 10px;
bottom: 10px;
z-index: 19;
display: flex;
flex-direction: column;
width: 404px;
height: 412px;
background: #fff;
transform: translate(50%, 110%) scale(0.5);
transition: 0.2s ease-in;
&.hide {
transform: translate(50%, 110%) scale(0.5);
opacity: 0;
transition: 0.2s ease-out;
}
&.show {
transform: translateY(0) scale(1);
opacity: 1;
transition: 0.2s ease-in;
}
&__header {
&--title {
position: relative;
padding-left: 35px;
color: #fff;
font-weight: 700;
font-size: 16px;
line-height: 50px;
background: #1890ff url('https://img.lkbang.net/notice.94d42513.png') no-repeat 10px center;
background-size: 20px 20px;
.close {
position: absolute;
right: 20px;
font-weight: 400;
cursor: pointer;
}
}
}
&__body {
flex: 1;
padding: 15px;
overflow: auto;
background: #f8f8f9;
&--list {
& .item:first-child {
margin-top: 0;
}
.item {
margin-top: 15px;
padding: 15px 0;
background: #fff;
border: 1px solid #efefef;
&:hover {
// border-color: #ccc;
// transition: 0.2s linear;
}
}
.info {
display: flex;
padding: 0 15px;
.order-number {
flex: 1;
padding-left: 20px;
background: url('https://img.lkbang.net/order_no.f4e0a8b3.png') no-repeat scroll 0 center;
}
.mark-read {
color: #178fff;
cursor: pointer;
}
}
.time {
display: flex;
margin-top: 12px;
padding: 0 15px;
span {
flex: 1;
padding-left: 20px;
background: url('https://img.lkbang.net/datetime.1b5f9199.png') no-repeat scroll 0 center;
}
}
.notice {
margin-top: 12px;
padding: 10px 15px 0;
border-top: 1px solid #efefef;
a {
position: relative;
display: block;
color: #ff1515;
&::after {
position: absolute;
top: 50%;
right: 0;
display: block;
width: 0;
height: 0;
margin-top: -3px;
border: 6px solid transparent;
border-left: 8px solid #999;
content: '';
}
}
}
}
}
&__footer {
background-color: #fff;
border: 1px solid #efefef;
&--actions {
display: flex;
& div:first-child {
border: none;
}
> div {
flex: 1;
color: #2d8cf0;
line-height: 40px;
text-align: center;
border-left: 1px solid #efefef;
cursor: pointer;
// &:hover {
// background: #f9f9f9;
// cursor: pointer;
// }
}
}
}
}
import Simple from './Simple';
import Complex from './Complex';
const MessageReminder = {};
MessageReminder.Simple = Simple;
MessageReminder.Complex = Complex;
export default MessageReminder;
......@@ -53,3 +53,19 @@ ol {
}
}
}
// Button
@btn-warning-color: #f50;
.ant-btn {
&-warning,
&-warning:active,
&-warning:hover,
&-warning:visited {
color: #fff !important;
background-color: @btn-warning-color !important;
border-color: @btn-warning-color !important;
&:hover {
opacity: 0.7;
}
}
}
......@@ -5,17 +5,20 @@
*/
/* eslint-disable no-underscore-dangle, no-confusing-arrow */
import ProLayout from '@ant-design/pro-layout';
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { Link } from 'umi';
import { connect } from 'dva';
import { Icon as LegacyIcon } from '@ant-design/compatible';
import { Result, Button, Layout, Menu } from 'antd';
import Authorized from '@/utils/Authorized';
import RightContent from '@/components/GlobalHeader/RightContent';
import MessageReminder from '@/components/MessageReminder';
import { getAuthorityFromRouter } from '@/utils/utils';
import logo from '../assets/logo.png';
import style from './BasicLayout.less';
import Socket from '@/utils/websocket';
const { Sider } = Layout;
const { SubMenu } = Menu;
......@@ -48,12 +51,45 @@ const BasicLayout = props => {
} = props;
const [siderCollapsed, setSiderCollapsed] = useState(false);
const messageReminderComplexRef = useRef();
/**
* constructor
*/
useEffect(() => {
const token = window.localStorage.getItem('token');
const channelId = 100001;
const socket = new Socket({
url: `ws://ws-sc.liangkebang.net/ws?token=${token}&channelId=${channelId}`,
});
socket.connection();
socket.event.on('open', () => {
console.log('连接成功3');
});
socket.event.on('message', msg => {
dispatch({
type: 'messageReminder/setUnReadData',
payload: JSON.parse(msg.data),
});
});
let a = 100000000000;
setInterval(() => {
a++;
socket.sendMessage([
{
id: a,
channelId: 100001,
bussinessId: 'self_40',
type: 0,
sendContent: `{"name":"商品21111", "orderNo":${a},"createdAt":"2022-10-13 12:12:12", "count":11}`,
readStatus: 0,
createdAt: '2022-10-18 14:05:12',
updatedAt: '2022-10-18 17:15:19',
},
]);
}, 5000);
if (dispatch) {
dispatch({
type: 'settings/getSetting',
......@@ -61,6 +97,14 @@ const BasicLayout = props => {
dispatch({
type: 'menu/getMenuData',
});
dispatch({
type: 'messageReminder/getUnReadMsgList',
});
setTimeout(() => {
dispatch({
type: 'messageReminder/getUnReadCount',
});
}, 2000);
}
}, []);
/**
......@@ -156,7 +200,9 @@ const BasicLayout = props => {
);
}}
menuDataRender={() => menuData}
rightContentRender={rightProps => <RightContent {...rightProps} />}
rightContentRender={rightProps => (
<RightContent {...rightProps} messageReminderComplexRef={messageReminderComplexRef} />
)}
pageTitleRender={() => ''}
menuHeaderRender={() => (
<div className={style['custom-title-box']} onClick={toIndex}>
......@@ -175,6 +221,9 @@ const BasicLayout = props => {
<Authorized authority={authorized.authority} noMatch={noMatch}>
{children}
</Authorized>
<MessageReminder.Simple complexRef={messageReminderComplexRef} />
<MessageReminder.Complex ref={messageReminderComplexRef} />
</ProLayout>
);
};
......
import { apiUpdageBusinessMsgStatus } from '@/services/messageReminder';
const MessageReminderModel = {
namespace: 'messageReminder',
state: {
unReadCount: 0, // 未读总数
unReadData: [], // 未读数据
},
effects: {
*setUnReadData({ payload, options }, { put, call }) {
yield setTimeout(() => {
put({
type: 'setUnReadData',
payload,
options,
});
});
},
*setMarkRead({ payload, options }, { put, call }) {
// const res = yield call(apiUpdageBusinessMsgStatus, payload)
// console.log(res)
yield put({
type: 'updateUnReadData',
payload,
options,
});
},
},
reducers: {
setUnReadData(state, { payload, options = {} }) {
const currentData = [...payload, ...state.unReadData];
let unReadCount = state.unReadCount + 1;
if (options.unReadCount || options.unReadCount === 0) {
// eslint-disable-next-line prefer-destructuring
unReadCount = options.unReadCount;
}
return { ...state, unReadData: currentData, unReadCount };
},
updateUnReadData(state, { payload, options = {} }) {
// 删除已读的数据
const currentData = state.unReadData.filter(message => !payload.idList.includes(message.id));
// 重置当前未读数量
const unReadCount = currentData.length; // state.unReadCount - (options.readCount || payload.idList.length || 1)
return { ...state, unReadCount, unReadData: currentData };
},
},
};
export default MessageReminderModel;
......@@ -107,6 +107,16 @@ const Admin = props => {
<p>退款待审核{'<'}24小时</p>
</Link>
</Col>
<Col span={4}>
<Link
to={{
pathname: 'afterSaleManage',
}}
>
<strong>{pendingNum?.interceptRefundPendingAuditNum || 0}</strong>
<p>物流拦截待审核{'<'}48小时</p>
</Link>
</Col>
</>
) : (
''
......
/* eslint-disable no-restricted-syntax */
/* eslint-disable guard-for-in */
import React, { useState, useEffect } from 'react';
import { Modal, Timeline, Button } from 'antd';
import styles from '../index.less';
const LogisticsCom = props => {
const { visible, onCancel, data } = props;
return (
<Modal
destroyOnClose
title="查看记录"
visible={visible}
onCancel={() => onCancel()}
onOk={() => onCancel()}
footer={false}
>
{data?.length ? (
<Timeline>
{data?.map((item, index) => (
// eslint-disable-next-line react/no-array-index-key
<Timeline.Item color={index > 0 ? 'gray' : 'blue'} key={index}>
<p>{item.optDesc}</p>
<p>{item.optTime}</p>
</Timeline.Item>
))}
</Timeline>
) : (
'暂无查看记录'
)}
<Button type="primary" onClick={props.onCancel} className={styles.logBtn}>
关闭
</Button>
</Modal>
);
};
export default LogisticsCom;
import React from 'react';
import { Form } from '@ant-design/compatible';
import '@ant-design/compatible/assets/index.css';
import { Modal, Input, Cascader, notification, InputNumber } from 'antd';
import { shopAudit } from '../services';
import styles from '../index.less';
const FormItem = Form.Item;
const { TextArea } = Input;
const { confirm } = Modal;
const AuditModal = props => {
const {
visible,
onCancel,
form: { getFieldDecorator, getFieldValue, validateFields, resetFields },
formData = {},
} = props;
const handleCancel = isSuccess => {
resetFields();
onCancel(isSuccess);
};
const treeData = [
{
label: '同意',
value: 1,
},
{
label: '不同意',
value: 2,
children: [
{
label: '商品与出售商品不符',
value: 1,
},
{
label: '影响二次销售',
value: 2,
},
{
label: '未提供商品照片,需要上传照片',
value: 4,
},
{
label: '快递正在配送中',
value: 5,
},
{
label: '订单已经取消',
value: 6,
},
{
label: '商品已超过售后期',
value: 7,
},
{
label: '其他',
value: 3,
},
],
},
];
const submitCheckResult = async fieldsValue => {
const { auditResult } = fieldsValue;
const data = await shopAudit({
...fieldsValue,
refuseCode: auditResult?.[1],
auditResult: auditResult?.[0],
serviceNo: formData?.serviceNo,
});
if (data.businessCode === '0000') {
notification.success({ message: '审核成功' });
handleCancel(true);
}
};
const handleOk = () => {
validateFields((error, fieldsValue) => {
if (!error) {
const { auditResult } = fieldsValue;
// 如果为仅退款 并且 审核同意 弹出二次确认提示
if (+formData.serviceType === 1 && auditResult?.[0] === 1) {
confirm({
title: '温馨提示',
content: (
<div>
当前类型为【<span className={styles.redTipBold}>仅退款</span>
】,请核查该订单物流状态,如有在途物流,务必自行拦截后点击确认;
</div>
),
okText: '确认退款',
onOk() {
submitCheckResult(fieldsValue);
},
onCancel() {
handleCancel(true);
},
});
} else {
submitCheckResult(fieldsValue);
}
}
});
};
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 16 },
};
const auditResult = getFieldValue('auditResult');
const isAgree = () => auditResult?.[0] === 1;
const isRefuse = () => auditResult && auditResult[0] !== 1;
return (
<Modal
title="售后操作确认"
visible={visible}
onOk={() => handleOk()}
onCancel={() => handleCancel()}
>
{+formData.serviceType === 1 && (
<div className={styles.redTip}>
温馨提示:当前售后类型为用户未收到产品,申请
<span className={styles.redTipBold}>仅退款</span>
,请务必检查此单物流状态后审核。
</div>
)}
<Form {...layout} name="formData">
<FormItem label="审核结果">
{getFieldDecorator('auditResult', {
rules: [
{
required: true,
message: '请选择审核结果!',
},
],
})(
<Cascader
allowClear
showSearch
style={{ width: '315px' }}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
options={treeData}
placeholder="请选择"
/>,
)}
</FormItem>
{isAgree() && +formData.serviceType === 2 && (
<div>
<FormItem label="退货地址">
{getFieldDecorator('receiveAddress', {
initialValue: formData.address,
rules: [
{
required: true,
message: '请填写退货地址!',
},
{
message: '格式有误,请填写正确的退货地址!',
pattern: /[a-zA-Z0-9_\u4e00-\u9fa5]/g,
},
],
})(<Input placeholder="最多输入50个字符" maxLength="50" allowClear />)}
</FormItem>
<FormItem label="收件人">
{getFieldDecorator('receiverName', {
initialValue: formData.name,
rules: [
{
required: true,
message: '请填写收件人!',
},
{
message: '格式有误,请输入中英文、数字!',
pattern: /^[A-Za-z0-9\u4e00-\u9fa5]+$/,
},
],
})(<Input placeholder="最多输入25个字符" maxLength="25" allowClear />)}
</FormItem>
<FormItem label="手机号码">
{getFieldDecorator('receiverPhone', {
initialValue: formData.phone,
rules: [
// {
// pattern: new RegExp(/^1[3456789]\d{9}$/, 'g'),
// message: '手机号码格式不正确!',
// },
{
pattern: new RegExp(/^[0-9]*$/),
message: '手机号码格式不正确!',
},
{
required: true,
message: '请填写手机号码!',
},
],
})(<Input placeholder="请填写手机号码" maxLength="11" allowClear />)}
</FormItem>
</div>
)}
{isRefuse() && auditResult[1] === 3 && (
<FormItem label="拒绝原因">
{getFieldDecorator('refuseDesc', {
initialValue: formData.refuseDesc,
rules: [
{
required: true,
message: '请填写拒绝原因!',
},
],
})(
<TextArea
placeholder="请填写拒绝原因"
allowClear
autoSize={{ minRows: 3, maxRows: 6 }}
/>,
)}
</FormItem>
)}
{isRefuse() && auditResult[1] !== 3 && (
<FormItem label="补充说明">
{getFieldDecorator('refuseDesc', {
initialValue: formData.refuseDesc,
rules: [
{
message: '请填写补充说明!',
},
],
})(
<TextArea
placeholder="请填写补充说明"
allowClear
autoSize={{ minRows: 3, maxRows: 6 }}
maxLength={120}
showCount
/>,
)}
</FormItem>
)}
</Form>
</Modal>
);
};
export default Form.create()(AuditModal);
import React from 'react';
import { Modal, Table } from 'antd';
export default props => {
const { visible, onCancel, dataSource } = props;
const handleCancel = () => {
onCancel();
};
const columns = [
{
title: '商品名称',
width: 400,
dataIndex: 'skuName',
},
{
title: '商品属性',
dataIndex: 'skuAttr',
},
{
title: '商品件数',
dataIndex: 'count',
},
];
return (
<Modal title="订单详情" visible={visible} onCancel={handleCancel} footer={null} width={800}>
<Table dataSource={dataSource} columns={columns} key="skuName" pagination={false} bordered />
</Modal>
);
};
import React, { useState } from 'react';
import { Modal } from 'antd';
import style from '../index.less';
export default props => {
const { visible, onCancel, data } = props;
const [detailModal, setDetailModal] = useState(false);
const [detailSrc, setDetailSrc] = useState(null);
const handleCancel = () => {
onCancel();
};
const clickImg = src => {
setDetailSrc(src);
setDetailModal(true);
};
return (
<Modal title="售后凭证" visible={visible} onCancel={handleCancel} footer={null} width={600}>
<div className={style.proofsWrap}>
{data.map(item => (
<img
src={item}
key={item}
alt={item}
className={style.proofs}
width={250}
onClick={() => clickImg(item)}
/>
))}
</div>
<Modal
title="凭证详情"
visible={detailModal}
onCancel={() => setDetailModal(false)}
footer={null}
width={700}
>
<p className={style.detailWrap}>
<img src={detailSrc} alt={detailSrc} />
</p>
</Modal>
</Modal>
);
};
import React from 'react';
import { Form } from '@ant-design/compatible';
import '@ant-design/compatible/assets/index.css';
import { Modal, Input, notification } from 'antd';
import { shopCheck } from '../services';
const FormItem = Form.Item;
const { TextArea } = Input;
const RejectModal = props => {
const {
visible,
onCancel,
form: { getFieldDecorator, validateFields, resetFields },
serviceNo = null,
} = props;
const handleCancel = isSuccess => {
resetFields();
onCancel(isSuccess);
};
const handleOk = () => {
validateFields(async (error, fieldsValue) => {
if (!error) {
const data = await shopCheck({
...fieldsValue,
serviceNo,
auditResult: 2,
});
if (data.code === '0000') {
notification.success({ message: '操作成功' });
handleCancel(true);
}
}
});
};
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 16 },
};
return (
<Modal title="驳回" visible={visible} onOk={() => handleOk()} onCancel={() => handleCancel()}>
<Form {...layout} name="formData">
<FormItem label="原因">
{getFieldDecorator('refuseDesc', {
rules: [
{
required: true,
message: '请填写原因!',
},
],
})(
<TextArea placeholder="请填写原因" allowClear autoSize={{ minRows: 3, maxRows: 6 }} />,
)}
</FormItem>
</Form>
</Modal>
);
};
export default Form.create()(RejectModal);
import { Tag, Badge, Statistic, Button } from 'antd';
import React from 'react';
import moment from 'moment';
import { SEARCH_TYPE } from '@/components/FormSearch';
const { Countdown } = Statistic;
// 商户审核中/商户审核拒绝/待填写退货物流信息/待退货入库/退货拒收/退款成功/用户撤销”
// 仅退款(未发货)/仅退款/退货退款,新增【仅退款(未发货)】即取消订单待审核
// 售后类型
const AFTER_TYPE = [
{
value: '1',
name: '商户审核中',
},
{
value: '2',
name: '商户审核拒绝',
},
{
value: '3',
name: '待填写退货物流信息',
},
{
value: '4',
name: '待退货入库',
},
{
value: '5',
name: '退货拒收',
},
{
value: '5',
name: '退款成功',
},
{
value: '6',
name: '用户撤销”',
},
];
// 售后状态
const AFTER_STATUS = [
{
value: '1',
name: '仅退款(未发货)',
},
{
value: '1',
name: '仅退款',
},
{
value: '1',
name: '退货退款',
},
];
export const getFormConfig = (props = {}) => {
const { setTableParams } = props;
return {
formConfig: [
{
type: SEARCH_TYPE.INPUT,
label: '订单ID',
bindKey: 'orderNo',
column: 5,
},
{
type: SEARCH_TYPE.INPUT,
label: '售后单ID',
bindKey: 'serviceNo',
column: 5,
},
{
type: SEARCH_TYPE.SELECT,
label: '售后状态',
column: 5,
bindKey: 'dealStatus',
options: AFTER_STATUS,
},
{
type: SEARCH_TYPE.SELECT,
label: '售后类型',
column: 5,
bindKey: 'type',
options: AFTER_TYPE,
},
{
type: SEARCH_TYPE.INPUT,
label: '收货人姓名',
column: 5,
bindKey: 'receiverName',
},
{
type: SEARCH_TYPE.INPUT,
label: '收货人手机号',
column: 5,
bindKey: 'receiverPhone',
},
{
type: SEARCH_TYPE.DATE_PICKER,
label: '订单开始日期',
column: 5,
bindKey: 'startDate',
},
{
type: SEARCH_TYPE.DATE_PICKER,
label: '订单结束日期',
column: 5,
bindKey: 'endDate',
},
],
btnConfig: [
{
label: '筛选',
clickType: 'submit',
onClick: ({ type, params }) => {
setTableParams(params);
},
},
{
label: '重置',
type: '',
clickType: 'reset',
onClick: () => {
setTableParams({});
},
},
],
};
};
export const appealType = {
1: '已申诉',
0: '未申诉',
};
export const getColumns = props => {
const {
openAudit,
viewAppeal,
viewProofs,
openLogistics,
viewDetail,
viewLog,
canEditable,
} = props;
return [
{
title: '审核倒计时',
dataIndex: 'serviceTime',
key: 'serviceTime',
hideInSearch: true,
width: 150,
render: (val, record) => {
const serviceTime = moment(record.approvalEndTime).valueOf();
return (
<Countdown
format="HH时mm分ss秒"
value={serviceTime}
valueStyle={{ color: 'red', fontSize: '14px' }}
/>
);
},
},
{
title: '售后状态',
dataIndex: 'serviceStatus',
hideInSearch: true,
width: 120,
},
{
title: '售后类型',
dataIndex: 'serviceType',
hideInSearch: true,
width: 120,
render: serviceType => {
if (+serviceType === 1) {
return <span style={{ color: '#ff1616' }}>仅退款</span>;
}
return <span>退货退款</span>;
},
},
{
title: '订单ID',
dataIndex: 'orderNo',
hideInTable: true,
width: 200,
},
{
title: '售后单ID',
dataIndex: 'serviceNo',
width: 300,
render: (serviceNo, r) => (
<div>
{r.timeout ? <Tag color="red">{serviceNo}</Tag> : serviceNo}
{<Badge count={r.reminderFlag ? '' : ''} size="default" />}
</div>
),
},
{
title: '订单ID',
dataIndex: 'orderNo',
hideInSearch: true,
width: 300,
},
{
title: '售后状态',
dataIndex: 'dealStatus',
hideInTable: true,
valueEnum: {
0: '待审核',
10: '三方审核中',
11: '三方审核通过',
12: '三方审核拒绝',
13: '客服审核通过',
14: '商户审核中',
15: '商户审核通过',
16: '商户审核拒绝',
20: '审核拒绝',
21: '申诉中',
30: '待填写退货物流信息',
40: '待退货入库',
50: '退货拒收',
60: '待退款',
61: '退货处理中',
70: '售后成功',
99: '用户取消',
},
width: 100,
},
{
title: '售后类型',
dataIndex: 'type',
hideInTable: true,
width: 120,
valueEnum: {
1: '仅退款',
2: '退货退款',
},
},
{
title: '收货人姓名',
dataIndex: 'receiverName',
width: 200,
},
{
title: '收货人手机号',
dataIndex: 'receiverPhone',
width: 200,
},
{
title: '收货人地址',
dataIndex: 'receiveAddress',
width: 200,
hideInSearch: true,
},
{
title: '订单开始时间',
width: 120,
dataIndex: 'startDate',
key: 'startDate',
valueType: 'date',
hideInTable: true,
},
{
title: '订单结束时间',
width: 120,
dataIndex: 'endDate',
key: 'endDate',
valueType: 'date',
hideInTable: true,
},
{
title: '售后原因',
dataIndex: 'serviceReason',
hideInSearch: true,
width: 200,
},
{
title: '售后发生时间',
dataIndex: 'serviceTime',
hideInSearch: true,
width: 200,
},
{
title: '超时时间',
dataIndex: 'overtime',
hideInSearch: true,
width: 200,
},
{
title: '是否催办',
dataIndex: 'reminderFlag',
hideInSearch: true,
width: 120,
valueEnum: {
true: '',
false: '',
},
},
{
title: '是否同意售后',
dataIndex: 'isAgree',
hideInSearch: true,
width: 120,
},
{
title: '拒绝原因',
dataIndex: 'refuseReason',
hideInSearch: true,
width: 200,
},
{
title: '售后凭证',
dataIndex: 'proofs',
hideInSearch: true,
width: 100,
render: (val, r) => <a onClick={() => viewProofs(r.proofs)}>查看凭证</a>,
},
{
title: '售后申诉',
dataIndex: 'appealFlag',
valueEnum: appealType,
hideInSearch: true,
width: 120,
render: (appealFlag, r) => {
if (appealFlag) {
return <a onClick={() => viewAppeal(r)}>已申诉</a>;
}
return <div>未申诉</div>;
},
},
{
title: '操作',
hideInSearch: true,
dataIndex: 'action',
width: 250,
fixed: 'right',
render: (val, r) => {
const operations = [
<Button key="link1" onClick={() => openAudit(r)} className="mr10 mt10" type="primary">
审核
</Button>,
<Button
disabled={r.serviceType !== 1 || (r.serviceType === 1 && r.intercept)}
onClick={() => openLogistics(r)}
className="mr10 mt10"
type="primary"
>
物流拦截
</Button>,
<Button className="mr10 mt10" key="link" onClick={() => viewDetail(r)} type="primary">
订单详情
</Button>,
<Button className="mr10 mt10" type="primary" onClick={() => viewLog(r)}>
查看记录
</Button>,
];
// 不可编辑直接隐藏可操作按钮
if (!canEditable) {
return [operations[2], operations[3]];
}
// 服务订单删除物流拦截
if (!['vip', 'self'].includes(r.supplierType)) {
return [operations[0], operations[2], operations[3]];
}
return operations;
},
},
];
};
import React, { useRef, useState } from 'react';
import { Tabs, notification, Form, Modal } from 'antd';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import ProTable from '@ant-design/pro-table';
import { AFTER_SALE_ORDER } from '@/../config/permission.config';
import { connect } from 'dva';
import { FormSearch } from '@/components/FormSearch';
import moment from 'moment';
import AuditModal from './components/auditModal';
import DetailTable from './components/detailTable';
import ProofsModal from './components/proofsModal';
import AppealDetail from '@/pages/afterSale/components/detail';
import AfterLog from './components/AfterLog';
import { getColumns, getFormConfig } from './data.js';
import { getDetail } from '@/pages/afterSale/appeal/services';
import { searchList, logisticsIntercept, orderDetail, getOpLog, auditInfoApi } from './services';
import styles from './index.less';
const { TabPane } = Tabs;
const { confirm } = Modal;
const AfterSale = props => {
const { permissions } = props;
const canEditable = permissions[AFTER_SALE_ORDER.EDITABLE];
const actionRef = useRef();
const formRef = useRef();
const [tableParams, setTableParams] = useState({});
const [appealDetailModal, setAppealDetailModal] = useState(false);
const [selectedRow, setSelectedRow] = useState({});
// 申诉
const [proofsData, setProofsData] = useState([]);
const [proofsVisible, setProofsVisible] = useState(false);
// 详情
const [detailVisible, setDetailVisible] = useState(false);
const [detailInfo, setDetailInfo] = useState([]);
// 售后操作记录
const [afterVisible, setAfterVisible] = useState(false);
const [afterList, setAfterList] = useState([]);
// 审核
const [visible, setVisible] = useState(false);
const [auditInfo, setAuditInfo] = useState({});
// 关闭modal
const closeModal = isReload => {
if (isReload === true) {
// eslint-disable-next-line no-unused-expressions
actionRef.current?.reload?.();
}
setVisible(false);
setDetailVisible(false);
setProofsVisible(false);
setAppealDetailModal(false);
setAfterVisible(false);
};
// 查看申诉详情
const viewAppeal = async r => {
const detailData = await getDetail({ appealNo: r.appealNo });
setAppealDetailModal(true);
setSelectedRow(detailData);
};
// 查看凭证
const viewProofs = proofs => {
if (!proofs) {
notification.warning({ message: '该订单没有凭证' });
return;
}
const list = proofs.replace(/(\uff1b|\uff0c|\u003b)/g, ',').split(',');
setProofsData(list);
setProofsVisible(true);
};
// 审核
const openAudit = async ({ serviceNo, serviceType }) => {
const data = await auditInfoApi({ serviceNo });
setAuditInfo({ ...data?.data, serviceNo, serviceType });
setVisible(true);
};
// 物流拦截
const openLogistics = r => {
confirm({
title: '温馨提示',
okText: '确认拦截',
cancelText: '取消拦截',
content: (
<div>
请48小时内自行联系物流公司进行物流拦截,系统监测拦截成功后
<span className={styles.redTipBold}>自动同意</span>退款
</div>
),
async onOk() {
const data = await logisticsIntercept({ serviceNo: r.serviceNo });
if (data.businessCode === '0000') {
notification.success({ message: '拦截成功' });
actionRef.current.reload();
} else {
notification.error({ message: data.msg || '拦截失败' });
}
},
onCancel() {
console.log('Cancel');
},
});
};
// 查看
const viewDetail = async ({ serviceNo }) => {
const res = await orderDetail({ serviceNo });
const data = res.data || [];
setDetailInfo(data);
setDetailVisible(true);
};
// 查看售后操作日志
const viewLog = async r => {
const data = await getOpLog(r.serviceNo);
if (data?.data?.length) {
setAfterList(data.data);
setAfterVisible(true);
}
};
const tabChange = e => {
console.log(e);
};
const [form] = Form.useForm();
const formConfig = getFormConfig({
setTableParams,
});
const FormSearchProps = {
form,
initialValues: {},
formOptions: {},
...formConfig,
};
const columns = getColumns({
openAudit,
viewAppeal,
viewProofs,
openLogistics,
viewDetail,
viewLog,
canEditable,
});
// 表格属性
const tableProps = {
columns,
params: tableParams,
bordered: true,
scroll: { x: '100%', y: 500 },
rowKey: r => r.serviceNo,
request: async params => {
console.log(params);
console.log('搜索', params);
const { current: page, pageSize: size } = params;
const startDate = params.startDate ? moment(params.startDate).format('YYYY-MM-DD') : '';
const endDate = params.endDate ? moment(params.endDate).format('YYYY-MM-DD') : '';
const requestParams = {
page,
size,
queryStatus: 1,
...params,
startDate,
endDate,
};
const res = await searchList(requestParams);
const { records = [], total } = res.data;
return {
data: records,
total,
};
},
toolBarRender: null,
};
return (
<PageHeaderWrapper>
<FormSearch {...FormSearchProps} />
<div className={styles['tab-box']}>
<Tabs onChange={tabChange} size="large" tabBarStyle={{ padding: '0 30px' }}>
<TabPane key="" tab="全部"></TabPane>
<TabPane key={1} tab="仅退款(未发货)"></TabPane>
<TabPane key={2} tab="待审核0"></TabPane>
<TabPane key={3} tab="仅退款待审核8"></TabPane>
<TabPane key={4} tab="退货退款待审核0"></TabPane>
<TabPane key={5} tab="退货入库待审核0"></TabPane>
<TabPane key={6} tab="已完成0"></TabPane>
</Tabs>
</div>
<ProTable
{...tableProps}
actionRef={actionRef}
formRef={formRef}
search={false}
toolBarRender={false}
/>
<AuditModal visible={visible} onCancel={closeModal} formData={auditInfo} />
<DetailTable visible={detailVisible} onCancel={closeModal} dataSource={detailInfo} />
<ProofsModal visible={proofsVisible} onCancel={closeModal} data={proofsData} />
<AppealDetail
data={selectedRow}
modalVisible={appealDetailModal}
onCancel={closeModal}
></AppealDetail>
<AfterLog visible={afterVisible} onCancel={closeModal} data={afterList} />
</PageHeaderWrapper>
);
};
export default connect(({ menu }) => ({
permissions: menu.permissions,
}))(AfterSale);
.proofs {
padding: 5px;
border: 1px solid #ddd;
border-radius: 10px;
}
.proofsWrap {
display: flex;
justify-content: space-between;
min-height: 300px;
max-height: 600px;
overflow: auto;
}
.detailWrap {
min-height: 300px;
max-height: 600px;
overflow: auto;
}
.logBtn {
display: inherit;
margin: 20px auto;
}
.redTip {
color: #ff4d4f;
font-size: 14px;
}
.redTipBold {
color: #ff1616;
font-weight: bold;
}
.tab-box {
margin-top: 20px;
background-color: #fff;
}
import request from '@/utils/request';
import config from '../../../config/env.config';
import { stringify } from 'qs';
import _ from 'lodash';
const { kdspApi } = config;
// 分页查询所有数据
export const searchList = params =>
request.post('/api/kdsp/op/afs/shop/list', {
prefix: kdspApi,
data: stringify(_.omitBy(params, v => !v)),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
// export async function searchList(params, queryStatus) {
// const param = {
// ...params,
// pageNo: params.current,
// pageSize: params.pageSize || 20,
// queryStatus,
// };
// const data = await request.post('/api/kdsp/op/afs/shop/list', {
// prefix: kdspApi,
// data: stringify(_.omitBy(param, v => !v)),
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded',
// },
// });
// if (data.data) {
// return {
// total: data.data.total,
// data: data.data.records,
// };
// }
// return {
// total: 0,
// data: [],
// };
// }
// 售后单详情
export async function orderDetail(params) {
const data = await request.get('/api/kdsp/op/afs/sku', {
prefix: kdspApi,
params,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
return data.data || [];
}
// 售后审核
export async function shopAudit(params) {
return request.post('/api/kdsp/op/afs/shop/audit', {
data: params,
prefix: kdspApi,
});
}
// 查询审核信息
export async function auditInfoApi(params) {
return request.get('/api/kdsp/op/afs/back-info', {
params,
prefix: kdspApi,
});
}
// 审核核检
export async function shopCheck(params) {
return request.post('/api/kdsp/op/afs/shop/check', {
params,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
prefix: kdspApi,
});
}
// 查询物流信息
export async function trackInfo(params) {
const data = await request.get('/api/kdsp/op/logistics/kd100/track-list', {
params,
prefix: kdspApi,
});
if (data.businessCode === '0000') {
return data.data;
}
return {};
}
// 物流拦截
export async function logisticsIntercept(params) {
return request.get('/api/kdsp/op/afs/shop/logisticsIntercept', {
params,
prefix: kdspApi,
});
}
// 售后操作日志
export async function getOpLog(params) {
return request.get(`/afterSaleAdmin/getOpLog/${params}`, {
prefix: kdspApi,
});
}
......@@ -110,6 +110,7 @@ const StoreModal = props => {
params.parentId = id;
}
const res = await apiAddrArea(params);
console.log(res);
if (res) {
const arr = res.map(item => ({
isLeaf: false,
......
import { Button, notification, Modal } from 'antd';
import { Button, notification, Modal, Popover } from 'antd';
import React, { useState, useEffect, useRef } from 'react';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import ProTable from '@ant-design/pro-table';
......@@ -6,8 +6,8 @@ import { FormInstance } from 'antd/lib/form';
import moment from 'moment';
import { connect } from 'dva';
import { PENDING_DELIVERY_ORDER, DELIVERY_ORDER } from '@/../config/permission.config';
import style from './index.less';
import LogisticsForm from './components/LogisticsForm';
import style from './styles.less';
import PopoverDom from './components/PreviewImage';
import LogisticsCom from './components/LogisticsCom';
import DelayDeliverGoods from './components/DelayDeliverGoods';
......@@ -141,6 +141,37 @@ const TableList = props => {
order: 6,
width: 250,
fixed: 'left',
render: (_, record) => (
<>
{_}
<Popover placement="top" title="催发货" content={<>111</>} trigger="click">
<span className={style.urge}></span>
</Popover>
</>
),
},
{
title: '订单提醒',
dataIndex: 'orderNotice',
key: 'orderNotice',
width: 150,
render: (_, record) => {
const { address } = record;
return (
<div className={style['notice-btn']}>
<Popover placement="top" title="收货地址变更" content={<>111</>} trigger="click">
<Button block type="primary">
收货地址变更
</Button>
</Popover>
<Popover placement="top" title="平台备注" content={<>222</>} trigger="click">
<Button block type="warning">
平台备注
</Button>
</Popover>
</div>
);
},
},
{
title: '商品名称',
......
......@@ -73,3 +73,24 @@
margin-right: 10px;
}
}
.notice-btn {
max-width: 120px;
margin: 0 auto;
text-align: center;
button {
margin: 5px 0;
}
}
// 催发货图标
.urge {
display: inline-block;
width: 20px;
height: 20px;
color: #fff;
line-height: 20px;
text-align: center;
background-color: red;
border-radius: 50%;
}
import request from '@/utils/request';
import config from '../../config/env.config';
const { msgApi } = config;
/**
* @name 商户消息列表
* @see http://yapi.quantgroups.com/project/193/interface/api/41792
*/
export function apiGetBussinessMsgList(data, params) {
return request.post('/v1/send/message/getBussinessMsgList', {
data,
params,
prefix: msgApi,
});
}
/**
* @name 获取未读消息数量
* @see http://yapi.quantgroups.com/project/193/interface/api/42792
*/
export function apiGetBusinessMsgUnReadCount(data) {
return request.post('/v1/send/message/getBusinessMsgUnReadCount', {
data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
prefix: msgApi,
});
}
/**
* @name 设置消息为已读
* @see http://yapi.quantgroups.com/project/193/interface/api/41808
*/
export function apiUpdageBusinessMsgStatus(data) {
return request.post('/v1/send/message/updateBussinessMsgReadStatus', {
data,
prefix: msgApi,
});
}
export const APP_CODE = 'APP013';
export const TENANT_ID = 560761;
export const CHANNEL_ID = 100001;
// 小程序权限标识符, 用于过滤掉返回的小程序权限数据
export const APPLETS_PERMISSION_IDENTIFIER = 'applets';
import EventEmitter from 'events';
class Socket extends EventEmitter {
event = new EventEmitter();
constructor(options) {
super();
this.options = options;
this.reconnectCount = 0;
this.socket = null;
this.taskRemindInterval = null;
this.connected = false;
this.waitingSendData = [];
return this;
}
connection = () => {
const { url, timeout = 0 } = this.options;
// 检测当前浏览器是什么浏览器来决定用什么socket
if ('WebSocket' in window) {
console.log('WebSocket');
this.socket = new WebSocket(url);
} else if ('MozWebSocket' in window) {
console.log('MozWebSocket');
// eslint-disable-next-line no-undef
this.socket = new MozWebSocket(url);
} else {
console.log('SockJS');
// eslint-disable-next-line no-undef
this.socket = new SockJS(url);
}
// 链接回调
this.socket.onopen = this.onopen;
this.socket.onmessage = this.onmessage;
this.socket.onclose = this.onclose;
this.socket.onerror = this.onerror;
this.socket.sendMessage = this.sendMessage;
// 检测返回的状态码 如果socket.readyState不等于1则连接失败,关闭连接
if (timeout) {
const time = setTimeout(() => {
if (this.socket && this.socket.readyState !== 1) {
console.log('主动触发关闭');
this.socket.close();
}
clearInterval(time);
}, timeout);
}
};
// 连接成功触发
onopen = () => {
console.log('ws:连接成功');
this.connected = true;
this.checkWaitingData();
this.event.emit('open');
};
// 后端向前端推得数据
onmessage = msg => {
console.log('接收数据:', msg);
this.event.emit('message', msg);
// 打印出后端推得数据
};
// 关闭连接触发
onclose = e => {
this.connected = false; // 关闭将标识符改为true
console.log('关闭socket收到的数据');
this.event.emit('close', e);
// 根据后端返回的状态码做操作
// 我的项目是当前页面打开两个或者以上,就把当前以打开的socket关闭
// 否则就20秒重连一次,直到重连成功为止
if (e.code === '4500') {
this.socket.close();
} else {
this.taskRemindInterval = setInterval(() => {
if (!this.connected) {
this.connection();
} else {
clearInterval(this.taskRemindInterval);
}
}, 2000);
}
};
onerror = e => {
this.socket = null;
this.event.emit('error', e);
};
sendMessage = value => {
// 向后端发送数据
if (this.socket) {
if (!this.connected) {
this.waitingSendData.unshift(value);
return;
}
this.socket.send(JSON.stringify(value));
}
};
checkWaitingData() {
if (this.waitingSendData.length) {
this.sendMessage(this.waitingSendData.splice(0, 1));
this.checkWaitingData();
}
}
}
export default Socket;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment