浏览代码

Merge branch 'develop'

gaozhan 5 年之前
父节点
当前提交
8548555d27

+ 6 - 0
package.json

@@ -10,10 +10,16 @@
     "antd": "^3.23.4",
     "axios": "^0.19.0",
     "dva": "^2.4.1",
+    "fabric": "^3.6.3",
+    "jr-qrcode": "^1.1.4",
     "lodash": "^4.17.15",
+    "lodash.clonedeep": "^4.5.0",
+    "lodash.throttle": "^4.1.1",
+    "rc-color-picker": "^1.2.6",
     "react": "^16.9.0",
     "react-copy-to-clipboard": "^5.0.2",
     "react-dom": "^16.9.0",
+    "react-rnd": "^10.1.8",
     "wptpc-design": "^1.2.31"
   },
   "devDependencies": {

+ 213 - 212
src/pages/platform/auth/powerlist/index.js

@@ -1,7 +1,7 @@
-import React, { Component } from 'react';
-import get from 'lodash/get';
-import pick from 'lodash/pick';
-import { connect } from 'dva';
+import React, { Component } from 'react'
+import get from 'lodash/get'
+import pick from 'lodash/pick'
+import { connect } from 'dva'
 import {
   Button,
   Icon,
@@ -14,29 +14,29 @@ import {
   Popconfirm,
   Spin,
   Radio,
-  message,
-} from 'antd';
-import authority from '@/conf/authority';
-import { validateAuthority } from '@/utils/authority';
-import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { WhiteSpace } from 'wptpc-design';
-import SpinningTree from './spinningTree';
-import { addPower } from '../services';
+  message
+} from 'antd'
+import authority from '@/conf/authority'
+import { validateAuthority } from '@/utils/authority'
+import { CopyToClipboard } from 'react-copy-to-clipboard'
+import { WhiteSpace } from 'wptpc-design'
+import SpinningTree from './spinningTree'
+import { addPower } from '../services'
 
-import styles from './index.less';
+import styles from './index.less'
 
 const {
   updaePower,
   insertPower,
   delPower,
   insertMasterPower,
-  updatemasterPower,
-} = authority;
+  updatemasterPower
+} = authority
 
-const { TreeNode } = TreeSelect;
-const { Option } = Select;
-const RadioGroup = Radio.Group;
-const { TextArea } = Input;
+const { TreeNode } = TreeSelect
+const { Option } = Select
+const RadioGroup = Radio.Group
+const { TextArea } = Input
 
 const defaultPower = {
   name: '',
@@ -48,44 +48,44 @@ const defaultPower = {
   vpn: '0',
   sort: 0,
   action_type: '1',
-  log: '1',
-};
+  log: '1'
+}
 
-function getAddPowerItem(formItem) {
+function getAddPowerItem (formItem) {
   // formItem.url = getReplaceUrl(formItem.url);
-  return addPower(formItem);
+  return addPower(formItem)
 }
 @connect(({ auth, user, loading }) => ({
   auth,
   user,
-  powerLoading: loading.effects['auth/fetchPower'],
+  powerLoading: loading.effects['auth/fetchPower']
 }))
 class PowerList extends Component {
   state = {
     powerItemModalVisible: false,
     power: {
-      ...defaultPower,
+      ...defaultPower
     },
     showPowerSetModal: false,
     viewflag: false,
     power2Json: {
       state: false,
-      data: {},
+      data: {}
     },
     importPowerByJson: {
       state: false,
       sourceData: '',
       data: [],
-      parentId: '',
+      parentId: ''
     },
     importPowerByJsonConfirm: {
       state: false,
-      loading: 0, // 0:未开始,1:正在导入 2:结束
-    },
+      loading: 0 // 0:未开始,1:正在导入 2:结束
+    }
   };
 
-  componentDidMount() {
-    this.fetchPower();
+  componentDidMount () {
+    this.fetchPower()
   }
 
   // componentWillReceiveProps(nextProps) {
@@ -100,19 +100,19 @@ class PowerList extends Component {
     this.setState({
       power: {
         ...this.state.power,
-        ...state,
-      },
-    });
+        ...state
+      }
+    })
   };
 
   showModal = key => () => {
     this.setState({
       [key]: true,
       power: {
-        ...defaultPower,
+        ...defaultPower
       },
-      viewflag: false,
-    });
+      viewflag: false
+    })
   };
 
   showImportByPowerJsonModal = () => {
@@ -121,19 +121,19 @@ class PowerList extends Component {
         state: true,
         sourceData: '',
         data: [],
-        parentId: '',
-      },
-    });
+        parentId: ''
+      }
+    })
   };
 
   hideModal = key => () => {
-    this.setState({ [key]: false });
+    this.setState({ [key]: false })
   };
 
   fetchPower = () => {
     this.props.dispatch({
-      type: 'auth/fetchPower',
-    });
+      type: 'auth/fetchPower'
+    })
   };
 
   sureEditPower = key => () => {
@@ -147,8 +147,8 @@ class PowerList extends Component {
       vpn,
       sort,
       action_type,
-      log,
-    } = this.state.power;
+      log
+    } = this.state.power
     if (String(id).length > 0) {
       this.props.dispatch({
         type: 'auth/_UpdatePower',
@@ -162,13 +162,13 @@ class PowerList extends Component {
           vpn: type !== 'menu' ? null : vpn,
           sort,
           action_type,
-          log,
+          log
         },
         callback: () => {
-          this.hideModal(key)();
-          this.fetchPower();
-        },
-      });
+          this.hideModal(key)()
+          this.fetchPower()
+        }
+      })
     } else {
       this.props.dispatch({
         type: 'auth/_addPower',
@@ -181,69 +181,69 @@ class PowerList extends Component {
           vpn: type !== 'menu' ? null : vpn,
           sort,
           action_type,
-          log,
+          log
         },
         callback: () => {
-          this.hideModal(key)();
-          this.fetchPower();
-        },
-      });
+          this.hideModal(key)()
+          this.fetchPower()
+        }
+      })
     }
   };
 
   onPowerSetModalOk = () => {
-    const { id: power_id, particle_json } = this.state.power;
+    const { id: power_id, particle_json } = this.state.power
     this.props.dispatch({
       type: 'auth/_setParticle',
       payload: {
         power_id,
-        particle_json,
+        particle_json
       },
       callback: () => {
-        this.onPowerSetModalCancel();
-      },
-    });
+        this.onPowerSetModalCancel()
+      }
+    })
   };
 
   onPowerSetModalCancel = () => {
-    this.setState({ showPowerSetModal: false });
+    this.setState({ showPowerSetModal: false })
   };
 
   delPower = record => () => {
     this.props.dispatch({
       type: 'auth/_delPower',
       payload: {
-        power_id: record.id,
+        power_id: record.id
       },
       callback: () => {
-        this.fetchPower();
-      },
-    });
+        this.fetchPower()
+      }
+    })
   };
 
   updatePowerSet = ({ ...power }) => () => {
-    const { id: power_id } = power;
+    const { id: power_id } = power
     this.props.dispatch({
       type: 'auth/_getParticle',
       payload: {
-        power_id,
+        power_id
       },
       callback: res => {
         if (res.code === 0) {
-          power.particle_json = res.data.particle_json;
-          this.setState({ power, showPowerSetModal: true });
+          power.particle_json = res.data.particle_json
+          this.setState({ power, showPowerSetModal: true })
         }
-      },
-    });
+      }
+    })
   };
 
   exportPower2Json = ({ ...power }) => () => {
     this.setState({
       power2Json: {
         state: true,
-        data: power,
-      },
-    });
+        data: power
+      }
+    })
   };
 
   updatePower = record => () => {
@@ -257,8 +257,8 @@ class PowerList extends Component {
       parent_id: parentId,
       sort,
       action_type,
-      log,
-    } = record;
+      log
+    } = record
     this.setState({
       powerItemModalVisible: true,
       viewflag: true,
@@ -272,13 +272,13 @@ class PowerList extends Component {
         vpn,
         sort,
         action_type,
-        log,
-      },
-    });
+        log
+      }
+    })
   };
 
   powerTree = () => {
-    const { user: { apiRightList } } = this.props;
+    const { user: { apiRightList } } = this.props
     const mapTree = data => {
       return data.map(item => (
         <div key={item.id} className="powerTree" id={`powerTree${item.id}`}>
@@ -290,7 +290,7 @@ class PowerList extends Component {
                   onClick={() => {
                     document
                       .getElementById(`powerTree${item.id}`)
-                      .setAttribute('class', 'powerTree open');
+                      .setAttribute('class', 'powerTree open')
                   }}
                 />
               </span>
@@ -300,7 +300,7 @@ class PowerList extends Component {
                   onClick={() => {
                     document
                       .getElementById(`powerTree${item.id}`)
-                      .setAttribute('class', 'powerTree');
+                      .setAttribute('class', 'powerTree')
                   }}
                 />
               </span>
@@ -310,8 +310,8 @@ class PowerList extends Component {
             {item.type === 'api' &&
               (validateAuthority(apiRightList, updaePower) ||
                 validateAuthority(apiRightList, updatemasterPower)) && (
-                <Icon type="filter" onClick={this.updatePowerSet(item)} />
-              )}
+              <Icon type="filter" onClick={this.updatePowerSet(item)} />
+            )}
             {validateAuthority(apiRightList, delPower) && (
               <Popconfirm
                 title={`确定删除 ${item.name} 权限?`}
@@ -324,22 +324,22 @@ class PowerList extends Component {
             )}
             {(validateAuthority(apiRightList, updaePower) ||
               validateAuthority(apiRightList, updatemasterPower)) && (
-                <Icon type="edit" onClick={this.updatePower(item)} />
-              )}
+              <Icon type="edit" onClick={this.updatePower(item)} />
+            )}
           </div>
           <div className="childTree">{item.children && mapTree(item.children)}</div>
         </div>
-      ));
-    };
-    return <div className={styles.powerList}>{mapTree(this.props.auth.powerList)}</div>;
+      ))
+    }
+    return <div className={styles.powerList}>{mapTree(this.props.auth.powerList)}</div>
   };
 
   onPowerChange = (key, value) => {
-    this.setState({ power: { ...this.state.power, [key]: value } });
+    this.setState({ power: { ...this.state.power, [key]: value } })
   };
 
-  powerSetModal() {
-    const { showPowerSetModal, power } = this.state;
+  powerSetModal () {
+    const { showPowerSetModal, power } = this.state
     return (
       <Modal
         title={`编辑【${power.name}】的数据权限`}
@@ -361,16 +361,16 @@ class PowerList extends Component {
           </Col>
         </Row>
       </Modal>
-    );
+    )
   }
 
   // 复制按钮
   onExportPower2JsonModalCopy = (text, result) => {
     if (result) {
-      message.success('复制成功!');
-      this.onExportPower2JsonModalCancel();
+      message.success('复制成功!')
+      this.onExportPower2JsonModalCancel()
     } else {
-      message.warning('复制失败,请手动复制!');
+      message.warning('复制失败,请手动复制!')
     }
   };
 
@@ -378,17 +378,17 @@ class PowerList extends Component {
     this.setState({
       power2Json: {
         state: false,
-        data: {},
-      },
-    });
+        data: {}
+      }
+    })
   };
 
-  exportPower2JsonModal() {
-    const { power2Json: { state, data } } = this.state;
-    const pickKeyArr = ['name', 'url', 'identify', 'type', 'vpn', 'sort', 'action_type', 'log'];
+  exportPower2JsonModal () {
+    const { power2Json: { state, data } } = this.state
+    const pickKeyArr = ['name', 'url', 'identify', 'type', 'vpn', 'sort', 'action_type', 'log']
     // const textJson = JSON.stringify(data);
-    const childrenMenuData = get(data, 'children', []).map(item => pick(item, pickKeyArr));
-    const textJson = JSON.stringify(childrenMenuData);
+    const childrenMenuData = get(data, 'children', []).map(item => pick(item, pickKeyArr))
+    const textJson = JSON.stringify(childrenMenuData)
     return (
       <Modal
         title={`需要导出的【${data.name}】子菜单第一层数据结构`}
@@ -404,37 +404,37 @@ class PowerList extends Component {
                 拷贝
               </Button>
             </div>
-          </CopyToClipboard>,
+          </CopyToClipboard>
         ]}
       >
         <Row gutter={12}>
           <TextArea placeholder="需要批量导出的菜单数据结构" rows={15} value={textJson} readOnly />
         </Row>
       </Modal>
-    );
+    )
   }
 
   // 打开确认弹窗进行导入
   onImportByPowerJsonModalConfirm = () => {
-    const { importPowerByJson: { sourceData, parentId, data } } = this.state;
+    const { importPowerByJson: { sourceData, parentId, data } } = this.state
     if (!parentId) {
-      message.warning('必须选择一个权限父级!');
-      return false;
+      message.warning('必须选择一个权限父级!')
+      return false
     }
     if (!sourceData) {
-      message.warning('必须输入一个菜单的数据结构!');
-      return false;
+      message.warning('必须输入一个菜单的数据结构!')
+      return false
     }
     if (!data || !data.length) {
-      message.warning('输入的数据结构解析失败,请确认数据结构后再进行操作!');
-      return false;
+      message.warning('输入的数据结构解析失败,请确认数据结构后再进行操作!')
+      return false
     }
     this.setState({
       importPowerByJsonConfirm: {
         state: true,
-        loading: 0,
-      },
-    });
+        loading: 0
+      }
+    })
   };
 
   onImportByPowerJsonModalCancel = () => {
@@ -443,72 +443,72 @@ class PowerList extends Component {
         state: false,
         sourceData: '',
         data: [],
-        parentId: '',
-      },
-    });
+        parentId: ''
+      }
+    })
   };
 
