Commit e8408255 authored by 武广's avatar 武广

Merge branch 'feature/brand' into 'master'

Feature/brand

See merge request !100
parents d277d3b9 f5f5d732
...@@ -20,6 +20,7 @@ yarn-error.log ...@@ -20,6 +20,7 @@ yarn-error.log
yarn.lock yarn.lock
*bak *bak
.vscode .vscode
.scannerwork
# visual studio code # visual studio code
.history .history
......
...@@ -288,6 +288,12 @@ export default { ...@@ -288,6 +288,12 @@ export default {
name: 'businessInfo', name: 'businessInfo',
component: './businessManage/info', component: './businessManage/info',
}, },
{
title: '商户管理后台-品牌管理',
path: '/brandManage',
name: 'brandManage',
component: './BrandManage',
},
...groupMealRoute, ...groupMealRoute,
{ {
component: './404', component: './404',
......
import RoleType, { isPlatForm } from './role.config'; import RoleType, { isPlatForm } from './role.config';
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
const isPre = process.env.PRE_ENV === 'pre'; const isPre = process.env.PRE_ENV === 'pre';
const environment = 'yxm2'; const environment = 'sc';
const envAPi = { const envAPi = {
api: `https://security-${environment}.liangkebang.net`, //'https://security-xyqb.liangkebang.net', api: `https://security-${environment}.liangkebang.net`, //'https://security-xyqb.liangkebang.net',
......
import { DeleteOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons';
import { Modal, Upload, notification, Spin } from 'antd';
import React, { useState, useEffect, useRef, forwardRef } from 'react';
import lodash from 'lodash';
import { ReactSortable } from 'react-sortablejs';
import { merchantUpload } from './service.js';
import styles from './style.less';
const MAX_FILE_SIZE = 5;
const UNIT = 1024 * 1024;
const UploadButton = (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>上传图片</div>
</div>
);
const UploadImage = forwardRef((props, ref) => {
const {
name = `${Date.now()}`,
limit = null,
multiple = true,
disabled,
uploadParams,
value,
width = 0,
height = 0,
accept = ['jpg', 'jpeg', 'png'],
} = props;
const [uploadLoading, setUploadLoading] = useState(false);
const [previewVisible, setPreviewVisible] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [previewTitle, setPreviewTitle] = useState('');
const [fileList, setFileList] = useState([]);
const [activeImgIndex, setActiveImgIndex] = useState(null);
const fileListRef = useRef([]);
useEffect(() => {
const newPictures = (value || []).map((url, ind) => ({
url,
name: url,
uid: `${ind}`,
}));
fileListRef.current = [...newPictures];
setFileList([...newPictures]);
}, [value]);
const handleCancel = () => setPreviewVisible(false);
const handlePreview = async file => {
setPreviewImage(file.url);
setPreviewVisible(true);
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
};
const bundleChange = imgFile => {
const imgList = imgFile.map(item => item.url);
props.onChange(imgList);
};
const handleRemove = file => {
const freshFiles = fileList?.filter(ele => ele.uid !== file.uid);
bundleChange(freshFiles);
};
const checkFile = file =>
new Promise(resolve => {
const curType = file.name.substr(file.name.lastIndexOf('.') + 1).toLowerCase();
if (!accept.includes(curType)) {
notification.open({
message: file.name,
description: `图片格式须为${accept.join('')}!`,
});
return resolve(null);
}
if (file.size > MAX_FILE_SIZE * UNIT) {
notification.open({
message: file.name,
description: `单个图片大小不能超过${MAX_FILE_SIZE}M!`,
});
return resolve(null);
}
return resolve(file);
});
const imageLoading = (file, ret) =>
new Promise(resolve => {
const reader = new FileReader();
// 监听图片转换完成
reader.addEventListener(
'load',
() => {
const temFile = { uid: file.uid, status: 'done', name: file.name, url: ret };
resolve(temFile);
},
false,
);
reader.readAsDataURL(file);
});
const getImageSize = async file =>
new Promise((resolve, reject) => {
const fileObj = file;
// 获取上传的图片的宽高
const reader = new FileReader();
reader.readAsDataURL(fileObj);
reader.onload = evt => {
const replaceSrc = evt.target.result;
const imageObj = new Image();
imageObj.src = replaceSrc;
imageObj.onload = () => {
const { width: widthPx, height: heightPx } = imageObj;
file.width = widthPx;
file.height = heightPx;
resolve(file);
};
};
reader.onerror = error => reject(error);
});
const validateImageSize = async files => {
const fileAll = files.map(item => getImageSize(item));
const checkFiles = (await Promise.all(fileAll)).filter(item => item !== null);
const checkSize = checkFiles.some(
item => (item.width !== width && width !== 0) || (item.height !== height && height !== 0),
);
if (checkSize) {
notification.warning({ message: '图片尺寸不正确' });
return false;
}
return true;
};
const defaultBeforeUpload = lodash.debounce(
(file, fileArray) =>
// 文件显示
new Promise(async () => {
if (limit && fileListRef.current.length + fileArray.length > limit) {
Modal.warning({
maskClosable: true,
title: '超出上传个数',
});
return Upload.LIST_IGNORE;
}
if (width !== 0 || height !== 0) {
const checkSize = await validateImageSize(fileArray);
if (!checkSize) {
return Upload.LIST_IGNORE;
}
}
const fileAll = fileArray.map(item => checkFile(item));
const checkFiles = (await Promise.all(fileAll)).filter(item => item !== null);
try {
if (checkFiles.length) {
setUploadLoading(true);
const res = await merchantUpload(checkFiles);
if (res?.data) {
const proFiles = (res.data || []).map((urlItem, urlIndex) =>
imageLoading(checkFiles[urlIndex], urlItem),
);
const imagList = await Promise.all(proFiles);
const newFiles = [...fileListRef.current, ...imagList];
bundleChange(newFiles);
} else {
notification.warning({
message: '警告',
description: res?.msg || '上传失败,请重新尝试!',
});
}
setUploadLoading(false);
}
} catch (error) {
setUploadLoading(false);
Modal.warning({
maskClosable: true,
title: '上传失败,请重新尝试!',
});
}
return null;
}),
);
return (
<Spin tip="正在上传..." spinning={uploadLoading} delay={100}>
<div>
{fileList.length > 0 && (
<ReactSortable animation={300} list={fileList} setList={list => bundleChange(list)}>
{fileList.map((item, index) => (
<div
key={item.uid}
className={styles.sortImg}
onMouseEnter={() => setActiveImgIndex(index)}
onMouseLeave={() => setActiveImgIndex(null)}
>
<div style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
<img width="100%" key={item.uid} src={item.url} alt="" />
</div>
{activeImgIndex === index && (
<div className={styles.mask}>
<EyeOutlined className={styles.maskIcon} onClick={() => handlePreview(item)} />
<DeleteOutlined
className={styles.maskIcon}
onClick={() => handleRemove(item)}
/>
</div>
)}
</div>
))}
</ReactSortable>
)}
</div>
<Upload
{...uploadParams}
disabled={Boolean(disabled)}
multiple={multiple}
name={name}
customRequest={() => {}}
listType="picture-card"
beforeUpload={defaultBeforeUpload}
fileList={fileList}
onPreview={handlePreview}
onRemove={handleRemove}
showUploadList={false}
>
{limit !== null && fileList.length >= limit ? null : UploadButton}
</Upload>
<Modal open={previewVisible} title={previewTitle} footer={null} onCancel={handleCancel}>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</Spin>
);
});
export default UploadImage;
import request from '@/utils/request';
import config from '@/../config/env.config';
const { goodsApi } = config;
export const merchantUpload = async files => {
const params = new FormData();
files.forEach(file => params.append('file', file));
const data = await request.post('/image/api/merchant/upload', {
prefix: goodsApi,
data: params,
});
return data;
};
.sortImg {
position: relative;
display: inline-block;
width: 105px;
height: 105px;
margin-right: 10px;
margin-bottom: 10px;
padding: 9px;
border: 1px solid #ddd;
border-radius: 6px;
}
.mask {
position: absolute;
top: 9px;
right: 9px;
bottom: 9px;
left: 9px;
padding-top: 27%;
text-align: center;
background: #000;
opacity: 0.5;
}
.maskIcon {
margin-right: 5px;
color: #efefef !important;
font-size: 16px;
cursor: pointer;
}
/**
* 数据模型转换-接口获取数据转换为表单数据
*/
export const transformVOToFormData = data => {
const params = { ...data };
if (params.authorizationUrl) {
params.authorizationUrl = [params.authorizationUrl];
}
if (params.horizontalLogo) {
params.horizontalLogo = [params.horizontalLogo];
}
if (params.logo) {
params.logo = [params.logo];
}
if (params.qualifyUrl) {
params.qualifyUrl = [params.qualifyUrl];
}
return params;
};
/*
* 表单数据转换-表单数据转换为接口数据
*/
export const transformFormDataToDTO = res => {
const params = { ...res };
if (params.authorizationUrl?.length) {
params.authorizationUrl = params.authorizationUrl.join(',');
}
if (params.horizontalLogo?.length) {
params.horizontalLogo = params.horizontalLogo.join(',');
}
if (params.logo?.length) {
params.logo = params.logo.join(',');
}
if (params.qualifyUrl?.length) {
params.qualifyUrl = params.qualifyUrl.join(',');
}
return params;
};
import React from 'react';
import { BetaSchemaForm } from '@ant-design/pro-components';
import { notification } from 'antd';
import { brandInfoColumn } from '../staticData.js';
import { layout } from '@/utils/bll';
import { apiBrandDetail, apiCreateBrand, apiEditBrand, apiAppendQualifyBrand } from '../services';
import { transformVOToFormData, transformFormDataToDTO } from '../bll.js';
/**
* 品牌信息组件
*/
const BrandInfo = props => {
const refForm = React.useRef();
const { actionStatus, brandId } = props;
const closeModal = v => {
refForm.current?.resetFields();
!v && props.onClose(false);
};
const getAPI = () => {
switch (actionStatus) {
case 'edit':
return apiEditBrand;
case 'supplement':
return apiAppendQualifyBrand;
default:
return apiCreateBrand;
}
};
const submitForm = async values => {
const params = transformFormDataToDTO(values);
params.brandId = brandId;
params.id = brandId;
const api = getAPI();
const res = await api(params);
if (res?.success) {
notification.success({
message: '提交成功',
});
props.onClose(true);
}
};
const getInfo = () =>
new Promise(resolve => {
setTimeout(async () => {
if (brandId) {
const res = await apiBrandDetail({ brandId });
if (res?.data) {
const data = transformVOToFormData(res.data);
resolve(data);
}
}
resolve({});
}, 200);
});
const config = { actionStatus };
return (
props.visible && (
<BetaSchemaForm
layoutType="ModalForm"
title="品牌信息"
open={props.visible}
width="600px"
modalProps={{
maskClosable: true,
destroyOnClose: true,
}}
request={getInfo}
formRef={refForm}
onOpenChange={closeModal}
layout="horizontal"
{...layout}
onFinish={submitForm}
columns={brandInfoColumn(config)}
/>
)
);
};
export default BrandInfo;
import React, { useState, useRef } from 'react';
import { ProTable } from '@ant-design/pro-components';
import { Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { apiBrandList } from './services';
import { brandColumn, brandActionAdd } from './staticData.js';
import utilStyle from '@/utils/utils.less';
import BrandInfo from './components/BrandInfo.jsx';
/**
* 品牌管理列表
*/
const BrandManage = () => {
const refTable = useRef();
const [id, setId] = useState('');
const [visible, setVisible] = useState(false);
const [actionStatus, setActionStatus] = useState(brandActionAdd);
// 编辑品牌
const onAction = (record, status) => {
setId(record.brandId);
setActionStatus(status);
setVisible(!0);
};
const onClose = refresh => {
setVisible(false);
refresh && refTable.current?.reload();
};
return (
<>
<ProTable
actionRef={refTable}
search={{
collapsed: false,
span: 8,
className: utilStyle.formSearch,
collapseRender: () => null,
}}
columns={brandColumn({
refTable,
onAction,
})}
request={apiBrandList}
toolBarRender={() => [
<Button
key="add"
icon={<PlusOutlined />}
type="primary"
onClick={() => {
setId('');
setActionStatus(brandActionAdd);
setVisible(!0);
}}
>
添加
</Button>,
]}
rowKey={r => r.brandId}
bordered
options={false}
/>
<BrandInfo visible={visible} actionStatus={actionStatus} brandId={id} onClose={onClose} />
</>
);
};
export default BrandManage;
import qs from 'qs';
import request from '@/utils/request';
import config from '@/../config/env.config';
const { kdspApi } = config;
/**
* 分页查询所有品牌列表
* http://yapi.quantgroups.com/project/389/interface/api/66404
*/
export async function apiBrandList(params) {
const param = {
...params,
pageNo: params.current,
};
const res = await request.get(`/api/merchants/brands/list?${qs.stringify(param)}`, {
prefix: kdspApi,
});
if (res?.data) {
return {
total: res.data.total,
data: res.data.records,
};
}
return {
total: 0,
data: [],
};
}
/**
* 添加品牌
* http://yapi.quantgroups.com/project/389/interface/api/66389
* */
export async function apiCreateBrand(data) {
return request.post('/api/merchants/brands/add', {
data,
prefix: kdspApi,
});
}
/**
* 编辑品牌
* http://yapi.quantgroups.com/project/389/interface/api/66394
*/
export async function apiEditBrand(data) {
return request.post('/api/merchants/brands/edit', {
data,
prefix: kdspApi,
});
}
/**
* 补充资质
* http://yapi.quantgroups.com/project/389/interface/api/66399
*/
export async function apiAppendQualifyBrand(data) {
return request.post('/api/merchants/brands/qualify/add', {
data,
prefix: kdspApi,
});
}
/**
* 查询品牌信息
* http://yapi.quantgroups.com/project/389/interface/api/66409
* */
export async function apiBrandDetail(params) {
return request.get(`/api/merchants/brands/detail?${qs.stringify(params)}`, {
prefix: kdspApi,
});
}
import React from 'react';
import { Button, Popover } from 'antd';
import UploadImage from '@/components/UploadImg/index.jsx';
// 品牌审核状态
/**
* @description: 品牌审核状态 1-待审核
*/
export const auditStatusWait = 1;
/**
* @description: 品牌审核状态 2-审核通过
*/
export const auditStatusPass = 2;
/**
* @description: 品牌审核状态 3-审核拒绝
*/
export const auditStatusReject = 3;
/**
* @description: 品牌审核状态枚举 0-无 1-待审核,2-审核通过,3-审核拒绝
*/
export const brandStatusEnum = {
// 0: '-',
[auditStatusWait]: '待审核',
[auditStatusPass]: '审核通过',
[auditStatusReject]: '驳回',
};
/**
* @description: 品牌审核 2审核通过 3驳回
*/
export const brandAuditEnum = {
[auditStatusPass]: '审核通过',
[auditStatusReject]: '驳回',
};
// 操作状态 查看、修改、添加、补充资质
/**
* @description: 品牌操作状态 查看
*/
export const brandActionFind = 'find';
/**
* @description: 品牌操作状态 修改
*/
export const brandActionEdit = 'edit';
/**
* @description: 品牌操作状态 添加
*/
export const brandActionAdd = 'add';
/**
* @description: 品牌操作状态 补充资质
*/
export const brandActionSupplement = 'supplement';
/**
* @description: 列表基础字段
*/
export const brandBaseColumn = [
{
title: '品牌名称',
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: '中文名称',
dataIndex: 'chineseName',
key: 'chineseName',
hideInSearch: true,
align: 'center',
},
{
title: '英文名称',
key: 'englishName',
dataIndex: 'englishName',
hideInSearch: true,
align: 'center',
},
{
title: '品牌缩写或别称',
key: 'alias',
dataIndex: 'alias',
hideInSearch: true,
align: 'center',
},
];
// 品牌列表字段
export const brandColumn = config => {
const { onAction } = config;
return [
...brandBaseColumn,
{
title: '审核状态',
key: 'status',
dataIndex: 'status',
align: 'center',
valueEnum: brandStatusEnum,
render: (_, r) => {
const { status } = r;
return status === auditStatusReject ? (
<Popover content={r.rejectReason} title="驳回原因" trigger="hover">
<Button type="link" danger>
驳回
</Button>
</Popover>
) : (
brandStatusEnum[status]
);
},
},
{
title: '操作',
hideInSearch: true,
dataIndex: 'option',
align: 'center',
width: 200,
render: (_, r) => [
(r.modifiable && (
<Button key="check" type="primary" onClick={() => onAction(r, 'edit')}>
修改
</Button>
)) ||
'',
([null, auditStatusReject].includes(r.status) && !r.modifiable && (
<Button key="supplement" type="primary" ghost onClick={() => onAction(r, 'supplement')}>
补充资质
</Button>
)) ||
'',
],
},
];
};
// 品牌信息字段
export const brandInfoColumn = config => {
const { actionStatus } = config;
const disabled = brandActionSupplement === actionStatus;
const baseInfo = [
{
title: '资质证书',
dataIndex: 'qualifyUrl',
formItemProps: {
rules: [{ required: true, message: '请选择资质证书' }],
},
renderFormItem: () => <UploadImage limit={1} />,
},
{
title: '授权证书',
dataIndex: 'authorizationUrl',
formItemProps: {
rules: [{ required: true, message: '请选择授权证书' }],
},
renderFormItem: () => <UploadImage limit={1} />,
},
{
title: '品牌名称',
dataIndex: 'name',
maxLength: 50,
fieldProps: {
disabled,
},
formItemProps: {
rules: [{ required: true, message: '请输入品牌名称' }],
},
},
{
title: '中文名称',
dataIndex: 'chineseName',
maxLength: 50,
fieldProps: {
disabled,
},
},
{
title: '英文名称',
dataIndex: 'englishName',
maxLength: 100,
fieldProps: {
disabled,
},
},
{
title: '品牌缩写或别称',
dataIndex: 'alias',
maxLength: 50,
formItemProps: {
rules: [{ required: true, message: '请输入品牌缩写或别称' }],
},
fieldProps: {
disabled,
},
},
{
title: '长方形LOGO上传',
dataIndex: 'horizontalLogo',
formItemProps: {
rules: [{ required: true, message: '请上传长方形LOGO上传' }],
extra: (
<div>
<div>尺寸要求:219*72</div>
<div>
素材要求:1、透明底;2、上下左右最少留白2px,具体以保证整体大小一致,视觉平衡为准
</div>
</div>
),
},
fieldProps: {
disabled,
},
renderFormItem: () => <UploadImage limit={1} disabled={disabled} width={219} height={72} />,
},
{
title: 'LOGO上传',
dataIndex: 'logo',
formItemProps: {
rules: [{ required: true, message: '请上传LOGO' }],
extra: <span>尺寸要求:192*192</span>,
},
fieldProps: {
disabled,
},
renderFormItem: () => <UploadImage limit={1} disabled={disabled} width={192} height={192} />,
},
];
return baseInfo;
};
...@@ -308,7 +308,6 @@ const FormInformationBasic = forwardRef((props, ref) => { ...@@ -308,7 +308,6 @@ const FormInformationBasic = forwardRef((props, ref) => {
name="brandId" name="brandId"
label="商品品牌" label="商品品牌"
rules={[{ required: true, message: '请选择商品品牌' }]} rules={[{ required: true, message: '请选择商品品牌' }]}
extra="若需新增品牌请联系业务员"
> >
<Select <Select
showSearch showSearch
......
...@@ -35,6 +35,10 @@ export const merchantBrandList = () => ...@@ -35,6 +35,10 @@ export const merchantBrandList = () =>
request.post('/product/brand/api/merchant/list', { request.post('/product/brand/api/merchant/list', {
prefix: goodsApi, prefix: goodsApi,
}); });
// export const merchantBrandList = () =>
// request.post('/api/merchants/brands/getAll', {
// prefix: goodsApi,
// });
// 获取规格列表 // 获取规格列表
export const merchantSpecList = () => export const merchantSpecList = () =>
......
export const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 16 },
};
...@@ -12,7 +12,7 @@ import config from '../../config/env.config'; ...@@ -12,7 +12,7 @@ import config from '../../config/env.config';
import IframeBridge from './iframeBridge'; import IframeBridge from './iframeBridge';
let isRefreshing = true; let isRefreshing = true;
let subscriber = []; const subscriber = [];
const codeMessage = { const codeMessage = {
200: '服务器成功返回请求的数据。', 200: '服务器成功返回请求的数据。',
...@@ -55,10 +55,12 @@ const request = extend({ ...@@ -55,10 +55,12 @@ const request = extend({
const addSubscriber = callback => subscriber.push(callback); const addSubscriber = callback => subscriber.push(callback);
const wait = async seconds => new Promise(resolve => setTimeout(resolve, seconds)); const wait = async seconds => new Promise(resolve => setTimeout(resolve, seconds));
const onAccessTokenFetched = () => { const onAccessTokenFetched = () => {
console.log('subscriber :>> ', subscriber);
subscriber.forEach(callback => callback()); subscriber.forEach(callback => callback());
subscriber = []; // subscriber = [];
}; };
const refreshRequest = async (url, options) => { const refreshRequest = async (url, options) => {
console.log('url, options :>> ', url, options);
const promise = new Promise(resolve => addSubscriber(() => resolve(request(url, options)))); const promise = new Promise(resolve => addSubscriber(() => resolve(request(url, options))));
if (isRefreshing) { if (isRefreshing) {
isRefreshing = false; isRefreshing = false;
...@@ -67,6 +69,7 @@ const refreshRequest = async (url, options) => { ...@@ -67,6 +69,7 @@ const refreshRequest = async (url, options) => {
if (data.code === 2000) { if (data.code === 2000) {
localStorage.set('token', data.data.accessToken); localStorage.set('token', data.data.accessToken);
} }
console.log('1111111 :>> ', subscriber.length);
onAccessTokenFetched(); onAccessTokenFetched();
isRefreshing = true; isRefreshing = true;
} }
...@@ -115,7 +118,7 @@ request.interceptors.response.use(async (response, options) => { ...@@ -115,7 +118,7 @@ request.interceptors.response.use(async (response, options) => {
}); });
} }
// token过期 // token过期
const url = response.url.split(config.api)[1]; const url = response.url.replace(options.prefix, '');
return refreshRequest(url, options); return refreshRequest(url, options);
} }
if (data.code === 4011) { if (data.code === 4011) {
......
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