浏览代码

new event view

lianghuang 4 年之前
父节点
当前提交
cef0cb436e

+ 110 - 0
coms/jenga/block/BlockComment.jsx

@@ -0,0 +1,110 @@
+import React from 'react'
+import LazyLoad from 'react-lazyload'
+import cls from 'classnames'
+import newEventStores from 'stores/newEvent'
+import Block from './base/Block'
+import BlockOrder from './base/BlockOrder'
+import BlockDel from './base/BlockDel'
+import BlockCommentWrap from './base/comment/BlockCommentWrap'
+
+export default class BlockComment extends Block {
+  constructor(props) {
+    super(props)
+    this.state.showComment = newEventStores.showComment
+
+    this.changeShowComment = this.changeShowComment.bind(this)
+  }
+
+  changeShowComment(showComment) {
+    if (this.state.showComment !== showComment) {
+      this.setState({ showComment })
+    }
+  }
+
+  render() {
+    let jengaContent = document.getElementsByClassName('jenga-content')[0]
+
+    let getRelChildComs = () => {
+      return (
+        <div
+          className={cls(
+            'btn-clear btn-toggle-sub btn-toggle-sub-empty',
+            'btn-toggle-sub-empty-' + this.getLineColor(),
+            { 'btn-toggle-sub-comment-zip': !this.state.showComment }
+          )}
+        />
+      )
+    }
+
+    return (
+      <LazyLoad height={26} overflow={true} scrollContainer={jengaContent}>
+        <div className="block-wrap block-wrap-comment f--h">
+          <span
+            className={cls('index-line', 'index-line-' + this.getLineColor(), {
+              'index-last-line': this.props.isLast,
+              'index-last-line-comment-zip': !this.state.showComment,
+              'index-virtual-layer': this.props.virtualLayer
+            })}
+            style={this.indexLeftStyle()}
+          />
+          <div
+            className={cls('flex-1', {
+              'flex-wrap-disabled': !this.state.isEnable
+            })}
+          >
+            <div
+              className={cls('block-item block-comment f--h', {
+                'block-active': this.state.isActive
+              })}
+              // onDragOver={this.onDragOver}
+              // onDrop={this.onDrop}
+              onClick={this.onSelectBlock}
+              onContextMenu={this.onContextMenu}
+            >
+              <BlockDel
+                isActive={this.state.isActive}
+                layer={this.props.layer}
+                bid={this.props.bid}
+              />
+              <BlockOrder
+                layer={this.props.layer}
+                bid={this.props.bid}
+                eventNodeId={this.props.eventNodeId}
+                className={
+                  !this.state.showComment ? 'block-order-comment-zip' : ''
+                }
+                extraGapGroupLayer={this.extraGapGroupLayer()}
+              />
+              <div
+                className="block-gap"
+                style={{
+                  left: -this.getGapLeft() + 'px',
+                  padding: '0 ' + this.getGapLeft() / 2 + 'px'
+                }}
+              />
+              {getRelChildComs()}
+              <div
+                id={'block-main-' + this.props.bid}
+                className="block-main flex-1 f--h"
+                style={this.blockMainStyle()}
+                draggable={true}
+                // onDragStart={this.onDragStart}
+                // onDragEnd={this.onDragEnd}
+              >
+                <div
+                  id={'over-cover-' + this.props.bid}
+                  className="over-cover"
+                />
+                <BlockCommentWrap
+                  bid={this.props.bid}
+                  eventNodeId={this.props.eventNodeId}
+                  changeShowComment={this.changeShowComment}
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+      </LazyLoad>
+    )
+  }
+}

+ 161 - 0
coms/jenga/block/BlockGroup.jsx

@@ -0,0 +1,161 @@
+import React from 'react'
+import LazyLoad from 'react-lazyload'
+import cls from 'classnames'
+import Block from './base/Block'
+import BlockChildren from './base/BlockChildren'
+import BlockOrder from './base/BlockOrder'
+import BlockDel from './base/BlockDel'
+import BlockDescWrap from './base/group/BlockDescWrap'
+import BlockEnable from './base/BlockEnable'
+
+export default class BlockGroup extends Block {
+  constructor(props) {
+    super(props)
+  }
+
+  render() {
+    let jengaContent = document.getElementsByClassName('jenga-content')[0]
+
+    let getRelChildComs = () => {
+      return this.state.hasChildren ? (
+        <div className="f--h">
+          <div
+            className={cls(
+              'btn-clear btn-toggle-sub btn-toggle-sub-empty',
+              'btn-toggle-sub-empty-' + this.getLineColor(),
+              'btn-toggle-sub-empty-small'
+            )}
+            style={{ marginTop: '9px' }}
+          />
+          <button
+            className={cls('btn-clear btn-toggle-sub btn-toggle-group f--hcc')}
+            onClick={this.onToggleBlockExpand}
+          >
+            <div className={cls('icon', { collpase: !this.state.isExpand })} />
+          </button>
+        </div>
+      ) : (
+        <div
+          className={cls(
+            'btn-clear btn-toggle-sub btn-toggle-sub-empty',
+            'btn-toggle-sub-empty-' + this.getLineColor()
+          )}
+        />
+      )
+    }
+
+    let extraLineCom = () => {
+      return (
+        this.state.isExpand &&
+        this.state.hasChildren && (
+          <div
+            style={{
+              left: -this.getGapLeft() + 'px',
+              marginLeft: this.getGapLeft() - 1 + 'px'
+            }}
+            className={cls(
+              'index-line',
+              'index-extra-children',
+              'index-line-' + this.getLineColor()
+            )}
+          />
+        )
+      )
+    }
+
+    return (
+      <LazyLoad height={26} overflow={true} scrollContainer={jengaContent}>
+        <div
+          className={cls(
+            'block-wrap block-wrap-group f--h',
+            `block-wrap-group-${this.getGroupColor()}`
+          )}
+        >
+          <span
+            className={cls('index-line', 'index-line-' + this.getLineColor(), {
+              'index-last-line': this.props.isLast,
+              'index-virtual-layer': this.props.virtualLayer
+            })}
+            style={this.indexLeftStyle()}
+          />
+          <div
+            className={cls('flex-1 block-wrapper-inner', {
+              'flex-wrap-disabled': !this.state.isEnable
+            })}
+          >
+            <div
+              className={cls('block-item block-group f--h', {
+                'block-active': this.state.isActive
+              })}
+              // onDragOver={this.onDragOver}
+              // onDrop={this.onDrop}
+              onClick={this.onSelectBlock}
+              onContextMenu={this.onContextMenu}
+            >
+              <BlockDel
+                isActive={this.state.isActive}
+                layer={this.props.layer}
+                bid={this.props.bid}
+              />
+              <BlockOrder
+                layer={this.props.layer}
+                bid={this.props.bid}
+                eventNodeId={this.props.eventNodeId}
+                extraGapGroupLayer={this.extraGapGroupLayer()}
+              />
+              <div
+                className="block-gap"
+                style={{
+                  left: -this.getGapLeft() + 'px',
+                  padding: '0 ' + this.getGapLeft() / 2 + 'px'
+                }}
+              />
+              {extraLineCom()}
+              {getRelChildComs()}
+              <div
+                id={'block-main-' + this.props.bid}
+                className="block-main flex-1 f--h"
+                style={this.blockMainStyle()}
+                draggable={true}
+                onDragStart={this.onDragStart}
+                onDragEnd={this.onDragEnd}
+              >
+                <div
+                  id={'over-cover-' + this.props.bid}
+                  className="over-cover"
+                />
+                <BlockEnable
+                  className="btn-toggle-con-enable"
+                  bid={this.props.bid}
+                  isEnable={this.state.isEnable}
+                  eventNodeId={this.props.eventNodeId}
+                />
+                <BlockDescWrap
+                  bid={this.props.bid}
+                  hasGroupExtraLine={
+                    this.props.groupLayer === 0 && this.state.hasChildren
+                  }
+                />
+              </div>
+            </div>
+            {/*group 不需要设置layer+1*/}
+            {this.state.isExpand ? (
+              <BlockChildren
+                loops={this.getLoops()}
+                layer={this.props.layer}
+                groupLayer={this.props.groupLayer + 1}
+                nestGroupLayer={this.props.nestGroupLayer + 1}
+                offspringInNestGroupLayer={this.props.nestGroupLayer + 1}
+                extraGapGroupLayer={this.extraGapGroupLayer()}
+                parentBid={this.props.bid}
+                eventNodeId={this.props.eventNodeId}
+                visibleBlockIds={this.props.visibleBlockIds}
+                virtualLayer={this.props.layer}
+              />
+            ) : null}
+          </div>
+        </div>
+      </LazyLoad>
+    )
+  }
+}

+ 0 - 1
coms/jenga/block/BlockLoop.jsx

