场景:有一天,产品经理突然发了疯,要做一个可以动态的选择表格的显示字段的功能,然后字段分为商品相关的字段(40多个),库存相关的字段(30多个),采购相关的字段(30多个),订单相关的字段(数不清)... MMP的,总共好几百个字段,不同类型的字段在各个地方重用,然后又要选择性的导出。
如图:
(1)字段选择框
(2)主页面
解决过程:
1.封装框架自带的穿梭框。
class OutputWordsSelector extends BaseComponent{
mDataList=[];
constructor(props){
super(props);
if(props.datalist!==null){
this.mDataList=props.datalist;
}
this.state={
leftTitle:"未选项目",
rightTitle:"已选项目",
targetKeys: props.targetKeys,
selectedKeys: props.selectedKeys,
disabled: false,
}
}
handleChange = (nextTargetKeys, direction, moveKeys) => {
this.setState({ targetKeys: nextTargetKeys });
LogUtil.debugTag("nextTargetKeys=",nextTargetKeys);
if(this.props.selectChange!=null){
this.props.selectChange(nextTargetKeys);
}
}
handleSelectChange = (sourceSelectedKeys, targetSelectedKeys) => {
this.setState({ selectedKeys: [...sourceSelectedKeys, ...targetSelectedKeys] });
}
render() {
const { targetKeys, selectedKeys,leftTitle,rightTitle} = this.state;
return(
<div>
<Transfer
className={style.width}
dataSource={this.mDataList}
titles={[leftTitle,rightTitle]}
targetKeys={targetKeys}
selectedKeys={selectedKeys}
onChange={this.handleChange}
onSelectChange={this.handleSelectChange}
onScroll={this.handleScroll}
render={item => item.title}
/>
</div>
)
}
}
export default OutputWordsSelector
2.构造商品模块通用的表格输出字段工具。
class GoodsInfoColumns {
/**
* 商品资料输出字段
* @type {*[]}
*/
static colums = [
{
code: MyConstants.OUTPUT_PDT_NAME,
title: '商品名称',
dataIndex: 'goodsInfo',
render: (text, record) => (
<div style={{ width: 280 }}>
<GoodsInfo goodsInfo={record.goodsInfo}>
</GoodsInfo>
</div>
),
},
{
code: MyConstants.OUTPUT_PDT_GOODS_CODE,
title: '货号',
dataIndex: 'number',
render: (text, record) => {
return <div style={{width:100}}>{record.number==null?"-":record.number}</div>;
},
},
{
code: MyConstants.OUTPUT_PDT_STYLE_CODE,
title: '款号',
dataIndex: 'styleNumber',
render: (text, record) => {
return <div style={{width:100}}>{record.styleNumber==null?"-":record.styleNumber}</div>;
},
},
{
code: MyConstants.OUTPUT_PDT_STOCK_UNIT,
title: '库存单位',
dataIndex: 'unit',
render: (text, record) => {
return <div style={{width:100}}>{record.unit==null?"-":record.unit}</div>;
},
},
{
code: MyConstants.OUTPUT_PDT_COOPERATE_WAY,
title: '合作方式',
dataIndex: 'saleMold',
render: (text, record) => {
return <div style={{width:100}}>{record.saleMold==null?"-":record.saleMold}</div>;
},
},
{
code: MyConstants.OUTPUT_PDT_BRAND,
title: '品牌',
dataIndex: 'brandName',
render: (text, record) => {
return <div style={{width:100}}>{record.brandName==null?"-":record.brandName}</div>;
},
},
{
code: MyConstants.OUTPUT_PDT_MF_TYPE,
title: '品类',
dataIndex: 'mfCategoryName',
render: (text, record) => {
return <div style={{width:100}}>{record.mfCategoryName==null?"-":record.mfCategoryName}</div>;
},
},
...
}
export default GoodsInfoColumns;
3.构造库存列表独有的字段表格工具
import MoneyUtil from '../../../../../utils/erp/MoneyUtil';
import PayUtil from '../../../../../utils/erp/PayUtil';
import TimeUtil from '../../../../../utils/erp/TimeUtil';
import OrderUtil from '../../../../../utils/erp/OrderUtil';
import MyConstants from '../../../../../utils/erp/MyConstants';
import ExcelUtil from '../../../../../utils/erp/ExcelUtil';
import GoodsInfo from '../../../common/goodsInfo/GoodsInfo';
class StockListUtil {
/**
* 所有表格数据字段
* @type {*[]}
*/
static colums = [
//基础字段
{
code: MyConstants.OUTPUT_STOCK_TOTAL,
title: '总库存',
dataIndex: 'totalAmount',
render: (text, record) => {
return <div style={{width:100}}>{record.totalAmount==null?"-":record.totalAmount}</div>;
},
},
{
code: MyConstants.OUTPUT_STOCK_ON_THE_WAY,
title: '在途库存',
dataIndex: 'onWayAmount',
render: (text, record) => {
return <div style={{width:80}}>{record.onWayAmount==null?"-":record.onWayAmount}</div>;
},
},
{
code: MyConstants.OUTPUT_SS_STOCK_CAN_SALE,
title: '可售库存',
dataIndex: 'salableAmount',
render: (text, record) => {
return <div style={{width:80}}>{record.salableAmount==null?"-":record.salableAmount}</div>;
},
},
{
code: MyConstants.OUTPUT_SS_STOCK_AVG_COST,
title: '平均成本(元)',
dataIndex: 'averageCost',
render:(text,record)=>{
return <div style={{width:100}}>{MoneyUtil.changeMoneyNumToShow(record.averageCost)}</div>
}
},
{
code: MyConstants.OUTPUT_STOCK_TOTAL_COST,
title: '总成本(元)',
dataIndex: 'totalCost',
render:(text,record)=>{
return <div style={{width:100}}>{MoneyUtil.changeMoneyNumToShow(record.totalCost)}</div>
}
},
];
/**
* 对JSON数据进行相应的转化,导出excel表格(动态控制导出字段)
* @param datalist
* @param fileName
*/
static outputExcelDynamic(datalist,columns, fileName) {
let tempList = [];
for (let i = 0; i < datalist.length; i++) {
let temp = {};
for(let j=0;j<columns.length;j++){
let title=columns[j].title;
let dataIndex=columns[j].dataIndex;
temp[title]=datalist[i][dataIndex];
}
tempList.push(temp);
}
ExcelUtil.exportExcelTable(tempList, fileName);
}
// /**
// * 对JSON数据进行相应的转化,导出excel表格
// * @param datalist
// * @param fileName
// */
// static outputExcel(datalist, fileName) {
// let tempList = [];
// for (let i = 0; i < datalist.length; i++) {
// let temp = {};
// temp.订单号 = datalist[i].orderNum;
// temp.金额 = MoneyUtil.changeMoneyNumToShow(datalist[i].money);
// temp.支付方式 = PayUtil.changeChannelPayToStr(datalist[i].payway);
// temp.会员 = datalist[i].member;
// temp.所属门店 = datalist[i].belongTo;
// temp.创建时间 = TimeUtil.changeTimeStampToDateTime(datalist[i].createTime);
// temp.状态 = OrderUtil.changeOrderStatusToStr(datalist[i].status);
// temp.支付时间 = TimeUtil.changeTimeStampToDateTime(datalist[i].payTime);
// temp.运费 = MoneyUtil.changeMoneyNumToShow(datalist[i].dlvPrice);
// temp.快递单号 = datalist[i].dlvNum;
// temp.物流公司 = datalist[i].dlvCompany;
// tempList.push(temp);
// }
// ExcelUtil.exportExcelTable(tempList, fileName);
// }
}
export default StockListUtil;
4.主页面
import BaseComponent from '../../../base/BaseComponent';
import { connect } from 'dva';
import { Input, Select, Table, Modal, Button, Card } from 'antd';
import LogUtil from '../../../../../utils/erp/LogUtil';
import MyConstants from '../../../../../utils/erp/MyConstants';
import OutputWordsSelector from '../../../common/outputWordsSelector/OutputWordsSelector';
import router from 'umi/router';
import StockListUtil from './StockListUtil';
import OutputWordUtil from '../../../../../utils/erp/OutputWordUtil';
import style from '../../StockStyle.css';
import GoodsInfoColumns from '../../../common/columns/GoodsInfoColumns';
import StockDistribution from './StockDistribution';
import HttpCode from '../../../../../utils/erp/HttpCode';
import MsgUtil from '../../../../../utils/erp/MsgUtil';
const Option = Select.Option;
@connect(({ stockSearch, stockCommon,loading }) => ({
stockSearch,
stockCommon,
loading: loading.models.stockSearch,
}))
class StockList extends BaseComponent {
//可操作列
columnOperator = {
title: '操作',
dataIndex: 'operate',
key: 'operate',
render: (text, record) => (
<div style={{ width: 150 }}>
<a onClick={this.showDialog2.bind(this, record)}>分配</a>
<span> | </span>
<a onClick={this.goToStockStream.bind(this,record)}>流水</a>
</div>
),
};
//所有可选字段
mWordList = [];
//已经选择的字段
mSelectedWordList = [];
//已选字段暂存数组
tempKeys = [];
//输出字段字符串
outputValue = '';
//数据分页
mPageSize = 20;
mCurrentPage = 0;
//查询参数
text=MyConstants.DefaultEmptyStringValue;
mfBrand=MyConstants.DefaultNumValue;
brand=MyConstants.DefaultNumValue;
supplier=MyConstants.DefaultNumValue;
constructor(props) {
super(props);
this.initOutputWords();
this.getSelectedColumns();
}
componentDidMount() {
this.getDataList();
this.getMFBrandList();
this.getBrandList();
this.getSupplierList();
}
//默认的表格显示字段
defaultOutputKeys = [
MyConstants.OUTPUT_PDT_NAME,
MyConstants.OUTPUT_PDT_DESCRIPTION,
MyConstants.OUTPUT_PDT_COOPERATE_WAY,
MyConstants.OUTPUT_PDT_BRAND,
MyConstants.OUTPUT_PDT_MF_TYPE,
MyConstants.OUTPUT_PDT_STOCK_UNIT,
MyConstants.OUTPUT_SS_STOCK_TOTAL,
MyConstants.OUTPUT_SS_STOCK_ON_THE_WAY,
MyConstants.OUTPUT_SS_STOCK_CAN_SALE,
MyConstants.OUTPUT_SS_STOCK_AVG_COST,
MyConstants.OUTPUT_SS_STOCK_TOTAL_COST,
];
/**
* 初始化输出字段
*/
initOutputWords() {
StockListUtil.colums.map((item) => {
let temp = {};
temp.key = item.code;
temp.title = item.title;
this.mWordList.push(temp);
});
GoodsInfoColumns.colums.map(item => {
let temp = {};
temp.key = item.code;
temp.title = item.title;
this.mWordList.push(temp);
});
this.mSelectedWordList = JSON.parse(JSON.stringify(this.defaultOutputKeys));
}
/**
* 获取输出表格列
*/
getSelectedColumns() {
this.colums = OutputWordUtil.getColums(this.mSelectedWordList, GoodsInfoColumns.colums);
// //按code排序,需要的时候使用
// this.colums = this.colums.sort(function(a, b) {
// return a.code - b.code;
// });
let tempList = OutputWordUtil.getColums(this.mSelectedWordList, StockListUtil.colums);
for (let i = 0; i < tempList.length; i++) {
this.colums.push(tempList[i]);
}
this.colums.push(this.columnOperator);
//获取表格头字符
this.outputValue = '';
for (let i = 0; i < this.colums.length; i++) {
this.outputValue = this.outputValue + this.colums[i].title + ',';
}
}
render() {
const { stockSearch: { stockListData, loading },stockCommon:{mfList,brandList,supplierList} } = this.props;
return (
<Card bordered={false}>
<div className={style.select_div}>
<div className={style.select_line}>
<span>商品筛选:</span>
<Input placeholder="商品名称/货号/条码" className={style.input} onChange={this.onTextInputChange} value={this.text}/>
<span className={style.space_left}>MF品牌:</span>
<Select defaultValue={MyConstants.DefaultNumValue} value={this.mfBrand} className={style.select_width} onSelect={this.onMFbrandSelect}>
<Option value={MyConstants.DefaultNumValue}>全部</Option>
{mfList.map(item=>{
return <Option value={item.id}>{item.name}</Option>
})}
</Select>
<span className={style.space_left}>商品品牌:</span>
<Select defaultValue={MyConstants.DefaultNumValue} value={this.brand} className={style.select_width} onSelect={this.onBrandSelect}>
<Option value={MyConstants.DefaultNumValue}>全部</Option>
{brandList.map(item=>{
return <Option value={item.id}>{item.name}</Option>
})}
</Select>
</div>
<div className={style.select_line} style={{ marginTop: 10 }}>
<span>供应商:</span>
<Select defaultValue={MyConstants.DefaultNumValue} value={this.supplier} className={style.select_width} onSelect={this.onSupplierSelect}>
<Option value={MyConstants.DefaultNumValue}>全部</Option>
{supplierList.map(item=>{
return <Option value={item.id}>{item.name}</Option>
})}
</Select>
</div>
<div className={style.select_line} style={{ marginTop: 10 }}>
<span>输出:</span>
<Input value={this.outputValue} style={{ width: 300, marginLeft: 30 }} disabled={true}/>
<Button type="primary" onClick={this.showDialog} ghost>...</Button>
</div>
<div className={style.select_line} style={{ marginTop: 10 }}>
<Button type="primary" onClick={this.searchInfo}>筛选</Button>
<Button type="primary" onClick={this.outputData} ghost style={{ marginLeft: 10 }}>导出</Button>
<a onClick={this.initSearchCondition} style={{ marginLeft: 10 }}>清除筛选条件</a>
</div>
</div>
<div style={{ marginTop: 20 }}>
<Table
rowKey={this.getRowKey}
columns={this.colums}
dataSource={stockListData.pageData}
loading={loading}
// scroll={{ x: 100 }}
style={{overflowX:"scroll",tableLayout:"fixed"}}
pagination={{
onChange: page => {
console.log(page);
/**
* 数据库数据页是从0开始的,antD是从1开始的
* */
this.mCurrentPage = page - 1;
this.getData();
},
pageSize: this.mPageSize,
total: stockListData.totalSize,
}}
/>
</div>
<Modal
title="输出栏目"
visible={this.state.dialogVisible}
onOk={this.dialogOk}
onCancel={this.dialogCancel}
>
<OutputWordsSelector datalist={this.mWordList} selectedKeys={[]} targetKeys={this.mSelectedWordList}
selectChange={this.outputWordsChange}/>
</Modal>
<Modal
width="800px"
title="分布"
visible={this.state.dialogVisible2}
onOk={this.dialogOk2}
onCancel={this.dialogCancel2}
footer={null}
destroyOnClose={true}
>
<StockDistribution info={this.goodsInfo}></StockDistribution>
</Modal>
</Card>);
}
getRowKey = (record) => {
return record.skuId;
};
//================= 弹出框相关 ===============
state = { dialogVisible: false, dialogVisible2: false };
showDialog = () => {
this.setState({
dialogVisible: true,
});
};
goodsInfo=null;
showDialog2 = (record) => {
this.goodsInfo=JSON.parse(JSON.stringify(record.goodsInfo));
this.goodsInfo.skuId=record.skuId;
this.goodsInfo.brand=record.brand;
this.goodsInfo.mfCategoryName=record.mfCategoryName;
this.goodsInfo.unit=record.unit;
this.setState({
dialogVisible2: true,
});
};
dialogOk = (e) => {
this.mSelectedWordList = this.tempKeys;
this.getSelectedColumns();
this.setState({
dialogVisible: false,
});
};
dialogOk2 = (e) => {
this.setState({
dialogVisible2: false,
});
};
dialogCancel = (e) => {
this.setState({
dialogVisible: false,
});
};
dialogCancel2 = (e) => {
this.setState({
dialogVisible2: false,
});
};
//======================= 状态回调函数 =====================
outputWordsChange = (keys) => {
this.tempKeys = [];
keys.map(item => {
this.tempKeys.push(item);
});
this.tempKeys.sort();
};
onMFbrandSelect=(key)=>{
this.mfBrand=key;
this.setState({});
};
onBrandSelect=(key)=>{
this.brand=key;
this.setState({});
};
onSupplierSelect=(key)=>{
this.supplier=key;
this.setState({});
};
onTextInputChange=(e)=>{
this.text=e.target.value;
this.setState({});
};
//=================== 功能点击按钮 ====================
/**
* 搜索
*/
searchInfo = () => {
this.getDataList();
};
/**
* 导出数据
*/
outputData = () => {
const { stockSearch: { stockListData } } = this.props;
StockListUtil.outputExcel(stockListData.pageData, this.colums,'线上销售单数据表.xlsx');
};
/**
* 初始化搜索条件
*/
initSearchCondition = () => {
this.text=MyConstants.DefaultEmptyStringValue;
this.mfBrand=MyConstants.DefaultNumValue;
this.brand=MyConstants.DefaultNumValue;
this.supplier=MyConstants.DefaultNumValue;
this.setState({});
};
goToStockStream=(record)=>{
let path = {
pathname: '/stock/stream/singleList',
state: {
skuId: record.skuId,
},
};
router.push(path);
}
//================= 网络接口调用 ====================
getDataList = () => {
const { dispatch } = this.props;
let params={};
params.pageNumber=this.mCurrentPage;
params.pageSize=this.mPageSize;
if(this.text!==MyConstants.DefaultEmptyStringValue){
params.text=this.text;
}
if(this.mfBrand!==MyConstants.DefaultNumValue){
params.categoryId=this.mfBrand;
}
if(this.brand!==MyConstants.DefaultNumValue){
params.brandId=this.brand;
}
if(this.supplier!==MyConstants.DefaultNumValue){
params.supplierId=this.supplier;
}
dispatch({
type: 'stockSearch/getStockList',
payload: params,
});
};
getMFBrandList=()=>{
const{dispatch}=this.props;
dispatch({
type:"stockCommon/getMFList",
payload:{}
});
}
getBrandList=()=>{
const{dispatch}=this.props;
dispatch({
type:"stockCommon/getBrandList",
payload:{}
});
}
getSupplierList=()=>{
const{dispatch}=this.props;
dispatch({
type:"stockCommon/getSupplierList",
payload:{}
});
}
}
export default StockList;
5.导出工具类
import XLSX from 'xlsx';
import LogUtil from './LogUtil';
import MoneyUtil from './MoneyUtil';
import TimeUtil from './TimeUtil';
function createCeil(type,text) {
var el = document.createElement(type);
el.innerHTML = text;
return el;
}
function jsonToTable(data,serialNum,dataTime) {
let table = document.createElement("table");
let thead = document.createElement("thead");
let title = document.createElement("tr");
title.appendChild(createCeil('th','单号:'));
title.appendChild(createCeil('th',serialNum));
title.appendChild(createCeil('th','单据日期:'));
title.appendChild(createCeil('th',dataTime));
thead.appendChild(title);
let clomunsH = document.createElement("tr");
let keys = Object.keys(data[0]);
keys.map(key => {
clomunsH.appendChild(createCeil('th',key));
})
thead.appendChild(clomunsH);
table.appendChild(thead);
let tbody = document.createElement("tbody");
data.map(item => {
let tr = document.createElement("tr");
keys.map(key => {
tr.appendChild(createCeil('td',item[key]))
})
tbody.appendChild(tr);
})
table.appendChild(tbody);
return table;
}
class ExcelUtil {
/**
* 通过转化好的json数据,输出excel表格(当前使用这种方式)
* @param datalist
* @param fileName
*/
static exportExcelTable(datalist,fileName){
var sheet=XLSX.utils.json_to_sheet(datalist);
LogUtil.debugTag("sheet",sheet);
var workbook = {
SheetNames: ["mySheet"],
Sheets: {
mySheet:sheet
}
};
XLSX.writeFile(workbook,fileName);
}
/**
* 通过转化好的json数据,输出excel表格
* @param datalist
* @param serialNum EXCEL头部的单号
* @param dataTime EXCEL头部的时间
* @param fileName
*/
static exportExcelHasHeader(datalist,serialNum,dataTime,fileName){
let table = jsonToTable(datalist,serialNum,dataTime);
let sheet = XLSX.utils.table_to_sheet(table);
LogUtil.debugTag("sheet",sheet);
var workbook = {
SheetNames: ["mySheet"],
Sheets: {
mySheet:sheet
}
};
XLSX.writeFile(workbook,fileName);
}
/**
* 通过表格和数据输出excel表格
* 使用方式:ExcelUtil.exportExcel(this.colums,onlineSaleList,"线上销售单数据表.xlsx");
* @param headers
* @param data
* @param fileName
*/
static exportExcel(headers, data, fileName = '数据表格.xlsx') {
const _headers = headers
.map((item, i) => Object.assign({}, { key: item.key, title: item.title, position: String.fromCharCode(65 + i) + 1 }))
.reduce((prev, next) => Object.assign({}, prev, { [next.position]: { key: next.key, v: next.title } }), {});
const _data = data
.map((item, i) => headers.map((key, j) => Object.assign({}, { content: item[key.key], position: String.fromCharCode(65 + j) + (i + 2) })))
// 对刚才的结果进行降维处理(二维数组变成一维数组)
.reduce((prev, next) => prev.concat(next))
// 转换成 worksheet 需要的结构
.reduce((prev, next) => Object.assign({}, prev, { [next.position]: { v: next.content } }), {});
// 合并 headers 和 data
const output = Object.assign({}, _headers, _data);
// 获取所有单元格的位置
const outputPos = Object.keys(output);
// 计算出范围 ,["A1",..., "H2"]
const ref = `${outputPos[0]}:${outputPos[outputPos.length - 1]}`;
// 构建 workbook 对象
const wb = {
SheetNames: ['mySheet'],
Sheets: {
mySheet: Object.assign(
{},
output,
{
'!ref': ref,
'!cols': [{ wpx: 45 }, { wpx: 100 }, { wpx: 200 }, { wpx: 80 }, { wpx: 150 }, { wpx: 100 }, { wpx: 300 }, { wpx: 300 }],
},
),
},
};
// 导出 Excel
XLSX.writeFile(wb, fileName);
}
}
export default ExcelUtil;