-  setImportPowerByJson(changeData) {
-    const { importPowerByJson: importPowerByJsonOld } = this.state;
+  setImportPowerByJson (changeData) {
+    const { importPowerByJson: importPowerByJsonOld } = this.state
     this.setState({
       importPowerByJson: {
         ...importPowerByJsonOld,
-        ...changeData,
-      },
-    });
+        ...changeData
+      }
+    })
   }
 
   changeImportSource = event => {
-    const sourceData = event.target.value;
-    let finalSourceData = sourceData;
+    const sourceData = event.target.value
+    const finalSourceData = sourceData
     // for (let i = 0, l = replaceUrlRegexp.length; i < l; i += 1) {
     //   const curRegexp = replaceUrlRegexp[i];
     //   finalSourceData = finalSourceData.replace(curRegexp.regexp, curRegexp.str);
     // }
 
-    let sourceDataArr = [];
+    let sourceDataArr = []
     if (finalSourceData) {
       try {
-        sourceDataArr = JSON.parse(finalSourceData);
+        sourceDataArr = JSON.parse(finalSourceData)
       } catch (error) {
         // console.error(error);
-        sourceDataArr = [];
+        sourceDataArr = []
       }
     }
-    if (!Array.isArray(sourceDataArr)) sourceDataArr = [];
+    if (!Array.isArray(sourceDataArr)) sourceDataArr = []
     const data = sourceDataArr.map(item => {
       return {
         ...item,
-        addStatus: 0, // 0:初始状态,1:成功,2:失败
-      };
-    });
+        addStatus: 0 // 0:初始状态,1:成功,2:失败
+      }
+    })
     this.setImportPowerByJson({
       sourceData: finalSourceData,
-      data,
-    });
+      data
+    })
   };
 
-  importByPowerJsonModal() {
-    const { importPowerByJson: { state, sourceData, parentId } } = this.state;
-    const parentIdStr = String(parentId || '');
+  importByPowerJsonModal () {
+    const { importPowerByJson: { state, sourceData, parentId } } = this.state
+    const parentIdStr = String(parentId || '')
     const tProps = {
       value: parentIdStr,
       onChange: value => {
         this.setImportPowerByJson({
-          parentId: value,
-        });
+          parentId: value
+        })
       },
       searchPlaceholder: '选择父级权限',
       style: {
-        width: '100%',
+        width: '100%'
       },
       showSearch: true,
       dropdownStyle: { maxHeight: 300, overflow: 'auto' },
       placeholder: '选择父级权限',
       allowClear: true,
       treeDefaultExpandAll: false,
-      treeNodeFilterProp: 'title',
-    };
+      treeNodeFilterProp: 'title'
+    }
 
     const powerOptions = () => {
       const mapTree = data => {
@@ -516,10 +516,10 @@ class PowerList extends Component {
           <TreeNode key={item.id} value={item.id.toString()} title={item.name}>
             {item.children && mapTree(item.children)}
           </TreeNode>
-        ));
-      };
-      return mapTree(this.props.auth.powerList);
-    };
+        ))
+      }
+      return mapTree(this.props.auth.powerList)
+    }
 
     return (
       <Modal
@@ -545,51 +545,51 @@ class PowerList extends Component {
           </Col>
         </Row>
       </Modal>
-    );
+    )
   }
 
-  fetchAddPower() {
-    const { importPowerByJson: { data, parentId } } = this.state;
+  fetchAddPower () {
+    const { importPowerByJson: { data, parentId } } = this.state
     const promiseGroup = data.map(formItem => {
       const finalFormItem = {
         ...formItem,
-        power_parent_id: parentId,
-      };
-      return getAddPowerItem(finalFormItem);
-    });
+        power_parent_id: parentId
+      }
+      return getAddPowerItem(finalFormItem)
+    })
     Promise.all(promiseGroup).then(resGroup => {
-      const dataGroup = [];
+      const dataGroup = []
       resGroup.forEach((resItem, index) => {
-        const isSuccess = !!(resItem && resItem.code === 0);
+        const isSuccess = !!(resItem && resItem.code === 0)
         dataGroup.push({
           ...data[index],
-          addStatus: isSuccess ? 1 : 2,
-        });
-      });
+          addStatus: isSuccess ? 1 : 2
+        })
+      })
       this.setState({
         importPowerByJsonConfirm: {
           state: this.state.importPowerByJsonConfirm.state,
-          loading: 2,
+          loading: 2
         },
         importPowerByJson: {
           ...this.state.importPowerByJson,
-          data: dataGroup,
-        },
-      });
-      this.fetchPower();
-    });
+          data: dataGroup
+        }
+      })
+      this.fetchPower()
+    })
   }
 
   onImportByPowerJsonConfirmModalStep = () => {
-    const { importPowerByJsonConfirm: { loading } } = this.state;
+    const { importPowerByJsonConfirm: { loading } } = this.state
     if (loading === 0) {
       this.setState({
         importPowerByJsonConfirm: {
           state: this.state.importPowerByJsonConfirm.state,
-          loading: 1,
-        },
-      });
-      this.fetchAddPower();
+          loading: 1
+        }
+      })
+      this.fetchAddPower()
       // setTimeout(() => {
       //   this.setState({
       //     importPowerByJsonConfirm: {
@@ -602,15 +602,15 @@ class PowerList extends Component {
       this.setState({
         importPowerByJsonConfirm: {
           state: false,
-          loading: 0,
+          loading: 0
         },
         importPowerByJson: {
           state: false,
           sourceData: '',
           data: [],
-          parentId: '',
-        },
-      });
+          parentId: ''
+        }
+      })
     }
   };
 
@@ -618,18 +618,18 @@ class PowerList extends Component {
     this.setState({
       importPowerByJsonConfirm: {
         state: false,
-        loading: 0,
-      },
-    });
+        loading: 0
+      }
+    })
   };
 
-  importByPowerJsonConfirmModal() {
-    const { importPowerByJsonConfirm: { state, loading } } = this.state;
-    const { importPowerByJson: { data } } = this.state;
-    const loadingStatus = ['确认导入', '正在导入', '完成'];
+  importByPowerJsonConfirmModal () {
+    const { importPowerByJsonConfirm: { state, loading } } = this.state
+    const { importPowerByJson: { data } } = this.state
+    const loadingStatus = ['确认导入', '正在导入', '完成']
     // 0:初始状态,1:成功,2:失败
-    const addStatusIconType = ['unordered-list', 'check-circle', 'close-circle'];
-    const addStatusColor = ['#1890ff', '#52c41a', '#f5222d'];
+    const addStatusIconType = ['unordered-list', 'check-circle', 'close-circle']
+    const addStatusColor = ['#1890ff', '#52c41a', '#f5222d']
     return (
       <Modal
         title="请确认是否是您需要添加的菜单"
@@ -646,7 +646,7 @@ class PowerList extends Component {
             onClick={this.onImportByPowerJsonConfirmModalStep}
           >
             {loadingStatus[loading]}
-          </Button>,
+          </Button>
         ]}
       >
         <ul
@@ -656,7 +656,7 @@ class PowerList extends Component {
             marginBlockEnd: 0,
             paddingInlineStart: 0,
             maxHeight: '600px',
-            overflow: 'auto',
+            overflow: 'auto'
           }}
         >
           {data.map(item => (
@@ -670,29 +670,29 @@ class PowerList extends Component {
           ))}
         </ul>
       </Modal>
-    );
+    )
   }
 
   powerItemModal = () => {
-    const defaultValue = String(this.state.power.parentId || '');
+    const defaultValue = String(this.state.power.parentId || '')
     const tProps = {
       value: defaultValue,
       onChange: value => {
         this.setPower({
-          parentId: value,
-        });
+          parentId: value
+        })
       },
       searchPlaceholder: '选择父级权限',
       style: {
-        width: '100%',
+        width: '100%'
       },
       showSearch: true,
       dropdownStyle: { maxHeight: 300, overflow: 'auto' },
       placeholder: '选择父级权限',
       allowClear: true,
       treeDefaultExpandAll: false,
