diff --git a/admin-web/src/components/Editor/HtmlEditor.js b/admin-web/src/components/Editor/HtmlEditor.js
new file mode 100644
index 000000000..7cd8d1611
--- /dev/null
+++ b/admin-web/src/components/Editor/HtmlEditor.js
@@ -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: (
+
+ {/* 这里的按钮最好加上type="button",以避免在表单容器中触发表单提交,用Antd的Button组件则无需如此 */}
+
+
+ )
+ }
+ ];
+
+ return (
+
+
+
+ )
+ }
+
+}
+
+
+{/**/}
+
+//
+
+export default HtmlEditor;
diff --git a/admin-web/src/components/Image/PicturesWall.js b/admin-web/src/components/Image/PicturesWall.js
index e8b4176e6..a51f0785a 100644
--- a/admin-web/src/components/Image/PicturesWall.js
+++ b/admin-web/src/components/Image/PicturesWall.js
@@ -77,7 +77,6 @@ class PicturesWall extends React.Component {
// });
// 使用 FileReader 将上传的文件转换成二进制流,满足 'application/octet-stream' 格式的要求
- debugger;
const reader = new FileReader();
reader.readAsArrayBuffer(file);
let fileData = null;
diff --git a/admin-web/src/components/Product/ProductAttrSelectFormItem.js b/admin-web/src/components/Product/ProductAttrSelectFormItem.js
index 958e3e06e..aa01fcfc1 100644
--- a/admin-web/src/components/Product/ProductAttrSelectFormItem.js
+++ b/admin-web/src/components/Product/ProductAttrSelectFormItem.js
@@ -68,7 +68,7 @@ class AttrValueSelect extends Select {
export default class ProductAttrSelectFormItem extends PureComponent {
handleSelectAttr = (value, option) => {
- debugger;
+ // debugger;
// console.log(value);
// console.log(option);
// debugger;
diff --git a/admin-web/src/models/product/productSpuAddOrUpdate.js b/admin-web/src/models/product/productSpuAddOrUpdate.js
index e4468048c..bfbf544ed 100644
--- a/admin-web/src/models/product/productSpuAddOrUpdate.js
+++ b/admin-web/src/models/product/productSpuAddOrUpdate.js
@@ -35,7 +35,8 @@ export default {
// price: // 价格
// quantity: // 数量
// }
- ]
+ ],
+
},
effects: {
@@ -308,6 +309,7 @@ export default {
...state,
skus: [],
attrTree: [],
+ spu: {},
}
},
changeLoading(state, { payload }) {
diff --git a/admin-web/src/pages/Product/ProductSpuAddOrUpdate.js b/admin-web/src/pages/Product/ProductSpuAddOrUpdate.js
index 2b0816c17..d1741550c 100644
--- a/admin-web/src/pages/Product/ProductSpuAddOrUpdate.js
+++ b/admin-web/src/pages/Product/ProductSpuAddOrUpdate.js
@@ -5,12 +5,8 @@ import React, {PureComponent, Fragment, Component} from 'react';
// import fs from 'fs';
import { connect } from 'dva';
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 '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 uuid from 'js-uuid';
@@ -23,13 +19,14 @@ import PicturesWall from "../../components/Image/PicturesWall";
import {fileGetQiniuToken} from "../../services/admin";
import uuid from "js-uuid";
import * as qiniu from "qiniu-js";
+import HtmlEditor from "../../components/Editor/HtmlEditor";
const FormItem = Form.Item;
const RadioGroup = Radio.Group;
const Option = Select.Option;
// roleList
-@connect(({ productAttrList, productSpuAddOrUpdate, }) => ({
+@connect(({ productAttrList, productSpuAddOrUpdate, productCategoryList }) => ({
// list: productSpuList.list.spus,
// loading: loading.models.productSpuList,
productAttrList,
@@ -39,6 +36,7 @@ const Option = Select.Option;
spu: productSpuAddOrUpdate.spu,
attrTree: productSpuAddOrUpdate.attrTree,
skus: productSpuAddOrUpdate.skus,
+ categoryTree: productCategoryList.list,
}))
@Form.create()
@@ -47,12 +45,16 @@ class ProductSpuAddOrUpdate extends Component {
// modalVisible: false,
modalType: 'add', //add update
// initValues: {},
- editorState: BraftEditor.createEditorState(null),
+ htmlEditor: undefined,
};
componentDidMount() {
const { dispatch } = this.props;
const that = this;
+ // 重置表单
+ dispatch({
+ type: 'productSpuAddOrUpdate/clear',
+ });
// 判断是否是更新
const params = new URLSearchParams(this.props.location.search);
if (params.get("id")) {
@@ -66,6 +68,8 @@ class ProductSpuAddOrUpdate extends Component {
payload: parseInt(id),
callback: function (data) {
that.refs.picturesWall.setUrls(data.picUrls); // TODO 后续找找,有没更合适的做法
+ // debugger;
+ that.state.htmlEditor.setHtml(data.description);
}
})
}
@@ -78,53 +82,13 @@ class ProductSpuAddOrUpdate extends Component {
pageSize: 10,
},
});
- // 重置表单
+ // 获得商品分类
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 => {
// alert('你猜');
const { dispatch } = this.props;
@@ -139,6 +103,11 @@ class ProductSpuAddOrUpdate extends Component {
e.preventDefault();
const { skus, dispatch } = this.props;
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 芋艿,后续找找其他做法
if (picUrls.length === 0) {
@@ -166,9 +135,11 @@ class ProductSpuAddOrUpdate extends Component {
alert('请设置商品规格!');
return;
}
+
// debugger;
this.props.form.validateFields((err, values) => {
// debugger;
+ // 获得富文本编辑的描述
if (!err) {
if (modalType === 'add') {
dispatch({
@@ -177,7 +148,8 @@ class ProductSpuAddOrUpdate extends Component {
body: {
...values,
picUrls: picUrls.join(','),
- skuStr: JSON.stringify(skuStr)
+ skuStr: JSON.stringify(skuStr),
+ description,
}
},
});
@@ -189,7 +161,8 @@ class ProductSpuAddOrUpdate extends Component {
...values,
id,
picUrls: picUrls.join(','),
- skuStr: JSON.stringify(skuStr)
+ skuStr: JSON.stringify(skuStr),
+ description,
}
},
});
@@ -201,27 +174,26 @@ class ProductSpuAddOrUpdate extends Component {
render() {
// 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 controls = ['bold', 'italic', 'underline', 'text-color', 'separator', 'link', 'separator'];
- const extendControls = [
- {
- key: 'antd-uploader',
- type: 'component',
- component: (
-
- {/* 这里的按钮最好加上type="button",以避免在表单容器中触发表单提交,用Antd的Button组件则无需如此 */}
-
-
- )
- }
- ];
+
+ // 处理分类筛选
+ const buildSelectTree = (list) => {
+ return list.map(item => {
+ let children = [];
+ if (item.children) {
+ children = buildSelectTree(item.children);
+ }
+ return {
+ title: item.name,
+ value: item.id,
+ key: item.id,
+ children,
+ selectable: item.pid > 0
+ };
+ });
+ };
+ let categoryTreeSelect = buildSelectTree(categoryTree);
// 添加规格
// debugger;
@@ -254,6 +226,7 @@ class ProductSpuAddOrUpdate extends Component {
dispatch: dispatch,
};
// console.log(productSkuProps);
+ // let htmlEditor = undefined;
return (
@@ -275,8 +248,16 @@ class ProductSpuAddOrUpdate extends Component {
{form.getFieldDecorator('cid', {
rules: [{ required: true, message: '请输入分类编号!' }],
- initialValue: spu.cid, // TODO 芋艿,和面做成下拉框
- })()}
+ initialValue: spu.cid,
+ })(
+
+ )}
@@ -307,21 +288,8 @@ class ProductSpuAddOrUpdate extends Component {
: ''
}
-
- {form.getFieldDecorator('description', {
- rules: [{ required: true, message: '请输入商品描述!' }],
- initialValue: spu.description, // TODO 修改
- })(
-
-
-
- )}
+
+ this.state.htmlEditor = node} />
diff --git a/docs/guides/功能列表/功能列表-管理后台.md b/docs/guides/功能列表/功能列表-管理后台.md
index e8290813f..7dd3ab874 100644
--- a/docs/guides/功能列表/功能列表-管理后台.md
+++ b/docs/guides/功能列表/功能列表-管理后台.md
@@ -9,7 +9,7 @@
- [ ] 店铺资产
- [ ] TODO 未开始
- [ ] 商品管理
- - [ ] 发布商品
+ - [x] 发布商品
- [ ] 商品管理
- [x] 展示类目
- [ ] 品牌管理
diff --git a/product/product-service-api/src/main/java/cn/iocoder/mall/product/api/constant/ProductErrorCodeEnum.java b/product/product-service-api/src/main/java/cn/iocoder/mall/product/api/constant/ProductErrorCodeEnum.java
index 34307d844..f1d914f87 100644
--- a/product/product-service-api/src/main/java/cn/iocoder/mall/product/api/constant/ProductErrorCodeEnum.java
+++ b/product/product-service-api/src/main/java/cn/iocoder/mall/product/api/constant/ProductErrorCodeEnum.java
@@ -21,6 +21,7 @@ public enum ProductErrorCodeEnum {
PRODUCT_SPU_ATTR_NUMBERS_MUST_BE_EQUALS(1003002001, "一个 Spu 下的每个 Sku ,其规格数必须一致"),
PRODUCT_SPU_SKU__NOT_DUPLICATE(1003002002, "一个 Spu 下的每个 Sku ,必须不重复"),
PRODUCT_SPU_NOT_EXISTS(1003002003, "Spu 不存在"),
+ PRODUCT_SPU_CATEGORY_MUST_BE_LEVEL2(1003002003, "Spu 只能添加在二级分类下"),
// ========== PRODUCT ATTR + ATTR_VALUE 模块 ==========
PRODUCT_ATTR_VALUE_NOT_EXIST(1003003000, "商品属性值不存在"),
diff --git a/product/product-service-impl/src/main/java/cn/iocoder/mall/product/service/ProductSpuServiceImpl.java b/product/product-service-impl/src/main/java/cn/iocoder/mall/product/service/ProductSpuServiceImpl.java
index 94396ebfc..7c3208297 100644
--- a/product/product-service-impl/src/main/java/cn/iocoder/mall/product/service/ProductSpuServiceImpl.java
+++ b/product/product-service-impl/src/main/java/cn/iocoder/mall/product/service/ProductSpuServiceImpl.java
@@ -7,6 +7,7 @@ import cn.iocoder.common.framework.util.StringUtil;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.product.api.ProductSpuService;
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.ProductSpuConstants;
import cn.iocoder.mall.product.api.dto.ProductSkuAddOrUpdateDTO;
@@ -107,6 +108,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
if (validCategoryResult.isError()) {
return CommonResult.error(validCategoryResult);
}
+ if (ProductCategoryConstants.PID_ROOT.equals(validCategoryResult.getData().getPid())) { // 商品只能添加到二级分类下
+ return ServiceExceptionUtil.error(ProductErrorCodeEnum.PRODUCT_SPU_CATEGORY_MUST_BE_LEVEL2.getCode());
+ }
// 校验规格是否存在
Set productAttrValueIds = new HashSet<>();
productSpuAddDTO.getSkus().forEach(productSkuAddDTO -> productAttrValueIds.addAll(productSkuAddDTO.getAttrs()));
@@ -167,6 +171,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
if (validCategoryResult.isError()) {
return CommonResult.error(validCategoryResult);
}
+ if (ProductCategoryConstants.PID_ROOT.equals(validCategoryResult.getData().getPid())) { // 商品只能添加到二级分类下
+ return ServiceExceptionUtil.error(ProductErrorCodeEnum.PRODUCT_SPU_CATEGORY_MUST_BE_LEVEL2.getCode());
+ }
// 校验规格是否存在
Set productAttrValueIds = new HashSet<>();
productSpuUpdateDTO.getSkus().forEach(productSkuAddDTO -> productAttrValueIds.addAll(productSkuAddDTO.getAttrs()));