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()));