@@ -100,7 +100,6 @@ export default class BlockLoop extends Block {
                 draggable={true}
                 // onDragStart={this.onDragStart}
                 // onDragEnd={this.onDragEnd}
-                onClick={this.onToggleParamsVisible}
               >
                 <div
                   id={'over-cover-' + this.props.bid}

+ 295 - 0
coms/jenga/block/base/BlockChildren.jsx

@@ -0,0 +1,295 @@
+import React from 'react'
+import cls from 'classnames'
+import newEventStores from 'stores/newEvent'
+import { getEventBlockByBid } from 'stores/funcs/newEvent'
+import { shallowEqualImmutable } from 'utils/utils'
+import { EVENT_BLOCK_TYPE, BLOCK_OP_TYPE } from 'const/const'
+
+export default class BlockChildren extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      subBlockIds: this.getBlockChildren(props.parentBid)
+    }
+    // 加载相关组件
+    this.blockAction = require('../BlockAction').default
+    this.blockCon = require('../BlockCon').default
+    this.blockLoop = require('../BlockLoop').default
+    this.blockComment = require('../BlockComment').default
+    this.blockGroup = require('../BlockGroup').default
+    // this.blockError = require('./BlockError').default
+
+    this.onEventChange = this.onEventChange.bind(this)
+    this.getBlockChildren = this.getBlockChildren.bind(this)
+    this.getChild = this.getChild.bind(this)
+    this.isBlockGroupChildren = this.isBlockGroupChildren.bind(this)
+    this.getBlockGroupBg = this.getBlockGroupBg.bind(this)
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    return (
+      !shallowEqualImmutable(this.props, nextProps) ||
+      !shallowEqualImmutable(this.state, nextState)
+    )
+  }
+
+  componentDidUpdate(preProps) {
+    // parentBid改变时需要更新
+    if (preProps.parentBid !== this.props.parentBid) {
+      let obj = {}
+      let subBlockIds = this.getBlockChildren()
+      if (!shallowEqualImmutable(this.state.subBlockIds, subBlockIds)) {
+        obj.subBlockIds = subBlockIds
+      }
+      if (Object.keys(obj).length > 0) {
+        this.setState(obj)
+      }
+    }
+  }
+
+  componentDidMount() {
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (
+        (status.types.includes('changeBlockChildren') &&
+          status.data &&
+          status.data.parentBid === this.props.parentBid) ||
+        (status.types.includes('reloadBlock') &&
+          this.props.parentBid === status.data.affectBlockId)
+      ) {
+        obj.subBlockIds = this.getBlockChildren()
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  getBlockChildren() {
+    let childBids = []
+    let nodeId = this.props.eventNodeId
+    let parentBlock = getEventBlockByBid(this.props.parentBid, nodeId)
+    if (parentBlock) {
+      if (parentBlock.eventId) {
+        if (
+          parentBlock.ast &&
+          parentBlock.ast.args &&
+          parentBlock.ast.args.length > 0
+        ) {
+          parentBlock.ast.args.forEach(child => {
+            if (child.ln) {
+              childBids.push(child.ln)
+            }
+          })
+        }
+      } else {
+        switch (parentBlock.op) {
+          case BLOCK_OP_TYPE.IF:
+          case BLOCK_OP_TYPE.ELSE:
+          case BLOCK_OP_TYPE.FOR:
+          case BLOCK_OP_TYPE.WHILE:
+            if (
+              parentBlock.args &&
+              parentBlock.args.length > 0 &&
+              parentBlock.args[1]
+            ) {
+              if (
+                parentBlock.args[1].op === 'block' &&
+                parentBlock.args[1].args &&
+                parentBlock.args[1].args.length > 0
+              ) {
+                parentBlock.args[1].args.forEach(child => {
+                  if (child.ln) {
+                    childBids.push(child.ln)
+                  }
+                })
+              }
+            }
+            break
+          case BLOCK_OP_TYPE.BLOCK:
+            if (parentBlock.args && parentBlock.args.length > 0) {
+              parentBlock.args.forEach(child => {
+                if (child.ln) {
+                  childBids.push(child.ln)
+                }
+              })
+            }
+            break
+        }
+      }
+    }
+    return childBids
+  }
+
+  getChild(bid, index) {
+    let nodeId = this.props.eventNodeId
+    let block = getEventBlockByBid(bid, nodeId)
+    if (block) {
+      switch (block.op) {
+        case BLOCK_OP_TYPE.SET:
+        case BLOCK_OP_TYPE.CALL:
+        case BLOCK_OP_TYPE.LET:
+        case undefined:
+          let BlockAction = this.blockAction
+          return (
+            <BlockAction
+              key={index}
+              layer={this.props.layer}
+              groupLayer={this.props.groupLayer}
+              nestGroupLayer={this.props.nestGroupLayer}
+              offspringInNestGroupLayer={this.props.offspringInNestGroupLayer}
+              extraGapGroupLayer={this.props.extraGapGroupLayer}
+              loops={this.props.loops}
+              bid={bid}
+              eventNodeId={this.props.eventNodeId}
+              visibleBlockIds={this.props.visibleBlockIds}
+              isLast={index === this.state.subBlockIds.length - 1}
+              virtualLayer={this.props.virtualLayer}
+            />
+          )
+        case BLOCK_OP_TYPE.IF:
+        case BLOCK_OP_TYPE.ELSE:
+          let BlockCon = this.blockCon
+          return (
+            <BlockCon
+              key={index}
+              layer={this.props.layer}
+              groupLayer={this.props.groupLayer}
+              nestGroupLayer={this.props.nestGroupLayer}
+              offspringInNestGroupLayer={this.props.offspringInNestGroupLayer}
+              extraGapGroupLayer={this.props.extraGapGroupLayer}
+              loops={this.props.loops}
+              bid={bid}
+              eventNodeId={this.props.eventNodeId}
+              visibleBlockIds={this.props.visibleBlockIds}
+              isLast={index === this.state.subBlockIds.length - 1}
+              virtualLayer={this.props.virtualLayer}
+            />
+          )
+        case BLOCK_OP_TYPE.FOR:
+        case BLOCK_OP_TYPE.WHILE:
+          let BlockLoop = this.blockLoop
+          return (
+            <BlockLoop
+              key={index}
+              layer={this.props.layer}
+              groupLayer={this.props.groupLayer}
+              nestGroupLayer={this.props.nestGroupLayer}
+              offspringInNestGroupLayer={this.props.offspringInNestGroupLayer}
+              extraGapGroupLayer={this.props.extraGapGroupLayer}
+              loops={this.props.loops}
+              bid={bid}
+              eventNodeId={this.props.eventNodeId}
+              visibleBlockIds={this.props.visibleBlockIds}
+              isLast={index === this.state.subBlockIds.length - 1}
+              virtualLayer={this.props.virtualLayer}
+            />
+          )
+        case BLOCK_OP_TYPE.NOP:
+          let BlockComment = this.blockComment
+          return (
+            <BlockComment
+              key={index}
+              layer={this.props.layer}
+              groupLayer={this.props.groupLayer}
+              nestGroupLayer={this.props.nestGroupLayer}
+              offspringInNestGroupLayer={this.props.offspringInNestGroupLayer}
+              extraGapGroupLayer={this.props.extraGapGroupLayer}
+              loops={this.props.loops}
+              bid={bid}
+              eventNodeId={this.props.eventNodeId}
+              visibleBlockIds={this.props.visibleBlockIds}
+              isLast={index === this.state.subBlockIds.length - 1}
+              virtualLayer={this.props.virtualLayer}
+            />
+          )
+        case BLOCK_OP_TYPE.BLOCK:
+          let BlockGroup = this.blockGroup
+          //Group的嵌套处理
+          return (
+            <BlockGroup
+              key={index}
+              layer={this.props.layer}
+              groupLayer={this.props.groupLayer}
+              nestGroupLayer={this.props.nestGroupLayer}
+              offspringInNestGroupLayer={this.props.offspringInNestGroupLayer}
+              extraGapGroupLayer={this.props.extraGapGroupLayer}
+              loops={this.props.loops}
+              bid={bid}
+              eventNodeId={this.props.eventNodeId}
+              visibleBlockIds={this.props.visibleBlockIds}
+              isLast={index === this.state.subBlockIds.length - 1}
+              virtualLayer={this.props.virtualLayer}
+            />
+          )
+      }
+    } else {
+      // let BlockError = this.blockError
+      // return (
+      //   <BlockError
+      //     key={index}
+      //     layer={this.props.layer}
+      //     groupLayer={this.props.groupLayer}
+      //     nestGroupLayer={this.props.nestGroupLayer}
+      //     offspringInNestGroupLayer={this.props.offspringInNestGroupLayer}
+      //     extraGapGroupLayer={this.props.extraGapGroupLayer}
+      //     loops={this.props.loops}
+      //     bid={bid}
+      //     eventNodeId={this.props.eventNodeId}
+      //     visibleBlockIds={this.props.visibleBlockIds}
+      //     isLast={index === this.state.subBlockIds.length - 1}
+      //     virtualLayer={this.props.virtualLayer}
+      //   />
+      // )
+    }
+    return null
+  }
+
+  isBlockGroupChildren = () => {
+    let nodeId = this.props.eventNodeId
+    let parentBlock = getEventBlockByBid(this.props.parentBid, nodeId)
+    return parentBlock?.op === BLOCK_OP_TYPE.BLOCK
+  }
+
+  getBlockGroupBg = () => {
+    const leftGap = 18 + (this.props.nestGroupLayer - 1) * 2
+    const gapping = `${leftGap + (this.props.groupLayer - 1) * 2}px`
+    const left = `${leftGap}px`
+    const style = {
+      width: `calc(100% - ${gapping})`,
+      left: left
+    }
+    return this.isBlockGroupChildren() ? (
+      <div className={'block-group-bg'} style={style} />
+    ) : null
+  }
+
+  render() {
+    let subBlockIds = this.state.subBlockIds
+    if (this.props.visibleBlockIds) {
+      subBlockIds = subBlockIds.filter(id => {
+        return this.props.visibleBlockIds.indexOf(id) >= 0
+      })
+    }
+    return subBlockIds && subBlockIds.length > 0 ? (
+      <div
+        className={cls('block-children', {
+          'block-group-children': this.isBlockGroupChildren()
+        })}
+      >
+        {this.getBlockGroupBg()}
+        {subBlockIds.map((subBlockId, index) => {
+          return this.getChild(subBlockId, index)
+        })}
+      </div>
+    ) : null
+  }
+}

+ 6 - 0
coms/jenga/block/base/ConItem.jsx

@@ -26,6 +26,7 @@ export default class ConItem extends React.Component {
     this.onChangeOperator = this.onChangeOperator.bind(this)
     this.onChangeCon = this.onChangeCon.bind(this)
     this.onAddCon = this.onAddCon.bind(this)
+    this.onDeleteCon = this.onDeleteCon.bind(this)
 
     this.isSelectedFlag = this.isSelectedFlag.bind(this)
     this.isSelectedOperator = this.isSelectedOperator.bind(this)
@@ -117,6 +118,11 @@ export default class ConItem extends React.Component {
     this.props.onConsChange()
   }
 
+  onDeleteCon() {
+    this.props.onDelBlockCon({ index: this.props.index, resetLast: true })
+    this.props.onConsChange()
+  }
+
   onChangeCon(prop, value, forceUpdate = true) {
     let con = this.state.conItem
     con[prop] = value

+ 131 - 0
coms/jenga/block/base/comment/BlockCommentWrap.jsx

@@ -0,0 +1,131 @@
+import React from 'react'
+import i18n from 'i18next'
+import { message } from 'antd'
+import TextareaAutosize from 'react-autosize-textarea'
+import newEventActions from 'actions/newEvent'
+import newEventStores from 'stores/newEvent'
+import { getEventBlockByBid } from 'stores/funcs/newEvent'
+import { BLOCK_OP_TYPE } from 'const/const'
+import { copyToClipboard } from 'utils/commons/copyToClipboard'
+import { shallowEqualImmutable } from 'utils/utils'
+
+export default class BlockCommentWrap extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      showComment: newEventStores.showComment,
+      comment: this.getComment()
+    }
+
+    this.onEventChange = this.onEventChange.bind(this)
+    this.getComment = this.getComment.bind(this)
+    this.copy = this.copy.bind(this)
+    this.onChange = this.onChange.bind(this)
+    this.onBlur = this.onBlur.bind(this)
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    return (
+      !shallowEqualImmutable(this.props, nextProps) ||
+      !shallowEqualImmutable(this.state, nextState)
+    )
+  }
+
+  componentDidMount() {
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+    let { changeShowComment } = this.props
+    let { showComment } = this.state
+    changeShowComment && changeShowComment(showComment)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  componentDidUpdate(preProps) {
+    if (preProps.bid !== this.props.bid) {
+      this.setState({
+        comment: this.getComment()
+      })
+    }
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (status.types.includes('changeShowComment')) {
+        let { showComment } = this.state
+        if (showComment !== status.data.showComment) {
+          obj.showComment = status.data.showComment
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj, () => {
+        let { changeShowComment } = this.props
+        let { showComment } = this.state
+        changeShowComment && changeShowComment(showComment)
+      })
+    }
+  }
+
+  getComment() {
+    let result = ''
+    let nodeId = this.props.eventNodeId || newEventStores.curEventNodeId
+    let block = getEventBlockByBid(this.props.bid, nodeId)
+    if (block && block.op === BLOCK_OP_TYPE.NOP) {
+      result = block.val || ''
+    }
+    return result
+  }
+
+  copy() {
+    copyToClipboard(this.state.comment)
+    message.success(i18n.t('EventView.jenga_log.copy.success'), 1)
+  }
+
+  onChange(e) {
+    let value = e && e.target ? e.target.value : e
+    this.setState({
+      comment: value
+    })
+  }
+
+  onBlur() {
+    if (this.getComment() !== this.state.comment) {
+      let nodeId = this.props.eventNodeId || newEventStores.curEventNodeId
+      newEventActions.updateBlockProp({
+        nodeId,
+        blockId: this.props.bid,
+        prop: 'val',
+        value: this.state.comment
+      })
+    }
+  }
+
+  render() {
+    let { comment } = this.state
+    return (
+      <div className="comment-wrap">
+        {!this.state.showComment ? (
+          <div className="zip-wrap" />
+        ) : (
+          <div className="input-wrap f--h">
+            <button className="btn-clear btn-icon" onClick={this.copy} />
+            <div className="input-area f--hlc">
+              <TextareaAutosize
+                value={comment || ''}
+                placeholder={'备注详情'}
+                onChange={this.onChange}
+                onBlur={this.onBlur}
+                className={'comment-textarea'}
+                maxRows={4}
+                async={true}
+              />
+            </div>
+          </div>
+        )}
+      </div>
+    )
+  }
+}

+ 89 - 0
coms/jenga/block/base/group/BlockDescWrap.jsx

@@ -0,0 +1,89 @@
+import React from 'react'
+import TextareaAutosize from 'react-autosize-textarea'
+import newEventActions from 'actions/newEvent'
+import newEventStores from 'stores/newEvent'
+import { getEventBlockByBid } from 'stores/funcs/newEvent'
+import { BLOCK_OP_TYPE } from 'const/const'
+import { shallowEqualImmutable } from 'utils/utils'
+
+export default class BlockDescWrap extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      desc: this.getDesc()
+    }
+
+    this.getDesc = this.getDesc.bind(this)
+    this.onChange = this.onChange.bind(this)
+    this.onBlur = this.onBlur.bind(this)
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    return (
+      !shallowEqualImmutable(this.props, nextProps) ||
+      !shallowEqualImmutable(this.state, nextState)
+    )
+  }
+
+  componentDidUpdate(preProps) {
+    if (preProps.bid !== this.props.bid) {
+      this.setState({
+        desc: this.getDesc()
+      })
+    }
+  }
+
+  getDesc() {
+    let result = ''
+    let nodeId = this.props.eventNodeId || newEventStores.curEventNodeId
+    let block = getEventBlockByBid(this.props.bid, nodeId)
+    if (block && block.type === BLOCK_OP_TYPE.BLOCK) {
+      result = block.com || ''
+    }
+    return result
+  }
+
+  onChange(e) {
+    let value = e && e.target ? e.target.value : e
+    this.setState({
+      desc: value
+    })
+  }
+
+  onBlur() {
+    if (this.getDesc() !== this.state.desc) {
+      let nodeId = this.props.eventNodeId || newEventStores.curEventNodeId
+      newEventActions.updateBlockProp({
+        nodeId,
+        blockId: this.props.bid,
+        prop: 'com',
+        value: this.state.desc
+      })
+    }
+  }
+
+  render() {
+    let { desc } = this.state
+    return (
+      <div className="desc-wrap">
+        {this.props.hasGroupExtraLine ? (
+          <div className="group-desc-extra-line" />
+        ) : null}
+        <div className="input-wrap f--h">
+          <button className="btn-clear btn-icon"/>
+          <div className="input-area f--hlc">
+            <TextareaAutosize
+              value={desc || ''}
+              placeholder={'分组详情'}
+              onChange={this.onChange}
+              onBlur={this.onBlur}
+              className={'desc-textarea'}
+              maxRows={4}
+              async={true}
+            />
+          </div>
+        </div>
+      </div>
+    )
+  }
+}

