Commit d8defddc authored by 武广's avatar 武广

Merge branch 'master' of git.quantgroup.cn:ui/merchant-manage-ui into feature/addr20221031

parents 0fc293c8 baa19369
......@@ -146,19 +146,26 @@ export default {
icon: 'smile',
component: './AfterSaleManage/index',
},
{
title: '商户管理后台',
path: '/afterSaleManageOld',
name: 'afterSaleManageOld',
icon: 'smile',
component: './AfterSaleManageOld/index',
},
{
title: '商户管理后台',
path: '/auditPending',
name: 'auditPending',
icon: 'smile',
component: './AfterSaleManage/Pending',
component: './AfterSaleManageOld/Pending',
},
{
title: '商户管理后台',
path: '/passAudit',
name: 'passAudit',
icon: 'smile',
component: './AfterSaleManage/PassAudit',
component: './AfterSaleManageOld/PassAudit',
},
{
title: '商户管理后台',
......
......@@ -2,14 +2,16 @@ const isProduction = process.env.NODE_ENV === 'production';
const isPre = process.env.PRE_ENV === 'pre';
const environment = 'sc';
const envAPi = {
api: `https://security-${environment}.liangkebang.net`,
api: `https://security-${environment}.liangkebang.net`, //'https://security-xyqb.liangkebang.net',
kdspOpApi: `https://sc-merchant-api-${environment}.liangkebang.net`,
kdspApi: `https://sc-merchant-api-${environment}.liangkebang.net`,
goodsApi: `https://sc-merchant-api-${environment}.liangkebang.net`,
querysApi: `https://sc-merchant-api-${environment}.liangkebang.net/admin/merchant/sc-settlement`,
querysApi: `https://sc-merchant-api-${environment}.liangkebang.net`,
prologueDomain: `https://mall-${environment}.liangkebang.net`,
qiniuHost: `https://kdspstatic.q-gp.com/`,
opapiHost: `https://yxm-gateway-${environment}.liangkebang.net`,
qiniuHost: 'https://kdspstatic.q-gp.com/',
opapiHost: `https://opapi-${environment}.liangkebang.net`,
wsApi: `wss://push-${environment}.liangkebang.net`,
msgApi: `https://msgapi-${environment}.liangkebang.net`,
};
const prodApi = {
......@@ -25,6 +27,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',
wsApi: 'wss://push.q-gp.com',
msgApi: 'https://msgapi.q-gp.com',
};
const preProdApi = {
......@@ -36,6 +40,8 @@ const preProdApi = {
qiniuHost: 'https://kdspstatic.q-gp.com/',
opapiHost: 'https://opapi-pre.q-gp.com',
querysApi: 'https://sc-settlement-api.q-gp.com',
wsApi: 'wss://push.q-gp.com',
msgApi: 'https://msgapi.q-gp.com',
};
let exportApi = envAPi;
......
import React, { useState } from 'react';
import { Form, Input, Select, DatePicker, Button, Space } from 'antd';
import 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 = {}, bindKey, limit } = config;
const attrs = { placeholder: ['开始日期', '结束日期'], ...originOptions };
// 自动截取日期限制的范围
const onOpenChange = open => {
if (!open) {
if (limit) {
const { rangeNum, rangeUnit } = limit;
const [date1, date2] = form.getFieldValue(bindKey);
const diffNum = date2.diff(date1, rangeUnit);
const unitMapping = {
months: 'month',
};
if (diffNum >= rangeNum) {
setTimeout(() => {
form.setFieldsValue({
[bindKey]: [moment(date1), moment(date1).add(rangeNum, unitMapping[rangeUnit])],
});
});
}
}
}
};
return (
<FormItemBox {...config}>
<RangePicker className={style['form-item-tag']} onOpenChange={onOpenChange} {...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: '请输入', allowClear: true, ...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 { useHistory } from 'react-router-dom';
import { Modal, Tabs, Pagination, Spin, notification, Checkbox, Badge } from 'antd';
import { apiGetBussinessMsgList, apiGetBusinessMsgUnReadCount } from '@/services/messageReminder';
import { connect } from 'dva';
import Empty from '../Empty';
import styles from './index.less';
import { CHANNEL_ID } from '@/utils/constants';
const INIT_PAGINATION = {
pageSize: 20,
pageNo: 1,
};
const INIT_QUERY_PARAMS = {
type: '0',
readStatus: '',
};
const MessageItem = props => {
const { item, onMark, viewDetail } = props;
/**
* type: 0订单消息,1售后消息
* readStatus: 0未读,1已读
*/
const { readStatus, type } = item;
let message = {};
try {
message = JSON.parse(item.sendContent || '{}');
} catch (e) {
console.error('消息数据格式错误');
}
const goodList = message.items.map((good, index) => (
<div className={styles.good} key={String(index)}>
<span className={styles.good__name}>{good.skuName}</span>
<span className={styles.good__count}>x{good.quantity}</span>
</div>
));
return (
<div className={styles['complex-list__item']}>
<div className={styles['complex-list__item--header']}>
<span className={styles['order-number']}> 订单编号:{message.orderNo}</span>
<span className={styles.time}>订单时间:{message.time}</span>
<span className={styles['read-status']}>
{readStatus === 0 ? (
<a className={styles['read-status--un-read']} onClick={() => onMark([item.id])}>
标记为已读
</a>
) : (
<span className={styles['read-status--read']}>已读</span>
)}
</span>
</div>
<div className={styles['complex-list__item--body']}>{goodList}</div>
<div className={styles['complex-list__item--footer']}>
<div className={styles.actions}>
<a
onClick={() => viewDetail(message)}
className={[styles.notice, readStatus === 0 ? styles['un-read'] : ''].join(' ')}
>
{message.title},请查看
</a>
</div>
</div>
</div>
);
};
const Complex = props => {
const { dispatch, refInstance } = props;
const history = useHistory();
const [visible, setVisible] = useState(false);
const [dataTotal, setDataTotal] = useState(10);
const [loading, setLoading] = useState(false);
// 消息列表数据
const [messageData, setMessageData] = useState([]);
// 未读消息
const [orderUnReadCount, setOrderUnReadCount] = useState(0);
const [afterUnReadCount, setAfterUnReadCount] = useState(0);
// 消息列表参数
const [queryParams, setQueryParams] = useState({ ...INIT_QUERY_PARAMS, ...INIT_PAGINATION });
const userInfo = JSON.parse(localStorage.getItem('user') || '{}');
// 获取信息列表
const getMsgList = async () => {
const params = {
pageSize: queryParams.pageSize,
pageNo: queryParams.pageNo,
};
const data = {
channelId: CHANNEL_ID,
bussinessId: userInfo.supplierCode,
...queryParams,
};
delete data.pageSize;
delete data.pageNo;
setLoading(true);
const res = await apiGetBussinessMsgList(data, params);
setLoading(false);
if (res.code !== '0000') {
notification.error(res.msg);
return;
}
const { content, totalElements } = res.data;
setMessageData(content);
setDataTotal(totalElements);
};
// 获取未读数量
const getMsgReadCount = async () => {
const data = {
channelId: CHANNEL_ID,
bussinessId: userInfo.supplierCode,
};
const res = await apiGetBusinessMsgUnReadCount(data);
if (res.code !== '0000') {
notification.error(res.msg);
return;
}
const { afterSalesUnRead, orderUnRead } = res.data;
setOrderUnReadCount(orderUnRead);
setAfterUnReadCount(afterSalesUnRead);
};
// 分页操作
const onPageChange = (page, size) => {
const current = queryParams.pageSize !== size ? 1 : page;
setQueryParams({
...queryParams,
pageNo: current,
pageSize: size,
});
};
// 筛选未读/已读
const onReadStatusChange = e => {
let { value } = e.target;
if (queryParams.readStatus === e.target.value) {
value = '';
}
setQueryParams({
...queryParams,
readStatus: value,
});
};
// 过滤参数,获取当前页未读消息数据的id
const onFilterMessageParams = msgList =>
msgList.filter(message => message.readStatus === 0).map(item => item.id);
const open = () => {
setVisible(true);
getMsgReadCount();
};
const close = () => {
setMessageData([]);
setVisible(false);
};
//
/**
* 跳转到详情
* type
* 1-待发货订单
* 2-超时发货
* 3-仅退款(未发货)申请单
* 4-仅退款申请单
* 5-退货退款申请单
* 6-退货入库待审核
*/
const viewDetail = message => {
const { orderNo, type } = message;
if ([1, 2].includes(type)) {
history.push({
pathname: '/orderManage/pendingDeliveryOrder',
query: { orderNo },
});
}
if ([3, 4, 5, 6].includes(type)) {
history.push({
pathname: '/afterSaleManage',
query: { orderNo },
});
}
close();
};
// 切换消息类型
const onTabChange = index => {
setQueryParams({
...queryParams,
...INIT_PAGINATION,
type: index,
});
};
// 标记已读
const onMark = idsList => {
if (!idsList.length) {
return;
}
const payload = {
channelId: CHANNEL_ID,
bussinessId: userInfo.supplierCode,
type: Number(queryParams.type),
idsList,
};
dispatch({
type: 'messageReminder/setMarkRead',
payload,
options: {
setLoading,
callback: () => {
getMsgReadCount();
getMsgList();
},
},
});
};
// 展开初始化
useEffect(() => {
if (visible) {
getMsgList();
}
}, [visible, queryParams]);
useEffect(() => {
if (!visible) {
setQueryParams({ ...INIT_QUERY_PARAMS, ...INIT_PAGINATION });
}
}, [visible]);
useImperativeHandle(refInstance, () => ({
open,
}));
const modalProps = {
bodyStyle: {
display: 'flex',
flexWrap: 'wrap',
padding: 0,
backgroundColor: '#f7f8f9',
},
wrapClassName: 'complex-modal',
width: '880px',
height: '650px',
visible,
title: <span className="title-icon">消息提醒</span>,
footer: null,
onCancel: close,
};
const TabRender = tabProps => {
const { title, count = 0 } = tabProps;
return (
<span className={styles['tab-render']}>
{title}
<Badge overflowCount={999} count={count} />
</span>
);
};
const FilterRender = filterProps => (
<div className={styles['filter-box']}>
<div className={styles['filter-box__content']}>
<Checkbox checked={queryParams.readStatus === '0'} value="0" onChange={onReadStatusChange}>
未读
</Checkbox>
<Checkbox checked={queryParams.readStatus === '1'} value="1" onChange={onReadStatusChange}>
已读
</Checkbox>
</div>
<div className={styles['filter-box__actions']}>
<a onClick={() => onMark(onFilterMessageParams(messageData))}>全部标记为已读</a>
</div>
</div>
);
return (
<Modal {...modalProps}>
<Tabs
className={styles.tabs}
activeKey={queryParams.type}
tabBarStyle={{
width: '150px',
}}
tabPosition="left"
tabBarGutter={0}
onChange={onTabChange}
>
<Tabs.TabPane tab={<TabRender title="订单消息" count={orderUnReadCount} />} key="0" />
<Tabs.TabPane tab={<TabRender title="售后消息" count={afterUnReadCount} />} key="1" />
</Tabs>
<div className={styles['tab-pane']}>
<Spin spinning={loading}>
<FilterRender />
<div className={styles['complex-list']}>
{visible && messageData.length ? (
messageData.map((item, index) => (
<MessageItem
key={String(index)}
item={item}
onMark={onMark}
viewDetail={viewDetail}
/>
))
) : (
<Empty text="暂无数据" />
)}
</div>
</Spin>
{dataTotal > 0 ? (
<div className={styles.pagination}>
<Pagination
onChange={onPageChange}
total={dataTotal}
showTotal={(total, range) => `第${range[0]}-${range[1]}条 /总共${total}条`}
showSizeChanger
pageSize={queryParams.pageSize}
current={queryParams.pageNo}
/>
</div>
) : (
''
)}
</div>
</Modal>
);
};
const MiddleComponent = connect(({ messageReminder }) => ({
unReadCount: messageReminder.unReadCount,
unReadData: messageReminder.unReadData,
}))(Complex);
// 注意:这里不要在Component上使用ref;换个属性名字比如refInstance;不然会导致覆盖
export default forwardRef((props, ref) => <MiddleComponent {...props} refInstance={ref} />);
.tab-pane {
flex: 1;
}
.tabs {
:global .ant-tabs-tab {
height: 48px;
line-height: 48px;
border-bottom: 1px solid #efefef;
}
:global .ant-tabs-ink-bar {
height: 40px !important;
margin-top: 4px !important;
}
}
.tab-render {
display: flex;
align-items: center;
:global .ant-badge {
margin-left: 5px;
}
}
.filter-box {
display: flex;
margin-top: 15px;
padding-right: 25px;
&__content {
flex: 1;
}
}
:global .complex-modal {
.ant-modal-header {
text-align: center;
background-color: #2d8cf0;
}
.ant-modal-title,
.ant-modal-close {
color: #fff;
.title-icon {
padding-left: 25px;
background: #1890ff url('https://img.lkbang.net/notice.94d42513.png') no-repeat 0 center;
background-size: 20px 20px;
}
}
}
.complex-list {
min-height: 300px;
max-height: 500px;
margin-top: 20px;
padding-right: 25px;
overflow-y: auto;
& :first-child {
margin: 0;
}
&__item {
margin-top: 15px;
background: #fff;
border: 1px solid #efefef;
&--header {
display: flex;
width: 100%;
height: 40px;
padding: 0 15px;
line-height: 40px;
background: #fff;
border-bottom: 1px solid #efefef;
.order-number,
.time {
flex: 1;
padding-right: 20px;
color: #999;
word-break: break-all;
// white-space: nowrap
}
.read-status {
min-width: 80px;
text-align: right;
&--read {
color: #999;
}
}
}
&--body {
padding: 10px 15px;
.good {
display: flex;
padding: 5px 0;
line-height: 1.5;
&__name {
flex: 1;
word-break: break-all;
}
&__count {
min-width: 80px;
text-align: right;
}
}
}
&--footer {
display: flex;
border-top: 1px solid #efefef;
.actions {
display: flex;
width: 100%;
margin-left: 20px;
line-height: 40px;
.notice {
position: relative;
display: block;
flex: 1;
color: #999;
font-weight: 400;
&.un-read {
color: #ff1515;
&::before {
display: inline-block;
width: 6px;
height: 6px;
margin-top: 17px;
margin-right: 6px;
vertical-align: top;
background: #ff1515;
border-radius: 50%;
content: '';
}
}
&::after {
position: absolute;
top: 50%;
right: 10px;
display: block;
width: 0;
height: 0;
margin-top: -3px;
border: 6px solid transparent;
border-left: 8px solid #999;
content: '';
}
}
}
}
}
}
.pagination {
width: 100%;
padding: 25px;
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;
background-color: #fff;
img {
display: inline-block;
height: 76px;
margin-bottom: 16px;
}
}
import React, { useState, useEffect } from 'react';
import { Badge, notification, Spin } from 'antd';
import { CloseOutlined, DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
// import copy from 'copy-to-clipboard';
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, stowClass, onStow } = props;
return (
<div
className={classNames(styles.horn, styles[animationClass], styles[stowClass])}
onClick={toggle}
>
<span className={styles['horn--btn']} onClick={e => onStow(e)}>
{stowClass ? <DoubleLeftOutlined /> : <DoubleRightOutlined />}
</span>
<Badge count={count}>
<div className={styles['horn--num']}>消息提醒</div>
</Badge>
</div>
);
};
const Message = props => {
const {
toggle,
animationClass,
messageData,
openComplex,
onMark,
loading,
viewDetail,
onStow,
stowClass,
} = props;
const ReminderItem = args => {
const { item } = args;
let message = {};
try {
message = JSON.parse(item.sendContent || '{}');
} catch (e) {
console.error('消息数据格式错误');
}
return (
<div className={styles.item}>
<div className={styles.info}>
<span className={styles['order-number']}>{message.orderNo}</span>
<span className={styles['mark-read']} onClick={() => onMark([item.id])}>
标记为已读
</span>
</div>
<div className={styles.time}>
<span>{message.time}</span>
</div>
<div className={styles.notice}>
<a onClick={() => viewDetail(message)}>{message.title},请查看</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']}>
<Spin spinning={loading}>
<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>
</Spin>
</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 history = useHistory();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [count, setCount] = useState(0);
const [hornClass, setHornClass] = useState('');
const [messageClass, setMessageClass] = useState('');
const [stowClass, setStowClass] = useState('');
const [messageData, setMessageData] = useState([]);
let userInfo = JSON.parse(localStorage.getItem('user') || '{}');
const toggle = () => {
setVisible(!visible);
};
// 初始化获取数据
const getMsgList = async () => {
if (!userInfo.supplierCode) {
userInfo = JSON.parse(localStorage.getItem('user') || '{}');
setTimeout(() => {
getMsgList();
}, 1000);
return;
}
const params = {
pageNo: 1,
pageSize: 1000,
};
const data = {
channelId: CHANNEL_ID,
bussinessId: userInfo.supplierCode,
readStatus: 0,
};
setLoading(true);
const res = await apiGetBussinessMsgList(data, params);
setLoading(false);
if (res.code !== '0000') {
notification.error(res.msg);
return;
}
const { content } = res.data;
setVisible(!!content.length);
dispatch({
type: 'messageReminder/setUnReadData',
payload: content,
options: {
unReadCount: content.length,
},
});
};
const viewDetail = message => {
const { orderNo, type } = message;
console.log(type);
if ([1, 2].includes(type)) {
history.push({
pathname: '/orderManage/pendingDeliveryOrder',
query: { orderNo },
});
}
if ([3, 4, 5, 6].includes(type)) {
history.push({
pathname: '/afterSaleManage',
query: { orderNo },
});
}
toggle();
};
// 打开消息提醒弹框
const openComplex = () => {
complexRef.current.open();
toggle();
};
const onStow = e => {
e.preventDefault();
e.stopPropagation();
if (stowClass) {
setStowClass('');
} else {
setStowClass('stow');
}
};
// 标记已读信息
const onMark = idsList => {
if (!idsList.length) {
return;
}
const payload = {
channelId: CHANNEL_ID,
bussinessId: userInfo.supplierCode,
idsList,
};
dispatch({
type: 'messageReminder/setMarkRead',
payload,
options: {
setLoading,
},
});
};
useEffect(() => {
setCount(unReadCount);
}, [unReadCount]);
useEffect(() => {
setHornClass(visible ? 'hide' : 'show');
setMessageClass(visible ? 'show' : 'hide');
}, [visible]);
useEffect(() => {
getMsgList();
}, []);
useEffect(() => {
setMessageData(unReadData);
}, [unReadData]);
const hornProps = {
animationClass: hornClass,
stowClass,
toggle,
count,
onStow,
};
const messageProps = {
animationClass: messageClass,
toggle,
messageData,
openComplex,
onMark,
loading,
viewDetail,
};
// 隐藏消息提醒
return (
<>
<Horn {...hornProps} />
<Message {...messageProps} />
</>
);
};
export default connect(({ messageReminder }) => ({
unReadCount: messageReminder.unReadCount,
unReadData: messageReminder.unReadData,
}))(Simple);
.horn {
position: fixed;
right: 30px;
bottom: 10px;
z-index: 19;
// width: 112px;
// height: 50px;
color: #fff;
background: #2d8cf0;
transform: translate(0, 0);
cursor: pointer;
opacity: 1;
transition: 0.2s ease-in;
&.hide {
transform: translateY(100%) scale(0.3);
opacity: 0;
transition: 0.2s ease-out;
}
&.stow {
transform: translateX(142px);
transition: 0.2s ease-out;
}
&--btn {
display: inline-block;
width: 20px;
height: 50px;
line-height: 50px;
text-align: center;
vertical-align: top;
background: #006ad9;
border-right: 1px solid #2479d3;
&:hover {
background: #2479d3;
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;
&::before {
display: inline-block;
width: 6px;
height: 6px;
margin-top: 8px;
margin-right: 6px;
vertical-align: top;
background: #ff1515;
border-radius: 50%;
content: '';
}
&::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,22 @@
*/
/* 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 { getSocketUrl } from '@/services/messageReminder';
import logo from '../assets/logo.png';
import style from './BasicLayout.less';
import Socket from '@/utils/websocket';
import { CHANNEL_ID } from '@/utils/constants';
const { Sider } = Layout;
const { SubMenu } = Menu;
......@@ -48,12 +53,27 @@ const BasicLayout = props => {
} = props;
const [siderCollapsed, setSiderCollapsed] = useState(false);
/**
* constructor
*/
const messageReminderComplexRef = useRef();
// const audioRef = useRef()
useEffect(() => {
try {
const token = window.localStorage.getItem('token');
const socket = new Socket({
url: getSocketUrl({ token, channelId: CHANNEL_ID }),
});
socket.connection();
socket.event.on('message', msg => {
dispatch({
type: 'messageReminder/setUnReadData',
payload: [JSON.parse(msg.data)],
});
});
} catch (e) {
console.log(e);
}
if (dispatch) {
dispatch({
type: 'settings/getSetting',
......@@ -61,6 +81,9 @@ const BasicLayout = props => {
dispatch({
type: 'menu/getMenuData',
});
dispatch({
type: 'messageReminder/getUnReadMsgList',
});
}
}, []);
/**
......@@ -156,7 +179,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 +200,10 @@ const BasicLayout = props => {
<Authorized authority={authorized.authority} noMatch={noMatch}>
{children}
</Authorized>
<MessageReminder.Simple complexRef={messageReminderComplexRef} />
<MessageReminder.Complex ref={messageReminderComplexRef} />
{/* <Button ref={audioRef} onClick={() => { socket.play() }}>声音</Button> */}
</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 { setLoading } = options;
if (setLoading) {
setLoading(true);
}
const res = yield call(apiUpdageBusinessMsgStatus, payload);
// console.log(res)
if (setLoading) {
setLoading(false);
}
if (res.code !== '0000') {
return;
}
if (
Object.hasOwnProperty.call(options, 'callback') &&
typeof options.callback === 'function'
) {
options.callback(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.idsList.includes(message.id));
// 重置当前未读数量
const unReadCount = currentData.length; // state.unReadCount - (options.readCount || payload.idsList.length || 1)
return { ...state, unReadCount, unReadData: currentData };
},
},
};
export default MessageReminderModel;
......@@ -36,7 +36,7 @@ const Admin = props => {
</div>
<div style={{ marginTop: '20px' }}>
<Row className={style.orderContent}>
{showPendingDeliveryOrderList || showCancelBillList ? (
{showPendingDeliveryOrderList || showAfterSaleList ? (
<Col span={2} className="titleOne">
<div>
<FileTextOutlined />
......@@ -74,11 +74,14 @@ const Admin = props => {
) : (
''
)}
{showCancelBillList ? (
{showAfterSaleList ? (
<Col span={4}>
<Link
to={{
pathname: 'cancelBillManage',
pathname: 'afterSaleManage',
query: {
tab: '1',
},
}}
>
<strong>{pendingNum?.cancelOrderPendingAuditNum || 0}</strong>
......@@ -107,6 +110,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>
</>
) : (
''
......
......@@ -2,7 +2,7 @@
/* eslint-disable guard-for-in */
import React, { useState, useEffect } from 'react';
import { Modal, Timeline, Button } from 'antd';
import styles from '../styles.less';
import styles from '../index.less';
const LogisticsCom = props => {
const { visible, onCancel, data } = props;
......
import React from 'react';
import { Form } from '@ant-design/compatible';
import '@ant-design/compatible/assets/index.css';
import { Modal, Input, Cascader, notification } from 'antd';
import { cancelShopAudit } from '../services';
const FormItem = Form.Item;
const { TextArea } = Input;
const AuditModal = props => {
const {
visible,
onCancel,
form: { getFieldDecorator, getFieldValue, validateFields, resetFields },
formData = {},
} = props;
const handleCancel = isSuccess => {
resetFields();
onCancel(isSuccess);
};
const treeData = [
{
label: '同意',
value: true,
},
{
label: '不同意',
value: false,
children: [
{
label: '订单已发货',
value: 10,
},
{
label: '已与客户沟通继续发货',
value: 20,
},
{
label: '其他',
value: 30,
},
],
},
];
const handleOk = () => {
validateFields(async (error, fieldsValue) => {
if (!error) {
const { approve } = fieldsValue;
const data = await cancelShopAudit({
...fieldsValue,
refuseReasonCode: approve?.[1],
approve: approve?.[0],
serviceNo: formData?.serviceNo,
});
if (data.businessCode === '0000') {
notification.success({ message: approve?.[0] ? '审核成功' : '审核拒绝提交成功' });
handleCancel(true);
}
}
});
};
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 16 },
};
const approve = getFieldValue('approve');
const isRefuse = () => approve && approve[0] !== 1;
return (
<Modal
title="售后操作确认"
visible={visible}
onOk={() => handleOk()}
onCancel={() => handleCancel()}
>
<Form {...layout} name="formData">
<FormItem label="审核结果">
{getFieldDecorator('approve')(
<Cascader
allowClear
showSearch
style={{ width: '315px' }}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
options={treeData}
placeholder="请选择"
/>,
)}
</FormItem>
{isRefuse() && approve[1] === 30 && (
<FormItem label="拒绝原因">
{getFieldDecorator('refuseReasonRemark', {
initialValue: formData.refuseDesc,
rules: [
{
required: true,
message: '请填写拒绝原因!',
},
],
})(
<TextArea
placeholder="请填写拒绝原因"
allowClear
autoSize={{ minRows: 3, maxRows: 6 }}
/>,
)}
</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={r => r.orderId}
pagination={false}
bordered
/>
</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 '../service';
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('refuseReasonRemark', {
rules: [
{
required: true,
message: '请填写原因!',
},
],
})(
<TextArea placeholder="请填写原因" allowClear autoSize={{ minRows: 3, maxRows: 6 }} />,
)}
</FormItem>
</Form>
</Modal>
);
};
export default Form.create()(RejectModal);
import React, { useState, forwardRef, useImperativeHandle, useEffect } from 'react';
import { Modal, Timeline, notification, Spin } from 'antd';
import { getLogisticsRecord } from '../services';
const LogisticsRecordModal = (props, ref) => {
// const { } = props;
const [visible, setVisible] = useState(false);
const [result, setResult] = useState({});
const [loading, setLoading] = useState(false);
const getRecordList = async orderNo => {
const tempObj = {
detailList: [],
key: Date.now(),
};
setLoading(true);
const res = await getLogisticsRecord({ orderNo });
setLoading(false);
if (!res) {
notification.info({ message: '暂无物流信息' });
return;
}
const { logisticsName, logisticsBillNo, logisticsList = [] } = res.data;
tempObj.expressCompanyName = logisticsName;
tempObj.deliveryNo = logisticsBillNo;
logisticsList.forEach(v => {
tempObj.detailList = [...tempObj.detailList, ...v.detailList];
});
setResult(tempObj);
};
const open = orderNo => {
setVisible(true);
getRecordList(orderNo);
};
const onCancel = () => {
setVisible(false);
setResult([]);
};
useImperativeHandle(ref, () => ({
open,
}));
const modalProps = {
title: '查看物流',
visible,
onCancel,
destroyOnClose: true,
bodyStyle: { maxHeight: '600px', minHeight: '200px', overflow: 'auto' },
footer: null,
};
const emptyStyle = {
textAlign: 'center',
padding: '30px 0',
color: '#999',
};
return (
<Modal {...modalProps}>
<Spin spinning={loading}>
{result.detailList?.length ? (
<Timeline>
{result?.detailList?.map((item, index) => (
<Timeline.Item color={index > 0 ? 'gray' : 'blue'} key={index.toString()}>
<p>{item.desc}</p>
<p>{item.logisticsTime}</p>
</Timeline.Item>
))}
</Timeline>
) : (
<div style={emptyStyle}>暂无物流信息</div>
)}
</Spin>
</Modal>
);
};
export default forwardRef(LogisticsRecordModal);
import React from 'react';
import React, { useRef } 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 '../styles.less';
import styles from '../index.less';
import LogisticsRecordModal from './LogisticsRecordModal';
const FormItem = Form.Item;
const { TextArea } = Input;
......@@ -15,7 +17,7 @@ const AuditModal = props => {
form: { getFieldDecorator, getFieldValue, validateFields, resetFields },
formData = {},
} = props;
const logisticsRecordModalRef = useRef();
const handleCancel = isSuccess => {
resetFields();
onCancel(isSuccess);
......@@ -102,6 +104,10 @@ const AuditModal = props => {
});
};
const openLogisticsRecord = () => {
logisticsRecordModalRef.current.open(formData.orderNo);
};
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 16 },
......@@ -110,133 +116,142 @@ const AuditModal = props => {
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="请选择"
/>,
<>
<Modal
title="售后操作确认"
visible={visible}
onOk={() => handleOk()}
onCancel={() => handleCancel()}
>
{+formData.serviceType === 1 && (
<div className={styles.redTip}>
温馨提示:当前售后类型为用户未收到产品,申请
<span className={styles.redTipBold}>仅退款</span>
,请务必检查此单物流状态后审核。<a onClick={openLogisticsRecord}>查看物流</a>
</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>
)}
</FormItem>
{isAgree() && +formData.serviceType === 2 && (
<div>
<FormItem label="退货地址">
{getFieldDecorator('receiveAddress', {
initialValue: formData.address,
{isRefuse() && auditResult[1] === 3 && (
<FormItem label="拒绝原因">
{getFieldDecorator('refuseDesc', {
initialValue: formData.refuseDesc,
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]+$/,
message: '请填写拒绝原因!',
},
],
})(<Input placeholder="最多输入25个字符" maxLength="25" allowClear />)}
})(
<TextArea
placeholder="请填写拒绝原因"
allowClear
autoSize={{ minRows: 3, maxRows: 6 }}
/>,
)}
</FormItem>
<FormItem label="手机号码">
{getFieldDecorator('receiverPhone', {
initialValue: formData.phone,
)}
{isRefuse() && auditResult[1] !== 3 && (
<FormItem label="补充说明">
{getFieldDecorator('refuseDesc', {
initialValue: formData.refuseDesc,
rules: [
// {
// pattern: new RegExp(/^1[3456789]\d{9}$/, 'g'),
// message: '手机号码格式不正确!',
// },
{
pattern: new RegExp(/^[0-9]*$/),
message: '手机号码格式不正确!',
},
{
required: true,
message: '请填写手机号码!',
message: '请填写补充说明!',
},
],
})(<Input placeholder="请填写手机号码" maxLength="11" allowClear />)}
})(
<TextArea
placeholder="请填写补充说明"
allowClear
autoSize={{ minRows: 3, maxRows: 6 }}
maxLength={120}
showCount
/>,
)}
</FormItem>
)}
</Form>
{+formData.serviceType === 2 && (
<div align="right" style={{ paddingRight: '42px', marginTop: '-25px' }}>
<a onClick={openLogisticsRecord}>查看物流</a>
</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>
</Modal>
<LogisticsRecordModal ref={logisticsRecordModalRef} />
</>
);
};
......
import React, { useState } from 'react';
import { Modal } from 'antd';
import style from '../styles.less';
import style from '../index.less';
export default props => {
const { visible, onCancel, data } = props;
......
import { Tag, Badge } from 'antd';
import { Tag, Badge, Statistic, Button, Popconfirm } from 'antd';
import React from 'react';
import moment from 'moment';
export const appealType = {
1: '已申诉',
0: '未申诉',
};
export const columnSticData = [
{
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: '退货退款',
},
},
import { SEARCH_TYPE } from '@/components/FormSearch';
{
title: '收货人姓名',
dataIndex: 'receiverName',
width: 200,
const { Countdown } = Statistic;
export const TAB_MAPPING_DATA = {
'': {},
1: {
label: '仅退款(未发货)待审核',
type: 0,
dealStatus: 14,
},
{
title: '收货人手机号',
dataIndex: 'receiverPhone',
width: 200,
2: {
label: '仅退款待审核',
type: 1,
dealStatus: 14,
},
{
title: '收货人地址',
dataIndex: 'receiveAddress',
width: 200,
hideInSearch: true,
3: {
label: '退货退款待审核',
type: 2,
dealStatus: 14,
},
{
title: '订单开始时间',
width: 120,
dataIndex: 'startDate',
key: 'startDate',
valueType: 'date',
hideInTable: true,
4: {
label: '退货入库待审核',
dealStatus: 40,
},
{
title: '订单结束时间',
width: 120,
dataIndex: 'endDate',
key: 'endDate',
valueType: 'date',
hideInTable: true,
5: {
label: '已完成',
dealStatus: 70,
},
};
// 售后类型
const AFTER_TYPE = [
{
title: '售后原因',
dataIndex: 'serviceReason',
hideInSearch: true,
width: 200,
value: 0,
name: '仅退款(未发货)',
},
{
title: '售后发生时间',
dataIndex: 'serviceTime',
hideInSearch: true,
width: 200,
value: 1,
name: '仅退款',
},
{
title: '超时时间',
dataIndex: 'overtime',
hideInSearch: true,
width: 200,
value: 2,
name: '退货退款',
},
];
// 售后状态
const AFTER_STATUS = [
{
title: '是否催办',
dataIndex: 'reminderFlag',
hideInSearch: true,
width: 120,
valueEnum: {
true: '',
false: '',
},
value: 14,
name: '商户审核中',
},
{
title: '是否同意售后',
dataIndex: 'isAgree',
hideInSearch: true,
width: 120,
value: 16,
name: '商户审核拒绝',
},
{
title: '拒绝原因',
dataIndex: 'refuseReason',
hideInSearch: true,
width: 200,
value: 30,
name: '待填写退货物流信息',
},
];
export const columnPassAudit = [
...columnSticData,
{
title: '商家退货地址',
dataIndex: 'merchantAddress',
hideInSearch: true,
width: 200,
value: 40,
name: '待退货入库',
},
{
title: '退回物流',
dataIndex: 'expressCompanyName',
hideInSearch: true,
width: 150,
value: 50,
name: '退货拒收',
},
{
title: '退回物流单号',
dataIndex: 'deliveryNo',
hideInSearch: true,
width: 200,
value: 70,
name: '退款成功',
},
{
title: '售后状态',
dataIndex: 'serviceStatus',
hideInSearch: true,
width: 120,
value: 99,
name: '用户撤销',
},
];
export const getFormConfig = (props = {}) => {
const { setTableParams, setCurrentTab, tableParams, actionRef } = 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,
originOptions: {
allowClear: true,
},
},
{
type: SEARCH_TYPE.SELECT,
label: '售后类型',
column: 5,
bindKey: 'type',
options: AFTER_TYPE,
originOptions: {
allowClear: true,
},
},
{
type: SEARCH_TYPE.INPUT,
label: '收货人姓名',
column: 5,
bindKey: 'receiverName',
},
{
type: SEARCH_TYPE.INPUT,
label: '收货人手机号',
column: 5,
bindKey: 'receiverPhone',
},
{
type: SEARCH_TYPE.RANGE_PICKER,
label: '售后日期',
column: 5,
bindKey: 'afterTime',
limit: {
rangeNum: 3,
rangeUnit: 'months',
},
originOptions: {
placeholder: ['开始日期', '结束日期'],
allowClear: false,
},
},
],
btnConfig: [
{
label: '筛选',
clickType: 'submit',
onClick: ({ params }) => {
// 参数相同,直接执行刷新
if (JSON.stringify(params) === JSON.stringify(tableParams)) {
actionRef.current.reload();
return;
}
const { type, dealStatus } = params;
let hasMatchingKey = '';
Object.keys(TAB_MAPPING_DATA).forEach(key => {
const item = TAB_MAPPING_DATA[key];
if (type === item.type && dealStatus === item.dealStatus) {
hasMatchingKey = key;
}
});
setCurrentTab(hasMatchingKey);
setTableParams(params);
},
},
{
label: '重置',
type: '',
clickType: 'reset',
onClick: ({ params }) => {
setTableParams({});
actionRef.current.reload();
setCurrentTab('');
},
},
],
};
};
export const appealType = {
1: '已申诉',
0: '未申诉',
};
export const getColumns = props => {
const {
openAudit,
// viewAppeal,
viewProofs,
openLogistics,
viewDetail,
viewLog,
handleCom,
refund,
reject,
canEditable,
openCancelAudit,
viewCancelDetail,
tableParams,
} = props;
const { dealStatus } = tableParams;
let auditCountDown = [
{
title: '审核倒计时',
dataIndex: 'serviceTime',
key: 'serviceTime',
hideInSearch: true,
width: 150,
render: (val, record) => {
if (record.status !== 14) {
return <div align="center">-</div>;
}
const serviceTime = moment(record.approvalEndTime).valueOf();
return (
<Countdown
format="HH时mm分ss秒"
value={serviceTime}
valueStyle={{ color: 'red', fontSize: '14px' }}
/>
);
},
},
];
// 售后状态:已完成/退款成功不展示审核倒计时
if (dealStatus === 70) {
auditCountDown = [];
}
return [
...auditCountDown,
{
title: '售后状态',
dataIndex: 'serviceStatus',
hideInSearch: true,
width: 120,
},
{
title: '售后类型',
dataIndex: 'serviceType',
hideInSearch: true,
width: 140,
render: serviceType => {
if (+serviceType === 0) {
return <span style={{ color: '#ff1616' }}>仅退款(未发货)</span>;
}
if (+serviceType === 1) {
return (
<span style={{ color: '#ff1616' }}>
{/* {AFTER_TYPE.find(item=> item.value === serviceType)} */}
仅退款
</span>
);
}
return <span>退货退款</span>;
},
},
{
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: 'receiverName',
width: 200,
render: (_, record) => {
const { receiverPhone, receiveAddress } = record;
return (
<>
<p>{_}</p>
<p>{receiverPhone}</p>
<p>{receiveAddress}</p>
</>
);
},
},
{
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: 'merchantAddress',
hideInSearch: true,
width: 200,
render: (_, record) => {
const { expressCompanyName, deliveryNo } = record;
return (
<>
<p>{_}</p>
<p>{expressCompanyName}</p>
<p>{deliveryNo}</p>
</>
);
},
},
{
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, record) => {
const {
status,
supplierType,
serviceType,
intercept,
showRefuse,
showRefund,
showLogistics,
showRefunded,
isReceived,
} = record;
// 是否是服务类商品
const isServiceGoods = ['pop'].includes(supplierType);
// 按钮通用属性
const btnProps = {
className: 'mr10 mt10',
type: 'primary',
};
const refundBtnProps = {
title: '确定允许退款?',
okText: '确认',
cancelText: '取消',
key: 'pop',
};
/** @name applyTpye=1 */
// 允许退款/已退款按钮
const refundBtn = (
<Popconfirm {...refundBtnProps} onConfirm={() => refund(record)} disabled={!showRefund}>
<Button key="link1" {...btnProps} disabled={!showRefund}>
{showRefunded ? '已退款' : '允许退款'}
</Button>
</Popconfirm>
);
// 审核按钮
let auditBtn = (
<Button key="link2" onClick={() => openAudit(record)} {...btnProps}>
审核
</Button>
);
// 驳回按钮
let refuseBtn = (
<Button key="link3" onClick={() => reject(record)} {...btnProps} disabled={!showRefuse}>
驳回
</Button>
);
// 物流拦截按钮
let logisticsInterceptionBtn = (
<Button
key="link7"
onClick={() => openLogistics(record)}
{...btnProps}
disabled={serviceType !== 1 || (serviceType === 1 && intercept)}
>
物流拦截
</Button>
);
// 订单详情
const detailBtn = (
<Button key="link4" onClick={() => viewDetail(record)} {...btnProps}>
订单详情
</Button>
);
// 查看物流
const viewLogisticsBtn = (
<Button
key="link5"
onClick={() => handleCom(record)}
{...btnProps}
disabled={!showLogistics}
>
退货物流
</Button>
);
// 查看记录
const viewLogBtn = (
<Button key="link6" onClick={() => viewLog(record)} {...btnProps}>
查看记录
</Button>
);
let cancelAuditBtn = (
<Button key="cancellink1" onClick={() => openCancelAudit(record)} {...btnProps}>
审核
</Button>
);
const cancelDetailBtn = (
<Button key="cancellink2" onClick={() => viewCancelDetail(record)} {...btnProps}>
订单详情
</Button>
);
// 仅退款状态 && 用户已收到货 不展示物流拦截按钮
if (serviceType === 1 && isReceived === 1) {
logisticsInterceptionBtn = '';
}
// 无权限 不展示审核、驳回、物流拦截
if (!canEditable) {
auditBtn = '';
refuseBtn = '';
logisticsInterceptionBtn = '';
cancelAuditBtn = '';
}
// 服务商品
if (isServiceGoods) {
// 待商户审核14
if ([14].includes(status)) {
return [auditBtn, detailBtn, viewLogBtn];
}
// 拒绝16, 处理成功70
if ([16, 70].includes(status)) {
return [detailBtn, viewLogBtn];
}
}
// 实物商品-仅退款未发货 serviceType = 0
if (serviceType === 0) {
// 待商户审核14 待审核0
if ([14, 0].includes(status)) {
return [cancelAuditBtn, cancelDetailBtn, viewLogBtn];
}
// 拒绝16, 处理成功70
if ([16, 70].includes(status)) {
return [cancelDetailBtn, viewLogBtn];
}
}
// 实物商品-仅退款 serviceType = 1
if (serviceType === 1) {
// 待商户审核14
if ([14].includes(status)) {
return [auditBtn, detailBtn, viewLogBtn, logisticsInterceptionBtn];
}
// 拒绝16/处理成功70
if ([16, 70].includes(status)) {
return [detailBtn, viewLogBtn];
}
}
// 实物商品-退货退款 serviceType = 2
if (serviceType === 2) {
// 待商户审核14
if ([14].includes(status)) {
return [auditBtn, detailBtn, viewLogBtn];
}
// 待退货入库40
if ([40].includes(status)) {
return [refundBtn, refuseBtn, viewLogisticsBtn, detailBtn, viewLogBtn];
}
// 拒绝16/待填写退货物流信息30/处理成功70
if ([16, 30, 70].includes(status)) {
return [viewLogisticsBtn, detailBtn, viewLogBtn];
}
}
return [detailBtn, viewLogBtn];
},
},
];
};
import { Tabs } from 'antd';
import React from 'react';
import React, { useRef, useState, useEffect } 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 Pending from './Pending';
import PassAudit from './PassAudit';
import { FormSearch } from '@/components/FormSearch';
import moment from 'moment';
import { useLocation } from 'react-router-dom';
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 RejectModal from './components/rejectModal';
import LogisticsCom from '../orderManage/pendingDeliveryOrder/components/LogisticsCom';
import CancelAuditModal from './components/CancelAuditModal';
import CancelDetailTable from './components/CancelDetailTable';
import { getColumns, getFormConfig, TAB_MAPPING_DATA } from './data.js';
import { getDetail } from '@/pages/afterSale/appeal/services';
import { getClientInfo } from '@/utils/utils';
import {
searchList,
logisticsIntercept,
orderDetail,
getOpLog,
auditInfoApi,
trackInfo,
shopCheck,
getAfterPendingNum,
cancelOrderDetail,
} from './services';
import styles from './index.less';
const { TabPane } = Tabs;
const { confirm } = Modal;
function AfterSale(props) {
const AfterSale = props => {
const { permissions } = props;
const canEditable = permissions[AFTER_SALE_ORDER.EDITABLE];
const location = useLocation();
const actionRef = useRef();
const formRef = useRef();
const [tableParams, setTableParams] = useState({});
const [currentTab, setCurrentTab] = useState('');
// const [appealDetailModal, setAppealDetailModal] = useState(false);
// const [selectedRow, setSelectedRow] = useState({});
const [tabInfoData, setTabInfoData] = useState({
cancelOrderPendingAuditNum: 0,
refundPendingAuditNum: 0,
returnPendingAuditNum: 0,
returnWaitAuditNum: 0,
afterSalseDoneNum: 0,
});
const scrollYRef = useRef();
const [tableScrollY, setTableScrollY] = useState(0);
// 申诉
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({});
// 查看物流
const [LogisticsComList, setLogisticsComList] = useState({});
const [logisticsComModalVisible, setLogisticsComModalVisible] = useState(false);
// 驳回
const [serviceNoInfo, setServiceNoInfo] = useState({});
const [rejectVisible, setRejectVisible] = useState(false);
// 取消订单审核
const [cancelVisible, setCancelVisible] = useState(false);
const [cancelAuditInfo, setCancelAuditInfo] = useState({});
// 取消订单审核详情
const [cancelDetailVisible, setCancelDetailVisible] = useState(false);
const [cancelDetailInfo, setCancelDetailInfo] = 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);
setLogisticsComModalVisible(false);
setCancelVisible(false);
setCancelDetailVisible(false);
setRejectVisible(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, orderNo }) => {
const data = await auditInfoApi({ serviceNo });
setAuditInfo({ ...data?.data, serviceNo, serviceType, orderNo });
setVisible(true);
};
// 查看物流
const handleCom = async ({ expressCompanyCode, deliveryNo }) => {
const tempObj = {
detailList: [],
key: Date.now(),
};
const data = await trackInfo({ expressCompanyCode, logisticsNo: deliveryNo });
if (!data) {
notification.info({ message: '暂无物流信息' });
return;
}
tempObj.expressCompanyName = data.logisticsName;
tempObj.deliveryNo = data.logisticsBillNo;
if (data.logisticsList?.length) {
data.logisticsList.forEach(v => {
tempObj.detailList = [...tempObj.detailList, ...v.detailList];
});
}
setLogisticsComModalVisible(true);
setLogisticsComList(tempObj);
};
// 物流拦截
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 refund = async ({ serviceNo }) => {
const data = await shopCheck({
serviceNo,
auditResult: 1,
});
if (data.businessCode === '0000') {
notification.success({ message: '操作成功' });
closeModal(true);
}
};
// 驳回
const reject = async ({ serviceNo }) => {
setServiceNoInfo(serviceNo);
setRejectVisible(true);
};
// 取消订单审核
const openCancelAudit = async ({ serviceNo }) => {
// const data = await auditInfoApi({ serviceNo });
setCancelAuditInfo({ serviceNo });
setCancelVisible(true);
};
// 取消订单审核详情
const viewCancelDetail = async ({ serviceNo }) => {
const data = await cancelOrderDetail({ serviceNo });
setCancelDetailInfo(data || []);
setCancelDetailVisible(true);
};
const [form] = Form.useForm();
const formConfig = getFormConfig({
actionRef,
tableParams,
setTableParams,
setCurrentTab,
});
const afterTime = [moment().subtract(3, 'month'), moment()];
const FormSearchProps = {
form,
initialValues: {
afterTime,
},
formOptions: {},
...formConfig,
};
// tab选项框回调
const tabChange = tabIndex => {
setCurrentTab(tabIndex);
const { type, dealStatus } = TAB_MAPPING_DATA[tabIndex];
form.resetFields();
form.setFieldsValue({
...tableParams,
dealStatus,
type,
});
setTableParams({ ...tableParams, dealStatus, type });
};
// 监听路由query参数变化重新发起请求
useEffect(() => {
if (location?.query) {
const { orderNo: orderNumber, tab } = location?.query;
if (orderNumber) {
tabChange('');
}
const params = {
orderNo: orderNumber,
dealStatus: null,
serviceNo: null,
type: null,
receiverPhone: null,
receiverName: null,
};
form.setFieldsValue(params);
setTableParams(params);
if (tab) {
tabChange(tab);
}
if (actionRef.current) {
actionRef.current.reload();
}
}
}, [location]);
// 动态计算表格滚动区域高度
const clientInfo = getClientInfo();
const calculationLayoutHeight = e => {
const otherHeight = typeof e === 'number' ? e : 330;
const h = clientInfo.height - scrollYRef.current.offsetTop - otherHeight;
setTableScrollY(h < 500 ? '500px' : h);
};
useEffect(() => {
calculationLayoutHeight(330);
}, []);
window.removeEventListener('resize', calculationLayoutHeight);
window.addEventListener('resize', calculationLayoutHeight);
const columns = getColumns({
openAudit,
// viewAppeal,
viewProofs,
openLogistics,
viewDetail,
viewLog,
handleCom,
refund,
reject,
canEditable,
openCancelAudit,
viewCancelDetail,
tableParams,
});
// 表格属性
const tableProps = {
columns,
params: tableParams,
bordered: true,
scroll: { x: '100%', y: tableScrollY },
rowKey: r => r.serviceNo,
request: async params => {
const [start, end] = params.afterTime || afterTime;
const { current: page, pageSize: size } = params;
const startDate = start ? moment(start).format('YYYY-MM-DD') : '';
const endDate = end ? moment(end).format('YYYY-MM-DD') : '';
const requestParams = {
page,
size,
...params,
startDate,
endDate,
};
delete requestParams.afterTime;
delete requestParams.current;
delete requestParams.pageSize;
const countRes = await getAfterPendingNum({
startDate,
endDate,
});
const res = await searchList(requestParams);
const { records = [], total } = res.data;
if (countRes.code === '0000') {
setTabInfoData(countRes.data);
}
return {
data: records,
total,
};
},
toolBarRender: null,
};
const TabCountElement = ({ count, text }) => (
<span>
{text}
<span className="count">{count}</span>
</span>
);
return (
<PageHeaderWrapper>
<Tabs defaultActiveKey="1">
<TabPane tab="未审核" key="1">
<Pending canEditable={canEditable} />
</TabPane>
<TabPane tab="已审核" key="2">
<PassAudit canEditable={canEditable} />
</TabPane>
</Tabs>
<FormSearch {...FormSearchProps} />
<div className={styles['tab-box']}>
<Tabs
activeKey={currentTab}
onChange={tabChange}
size="large"
tabBarStyle={{ padding: '0 30px 0 15px', border: 'none' }}
>
<TabPane key="" tab="全部"></TabPane>
<TabPane
key="1"
tab={
<TabCountElement
text="仅退款(未发货)待审核"
count={tabInfoData.cancelOrderPendingAuditNum}
/>
}
/>
<TabPane
key="2"
tab={<TabCountElement text="仅退款待审核" count={tabInfoData.refundPendingAuditNum} />}
/>
<TabPane
key="3"
tab={
<TabCountElement text="退货退款待审核" count={tabInfoData.returnPendingAuditNum} />
}
/>
<TabPane
key="4"
tab={<TabCountElement text="退货入库待审核" count={tabInfoData.returnWaitAuditNum} />}
/>
<TabPane
key="5"
tab={<TabCountElement text="已完成" count={tabInfoData.afterSalseDoneNum} />}
/>
</Tabs>
</div>
<div ref={scrollYRef}></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} />
<LogisticsCom
onSubmit={closeModal}
onCancel={closeModal}
modalVisible={logisticsComModalVisible}
value={LogisticsComList}
key={LogisticsComList.key}
/>
{/* <AppealDetail
data={selectedRow}
modalVisible={appealDetailModal}
onCancel={closeModal}
></AppealDetail> */}
<AfterLog visible={afterVisible} onCancel={closeModal} data={afterList} />
<RejectModal visible={rejectVisible} onCancel={closeModal} serviceNo={serviceNoInfo} />
<CancelAuditModal visible={cancelVisible} onCancel={closeModal} formData={cancelAuditInfo} />
<CancelDetailTable
visible={cancelDetailVisible}
onCancel={closeModal}
dataSource={cancelDetailInfo}
/>
</PageHeaderWrapper>
);
}
};
export default connect(({ menu }) => ({
permissions: menu.permissions,
......
.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 {
position: relative;
z-index: 2;
width: calc(100% - 100px);
// margin-top: 20px;
// background-color: #fff;
margin-top: 20px;
margin-bottom: -80px;
:global .ant-tabs-nav::before {
display: none;
}
:global .ant-tabs-tab {
.count {
padding-left: 5px;
color: #888;
transition: 0.3s ease;
}
&:hover .count {
color: #1890ff;
transition: 0.3s ease;
}
}
:global .ant-tabs-tab-active {
.count {
color: #1890ff;
}
}
}
......@@ -6,35 +6,55 @@ import _ from 'lodash';
const { kdspApi } = config;
// 分页查询所有数据
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', {
export const searchList = params =>
request.post('/api/kdsp/op/afs/shop/list', {
prefix: kdspApi,
data: stringify(_.omitBy(param, v => !v)),
data: stringify(_.omitBy(params, v => !v && v !== 0)),
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 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', {
export function orderDetail(params) {
return request.get('/api/kdsp/op/afs/sku', {
prefix: kdspApi,
params,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
}
// 取消售后单详情
export async function cancelOrderDetail(params) {
const data = await request.get('/cancel-order/sku', {
prefix: kdspApi,
params,
headers: {
......@@ -43,6 +63,7 @@ export async function orderDetail(params) {
});
return data.data || [];
}
// 售后审核
export async function shopAudit(params) {
return request.post('/api/kdsp/op/afs/shop/audit', {
......@@ -50,6 +71,15 @@ export async function shopAudit(params) {
prefix: kdspApi,
});
}
// 取消订单审核
export async function cancelShopAudit(params) {
return request.post('/cancel-order/audit', {
data: params,
prefix: kdspApi,
});
}
// 查询审核信息
export async function auditInfoApi(params) {
return request.get('/api/kdsp/op/afs/back-info', {
......@@ -93,3 +123,18 @@ export async function getOpLog(params) {
prefix: kdspApi,
});
}
// 查询物流记录信息
export async function getLogisticsRecord(params) {
return request.get(`/api/kdsp/op/logistics/kd100/track-list?orderNo=${params.orderNo}`, {
prefix: kdspApi,
});
}
// 查询售后待办数量
export function getAfterPendingNum(data) {
return request.post('/api/kdsp/op/afs/getPendingNum', {
prefix: kdspApi,
data,
});
}
/* 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 '../styles.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 '../styles.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 '../styles.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 } from 'antd';
import React from 'react';
export const appealType = {
1: '已申诉',
0: '未申诉',
};
export const columnSticData = [
{
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,
},
];
export const columnPassAudit = [
...columnSticData,
{
title: '商家退货地址',
dataIndex: 'merchantAddress',
hideInSearch: true,
width: 200,
},
{
title: '退回物流',
dataIndex: 'expressCompanyName',
hideInSearch: true,
width: 150,
},
{
title: '退回物流单号',
dataIndex: 'deliveryNo',
hideInSearch: true,
width: 200,
},
{
title: '售后状态',
dataIndex: 'serviceStatus',
hideInSearch: true,
width: 120,
},
];
import { Tabs } from 'antd';
import React from 'react';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import { AFTER_SALE_ORDER } from '@/../config/permission.config';
import { connect } from 'dva';
import Pending from './Pending';
import PassAudit from './PassAudit';
const { TabPane } = Tabs;
function AfterSale(props) {
const { permissions } = props;
const canEditable = permissions[AFTER_SALE_ORDER.EDITABLE];
return (
<PageHeaderWrapper>
<Tabs defaultActiveKey="1">
<TabPane tab="未审核" key="1">
<Pending canEditable={canEditable} />
</TabPane>
<TabPane tab="已审核" key="2">
<PassAudit canEditable={canEditable} />
</TabPane>
</Tabs>
</PageHeaderWrapper>
);
}
export default connect(({ menu }) => ({
permissions: menu.permissions,
}))(AfterSale);
import request from '@/utils/request';
import config from '../../../config/env.config';
import { stringify } from 'qs';
import _ from 'lodash';
const { kdspApi } = config;
// 分页查询所有数据
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,
});
}
......@@ -59,7 +59,7 @@ const FormCai = forwardRef((props, ref) => {
name={[name, 'dishName']}
rules={[{ required: true, message: '请输入菜名.' }]}
>
<Input maxLength={10} placeholder="最多10个字符" />
<Input maxLength={13} placeholder="最多13个字符" />
</Form.Item>
</Col>
<Col span={2}>
......@@ -312,8 +312,8 @@ const FormPackage = forwardRef((props, ref) => {
onCancel={() => setVisibleGroup(false)}
>
<Input
placeholder="请输入分组名称,最多10个字"
maxLength={10}
placeholder="请输入分组名称,最多13个字"
maxLength={13}
onChange={e => setGroupName(e.target.value)}
/>
</Modal>
......
......@@ -223,7 +223,11 @@ const StoreModal = props => {
onCancel={() => handleCancel()}
>
<Form {...layout} name="formData">
<FormItem label="门店名称" name="name">
<FormItem
label="门店名称"
name="name"
extra="门店名称由商家名称+门店地址组成,示例:优衣库(三里屯店)"
>
{getFieldDecorator('name', {
rules: [{ required: true, message: '请输入门店名称!' }],
initialValue: formData.name,
......@@ -286,7 +290,7 @@ const StoreModal = props => {
{getFieldDecorator('address', {
rules: [{ required: true, message: '请输入详细地址!' }],
initialValue: formData.address,
})(<Input placeholder="请输入详细地址" allowClear maxLength={50} />)}
})(<Input placeholder="请输入详细地址,不包含省市区地址" allowClear maxLength={50} />)}
</FormItem>
<FormItem label="经纬度">
{getFieldDecorator('lnglat', {
......
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 { useLocation } from 'react-router-dom';
import ProTable from '@ant-design/pro-table';
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';
......@@ -23,8 +24,10 @@ import {
const { confirm } = Modal;
const TableList = props => {
const location = useLocation();
const dateFormat = 'YYYY-MM-DD';
const { permissions } = props;
// 因待发货 和 已发货都使用当前组件,功能且相同,所以只要授权其中一个均可获得发货物流权限
const canEditable =
permissions[PENDING_DELIVERY_ORDER.EDITABLE] || permissions[DELIVERY_ORDER.EDITABLE];
......@@ -44,6 +47,7 @@ const TableList = props => {
const [endTime, setEndTime] = useState(moment());
const [orderStatus, setorderStatus] = useState(1);
const [orderNo, setOrderNo] = useState('');
const [endTimeStr, setEndTimeStr] = useState(moment().format(dateFormat));
const [visibleDelay, setVisibleDelay] = useState(false);
......@@ -133,6 +137,15 @@ const TableList = props => {
return '';
};
const PopoverNotice = noticeProps => {
const { time, content } = noticeProps || {};
return (
<div className={style['popover-notice']}>
{time}{content}
</div>
);
};
const columns = [
{
title: '订单ID',
......@@ -141,6 +154,81 @@ const TableList = props => {
order: 6,
width: 250,
fixed: 'left',
formItemProps: {
value: orderNo,
onChange(e) {
console.log(e.target.value);
setOrderNo(e.target.value);
},
},
render: (_, record) => {
const { urgeDeliverGoodsList } = record;
const content = () =>
urgeDeliverGoodsList?.map(item => (
<PopoverNotice time={item?.time} content={item?.content} />
));
return (
<div className={style['urge-box']}>
{_}
{urgeDeliverGoodsList ? (
<Popover placement="top" title="催发货" content={<>{content()}</>} trigger="hover">
<span className={style.urge}>催单</span>
</Popover>
) : (
''
)}
</div>
);
},
},
{
title: '订单提醒',
dataIndex: 'orderNotice',
key: 'orderNotice',
width: 150,
render: (_, record) => {
const { updateAddressList, platformRemark } = record;
// let addressData;
// if (updateAddressList) {
// addressData = updateAddressList.pop();
// }
const content = () => (
<div className={style['popover-notice-box']}>
{updateAddressList?.map(item => (
<PopoverNotice time={item?.time} content={item?.content} />
))}
</div>
);
return (
<div className={style['notice-btn']}>
{updateAddressList ? (
<Popover placement="top" title="收货地址变更" content={content()} trigger="hover">
<Button block type="primary">
收货地址变更
</Button>
</Popover>
) : (
''
)}
{platformRemark ? (
<Popover
placement="top"
title="平台备注"
content={
<PopoverNotice time={platformRemark?.time} content={platformRemark?.content} />
}
trigger="hover"
>
<Button block type="warning">
平台备注
</Button>
</Popover>
) : (
''
)}
</div>
);
},
},
{
title: '商品名称',
......@@ -156,7 +244,7 @@ const TableList = props => {
hideInTable: true,
formItemProps: {
// eslint-disable-next-line no-nested-ternary
value: orderStatus ? (orderStatus === 1 ? '' : '') : '',
value: orderStatus ? (orderStatus === 1 ? '' : '') : null,
onChange(status) {
setorderStatus(Number(status));
},
......@@ -364,6 +452,23 @@ const TableList = props => {
}
}, []);
// 监听路由query参数变化重新发起请求
useEffect(() => {
if (location?.query) {
const { orderNo: orderNumber } = location?.query;
if (orderNumber) {
setorderStatus('');
}
setOrderNo(orderNumber);
if (ref.current) {
ref.current.resetFields();
ref.current.submit();
}
} else {
setOrderNo('');
}
}, [location]);
const queryToSendFn = params => {
const transformedParam = {
...params,
......@@ -371,6 +476,7 @@ const TableList = props => {
pageNo: params.current,
pageSize: params.pageSize || 20,
timeOutType: orderStatus || '',
orderNo,
};
return queryToSend(transformedParam);
};
......@@ -393,6 +499,7 @@ const TableList = props => {
// eslint-disable-next-line no-unused-expressions
form?.resetFields();
setorderStatus('');
setOrderNo('');
// eslint-disable-next-line no-unused-expressions
form?.submit();
}}
......@@ -406,7 +513,11 @@ const TableList = props => {
type="primary"
onClick={() => {
if (ref.current) {
const obj = { ...ref.current.getFieldsValue(), logisticsStatus: props.type || 1 };
const obj = {
...ref.current.getFieldsValue(),
orderNo,
logisticsStatus: props.type || 1,
};
obj.startTime = startTimeStr;
obj.endTime = endTimeStr;
downOrder(obj);
......
@import '~antd/es/style/themes/default.less';
.protable {
:global {
.tableContent {
display: flex;
align-items: center;
height: 60px;
padding: 16px;
}
.border {
border-bottom: 1px solid #e8e8e8;
}
tbody .colStyle {
padding: 0;
}
.ant-popover-title {
max-width: 300px;
overflow: hidden;
}
}
}
.tabpane {
:global {
.ant-tabs-content {
min-height: 200px;
max-height: 700px;
overflow: auto;
}
}
}
.btnWarning {
background-color: rgb(247, 143, 74);
border-color: rgb(247, 143, 74);
}
.btnWarning:hover,
.btnWarning:focus {
background-color: rgb(253, 168, 111);
border-color: rgb(253, 168, 111);
}
.tableList {
.tableListOperator {
margin-bottom: 16px;
......@@ -73,3 +112,42 @@
margin-right: 10px;
}
}
.notice-btn {
max-width: 120px;
margin: 0 auto;
text-align: center;
button {
margin: 5px 0;
}
}
// 催发货图标
.urge-box {
white-space: nowrap;
}
.urge {
display: inline-block;
width: 36px;
height: 17px;
margin-left: 5px;
color: #ff1515;
font-size: 12px;
line-height: 16px;
text-align: center;
border: 1px solid #ff1515;
border-radius: 18px;
cursor: pointer;
}
.popover-notice-box {
max-height: 300px;
margin-right: -15px;
padding-right: 15px;
overflow: auto;
}
.popover-notice {
max-width: 400px;
line-height: 32px;
word-break: break-all;
}
.protable {
:global {
.tableContent {
display: flex;
align-items: center;
height: 60px;
padding: 16px;
}
.border {
border-bottom: 1px solid #e8e8e8;
}
tbody .colStyle {
padding: 0;
}
.ant-popover-title {
max-width: 300px;
overflow: hidden;
}
}
}
.tabpane {
:global {
.ant-tabs-content {
min-height: 200px;
max-height: 700px;
overflow: auto;
}
}
}
.btnWarning {
background-color: rgb(247, 143, 74);
border-color: rgb(247, 143, 74);
}
.btnWarning:hover,
.btnWarning:focus {
background-color: rgb(253, 168, 111);
border-color: rgb(253, 168, 111);
}
import request from '@/utils/request';
import config from '../../config/env.config';
const { msgApi, wsApi } = config;
export const getSocketUrl = ({ token, channelId }) =>
`${wsApi}/ws?token=${token}&channelId=${channelId}`;
/**
* @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,
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';
......@@ -92,3 +92,21 @@ export function deepClone(obj) {
}
return result;
}
export const getClientInfo = () => {
if (window.innerHeight !== undefined) {
return {
width: window.innerWidth,
height: window.innerHeight,
};
}
if (document.compatMode === 'CSS1Compat') {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
};
}
return {
width: document.body.clientWidth,
height: document.body.clientHeight,
};
};
import EventEmitter from 'events';
import { Modal } from 'antd';
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 = [];
this.reconnectCount = 0;
this.heartBeatTimer = null;
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();
}
clearTimeout(time);
}, timeout);
}
};
// 连接成功触发
onopen = () => {
const { heartBeatTime } = this.options;
console.log('ws: 连接成功', new Date().getTime());
this.connected = true;
this.heartBeat(heartBeatTime);
this.checkWaitingData();
this.event.emit('open');
};
// 后端向前端推得数据
onmessage = msg => {
console.log('ws:接收数据:', msg);
this.event.emit('message', msg);
// 打印出后端推得数据
};
// 关闭连接触发
onclose = e => {
console.log('ws: 关闭连接', new Date().getTime());
this.connected = false; // 关闭将标识符改为true
if (this.heartBeatTimer) {
clearTimeout(this.heartBeatTimer);
}
this.event.emit('close', e);
// 最多重连10次
if (this.reconnectCount > 10) {
this.reconnectCount = 0;
return;
}
const reconnect = () => {
if (this.taskRemindInterval) {
clearTimeout(this.taskRemindInterval);
}
this.taskRemindInterval = setTimeout(() => {
if (!this.connected) {
this.reconnectCount++;
this.connection();
reconnect();
}
}, 5000);
};
reconnect();
};
onerror = e => {
console.log('ws: error', e);
this.socket = null;
this.event.emit('error', e);
};
sendMessage = value => {
console.log('ws: send', value);
// 向后端发送数据
if (this.socket) {
if (!this.connected) {
this.waitingSendData.unshift(value);
return;
}
const sendValue = typeof value === 'string' ? value : JSON.stringify(value);
this.socket.send(sendValue);
}
};
checkWaitingData() {
console.log('ws: checkWaitingData', this.waitingSendData);
if (this.waitingSendData.length) {
this.sendMessage(this.waitingSendData.splice(0, 1));
this.checkWaitingData();
}
}
// 保持连接-默认每3分钟重置一下服务器关闭时间
heartBeat(time) {
console.log('ws: call heartBeat', new Date().getTime());
this.heartBeatTimer = setTimeout(() => {
console.log('ws: sent heart beat', new Date().getTime());
this.sendMessage('HeartBeat');
this.heartBeat(time);
}, time || 45000);
}
}
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