diff --git a/admin-web/src/components/Image/PicturesWall.js b/admin-web/src/components/Image/PicturesWall.js index ff641f02..4011db04 100644 --- a/admin-web/src/components/Image/PicturesWall.js +++ b/admin-web/src/components/Image/PicturesWall.js @@ -27,6 +27,12 @@ class PicturesWall extends React.Component { ], }; + componentDidMount() { + if (this.props.urls) { + this.setUrls(this.props.urls); + } + } + handleCancel = () => this.setState({ previewVisible: false }) handlePreview = (file) => { @@ -137,6 +143,14 @@ class PicturesWall extends React.Component { return urls; }; + getUrl = () => { + let urls = this.getUrls(); + if (urls && urls.length > 0) { + return urls[0]; + } + return undefined; + }; + setUrls = (urls) => { // let urls = this.props.urls; if (urls) { @@ -188,7 +202,7 @@ class PicturesWall extends React.Component { PicturesWall.propTypes = { maxLength: Number, // 最大照片墙图片数量 - // urls: String[], // 初始图片列表 + urls: Array, // 初始图片列表 }; export default PicturesWall; diff --git a/admin-web/src/models/admin/adminList.js b/admin-web/src/models/admin/adminList.js index a5cd18ce..c9c433ec 100644 --- a/admin-web/src/models/admin/adminList.js +++ b/admin-web/src/models/admin/adminList.js @@ -69,7 +69,6 @@ export default { }); }, * add({ payload }, { call, put }) { - const { callback, body } = payload; // 显示加载中 yield put({ type: 'changeModalLoading', @@ -77,6 +76,7 @@ export default { }); // 请求 + const { callback, body } = payload; const response = yield call(addAdmin, body); // 响应 if (response.code === 0) { diff --git a/admin-web/src/models/product/productCategoryList.js b/admin-web/src/models/product/productCategoryList.js index 67a264bc..64e1072f 100644 --- a/admin-web/src/models/product/productCategoryList.js +++ b/admin-web/src/models/product/productCategoryList.js @@ -5,61 +5,129 @@ export default { namespace: 'productCategoryList', state: { + // 分页列表相关 list: [], + listLoading: false, + + // 添加 or 修改表单相关 + modalVisible: false, + modalType: undefined, // 'add' or 'update' 表单 + formVals: {}, // 当前表单值 + modalLoading: false, + }, effects: { - *add({ payload }, { call, put }) { - const { callback, body } = payload; - const response = yield call(productCategoryAdd, body); - if (callback) { - callback(response); - } - yield put({ - type: 'tree', - payload: {}, - }); - }, - *update({ payload }, { call, put }) { - const { callback, body } = payload; - const response = yield call(productCategoryUpdate, body); - if (callback) { - callback(response); - } - yield put({ - type: 'tree', - payload: {}, - }); - }, - *updateStatus({ payload }, { call, put }) { - const { callback, body } = payload; - const response = yield call(productCategoryUpdateStatus, body); - if (callback) { - callback(response); - } - yield put({ - type: 'tree', - payload: {}, - }); - }, - *delete({ payload }, { call, put }) { - const response = yield call(productCategoryDelete, payload); - message.info('删除成功!'); - yield put({ - type: 'tree', - payload: {}, - }); - }, + // 查询列表 *tree({ payload }, { call, put }) { + // 显示加载中 + yield put({ + type: 'changeListLoading', + payload: true, + }); + + // 请求 const { queryParams } = payload; + // 响应 const response = yield call(productCategoryTree, queryParams); - message.info('查询成功!'); yield put({ type: 'treeSuccess', payload: { list: response.data, }, }); + + // 隐藏加载中 + yield put({ + type: 'changeListLoading', + payload: false, + }); + }, + *add({ payload }, { call, put }) { + // 显示加载中 + yield put({ + type: 'changeModalLoading', + payload: true, + }); + + // 请求 + const { callback, body } = payload; + const response = yield call(productCategoryAdd, body); + // 响应 + if (response.code === 0) { + if (callback) { + callback(response); + } + // 刷新列表 + yield put({ + type: 'tree', + payload: {}, + }); + } + + // 隐藏加载中 + yield put({ + type: 'changeModalLoading', + payload: false, + }); + }, + *update({ payload }, { call, put }) { + // 显示加载中 + yield put({ + type: 'changeModalLoading', + payload: true, + }); + + // 请求 + const { callback, body } = payload; + const response = yield call(productCategoryUpdate, body); + // 响应 + if (response.code === 0) { + if (callback) { + callback(response); + } + // 刷新列表 + yield put({ + type: 'tree', + payload: {}, + }); + } + + // 隐藏加载中 + yield put({ + type: 'changeModalLoading', + payload: false, + }); + }, + *updateStatus({ payload }, { call, put }) { + // 请求 + const { callback, body } = payload; + // 响应 + const response = yield call(productCategoryUpdateStatus, body); + if(response.code === 0) { + message.info('更新状态成功!'); + if (callback) { + callback(response); + } + yield put({ + type: 'tree', + payload: {}, + }); + } + }, + *delete({ payload }, { call, put }) { + // 响应 + const response = yield call(productCategoryDelete, payload); + if(response.code === 0) { + message.info('删除成功!'); + if (callback) { + callback(response); + } + yield put({ + type: 'tree', + payload: {}, + }); + } }, }, @@ -70,5 +138,25 @@ export default { ...payload, }; }, + // 修改加载中的状态 + changeModalLoading(state, { payload }) { + return { + ...state, + modalLoading: payload, + }; + }, + changeListLoading(state, { payload }) { + return { + ...state, + listLoading: payload, + }; + }, + // 设置所有属性 + setAll(state, { payload }) { + return { + ...state, + ...payload, + }; + } }, }; diff --git a/admin-web/src/pages/Admin/AdminList.js b/admin-web/src/pages/Admin/AdminList.js index e0d080f7..8823253d 100644 --- a/admin-web/src/pages/Admin/AdminList.js +++ b/admin-web/src/pages/Admin/AdminList.js @@ -213,7 +213,7 @@ const AddOrUpdateForm = Form.create()(props => { // 清空表单 form.resetFields(); // 提示 - message.success('添加成功'); + message.success('新建成功'); // 关闭弹窗 handleModalVisible(); }, @@ -232,7 +232,7 @@ const AddOrUpdateForm = Form.create()(props => { // 清空表单 form.resetFields(); // 提示 - message.success('更新成功'); + message.success('编辑成功'); // 关闭弹窗 handleModalVisible(); }, @@ -242,7 +242,7 @@ const AddOrUpdateForm = Form.create()(props => { }); }; - const title = modalType === 'add' ? '新建管理员' : '更新管理员'; + const title = modalType === 'add' ? '新建员工' : '更新员工'; return ( { // 主界面 @Form.create() class AdminList extends PureComponent { - state = { - // 分配角色弹窗 - modalRoleVisible: false, - modalRoleRow: {}, - }; componentDidMount() { const { dispatch } = this.props; @@ -461,6 +456,7 @@ class AdminList extends PureComponent { dispatch, handleModalVisible: this.handleModalVisible, // Function }; + // 分配角色 Modal 属性 const roleAssignModal = { loading: roleAssignLoading, @@ -485,7 +481,7 @@ class AdminList extends PureComponent { type="primary" onClick={() => this.handleModalVisible(true, 'add', {})} > - 新建管理员 + 新建员工 @@ -500,4 +496,4 @@ class AdminList extends PureComponent { } } -export default AdminList; \ No newline at end of file +export default AdminList; diff --git a/admin-web/src/pages/Product/ProductCategoryList.js b/admin-web/src/pages/Product/ProductCategoryList.js index b5738a29..076c078d 100644 --- a/admin-web/src/pages/Product/ProductCategoryList.js +++ b/admin-web/src/pages/Product/ProductCategoryList.js @@ -3,37 +3,208 @@ import React, { PureComponent, Fragment } from 'react'; import { connect } from 'dva'; import moment from 'moment'; -import {Card, Form, Input, Select, Button, Modal, message, Table, Divider, InputNumber} from 'antd'; +import {Card, Form, Input, Select, Button, Modal, message, Table, Divider, InputNumber, TreeSelect, Spin} from 'antd'; import PageHeaderWrapper from '@/components/PageHeaderWrapper'; import styles from './ProductCategoryList.less'; +import PicturesWall from "../../components/Image/PicturesWall"; const FormItem = Form.Item; const { Option } = Select; const status = ['未知', '开启', '禁用']; -// 添加 form 表单 -const CreateForm = Form.create()(props => { - const { modalVisible, form, handleAdd, handleModalVisible, modalType, initValues } = props; +// 列表 +function List({ dataSource, loading, dispatch, + handleModalVisible}) { + + function handleStatus(record) { + Modal.confirm({ + title: record.status === 1 ? '确认禁用?' : '确认开启?', + content: `${record.name}`, + onOk() { + dispatch({ + type: 'productCategoryList/updateStatus', + payload: { + body: { + id: record.id, + status: record.status === 1 ? 2 : 1, + } + }, + }); + }, + onCancel() {}, + }); + } + + function handleDelete(record) { + Modal.confirm({ + title: `确认删除?`, + content: `${record.name}`, + onOk() { + dispatch({ + type: 'productCategoryList/delete', + payload: { + id: record.id, + }, + }); + }, + onCancel() {}, + }); + } + + const columns = [ + // { + // title: 'id', + // dataIndex: 'id', + // render: text => {text}, + // }, + { + title: '分类名称', + dataIndex: 'name', + }, + { + title: '图片', + dataIndex: 'picUrl', + render(val) { + return ; + }, + }, + { + title: '排序值', + dataIndex: 'sort', + }, + { + title: '状态', + dataIndex: 'status', + render(val) { + return {status[val]}; + }, + }, + { + title: '描述', + dataIndex: 'description', + }, + { + title: '创建时间', + dataIndex: 'createTime', + render: val => {moment(val).format('YYYY-MM-DD')}, + }, + { + title: '操作', + render: (text, record) => { + const statusText = record.status === 1 ? '禁用' : '开启'; + return ( + + handleModalVisible(true, 'update', record)}>编辑 + + handleStatus(record)}> + {statusText} + + + { + record.status === 2 ? ( + + + handleDelete(record)}> + 删除 + + + ) : '' + } + + ); + } + }, + ]; + + return ( + + ) +} + +// 新建 form 表单 +const AddOrUpdateForm = Form.create()(props => { + const { dispatch, loading, modalVisible, form, handleModalVisible, modalType, categoryTree, formVals } = props; + let picturesWall = null; const okHandle = () => { - form.validateFields((err, fieldsValue) => { + form.validateFields((err, fields) => { if (err) return; - form.resetFields(); - handleAdd({ - fields: fieldsValue, - modalType, - initValues, - }); + if (modalType === 'add') { + dispatch({ + type: 'productCategoryList/add', + payload: { + body: { + ...fields, + picUrl: picturesWall ? picturesWall.getUrl() : undefined, + }, + callback: () => { + // 清空表单 + form.resetFields(); + // 提示 + message.success('新建成功'); + // 关闭弹窗 + handleModalVisible(); + }, + }, + }); + } else { + dispatch({ + type: 'productCategoryList/update', + payload: { + body: { + ...formVals, + ...fields, + picUrl: picturesWall ? picturesWall.getUrl() : undefined, + }, + callback: () => { + // 清空表单 + form.resetFields(); + // 提示 + message.success('编辑成功'); + // 关闭弹窗 + handleModalVisible(); + }, + }, + }); + } }); }; - const selectStyle = { - width: 200, - }; + function onPidChange(value) { + formVals.pid = parseInt(value); + } - const title = modalType === 'add' ? '添加一个 Resource' : '更新一个 Resource'; - const okText = modalType === 'add' ? '添加' : '更新'; + // 处理分类筛选 + 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, + }; + }); + }; + let categoryTreeSelect = buildSelectTree(categoryTree); + categoryTreeSelect.unshift({ + title: '无父分类', + value: 0, + key: 0, + children: [], + }); + + const title = modalType === 'add' ? '新建分类' : '编辑分类'; + const okText = modalType === 'add' ? '新建' : '编辑'; return ( { okText={okText} onCancel={() => handleModalVisible()} > - - {form.getFieldDecorator('name', { - rules: [{ required: true, message: '请输入分类名!', min: 2 }], - initialValue: initValues.name, - })()} - - - {form.getFieldDecorator('picUrl', { - initialValue: initValues.handler, - })()} - - - {form.getFieldDecorator('pid', { - rules: [{ required: true, message: '请输入父分类编号!' }], - initialValue: initValues.pid, - })()} - - - {form.getFieldDecorator('sort', { - rules: [{ required: true, message: '请输入排序值!' }], - initialValue: initValues.sort, - })()} - - - {form.getFieldDecorator('description', { - rules: [{ required: true, message: '请输入描述!' }], - initialValue: initValues.description, - })()} - + + + {form.getFieldDecorator('name', { + rules: [{ required: true, message: '请输入分类名称!', min: 2 }], + initialValue: formVals.name, + })()} + + + {form.getFieldDecorator('pid', { + rules: [{ required: true, message: '请选择父分类!' }], + initialValue: formVals.pid, + })( + + )} + + { + formVals.pid > 0 ? ( + + picturesWall = node} maxLength={1} /> + + ) : '' + } + + {form.getFieldDecorator('sort', { + rules: [{ required: true, message: '请输入排序值!' }], + initialValue: formVals.sort, + })()} + + + {form.getFieldDecorator('description', { + rules: [{ required: true, message: '请输入描述!' }], + initialValue: formVals.description, + })()} + + ); }); -@connect(({ productCategoryList, loading }) => ({ - productCategoryList, - list: productCategoryList.list, - loading: loading.models.productCategoryList, +@connect(({ productCategoryList, }) => ({ + ...productCategoryList, + // list: productCategoryList.list, + // loading: loading.models.productCategoryList, })) + @Form.create() class ProductCategoryList extends PureComponent { - state = { - modalVisible: false, - modalType: 'add', //add update - initValues: {}, - }; componentDidMount() { const { dispatch } = this.props; @@ -97,162 +278,41 @@ class ProductCategoryList extends PureComponent { }); } - handleModalVisible = (flag, modalType, initValues) => { - this.setState({ - modalVisible: !!flag, - initValues: initValues || {}, - modalType: modalType || 'add', + handleModalVisible = (modalVisible, modalType, record) => { + const { dispatch } = this.props; + dispatch({ + type: 'productCategoryList/setAll', + payload: { + modalVisible, + modalType, + formVals: record || {} + }, }); }; - handleAdd = ({ fields, modalType, initValues }) => { - const { dispatch } = this.props; - if (modalType === 'add') { - dispatch({ - type: 'productCategoryList/add', - payload: { - body: { - ...fields, - }, - callback: () => { - message.success('添加成功'); - this.handleModalVisible(); - }, - }, - }); - } else { - dispatch({ - type: 'productCategoryList/update', - payload: { - body: { - ...initValues, - ...fields, - }, - callback: () => { - message.success('更新成功'); - this.handleModalVisible(); - }, - }, - }); - } - }; - - handleStatus(row) { - const { dispatch, data } = this.props; - - const title = row.status === 1 ? '确认禁用?' : '确认开启?'; - const updateStatus = row.status === 1 ? 2 : 1; - - Modal.confirm({ - title: `${title}`, - content: `${row.name}`, - onOk() { - dispatch({ - type: 'productCategoryList/updateStatus', - payload: { - body: { - id: row.id, - status: updateStatus, - } - }, - }); - }, - onCancel() {}, - }); - } - - handleDelete(row) { - const { dispatch } = this.props; - Modal.confirm({ - title: `确认删除?`, - content: `${row.displayName}`, - onOk() { - dispatch({ - type: 'productCategoryList/delete', - payload: { - id: row.id, - }, - }); - }, - onCancel() {}, - }); - } - render() { - const { list } = this.props; - const { modalVisible, modalType, initValues } = this.state; - const parentMethods = { - handleAdd: this.handleAdd, - handleModalVisible: this.handleModalVisible, - modalType, - initValues, + const { dispatch, + list,listLoading, + modalVisible, modalType, formVals, modalLoading,} = this.props; + + // 列表属性 + const listProps = { + dataSource: list, + dispatch, + loading: listLoading, + handleModalVisible: this.handleModalVisible, // Function }; - const that = this; - const columns = [ - { - title: 'id', - dataIndex: 'id', - render: text => {text}, - }, - { - title: '分类名', - dataIndex: 'name', - }, - { - title: '分类图片', - dataIndex: 'picUrl', - render(val) { - return ; - }, - }, - { - title: '排序值', - dataIndex: 'sort', - }, - { - title: '创建时间', - dataIndex: 'createTime', - render: val => {moment(val).format('YYYY-MM-DD')}, - }, - { - title: '状态', - dataIndex: 'status', - render(val) { - return {status[val]}; - }, - }, - { - title: '描述', - dataIndex: 'description', - }, - { - title: '操作', - render: (text, record) => { - const statusText = record.status === 1 ? '禁用' : '开启'; - return ( - - this.handleModalVisible(true, 'update', record)}>更新 - - this.handleStatus(record)}> - {statusText} - - - { - record.status === 2 ? ( - - - this.handleDelete(record)}> - 删除 - - - ) : '' - } - - ); - } - }, - ]; + // 添加 or 编辑表单属性 + const addOrUpdateFormProps = { + modalVisible, + modalType, + formVals, + dispatch, + loading: modalLoading, + categoryTree: list, + handleModalVisible: this.handleModalVisible, // Function + }; return ( @@ -264,13 +324,14 @@ class ProductCategoryList extends PureComponent { type="primary" onClick={() => this.handleModalVisible(true, 'add', {})} > - 新建商品分类 + 新建分类 -
+ - + + ); } diff --git a/docs/guides/功能列表/功能列表-H5 商城.md b/docs/guides/功能列表/功能列表-H5 商城.md index e69de29b..1b7b7cdc 100644 --- a/docs/guides/功能列表/功能列表-H5 商城.md +++ b/docs/guides/功能列表/功能列表-H5 商城.md @@ -0,0 +1,30 @@ +如下是 onemall 的功能列表。 + +* 当功能被完成时,会标记已完成。 +* 未完成的功能,欢迎一起来开发。 + +- [ ] 首页 +- 商品相关 + - [ ] 分类列表 + - [ ] 商品搜索 + - [ ] 商品列表(基于分类) + - [ ] 商品列表(基于促销活动) + - [ ] 商品详情 + - [ ] 商品收藏 +- 订单相关 + - [ ] 下单(直接购买) + - [ ] 下单(购物车购买) + - [ ] 下单(拼团) + - [ ] 订单列表 + - [ ] 订单详情 + - [ ] 支付 + - [ ] 退款 + - [ ] 购物车 + - [ ] 收获地址 +- 营销相关 + - [ ] 优惠劵 + - [ ] 优惠码 +- 用户相关 + - [ ] 登陆 + - [ ] 注册 + - [ ] 个人信息 diff --git a/docs/guides/功能列表/功能列表-管理后台.md b/docs/guides/功能列表/功能列表-管理后台.md index 064fa2e2..0d221cdf 100644 --- a/docs/guides/功能列表/功能列表-管理后台.md +++ b/docs/guides/功能列表/功能列表-管理后台.md @@ -1,5 +1,10 @@ +如下是 onemall 的功能列表。 + +* 当功能被完成时,会标记已完成。 +* 未完成的功能,欢迎一起来开发。 + - [ ] 概述 TODO -- [ ] 数据分析 +- [ ] 数据分析 - [ ] TODO 未开始 - [ ] 店铺资产 - [ ] TODO 未开始 @@ -14,9 +19,9 @@ - [ ] 订单评价 - [ ] 会员管理 - [ ] 会员资料 - - TODO 需要补充 + - TODO 需要补充 - [ ] 营销管理 - - [ ] 广告管理 + - [ ] 广告管理 - [ ] 优惠劵 - [ ] 优惠码 - [ ] 商品推荐 @@ -29,5 +34,5 @@ - [ ] 权限管理 - [ ] 短信管理 - [ ] 短信模板 - - [ ] 发送日志 - - [ ] 操作日志 \ No newline at end of file + - [ ] 发送日志 + - [ ] 操作日志 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 629fde73..34307d84 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 @@ -14,6 +14,7 @@ public enum ProductErrorCodeEnum { PRODUCT_CATEGORY_STATUS_EQUALS(1002001003, "商品分类已经是该状态"), PRODUCT_CATEGORY_DELETE_ONLY_DISABLE(1002001004, "只有关闭的商品分类才可以删除"), PRODUCT_CATEGORY_MUST_ENABLE(1002001005, "只有开启的商品分类,才可以使用"), + PRODUCT_CATEGORY_PARENT_CAN_NOT_BE_LEVEL2(1002001005, "父分类必须是一级分类"), // ========== PRODUCT SPU + SKU 模块 ========== PRODUCT_SKU_ATTR_CANT_NOT_DUPLICATE(1003002000, "一个 Sku 下,不能有重复的规格"), @@ -46,4 +47,4 @@ public enum ProductErrorCodeEnum { return message; } -} \ No newline at end of file +} diff --git a/product/product-service-impl/src/main/java/cn/iocoder/mall/product/service/ProductCategoryServiceImpl.java b/product/product-service-impl/src/main/java/cn/iocoder/mall/product/service/ProductCategoryServiceImpl.java index a235a56f..8b3e9bd9 100644 --- a/product/product-service-impl/src/main/java/cn/iocoder/mall/product/service/ProductCategoryServiceImpl.java +++ b/product/product-service-impl/src/main/java/cn/iocoder/mall/product/service/ProductCategoryServiceImpl.java @@ -47,11 +47,8 @@ public class ProductCategoryServiceImpl implements ProductCategoryService { @Override public CommonResult addProductCategory(Integer adminId, ProductCategoryAddDTO productCategoryAddDTO) { - // 校验父分类是否存在 - if (!ProductCategoryConstants.PID_ROOT.equals(productCategoryAddDTO.getPid()) - && productCategoryMapper.selectById(productCategoryAddDTO.getPid()) == null) { - return ServiceExceptionUtil.error(ProductErrorCodeEnum.PRODUCT_CATEGORY_PARENT_NOT_EXISTS.getCode()); - } + // 校验父分类 + validParent(productCategoryAddDTO.getPid()); // 保存到数据库 ProductCategoryDO productCategory = ProductCategoryConvert.INSTANCE.convert(productCategoryAddDTO) .setStatus(ProductCategoryConstants.STATUS_ENABLE); @@ -65,10 +62,8 @@ public class ProductCategoryServiceImpl implements ProductCategoryService { @Override public CommonResult updateProductCategory(Integer adminId, ProductCategoryUpdateDTO productCategoryUpdateDTO) { - // 校验分类是否存在 - if (productCategoryMapper.selectById(productCategoryUpdateDTO.getId()) == null) { - return ServiceExceptionUtil.error(ProductErrorCodeEnum.PRODUCT_CATEGORY_NOT_EXISTS.getCode()); - } + // 校验父分类 + validParent(productCategoryUpdateDTO.getPid()); // 校验不能设置自己为父分类 if (productCategoryUpdateDTO.getId().equals(productCategoryUpdateDTO.getPid())) { return ServiceExceptionUtil.error(ProductErrorCodeEnum.PRODUCT_CATEGORY_PARENT_NOT_SELF.getCode()); @@ -153,4 +148,18 @@ public class ProductCategoryServiceImpl implements ProductCategoryService { || ProductCategoryConstants.STATUS_DISABLE.equals(status); } + private void validParent(Integer pid) { + if (!ProductCategoryConstants.PID_ROOT.equals(pid)) { + ProductCategoryDO parentCategory = productCategoryMapper.selectById(pid); + // 校验父分类是否存在 + if (parentCategory == null) { + throw ServiceExceptionUtil.exception(ProductErrorCodeEnum.PRODUCT_CATEGORY_PARENT_NOT_EXISTS.getCode()); + } + // 父分类必须是一级分类 + if (!ProductCategoryConstants.PID_ROOT.equals(parentCategory.getPid())) { + throw ServiceExceptionUtil.exception((ProductErrorCodeEnum.PRODUCT_CATEGORY_PARENT_CAN_NOT_BE_LEVEL2.getCode())); + } + } + } + }