前端:抽出 HtmlEditor 组件

前端:完善商品编辑,基本算完成了
pull/1/head
YunaiV 2019-05-06 00:06:32 +08:00
parent 35077dcf53
commit 2519cf000e
8 changed files with 187 additions and 94 deletions

View File

@ -0,0 +1,116 @@
import React from "react";
import 'braft-editor/dist/index.css'
import BraftEditor from 'braft-editor'
import { ContentUtils } from 'braft-utils'
import { ImageUtils } from 'braft-finder'
import {fileGetQiniuToken} from "../../services/admin";
import uuid from "js-uuid";
import * as qiniu from "qiniu-js";
import {Icon, Upload} from "antd";
class HtmlEditor extends React.Component {
state = {
editorState: BraftEditor.createEditorState(null),
};
handleChange = (editorState) => {
this.setState({editorState})
};
uploadHandler = async (param) => {
if (!param.file) {
return false
}
debugger;
const tokenResult = await fileGetQiniuToken();
if (tokenResult.code !== 0) {
alert('获得七牛上传 Token 失败');
return false;
}
let token = tokenResult.data;
let that = this;
const reader = new FileReader();
const file = param.file;
reader.readAsArrayBuffer(file);
let fileData = null;
reader.onload = (e) => {
let key = uuid.v4(); // TODO 芋艿可能后面要优化。MD5
let observable = qiniu.upload(file, key, token); // TODO 芋艿,最后后面去掉 qiniu 的库依赖,直接 http 请求,这样更轻量
observable.subscribe(function () {
// next
}, function (e) {
// error
// TODO 芋艿,后续补充
// debugger;
}, function (response) {
// complete
that.setState({
editorState: ContentUtils.insertMedias(that.state.editorState, [{
type: 'IMAGE',
url: 'http://static.shop.iocoder.cn/' + response.key,
}])
})
});
}
};
getHtml() {
return this.state.editorState.toHTML();
}
setHtml = (html) => {
this.setState({
editorState: BraftEditor.createEditorState(html),
})
};
isEmpty = () => {
return this.state.editorState.isEmpty();
};
render() {
// const controls = ['bold', 'italic', 'underline', 'text-color', 'separator', 'link', 'separator'];
const extendControls = [
{
key: 'antd-uploader',
type: 'component',
component: (
<Upload
accept="image/*"
showUploadList={false}
customRequest={this.uploadHandler}
>
{/* 这里的按钮最好加上type="button"以避免在表单容器中触发表单提交用Antd的Button组件则无需如此 */}
<button type="button" className="control-item button upload-button" data-title="插入图片">
<Icon type="picture" theme="filled" />
</button>
</Upload>
)
}
];
return (
<div style={{border: '1px solid #d1d1d1', 'border-radius': '5px'}}>
<BraftEditor
value={this.state.editorState}
onChange={this.handleChange}
defaultValue={this.state.initialContent}
// controls={controls}
extendControls={extendControls}
contentStyle={{height: 200}}
/>
</div>
)
}
}
{/**/}
// </div>
export default HtmlEditor;

View File