-      treeNodeFilterProp: 'title',
-    };
+      treeNodeFilterProp: 'title'
+    }
 
     const powerOptions = () => {
       const mapTree = data => {
@@ -700,10 +700,10 @@ class PowerList extends Component {
           <TreeNode key={item.id} value={item.id.toString()} title={item.name}>
             {item.children && mapTree(item.children)}
           </TreeNode>
-        ));
-      };
-      return mapTree(this.props.auth.powerList);
-    };
+        ))
+      }
+      return mapTree(this.props.auth.powerList)
+    }
 
     return (
       <Modal
@@ -722,7 +722,7 @@ class PowerList extends Component {
               placeholder="请输入权限名称"
               value={this.state.power.name}
               onChange={e => {
-                this.setPower({ name: e.target.value });
+                this.setPower({ name: e.target.value })
               }}
             />
           </Col>
@@ -739,7 +739,7 @@ class PowerList extends Component {
               placeholder="请输入权限路径"
               value={this.state.power.url}
               onChange={e => {
-                this.setPower({ url: e.target.value });
+                this.setPower({ url: e.target.value })
               }}
             />
           </Col>
@@ -756,7 +756,7 @@ class PowerList extends Component {
               placeholder="请输入权限标识"
               value={this.state.power.identify}
               onChange={e => {
-                this.setPower({ identify: e.target.value });
+                this.setPower({ identify: e.target.value })
               }}
             />
           </Col>
@@ -772,7 +772,7 @@ class PowerList extends Component {
               placeholder="请输入权限排序"
               value={this.state.power.sort}
               onChange={e => {
-                this.setPower({ sort: e.target.value });
+                this.setPower({ sort: e.target.value })
               }}
             />
           </Col>
@@ -788,7 +788,7 @@ class PowerList extends Component {
               value={this.state.power.type}
               style={{ width: 120 }}
               onChange={value => {
-                this.setPower({ type: value });
+                this.setPower({ type: value })
               }}
             >
               <Option value="menu">menu</Option>
@@ -805,7 +805,7 @@ class PowerList extends Component {
           <Col className="gutter-row" span={15}>
             <RadioGroup
               onChange={e => {
-                this.setPower({ action_type: e.target.value });
+                this.setPower({ action_type: e.target.value })
               }}
               value={this.state.power.action_type.toString()}
             >
@@ -822,7 +822,7 @@ class PowerList extends Component {
           <Col className="gutter-row" span={15}>
             <RadioGroup
               onChange={e => {
-                this.setPower({ log: e.target.value });
+                this.setPower({ log: e.target.value })
               }}
               value={this.state.power.log.toString()}
             >
@@ -855,7 +855,7 @@ class PowerList extends Component {
             <Col className="gutter-row" span={15}>
               <RadioGroup
                 onChange={e => {
-                  this.setPower({ vpn: e.target.value });
+                  this.setPower({ vpn: e.target.value })
                 }}
                 value={this.state.power.vpn.toString()}
               >
@@ -866,19 +866,20 @@ class PowerList extends Component {
           </Row>
         )}
       </Modal>
-    );
+    )
   };
 
-  render() {
-    const { user: { apiRightList } } = this.props;
+  render () {
+    const { user: { apiRightList } } = this.props
+    console.log(apiRightList, insertPower, insertMasterPower, '23456789')
     return (
       <div>
         {(validateAuthority(apiRightList, insertPower) ||
           validateAuthority(apiRightList, insertMasterPower)) && (
-            <Button type="primary" onClick={this.showModal('powerItemModalVisible')}>
+          <Button type="primary" onClick={this.showModal('powerItemModalVisible')}>
               新增权限
-            </Button>
-          )}
+          </Button>
+        )}
         {validateAuthority(apiRightList, insertPower) && (
           <Button
             type="primary"
@@ -886,7 +887,7 @@ class PowerList extends Component {
             style={{ marginLeft: '10px' }}
           >
             批量导入权限
-            </Button>
+          </Button>
         )}
         <WhiteSpace height="10" />
 
@@ -906,7 +907,7 @@ class PowerList extends Component {
         {this.importByPowerJsonModal()}
         {this.importByPowerJsonConfirmModal()}
       </div>
-    );
+    )
   }
 }
 

+ 570 - 0
src/pages/poster/edit/$id$.js

@@ -0,0 +1,570 @@
+/* eslint-disable camelcase */
+import React from 'react'
+import { Button, Form, Input, Select, Slider, Switch, InputNumber, Icon, Popover, Modal, message } from 'antd'
+import { Rnd } from 'react-rnd'
+import isEmpty from 'lodash/isEmpty'
+// import { connect } from 'dva'
+
+import RcColorPicker from 'rc-color-picker'
+import 'rc-color-picker/assets/index.css'
+import { create, fetchDetail, update } from './service'
+
+import styles from './index.less'
+
+const { Option } = Select
+const { confirm } = Modal
+
+export default class Edit extends React.PureComponent {
+  state={
+    name: '',
+    description: '',
+    bg: {
+      type: 'bg',
+      w: 750,
+      h: 1000,
+      c: '#eaeaea'
+    },
+    element: [],
+    activeObjectIndex: 0,
+    editName: ''
+  }
+
+  componentDidMount () {
+    this.setState({ posterHeight: document.body.clientHeight - document.getElementById('poster').offsetTop - 35 })
+    if (this.props.match.params.id) {
+      fetchDetail({ id: this.props.match.params.id }).then(res => {
+        if (!isEmpty(res.data)) {
+          const { data: { description, name, id, config: [bg, ...element] } } = res
+          this.setState({ name: name, description: description, element: element, id: id, bg: bg })
+        }
+      })
+    }
+  }
+
+  updateBgState = (value) => {
+    this.setState({ bg: { ...this.state.bg, ...value } })
+  }
+
+  updateElement =(index, value) => {
+    const _ele = this.state.element.slice()
+    const _tar = _ele[index]
+    _ele[index] = { ..._tar, ...value }
+    this.setState({ element: _ele })
+  }
+
+  delElement =(index, name) => {
+    confirm({
+      title: '确定删除' + name + '?',
+      okText: 'Yes',
+      okType: 'danger',
+      cancelText: 'No',
+      onOk: () => {
+        const _ele = this.state.element.slice()
+        _ele.splice(index, 1)
+        this.setState({ element: _ele })
+      }
+
+    })
+  }
+
+  addShape = (type) => {
+    const _ele = this.state.element.slice()
+    const { dx, dy } = _ele[this.state.activeObjectIndex] || { dx: 0, dy: 0 }
+
+    switch (type) {
+      case 'text':
+        _ele.push({
+          content: '我是预览文本',
+          type: 'text',
+          t: '#text_' + (this.state.element.filter(item => item.type === 'text').length + 1) + '#',
+          font: 'pingfang_sc_semibold',
+          size: 36,
+          dx: dx + 5,
+          dy: dy + 5,
+          width: 490,
+          height: 45,
+          c: '%23333333'
+        })
+        break
+      case 'pic':
+        _ele.push({
+          type: 'pic',
+          path: '#pic_' + (this.state.element.filter(item => item.type === 'pic').length + 1) + '#',
+          content: 'https://cdn01t.weipaitang.com/img/20200407u1hmwwai-75so-tvze-5z15-oynukyk8esad-W1125H1500/w/640',
+          w: '100',
+          h: '100',
+          dx: dx + 5,
+          dy: dy + 5,
+          round: 'false',
+          align: 'both-align'
+        })
+        break
+      case 'qr':
+        _ele.push({
+          content: '我是预览二维码',
+          type: 'qr',
+          t: '#qr_' + (this.state.element.filter(item => item.type === 'pic').length + 1) + '#',
+          dx: dx + 5,
+          dy: dy + 5,
+          size: 140,
+          logo: 'true'
+        })
+        break
+      default:
+        break
+    }
+    this.setState({ element: _ele, activeObjectIndex: _ele.length - 1 }, () => {
+      // this.setState({ activeObjectIndex: this.state.element.length - 1 })
+    })
+  }
+
+  save = () => {
+    const { id, name, description, bg, element } = this.state
+    const handleFun = id ? update : create
+
+    element.map(item => {
+      delete item.x
+      delete item.y
+      !item.fixed && (delete item.fixed)
+      !item.relative && (delete item.relative)
+      !item.group && (delete item.group)
+    })
+    handleFun({
+      id,
+      name,
+      description,
+      config: ([bg]).concat(element)
+    }).then(res => {
+      if (res.code === 0) {
+        message.success('设置成功~')
+      }
+    })
+  }
+
+  render () {
+    const { bg: { w: bgWidth, h: bgHeight, c: bgColor }, activeObjectIndex, element, name, description } = this.state
+    const activeObject = element[activeObjectIndex] || {}
+
+    return (
+      <div id="poster" style={{ height: this.state.posterHeight, display: 'flex', flexDirection: 'column' }}>
+        <div className={styles.header}>
+          <span>模版:<Input style={{ width: '120px' }} value={name} onChange={e => this.setState({ name: e.target.value })}/></span>
+          <span>&emsp;描述:<Input style={{ width: '200px' }} value={description} onChange={e => this.setState({ description: e.target.value })}/></span>
+          <span>
+            {/* <Button>预览</Button> */}
+            <Button type="primary" onClick={this.save}>保存</Button>
+          </span>
+        </div>
+        <div className={styles.content}>
+          <div className={styles.left}>
+            <div className={styles.box}>
+              <h2>背景</h2>
+              <Form layout="vertical">
+                <Form.Item label="宽度" >
+                  <Input value={bgWidth} onChange={(e) => {
+                    this.updateBgState({ w: Number.parseFloat(e.target.value) })
+                  }}/>
+                </Form.Item>
+                <Form.Item label="高度">
+                  <Input value={bgHeight} onChange={(e) => {
+                    this.updateBgState({ h: Number.parseFloat(e.target.value) })
+                  }}/>
+                </Form.Item>
+                <Form.Item label="背景色">
+                  <div style={{ width: '60px' }}>
+                    <RcColorPicker
+                      color={bgColor}
+                      onChange={({ color }) => this.updateBgState({ c: color })}
+                    >
+                      <Input />
+                    </RcColorPicker>
+                  </div>
+                </Form.Item>
+              </Form>
+            </div>
+            <div className={styles.box}>
+              <h2>元素</h2>
+              <ul className={styles.elem}>
+                <li onClick={() => this.addShape('text')}><img src="https://cdn.weipaitang.com/static/20200416b0b57798-76f7-779876f7-9088-1d2665151e70-W200H200" />文字</li>
+                <li className={styles.pic} onClick={() => this.addShape('pic')}><img src="https://cdn.weipaitang.com/static/20200416a625b5f0-c5a1-b5f0c5a1-7472-58fbb8d4fb83-W200H200"/>图片</li>
+                <li className={styles.qr} onClick={() => this.addShape('qr')}><img src="https://cdn.weipaitang.com/static/20200416360ff7ff-c855-f7ffc855-0070-1542fd7ea01e-W200H200"/>二维码</li>
+              </ul>
+            </div>
+            <div className={styles.box}>
+              <h2>图层列表</h2>
+              <ul className={styles.layer}>
+                {
+                  element.map((item, index) => (
+                    <li className={`${activeObjectIndex === index ? styles.selected : ''} ${styles[item.type]}`} onClick={() => this.setState({ activeObjectIndex: index })}>
+                      {(item.type === 'pic' ? item.path : item.t || '').toString().replace(/#/g, '')}
+                      <Popover placement="right" content={<div><p onClick={() => this.setState({ visible: true, editIndex: index, editType: item.type, editName: item.type === 'pic' ? item.path : item.t || '' })}>重命名</p><p onClick={() => this.delElement(index, item.type === 'pic' ? item.path : item.t)}>删除</p></div>}>
+                        <Icon type="setting" theme="filled" />
+                      </Popover>
+                    </li>
+                  ))
+                }
+              </ul>
+            </div>
+          </div>
+          <div className={styles.middle}>
+            <div id="canvas" style={{
+              position: 'relative',
+              width: Number(bgWidth),
+              height: Number(bgHeight),
+              lineHeight: 1.3,
+              backgroundColor: bgColor,
+              marginBottom: '190px'
+            }}
+            >
+              {
+                element.map((item, index) => {
+                  const clampLine = Number((Number(item.height) / Number(item.size)).toFixed(0)) - 1 || 1
+                  if (item.type === 'text') {
+                    return (
+                      <Rnd
+                        size={{ width: Number.parseFloat(item.weight), height: Number.parseFloat(item.height) }}
+                        position={{ x: item.dx, y: item.dy - item.size }}
+                        onDragStop={(e, d) => { this.updateElement(index, { dx: d.x, dy: d.y + item.size }) }}
+                        onResizeStop={(e, direction, ref, delta, position) => {
+                          this.updateElement(index, { width: Number.parseFloat(ref.style.width), height: Number.parseFloat(ref.style.height), ...position })
+                        }}
+                        bounds="parent"
+                        className={activeObjectIndex === index ? styles.outline : ''}
+                      >
+                        <div
+                          className={styles.textRnd}
+                          onClick={() => this.setState({ activeObjectIndex: index })}
+                          style={{
+                            WebkitBoxOrient: 'vertical',
+                            WebkitLineClamp: clampLine,
+                            width: Number.parseFloat(item.width) || 'auto',
+                            height: Number.parseFloat(item.height) || 'auto',
+                            fontFamily: item.font,
+                            fontSize: item.size,
+                            color: item.c
+                          }}>
+                          {item.content}
+                        </div>
+                      </Rnd>
+                    )
+                  }
+                  if (item.type === 'pic' || item.type === 'mat') {
+                    return (
+                      <Rnd
+                        size={{ width: Number.parseFloat(item.w), height: Number.parseFloat(item.h) }}
+                        position={{ x: item.dx, y: item.dy }}
+                        onDragStop={(e, d) => { this.updateElement(index, { dx: d.x, dy: d.y }) }}
+                        onResizeStop={(e, direction, ref, delta, position) => {
+                          this.updateElement(index, { w: Number.parseFloat(ref.style.width), h: Number.parseFloat(ref.style.height), ...position })
+                        }}
+                        bounds="#canvas"
+                        className={activeObjectIndex === index ? styles.outline : ''}
+
+                      >
+                        <img onClick={() => this.setState({ activeObjectIndex: index })}
+                          src={item.content}
+                          style={{ width: Number.parseFloat(item.w) + 'px', height: Number.parseFloat(item.h) + 'px', borderRadius: (item.round || '').toString() === 'true' ? item.w : 0 }}
+                        />
+                      </Rnd>
+                    )
+                  }
+                  if (item.type === 'qr') {
+                    return (
+                      <Rnd
+                        size={{ width: Number.parseFloat(item.size), height: Number.parseFloat(item.size) }}
+                        position={{ x: item.dx, y: item.dy }}
+                        onDragStop={(e, d) => { this.updateElement(index, { dx: d.x, dy: d.y }) }}
+                        onResizeStop={(e, direction, ref, delta, position) => {
+                          this.updateElement(index, { size: Number.parseFloat(ref.style.width), ...position })
+                        }}
+                        bounds="#canvas"
+                        className={activeObjectIndex === index ? styles.outline : ''}
+
+                      >
+                        <img
+                          onClick={() => this.setState({ activeObjectIndex: index })}
+                          style={{ width: Number.parseFloat(item.size) + 'px', height: Number.parseFloat(item.size) + 'px' }}
+                          src={item.logo === 'wpt' || item.logo === 'youjiang'
+                            ? (item.logo === 'wpt' ? 'https://cdn.weipaitang.com/static/20200413eb9effec-3871-ffec3871-cd41-baed9e98535e-W200H200' : 'https://cdn.weipaitang.com/static/202004141c0b320a-9cdc-320a9cdc-d090-e4b5964fee9d-W200H200')
+                            : 'https://cdn.weipaitang.com/static/20200413d4263aa5-6423-3aa56423-4e8d-a96e1d9d93f3-W200H200'
+                          }
+                        />
+                      </Rnd>
+                    )
+                  }
+                })
+
+              }
+            </div>
+          </div>
+          <div className={styles.right}>
+            {
+              activeObject.type === 'text' && (
+                <div className={styles.box}>
+                  <h2>文字</h2>
+                  <Form layout="vertical">
+                    <Form.Item label="内容">
+                      <Input.TextArea rows={3} value={activeObject.content} onChange={(e) => this.updateElement(activeObjectIndex, { content: e.target.value })}/>
+                    </Form.Item>
+                    <Form.Item label="字体">
+                      <Select value={activeObject.font} onChange={(value) => this.updateElement(activeObjectIndex, { font: value })}>
+                        <Option value="msyh">msyh</Option>
+                        <Option value="msyhbd">msyhbd</Option>
+                        <Option value="NotoSerifCJK">NotoSerifCJK</Option>
+                        <Option value="PingFang">PingFang</Option>
+                        <Option value="pingfang-sc-semibold">pingfang-sc-semibold</Option>
+                        <Option value="pingfang_jiancu">pingfang_jiancu</Option>
+                        <Option value="pingfang_jianxi">pingfang_jianxi</Option>
+                        <Option value="pingfang_regular">pingfang_regular</Option>
+                        <Option value="PingFang_SC">PingFang_SC</Option>
+                        <Option value="pingfang_sc_medium">pingfang_sc_medium</Option>
+                        <Option value="pingfang_sc_semibold">pingfang_sc_semibold</Option>
+                        <Option value="simhei">simhei</Option>
+                        <Option value="SourceHanSansCN-Normal">SourceHanSansCN-Normal</Option>
+                        <Option value="喜鹊招牌体">喜鹊招牌体</Option>
+                        <Option value="喜鹊聚珍体regular">喜鹊聚珍体regular</Option>
+                        <Option value="思源宋体">思源宋体</Option>
+                        <Option value="方正正粗黑简体">方正正粗黑简体</Option>
+                      </Select>
+                    </Form.Item>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="字体大小" style={{ flex: 1 }}>
+                        <InputNumber style={{ width: 60 }} min={12} max={100} value={activeObject.size} onChange={(value) => this.updateElement(activeObjectIndex, { size: value })} />
+                      </Form.Item>
+                      <Form.Item label="字体颜色" style={{ flex: 1 }}>
+                        <div style={{ width: '60px' }}
+                        >
+                          <RcColorPicker
+                            color={activeObject.c}
+                            onChange={({ color }) => this.updateElement(activeObjectIndex, { c: color })}
+                          >
+                            <Input />
+                          </RcColorPicker>
+                        </div>
+
+                      </Form.Item>
+                    </div>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="X偏移" style={{ flex: 1 }}>
+                        <InputNumber style={{ width: 60 }} value={activeObject.dx} onChange={(value) => this.updateElement(activeObjectIndex, { dx: value })}/>
+                      </Form.Item>
+                      <Form.Item label="Y偏移" style={{ flex: 1 }}>
+                        <InputNumber style={{ width: 60 }} value={activeObject.dy} onChange={(value) => this.updateElement(activeObjectIndex, { dy: value })}/>
+                      </Form.Item>
+                    </div>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="文字宽度" style={{ flex: 1 }}>
+                        <InputNumber value={activeObject.width} style={{ width: 60 }} onChange={(value) => this.updateElement(activeObjectIndex, { width: value })}/>
+                      </Form.Item>
+                      <Form.Item label="文字高度" style={{ flex: 1 }}>
+                        <InputNumber value={activeObject.height} style={{ width: 60 }} onChange={(value) => this.updateElement(activeObjectIndex, { height: value })}/>
+                      </Form.Item>
+                    </div>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="固定位置" style={{ flex: 1 }}>
+                        <Switch
+                          checked={activeObject.fixed}
+                          onChange={(checked) => this.updateElement(activeObjectIndex, { fixed: checked })}
+                          checkedChildren="开"
+                          unCheckedChildren="关"
+                        />
+                      </Form.Item>
+                      <Form.Item label="相对位置" style={{ flex: 1 }}>
+                        <Switch
+                          checked={activeObject.relative}
+                          onChange={(checked) => this.updateElement(activeObjectIndex, { relative: checked })}
+                          checkedChildren="开"
+                          unCheckedChildren="关"
+                        />
+                      </Form.Item>
+                    </div>
+                    <Form.Item label="分组" style={{ flex: 1 }}>
+                      <InputNumber
+                        style={{ width: 100 }}
+                        value={activeObject.group}
+                        onChange={(value) => this.updateElement(activeObjectIndex, { group: value })}
+                      />
+                    </Form.Item>
+                    {/* <Form.Item label="X居中"> */}
+                    {/* <Select value={this.state.currentOptionArr[1].css.textAlign} style={{ width: 150 }} onChange={(value) => this.handleChange(1, 'textAlign', value)}>
+                        <Option value="left">left</Option>
+                        <Option value="center">center</Option>
+                        <Option value="right">right</Option>
+                      </Select>
+                    </Form.Item>
+                    <Form.Item label="文字垂直">
+                      <Input/>
+                    </Form.Item>
+                    <Form.Item label="不透明">
+                      <Slider
+                        min={1}
+                        max={20}
+                        onChange={(value) => console.log(value)}
+                        value={10}
+                      />
+                    </Form.Item> */}
+                  </Form>
+                </div>
+              )
+            }
+
+            {
+              activeObject.type === 'qr' && (
+                <div className={styles.box}>
+                  <h2>二维码</h2>
+                  <Form layout="vertical">
+                    <Form.Item label="内容">
+                      <Input value={activeObject.content} onChange={(e) => this.updateElement(activeObjectIndex, { content: e.target.value })}/>
+                    </Form.Item>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="X偏移" style={{ flex: 1 }}>
+                        <InputNumber style={{ width: 60 }} value={activeObject.dx} onChange={(value) => this.updateElement(activeObjectIndex, { dx: value })}/>
+                      </Form.Item>
+                      <Form.Item label="Y偏移" style={{ flex: 1 }}>
+                        <InputNumber style={{ width: 60 }} value={activeObject.dy} onChange={(value) => this.updateElement(activeObjectIndex, { dy: value })}/>
+                      </Form.Item>
+                    </div>
+                    <Form.Item label="LOGO">
+                      <Switch
+                        checked={activeObject.logo === 'wpt' || activeObject.logo === 'youjiang'}
+                        onChange={(checked) => this.updateElement(activeObjectIndex, { logo: checked ? 'wpt' : false })}
+                        checkedChildren="开"
+                        unCheckedChildren="关"
+                      />
+                      {
+                        (activeObject.logo === 'wpt' || activeObject.logo === 'youjiang') && (
+                          <Select value={activeObject.logo} style={{ width: 120, marginLeft: '5px' }} onChange={value => this.updateElement(activeObjectIndex, { logo: value })}>
+                            <Option value="wpt">微拍堂</Option>
+                            <Option value="youjiang">有匠</Option>
+                          </Select>
+                        )
+                      }
+                    </Form.Item>
+                    <Form.Item label="大小">
+                      <InputNumber style={{ width: 100 }} value={activeObject.size} onChange={(value) => this.updateElement(activeObjectIndex, { size: value })}/>
+                    </Form.Item>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="固定位置" style={{ flex: 1 }}>
+                        <Switch
+                          checked={activeObject.fixed}
+                          onChange={(checked) => this.updateElement(activeObjectIndex, { fixed: checked })}
+                          checkedChildren="开"
+                          unCheckedChildren="关"
+                        />
+                      </Form.Item>
+                      <Form.Item label="相对位置" style={{ flex: 1 }}>
+                        <Switch
+                          checked={activeObject.relative}
+                          onChange={(checked) => this.updateElement(activeObjectIndex, { relative: checked })}
+                          checkedChildren="开"
+                          unCheckedChildren="关"
+                        />
+                      </Form.Item>
+                    </div>
+                    <Form.Item label="分组" style={{ flex: 1 }}>
+                      <InputNumber
+                        style={{ width: 100 }}
+                        value={activeObject.group}
+                        onChange={(value) => this.updateElement(activeObjectIndex, { group: value })}
+                      />
+                    </Form.Item>
+                  </Form>
+                </div>
+              )
+            }
+
+            {
+              activeObject.type === 'pic' && (
+                <div className={styles.box}>
+                  <h2>图片</h2>
+                  <Form layout="vertical">
+                    <Form.Item label="路径">
+                      <Input value={activeObject.content} onChange={(e) => this.updateElement(activeObjectIndex, { content: e.target.value })}/>
+                    </Form.Item>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="宽" style={{ flex: 1 }}>
+                        <InputNumber style={{ width: 60 }} value={activeObject.w} onChange={(value) => this.updateElement(activeObjectIndex, { w: value })}/>
+                      </Form.Item>
+                      <Form.Item label="高" style={{ flex: 1 }}>
+                        <InputNumber style={{ width: 60 }} value={activeObject.h} onChange={(value) => this.updateElement(activeObjectIndex, { h: value })}/>
+                      </Form.Item>
+                    </div>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="X偏移" style={{ flex: 1 }}>
+                        <InputNumber style={{ width: 60 }} value={activeObject.dx} onChange={(value) => this.updateElement(activeObjectIndex, { dx: value })}/>
+                      </Form.Item>
+                      <Form.Item label="Y偏移" style={{ flex: 1 }}>
+                        <InputNumber style={{ width: 60 }} value={activeObject.dy} onChange={(value) => this.updateElement(activeObjectIndex, { dy: value })}/>
+                      </Form.Item>
+                    </div>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="圆形" style={{ flex: 1 }}>
+                        <Switch checked={(activeObject.round || '').toString() === 'true'} onChange={(checked) => {
+                          if (checked) {
+                            this.updateElement(activeObjectIndex, { round: true })
+                          } else {
+                            this.updateElement(activeObjectIndex, { round: false })
+                          }
+                        }} checkedChildren="开" unCheckedChildren="关" />
+                      </Form.Item>
+                      <Form.Item label="圆角" style={{ flex: 1 }}>
+                        <Input
+                          placeholder="水平,垂直"
+                          style={{ width: 100 }}
+                          value={activeObject.corner}
+                          onChange={(e) => this.updateElement(activeObjectIndex, { corner: e.target.value })}
+                        />
+                      </Form.Item>
+                    </div>
+                    <Form.Item label="高斯模糊">
+                      <Input/>
+                    </Form.Item>
+                    <Form.Item label="图片裁剪选项">
+                      <Input/>
+                    </Form.Item>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="固定位置" style={{ flex: 1 }}>
+                        <Switch
+                          checked={activeObject.fixed}
+                          onChange={(checked) => this.updateElement(activeObjectIndex, { fixed: checked })}
+                          checkedChildren="开"
+                          unCheckedChildren="关"
+                        />
+                      </Form.Item>
+                      <Form.Item label="相对位置" style={{ flex: 1 }}>
+                        <Switch
+                          checked={activeObject.relative}
+                          onChange={(checked) => this.updateElement(activeObjectIndex, { relative: checked })}
+                          checkedChildren="开"
+                          unCheckedChildren="关"
+                        />
+                      </Form.Item>
+                    </div>
+                    <Form.Item label="分组" style={{ flex: 1 }}>
+                      <InputNumber
+                        style={{ width: 100 }}
+                        value={activeObject.group}
+                        onChange={(value) => this.updateElement(activeObjectIndex, { group: value })}
+                      />
+                    </Form.Item>
+                  </Form>
+                </div>
+              )
+            }
+          </div>
+        </div>
+        <Modal
+          title="重命名"
+          visible={this.state.visible}
+          onOk={() => {
+            this.updateElement(this.state.editIndex, { [this.state.editType === 'pic' ? 'path' : 't']: '#' + this.state.editName + '#' })
+            this.setState({ visible: false })
+          }}
+          onCancel={() => this.setState({ visible: false })}
+        >
+          <Input value={this.state.editName.replace(/#/g, '')} onChange={e => this.setState({ editName: e.target.value })} />
+        </Modal>
+      </div>
+    )
+  }
+}

+ 986 - 0
src/pages/poster/edit/index copy.js

@@ -0,0 +1,986 @@
+/* eslint-disable camelcase */
+import React from 'react'
+import { Button, Form, Input, Select, Slider, Switch, InputNumber } from 'antd'
+import fabric from 'fabric'
+import cloneDeep from 'lodash.clonedeep'
+import throttle from 'lodash.throttle'
+import RcColorPicker from 'rc-color-picker'
+import 'rc-color-picker/assets/index.css'
+import jrQrcode from 'jr-qrcode'
+
+import { optionArr, newOptionArr } from './util/data.js'
+
+import styles from './index.less'
+
+fabric = fabric.fabric
+const { Option } = Select
+
+const QRErrorCorrectLevel = {
+  L: 1,
+  M: 0,
+  Q: 3,
+  H: 2
+}
+
+const _config = {
+  canvasState: [],
+  currentStateIndex: -1,
+  undoStatus: false,
+  redoStatus: false,
+  undoFinishedStatus: 1,
+  redoFinishedStatus: 1
+}
+
+export default class Edit extends React.PureComponent {
+  state={
+    name: '模版一',
+    description: '描述',
+    canvas: {},
+
+    currentOptionArr: cloneDeep(newOptionArr), // 当前可设置的数组的值
+    optionArr: cloneDeep(newOptionArr) // 当前可设置的数组的值
+    // currentObjectType: 'text' // 当前要添加对象的类型
+  }
+
+  activeObject={}
+  currentOptionArr=cloneDeep(newOptionArr)
+
+  componentDidMount () {
+    this.canvas_sprite = new fabric.Canvas('canvas', {
+      ...this.state.currentOptionArr[0].css,
+      backgroundColor: this.state.currentOptionArr[0].css.background
+    })
+    this.addEventListener()
+  }
+
+  async addShape (index, action) {
+    const currentOptionArr = this.state.currentOptionArr
+    const { type } = currentOptionArr[index]
+    console.log(type, 1234567890)
+    let Shape
+    switch (type) {
+      case 'text':
+        Shape = await this.addTextObject(index, action)
+        break
+      case 'image':
+        Shape = await this.addImageObject(index, action)
+        break
+      case 'qrcode':
+        Shape = await this.addQrcodeObject(index, action)
+        break
+      default:
+        break
+    }
+    this.canvas_sprite.setActiveObject(Shape)
+    this.activeObject = Shape
+    // this.setState({
+    //   visible: true
+    // })
+    this.canvas_sprite.add(Shape)
+    if (action !== 'update') {
+      this.changeActiveObjectValue()
+    }
+  }
+
+  async addTextObject (index, action) {
+    let currentOptionArr
+    if (action === 'update') {
+      currentOptionArr = this.state.currentOptionArr
+    } else {
+      currentOptionArr = this.currentOptionArr
+    }
+    const { css } = currentOptionArr[index]
+    let {
+      width,
+      text,
+      color,
+      fontSize,
+      left,
+      top,
+      fontWeight,
+      fontFamily,
+      padding,
+      textDecoration,
+      borderRadius,
+      borderWidth,
+      borderColor,
+      rotate,
+      // align,
+      shadow,
+      lineHeight,
+      textAlign,
+      maxLines,
+      textStyle,
+      background
+    } = css
+    width = width / 1
+    left = left / 1
+    top = top / 1
+    borderRadius = borderRadius / 1
+    borderWidth = borderWidth / 1
+    rotate = rotate / 1
+    fontSize = fontSize / 1
+    maxLines = maxLines / 1
+    padding = 0
+    lineHeight = lineHeight / 1 // 和painter调试得出的值
+    shadow = shadow
+      .trim()
+      .split(/\s+/)
+      .join(' ')
+    // let Shape
+    const config = {
+      width, // 文字的高度随行高
+      fill: color,
+      fontWeight,
+      left: 0, // 距离画布左侧的距离,单位是像素
+      top: 0,
+      fontSize, // 文字大小
+      fontFamily,
+      padding,
+      [textDecoration]: true,
+      textAlign,
+      textStyle,
+      shadow,
+      myshadow: shadow,
+      splitByGrapheme: true, // 文字换行
+      lineHeight,
+      editable: true,
+      maxLines: maxLines,
+      textDecoration: textDecoration,
+      lockScalingY: true,
+      originX: 'center',
+      originY: 'center'
+    }
+    // 镂空
+    // if (textStyle === 'stroke') {
+    //   config = {
+    //     ...config,
+    //     stroke: color,
+    //     fill: 'rgba(0,0,0)'
+    //   }
+    // }
+
+    const textBox = new fabric.Textbox(text, config)
+    textBox.toObject = (function (toObject) {
+      return function () {
+        return fabric.util.object.extend(toObject.call(this), {
+          maxLines,
+          textDecoration,
+          textStyle
+        })
+      }
+    })(textBox.toObject)
+    // 通过最大行高计算高度,并删除多余文字,多出文字..表示,三个会换行
+    if (textBox.textLines.length > maxLines) {
+      let text = ''
+      for (let index = 0; index < maxLines; index++) {
+        const element = textBox.textLines[index]
+        if (index === maxLines - 1) {
+          text = text + element + '...'
+        } else {
+          text += element
+        }
+      }
+      textBox.set({
+        text
+      })
+      if (textBox.textLines.length > maxLines) {
+        let text = ''
+        for (let index = 0; index < maxLines; index++) {
+          const element = textBox.textLines[index]
+          if (index === maxLines - 1) {
+            text = text + element.substring(0, element.length - 3) + '...'
+          } else {
+            text += element
+          }
+        }
+        textBox.set({
+          text
+        })
+      }
+    }
+    const height = textBox.height / 1 + (textBox.lineHeight / 1 - 1) * textBox.fontSize + padding * 2
+    left = css.left - padding + borderWidth
+    top = css.top - padding + borderWidth
+    textBox.set({
+      top: -((textBox.lineHeight / 1 - 1) * textBox.fontSize) / 2
+    })
+
+    // const Rect = new fabric.Rect({
+    //   width: width + borderWidth,
+    //   height: height + borderWidth,
+    //   left: 0, // 距离画布左侧的距离,单位是像素
+    //   top: 0,
+    //   originX: 'center',
+    //   originY: 'center',
+    //   // padding,
+    //   rx: borderRadius,
+    //   strokeWidth: borderWidth / 1,
+    //   stroke: borderColor,
+    //   fill: background,
+    //   shadow,
+    //   selectable: false
+    // })
+    // this.canvas_sprite.add(Rect);
+    // let gradientOption = ''
+    // if (GD.api.isGradient(background)) {
+    //   alert(1)
+    //   gradientOption = GD.api.doGradient(background, width, height)
+    // } else {
+    //   alert(2)
+    // }
+    // if (gradientOption) Rect.setGradient('fill', gradientOption)
+    const Shape = new fabric.Group([], {
+      width: width + borderWidth,
+      height: height + borderWidth,
+      left: left + width / 2, // 距离画布左侧的距离,单位是像素
+      top: top + height / 2,
+      angle: rotate,
+      mytype: 'textGroup',
+      oldText: text,
+      originX: 'center',
+      originY: 'center',
+      rx: borderRadius,
+      strokeWidth: borderWidth / 1,
+      stroke: borderColor,
+      fill: background,
+      shadow,
+      myshadow: shadow,
+      lockScalingY: true
+    })
+    // Shape.add(Rect)
+    Shape.add(textBox)
+
+    Shape.toObject = (function (toObject) {
+      return function () {
+        return fabric.util.object.extend(toObject.call(this), {
+          mytype: 'textGroup',
+          oldText: text,
+          rx: borderRadius,
+          myshadow: shadow
+        })
+      }
+    })(Shape.toObject)
+    return Shape
+  }
+
+  async addImageObject (index, action) {
+    let currentOptionArr
+    if (action === 'update') {
+      currentOptionArr = this.state.currentOptionArr
+    } else {
+      currentOptionArr = this.currentOptionArr
+    }
+    const { css } = currentOptionArr[index]
+    let {
+      width,
+      height,
+      left,
+      top,
+      borderRadius,
+      borderWidth,
+      borderColor,
+      background,
+      rotate,
+      // align,
+      shadow,
+      mode,
+      url
+    } = css
+    width = width / 1
+    height = height / 1
+    left = left / 1
+    top = top / 1
+    borderRadius = borderRadius / 1
+    borderWidth = borderWidth / 1
+    rotate = rotate / 1
+    shadow = shadow
+      .trim()
+      .split(/\s+/)
+      .join(' ')
+    const Shape = await this.loadImageUrl(url)
+    const imgWidth = Shape.width
+    const imgHeight = Shape.height
+    Shape.set({
+      url,
+      // align,
+      mode,
+      shadow,
+      originX: 'center',
+      originY: 'center'
+    })
+    if (mode === 'scaleToFill') {
+      Shape.set({
+        width: imgWidth,
+        height: imgHeight,
+        scaleX: width / imgWidth,
+        scaleY: height / imgHeight,
+        oldScaleX: width / imgWidth,
+        oldScaleY: height / imgHeight
+      })
+      Shape.clipPath = new fabric.Rect({
+        width,
+        height,
+        originX: 'center',
+        originY: 'center',
+        rx: borderRadius,
+        scaleX: imgWidth / width,
+        scaleY: imgHeight / height
+      })
+    } else if (mode === 'auto') {
+      // 忽略高度会自适应宽度,等比缩放图片
+      Shape.set({
+        width: imgWidth,
+        height: imgHeight,
+        scaleX: width / imgWidth,
+        scaleY: width / imgWidth,
+        oldScaleX: width / imgWidth,
+        oldScaleY: height / imgHeight
+      })
+      Shape.clipPath = new fabric.Rect({
+        width,
+        height,
+        originX: 'center',
+        originY: 'center',
+        rx: borderRadius,
+        scaleX: imgWidth / width,
+        scaleY: imgHeight / height
+      })
+    } else if (mode === 'aspectFill') {
+      Shape.clipPath = new fabric.Rect({
+        width: width / 1,
+        height: height / 1,
+        originX: 'center',
+        originY: 'center',
+        rx: borderRadius / 1
+      })
+      Shape.set({
+        width,
+        height
+      })
+    }
+    const group = new fabric.Group([Shape], {
+      left: left + width / 2 + borderWidth,
+      top: top + height / 2 + borderWidth,
+      width: width + borderWidth,
+      height: height + borderWidth,
+      rx: borderRadius / 1,
+      strokeWidth: borderWidth / 1,
+      stroke: borderColor,
+      fill: background,
+      angle: rotate,
+      shadow,
+      myshadow: shadow,
+      originX: 'center',
+      originY: 'center',
+      mytype: 'image',
+      mode,
+      url,
+      lockUniScaling: true // 只能等比缩放
+    })
+    // 添加边框
+    group.add(
+      new fabric.Rect({
+        width: width + borderWidth,
+        height: height + borderWidth,
+        left: 0,
+        top: 0,
+        originX: 'center',
+        originY: 'center',
+        // padding,
+        rx: borderRadius + borderWidth / 2,
+        strokeWidth: borderWidth / 1,
+        stroke: borderColor,
+        fill: 'rgba(0,0,0,0)',
+        shadow,
+        selectable: false
+      })
+    )
+    group.toObject = (function (toObject) {
+      return function () {
+        return fabric.util.object.extend(toObject.call(this), {
+          mytype: 'image',
+          mode,
+          url,
+          rx: borderRadius + borderWidth / 2,
+          oldScaleX: width / imgWidth,
+          oldScaleY: height / imgHeight,
+          myshadow: shadow
+        })
+      }
+    })(group.toObject)
+    // console.log('group', group);
+    return group
+  }
+
+  async addQrcodeObject (index, action) {
+    let currentOptionArr
+    if (action === 'update') {
+      currentOptionArr = this.state.currentOptionArr
+    } else {
+      currentOptionArr = this.currentOptionArr
+    }
+    const { css } = currentOptionArr[index]
+    let {
+      width,
+      left,
+      top,
+      color,
+      borderRadius,
+      // borderWidth,
+      // borderColor,
+      background,
+      rotate,
+      url
+      // align,
+    } = css
+    width = width / 1
+    left = left / 1 + width / 2
+    top = top / 1 + width / 2
+    rotate = rotate / 1
+    const imgBase64 = jrQrcode.getQrBase64(url, {
+      padding: 0, // 二维码四边空白(默认为10px)
+      width: width / 1, // 二维码图片宽度(默认为256px)
+      height: width / 1, // 二维码图片高度(默认为256px)
+      correctLevel: QRErrorCorrectLevel.H, // 二维码容错level(默认为高)
+      reverse: false, // 反色二维码,二维码颜色为上层容器的背景颜色
+      background: background, // 二维码背景颜色(默认白色)
+      foreground: color // 二维码颜色(默认黑色)
+    })
+    const Shape = await this.loadImageUrl(imgBase64)
+    Shape.set({
+      url,
+      width: width / 1,
+      height: width / 1,
+      left,
+      top,
+      color,
+      background,
+      rx: borderRadius / 1,
+      // strokeWidth: borderWidth / 1,
+      // stroke: borderColor,
+      // align,
+      angle: rotate / 1,
+      lockUniScaling: true, // 只能等比缩放
+      originX: 'center',
+      originY: 'center',
+      mytype: 'qrcode'
+    })
+    Shape.clipPath = new fabric.Rect({
+      width,
+      height: width / 1,
+      originX: 'center',
+      originY: 'center',
+      rx: borderRadius,
+      angle: rotate / 1
+    })
+    Shape.toObject = (function (toObject) {
+      return function () {
+        return fabric.util.object.extend(toObject.call(this), {
+          mytype: 'qrcode',
+          url,
+          color,
+          background,
+          rx: borderRadius / 1
+        })
+      }
+    })(Shape.toObject)
+    return Shape
+  }
+
+  loadImageUrl = (imgUrl) => {
+    return new Promise(resolve => {
+      fabric.Image.fromURL(imgUrl, function (oImg) {
+        resolve(oImg)
+      })
+    })
+  }
+
+  changeActiveObjectValue = () => {
+    const type = this.activeObject.mytype
+    console.log(type, '99999999')
+    if (!type) return
+    // this.setState({
+    //   visible: true
+    // })
+    const item2 = this.activeObject
+    const width = `${(item2.width - item2.strokeWidth) * item2.scaleX}`
+    const height = `${(item2.height - item2.strokeWidth) * item2.scaleY}`
+    /* let left = `${(item2.left / item2.scaleY - (item2.width - item2.strokeWidth) / 2 - item2.strokeWidth).toFixed(2)}`;
+    let top = `${(item2.top / item2.scaleY - (item2.height - item2.strokeWidth) / 2 - item2.strokeWidth).toFixed(2)}`; */
+    const left = `${(item2.left - width / 2).toFixed(2)}`
+    const top = `${(item2.top - height / 2).toFixed(2)}`
+
+    let css = {
+      width,
+      height,
+      left,
+      top,
+      color: `${item2.color}`,
+      background: `${item2.fill}`,
+      rotate: `${item2.angle}`,
+      borderRadius: `${item2.rx * item2.scaleY}`,
+      borderWidth: `${item2.strokeWidth * item2.scaleY}`,
+      borderColor: `${item2.stroke}`,
+      shadow: `${item2.myshadow}`
+    }
+    let index = ''
+    switch (type) {
+      case 'textGroup':
+        index = 1
+        item2._objects.forEach(ele => {
+          // const css2 = {
+          //   text: '',
+          //   width,
+          //   maxLines: '',
+          //   lineHeight: '',
+          //   left,
+          //   top,
+          //   color: `${item2.color}`,
+          //   background: `${item2.fill}`,
+          //   fontSize: '',
+          //   fontWeight: '',
+          //   textDecoration: '',
+          //   rotate: `${item2.angle}`,
+          //   // padding: 0,
+          //   borderRadius: `${item2.rx * item2.scaleY}`,
+          //   borderWidth: `${item2.strokeWidth * item2.scaleY}`,
+          //   borderColor: `${item2.stroke}`,
+          //   shadow: `${item2.shadow}`,
+          //   textStyle: '',
+          //   textAlign: '',
+          //   fontFamily: ''
+          // }
+          // if (ele.type === 'rect') {
+          // } else {
+
+          // }
+          css = {
+            // text: '',
+            width,
+            // maxLines: '',
+            // lineHeight: '',
+            left,
+            top,
+            // color: `${item2.color}`,
+            background: `${item2.fill}`,
+            // fontSize: '',
+            // fontWeight: '',
+            // textDecoration: '',
+            rotate: `${item2.angle}`,
+            // padding: 0,
+            borderRadius: `${item2.rx * item2.scaleY}`,
+            borderWidth: `${item2.strokeWidth * item2.scaleY}`,
+            borderColor: `${item2.stroke}`,
+            // shadow: `${item2.shadow}`,
+            // textStyle: '',
+            // textAlign: '',
+            // fontFamily: '',
+            text: `${item2.oldText}`,
+            maxLines: `${ele.maxLines}`,
+            lineHeight: `${ele.lineHeight}`,
+            color: ele.fill,
+            // padding: `${ele.padding}`,
+            fontSize: `${ele.fontSize}`,
+            fontWeight: `${ele.fontWeight}`,
+            textStyle: `${ele.textStyle}`,
+            textDecoration: `${ele.textDecoration === 'linethrough' ? 'line-through' : ele.textDecoration}`,
+            fontFamily: `${ele.fontFamily}`,
+            textAlign: `${ele.textAlign}`,
+            shadow: `${item2.myshadow}`
+          }
+        })
+        break
+      case 'image':
+        index = 2
+        delete css.color
+        delete css.background
+        css = {
+          url: item2.url,
+          ...css,
+          mode: `${item2.mode}`,
+          shadow: `${item2.myshadow}`
+        }
+        break
+      case 'qrcode':
+        index = 3
+        delete css.height
+        delete css.borderWidth
+        delete css.borderColor
+        delete css.shadow
+        css = {
+          url: item2.url,
+          ...css,
+          color: item2.color,
+          background: item2.background
+        }
+        break
+      default:
+        break
+    }
+    const currentOptionArr = cloneDeep(this.state.currentOptionArr)
+    currentOptionArr[index].css = css
+    this.setState({
+      currentOptionArr
+    })
+  }
+
+  handleChange = (index, key, value) => {
+    const currentOptionArr = cloneDeep(this.state.currentOptionArr)
+    currentOptionArr[index].css[key] = value
+    this.setState(
+      {
+        currentOptionArr
+      },
+      () => {
+        this.updateObject()
+      }
+    )
+  }
+
+  async updateObject () {
+    const type = this.activeObject.mytype
+    this.canvas_sprite.remove(this.activeObject)
+    switch (type) {
+      case 'textGroup':
+        await this.addShape(1, 'update')
+        break
+      case 'image':
+        await this.addShape(2, 'update')
+        break
+      case 'qrcode':
+        await this.addShape(3, 'update')
+        break
+      default:
+        break
+    }
+    this.canvas_sprite.renderAll()
+  }
+
+  addEventListener = () => {
+    const throttlechangeActiveObjectValue = throttle(this.changeActiveObjectValue, 100)
+    this.canvas_sprite.on('object:moving', (e) => {
+      console.log('object:moving')
+      var obj = e.target
+      // if object is too big ignore
+      if (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) {
+        return
+      }
+      obj.setCoords()
+      // top-left  corner
+      if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
+        obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top)
+        obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left)
+      }
+      // bot-right corner
+      if (
+        obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height ||
+        obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
+      ) {
+        obj.top = Math.min(
+          obj.top,
+          obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top
+        )
+        obj.left = Math.min(
+          obj.left,
+          obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left
+        )
+      }
+
+      throttlechangeActiveObjectValue()
+    })
+    this.canvas_sprite.on('object:scaling', (e) => {
+      console.log('object:scaling')
+      var obj = e.target
+      // if object is too big ignore
+      if (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) {
+        return
+      }
+      obj.setCoords()
+      // top-left  corner
+      if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
+        obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top)
+        obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left)
+      }
+      // bot-right corner
+      if (
+        obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height ||
+        obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
+      ) {
+        obj.top = Math.min(
+          obj.top,
+          obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top
+        )
+        obj.left = Math.min(
+          obj.left,
+          obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left
+        )
+      }
+      throttlechangeActiveObjectValue()
+    })
+    this.canvas_sprite.on('mouse:down', (e) => {
+      console.log('mouse:down')
+      if (e.target) {
+        this.activeObject = e.target
+        this.changeActiveObjectValue()
+      }
+    })
+    // 解决放大缩小元素位置不对的问题
+    this.canvas_sprite.on('object:scaled', (e) => {
+      console.log('object:scaled')
+      if (e.target) {
+        this.activeObject = e.target
+        this.updateObject()
+      }
+    })
+    this.canvas_sprite.on('object:modified', () => {
+      console.log('object:modified')
+      this.updateCanvasState()
+    })
+
+    this.canvas_sprite.on('object:added', () => {
+      console.log('object:added')
+      this.updateCanvasState()
+    })
+  }
+
+  updateCanvasState = () => {
+    const canvas_sprite = this.canvas_sprite
+    if (_config.undoStatus === false && _config.redoStatus === false) {
+      var jsonData = canvas_sprite.toJSON()
+      var canvasAsJson = JSON.stringify(jsonData)
+      if (_config.currentStateIndex < _config.canvasState.length - 1) {
+        var indexToBeInserted = _config.currentStateIndex + 1
+        _config.canvasState[indexToBeInserted] = canvasAsJson
+        var numberOfElementsToRetain = indexToBeInserted + 1
+        _config.canvasState = _config.canvasState.splice(0, numberOfElementsToRetain)
+      } else {
+        _config.canvasState.push(canvasAsJson)
+      }
+      _config.currentStateIndex = _config.canvasState.length - 1
+      if (_config.currentStateIndex === _config.canvasState.length - 1 && _config.currentStateIndex !== -1) {
+        this.setState({
+          redoButtonStatus: 'disabled'
+        })
+      }
+    }
+  }
+
+  save = () => {
+  }
+
+  render () {
+    const canvasOption = this.currentOptionArr[0].css
+    return (
+      <React.Fragment>
+        <div className={styles.header}><span>模版:{this.state.name}</span><span>保存于00:00:00 <Button>预览</Button><Button type="primary" onClick={this.save}>保存</Button></span></div>
+        <div className={styles.content}>
+          <div className={styles.left}>
+            <div className={styles.box}>
+              <h2>背景</h2>
+              <Form layout="vertical">
+                <Form.Item label="宽度" >
+                  <Input defaultValue={canvasOption.width} onChange={(e) => {
+                    this.currentOptionArr[0].css.width = e.target.value
+                    this.canvas_sprite.setWidth(e.target.value)
+                  }}/>
+                </Form.Item>
+                <Form.Item label="高度">
+                  <Input defaultValue={canvasOption.height} onChange={(e) => {
+                    this.currentOptionArr[0].css.height = e.target.value
+                    this.canvas_sprite.setHeight(e.target.value)
+                  }}/>
+                </Form.Item>
+                <Form.Item label="背景色">
+                  <Input defaultValue={canvasOption.background} onChange={(e) => {
+                    this.currentOptionArr[0].css.background = e.target.value
+                    this.canvas_sprite.setBackgroundColor(e.target.value)
+                    this.canvas_sprite.renderAll()
+                  }}/>
+                </Form.Item>
+              </Form>
+            </div>
+            <div className={styles.box}>
+              <h2>元素</h2>
+              <ul className={styles.elem}>
+                <li onClick={() => this.addShape(1)}>文字</li>
+                <li onClick={() => this.addShape(2)}>图片</li>
+                <li onClick={() => this.addShape(3)}>二维码</li>
+              </ul>
+            </div>
+            <div className={styles.box}>
+              <h2>图层列表</h2>
+              <ul className={styles.layer}>
+                <li className={styles.selected}>xxxx</li>
+                <li>xxxx</li>
+              </ul>
+            </div>
+          </div>
+          <div className={styles.middle}>
+            <canvas id="canvas"></canvas>
+          </div>
+          <div className={styles.right}>
+            {
+              this.activeObject.mytype === 'textGroup' && (
+                <div className={styles.box}>
+                  <h2>文字</h2>
+                  <Form layout="vertical">
+                    <Form.Item label="内容">
+                      <Input.TextArea rows={3} defaultValue={this.state.currentOptionArr[1].css.text} onChange={(e) => this.handleChange(1, 'text', e.target.value)}/>
+                    </Form.Item>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="字体" style={{ width: 150 }}>
+                        <Select defaultValue={this.state.currentOptionArr[1].css.fontFamily} style={{ width: 145 }} onChange={(value) => this.handleChange(1, 'fontFamily', value)}>
+                          <Option value="msyh">msyh</Option>
+                          <Option value="msyhbd">msyhbd</Option>
+                          <Option value="NotoSerifCJK">NotoSerifCJK</Option>
+                          <Option value="PingFang">PingFang</Option>
+                          <Option value="pingfang-sc-semibold">pingfang-sc-semibold</Option>
+                          <Option value="pingfang_jiancu">pingfang_jiancu</Option>
+                          <Option value="pingfang_jianxi">pingfang_jianxi</Option>
+                          <Option value="pingfang_regular">pingfang_regular</Option>
+                          <Option value="PingFang_SC">PingFang_SC</Option>
+                          <Option value="pingfang_sc_medium">pingfang_sc_medium</Option>
+                          <Option value="pingfang_sc_semibold">pingfang_sc_semibold</Option>
+                          <Option value="simhei">simhei</Option>
+                          <Option value="SourceHanSansCN-Normal">SourceHanSansCN-Normal</Option>
+                          <Option value="喜鹊招牌体">喜鹊招牌体</Option>
+                          <Option value="喜鹊聚珍体regular">喜鹊聚珍体regular</Option>
+                          <Option value="思源宋体">思源宋体</Option>
+                          <Option value="方正正粗黑简体">方正正粗黑简体</Option>
+                        </Select>
+                      </Form.Item>
+                      <Form.Item label="字体大小" style={{ flex: 1 }}>
+                        <InputNumber style={{ width: 80 }} min={12} max={100} defaultValue={this.state.currentOptionArr[1].css.fontSize} onChange={(value) => this.handleChange(1, 'fontSize', value)} />
+                      </Form.Item>
+                      <Form.Item label="字体颜色" style={{ flex: 1 }}>
+                        <RcColorPicker
+                          color={this.state.currentOptionArr[1].css.color}
+                          onChange={({ color }) => this.handleChange(1, 'color', color)}
+                        >
+                          <Input style={{ width: 80 }} />
+                        </RcColorPicker>
+                      </Form.Item>
+                    </div>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="X偏移" style={{ flex: 1 }}>
+                        <Input style={{ width: 100 }} defaultValue={this.state.currentOptionArr[1].css.left} onChange={(e) => this.handleChange(1, 'left', e.target.value)}/>
+                      </Form.Item>
+                      <Form.Item label="Y偏移" style={{ flex: 1 }}>
+                        <Input style={{ width: 100 }} defaultValue={this.state.currentOptionArr[1].css.top} onChange={(e) => this.handleChange(1, 'top', e.target.value)}/>
+                      </Form.Item>
+                    </div>
+                    <Form.Item label="文字宽度">
+                      <Input defaultValue={this.state.currentOptionArr[1].css.width} onChange={(e) => this.handleChange(1, 'width', e.target.value)}/>
+                    </Form.Item>
+                    <Form.Item label="文字行数">
+                      <Input defaultValue={this.state.currentOptionArr[1].css.maxLines} onChange={(e) => this.handleChange(1, 'maxLines', e.target.value)}/>
+                    </Form.Item>
+                    <Form.Item label="X居中">
+                      <Select defaultValue={this.state.currentOptionArr[1].css.textAlign} style={{ width: 150 }} onChange={(value) => this.handleChange(1, 'textAlign', value)}>
+                        <Option value="left">left</Option>
+                        <Option value="center">center</Option>
+                        <Option value="right">right</Option>
+                      </Select>
+                    </Form.Item>
+                    <Form.Item label="文字垂直">
+                      <Input/>
+                    </Form.Item>
+                    <Form.Item label="不透明">
+                      <Slider
+                        min={1}
+                        max={20}
+                        onChange={(value) => console.log(value)}
+                        value={10}
+                      />
+                    </Form.Item>
+                  </Form>
+                </div>
+              )
+            }
+
+            {
+              this.activeObject.mytype === 'qrcode' && (
+                <div className={styles.box}>
+                  <h2>二维码</h2>
+                  <Form layout="vertical">
+                    <Form.Item label="内容">
+                      <Input defaultValue={this.state.currentOptionArr[3].css.url} onChange={(e) => this.handleChange(3, 'url', e.target.value)}/>
+                    </Form.Item>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="X偏移" style={{ flex: 1 }}>
+                        <Input style={{ width: 100 }} defaultValue={this.state.currentOptionArr[3].css.left} onChange={(e) => this.handleChange(3, 'left', e.target.value)}/>
+                      </Form.Item>
+                      <Form.Item label="Y偏移" style={{ flex: 1 }}>
+                        <Input style={{ width: 100 }} defaultValue={this.state.currentOptionArr[3].css.top} onChange={(e) => this.handleChange(3, 'top', e.target.value)}/>
+                      </Form.Item>
+                    </div>
+                    <Form.Item label="LOGO">
+                      <Switch defaultChecked onChange={(checked) => console.log(checked)} checkedChildren="开" unCheckedChildren="关" />
+                      <Select defaultValue="wpt" style={{ width: 120, marginLeft: '5px' }}>
+                        <Option value="wpt">微拍堂</Option>
+                        <Option value="youjiang"></Option>
+                      </Select>
+                    </Form.Item>
+                    <Form.Item label="大小">
+                      <Input style={{ width: 100 }} defaultValue={this.state.currentOptionArr[3].css.width} onChange={(e) => this.handleChange(3, 'width', e.target.value)}/>
+                    </Form.Item>
+                  </Form>
+                </div>
+              )
+            }
+
+            {
+              this.activeObject.mytype === 'image' && (
+                <div className={styles.box}>
+                  <h2>图片</h2>
+                  <Form layout="vertical">
+                    <Form.Item label="路径">
+                      <Input/>
+                    </Form.Item>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="宽" style={{ flex: 1 }}>
+                        <Input style={{ width: 100 }} defaultValue={this.state.currentOptionArr[2].css.width} onChange={(e) => this.handleChange(2, 'width', e.target.value)}/>
+                      </Form.Item>
+                      <Form.Item label="高" style={{ flex: 1 }}>
+                        <Input style={{ width: 100 }} defaultValue={this.state.currentOptionArr[2].css.height} onChange={(e) => this.handleChange(2, 'height', e.target.value)}/>
+                      </Form.Item>
+                    </div>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="X偏移" style={{ flex: 1 }}>
+                        <Input style={{ width: 100 }} defaultValue={this.state.currentOptionArr[2].css.left} onChange={(e) => this.handleChange(2, 'left', e.target.value)}/>
+                      </Form.Item>
+                      <Form.Item label="Y偏移" style={{ flex: 1 }}>
+                        <Input style={{ width: 100 }} defaultValue={this.state.currentOptionArr[2].css.top} onChange={(e) => this.handleChange(2, 'top', e.target.value)}/>
+                      </Form.Item>
+                    </div>
+                    <div style={{ display: 'flex' }}>
+                      <Form.Item label="圆形" style={{ flex: 1 }}>
+                        <Switch defaultChecked onChange={(checked) => {
+                          if (checked) {
+                            this.handleChange(2, 'borderRadius', 1000)
+                          } else {
+                            this.handleChange(2, 'borderRadius', 0)
+                          }
+                        }} checkedChildren="开" unCheckedChildren="关" />
+                      </Form.Item>
+                      <Form.Item label="圆角" style={{ flex: 1 }}>
+                        <Input style={{ width: 100 }} defaultValue={this.state.currentOptionArr[2].css.borderRadius} onChange={(e) => this.handleChange(2, 'borderRadius', e.target.value)}/>
+                      </Form.Item>
+                    </div>
+                    <Form.Item label="高斯模糊">
+                      <Input/>
+                    </Form.Item>
+                    <Form.Item label="图片裁剪选项">
+                      <Input/>
+                    </Form.Item>
+                  </Form>
+                </div>
+              )
+            }
+          </div>
+        </div>
+      </React.Fragment>
+    )
+  }
+}

+ 140 - 0
src/pages/poster/edit/index.less

@@ -0,0 +1,140 @@
+.header{
+  display: flex;
+  border-bottom: 1px solid #f5f5f5;
+  padding-bottom: 10px;
+  span:nth-child(2){
+    flex:1;
+  }
+  :nth-child(1), :nth-child(2){
+    // font-size: 16px;
+    // font-weight: 700;
+    line-height: 32px;
+  }
+  :nth-child(3){
+    text-align: right;
+    button, a{
+      margin-left: 10px;
+    }
+  }
+}
+.textRnd{
+  display: -webkit-box;
+  overflow: hidden;
+  word-break: break-all;
+  text-overflow: ellipsis;
+  -webkit-box-orient: vertical;
+}
+.content{
+  display: flex;
+  flex:1;
+  overflow: hidden;
+  h2{
+    font-size: 16px;
+    font-weight: normal;
+  }
+  .box{
+    // border-bottom: 1px solid #f5f5f5;
+    padding: 20px 0;
+    clear: both;
+  }
+  :global{
+    .ant-form-item {margin-bottom: 0;}
+    .ant-form-item label {
+      span{
+        color: rgba(0,0,0,.45);
+        margin-left: 8px;
+      }
+    }
+  }
+}
+.outline{
+  // background-color: #dcdcdc;
+  // content: '';
+  // position: absolute;
+  // left:0;
+  // top:0;
+  // width: 110%;
+  // height: 110%;
+  // outline: 1px solid #dcdcdc;
+  // background-image: url('https://cdn.weipaitang.com/static/20200415b2d0884d-0686-884d0686-b446-eed21b0b1b0d-W10H10'),
+  // url('https://cdn.weipaitang.com/static/20200415b2d0884d-0686-884d0686-b446-eed21b0b1b0d-W10H10'),
+  // url('https://cdn.weipaitang.com/static/20200415b2d0884d-0686-884d0686-b446-eed21b0b1b0d-W10H10');
+  // background-position: 0px 0px, 0px 50%, 0px 100%;
+  // background-repeat: no-repeat;
+  // background-size: 10px 10px;
+  // padding-bottom: 10px;
+  // box-sizing: content-box;
+  // padding-right: 10px;
+  opacity: 0.6;
+  outline: dotted #1890ff 2px;
+}
+.left{
+  width:200px;
+  overflow: auto;
+}
+.elem{
+  list-style: none;
+  padding:0 0 0 5px;
+  margin:0;
+  img{
+    width: 40px;
+  }
+  li{
+    width: 33.3%;
+    float: left;
+    border: 1px solid #f4f4f4;
+    height: 100px;
+    margin-left: -1px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+  }
+}
+.layer{
+  line-height:32px;
+  list-style: none;
+  padding:0;
+  margin:0;
+  li{
+    padding-left: 5px;
+    cursor: pointer;
+    border-left: 5px solid red;
+
+    i{
+      float: right;
+      margin-top: 9px;
+      display: none;
+      margin-right: 5px;
+    }
+  }
+  li.pic{
+    border-color: blue;
+  }
+  li.qr{
+    border-color: green;
+  }
+  li:hover{
+    background-color:#1890ff;
+    opacity: 0.6;
+    color:#fff;
+    i{display: block;}
+  }
+  .selected{
+    background-color:#1890ff;
+    color:#fff;
+  }
+}
+
+.middle{
+  flex: 1;
+  background: #dcdcdc;
+  margin: 0px 10px;
+  min-height: 100vh;
+  min-width: 750px;
+  overflow: auto;
+}
+.right{
+  width:200px;
+  float: right;
+}

+ 20 - 0
src/pages/poster/edit/service.js

@@ -0,0 +1,20 @@
+import { fetchApi, fetchApi_get as fetchApiGet } from '@/apis/'
+import { dc } from '@/conf/config'
+
+console.log(fetchApiGet)
+// 新增海报模版
+export async function create (params) {
+  const url = `${dc}/api/poster/create`
+  return fetchApi(url, params)
+}
+// 海报详情
+export async function fetchDetail (params) {
+  const url = `${dc}/api/poster/detail`
+  return fetchApiGet(url, params)
+}
+
+// 海报编辑
+export async function update (params) {
+  const url = `${dc}/api/poster/update`
+  return fetchApi(url, params)
+}

+ 99 - 0
src/pages/poster/edit/util/data.js

@@ -0,0 +1,99 @@
+import _ from 'lodash'
+const optionArr = [
+  {
+    type: 'canvas',
+    name: '画布',
+    css: {
+      width: '654',
+      height: '1000',
+      background: 'orange',
+      times: '1'
+    }
+  },
+  {
+    type: 'text',
+    name: '文字',
+    css: {
+      text: '文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本',
+      width: 200,
+      height: 100,
+      fontFamily: '微软雅黑字体', //
+      textAlign: ['left', 'center', 'right'], // 文字的对齐方式,分为 left, center, right
+      left: 0,
+      top: 250,
+      color: '#000000',
+      fontSize: 16,
+
+      maxLines: 2, // 最大行数
+      lineHeight: 1, // 行间距
+      background: 'rgba(0,0,0,0)',
+      fontWeight: ['normal', 'bold'], // 文字加粗 可以不写
+      textDecoration: ['none', 'overline', 'underline', 'linethrough'], // overline underline line-through 可组合
+      rotate: 0,
+      borderRadius: 0,
+      borderWidth: 0,
+      borderColor: '#000000',
+      shadow: '', // 10 10 5 #888888
+      textStyle: ['fill', 'stroke'] // fill: 填充样式,stroke:镂空样式
+    }
+  },
+  // {
+  //   type: 'rect',
+  //   name: '矩形',
+  //   css: {
+  //     // background: 'linear-gradient(190deg, rgba(243, 227, 223, 1) 20%, rgba(238, 214, 205, 1) 80%)',
+  //     // background: 'radial-gradient(rgba(0, 0, 0, 0) 5%, #0ff 15%, #f0f 60%)',
+  //     width: 200,
+  //     height: 122, // 高度,没有的话就自适应
+  //     left: 452,
+  //     top: 250,
+  //     background: '#f8d4d4',
+  //     rotate: 0,
+  //     borderRadius: 0,
+  //     borderWidth: 0,
+  //     borderColor: '#ff0000',
+  //     shadow: '' // 阴影
+  //   }
+  // },
+  {
+    type: 'image',
+    name: '图片',
+    css: {
+      url:
+        'https://desk-fd.zol-img.com.cn/t_s960x600c5/g5/M00/0F/08/ChMkJlauzXWIDrXBAAdCg2xP7oYAAH9FQOpVAIAB0Kb342.jpg',
+      width: 320,
+      height: 200, // 高度,没有的话就自适应
+      left: 200,
+      top: 400,
+      rotate: 0,
+      borderRadius: 0,
+      borderWidth: 0,
+      borderColor: '#000000',
+      shadow: '', // 阴影
+      mode: ['scaleToFill', 'aspectFill', 'auto']
+    }
+  },
+  {
+    type: 'qrcode',
+    name: '二维码',
+    css: {
+      url: '哈哈哈',
+      width: 200,
+      left: 454,
+      top: 403,
+      color: '#000000', // 字体颜色 linear-gradient(-135deg, #fedcba 0%, rgba(18, 52, 86, 1) 20%, #987 80%)
+      background: '#ffffff', // 文字区域背景色
+      rotate: 0,
+      borderRadius: 0
+    }
+  }
+]
+
+// 得到当前默认信息
+const newOptionArr = _.cloneDeep(optionArr)
+newOptionArr[1].css.textStyle = newOptionArr[1].css.textStyle[0]
+newOptionArr[1].css.textAlign = newOptionArr[1].css.textAlign[0]
+newOptionArr[1].css.fontWeight = newOptionArr[1].css.fontWeight[0]
+newOptionArr[1].css.textDecoration = newOptionArr[1].css.textDecoration[0]
+newOptionArr[2].css.mode = newOptionArr[2].css.mode[0]
+export { optionArr, newOptionArr }

+ 148 - 0
src/pages/poster/edit/util/gradient.js

@@ -0,0 +1,148 @@
+/* eslint-disable */
+// 当ctx传入当前文件,const grd = ctx.createCircularGradient() 和
+// const grd = this.ctx.createLinearGradient() 无效,因此只能分开处理
+// 先分析,在外部创建grd,再传入使用就可以
+
+!(function() {
+  var api = {
+    isGradient: function(bg) {
+      if (bg && (bg.startsWith('linear') || bg.startsWith('radial'))) {
+        return true;
+      }
+      return false;
+    },
+
+    doGradient: function(bg, width, height) {
+      if (bg.startsWith('linear')) {
+        return linearEffect(width, height, bg);
+      } else if (bg.startsWith('radial')) {
+        return radialEffect(width, height, bg);
+      }
+    }
+  };
+
+  function analizeGrad(string) {
+    const colorPercents = string.substring(0, string.length - 1).split('%,');
+    const colors = [];
+    const percents = [];
+    for (let colorPercent of colorPercents) {
+      colors.push(colorPercent.substring(0, colorPercent.lastIndexOf(' ')).trim());
+      percents.push(colorPercent.substring(colorPercent.lastIndexOf(' '), colorPercent.length) / 100);
+    }
+    return { colors: colors, percents: percents };
+  }
+
+  function radialEffect(width, height, bg, ctx) {
+    const colorPer = analizeGrad(bg.match(/radial-gradient\((.+)\)/)[1]);
+    //const grd = ctx.createCircularGradient(0, 0, width < height ? height / 2 : width / 2);
+    let colorStops = {};
+    for (let i = 0; i < colorPer.colors.length; i++) {
+      //grd.addColorStop(colorPer.percents[i], colorPer.colors[i]);
+      colorStops[colorPer.percents[i]] = colorPer.colors[i];
+    }
+    //ctx.setFillStyle(grd);
+    //ctx.fillRect(-(width / 2), -(height / 2), width, height);
+
+    return {
+      type: 'radial',
+      x1: 0 + width / 2,
+      y1: 0 + height / 2,
+      r1: 0,
+      x2: 0 + width / 2,
+      y2: 0 + height / 2,
+      r2: width < height ? height / 2 : width / 2,
+      colorStops
+    };
+  }
+
+  function analizeLinear(bg, width, height) {
+    const direction = bg.match(/([-]?\d{1,3})deg/);
+    const dir = direction && direction[1] ? parseFloat(direction[1]) : 0;
+    let coordinate;
+    switch (dir) {
+      case 0:
+        coordinate = [0, -height / 2, 0, height / 2];
+        break;
+      case 90:
+        coordinate = [width / 2, 0, -width / 2, 0];
+        break;
+      case -90:
+        coordinate = [-width / 2, 0, width / 2, 0];
+        break;
+      case 180:
+        coordinate = [0, height / 2, 0, -height / 2];
+        break;
+      case -180:
+        coordinate = [0, -height / 2, 0, height / 2];
+        break;
+      default:
+        let x1 = 0;
+        let y1 = 0;
+        let x2 = 0;
+        let y2 = 0;
+        if (direction[1] > 0 && direction[1] < 90) {
+          x1 =
+            width / 2 -
+            (((width / 2) * Math.tan(((90 - direction[1]) * Math.PI * 2) / 360) - height / 2) *
+              Math.sin((2 * (90 - direction[1]) * Math.PI * 2) / 360)) /
+              2;
+          y2 = Math.tan(((90 - direction[1]) * Math.PI * 2) / 360) * x1;
+          x2 = -x1;
+          y1 = -y2;
+        } else if (direction[1] > -180 && direction[1] < -90) {
+          x1 =
+            -(width / 2) +
+            (((width / 2) * Math.tan(((90 - direction[1]) * Math.PI * 2) / 360) - height / 2) *
+              Math.sin((2 * (90 - direction[1]) * Math.PI * 2) / 360)) /
+              2;
+          y2 = Math.tan(((90 - direction[1]) * Math.PI * 2) / 360) * x1;
+          x2 = -x1;
+          y1 = -y2;
+        } else if (direction[1] > 90 && direction[1] < 180) {
+          x1 =
+            width / 2 +
+            ((-(width / 2) * Math.tan(((90 - direction[1]) * Math.PI * 2) / 360) - height / 2) *
+              Math.sin((2 * (90 - direction[1]) * Math.PI * 2) / 360)) /
+              2;
+          y2 = Math.tan(((90 - direction[1]) * Math.PI * 2) / 360) * x1;
+          x2 = -x1;
+          y1 = -y2;
+        } else {
+          x1 =
+            -(width / 2) -
+            ((-(width / 2) * Math.tan(((90 - direction[1]) * Math.PI * 2) / 360) - height / 2) *
+              Math.sin((2 * (90 - direction[1]) * Math.PI * 2) / 360)) /
+              2;
+          y2 = Math.tan(((90 - direction[1]) * Math.PI * 2) / 360) * x1;
+          x2 = -x1;
+          y1 = -y2;
+        }
+        coordinate = [x1, y1, x2, y2];
+        break;
+    }
+    return coordinate;
+  }
+
+  function linearEffect(width, height, bg, ctx) {
+    const param = analizeLinear(bg, width, height);
+    //const grd = ctx.createLinearGradient(param[0], param[1], param[2], param[3]);
+    const content = bg.match(/linear-gradient\((.+)\)/)[1];
+    const colorPer = analizeGrad(content.substring(content.indexOf(',') + 1));
+    let colorStops = {};
+    for (let i = 0; i < colorPer.colors.length; i++) {
+      //grd.addColorStop(colorPer.percents[i], colorPer.colors[i]);
+      colorStops[colorPer.percents[i]] = colorPer.colors[i];
+    }
+    //ctx.setFillStyle(grd);
+    //ctx.fillRect(-(width / 2), -(height / 2), width, height);
+    return {
+      type: 'linear',
+      x1: param[0] + width / 2,
+      y1: param[1] + height / 2,
+      x2: param[2] + width / 2,
+      y2: param[3] + height / 2,
+      colorStops
+    };
+  }
+  module.exports = { api };
+})();

+ 76 - 0
src/pages/poster/list/components/ConfigModal.js

@@ -0,0 +1,76 @@
+import React, { Component } from 'react'
+import { Modal, Spin, Button } from 'antd'
+import { connect } from 'dva'
+
+// less
+import styles from './index.less'
+
+@connect(({ posterList, loading }) => {
+  const { configDetail = [] } = posterList
+  return {
+    configDetail,
+    loading: loading.effects['posterList/getConfigDetail']
+  }
+}, null, null, { withRef: true })
+
+class ConfigModal extends Component {
+  constructor (props) {
+    super(props)
+    this.state = {
+      modalVisible: false
+    }
+  }
+
+  // modal弹窗出现
+  showConfigModal = (id) => {
+    this.setState({
+      modalVisible: true
+    }, () => {
+      this.props.dispatch({
+        type: 'posterList/getConfigDetail',
+        payload: { id }
+      })
+    })
+  }
+
+  // modal弹窗消失
+  hideConfigModal = () => {
+    this.setState({
+      modalVisible: false
+    })
+  }
+
+  // render弹窗内容分
+  renderConfigContent = () => {
+    const { configDetail } = this.props
+    return configDetail.map((item, index) => {
+      return <p key={index}>{item}</p>
+    })
+  }
+
+  render () {
+    const { modalVisible } = this.state
+    const { loading } = this.props
+    return (
+      <div className={styles.configModal}>
+        <Modal
+          title="全部配置"
+          visible={modalVisible}
+          onOk={this.hideConfigModal}
+          onCancel={this.hideConfigModal}
+          footer={
+            <Button onClick={this.hideConfigModal}>
+              关闭
+            </Button>
+          }
+        >
+          <Spin spinning={loading}>
+            {this.renderConfigContent()}
+          </Spin>
+        </Modal>
+      </div>
+    )
+  }
+}
+
+export default ConfigModal

+ 109 - 0
src/pages/poster/list/components/CopyTemplateModal.js

@@ -0,0 +1,109 @@
+import React, { Component, Fragment } from 'react';
+import { Modal, Form, Input, message, Button } from 'antd';
+import { connect } from 'dva';
+
+// less
+import styles from './index.less';
+
+const { Item: FormItem } = Form;
+const formItemLayout = {
+  labelCol: { span: 7 },
+  wrapperCol: { span: 14 },
+};
+
+@connect(({ loading }) => {
+  return {
+    btnLoading: loading.effects['posterList/copyTemplate'],
+  }
+})
+
+@Form.create()
+class CopyTemplateModal extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      modalVisible: false,
+    }
+  }
+
+  // modal弹窗出现
+  showCopyModal = (id, refresh) => {
+    this.templateId = id;
+    this.refresh = refresh;
+    this.setState({
+      modalVisible: true,
+    })
+  }
+
+  // modal弹窗消失
+  hideCopyModal = () => {
+    this.props.form.resetFields();
+    this.setState({
+      modalVisible: false,
+    });
+  }
+  
+  // 点击提交
+  handleSubmit = () => {
+    const { form, dispatch } = this.props;
+    form.validateFields((err, values) => {
+      if (!err) {
+        dispatch({
+          type: 'posterList/copyTemplate',
+          payload: {
+            id: this.templateId,
+            ...values,
+          },
+          // 设置回调
+          cb: () => {
+            message.success('复制成功');
+            // 复制成功后,返回第一页
+            this.refresh(1);
+            this.hideCopyModal();
+          },
+        })
+      }
+    })
+  }
+
+  render() {
+    const { modalVisible } = this.state;
+    const { form: { getFieldDecorator }, btnLoading } = this.props;
+    return (
+      <div className={styles.copyTemplateModal}>
+        <Modal
+          title="复制新模板"
+          visible={modalVisible}
+          onOk={this.handleSubmit}
+          onCancel={this.hideCopyModal}
+          confirmLoading={btnLoading}
+        >
+          <Form { ...formItemLayout }>
+            <FormItem label="新模板标识" required>
+              {getFieldDecorator('name', {
+                rules: [{
+                  required: true,
+                  message: '新模板标识不能为空',
+                }]
+              })(
+                <Input placeholder="请输入新模板标识" />
+              )}
+            </FormItem>
+            <FormItem label="新模板描述" required>
+              {getFieldDecorator('description', {
+                rules: [{
+                  required: true,
+                  message: '新模板描述不能为空',
+                }]
+              })(
+                <Input placeholder="请输入新模板描述" />
+              )}
+            </FormItem>
+          </Form>
+        </Modal>
+      </div>
+    )
+  }
+}
+
+export default CopyTemplateModal;