+ 248 - 0
coms/jenga/extra/IotMessageConfig.jsx

@@ -0,0 +1,248 @@
+import React from 'react'
+import cls from 'classnames'
+import i18n from 'i18next'
+import treeActions from 'actions/tree'
+import widgetStore from 'stores/widget'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+
+export default class IotMessageConfig extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      groupId: this.getGroupId(),
+      releaseGroupId: this.getReleaseGroupId(),
+      topics: this.getTopics()
+    }
+    // this.msgData = undefined
+    this.prefix = '${ProductKey}/${deviceName}/'
+
+    this.getGroupId = this.getGroupId.bind(this)
+    this.getTopics = this.getTopics.bind(this)
+    this.getTitleName = this.getTitleName.bind(this)
+    this.onChangeGroupId = this.onChangeGroupId.bind(this)
+    this.onAddTopic = this.onAddTopic.bind(this)
+    this.onDeleteTopic = this.onDeleteTopic.bind(this)
+    this.onChangeTopic = this.onChangeTopic.bind(this)
+    this.genAddButton = this.genAddButton.bind(this)
+    this.genTopic = this.genTopic.bind(this)
+  }
+
+  componentDidUpdate(preProps) {
+    if (preProps.nodeId !== this.props.nodeId) {
+      this.setState({
+        groupId: this.getGroupId(),
+        releaseGroupId: this.getReleaseGroupId(),
+        topics: this.getTopics()
+      })
+    }
+  }
+
+  getGroupId() {
+    // 通过config获取
+    let groupId = ''
+    let node = getEventNodeById(this.props.nodeId)
+    if (node) {
+      groupId = node.props.groupId || groupId
+    }
+    return groupId
+  }
+
+  getReleaseGroupId() {
+    // 通过config获取
+    let groupId = ''
+    let node = getEventNodeById(this.props.nodeId)
+    if (node) {
+      groupId = node.props.releaseGroupId || groupId
+    }
+    return groupId
+  }
+
+  getTopics() {
+    let topics = []
+    let node = getEventNodeById(this.props.nodeId)
+    if (node) {
+      topics = node.props.topics || topics
+    }
+    return topics
+  }
+
+  getTitleName() {
+    let title = ''
+    let node = getEventNodeById(this.props.nodeId)
+    if (node && widgetStore.widget[node.type]) {
+      let map = widgetStore.widget[node.type].map
+      title = map.locale ? map.locale[i18n.language] : map.name
+    }
+    return title ? title + i18n.t('EventView.jenga_extra.defination') : title
+  }
+
+  onChangeGroupId(applyChange, release, e) {
+    let value = e && e.target ? e.target.value : e
+    let obj = {}
+    if (release) {
+      obj.releaseGroupId = value
+    } else {
+      obj.groupId = value
+    }
+    this.setState(obj, () => {
+      if (applyChange) {
+        treeActions.changeNodeProps({
+          nodeId: this.props.nodeId,
+          props: obj
+        })
+      }
+    })
+  }
+
+  onAddTopic() {
+    let obj = {}
+    let topics = this.state.topics
+    topics.push({
+      name: 'topic' + (this.state.topics.length + 1),
+      def: this.prefix + ''
+    })
+    obj.topics = topics
+    this.setState(obj, () => {
+      treeActions.changeNodeProps({
+        nodeId: this.props.nodeId,
+        props: obj
+      })
+    })
+  }
+
+  onDeleteTopic(index) {
+    let obj = {}
+    let topics = this.state.topics
+    topics.splice(index, 1)
+    obj.topics = topics
+    this.setState(obj, () => {
+      treeActions.changeNodeProps({
+        nodeId: this.props.nodeId,
+        props: obj
+      })
+    })
+  }
+
+  onChangeTopic(index, param, applyChange, e) {
+    let value = e && e.target ? e.target.value : e
+    let obj = {}
+    let topics = this.state.topics
+    topics[index][param] = value
+    obj.topics = topics
+    let nodeId = this.props.nodeId
+    this.setState(obj, () => {
+      if (applyChange) {
+        treeActions.changeNodeProps({
+          nodeId: nodeId,
+          props: obj
+        })
+      }
+    })
+  }
+
+  genAddButton() {
+    return (
+      <button className="btn-clear btn-add-param" onClick={this.onAddTopic}>
+        <div className="icon">
+          <span className="horizon" />
+          <span className="vertical" />
+        </div>
+      </button>
+    )
+  }
+
+  genTopic(topic, index) {
+    let length = this.state.topics.length
+    let extraBtn = length - 1 === index ? this.genAddButton() : null
+    return (
+      <div
+        className={cls('param-item f--hlc', {
+          'param-item-left': index % 2 !== 0,
+          'param-item-bottom': length > 2 && index < length - (length % 2 || 2)
+        })}
+        key={index}
+      >
+        <div className="item-input-wrap f--hlc flex-1">
+          <div className="item-inner-wrap name-wrap f--hlc">
+            <div className="item-name item-inner-name">
+              {i18n.t('EventView.jenga_extra.topicName')}
+            </div>
+            <input
+              value={topic.name}
+              onChange={this.onChangeTopic.bind(this, index, 'name', false)}
+              onBlur={this.onChangeTopic.bind(this, index, 'name', true)}
+            />
+          </div>
+          <div className="item-inner-wrap def-wrap f--hlc">
+            <div className="item-name item-inner-name">
+              {i18n.t('EventView.jenga_extra.topicDef')}
+            </div>
+            {/*<div className="def-prefix">{this.prefix}</div>*/}
+            <input
+              title={topic.def}
+              value={topic.def}
+              className="value"
+              onChange={this.onChangeTopic.bind(this, index, 'def', false)}
+              onBlur={this.onChangeTopic.bind(this, index, 'def', true)}
+            />
+          </div>
+          <button
+            className="btn-clear btn-delete-param"
+            onClick={this.onDeleteTopic.bind(this, index)}
+          />
+        </div>
+        {extraBtn}
+      </div>
+    )
+  }
+
+  render() {
+    return (
+      <div className="jenga-iot-message-config">
+        <div className="config-wrap f--hlc">
+          <div className="title">{i18n.t('EventView.jenga_extra.groupId')}</div>
+          <input
+            className="groupId-input"
+            value={this.state.groupId}
+            onChange={this.onChangeGroupId.bind(this, false, false)}
+            onBlur={this.onChangeGroupId.bind(this, true, false)}
+          />
+          <div className="title next">
+            {i18n.t('EventView.jenga_extra.releaseGroupId')}
+          </div>
+          <input
+            className="groupId-input"
+            value={this.state.releaseGroupId}
+            onChange={this.onChangeGroupId.bind(this, false, true)}
+            onBlur={this.onChangeGroupId.bind(this, true, true)}
+          />
+        </div>
+        <div
+          className={cls(
+            'params',
+            this.state.topics.length > 0 ? 'f--h' : 'f--hlc'
+          )}
+        >
+          <div
+            className={cls(
+              'params-title',
+              this.state.topics.length > 2 ? 'params-title-top f--h' : 'f--hlc'
+            )}
+          >
+            {i18n.t('EventView.jenga_extra.topics')}
+          </div>
+          <div className="main-wrap f--hlc">
+            {this.state.topics.length === 0
+              ? this.genAddButton('in')
+              : this.state.topics.map((topic, index) => {
+                  return this.genTopic(topic, index)
+                })}
+          </div>
+        </div>
+        <div className="params-title params-def-title">
+          {this.getTitleName()}
+        </div>
+      </div>
+    )
+  }
+}

+ 96 - 0
coms/jenga/extra/JengaExtra.jsx