@ -77,7 +77,6 @@ class PicturesWall extends React.Component {
// }); // });
// 使用 FileReader 将上传的文件转换成二进制流,满足 'application/octet-stream' 格式的要求 // 使用 FileReader 将上传的文件转换成二进制流,满足 'application/octet-stream' 格式的要求
debugger;
const reader = new FileReader(); const reader = new FileReader();
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
let fileData = null; let fileData = null;

View File

@ -68,7 +68,7 @@ class AttrValueSelect extends Select {
export default class ProductAttrSelectFormItem extends PureComponent { export default class ProductAttrSelectFormItem extends PureComponent {
handleSelectAttr = (value, option) => { handleSelectAttr = (value, option) => {
debugger; // debugger;
// console.log(value); // console.log(value);
// console.log(option); // console.log(option);
// debugger; // debugger;

View File

@ -35,7 +35,8 @@ export default {
// price: // 价格 // price: // 价格
// quantity: // 数量 // quantity: // 数量
// } // }
] ],
}, },
effects: { effects: {
@ -308,6 +309,7 @@ export default {
...state, ...state,
skus: [], skus: [],
attrTree: [], attrTree: [],
spu: {},
} }
}, },
changeLoading(state, { payload }) { changeLoading(state, { payload }) {

View File

@ -5,12 +5,8 @@ import React, {PureComponent, Fragment, Component} from 'react';
// import fs from 'fs'; // import fs from 'fs';
import { connect } from 'dva'; import { connect } from 'dva';
import moment from 'moment'; import moment from 'moment';
import {Card, Form, Input, Radio, Button, Modal, Select, Upload, Icon, Spin} from 'antd'; import {Card, Form, Input, Radio, Button, Modal, Select, Upload, Icon, Spin, TreeSelect} from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper'; import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import 'braft-editor/dist/index.css'
import BraftEditor from 'braft-editor'
import { ContentUtils } from 'braft-utils'
import { ImageUtils } from 'braft-finder'
// import * as qiniu from 'qiniu-js' // import * as qiniu from 'qiniu-js'
// import uuid from 'js-uuid'; // import uuid from 'js-uuid';
@ -23,13 +19,14 @@ import PicturesWall from "../../components/Image/PicturesWall";
import {fileGetQiniuToken} from "../../services/admin"; import {fileGetQiniuToken} from "../../services/admin";
import uuid from "js-uuid"; import uuid from "js-uuid";
import * as qiniu from "qiniu-js"; import * as qiniu from "qiniu-js";
import HtmlEditor from "../../components/Editor/HtmlEditor";
const FormItem = Form.Item; const FormItem = Form.Item;
const RadioGroup = Radio.Group; const RadioGroup = Radio.Group;
const Option = Select.Option; const Option = Select.Option;
// roleList // roleList
@connect(({ productAttrList, productSpuAddOrUpdate, }) => ({ @connect(({ productAttrList, productSpuAddOrUpdate, productCategoryList }) => ({
// list: productSpuList.list.spus, // list: productSpuList.list.spus,
// loading: loading.models.productSpuList, // loading: loading.models.productSpuList,
productAttrList, productAttrList,
@ -39,6 +36,7 @@ const Option = Select.Option;
spu: productSpuAddOrUpdate.spu, spu: productSpuAddOrUpdate.spu,
attrTree: productSpuAddOrUpdate.attrTree, attrTree: productSpuAddOrUpdate.attrTree,
skus: productSpuAddOrUpdate.skus, skus: productSpuAddOrUpdate.skus,
categoryTree: productCategoryList.list,
})) }))
@Form.create() @Form.create()
@ -47,12 +45,16 @@ class ProductSpuAddOrUpdate extends Component {
// modalVisible: false, // modalVisible: false,
modalType: 'add', //add update modalType: 'add', //add update
// initValues: {}, // initValues: {},
editorState: BraftEditor.createEditorState(null), htmlEditor: undefined,
}; };
componentDidMount() { componentDidMount() {
const { dispatch } = this.props; const { dispatch } = this.props;
const that = this; const that = this;
// 重置表单
dispatch({
type: 'productSpuAddOrUpdate/clear',
});
// 判断是否是更新 // 判断是否是更新
const params = new URLSearchParams(this.props.location.search); const params = new URLSearchParams(this.props.location.search);
if (params.get("id")) { if (params.get("id")) {
@ -66,6 +68,8 @@ class ProductSpuAddOrUpdate extends Component {
payload: parseInt(id), payload: parseInt(id),
callback: function (data) { callback: function (data) {
that.refs.picturesWall.setUrls(data.picUrls); // TODO 后续找找,有没更合适的做法 that.refs.picturesWall.setUrls(data.picUrls); // TODO 后续找找,有没更合适的做法
// debugger;
that.state.htmlEditor.setHtml(data.description);
} }
}) })
} }
@ -78,53 +82,13 @@ class ProductSpuAddOrUpdate extends Component {
pageSize: 10, pageSize: 10,
}, },
}); });
// 重置表单 // 获得商品分类
dispatch({ dispatch({
type: 'productSpuAddOrUpdate/clear', type: 'productCategoryList/tree',
}) payload: {},
});
} }
handleChange = (editorState) => {
this.setState({ editorState })
};
uploadHandler = async (param) => {
if (!param.file) {
return false
}
debugger;
const tokenResult = await fileGetQiniuToken();
if (tokenResult.code !== 0) {
alert('获得七牛上传 Token 失败');
return false;
}
let token = tokenResult.data;
let that = this;
const reader = new FileReader();
const file = param.file;
reader.readAsArrayBuffer(file);
let fileData = null;
reader.onload = (e) => {
let key = uuid.v4(); // TODO 芋艿可能后面要优化。MD5
let observable = qiniu.upload(file, key, token); // TODO 芋艿,最后后面去掉 qiniu 的库依赖,直接 http 请求,这样更轻量
observable.subscribe(function () {
// next
}, function (e) {
// error
// TODO 芋艿,后续补充
// debugger;
}, function (response) {
// complete
that.setState({
editorState: ContentUtils.insertMedias(that.state.editorState, [{
type: 'IMAGE',
url: 'http://static.shop.iocoder.cn/' + response.key,
}])
})
});
}
};
handleAddAttr = e => { handleAddAttr = e => {
// alert('你猜'); // alert('你猜');
const { dispatch } = this.props; const { dispatch } = this.props;
@ -139,6 +103,11 @@ class ProductSpuAddOrUpdate extends Component {
e.preventDefault(); e.preventDefault();
const { skus, dispatch } = this.props; const { skus, dispatch } = this.props;
const { modalType, id } = this.state; const { modalType, id } = this.state;
if (this.state.htmlEditor.isEmpty()) {
alert('请设置商品描述!');
return;
}
const description = this.state.htmlEditor.getHtml();
// 获得图片 // 获得图片
let picUrls = this.refs.picturesWall.getUrls(); // TODO 芋艿,后续找找其他做法 let picUrls = this.refs.picturesWall.getUrls(); // TODO 芋艿,后续找找其他做法
if (picUrls.length === 0) { if (picUrls.length === 0) {
@ -166,9 +135,11 @@ class ProductSpuAddOrUpdate extends Component {
alert('请设置商品规格!'); alert('请设置商品规格!');
return; return;
} }
// debugger; // debugger;
this.props.form.validateFields((err, values) => { this.props.form.validateFields((err, values) => {
// debugger; // debugger;
// 获得富文本编辑的描述
if (!err) { if (!err) {
if (modalType === 'add') { if (modalType === 'add') {
dispatch({ dispatch({
@ -177,7 +148,8 @@ class ProductSpuAddOrUpdate extends Component {
body: { body: {
...values, ...values,
picUrls: picUrls.join(','), picUrls: picUrls.join(','),
skuStr: JSON.stringify(skuStr) skuStr: JSON.stringify(skuStr),
description,
} }
}, },
}); });
@ -189,7 +161,8 @@ class ProductSpuAddOrUpdate extends Component {
...values, ...values,
id, id,
picUrls: picUrls.join(','), picUrls: picUrls.join(','),
skuStr: JSON.stringify(skuStr) skuStr: JSON.stringify(skuStr),
description,
} }
}, },
}); });
@ -201,27 +174,26 @@ class ProductSpuAddOrUpdate extends Component {
render() { render() {
// debugger; // debugger;
const { form, skus, attrTree, allAttrTree, loading, spu, dispatch } = this.props; const { form, skus, attrTree, allAttrTree, loading, spu, categoryTree, dispatch } = this.props;
// const that = this; // const that = this;
const controls = ['bold', 'italic', 'underline', 'text-color', 'separator', 'link', 'separator'];
const extendControls = [ // 处理分类筛选
{ const buildSelectTree = (list) => {
key: 'antd-uploader', return list.map(item => {
type: 'component', let children = [];
component: ( if (item.children) {
<Upload children = buildSelectTree(item.children);
accept="image/*" }
showUploadList={false} return {
customRequest={this.uploadHandler} title: item.name,
> value: item.id,
{/* 这里的按钮最好加上type="button"以避免在表单容器中触发表单提交用Antd的Button组件则无需如此 */} key: item.id,
<button type="button" className="control-item button upload-button" data-title="插入图片"> children,
<Icon type="picture" theme="filled" /> selectable: item.pid > 0
</button> };
</Upload> });
) };
} let categoryTreeSelect = buildSelectTree(categoryTree);
];
// 添加规格 // 添加规格
// debugger; // debugger;
@ -254,6 +226,7 @@ class ProductSpuAddOrUpdate extends Component {
dispatch: dispatch, dispatch: dispatch,
}; };
// console.log(productSkuProps); // console.log(productSkuProps);
// let htmlEditor = undefined;
return ( return (
<PageHeaderWrapper title=""> <PageHeaderWrapper title="">
@ -275,8 +248,16 @@ class ProductSpuAddOrUpdate extends Component {
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="分类编号"> <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="分类编号">
{form.getFieldDecorator('cid', { {form.getFieldDecorator('cid', {
rules: [{ required: true, message: '请输入分类编号!' }], rules: [{ required: true, message: '请输入分类编号!' }],
initialValue: spu.cid, // TODO 芋艿,和面做成下拉框 initialValue: spu.cid,
})(<Input placeholder="请输入" />)} })(
<TreeSelect
showSearch
style={{ width: 300 }}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={categoryTreeSelect}
placeholder="选择父分类"
/>
)}
</FormItem> </FormItem>
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="商品主图" <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="商品主图"
extra="建议尺寸800*800PX单张大小不超过 2M最多可上传 10 张"> extra="建议尺寸800*800PX单张大小不超过 2M最多可上传 10 张">
@ -307,21 +288,8 @@ class ProductSpuAddOrUpdate extends Component {
<ProductSkuAddOrUpdateTable {...productSkuProps} /> <ProductSkuAddOrUpdateTable {...productSkuProps} />
</FormItem> : '' </FormItem> : ''
} }
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="商品描述"> <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="商品描述" required={false}>
{form.getFieldDecorator('description', { <HtmlEditor ref={(node) => this.state.htmlEditor = node} />
rules: [{ required: true, message: '请输入商品描述!' }],
initialValue: spu.description, // TODO 修改
})(
<div style={{border: '1px solid #d1d1d1', 'border-radius': '5px'}}>
<BraftEditor
value={this.state.editorState}
onChange={this.handleChange}
controls={controls}
extendControls={extendControls}
contentStyle={{height: 200}}
/>
</div>
)}
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }} onSubmit={this.handleSubmit}>保存</Button> <Button type="primary" htmlType="submit" style={{ marginLeft: 8 }} onSubmit={this.handleSubmit}>保存</Button>
</FormItem> </FormItem>
</Form> </Form>

View File

@ -9,7 +9,7 @@
- [ ] 店铺资产 - [ ] 店铺资产
- [ ] TODO 未开始 - [ ] TODO 未开始
- [ ] 商品管理 - [ ] 商品管理
- [ ] 发布商品 - [x] 发布商品
- [ ] 商品管理 - [ ] 商品管理
- [x] 展示类目 - [x] 展示类目
- [ ] 品牌管理 - [ ] 品牌管理

View File

@ -21,6 +21,7 @@ public enum ProductErrorCodeEnum {
PRODUCT_SPU_ATTR_NUMBERS_MUST_BE_EQUALS(1003002001, "一个 Spu 下的每个 Sku ,其规格数必须一致"), PRODUCT_SPU_ATTR_NUMBERS_MUST_BE_EQUALS(1003002001, "一个 Spu 下的每个 Sku ,其规格数必须一致"),
PRODUCT_SPU_SKU__NOT_DUPLICATE(1003002002, "一个 Spu 下的每个 Sku ,必须不重复"), PRODUCT_SPU_SKU__NOT_DUPLICATE(1003002002, "一个 Spu 下的每个 Sku ,必须不重复"),
PRODUCT_SPU_NOT_EXISTS(1003002003, "Spu 不存在"), PRODUCT_SPU_NOT_EXISTS(1003002003, "Spu 不存在"),
PRODUCT_SPU_CATEGORY_MUST_BE_LEVEL2(1003002003, "Spu 只能添加在二级分类下"),
// ========== PRODUCT ATTR + ATTR_VALUE 模块 ========== // ========== PRODUCT ATTR + ATTR_VALUE 模块 ==========
PRODUCT_ATTR_VALUE_NOT_EXIST(1003003000, "商品属性值不存在"), PRODUCT_ATTR_VALUE_NOT_EXIST(1003003000, "商品属性值不存在"),

View File

@ -7,6 +7,7 @@ import cn.iocoder.common.framework.util.StringUtil;
import cn.iocoder.common.framework.vo.CommonResult; import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.product.api.ProductSpuService; import cn.iocoder.mall.product.api.ProductSpuService;
import cn.iocoder.mall.product.api.bo.*; import cn.iocoder.mall.product.api.bo.*;
import cn.iocoder.mall.product.api.constant.ProductCategoryConstants;
import cn.iocoder.mall.product.api.constant.ProductErrorCodeEnum; import cn.iocoder.mall.product.api.constant.ProductErrorCodeEnum;
import cn.iocoder.mall.product.api.constant.ProductSpuConstants; import cn.iocoder.mall.product.api.constant.ProductSpuConstants;
import cn.iocoder.mall.product.api.dto.ProductSkuAddOrUpdateDTO; import cn.iocoder.mall.product.api.dto.ProductSkuAddOrUpdateDTO;
@ -107,6 +108,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
if (validCategoryResult.isError()) { if (validCategoryResult.isError()) {
return CommonResult.error(validCategoryResult); return CommonResult.error(validCategoryResult);
} }
if (ProductCategoryConstants.PID_ROOT.equals(validCategoryResult.getData().getPid())) { // 商品只能添加到二级分类下
return ServiceExceptionUtil.error(ProductErrorCodeEnum.PRODUCT_SPU_CATEGORY_MUST_BE_LEVEL2.getCode());
}
// 校验规格是否存在 // 校验规格是否存在
Set<Integer> productAttrValueIds = new HashSet<>(); Set<Integer> productAttrValueIds = new HashSet<>();
productSpuAddDTO.getSkus().forEach(productSkuAddDTO -> productAttrValueIds.addAll(productSkuAddDTO.getAttrs())); productSpuAddDTO.getSkus().forEach(productSkuAddDTO -> productAttrValueIds.addAll(productSkuAddDTO.getAttrs()));
@ -167,6 +171,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
if (validCategoryResult.isError()) { if (validCategoryResult.isError()) {
return CommonResult.error(validCategoryResult); return CommonResult.error(validCategoryResult);
} }
if (ProductCategoryConstants.PID_ROOT.equals(validCategoryResult.getData().getPid())) { // 商品只能添加到二级分类下
return ServiceExceptionUtil.error(ProductErrorCodeEnum.PRODUCT_SPU_CATEGORY_MUST_BE_LEVEL2.getCode());
}
// 校验规格是否存在 // 校验规格是否存在
Set<Integer> productAttrValueIds = new HashSet<>(); Set<Integer> productAttrValueIds = new HashSet<>();
productSpuUpdateDTO.getSkus().forEach(productSkuAddDTO -> productAttrValueIds.addAll(productSkuAddDTO.getAttrs())); productSpuUpdateDTO.getSkus().forEach(productSkuAddDTO -> productAttrValueIds.addAll(productSkuAddDTO.getAttrs()));