+ 0 - 0
src/pages/poster/list/components/index.less


+ 154 - 0
src/pages/poster/list/index.js

@@ -0,0 +1,154 @@
+import React, { Component, Fragment } from 'react'
+import { Divider } from 'antd'
+import { FilterTable, ImagePreview } from 'wptpc-design'
+import { connect } from 'dva'
+import { routerRedux } from 'dva/router'
+
+import ConfigModal from './components/ConfigModal'
+import CopyTemplateModal from './components/CopyTemplateModal'
+import { dc } from '@/conf/config'
+import { fetchApi_get } from '@/apis/'
+// less
+import styles from './index.less'
+
+// apiUrl
+const apiUrl = `${dc}/api/poster/list`
+@connect()
+class Poster extends Component {
+  constructor (props) {
+    super(props)
+    this.state = {
+      imgs: [], // 预览图片的url
+      visible: false // 预览框是否展示
+    }
+  }
+
+  // 查看配置
+  handleConfig = ({ id }) => {
+    const configModalRef = this.configModal
+    if (configModalRef && configModalRef.getWrappedInstance()) {
+      configModalRef.getWrappedInstance().showConfigModal(id)
+    }
+  }
+
+  // 复制模板
+  handleCopyTemplate = ({ id }) => {
+    const copyTemplateModalRef = this.copyTemplateModal
+    if (copyTemplateModalRef) {
+      copyTemplateModalRef.showCopyModal(id, this.refresh)
+    }
+  }
+
+  // 预览图片
+  previewImg ({ preview = '' }) {
+    if (preview) {
+      this.setState({
+        imgs: [preview],
+        visible: true
+      })
+    }
+  }
+
+  render () {
+    // 搜索框配置
+    const filterSettingConfig = {
+      // 清空后是否触发查询
+      isClearSearch: true,
+      // 更改后端返回的数据结构
+      // changeApiData: ({ code, data }) => {
+      //   return {
+      //     code,
+      //     data: {
+      //       list: data,
+      //       total: 1,
+      //     },
+      //   }
+      // },
+      formFields: [{
+        label: '模版标识',
+        type: 'input',
+        key: 'keyword',
+        placeholder: '模板id,模板标识'
+      }]
+      // 查询前触发的函数,进行参数整合
+      // beforeSearchFunc: (data) => {
+      // },
+    }
+
+    // 表格配置
+    const tableSettingConfig = {
+      // key
+      rowKey: 'id',
+      // 设置分页信息
+      // piganition: {
+      //   pageSize: 10,
+      // },
+      // 每一列配置
+      columnConfig: [{
+        title: '序号',
+        dataIndex: 'id'
+      }, {
+        title: '海报标识',
+        dataIndex: 'name'
+      }, {
+        title: '海报描述',
+        dataIndex: 'description'
+      }, {
+        title: '操作',
+        dataIndex: 'actions',
+        render: (_, record) => {
+          return (
+            <Fragment>
+              <a href={`/poster/edit/${record.id}`}>编辑</a>
+              <Divider type="vertical" />
+              <a onClick={this.handleConfig.bind(this, record)}>查看配置</a>
+              <Divider type="vertical" />
+              <a onClick={this.handleCopyTemplate.bind(this, record)}>复制模版</a>
+              <Divider type="vertical" />
+              <a onClick={this.previewImg.bind(this, record)}>预览</a>
+            </Fragment>
+          )
+        }
+      }],
+      // 放在搜索和列表之间额外按钮
+      batchBtns: [{
+        key: 'craeteBtn',
+        label: '创建',
+        type: 'primary',
+        onClick: (e) => {
+          this.props.dispatch(routerRedux.push({ pathname: '/poster/edit' }))
+          console.log(this.props.dispatch, '1234567890')
+          // TODO:暂时没有创建功能
+        }
+      }],
+      // 刷新页面
+      getRefresh: refresh => {
+        this.refresh = refresh
+      }
+    }
+
+    const { imgs, visible } = this.state
+    return (
+      <div className={styles.poster}>
+        <FilterTable
+          fetchApi={fetchApi_get}
+          tableSetting={tableSettingConfig}
+          filterSetting={filterSettingConfig}
+          apiUrl={apiUrl}
+        />
+        {/* 预览图片 */}
+        <ImagePreview
+          images={imgs}
+          visible={visible}
+          onClose={() => { this.setState({ visible: false }) }}
+        />
+        {/* 查看配置弹窗 */}
+        <ConfigModal ref={r => this.configModal = r } />
+        {/* 复制模板弹窗 */}
+        <CopyTemplateModal wrappedComponentRef={r => this.copyTemplateModal = r } />
+      </div>
+    )
+  }
+}
+
+export default Poster