@@ -0,0 +1,96 @@
+import React from 'react'
+import newEventStores from 'stores/newEvent'
+import {
+  nodeIsDyTransaction,
+  nodeIsFunctionGroup,
+  nodeIsIotMessage,
+  nodeIsNormalService,
+  nodeIsTimerService,
+  nodeIsTransaction
+} from 'stores/funcs/nodeType'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+import NodeParams from './NodeParams'
+import NodeServiceParams from './NodeServiceParams'
+import TimerServiceParams from './TimerServiceParams'
+import NodePublicService from './NodePublicService'
+import NodeFuncGroup from './NodeFuncGroup'
+import IotMessageConfig from './IotMessageConfig'
+
+export default class JengaExtra extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      curEventNodeId: newEventStores.curEventNodeId,
+      type: this.getType(newEventStores.curEventNodeId)
+    }
+    this.onEventChange = this.onEventChange.bind(this)
+    this.getType = this.getType.bind(this)
+  }
+
+  componentDidMount() {
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (status.types.includes('curEventNodeId')) {
+        if (this.state.curEventNodeId !== status.data.curEventNodeId) {
+          obj.curEventNodeId = status.data.curEventNodeId
+          obj.type = this.getType(obj.curEventNodeId)
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  getType(nodeId) {
+    let type = null
+    let node = getEventNodeById(nodeId)
+    if (node) {
+      if (nodeIsTimerService(node.type)) {
+        type = 'timer'
+      } else if (nodeIsTransaction(node.type)) {
+        type = 'transaction'
+      } else if (nodeIsDyTransaction(node.type)) {
+        type = 'dyTransaction'
+      } else if (nodeIsIotMessage(node.type)) {
+        type = 'iotMessage'
+      } else if (nodeIsNormalService(node.type)) {
+        type = 'service'
+      } else if (nodeIsFunctionGroup(node.type)) {
+        type = 'funcGroup'
+      }
+    }
+    return type
+  }
+
+  render() {
+    return (
+      <React.Fragment>
+        {this.state.type === 'service' ? (
+          <NodePublicService nodeId={this.state.curEventNodeId} />
+        ) : this.state.type === 'funcGroup' ? (
+          <NodeFuncGroup nodeId={this.state.curEventNodeId} />
+        ) : null}
+        {['funcGroup', 'transaction', 'dyTransaction'].indexOf(
+          this.state.type
+        ) >= 0 ? (
+          <NodeParams nodeId={this.state.curEventNodeId} />
+        ) : this.state.type === 'service' ? (
+          <NodeServiceParams nodeId={this.state.curEventNodeId} />
+        ) : this.state.type === 'timer' ? (
+          <TimerServiceParams nodeId={this.state.curEventNodeId} />
+        ) : this.state.type === 'iotMessage' ? (
+          <IotMessageConfig nodeId={this.state.curEventNodeId} />
+        ) : null}
+      </React.Fragment>
+    )
+  }
+}

+ 60 - 0
coms/jenga/extra/NodeFuncGroup.jsx

@@ -0,0 +1,60 @@
+import React from 'react'
+import i18n from 'i18next'
+import { Switch } from 'antd'
+import treeActions from 'actions/tree'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+
+export default class NodeFuncGroup extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      isSync: this.getIsSync()
+    }
+
+    this.getIsSync = this.getIsSync.bind(this)
+    this.onChangeIsSync = this.onChangeIsSync.bind(this)
+  }
+
+  componentDidUpdate(preProps) {
+    if (this.props.nodeId !== preProps.nodeId) {
+      this.setState({
+        isSync: this.getIsSync()
+      })
+    }
+  }
+
+  getIsSync() {
+    let isSync = false
+    let node = getEventNodeById(this.props.nodeId)
+    if (node) {
+      // 默认为false
+      isSync = node.props.sync || isSync
+    }
+    return isSync
+  }
+
+  onChangeIsSync(e) {
+    let props = {
+      sync: e
+    }
+    this.setState({ isSync: props.sync }, () => {
+      treeActions.changeNodeProps({
+        nodeId: this.props.nodeId,
+        props: props
+      })
+    })
+  }
+
+  render() {
+    return (
+      <div className="jenga-node-func-group f--hlc">
+        <div className="title">{i18n.t('EventView.jenga_extra.func-sync')}</div>
+        <Switch
+          checked={this.state.isSync}
+          onChange={this.onChangeIsSync}
+          className="sync-switch"
+        />
+      </div>
+    )
+  }
+}

+ 323 - 0
coms/jenga/extra/NodeParams.jsx

@@ -0,0 +1,323 @@
+import React from 'react'
+import i18n from 'i18next'
+import cls from 'classnames'
+import treeActions from 'actions/tree'
+import widgetStore from 'stores/widget'
+import newEventStores from 'stores/newEvent'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+import { nodeIsDyTransaction } from 'stores/funcs/nodeType'
+import { isValidParamName } from 'stores/funcs/event/eventUtils'
+
+export default class NodeParams extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      inParams: this.getInParams(),
+      outParams: this.getOutParams(),
+      title: this.getTitleName(),
+      paramWarning: undefined
+    }
+    this.reloadAction = this.reloadAction.bind(this)
+    this.onAddParam = this.onAddParam.bind(this)
+    this.getInParams = this.getInParams.bind(this)
+    this.getOutParams = this.getOutParams.bind(this)
+    this.hasNoOutParams = this.hasNoOutParams.bind(this)
+    this.getInParamsName = this.getInParamsName.bind(this)
+    this.getOutParamsName = this.getOutParamsName.bind(this)
+    this.getTitleName = this.getTitleName.bind(this)
+
+    this.genAddButton = this.genAddButton.bind(this)
+    this.genInput = this.genInput.bind(this)
+    this.genError = this.genError.bind(this)
+  }
+
+  componentDidUpdate(preProps) {
+    if (preProps.nodeId !== this.props.nodeId) {
+      this.setState({
+        inParams: this.getInParams(),
+        outParams: this.getOutParams(),
+        title: this.getTitleName(),
+        paramWarning: undefined
+      })
+    }
+  }
+
+  reloadAction() {
+    newEventStores.reloadAction(this.props.nodeId)
+  }
+
+  onAddParam(type) {
+    let obj = { paramWarning: undefined }
+    let params = undefined
+    switch (type) {
+      case 'in':
+        params = this.state.inParams
+        params.push('param' + (this.state.inParams.length + 1))
+        obj.inParams = params
+        break
+      case 'out':
+        params = this.state.outParams
+        params.push('param' + (this.state.outParams.length + 1))
+        obj.outParams = params
+        break
+    }
+    this.setState(obj, () => {
+      delete obj.paramWarning
+      treeActions.changeNodeProps({
+        nodeId: this.props.nodeId,
+        props: obj
+      })
+      this.reloadAction()
+    })
+  }
+
+  onDeleteParam(index, type) {
+    let obj = { paramWarning: undefined }
+    let params = undefined
+    switch (type) {
+      case 'in':
+        params = this.state.inParams
+        params.splice(index, 1)
+        obj.inParams = params
+        break
+      case 'out':
+        params = this.state.outParams
+        params.splice(index, 1)
+        obj.outParams = params
+        break
+    }
+    this.setState(obj, () => {
+      delete obj.paramWarning
+      treeActions.changeNodeProps({
+        nodeId: this.props.nodeId,
+        props: obj
+      })
+      this.reloadAction()
+    })
+  }
+
+  onChangeParam(index, type, applyChange, e) {
+    let value = e && e.target ? e.target.value : e
+    let obj = {}
+    let params = undefined
+    // 处理有问题字段
+    if (applyChange && !isValidParamName(value)) {
+      let errorValue = value
+      if (value !== 'detail') {
+        // outParam为detail时,不对其强行修改,只给出提示.其余情况将值改为paramX
+        value = 'param' + (index + 1)
+      }
+      obj.paramWarning = {
+        error: 'paramError',
+        type: type,
+        index,
+        value: errorValue
+      }
+    } else {
+      obj.paramWarning = undefined
+    }
+    switch (type) {
+      case 'in':
+        params = this.state.inParams
+        params[index] = value
+        obj.inParams = params
+        break
+      case 'out':
+        params = this.state.outParams
+        params[index] = value
+        obj.outParams = params
+        break
+    }
+    let nodeId = this.props.nodeId
+    this.setState(obj, () => {
+      if (applyChange) {
+        delete obj.paramWarning
+        treeActions.changeNodeProps({
+          nodeId: nodeId,
+          props: obj
+        })
+        this.reloadAction()
+      }
+    })
+  }
+
+  getInParams() {
+    let inParams = []
+    let node = getEventNodeById(this.props.nodeId)
+    if (node) {
+      inParams = node.props.inParams || inParams
+    }
+    return inParams
+  }
+
+  getOutParams() {
+    let outParams = []
+    let node = getEventNodeById(this.props.nodeId)
+    if (node) {
+      outParams = node.props.outParams || outParams
+    }
+    return outParams
+  }
+
+  hasNoOutParams() {
+    let node = getEventNodeById(this.props.nodeId)
+    return nodeIsDyTransaction(node && node.type)
+  }
+
+  getInParamsName() {
+    let title = ''
+    let node = getEventNodeById(this.props.nodeId)
+    if (node && widgetStore.widget[node.type]) {
+      let prop = widgetStore.widget[node.type].map.propsMap['inParams']
+      title = prop.locale ? prop.locale[i18n.language] : prop.name
+    }
+    return title + ':'
+  }
+
+  getOutParamsName() {
+    let title = ''
+    let node = getEventNodeById(this.props.nodeId)
+    if (node && widgetStore.widget[node.type]) {
+      let prop = widgetStore.widget[node.type].map.propsMap['outParams']
+      title = prop.locale ? prop.locale[i18n.language] : prop.name
+    }
+    return title + ':'
+  }
+
+  getTitleName() {
+    let title = ''
+    let node = getEventNodeById(this.props.nodeId)
+    if (node && widgetStore.widget[node.type]) {
+      let map = widgetStore.widget[node.type].map
+      title = map.locale ? map.locale[i18n.language] : map.name
+    }
+    return title ? title + i18n.t('EventView.jenga_extra.defination') : title
+  }
+
+  genAddButton(type) {
+    return (
+      <button
+        className="btn-clear btn-add-param"
+        onClick={this.onAddParam.bind(this, type)}
+      >
+        <div className="icon">
+          <span className="horizon" />
+          <span className="vertical" />
+        </div>
+      </button>
+    )
+  }
+
+  genInput(value, index, type) {
+    let length =
+      type === 'in' ? this.state.inParams.length : this.state.outParams.length
+    let extraBtn = length - 1 === index ? this.genAddButton(type) : null
+    return (
+      <div
+        className={cls('param-item f--hlc', {
+          'param-item-left': index % 3 !== 0,
+          'param-item-bottom': length > 3 && index < length - (length % 3 || 3)
+        })}
+        key={index}
+      >
+        <div className="item-name">
+          {i18n.t('EventView.jenga_extra.param') + (index + 1)}
+        </div>
+        <div className="item-input-wrap f--hlc flex-1">
+          <input
+            value={value}
+            onChange={this.onChangeParam.bind(this, index, type, false)}
+            onBlur={this.onChangeParam.bind(this, index, type, true)}
+          />
+          <button
+            className="btn-clear btn-delete-param"
+            onClick={this.onDeleteParam.bind(this, index, type)}
+          />
+        </div>
+        {extraBtn}
+      </div>
+    )
+  }
+
+  genError(type) {
+    return this.state.paramWarning && this.state.paramWarning.type === type ? (
+      <div className="param-warning f--hlc">
+        {'* ' +
+          i18n.t('EventView.jenga_extra.param') +
+          (this.state.paramWarning.index + 1) +
+          i18n.t('EventView.jenga_extra.setAs') +
+          '"' +
+          this.state.paramWarning.value +
+          '"' +
+          ',' +
+          i18n.t('EventView.jenga_extra.' + this.state.paramWarning.error)}
+      </div>
+    ) : null
+  }
+
+  render() {
+    return (
+      <div className="jenga-node-params">
+        <div
+          className={cls(
+            'params params-in',
+            this.state.inParams.length > 0 ? 'f--h' : 'f--hlc'
+          )}
+        >
+          <div
+            className={cls(
+              'params-title params-in-title',
+              this.state.inParams.length > 3
+                ? 'params-title-top f--h'
+                : 'f--hlc'
+            )}
+          >
+            {this.getInParamsName()}
+          </div>
+          <div>
+            <div className="main-wrap f--hlc">
+              {this.state.inParams.length === 0
+                ? this.genAddButton('in')
+                : this.state.inParams.map((param, index) => {
+                    return this.genInput(param, index, 'in')
+                  })}
+            </div>
+            {this.genError('in')}
+          </div>
+        </div>
+        {this.hasNoOutParams() ? null : (
+          <div
+            className={cls(
+              'params params-out',
+              this.state.outParams.length > 0 ? 'f--h' : 'f--hlc'
+            )}
+          >
+            <div
+              className={cls(
+                'params-title params-out-title',
+                this.state.outParams.length > 3
+                  ? 'params-title-top f--h'
+                  : 'f--hlc'
+              )}
+            >
+              {this.getOutParamsName()}
+            </div>
+            <div>
+              <div className="main-wrap f--hlc">
+                {this.state.outParams.length === 0
+                  ? this.genAddButton('out')
+                  : this.state.outParams.map((param, index) => {
+                      return this.genInput(param, index, 'out')
+                    })}
+              </div>
+              {this.genError('out')}
+            </div>
+          </div>
+        )}
+        <div className="params-title params-def-title">
+          {this.getTitleName()}
+        </div>
+      </div>
+    )
+  }
+}

+ 407 - 0
coms/jenga/extra/NodePublicService.jsx