+ 1 - 0
src/pages/poster/list/index.less

@@ -0,0 +1 @@
+.poster {}

+ 38 - 0
src/pages/poster/list/model.js

@@ -0,0 +1,38 @@
+import { message } from 'antd';
+import { getConfigDetailAction, copyTemplateAction } from './services';
+
+export default {
+  namespace: 'posterList',
+
+  state: {
+    configDetail: [],
+  },
+
+  effects: {
+    // 获取海报配置属性信息
+    *getConfigDetail({ payload }, { call, put }) {
+      const res = yield call(getConfigDetailAction, payload);
+      if (res) {
+        const { data = [] } = res;
+        yield put({
+          type: 'updatePosterList',
+          payload: {
+            configDetail: data,
+          },
+        })
+      }
+    },
+    
+    // 复制新模板
+    *copyTemplate({ payload, cb }, { call }) {
+      const res = yield call(copyTemplateAction, payload);
+      if (res) cb && cb();
+    }
+  },
+
+  reducers: {
+    updatePosterList(state, { payload }) {
+      return { ...state, ...payload };
+    }
+  },
+}

+ 14 - 0
src/pages/poster/list/services.js

@@ -0,0 +1,14 @@
+import { request } from '@/apis/base';
+import { dc } from '@/conf/config';
+
+// 获取配置属性信息
+export async function getConfigDetailAction(params) {
+  const url = `${dc}/api/poster/property`;
+  return request({ url, params });
+}
+
+// 复制模板
+export async function copyTemplateAction(data) {
+  const url = `${dc}/api/poster/copy`;
+  return request({ url, method: 'POST', data });
+}

+ 21 - 0
yarn.lock

@@ -2466,6 +2466,11 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
     mime-types "~2.1.24"
     negotiator "0.6.2"
 
+ace-builds@^1.4.6:
+  version "1.4.9"
+  resolved "http://npm.wpt.la/ace-builds/-/ace-builds-1.4.9.tgz#2b9b020706871f30e97f5510af891149f144d3d8"
+  integrity sha1-K5sCBwaHHzDpf1UQr4kRSfFE09g=
+
 acorn-dynamic-import@^4.0.0:
   version "4.0.0"
   resolved "http://npm.wpt.la/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
@@ -5535,6 +5540,11 @@ didyoumean@1.2.1:
   resolved "http://npm.wpt.la/didyoumean/-/didyoumean-1.2.1.tgz#e92edfdada6537d484d73c0172fd1eba0c4976ff"
   integrity sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=
 
+diff-match-patch@^1.0.4:
+  version "1.0.4"
+  resolved "http://npm.wpt.la/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1"
+  integrity sha1-asS1UjdGN2HE2vDcYD64aRJHRLE=
+
 diff-sequences@^24.9.0:
   version "24.9.0"
   resolved "http://npm.wpt.la/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
@@ -13925,6 +13935,17 @@ rc@^1.2.7, rc@^1.2.8:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+react-ace@^8.1.0:
+  version "8.1.0"
+  resolved "http://npm.wpt.la/react-ace/-/react-ace-8.1.0.tgz#6680accb7306a1850e6ef4370a00f0007eadc8b4"
+  integrity sha1-ZoCsy3MGoYUObvQ3CgDwAH6tyLQ=
+  dependencies:
+    ace-builds "^1.4.6"
+    diff-match-patch "^1.0.4"
+    lodash.get "^4.4.2"
+    lodash.isequal "^4.5.0"
+    prop-types "^15.7.2"
+
 react-app-polyfill@^1.0.2:
   version "1.0.4"
   resolved "http://npm.wpt.la/react-app-polyfill/-/react-app-polyfill-1.0.4.tgz#4dd2636846b585c2d842b1e44e1bc29044345874"