@@ -0,0 +1,407 @@
+import React from 'react'
+import { Switch, Select } from 'antd'
+import cls from 'classnames'
+import i18n from 'i18next'
+import treeActions from 'actions/tree'
+import cooperateActions from 'actions/cooperate'
+import treeStores from 'stores/tree'
+import globalConfigStore from 'stores/globalConfig'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+import { isSa, isHySa } from 'utils/commons/ctrlHide'
+import externalPath from 'api/externalPath'
+import iconStrObj from 'src/assets/icons/iconsStr'
+
+export default class NodePublicService extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      publicService: this.getPublicService(),
+      servicePath: this.getServicePath(),
+      responseType: this.getResponseType(),
+      receiveType: this.getReceiveType(),
+      workSettings: globalConfigStore.workSettings,
+      isCusDomain: this.dealCusDomain(globalConfigStore.workSettings)
+    }
+    this.responseTypeOptions = ['json', 'xml']
+    // this.receiveTypeOptions = ['json', 'string', 'array', 'base64']
+    this.receiveTypeOptions = ['json', 'string', 'array']
+    this.onConfigChange = this.onConfigChange.bind(this)
+
+    this.getServicePath = this.getServicePath.bind(this)
+    this.getPublicService = this.getPublicService.bind(this)
+    this.getResponseType = this.getResponseType.bind(this)
+    this.getReceiveType = this.getReceiveType.bind(this)
+    this.onChangeProps = this.onChangeProps.bind(this)
+    this.onChangingPath = this.onChangingPath.bind(this)
+    this.onChangedPath = this.onChangedPath.bind(this)
+    this.onCopy = this.onCopy.bind(this)
+    this.onCopyPublishPath = this.onCopyPublishPath.bind(this)
+    this.onCopyPreviewPath = this.onCopyPreviewPath.bind(this)
+    this.onChangeResponseType = this.onChangeResponseType.bind(this)
+    this.onChangeReceiveType = this.onChangeReceiveType.bind(this)
+    this.pathPrefix = this.pathPrefix.bind(this)
+    this.publishPath = this.publishPath.bind(this)
+    this.previewPath = this.previewPath.bind(this)
+    this.targetPath = this.targetPath.bind(this)
+    this.sidPath = this.sidPath.bind(this)
+    this.isGroupWork = this.isGroupWork.bind(this)
+  }
+
+  componentDidMount() {
+    this.unsubscribe = globalConfigStore.listen(this.onConfigChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  componentDidUpdate(preProps) {
+    if (this.props.nodeId !== preProps.nodeId) {
+      this.setState({
+        servicePath: this.getServicePath(),
+        publicService: this.getPublicService(),
+        responseType: this.getResponseType(),
+        receiveType: this.getReceiveType(),
+        isCusDomain: this.dealCusDomain(this.state.workSettings)
+      })
+    }
+  }
+
+  dealCusDomain(workSettings) {
+    return [false, undefined].indexOf(treeStores.curCaseInfo.isPublished) >= 0
+      ? false // 首次发布配置默认使用平台公共域名
+      : workSettings
+      ? workSettings.customDomain
+      : false
+  }
+
+  onConfigChange(status) {
+    let obj = {}
+    if (status.types) {
+      if (status.types.includes('workSettings')) {
+        obj.workSettings = status.data.workSettings
+        obj.isCusDomain = this.dealCusDomain(obj.workSettings)
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  getServicePath() {
+    let node = getEventNodeById(this.props.nodeId)
+    return node && node.props ? node.props.servicePath || '' : ''
+  }
+
+  getPublicService() {
+    let node = getEventNodeById(this.props.nodeId)
+    return node && node.props && node.props.publicService !== undefined
+      ? node.props.publicService
+      : false
+  }
+
+  getResponseType() {
+    let node = getEventNodeById(this.props.nodeId)
+    return node && node.props && node.props.responseType !== undefined
+      ? node.props.responseType
+      : 'json'
+  }
+
+  getReceiveType() {
+    let node = getEventNodeById(this.props.nodeId)
+    return node && node.props && node.props.receiveType !== undefined
+      ? node.props.receiveType
+      : 'json'
+  }
+
+  onChangeProps(type, value) {
+    let props = {}
+    switch (type) {
+      case 'publicService':
+        props.publicService = value
+        if (props.publicService && !this.state.servicePath) {
+          props.servicePath = this.sidPath()
+        }
+        break
+      case 'servicePath':
+        props.servicePath = value
+        if (!value && this.state.publicService) {
+          props.servicePath = this.sidPath()
+        }
+        break
+      case 'responseType':
+        props.responseType = value
+        break
+      case 'receiveType':
+        props.receiveType = value
+        break
+    }
+    if (Object.keys(props).length > 0) {
+      this.setState(props)
+      treeActions.changeNodeProps({
+        nodeId: this.props.nodeId,
+        props: props
+      })
+    }
+  }
+
+  onChangingPath(e) {
+    let value = e && e.target ? e.target.value : e
+    this.setState({
+      servicePath: value
+    })
+  }
+
+  onChangedPath() {
+    this.onChangeProps('servicePath', this.state.servicePath)
+  }
+
+  onCopy(value) {
+    let tInput = document.createElement('textarea')
+    tInput.style.width = '0px'
+    tInput.style.height = '0px'
+    tInput.value = value
+    document.body.appendChild(tInput)
+    tInput.select()
+    document.execCommand('copy')
+    document.body.removeChild(tInput)
+  }
+
+  onCopyPublishPath() {
+    this.onCopy(this.publishPath())
+  }
+
+  onCopyPreviewPath() {
+    this.onCopy(this.previewPath())
+  }
+
+  onChangeResponseType(e) {
+    this.setState(
+      {
+        responseType: e
+      },
+      () => {
+        this.onChangeProps('responseType', this.state.responseType)
+      }
+    )
+  }
+
+  onChangeReceiveType(e) {
+    this.setState(
+      {
+        receiveType: e
+      },
+      () => {
+        this.onChangeProps('receiveType', this.state.receiveType)
+      }
+    )
+  }
+
+  pathPrefix(isPreview) {
+    let prefix =
+      (isPreview ? 'https://v4pre.h5sys.' : 'https://v4rel.h5sys.') +
+      (i18n.language === 'zh' ? 'cn' : 'com')
+    let { isCusDomain, workSettings } = this.state
+    if (isCusDomain) {
+      if (isPreview && workSettings && workSettings.previewDomain) {
+        prefix = workSettings.previewDomain
+      } else if (!isPreview && workSettings && workSettings.domain) {
+        prefix = workSettings.domain
+      }
+    }
+    if (isSa() && isPreview) {
+      let protocol = location.protocol + '//'
+      if (workSettings && workSettings.previewDomain) {
+        prefix = protocol + workSettings.previewDomain
+      } else {
+        prefix = protocol + document.domain
+      }
+    }
+    return prefix + '/api/' + treeStores.curCaseId + '/'
+  }
+
+  publishPath() {
+    return this.pathPrefix() + this.targetPath()
+  }
+
+  previewPath() {
+    return this.pathPrefix(true) + this.targetPath()
+  }
+
+  targetPath() {
+    // let node = getEventNodeById(this.props.nodeId)
+    return this.state.servicePath // || (node && node.id) || ''
+  }
+
+  sidPath() {
+    let node = getEventNodeById(this.props.nodeId)
+    return (node && node.id) || ''
+  }
+
+  isGroupWork() {
+    let showGroupTips = false
+    cooperateActions.getIsGroupWork(isGroupWork => {
+      if (isGroupWork) {
+        showGroupTips = true
+      }
+    })
+    return showGroupTips
+  }
+
+  render() {
+    // 小模块不需要
+    if (treeStores.curClassId) {
+      return null
+    }
+    // 多人开发添加client
+
+    return (
+      <div className={cls('jenga-node-public-service')}>
+        <div className="row-wrap f--hlc">
+          <div className="title">
+            {i18n.t('EventView.jenga_extra.public_service')}
+          </div>
+          <Switch
+            checked={this.state.publicService}
+            onChange={this.onChangeProps.bind(this, 'publicService')}
+            className="public-service-switch"
+          />
+          {!isHySa() && (
+            <a
+              className="help-link"
+              href={externalPath.publicService}
+              target="_blank"
+            >
+              <i
+                className="ih5-icon-help"
+                dangerouslySetInnerHTML={{ __html: iconStrObj.help }}
+              />
+            </a>
+          )}
+          {this.state.publicService ? (
+            <>
+              <div className="path-info">
+                <div className="f--hlc">
+                  {i18n.t('EventView.jenga_extra.service_path')}
+                  <input
+                    className="path-input"
+                    onChange={this.onChangingPath}
+                    onBlur={this.onChangedPath}
+                    value={this.state.servicePath}
+                    placeholder={i18n.t(
+                      'EventView.jenga_extra.input_path_placeholder'
+                    )}
+                  />
+                </div>
+              </div>
+              <div className="response-type">
+                {i18n.t('EventView.jenga_extra.receive_type')}
+                <Select
+                  onChange={this.onChangeReceiveType}
+                  getPopupContainer={triggerNode => triggerNode.parentNode}
+                  value={this.state.receiveType}
+                  size="small"
+                  className="response-type-select"
+                  dropdownClassName="response-type-select-dropdown"
+                >
+                  {this.receiveTypeOptions.map((op, optIndex) => (
+                    <Select.Option value={op} key={optIndex}>
+                      {i18n.t(
+                        'EventView.jenga_extra.receive_type_option.' + op
+                      )}
+                    </Select.Option>
+                  ))}
+                </Select>
+              </div>
+              <div className="response-type">
+                {i18n.t('EventView.jenga_extra.response_type')}
+                <Select
+                  onChange={this.onChangeResponseType}
+                  getPopupContainer={triggerNode => triggerNode.parentNode}
+                  value={this.state.responseType}
+                  size="small"
+                  className="response-type-select"
+                  dropdownClassName="response-type-select-dropdown"
+                >
+                  {this.responseTypeOptions.map((op, optIndex) => (
+                    <Select.Option value={op} key={optIndex}>
+                      {i18n.t(
+                        'EventView.jenga_extra.response_type_option.' + op
+                      )}
+                    </Select.Option>
+                  ))}
+                </Select>
+              </div>
+            </>
+          ) : null}
+        </div>
+        {this.state.publicService ? (
+          <>
+            <div className="row-wrap path-wrap f--hlc">
+              <div className="path-info">
+                <div className="f--hlc preview-info">
+                  {i18n.t('EventView.jenga_extra.preview_path')}
+                  {this.pathPrefix(true)}
+                  <div className="path-value" title={this.targetPath()}>
+                    {this.targetPath()}
+                  </div>
+                  <button
+                    className="btn-clear btn-copy-path"
+                    onClick={this.onCopyPreviewPath}
+                  >
+                    {i18n.t('EventView.jenga_extra.copy_path')}
+                  </button>
+                </div>
+                {this.isGroupWork() && (
+                  <div className="notice-wrap f--hlc">
+                    <div
+                      className="icon"
+                      dangerouslySetInnerHTML={{
+                        __html: iconStrObj.noticeIcon
+                      }}
+                    />
+                    <div className="notice-info">
+                      {i18n.t(
+                        'EventView.jenga_extra.preview_group_work_notice'
+                      )}
+                    </div>
+                  </div>
+                )}
+              </div>
+            </div>
+            {!isSa() && (
+              <div className="row-wrap path-wrap f--hlc">
+                <div className="path-info">
+                  <div className="f--hlc publish_path">
+                    {i18n.t('EventView.jenga_extra.publish_path')}
+                    {this.pathPrefix()}
+                    <div className="path-value" title={this.targetPath()}>
+                      {this.targetPath()}
+                    </div>
+                    <button
+                      className="btn-clear btn-copy-path"
+                      onClick={this.onCopyPublishPath}
+                    >
+                      {i18n.t('EventView.jenga_extra.copy_path')}
+                    </button>
+                  </div>
+                  <div className="notice-wrap f--hlc">
+                    <div
+                      className="icon"
+                      dangerouslySetInnerHTML={{
+                        __html: iconStrObj.noticeIcon
+                      }}
+                    />
+                    <div className="notice-info">
+                      {i18n.t('EventView.jenga_extra.publish_notice')}
+                    </div>
+                  </div>
+                </div>
+              </div>
+            )}
+          </>
+        ) : null}
+      </div>
+    )
+  }
+}

+ 848 - 0
coms/jenga/extra/NodeServiceParams.jsx

@@ -0,0 +1,848 @@
+import React from 'react'
+import { Select, Switch, message } from 'antd'
+import i18n from 'i18next'
+import cls from 'classnames'
+import treeActions from 'actions/tree'
+import uiActions from 'actions/ui'
+import serviceActions from 'actions/service'
+import widgetStore from 'stores/widget'
+import treeStore from 'stores/tree'
+import newEventStores from 'stores/newEvent'
+import serviceStore from 'stores/service'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+import { isValidParamName } from 'stores/funcs/event/eventUtils'
+import { nodeIsNormalService } from 'stores/funcs/nodeType'
+import { saveCaseBeforeActions } from 'stores/funcs/service'
+import propType from 'const/propType'
+
+const { Option } = Select
+
+export default class NodeServiceParams extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      inParams: this.getInParams(),
+      outParams: this.getOutParams(),
+      title: this.getTitleName(),
+      paramWarning: undefined,
+      allowConfigShare: false, // true, false, reload, loading
+      isShared: false,
+      sharedTitle: '', // 服务的title
+      sharedDesc: '' // 服务的desc
+    }
+    this.sharedServiceInfo = undefined // 读取的共享服务信息需要使用的部分
+    this.types = [
+      {
+        name: i18n.t('FunctionView.Any'),
+        value: 'undefined'
+      },
+      {
+        name: i18n.t('FunctionView.Number'),
+        value: propType.Number
+      },
+      {
+        name: i18n.t('FunctionView.String'),
+        value: propType.String
+      },
+      {
+        name: i18n.t('FunctionView.Boolean'),
+        value: propType.Boolean
+      },
+      {
+        name: i18n.t('FunctionView.Object'),
+        value: 'Object'
+      }
+    ]
+    this.loadSharedService = this.loadSharedService.bind(this)
+    this.configResult = this.configResult.bind(this)
+    this.reloadAction = this.reloadAction.bind(this)
+    this.onAddParam = this.onAddParam.bind(this)
+    this.saveCase = this.saveCase.bind(this)
+    this.onChangeShared = this.onChangeShared.bind(this)
+    this.onChangeSharedInfo = this.onChangeSharedInfo.bind(this)
+    this.getSharedData = this.getSharedData.bind(this)
+    this.addShareData = this.addShareData.bind(this)
+    this.modifySharedData = this.modifySharedData.bind(this)
+    this.deleteShareData = this.deleteShareData.bind(this)
+
+    this.getInParams = this.getInParams.bind(this)
+    this.getOutParams = this.getOutParams.bind(this)
+    this.getInParamsName = this.getInParamsName.bind(this)
+    this.getOutParamsName = this.getOutParamsName.bind(this)
+    this.getTitleName = this.getTitleName.bind(this)
+
+    this.genAddButton = this.genAddButton.bind(this)
+    this.getInParamsCom = this.getInParamsCom.bind(this)
+    this.genInput = this.genInput.bind(this)
+    this.genError = this.genError.bind(this)
+    this.getSharedConfig = this.getSharedConfig.bind(this)
+  }
+
+  componentDidMount() {
+    this.loadSharedService()
+  }
+
+  componentDidUpdate(preProps) {
+    if (preProps.nodeId !== this.props.nodeId) {
+      this.setState(
+        {
+          inParams: this.getInParams(),
+          outParams: this.getOutParams(),
+          title: this.getTitleName(),
+          paramWarning: undefined,
+          allowConfigShare: false
+        },
+        () => {
+          this.loadSharedService()
+        }
+      )
+    }
+  }
+
+  loadSharedService() {
+    // 小模块不需要
+    if (
+      treeStore.curClassId ||
+      !(treeStore.curCaseInfo.eid || treeStore.curCaseInfo.gid)
+    ) {
+      this.setState({
+        allowConfigShare: false
+      })
+      return
+    }
+    let nodeId = this.props.nodeId
+    let node = getEventNodeById(nodeId)
+    if (nodeIsNormalService(node && node.type)) {
+      this.setState({
+        allowConfigShare: 'loading'
+      })
+      serviceStore.loadSharedService({ nodeId }).then(result => {
+        // 保证修改的nodeId正确
+        let _nodeId = result && result.nodeId
+        if (_nodeId !== nodeId) {
+          return
+        }
+        let obj = {
+          allowConfigShare: true
+        }
+        this.sharedServiceInfo = undefined
+        if (result === false) {
+          obj.allowConfigShare = 'reload'
+        } else if (result && result.id) {
+          obj.isShared = true
+          obj = this.configResult(obj, result, nodeId)
+        } else {
+          obj.isShared = false
+          obj.inParams = []
+          obj.outParams = []
+          let node = getEventNodeById(nodeId)
+          if (node) {
+            obj.inParams = node.props.inParams || []
+            obj.outParams = node.props.outParams || []
+          }
+        }
+        this.setState(obj)
+      })
+    } else {
+      this.setState({
+        allowConfigShare: false
+      })
+    }
+  }
+
+  configResult(obj, result, nodeId) {
+    this.sharedServiceInfo = { id: result.id, uri: result.uri, gid: result.gid } // 记录当前的共享服务id
+    if (result.content) {
+      let params = undefined
+      try {
+        params = JSON.parse(result.content)
+      } catch (e) {}
+      if (params) {
+        obj.sharedTitle = result.filename
+        obj.sharedDesc = params.desc
+        let inParams = params.inParams || []
+        let outParams = params.outParams || []
+        let inParamsSame = false
+        let outParamsSame = false
+        try {
+          inParamsSame =
+            JSON.stringify(inParams) === JSON.stringify(this.state.inParams)
+        } catch (e) {}
+        try {
+          inParamsSame =
+            JSON.stringify(outParams) === JSON.stringify(this.state.outParams)
+        } catch (e) {}
+        if (!inParamsSame || !outParamsSame) {
+          obj.inParams = inParams
+          obj.outParams = outParams
+          treeActions.changeNodeProps({
+            nodeId: nodeId,
+            props: {
+              inParams: inParams,
+              outParams: outParams
+            },
+            addRecord: false
+          })
+          this.reloadAction()
+        }
+      }
+    }
+    return obj
+  }
+
+  reloadAction() {
+    newEventStores.reloadAction(this.props.nodeId)
+  }
+
+  onAddParam(type) {
+    let obj = { paramWarning: undefined }
+    let params = undefined
+    switch (type) {
+      case 'in':
+        params = this.state.inParams
+        params.push({
+          name: 'param' + (this.state.inParams.length + 1)
+        })
+        obj.inParams = params
+        break
+      case 'out':
+        params = this.state.outParams
+        params.push('param' + (this.state.outParams.length + 1))
+        obj.outParams = params
+        break
+    }
+    this.setState(obj, () => {
+      delete obj.paramWarning
+      this.modifySharedData(type, params)
+      treeActions.changeNodeProps({
+        nodeId: this.props.nodeId,
+        props: obj
+      })
+      this.reloadAction()
+    })
+  }
+
+  onDeleteParam(index, type) {
+    let obj = { paramWarning: undefined }
+    let params = undefined
+    switch (type) {
+      case 'in':
+        params = this.state.inParams
+        params.splice(index, 1)
+        obj.inParams = params
+        break
+      case 'out':
+        params = this.state.outParams
+        params.splice(index, 1)
+        obj.outParams = params
+        break
+    }
+    this.setState(obj, () => {
+      delete obj.paramWarning
+      this.modifySharedData(type, params)
+      treeActions.changeNodeProps({
+        nodeId: this.props.nodeId,
+        props: obj
+      })
+      this.reloadAction()
+    })
+  }
+
+  onChangeType(index, type, paramType, applyChange, e) {
+    let value = e === 'undefined' ? undefined : e
+    this.onChangeParam(index, type, paramType, applyChange, value)
+  }
+
+  onChangeParam(index, type, paramType, applyChange, e) {
+    let value = e && e.target ? e.target.value : e
+    let obj = {}
+    let params = undefined
+    // 处理有问题字段
+    if (type === 'out' || (type === 'in' && paramType === 'name')) {
+      if (applyChange && !isValidParamName(value)) {
+        let errorValue = value
+        if (value !== 'detail') {
+          // outParam为detail时,不对其强行修改,只给出提示.其余情况将值改为paramX
+          value = 'param' + (index + 1)
+        }
+        obj.paramWarning = {
+          error: 'paramError',
+          type: type,
+          index,
+          value: errorValue
+        }
+      } else {
+        obj.paramWarning = undefined
+      }
+    }
+    switch (type) {
+      case 'in':
+        params = this.state.inParams
+        params[index][paramType] = value
+        if (paramType === 'required' && !value) {
+          // 为false不需要保存
+          delete params[index][paramType]
+        }
+        obj.inParams = params
+        break
+      case 'out':
+        params = this.state.outParams
+        params[index] = value
+        obj.outParams = params
+        break
+    }
+    let nodeId = this.props.nodeId
+    this.setState(obj, () => {
+      if (applyChange) {
+        delete obj.paramWarning
+        this.modifySharedData(type, params)
+        treeActions.changeNodeProps({
+          nodeId: nodeId,
+          props: obj
+        })
+        if (type === 'out' || (type === 'in' && paramType === 'name')) {
+          this.reloadAction()
+        }
+      }
+    })
+  }
+
+  saveCase(done) {
+    let nodeId = this.props.nodeId
+    let node = getEventNodeById(nodeId)
+    if (node && nodeIsNormalService(node.type) && node.uis && node.uis.new) {
+      saveCaseBeforeActions({
+        success: () => {
+          done && done()
+        },
+        fail: () => {
+          uiActions.toggleLoadingBar({ show: false })
+        }
+      })
+    } else {
+      done && done()
+    }
+  }
+
+  onChangeShared(value) {
+    if (value) {
+      // 添加共享
+      uiActions.toggleLoadingBar({
+        show: true,
+        title: i18n.t('LoadingBar.addSharedServiceTitle'),
+        content: i18n.t('LoadingBar.addSharedServiceContent'),
+        progress: 100,
+        type: 'dbLoading'
+      })
+      this.saveCase(() => {
+        let nodeId = this.props.nodeId
+        let node = getEventNodeById(nodeId)
+        this.addShareData({
+          name: (node && node.uis && node.uis.name) || undefined
+        }).then(result => {
+          uiActions.toggleLoadingBar({ show: false })
+          // 添加后操作
+          if (result && result.isSuccess) {
+            this.sharedServiceInfo = {
+              id: result.id,
+              uri: result.uri,
+              gid: result.gid
+            }
+            this.setState({
+              isShared: value,
+              sharedTitle: result.title,
+              sharedDesc: result.desc
+            })
+            if (result.gid) {
+              serviceActions.clearGroupCache({ gid: result.gid })
+            }
+          } else if (result.err) {
+            message.error(result.err)
+          }
+        })
+      })
+    } else {
+      // 删除共享
+      uiActions.toggleLoadingBar({
+        show: true,
+        title: i18n.t('LoadingBar.delSharedServiceTitle'),
+        content: i18n.t('LoadingBar.delSharedServiceContent'),
+        progress: 100,
+        type: 'dbLoading'
+      })
+      this.deleteShareData().then(result => {
+        uiActions.toggleLoadingBar({ show: false })
+        // 删除后操作
+        if (result) {
+          this.sharedServiceInfo = undefined
+          this.setState({
+            isShared: value,
+            sharedTitle: '',
+            sharedDesc: ''
+          })
+        }
+      })
+    }
+  }
+
+  onChangeSharedInfo(type, applyChange, e) {
+    let value = e && e.target ? e.target.value : e
+    let obj = undefined
+    if (type === 'title') {
+      obj = {
+        sharedTitle: value
+      }
+    } else if (type === 'desc') {
+      obj = {
+        sharedDesc: value
+      }
+    }
+    if (obj) {
+      this.setState(obj, () => {
+        if (applyChange) {
+          this.modifySharedData(type, value)
+        }
+      })
+    }
+  }
+
+  getSharedData(final) {
+    let result = {
+      fileName: this.state.sharedTitle,
+      content: {
+        desc: this.state.sharedDesc,
+        inParams: this.state.inParams,
+        outParams: this.state.outParams
+      }
+    }
+    if (final) {
+      result.content = JSON.stringify(result.content)
+    }
+    return result
+  }
+
+  addShareData({ name }) {
+    return new Promise(resolve => {
+      let { nid, gid } = treeStore.curCaseInfo
+      let uri = this.props.nodeId + '-' + nid
+      let data = this.getSharedData()
+      data.fileName = name || i18n.t('EventView.jenga_extra.defaultServiceName')
+      data.content.desc = i18n.t('EventView.jenga_extra.defaultServiceDesc')
+      data.scope = gid ? 1 : 2
+      data.uri = uri
+      if (gid) {
+        data.gid = gid
+      }
+      // json化
+      data.content = JSON.stringify(data.content)
+      if (this.sharedServiceInfo) {
+        resolve({ isSuccess: false })
+        return
+      }
+      // 添加发送请求
+      serviceStore.addSharedService({ data }).then(result => {
+        if (result && result.isSuccess && result.data) {
+          let id = result.data.id
+          let title = result.data.filename
+          let desc = ''
+          try {
+            let params = JSON.parse(result.data.content)
+            desc = params.desc
+          } catch (e) {}
+          resolve({ isSuccess: true, id, uri, gid, title, desc })
+        } else {
+          resolve({ isSuccess: false, err: result.err })
+        }
+      })
+    })
+  }
+
+  modifySharedData(type, value) {
+    if (!this.sharedServiceInfo) {
+      return
+    }
+    // content形如:{desc: xxx, inParams: xxx, outParams: xxx}
+    let data = this.getSharedData()
+    switch (type) {
+      case 'title':
+        data.fileName = value
+        break
+      case 'desc':
+        data.content.desc = value
+        break
+      case 'in':
+        data.content.inParams = value
+        break
+      case 'out':
+        data.content.outParams = value
+        break
+    }
+    // json化
+    data.content = JSON.stringify(data.content)
+    let { uri, gid, id } = this.sharedServiceInfo
+    data.uri = uri
+    if (gid) {
+      data.gid = gid
+    }
+    // 修改发送请求
+    serviceStore.modifySharedService({ id, data })
+  }
+
+  deleteShareData() {
+    return new Promise(resolve => {
+      if (!this.sharedServiceInfo) {
+        resolve(false)
+        return
+      }
+      // 发送删除请求
+      let { uri, gid, id } = this.sharedServiceInfo
+      let data = {
+        uri: uri
+      }
+      if (gid) {
+        data.gid = gid
+      }
+      serviceStore.delSharedService({ id, data }).then(result => {
+        resolve(result)
+      })
+    })
+  }
+
+  getInParams() {
+    let inParams = []
+    let node = getEventNodeById(this.props.nodeId)
+    if (node) {
+      inParams = node.props.inParams || inParams
+    }
+    return inParams
+  }
+
+  getOutParams() {
+    let outParams = []
+    let node = getEventNodeById(this.props.nodeId)
+    if (node) {
+      outParams = node.props.outParams || outParams
+    }
+    return outParams
+  }
+
+  getInParamsName() {
+    let title = ''
+    let node = getEventNodeById(this.props.nodeId)
+    if (node && widgetStore.widget[node.type]) {
+      let prop = widgetStore.widget[node.type].map.propsMap['inParams']
+      title = prop.locale ? prop.locale[i18n.language] : prop.name
+    }
+    return title + ':'
+  }
+
+  getOutParamsName() {
+    let title = ''
+    let node = getEventNodeById(this.props.nodeId)
+    if (node && widgetStore.widget[node.type]) {
+      let prop = widgetStore.widget[node.type].map.propsMap['outParams']
+      title = prop.locale ? prop.locale[i18n.language] : prop.name
+    }
+    return title + ':'
+  }
+
+  getTitleName(range) {
+    let title = ''
+    let node = getEventNodeById(this.props.nodeId)
+    if (node && widgetStore.widget[node.type]) {
+      let map = widgetStore.widget[node.type].map
+      title = map.locale ? map.locale[i18n.language] : map.name
+    }
+    let text = range ? 'range' : 'define'
+    return title ? title + i18n.t('EventView.jenga_extra.' + text) : title
+  }
+
+  genAddButton(type) {
+    return (
+      <button
+        className="btn-clear btn-add-param"
+        onClick={this.onAddParam.bind(this, type)}
+      >
+        <div className="icon">
+          <span className="horizon" />
+          <span className="vertical" />
+        </div>
+      </button>
+    )
+  }
+
+  getInParamsCom(param, index, type) {
+    let length = this.state.inParams.length
+    let extraBtn = length - 1 === index ? this.genAddButton(type) : null
+    return (
+      <div
+        className={cls('param-item f--hlc', {
+          'param-item-bottom': length > 0 && index !== length - 1
+        })}
+        key={index}
+      >
+        <div className="item-name">
+          {i18n.t('EventView.jenga_extra.param') + (index + 1)}
+        </div>
+        <div className="item-input-wrap f--hlc flex-1">
+          <div className="item-name first inner-name">
+            {i18n.t('EventView.jenga_extra.name')}
+          </div>
+          <input
+            className="service-input"
+            value={param.name}
+            onChange={this.onChangeParam.bind(this, index, type, 'name', false)}
+            onBlur={this.onChangeParam.bind(this, index, type, 'name', true)}
+          />
+          <div className="item-name inner-name">
+            {i18n.t('EventView.jenga_extra.type')}
+          </div>
+          <Select
+            onChange={this.onChangeType.bind(this, index, type, 'type', true)}
+            value={param.type}
+            placeholder={i18n.t('FunctionView.Any')}
+            className="prop-select prop-pair-select"
+            dropdownClassName="prop-select-dropdown"
+          >
+            {this.types.map((item, index) => {
+              return (
+                <Option key={index} value={item.value} title={item.name}>
+                  {item.name}
+                </Option>
+              )
+            })}
+          </Select>
+          <div className="item-name inner-name">
+            {i18n.t('EventView.jenga_extra.required')}
+          </div>
+          <Switch
+            className="service-switch"
+            checked={param.required || false}
+            onChange={this.onChangeParam.bind(
+              this,
+              index,
+              type,
+              'required',
+              true
+            )}
+          />
+          <div className="f--hlc default-wrap">
+            <div className="item-name inner-name">
+              {i18n.t('EventView.jenga_extra.default')}
+            </div>
+            <input
+              className="service-input"
+              value={param.default || ''}
+              placeholder={i18n.t('EventView.jenga_extra.debug-placeholder')}
+              onChange={this.onChangeParam.bind(
+                this,
+                index,
+                type,
+                'default',
+                false
+              )}
+              onBlur={this.onChangeParam.bind(
+                this,
+                index,
+                type,
+                'default',
+                true
+              )}
+            />
+          </div>
+          <button
+            className="btn-clear btn-delete-param"
+            onClick={this.onDeleteParam.bind(this, index, type)}
+          />
+        </div>
+        {extraBtn}
+      </div>
+    )
+  }
+
+  genInput(value, index, type) {
+    let length =
+      type === 'in' ? this.state.inParams.length : this.state.outParams.length
+    let extraBtn = length - 1 === index ? this.genAddButton(type) : null
+    return (
+      <div
+        className={cls('param-item f--hlc', {
+          'param-item-left': index % 3 !== 0,
+          'param-item-bottom': length > 3 && index < length - (length % 3 || 3)
+        })}
+        key={index}
+      >
+        <div className="item-name">
+          {i18n.t('EventView.jenga_extra.param') + (index + 1)}
+        </div>
+        <div className="item-input-wrap f--hlc flex-1">
+          <input
+            value={value}
+            onChange={this.onChangeParam.bind(
+              this,
+              index,
+              type,
+              undefined,
+              false
+            )}
+            onBlur={this.onChangeParam.bind(this, index, type, undefined, true)}
+          />
+          <button
+            className="btn-clear btn-delete-param"
+            onClick={this.onDeleteParam.bind(this, index, type)}
+          />
+        </div>
+        {extraBtn}
+      </div>
+    )
+  }
+
+  genError(type) {
+    return this.state.paramWarning && this.state.paramWarning.type === type ? (
+      <div className="param-warning f--hlc">
+        {'* ' +
+          i18n.t('EventView.jenga_extra.param') +
+          (this.state.paramWarning.index + 1) +
+          i18n.t('EventView.jenga_extra.setAs') +
+          '"' +
+          this.state.paramWarning.value +
+          '"' +
+          ',' +
+          i18n.t('EventView.jenga_extra.' + this.state.paramWarning.error)}
+      </div>
+    ) : null
+  }
+
+  getSharedConfig() {
+    let { allowConfigShare, isShared, sharedTitle, sharedDesc } = this.state
+    return allowConfigShare ? (
+      <div
+        className={cls('shared-service-config', isShared ? 'f--h' : 'f--hlc')}
+      >
+        <div className="title">
+          {treeStore.curCaseInfo.gid
+            ? i18n.t('EventView.jenga_extra.shared_group_service')
+            : treeStore.curCaseInfo.eid
+            ? i18n.t('EventView.jenga_extra.shared_enterprise_service')
+            : i18n.t('EventView.jenga_extra.shared_service')}
+          {isShared && (
+            <span className="shared-service-tips">
+              {i18n.t('EventView.jenga_extra.shared-service-tips')}
+            </span>
+          )}
+        </div>
+        {allowConfigShare === 'loading' ? (
+          <div>{i18n.t('EventView.jenga_extra.loading-data')}</div>
+        ) : allowConfigShare === 'reload' ? (
+          <button
+            className="btn-clear btn-reload"
+            onClick={this.loadSharedService}
+          >
+            {i18n.t('EventView.jenga_extra.reload-data')}
+          </button>
+        ) : (
+          <div className="f--h">
+            <Switch
+              checked={isShared}
+              onChange={this.onChangeShared}
+              className="shared-service-switch"
+            />
+            {isShared ? (
+              <>
+                <div className="shared-item f--h">
+                  <div className="item-title">
+                    {i18n.t('EventView.jenga_extra.service-title')}
+                  </div>
+                  <input
+                    value={sharedTitle}
+                    onChange={this.onChangeSharedInfo.bind(
+                      this,
+                      'title',
+                      false
+                    )}
+                    onBlur={this.onChangeSharedInfo.bind(this, 'title', true)}
+                  />
+                </div>
+                <div className="shared-item f--h">
+                  <div className="item-title">
+                    {i18n.t('EventView.jenga_extra.service-desc')}
+                  </div>
+                  <textarea
+                    value={sharedDesc}
+                    onChange={this.onChangeSharedInfo.bind(this, 'desc', false)}
+                    onBlur={this.onChangeSharedInfo.bind(this, 'desc', true)}
+                  />
+                </div>
+              </>
+            ) : null}
+          </div>
+        )}
+      </div>
+    ) : null
+  }
+
+  render() {
+    return (
+      <div className="jenga-node-params jenga-normal-service-params">
+        {this.getSharedConfig()}
+        <div
+          className={cls(
+            'params params-in no-border',
+            this.state.inParams.length > 0 ? 'f--h' : 'f--hlc'
+          )}
+        >
+          <div
+            className={cls(
+              'params-title params-in-title',
+              this.state.inParams.length > 1
+                ? 'params-title-top f--h'
+                : 'f--hlc'
+            )}
+          >
+            {this.getInParamsName()}
+          </div>
+          <div>
+            <div className="main-wrap f--hlc">
+              {this.state.inParams.length === 0
+                ? this.genAddButton('in')
+                : this.state.inParams.map((param, index) => {
+                    return this.getInParamsCom(param, index, 'in')
+                  })}
+            </div>
+            {this.genError('in')}
+          </div>
+        </div>
+        <div
+          className={cls(
+            'params params-out',
+            this.state.outParams.length > 0 ? 'f--h' : 'f--hlc'
+          )}
+        >
+          <div
+            className={cls(
+              'params-title params-out-title',
+              this.state.outParams.length > 3
+                ? 'params-title-top f--h'
+                : 'f--hlc'
+            )}
+          >
+            {this.getOutParamsName()}
+          </div>
+          <div>
+            <div className="main-wrap f--hlc">
+              {this.state.outParams.length === 0
+                ? this.genAddButton('out')
+                : this.state.outParams.map((param, index) => {
+                    return this.genInput(param, index, 'out')
+                  })}
+            </div>
+            {this.genError('out')}
+          </div>
+        </div>
+        <div className="params-title params-def-title params-service-title f--hlc">
+          <div className="flex-1">{this.getTitleName()}</div>
+        </div>
+      </div>
+    )
+  }
+}

+ 329 - 0
coms/jenga/extra/TimerServiceParams.jsx

@@ -0,0 +1,329 @@
+import React from 'react'
+import { Switch, Tooltip } from 'antd'
+import i18n from 'i18next'
+import cls from 'classnames'
+import treeActions from 'actions/tree'
+import uiActions from 'actions/ui'
+import widgetStore from 'stores/widget'
+import treeStores from 'stores/tree'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+import { getDate } from 'utils/commons/getDate'
+import { isHySa } from 'utils/commons/ctrlHide'
+export default class TimerServiceParams extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      params: this.getParams(),
+      defaultParams: [
+        {
+          name: 'param1',
+          value: 'now'
+        },
+        {
+          name: 'param2',
+          value: 'finishTimes'
+        },
+        {
+          name: 'param3',
+          value: 'workerIndex'
+        },
+        {
+          name: 'param4',
+          value: 'workerCount'
+        }
+      ],
+      title: this.getTitleName()
+    }
+
+    this.onChangeParam = this.onChangeParam.bind(this)
+    this.onBlurParam = this.onBlurParam.bind(this)
+    this.dealPeriodValue = this.dealPeriodValue.bind(this)
+    this.onToggleRecordsPanel = this.onToggleRecordsPanel.bind(this)
+
+    this.getParams = this.getParams.bind(this)
+    this.getTitleName = this.getTitleName.bind(this)
+
+    this.genInput = this.genInput.bind(this)
+    this.genDefaultParams = this.genDefaultParams.bind(this)
+  }
+
+  componentDidUpdate(preProps) {
+    if (preProps.nodeId !== this.props.nodeId) {
+      this.setState({
+        params: this.getParams(),
+        title: this.getTitleName()
+      })
+    }
+  }
+
+  onChangeNoAuto(propName, index) {
+    let obj = {}
+    let newParams = JSON.parse(JSON.stringify(this.state.params))
+    let param = newParams[index]
+    param.value = !param.value
+    obj[propName] = param.value
+    this.setState(
+      {
+        params: newParams
+      },
+      () => {
+        treeActions.changeNodeProps({
+          nodeId: this.props.nodeId,
+          props: obj
+        })
+      }
+    )
+  }
+
+  onChangeParam(propName, index, e) {
+    let value = e && e.target ? e.target.value : e
+    let param = this.state.params[index]
+    param.value = value
+    this.setState({
+      params: this.state.params
+    })
+  }
+
+  onBlurParam(propName, index, e) {
+    let obj = {}
+    let value = e && e.target ? e.target.value : e
+    let param = this.state.params[index]
+    if (propName === 'startTime') {
+      let regExp = /^[1-9]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])\s+(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d$/
+      if (!regExp.test(value)) {
+        value = getDate(new Date(), 'yyyy-MM-dd hh:mm:ss')
+      }
+    } else {
+      // 对企业id为10000586作特殊处理
+      value = parseFloat(value)
+      if (isNaN(value) || value < 0) {
+        // 用回对象原来的值
+        let node = getEventNodeById(this.props.nodeId)
+        value = node.props[propName] || 0
+        if (propName === 'period') {
+          // 最小时间间隔:60s
+          value =
+            value < 60
+              ? treeStores.curCaseInfo.eid === 10000586
+                ? value
+                : 60
+              : value
+        } else if (propName === 'repeatTimes') {
+          value = value > 9999999 ? 9999999 : value
+        } else if (propName === 'workers') {
+          // 并发线程大于等于1,小于100,默认1
+          value = value === 0 ? 1 : value
+        }
+      } else {
+        if (propName === 'period') {
+          // 最小时间间隔:60s
+          // value =
+          //   value < 1
+          //     ? treeStores.curCaseInfo.eid === 10000586
+          //       ? value * 60
+          //       : 60
+          //     : value * 60
+          value =
+            value < 60
+              ? treeStores.curCaseInfo.eid === 10000586
+                ? value
+                : 60
+              : value
+        } else if (propName === 'repeatTimes') {
+          value = value > 9999999 ? 9999999 : value
+        } else if (propName === 'workers') {
+          value = parseInt(value)
+          // 并发线程大于等于1,小于100,默认1
+          value = value === 0 ? 1 : value > 100 ? 100 : value
+        }
+      }
+    }
+    if (propName === 'period') {
+      param.value = this.dealPeriodValue(value)
+      obj[propName] = this.dealPeriodValue(value)
+    } else {
+      param.value = value
+      obj[propName] = value
+    }
+    this.setState(
+      {
+        params: this.state.params
+      },
+      () => {
+        treeActions.changeNodeProps({
+          nodeId: this.props.nodeId,
+          props: obj
+        })
+      }
+    )
+  }
+
+  dealPeriodValue(value) {
+    // let newValue = parseFloat(value / 60)
+    // if (!isNaN(newValue)) {
+    //   // newValue小于1则自动变为最多2位小数
+    //   if (newValue < 1) {
+    //     newValue = parseFloat((value / 60).toFixed(2))
+    //   }
+    // }
+    // return isNaN(newValue) ? 1 : newValue
+    let newValue = parseInt(value)
+    return newValue
+  }
+
+  onToggleRecordsPanel() {
+    uiActions.toggleServicePanel({ show: true, nodeId: this.props.nodeId })
+  }
+
+  getParams() {
+    let params = []
+    let node = getEventNodeById(this.props.nodeId)
+    if (node && widgetStore.widget[node.type]) {
+      widgetStore.widget[node.type].map.props.forEach(prop => {
+        let value =
+          node.props[prop.name] === undefined
+            ? prop.name === 'noAuto'
+              ? false
+              : ''
+            : node.props[prop.name]
+        if (prop.name === 'period') {
+          value = this.dealPeriodValue(value)
+        }
+        params.push({
+          name: prop.name,
+          value: value,
+          locale: prop.locale,
+          desc: prop.desc,
+          showDes: prop.showDes,
+          placeholder: prop.placeholder
+        })
+      })
+    }
+    return params
+  }
+
+  getTitleName() {
+    let title = ''
+    let node = getEventNodeById(this.props.nodeId)
+    if (node && widgetStore.widget[node.type]) {
+      let map = widgetStore.widget[node.type].map
+      title = map.locale ? map.locale[i18n.language] : map.name
+    }
+    return title ? title + i18n.t('EventView.jenga_extra.defination') : title
+  }
+
+  genInput(param, index) {
+    let { desc, showDes } = param
+    let tipContent = desc && desc[i18n.language]
+    return (
+      <div
+        className={cls('param-item f--hlc', {
+          'param-item-left': index % 3 !== 0,
+          'param-item-bottom': index < 3
+        })}
+        key={index}
+      >
+        <div className="item-name">
+          {param.locale ? param.locale[i18n.language] : param.name}
+        </div>
+        {!isHySa() && showDes && showDes === 1 && (
+          <Tooltip
+            title={tipContent}
+            placement="right"
+            destroyTooltipOnHide={true}
+          >
+            <div className="service-param-help">
+              <div className="ih5-icon-help" />
+            </div>
+          </Tooltip>
+        )}
+        {index === 3 ? (
+          <div className="item-input-wrap default-input-wrap f--hlc flex-1">
+            <Switch
+              checked={param.value}
+              onChange={this.onChangeNoAuto.bind(this, param.name, index)}
+              className="no-auto-switch"
+            />
+          </div>
+        ) : (
+          <div className="item-input-wrap default-input-wrap f--hlc flex-1">
+            <input
+              value={param.value}
+              onChange={this.onChangeParam.bind(this, param.name, index)}
+              onBlur={this.onBlurParam.bind(this, param.name, index)}
+            />
+            {index === 0 || index === 4 ? null : (
+              <div className="input-postfix">
+                {i18n.t(
+                  'EventView.jenga_extra.postfix_' +
+                    (index === 2 ? 'times' : 'mins')
+                )}
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+    )
+  }
+
+  genDefaultParams(param, index) {
+    return (
+      <div
+        className={cls('param-item f--hlc', {
+          'param-item-left': index % 3 !== 0
+        })}
+        key={index}
+      >
+        <div className="item-name">
+          {i18n.t('EventView.jenga_extra.param') + (index + 1)}
+        </div>
+        <div className="item-input-wrap default-input-wrap f--hlc flex-1">
+          <input
+            className="default"
+            readOnly={true}
+            defaultValue={i18n.t('EventView.jenga_extra.' + param.value)}
+          />
+        </div>
+      </div>
+    )
+  }
+
+  render() {
+    return (
+      <div className="jenga-timer-service-params">
+        <div className={cls('params f--h')}>
+          <div className={cls('params-title f--hlc')}>
+            {i18n.t('EventView.jenga_extra.serviceParams')}
+          </div>
+          <div className="main-wrap f--hlc">
+            {this.state.params.map((param, index) => {
+              return this.genInput(param, index)
+            })}
+          </div>
+        </div>
+        <div className={cls('params f--h')}>
+          <div className={cls('params-title f--hlc')}>
+            {i18n.t('EventView.jenga_extra.defaultParams')}
+          </div>
+          <div className="main-wrap f--hlc">
+            {this.state.defaultParams.map((param, index) => {
+              return this.genDefaultParams(param, index)
+            })}
+            <div className="record-break f--hcc">
+              <div className="break" />
+            </div>
+            <button
+              className="btn-clear btn-records"
+              onClick={this.onToggleRecordsPanel}
+            >
+              {i18n.t('EventView.jenga_extra.recordsPanel')}
+            </button>
+          </div>
+        </div>
+        <div className="params-title params-def-title">
+          {this.getTitleName()}
+        </div>
+      </div>
+    )
+  }
+}

+ 2 - 2
coms/jenga/panel/JengaPanel.jsx

@@ -1,6 +1,6 @@
 import React from 'react'
 import uiStores from 'stores/ui'
-// import JengaExtra from '../extra/JengaExtra'
+import JengaExtra from '../extra/JengaExtra'
 import JengaItem from './JengaItem'
 
 const minWidth = 858 // 最小宽度
@@ -40,7 +40,7 @@ export default class JengaPanel extends React.Component {
   render() {
     return (
       <div className="jenga-panel scroll-hide-bar">
-        {/* {this.props.curEventNodeList ? undefined : <JengaExtra />} */}
+        {this.props.curEventNodeList ? undefined : <JengaExtra />}
         <div className="jenga-content" ref={this.jengaContent}>
           {this.props.curEventNodeList ? (
             this.props.curEventNodeList.map((nodeId, index) => {