king
2023-02-16 fc55bf4131e3056a84ac7bae16540a4e714214b8
2023-02-16
13个文件已修改
6个文件已添加
1822 ■■■■■ 已修改文件
src/assets/css/main.scss 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/components/card/cardcomponent/index.jsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/components/card/cardcomponent/options.jsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/components/card/double-data-card/index.jsx 643 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/components/card/double-data-card/index.scss 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/components/card/double-data-card/options.jsx 253 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/components/card/doublecardcomponent/index.jsx 308 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/components/card/doublecardcomponent/index.scss 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/components/card/doublecardcomponent/options.jsx 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/datasource/index.jsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/datasource/verifycard/index.jsx 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/datasource/verifycard/index.scss 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/datasource/verifycard/settingform/index.jsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/menushell/card.jsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/menu/modulesource/option.jsx 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/tabviews/custom/components/module/voucher/index.jsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/tabviews/zshare/actionList/normalbutton/index.jsx 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/tabviews/zshare/actionList/printbutton/index.jsx 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/templates/modalconfig/settingform/index.jsx 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/css/main.scss
@@ -326,6 +326,12 @@
    background: rgba(0, 0, 0, 0);
  }
}
.moveable-modal {
  overflow-x: hidden!important;
  .ant-modal-header {
    cursor: move;
  }
}
.ant-drawer {
  z-index: 1080!important;
src/menu/components/card/cardcomponent/index.jsx
@@ -13,7 +13,7 @@
import './index.scss'
const NormalForm = asyncIconComponent(() => import('@/components/normalform'))
const CardCellComponent = asyncComponent(() => import('../cardcellcomponent'))
const CardCellComponent = asyncComponent(() => import('@/menu/components/card/cardcellcomponent'))
const CopyComponent = asyncIconComponent(() => import('@/menu/components/share/copycomponent'))
const PasteController = asyncIconComponent(() => import('@/components/paste'))
src/menu/components/card/cardcomponent/options.jsx
@@ -73,7 +73,7 @@
      controlFields: [
        {field: 'transform', values: ['multi']},
      ],
      forbid: appType === 'mob'
      forbid: appType === 'mob' || subtype === 'dualdatacard'
    },
    {
      type: 'select',
@@ -91,7 +91,8 @@
        {value: 'opacity', label: '透明度'},
        {value: 'rotateX', label: '纵向展开'},
        {value: 'rotateY', label: '横向展开'},
      ]
      ],
      forbid: appType === 'mob' || subtype === 'dualdatacard'
    },
    {
      type: 'text',
src/menu/components/card/double-data-card/index.jsx
New file
@@ -0,0 +1,643 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import { is, fromJS } from 'immutable'
import { Popover, Modal, Pagination, message } from 'antd'
import { PlusOutlined, PlusCircleOutlined, PlusSquareOutlined, EditOutlined, ToolOutlined, DeleteOutlined, FontColorsOutlined, DownOutlined } from '@ant-design/icons'
import asyncComponent from '@/utils/asyncComponent'
import asyncIconComponent from '@/utils/asyncIconComponent'
import { resetStyle, getTables } from '@/utils/utils-custom.js'
import MKEmitter from '@/utils/events.js'
import Utils from '@/utils/utils.js'
import getWrapForm from './options'
import './index.scss'
const SettingComponent = asyncIconComponent(() => import('@/menu/datasource'))
const NormalForm = asyncIconComponent(() => import('@/components/normalform'))
const CardComponent = asyncComponent(() => import('../cardcomponent'))
const DoubleCardComponent = asyncComponent(() => import('../doublecardcomponent'))
const MobPagination = asyncIconComponent(() => import('@/menu/components/share/mobPagination'))
const CopyComponent = asyncIconComponent(() => import('@/menu/components/share/copycomponent'))
const UserComponent = asyncIconComponent(() => import('@/menu/components/share/usercomponent'))
const PasteComponent = asyncIconComponent(() => import('@/components/paste'))
const NormalHeader = asyncComponent(() => import('@/menu/components/share/normalheader'))
const ClockComponent = asyncIconComponent(() => import('@/menu/components/share/clockcomponent'))
const ActionComponent = asyncComponent(() => import('@/menu/components/share/actioncomponent'))
const { confirm } = Modal
class DoubleDataCardEditComponent extends Component {
  static propTpyes = {
    card: PropTypes.object,
    deletecomponent: PropTypes.func,
    updateConfig: PropTypes.func,
  }
  state = {
    card: null,
    appType: sessionStorage.getItem('appType'),
    back: false
  }
  UNSAFE_componentWillMount () {
    const { card } = this.props
    if (card.isNew) {
      let _card = {
        uuid: card.uuid,
        type: card.type,
        format: 'array',   // 组件属性 - 数据格式
        pageable: true,    // 组件属性 - 是否可分页
        switchable: true,  // 组件属性 - 数据是否可切换
        width: card.width || 24,
        name: card.name,
        subtype: card.subtype,
        setting: { interType: 'system' },
        wrap: { name: card.name, width: card.width || 24, title: '', pagestyle: 'page', cardType: '' },
        style: { marginLeft: '0px', marginRight: '0px', marginTop: '8px', marginBottom: '8px' },
        headerStyle: { fontSize: '16px', borderBottomWidth: '1px', borderBottomColor: '#e8e8e8' },
        columns: [],
        subColumns: [],
        scripts: [],
        action: [],
        search: [],
        subcards: [{
          uuid: Utils.getuuid(),
          setting: { width: 24},
          style: {
            borderWidth: '1px', borderColor: '#e8e8e8',
            paddingTop: '15px', paddingBottom: '15px', paddingLeft: '15px', paddingRight: '15px',
            marginLeft: '8px', marginRight: '8px', marginTop: '8px', marginBottom: '8px'
          },
          elements: [{
            uuid: Utils.getuuid(),
            eleType: 'text',
            datatype: 'static',
            value: '循环区域'
          }],
          backSetting: {},
          backStyle: {
            borderLeftWidth: '1px', borderLeftColor: '#e8e8e8',
            borderRightWidth: '1px', borderRightColor: '#e8e8e8',
            borderBottomWidth: '1px', borderBottomColor: '#e8e8e8',
            paddingTop: '15px', paddingBottom: '15px', paddingLeft: '15px', paddingRight: '15px'
          },
          backElements: [{
            uuid: Utils.getuuid(),
            eleType: 'text',
            datatype: 'static',
            value: '子表区域'
          }]
        }]
      }
      if (card.config) {
        let config = fromJS(card.config).toJS()
        _card.wrap = config.wrap
        _card.wrap.name = card.name
        _card.style = config.style
        _card.headerStyle = config.headerStyle
        _card.setting = config.setting
        _card.columns = config.columns
        _card.scripts = config.scripts
        _card.subcards = config.subcards.map(scard => {
          scard.uuid = Utils.getuuid()
          scard.elements = scard.elements.map(elem => {
            elem.uuid = Utils.getuuid()
            return elem
          })
          scard.backElements = scard.backElements.map(elem => {
            elem.uuid = Utils.getuuid()
            return elem
          })
          return scard
        })
        _card.action = config.action.map(col => {
          col.uuid = Utils.getuuid()
          return col
        })
        _card.search = config.search.map(col => {
          col.uuid = Utils.getuuid()
          return col
        })
      }
      this.updateComponent(_card)
    } else {
      let _card = fromJS(card).toJS()
      this.setState({
        card: _card
      })
    }
  }
  shouldComponentUpdate (nextProps, nextState) {
    return !is(fromJS(this.state), fromJS(nextState))
  }
  /**
   * @description 组件销毁,清除state更新,清除快捷键设置
   */
  componentWillUnmount () {
    this.setState = () => {
      return
    }
  }
  /**
   * @description 卡片行外层信息更新(数据源,样式等)
   */
  updateComponent = (card) => {
    card.width = card.wrap.width
    card.name = card.wrap.name
    if (window.GLOB.styling && card.errors) { // 样式修改时不做筛查
      this.setState({
        card: card
      })
      this.props.updateConfig(card)
      return
    }
    card.errors = []
    let columns = card.columns.map(c => c.field)
    if (card.setting.interType === 'system' && card.setting.execute !== 'false' && !card.setting.dataresource) {
      card.errors.push({ level: 0, detail: '未设置数据源!'})
    } else if (card.setting.interType === 'system' && card.setting.execute === 'false' && card.scripts.filter(script => script.status !== 'false').length === 0) {
      card.errors.push({ level: 0, detail: '数据源中无可用脚本!'})
    } else if (!card.setting.primaryKey) {
      card.errors.push({ level: 0, detail: '未设置主键!'})
    } else if (!columns.includes(card.setting.primaryKey)) {
      card.errors.push({ level: 0, detail: '主键已失效!'})
    } else if (!card.setting.supModule) {
      card.errors.push({ level: 0, detail: '未设置上级组件!'})
    }
    if (card.errors.length === 0) {
      card.$tables = getTables(card)
    }
    card.action.forEach(cell => {
      if (cell.OpenType === 'pop' || (cell.OpenType === 'funcbutton' && cell.execMode === 'pop')) {
        if (!cell.modal || cell.modal.fields.length === 0) {
          card.errors.push({ level: 1, detail: `按钮“${cell.label}”中表单尚未添加`})
        }
      }
    })
    card.subcards.forEach((item, i) => {
      let linkbtn = item.setting.linkbtn || ''
      item.elements.forEach(cell => {
        if (cell.eleType === 'button') {
          if (cell.OpenType === 'pop' || (cell.OpenType === 'funcbutton' && cell.execMode === 'pop')) {
            if (!cell.modal || cell.modal.fields.length === 0) {
              card.errors.push({ level: 1, detail: `按钮“${cell.label}”中表单尚未添加`})
            }
          }
          if (linkbtn && linkbtn === cell.uuid) {
            linkbtn = ''
          }
        } else if (cell.datatype === 'dynamic' && cell.field && !columns.includes(cell.field)) {
          card.errors.push({ level: 1, detail: `卡片中动态字段“${cell.field}”无效`})
        }
      })
      item.backElements.forEach(cell => {
        if (cell.eleType === 'button') {
          if (cell.OpenType === 'pop' || (cell.OpenType === 'funcbutton' && cell.execMode === 'pop')) {
            if (!cell.modal || cell.modal.fields.length === 0) {
              card.errors.push({ level: 1, detail: `按钮“${cell.label}”中表单尚未添加`})
            }
          }
          if (linkbtn && linkbtn === cell.uuid) {
            linkbtn = ''
          }
        } else if (cell.datatype === 'dynamic' && cell.field && !columns.includes(cell.field)) {
          card.errors.push({ level: 1, detail: `卡片中动态字段“${cell.field}”无效`})
        }
      })
      if (linkbtn) {
        card.errors.push({ level: 1, detail: `第${i + 1}张卡片中绑定按钮已删除`})
      }
    })
    this.setState({
      card: card
    })
    this.props.updateConfig(card)
  }
  /**
   * @description 单个卡片信息更新
   */
  updateCard = (cell, btn) => {
    let card = fromJS(this.state.card).toJS()
    card.subcards = card.subcards.map(item => {
      if (item.uuid === cell.uuid) return cell
      return item
    })
    if (btn) {
      card.action = card.action.filter(item => item.uuid !== btn.uuid)
    }
    this.updateComponent(card)
  }
  /**
   * @description 单个卡片信息更新
   */
  deleteCard = (cell) => {
    let card = fromJS(this.state.card).toJS()
    let _this = this
    confirm({
      content: '确定删除卡片吗?',
      onOk() {
        card.subcards = card.subcards.filter(item => item.uuid !== cell.uuid)
        _this.updateComponent(card)
      },
      onCancel() {}
    })
  }
  changeStyle = () => {
    const { card } = this.state
    MKEmitter.emit('changeStyle', ['background', 'height', 'border', 'padding', 'margin', 'shadow', 'clear', 'minHeight'], card.style, this.getStyle)
  }
  getStyle = (style) => {
    let _card = {...this.state.card, style}
    this.updateComponent(_card)
  }
  addSearch = (copy) => {
    const { card } = this.state
    let newcard = {}
    if (copy) {
      newcard = copy
      newcard.focus = true
    } else {
      newcard.uuid = Utils.getuuid()
      newcard.focus = true
      newcard.label = 'label'
      newcard.type = 'select'
      newcard.resourceType = '0'
      newcard.options = []
      newcard.orderType = 'asc'
      newcard.match = '='
    }
    // 注册事件-添加搜索
    MKEmitter.emit('addSearch', card.uuid, newcard)
  }
  addButton = (copy) => {
    const { card } = this.state
    let newcard = {}
    if (copy) {
      newcard = copy
      newcard.focus = true
    } else {
      newcard.uuid = Utils.getuuid()
      newcard.focus = true
      newcard.label = 'label'
      newcard.Ot = 'requiredSgl'
      newcard.OpenType = 'pop'
      newcard.icon = ''
      newcard.class = 'green'
      newcard.intertype = card.setting.interType || 'system'
      newcard.innerFunc = card.setting.innerFunc || ''
      newcard.sysInterface = card.setting.sysInterface || ''
      newcard.outerFunc = card.setting.outerFunc || ''
      newcard.interface = card.setting.interface || ''
      newcard.execSuccess = 'grid'
      newcard.execError = 'never'
      newcard.verify = null
      newcard.show = 'button'
      newcard.style = {marginRight: '15px'}
    }
    // 注册事件-添加按钮
    MKEmitter.emit('addButton', card.uuid, newcard)
  }
  setSubConfig = (item) => {
    const { card, appType } = this.state
    let btn = fromJS(item).toJS()
    if (btn.OpenType === 'pop' || btn.execMode === 'pop') {
      if (!btn.modal) {
        btn.modal = {
          setting: { title: btn.label, width: appType === 'mob' ? 100 : 60, cols: '2', container: 'view', focus: '', finish: 'close', clickouter: 'unclose', display: 'modal' },
          tables: [],
          groups: [],
          fields: []
        }
      }
      MKEmitter.emit('changeModal', card, btn)
    } else if (btn.OpenType === 'popview' && appType !== 'mob') {
      MKEmitter.emit('changePopview', card, btn)
    }
  }
  addCard = (copy) => {
    let card = fromJS(this.state.card).toJS()
    let newcard = {}
    if (copy) { // 粘贴
      newcard = copy
    } else {
      let height = card.subcards[0].style.height
      if (height === 'auto') {
        height = '100px'
      }
      newcard = {
        uuid: Utils.getuuid(),
        $cardType: 'extendCard',
        setting: { width: 24, click: ''},
        style: {
          height,
          borderWidth: '1px', borderColor: '#e8e8e8',
          paddingTop: '15px', paddingBottom: '15px', paddingLeft: '15px', paddingRight: '15px',
          marginLeft: '8px', marginRight: '8px', marginTop: '8px', marginBottom: '8px'
        },
        backStyle: {},
        elements: [],
        backElements: []
      }
    }
    card.subcards.push(newcard)
    this.updateComponent(card)
  }
  move = (item, direction) => {
    let card = fromJS(this.state.card).toJS()
    let dragIndex = card.subcards.findIndex(c => c.uuid === item.uuid)
    let hoverIndex = null
    if (direction === 'left') {
      hoverIndex = dragIndex - 1
    } else {
      hoverIndex = dragIndex + 1
    }
    if (hoverIndex === -1 || hoverIndex === card.subcards.length) return
    card.subcards.splice(hoverIndex, 0, ...card.subcards.splice(dragIndex, 1))
    this.updateComponent(card)
  }
  getWrapForms = () => {
    const { card } = this.state
    return getWrapForm(card.wrap, card.columns, card.setting)
  }
  updateWrap = (res) => {
    const { card } = this.state
    let _card = {...card, wrap: res}
    if (res.supNodes) {
      _card.supNodes = res.supNodes
      _card.supNodes = _card.supNodes.map(item => {
        item.componentId = item.nodes[item.nodes.length - 1]
        return item
      })
      delete res.supNodes
    } else {
      delete _card.supNodes
    }
    if (res.layout === 'flex') {
      _card.wrap.pagestyle = 'page'
    }
    if (res.selStyle === 'tabs' && card.wrap.selStyle !== 'tabs') {
      Object.keys(_card.style).forEach(key => {
        if (/^border/.test(key)) {
          delete _card.style[key]
        }
      })
      _card.style.borderBottomColor = '#eeeeee'
      _card.style.borderBottomWidth = '1px'
      _card.style.paddingBottom = '0px'
      _card.subcards.forEach(item => {
        delete item.style.marginBottom
      })
      this.setState({card: {..._card, subcards: []}}, () => {
        this.updateComponent(_card)
      })
    } else {
      this.updateComponent(_card)
    }
  }
  pasteComponent = (res, resolve) => {
    const { card, appType } = this.state
    let type = res.copyType
    delete res.copyType
    if (type === 'cardcell') {
      res.uuid = Utils.getuuid()
      res.setting = res.setting || {}
      res.$cardType = 'extendCard'
      res.setting.width = res.setting.width || 6
      let mobtypes = ['pop', 'prompt', 'exec', 'innerpage', 'funcbutton']
      let elements = []
      res.elements && res.elements.forEach(cell => {
        // if (cell.datatype === 'dynamic') {
        //   cell.datatype = 'static'
        // }
        if (cell.eleType !== 'button') {
          cell.uuid = Utils.getuuid()
          elements.push(cell)
        } else if (appType === 'mob' && !mobtypes.includes(cell.OpenType)) {
          return
        } else {
          cell.uuid = Utils.getuuid()
          elements.push(cell)
        }
      })
      res.elements = elements
      let backElements = []
      if (appType !== 'mob') {
        res.backElements && res.backElements.forEach(cell => {
          // if (cell.datatype === 'dynamic') {
          //   cell.datatype = 'static'
          // }
          if (cell.eleType !== 'button') {
            cell.uuid = Utils.getuuid()
            backElements.push(cell)
          } else if (appType === 'mob' && !mobtypes.includes(cell.OpenType)) {
            return
          } else {
            cell.uuid = Utils.getuuid()
            backElements.push(cell)
          }
        })
      }
      res.backElements = backElements
      resolve({status: true})
      this.addCard(res)
    } else if (type === 'search' || type === 'form') {
      if (appType === 'mob') {
        resolve({status: false, message: '移动端数据卡不支持添加搜索条件。'})
      } else {
        res.uuid = Utils.getuuid()
        let keys = card.search.map(item => item.field.toLowerCase())
        if (type === 'form') {
          if (['number', 'switch', 'textarea', 'fileupload', 'hint', 'color', 'funcvar'].includes(res.type)) {
            res.type = 'text'
          } else if (res.type === 'radio') {
            res.type = 'select'
          } else if (res.type === 'checkbox') {
            res.type = 'multiselect'
          } else if (res.type === 'datetime') {
            res.type = 'date'
          }
        }
        if (res.field && keys.includes(res.field.toLowerCase())) {
          resolve({status: false, message: '搜索字段已存在!'})
          return
        }
        resolve({status: true})
        this.addSearch(res)
      }
    } else if (type === 'action') {
      if (appType === 'mob' && !['pop', 'prompt', 'exec', 'innerpage'].includes(res.OpenType)) {
        resolve({status: false, message: '移动端不支持此类型的按钮。'})
      } else {
        resolve({status: true})
        res.uuid = Utils.getuuid()
        this.addButton(res)
      }
    }
  }
  clickComponent = (e) => {
    if (sessionStorage.getItem('style-control') === 'true' || sessionStorage.getItem('style-control') === 'component') {
      e.stopPropagation()
      MKEmitter.emit('clickComponent', this.state.card.uuid, null, (style) => {
        let _card = {...this.state.card}
        _card.style = {..._card.style, ...style}
        this.setState({ card: _card })
        this.props.updateConfig(_card)
      })
    }
  }
  render() {
    const { card, appType } = this.state
    let _style = resetStyle(card.style)
    return (
      <div className={'menu-double-data-card-edit-box ' + appType} style={_style} onClick={this.clickComponent} id={card.uuid}>
        <NormalHeader config={card} updateComponent={this.updateComponent}/>
        <Popover overlayClassName="mk-popover-control-wrap" mouseLeaveDelay={0.2} mouseEnterDelay={0.2} content={
          <div className="mk-popover-control">
            <PlusOutlined className="plus" title="添加卡片" onClick={() => this.addCard()}/>
            {appType !== 'mob' ? <PlusCircleOutlined className="plus" title="添加搜索" onClick={() => this.addSearch()}/> : null}
            <PlusSquareOutlined className="plus" title="添加按钮" onClick={() => this.addButton()}/>
            <NormalForm title="数据卡设置" width={800} update={this.updateWrap} getForms={this.getWrapForms}>
              <EditOutlined style={{color: '#1890ff'}} title="编辑"/>
            </NormalForm>
            <CopyComponent type="datacard" card={card}/>
            <PasteComponent options={['action', 'search', 'form', 'cardcell']} updateConfig={this.pasteComponent} />
            <FontColorsOutlined className="style" title="调整样式" onClick={this.changeStyle}/>
            <ClockComponent config={card} updateConfig={this.updateComponent}/>
            <UserComponent config={card}/>
            <DeleteOutlined className="close" title="删除组件" onClick={() => this.props.deletecomponent(card.uuid)} />
            <SettingComponent config={card} updateConfig={this.updateComponent} />
          </div>
        } trigger="hover">
          <ToolOutlined />
        </Popover>
        <ActionComponent config={card} setSubConfig={this.setSubConfig} updateaction={this.updateComponent}/>
        <div className={'float-' + (card.wrap.cardFloat || 'left') + ' select-' + card.wrap.selStyle}>
          {card.subcards.map(subcard => {
            if (subcard.$cardType === 'extendCard') {
              return (<CardComponent key={subcard.uuid} cards={card} card={subcard} move={this.move} updateElement={this.updateCard} deleteElement={this.deleteCard}/>)
            } else {
              return (<DoubleCardComponent key={subcard.uuid} cards={card} card={subcard} updateElement={this.updateCard}/>)
            }
          })}
        </div>
        <div style={{clear: 'both'}}></div>
        {card.wrap.pagestyle === 'page' && card.setting.laypage === 'true' && appType !== 'mob' ? <Pagination total={85} size="small" showTotal={total => `共 ${total} 条`} pageSize={20} defaultCurrent={1}/> : null}
        {card.wrap.pagestyle === 'page' && card.setting.laypage === 'true' && appType === 'mob' ? <MobPagination /> : null}
        {card.wrap.pagestyle === 'more' && card.setting.laypage === 'true' ? <div className="mk-more">查看更多<DownOutlined/></div> : null}
        <div className="component-name">
          <div className="center">
            <div className="title" onDoubleClick={() => {
              let oInput = document.createElement('input')
              oInput.value = card.uuid
              document.body.appendChild(oInput)
              oInput.select()
              document.execCommand('Copy')
              document.body.removeChild(oInput)
              message.success('复制成功。')
            }}>{card.name}</div>
            <div className="content">
              {card.errors && card.errors.map((err, index) => {
                if (err.level === 0) {
                  return <span key={index} className="error">{err.detail}</span>
                } else {
                  return <span key={index} className="waring">{err.detail};</span>
                }
              })}
            </div>
          </div>
        </div>
      </div>
    )
  }
}
export default DoubleDataCardEditComponent
src/menu/components/card/double-data-card/index.scss
New file
@@ -0,0 +1,141 @@
.menu-double-data-card-edit-box {
  position: relative;
  box-sizing: border-box;
  background: #ffffff;
  background-position: center center;
  background-repeat: no-repeat;
  background-size: cover;
  min-height: 20px;
  overflow-y: auto;
  >.anticon-tool {
    position: absolute;
    z-index: 2;
    font-size: 16px;
    right: 1px;
    top: 1px;
    cursor: pointer;
    padding: 5px;
    background: rgba(255, 255, 255, 0.55);
  }
  .card-item {
    overflow: hidden;
    position: relative;
    background-color: #ffffff;
    background-position: center center;
    background-repeat: no-repeat;
    background-size: cover;
    min-height: 20px;
  }
  .card-item.hover:not(:hover) {
    button {
      opacity: 0;
      transition: opacity 0.3s;
    }
  }
  .card-item:hover {
    box-shadow: 0px 0px 2px #1890ff;
  }
  .model-menu-card-cell-list .card-detail-row > .anticon-plus {
    position: absolute;
    right: -30px;
    font-size: 16px;
  }
  .model-menu-action-list {
    line-height: 40px;
    .ant-row > .anticon-plus {
      position: absolute;
      right: -30px;
      font-size: 16px;
    }
  }
  .ant-pagination {
    float: right;
    margin: 10px;
  }
  .model-menu-action-list {
    .page-card {
      line-height: 55px;
    }
  }
  .normal-pagination {
    .am-button::before {
      display: none;
    }
    .am-button {
      border: none;
      font-size: 16px;
    }
  }
  .float-center {
    text-align: center;
    >.ant-col {
      display: inline-block;
      float: none;
      text-align: left;
      vertical-align: top;
    }
  }
  .float-right {
    text-align: right;
    >.ant-col {
      display: inline-block;
      float: none;
      text-align: left;
      vertical-align: top;
    }
  }
  .select-tabs {
    .card-item {
      border-top: none!important;
      border-left: none!important;
      border-right: none!important;
      border-radius: 0px!important;
      border-bottom: 2px solid transparent!important;
    }
  }
  .mk-more {
    text-align: center;
    line-height: 40px;
    .anticon-down {
      margin-left: 2px;
    }
  }
}
.menu-double-data-card-edit-box::-webkit-scrollbar {
  width: 7px;
  height: 7px;
}
.menu-double-data-card-edit-box::-webkit-scrollbar-thumb {
  border-radius: 5px;
  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.13);
  background: rgba(0, 0, 0, 0.13);
}
.menu-double-data-card-edit-box::-webkit-scrollbar-track {
  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.05);
  border-radius: 3px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: rgba(0, 0, 0, 0);
}
.menu-double-data-card-edit-box::after {
  display: block;
  content: ' ';
  clear: both;
}
.menu-double-data-card-edit-box.mob {
  .model-menu-action-list {
    .page-card {
      line-height: 40px;
      margin-top: 5px;
    }
  }
}
.menu-double-data-card-edit-box:hover {
  z-index: 1;
  box-shadow: 0px 0px 4px #1890ff;
}
src/menu/components/card/double-data-card/options.jsx
New file
@@ -0,0 +1,253 @@
/**
 * @description Wrap表单配置信息
 */
export default function (wrap, columns = [], setting) {
  let appType = sessionStorage.getItem('appType')
  let MenuType = ''
  let menu = window.GLOB.customMenu
  let laypage = setting && setting.laypage !== 'false'
  if (menu.parentId === 'BillPrintTemp') {
    MenuType = 'billPrint'
  }
  let roleList = sessionStorage.getItem('sysRoles')
  if (roleList) {
    try {
      roleList = JSON.parse(roleList)
    } catch (e) {
      roleList = []
    }
  } else {
    roleList = []
  }
  let menulist = []
  if (appType === 'mob') {
    menulist = sessionStorage.getItem('appMenus')
    if (menulist) {
      try {
        menulist = JSON.parse(menulist)
      } catch (e) {
        menulist = []
      }
    } else {
      menulist = []
    }
  }
  const cardWrapForm = [
    {
      type: 'text',
      field: 'title',
      label: '标题',
      initval: wrap.title || '',
      required: false
    },
    {
      type: 'text',
      field: 'name',
      label: '组件名称',
      initval: wrap.name || '',
      tooltip: '用于组件间的区分。',
      required: true
    },
    {
      type: 'number',
      field: 'width',
      label: '宽度',
      initval: wrap.width || 24,
      tooltip: '栅格布局,每行等分为24列。',
      min: 1,
      max: 24,
      precision: 0,
      required: true
    },
    {
      type: 'radio',
      field: 'layout',
      label: '卡片布局',
      initval: wrap.layout || 'grid',
      tooltip: appType === 'mob' ? '弹性布局时,滑动加载无效' : '',
      required: false,
      options: [
        {value: 'grid', label: '栅格布局'},
        {value: 'flex', label: '弹性布局'},
      ],
      controlFields: [
        {field: 'printHeight', values: ['flex']},
        {field: 'cardFloat', values: ['grid']},
      ]
    },
    {
      type: 'radio',
      field: 'pagestyle',
      label: '分页风格',
      initval: wrap.pagestyle || 'page',
      tooltip: '数据源选择分页时有效。注:弹性布局时固定为页码。',
      required: false,
      disabled: !laypage,
      options: [
        {value: 'page', label: '页码'},
        {value: 'slide', label: '滑动加载', forbid: appType !== 'mob' || sessionStorage.getItem('editMenuType') === 'popview'},
        {value: 'more', label: '查看更多'},
      ],
      controlFields: [
        {field: 'slidetip', values: ['slide']},
      ],
    },
    {
      type: 'radio',
      field: 'cardType',
      label: '数据选择',
      initval: wrap.cardType || '',
      required: false,
      options: [
        {value: '', label: '不可选'},
        {value: 'radio', label: '单选'},
        {value: 'checkbox', label: '多选'},
      ],
      controlFields: [
        {field: 'selected', values: ['radio', 'checkbox']},
        {field: 'selStyle', values: ['radio', 'checkbox']},
      ],
    },
    {
      type: 'select',
      field: 'selected',
      label: '数据选中',
      initval: wrap.selected || 'false',
      tooltip: '初始化:数据加载时选中首行数据,仅执行一次。数据加载:每次数据加载时均选中首行(当按钮执行完成并返回主键值时,默认选中主键值对应行)。选中标记:返回数据中存在 selected 字段,且值为 true 的数据被选中。',
      required: false,
      options: [
        {value: 'false', label: '无'},
        {value: 'init', label: '初始化'},
        {value: 'always', label: '数据加载'},
        {value: 'sign', label: '选中标记'}
      ],
    },
    {
      type: 'select',
      field: 'selStyle',
      label: '选中风格',
      initval: wrap.selStyle || 'active',
      tooltip: '存在边框时,边框会使用系统色。',
      required: false,
      options: [
        {value: 'none', label: '无'},
        {value: 'active', label: '外阴影'},
        {value: 'backFont', label: '背景+文字'},
        {value: 'font', label: '文字'},
        {value: 'check', label: '勾选(圆框)'},
        {value: 'check square', label: '勾选(方框)'}
      ]
    },
    {
      type: 'radio',
      field: 'cardFloat',
      label: '对齐方式',
      initval: wrap.cardFloat || 'left',
      tooltip: '设置卡片的对齐方式。',
      required: false,
      options: [
        {value: 'left', label: '左对齐'},
        {value: 'center', label: '居中'},
        {value: 'right', label: '右对齐'},
      ],
    },
    {
      type: 'radio',
      field: 'parity',
      label: '奇偶背景',
      initval: wrap.parity || 'false',
      tooltip: '偶数行会添加背景色。',
      required: false,
      options: [
        {value: 'false', label: '无'},
        {value: 'true', label: '有'},
      ],
    },
    {
      type: 'number',
      field: 'printHeight',
      label: '换算高度',
      initval: wrap.printHeight || '',
      tooltip: '当前数据卡高度相当于几条数据。',
      required: false,
      forbid: MenuType !== 'billPrint'
    },
    {
      type: 'radio',
      field: 'empty',
      label: '空值隐藏',
      initval: wrap.empty || 'show',
      tooltip: '当查询数据为空时,隐藏该组件。',
      required: false,
      options: [
        {value: 'show', label: '否'},
        {value: 'hidden', label: '是'},
      ],
    },
    {
      type: 'select',
      field: 'controlField',
      label: '禁用字段',
      initval: wrap.controlField || '',
      tooltip: '用于控制行数据是否可选择。',
      required: false,
      allowClear: true,
      options: columns,
      controlFields: [
        {field: 'controlVal', notNull: true},
      ],
    },
    {
      type: 'text',
      field: 'controlVal',
      label: '禁用值',
      initval: wrap.controlVal || '',
      tooltip: '当字段值与禁用值相等时,行数据会禁用,多个值用逗号分隔。',
      required: false
    },
    {
      type: 'radio',
      field: 'permission',
      label: '权限验证',
      initval: wrap.permission || 'false',
      required: false,
      options: [
        {value: 'true', label: '启用'},
        {value: 'false', label: '禁用'},
      ],
      forbid: !appType || sessionStorage.getItem('editMenuType') === 'popview'
    },
    {
      type: 'multiselect',
      field: 'blacklist',
      label: '黑名单',
      initval: wrap.blacklist || [],
      required: false,
      options: roleList,
      forbid: !!appType
    },
    {
      type: 'text',
      field: 'slidetip',
      label: '底部提示',
      initval: wrap.slidetip || wrap.slidetip === '' ? wrap.slidetip : '没有更多了',
      tooltip: '滑动加载至底部时的提示信息。',
      required: false,
      forbid: !laypage || appType !== 'mob'
    },
  ]
  return cardWrapForm.filter(item => {
    if (['pagestyle'].includes(item.field)) {
      item.options = item.options.filter(option => !option.forbid)
    }
    return !item.forbid
  })
}
src/menu/components/card/doublecardcomponent/index.jsx
New file
@@ -0,0 +1,308 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import { is, fromJS } from 'immutable'
import { Popover, Col } from 'antd'
import { PlusOutlined, PlusSquareOutlined, EditOutlined, ToolOutlined, FontColorsOutlined } from '@ant-design/icons'
import asyncComponent from '@/utils/asyncComponent'
import asyncIconComponent from '@/utils/asyncIconComponent'
import { resetStyle } from '@/utils/utils-custom.js'
import getSettingForm from './options'
import Utils from '@/utils/utils.js'
import MKEmitter from '@/utils/events.js'
import './index.scss'
const NormalForm = asyncIconComponent(() => import('@/components/normalform'))
const CardCellComponent = asyncComponent(() => import('@/menu/components/card/cardcellcomponent'))
const CopyComponent = asyncIconComponent(() => import('@/menu/components/share/copycomponent'))
const PasteController = asyncIconComponent(() => import('@/components/paste'))
class DoubleCardBoxComponent extends Component {
  static propTpyes = {
    cards: PropTypes.object,         // 卡片行配置信息
    card: PropTypes.object,          // 卡片配置信息
    updateElement: PropTypes.func    // 菜单配置更新
  }
  state = {
    card: null,            // 卡片信息,包括正反面
    appType: sessionStorage.getItem('appType'),
    visible: false
  }
  /**
   * @description 搜索条件初始化
   */
  UNSAFE_componentWillMount () {
    const { card } = this.props
    this.setState({
      card: fromJS(card).toJS()
    })
  }
  shouldComponentUpdate (nextProps, nextState) {
    const { cards } = this.props
    return !is(fromJS(cards), fromJS(nextProps.cards)) || !is(fromJS(this.state), fromJS(nextState))
  }
  /**
   * @description 组件销毁,清除state更新,清除快捷键设置
   */
  componentWillUnmount () {
    this.setState = () => {
      return
    }
  }
  getStyle = (style) => {
    const { card, side } = this.state
    let _card = fromJS(card).toJS()
    if (side === 'back') {
      _card.backStyle = style
    } else {
      _card.style = style
    }
    this.setState({
      card: _card
    })
    this.props.updateElement(_card)
  }
  updateCard = (elements, btn) => {
    const { card, side } = this.state
    let _card = {}
    if (side === 'back') {
      _card = {...card, backElements: elements}
    } else {
      _card = {...card, elements: elements}
    }
    this.setState({
      card: _card
    })
    this.props.updateElement(_card, btn)
  }
  changeSide = () => {
    const { card } = this.props
    const { side } = this.state
    let _side = ''
    let _elements = null
    if (side === 'front') {
      _side = 'back'
    } else {
      _side = 'front'
    }
    if (_side === 'front') {
      _elements = fromJS(card.elements).toJS()
    } else {
      _elements = fromJS(card.backElements).toJS()
    }
    this.setState({side: _side, elements: _elements})
  }
  addElement = () => {
    const { card } = this.state
    let newcard = {}
    newcard.uuid = Utils.getuuid()
    newcard.focus = true
    newcard.eleType = 'text'
    newcard.datatype = 'dynamic'
    newcard.height = 1
    // 注册事件-添加元素
    MKEmitter.emit('cardAddElement', card.uuid, newcard)
  }
  addButton = () => {
    const { card } = this.state
    let newcard = {eleType: 'button', label: 'button', verify: null, show: 'link', sqlType: '', Ot: 'requiredSgl', OpenType: 'prompt', icon: '', class: 'primary', intertype: 'system', execSuccess: 'grid', execError: 'never', popClose: 'never'}
    newcard.uuid = Utils.getuuid()
    newcard.focus = true
    // 注册事件-添加元素
    MKEmitter.emit('cardAddElement', card.uuid, newcard)
  }
  changeStyle = () => {
    const { card, side } = this.state
    let _style = null
    let options = ['height', 'background', 'border', 'padding', 'margin', 'shadow']
    if (side === 'front') {
      _style = card.style ? fromJS(card.style).toJS() : {}
    } else if (side === 'back') {
      _style = card.backStyle ? fromJS(card.backStyle).toJS() : {}
      options = ['background', 'padding']
    }
    MKEmitter.emit('changeStyle', options, _style, this.getStyle)
  }
  getSettingForms = () => {
    const { cards } = this.props
    const { card, appType } = this.state
    let buttons = []
    card.elements && card.elements.forEach(item => {
      if (item.eleType === 'button') {
        buttons.push({
          value: item.uuid,
          label: item.label
        })
      }
    })
    if (appType !== 'mob' && card.backElements) {
      card.backElements.forEach(item => {
        if (item.eleType === 'button') {
          buttons.push({
            value: item.uuid,
            label: item.label
          })
        }
      })
    }
    return getSettingForm(card.setting, cards.subtype, buttons, card.$cardType, cards.columns)
  }
  updateSetting = (res) => {
    const { card, side, appType } = this.state
    if (appType === '' && res.menu) {
      let list = null
      try {
        list = JSON.parse(sessionStorage.getItem('thdMenuList')) || []
      } catch (e) {
        list = []
      }
      let id = res.menu[res.menu.length - 1]
      list.forEach(item => {
        if (item.MenuID === id) {
          res.MenuID = id
          res.MenuName = item.MenuName
          res.MenuNo = item.MenuNo
          res.tabType = item.type
        }
      })
    }
    let _card = {...card, setting: res}
    this.setState({
      card: _card
    })
    if (side === 'back' && res.type === 'simple') {
      this.setState({
        side: 'front',
        elements: fromJS(_card.elements).toJS()
      })
    }
    this.props.updateElement(_card)
  }
  paste = (element, resolve) => {
    const { card } = this.state
    let _uuid = Utils.getuuid()
    if (element.copyType === 'action') {
      element.eleType = 'button'
    }
    element.uuid = _uuid
    element.focus = true
    resolve({status: true})
    // 注册事件-添加元素
    MKEmitter.emit('cardAddElement', card.uuid, element)
  }
  doubleClickCard = () => {
    const { card } = this.state
    if (card.setting.click === 'menu' && card.setting.menu) {
      MKEmitter.emit('changeEditMenu', {MenuID: card.setting.menu})
    }
  }
  render() {
    const { cards } = this.props
    const { card } = this.state
    let _style = {...card.style}
    let _backStyle = {...card.backStyle}
    _backStyle.marginLeft = _style.marginLeft
    _backStyle.marginRight = _style.marginRight
    _backStyle.marginBottom = _style.marginBottom
    delete _style.marginBottom
    _style = resetStyle(_style)
    _backStyle = resetStyle(_backStyle)
    return (
      <Col span={card.setting.width || 24}>
        <div className={'card-item ' + (card.setting.btnControl || '')} style={_style} onDoubleClick={(e) => {e.stopPropagation(); this.doubleClickCard()}} id={card.uuid}>
          <div className="card-control" onDoubleClick={(e) => e.stopPropagation()}>
            <Popover overlayClassName="mk-popover-control-wrap" mouseLeaveDelay={0.2} mouseEnterDelay={0.2} content={
              <div className="mk-popover-control">
                <PlusOutlined className="plus" title="添加元素" onClick={this.addElement} />
                <PlusSquareOutlined className="plus" title="添加按钮" onClick={this.addButton} />
                <NormalForm title={'循环卡片设置'} width={950} update={this.updateSetting} getForms={this.getSettingForms}>
                  <EditOutlined className="edit" title="编辑"/>
                </NormalForm>
                <CopyComponent type="cardcell" card={card}/>
                <PasteController options={['action', 'customCardElement']} updateConfig={this.paste} />
                <FontColorsOutlined className="style" title="调整样式" onClick={this.changeStyle} />
              </div>
            } trigger="hover">
              <ToolOutlined />
            </Popover>
          </div>
          <CardCellComponent cards={cards} cardCell={card} side="" elements={card.elements} updateElement={this.updateCard}/>
        </div>
        <div className={'card-item ' + (card.setting.btnControl || '')} style={_backStyle} onDoubleClick={(e) => {e.stopPropagation(); this.doubleClickCard()}} id={card.uuid}>
          <div className="card-control" onDoubleClick={(e) => e.stopPropagation()}>
            <Popover overlayClassName="mk-popover-control-wrap" mouseLeaveDelay={0.2} mouseEnterDelay={0.2} content={
              <div className="mk-popover-control">
                <PlusOutlined className="plus" title="添加元素" onClick={this.addElement} />
                <PlusSquareOutlined className="plus" title="添加按钮" onClick={this.addButton} />
                <NormalForm title={'循环子卡片设置'} width={950} update={this.updateSetting} getForms={this.getSettingForms}>
                  <EditOutlined className="edit" title="编辑"/>
                </NormalForm>
                <CopyComponent type="cardcell" card={card}/>
                <PasteController options={['action', 'customCardElement']} updateConfig={this.paste} />
                <FontColorsOutlined className="style" title="调整样式" onClick={this.changeStyle} />
              </div>
            } trigger="hover">
              <ToolOutlined />
            </Popover>
          </div>
          <CardCellComponent cards={cards} cardCell={card} side="" elements={card.backElements} updateElement={this.updateCard}/>
        </div>
      </Col>
    )
  }
}
export default DoubleCardBoxComponent
src/menu/components/card/doublecardcomponent/index.scss
New file
@@ -0,0 +1,64 @@
.menus-detail-modal {
  .ant-modal {
    top: 70px;
  }
  .ant-modal-body {
    min-height: 200px;
    .menu-line {
      display: flex;
      div {
        padding: 16px 16px;
        border-top: 1px solid #e8e8e8;
        border-left: 1px solid #e8e8e8;
        word-break: break-all;
      }
      div:last-child {
        border-right: 1px solid #e8e8e8;
      }
      .sort {
        width: 10%;
        text-align: center;
      }
      .sign {
        width: 35%;
      }
      .name {
        width: 35%;
      }
      .action {
        width: 20%;
        text-align: center;
        span {
          display: inline-block;
          padding: 0 10px;
          cursor: pointer;
          color: #1890ff;
        }
      }
    }
    .menu-line:first-child {
      background-color: #fafafa;
    }
    .menu-line:last-child {
      div {
        border-bottom: 1px solid #e8e8e8;
      }
    }
  }
}
.card-control {
  position: absolute;
  top: 0px;
  left: 0px;
  .anticon-tool {
    position: absolute;
    left: 1px;
    top: 1px;
    padding: 1px;
    z-index: 2;
    font-size: 16px;
    cursor: pointer;
    background: rgba(255, 255, 255, 0.55);
  }
}
src/menu/components/card/doublecardcomponent/options.jsx
New file
@@ -0,0 +1,142 @@
/**
 * @description Setting表单配置信息
 */
export default function (setting, subtype, buttons = [], cardType, columns) {
  let appType = sessionStorage.getItem('appType')
  let menulist = []
  if (appType) {
    menulist = sessionStorage.getItem('appMenus')
    if (menulist) {
      try {
        menulist = JSON.parse(menulist)
      } catch (e) {
        menulist = []
      }
    } else {
      menulist = []
    }
  } else {
    menulist = sessionStorage.getItem('fstMenuList')
    if (menulist) {
      try {
        menulist = JSON.parse(menulist)
      } catch (e) {
        menulist = []
      }
    } else {
      menulist = []
    }
  }
  const cardSettingForm = [
    {
      type: 'number',
      field: 'width',
      label: '卡片宽度',
      initval: setting.width || 24,
      tooltip: '栅格布局,每行等分为24列。',
      min: 1,
      max: 24,
      precision: 0,
      required: true
    },
    {
      type: 'radio',
      field: 'click',
      label: '点击事件',
      initval: setting.click || '',
      required: false,
      options: [
        {value: '', label: '无'},
        {value: 'menu', label: '菜单'},
        {value: 'link', label: '链接'},
        {value: 'button', label: '按钮'}
      ],
      controlFields: [
        {field: 'menu', values: ['menu']},
        {field: 'linkurl', values: ['link']},
        {field: 'open', values: ['menu', 'link']},
        {field: 'joint', values: ['menu', 'link']},
        {field: 'linkbtn', values: ['button']},
        {field: 'clickType', values: ['button']},
      ]
    },
    {
      type: appType ? 'select' : 'cascader',
      field: 'menu',
      label: '关联菜单',
      initval: setting.menu || (appType ? '' : []),
      required: true,
      extendName: 'MenuNo',
      options: menulist,
    },
    {
      type: 'textarea',
      field: 'linkurl',
      label: '链接',
      initval: setting.linkurl || '',
      required: true,
      options: [],
      span: 24
    },
    {
      type: 'radio',
      field: 'open',
      label: '打开方式',
      initval: setting.open || 'blank',
      required: false,
      options: [
        {value: 'blank', label: appType !== 'mob' ? '新窗口' : '新页面'},
        {value: 'self', label: appType !== 'mob' ? '当前窗口' : '当前页面'},
      ],
      forbid: appType !== 'pc' && appType !== 'mob'
    },
    {
      type: 'radio',
      field: 'joint',
      label: '参数拼接',
      initval: setting.joint || 'true',
      required: false,
      options: [
        {value: 'true', label: '是'},
        {value: 'false', label: '否'},
      ],
    },
    {
      type: 'select',
      field: 'linkbtn',
      label: '关联按钮',
      initval: setting.linkbtn || '',
      required: true,
      options: buttons
    },
    {
      type: 'radio',
      field: 'clickType',
      label: '触发方式',
      initval: setting.clickType || 'normal',
      required: false,
      options: [
        {value: 'normal', label: '单击'},
        {value: 'multi', label: '双击'},
      ],
      forbid: appType === 'mob'
    },
    {
      type: 'radio',
      field: 'btnControl',
      label: '按钮控制',
      initval: setting.btnControl || 'show',
      tooltip: '可设置按钮显示规则,一直显示或鼠标悬浮时显示。',
      required: false,
      options: [
        {value: 'show', label: '正常显示'},
        {value: 'hover', label: '悬浮显示'},
      ],
      forbid: appType === 'mob'
    }
  ]
  return cardSettingForm
}
src/menu/datasource/index.jsx
@@ -198,6 +198,10 @@
      res.setting.maxScript = maxScript
      if (config.subtype !== 'dualdatacard') {
        delete res.subColumns
      }
      this.setState({loading: false, visible: false})
      this.props.updateConfig({...config, ...res})
    }, () => {
src/menu/datasource/verifycard/index.jsx
@@ -32,6 +32,7 @@
  state = {
    columns: [],
    subColumns: [],
    activeKey: 'setting',
    loading: false,
    initsql: '',          // sql验证时变量声明及赋值
@@ -197,6 +198,7 @@
    this.setState({
      scripts,
      columns: config.columns ? fromJS(config.columns).toJS() : [],
      subColumns: config.subColumns ? fromJS(config.subColumns).toJS() : [],
      setting: _setting,
      median: _setting,
      searches: search,
@@ -272,6 +274,26 @@
    values.uuid = Utils.getuuid()
    this.setState({ columns: [...columns, values] })
  }
  subColumnChange = (values, resolve) => {
    const { subColumns } = this.state
    let fields = subColumns.map(item => item.field.toLowerCase())
    if (fields.includes(values.field.toLowerCase())) {
      notification.warning({
        top: 92,
        message: '字段已存在!',
        duration: 5
      })
      return
    }
    resolve()
    values.uuid = Utils.getuuid()
    this.setState({ subColumns: [...subColumns, values] })
  }
  deleteScript = (record) => {
@@ -422,6 +444,23 @@
        loading: false
      })
      this.getdefaultSql()
    } else if (activeKey === 'subcolumns') {
      if (this.subdatasource && this.subdatasource.state.editingKey) {
        notification.warning({
          top: 92,
          message: '字段未保存,请保存后切换!',
          duration: 5
        })
        this.setState({
          loading: false
        })
        return
      }
      this.setState({
        activeKey: val,
        loading: false
      })
    } else if (activeKey === 'scripts') {
      let _loading = false
      if (this.scriptsForm && this.scriptsForm.state.editItem) {
@@ -514,7 +553,7 @@
  submitDataSource = () => {
    const { config, mainSearch } = this.props
    const { activeKey, setting, columns, scripts } = this.state
    const { activeKey, setting, columns, subColumns, scripts } = this.state
    return new Promise((resolve, reject) => {
      if (activeKey === 'setting') {
@@ -536,7 +575,7 @@
            defaultSearch: _search,
            setting: res
          }, () => {
            this.sqlverify(() => { resolve({setting: res, columns, scripts }) }, reject, false)
            this.sqlverify(() => { resolve({setting: res, columns, subColumns, scripts }) }, reject, false)
          })
        }, () => {
          reject()
@@ -551,7 +590,18 @@
          reject()
          return
        }
        this.sqlverify(() => { resolve({setting, columns, scripts }) }, reject, false)
        this.sqlverify(() => { resolve({setting, columns, subColumns, scripts }) }, reject, false)
      } else if (activeKey === 'subcolumns') {
        if (this.subdatasource && this.subdatasource.state.editingKey) {
          notification.warning({
            top: 92,
            message: '字段未保存,请保存后提交!',
            duration: 5
          })
          reject()
          return
        }
        this.sqlverify(() => { resolve({setting, columns, subColumns, scripts }) }, reject, false)
      } else if (activeKey === 'scripts') {
        let _loading = false
        if (this.scriptsForm && this.scriptsForm.state.editItem) {
@@ -570,7 +620,7 @@
          return
        }
        this.sqlverify(() => { resolve({setting, columns, scripts }) }, reject, false)
        this.sqlverify(() => { resolve({setting, columns, subColumns, scripts }) }, reject, false)
      }
    })
  }
@@ -701,6 +751,12 @@
    })
  }
  updateSubfields = (columns) => {
    this.setState({
      subColumns: columns
    })
  }
  copyDatasource = () => {
    const { config } = this.props
    const { columns, scripts } = this.state
@@ -823,6 +879,27 @@
    message.success('复制成功。')
  }
  copySubColumns = () => {
    const { subColumns } = this.state
    let m = []
    let n = []
    subColumns.forEach(col => {
      m.push(`${col.field}(${col.label})`)
      n.push(col.field)
    })
    let oInput = document.createElement('input')
    oInput.value = `/*${m.join(',')}*/
      ${n.join(',')}`
    document.body.appendChild(oInput)
    oInput.select()
    document.execCommand('Copy')
    document.body.removeChild(oInput)
    message.success('复制成功。')
  }
  /**
   * @description 组件销毁,清除state更新
   */
@@ -834,7 +911,7 @@
  render() {
    const { config } = this.props
    const { columns, median, setting, scripts, colColumns, scriptsColumns, activeKey, loading, searches, defaultsql, visible, pvisible, reload, script, scriptValue } = this.state
    const { columns, subColumns, median, setting, scripts, colColumns, scriptsColumns, activeKey, loading, searches, defaultsql, visible, pvisible, reload, script, scriptValue } = this.state
    return (
      <div className="model-data-source-wrap">
@@ -844,8 +921,6 @@
          <TabPane tab={
            <span>
              数据源
              {config.type !== 'interface' ? <CopyOutlined title="复制数据源" className="mk-copy-datasource" onClick={this.copyDatasource}/> : null}
              {config.type !== 'interface' ? <SnippetsOutlined title="导入数据源" className="mk-paste-datasource" onClick={() => this.setState({pvisible: true})}/> : null}
            </span>
          } key="setting">
            {!reload ? <SettingForm
@@ -861,21 +936,38 @@
            <span>
              字段集
              {columns.length ? <span className="count-tip">{columns.length}</span> : null}
              <CopyOutlined title="以逗号拼接形式复制字段" className="mk-copy-fields" onClick={this.copyColumns}/>
            </span>
          } key="columns">
            <ColForm columnChange={this.columnChange}/>
            <FieldsComponent
              config={{...config, columns}}
              config={{columns}}
              type="fields"
              updatefield={this.updatefields}
            />
            <EditTable actions={['edit', 'move', 'copy', 'del', 'clear']} type="datasourcefield" wrappedComponentRef={(inst) => this.datasource = inst} data={columns} columns={colColumns} onChange={(columns) => this.setState({columns})}/>
          </TabPane> : null}
          {config.subtype === 'dualdatacard' ? <TabPane tab={
            <span>
              子表字段集
              {subColumns.length ? <span className="count-tip">{subColumns.length}</span> : null}
            </span>
          } key="subcolumns">
            <ColForm columnChange={this.subColumnChange}/>
            <FieldsComponent
              config={{columns: subColumns}}
              type="fields"
              updatefield={this.updateSubfields}
            />
            <EditTable actions={['edit', 'move', 'copy', 'del', 'clear']} type="datasourcefield" wrappedComponentRef={(inst) => this.subdatasource = inst} data={subColumns} columns={colColumns} onChange={(subColumns) => this.setState({subColumns})}/>
          </TabPane> : null}
          <TabPane tab={
            <span>
              自定义脚本
              {scripts.length ? <span className="count-tip">{scripts.length}</span> : null}
              {config.type !== 'interface' && activeKey === 'setting' ? <CopyOutlined title="复制数据源" className="mk-copy-datasource" onClick={(e) => {e.stopPropagation();this.copyDatasource()}}/> : null}
              {config.type !== 'interface' && activeKey === 'setting' ? <SnippetsOutlined title="导入数据源" className="mk-paste-datasource" onClick={(e) => {e.stopPropagation();this.setState({pvisible: true})}}/> : null}
              {activeKey === 'columns' ? <CopyOutlined title="以逗号拼接形式复制字段" className="mk-copy-fields" onClick={(e) => {e.stopPropagation();this.copyColumns()}}/> : null}
              {activeKey === 'subcolumns' ? <CopyOutlined title="以逗号拼接形式复制字段" className="mk-copy-fields" onClick={(e) => {e.stopPropagation();this.copySubColumns()}}/> : null}
            </span>
          } key="scripts" disabled={median.interType !== 'system'} id="mk-scripts-tabpane">
            {scripts.length ? <BorderOutlined className="full-scripts" onClick={() => {
src/menu/datasource/verifycard/index.scss
@@ -14,9 +14,9 @@
      cursor: pointer;
      z-index: 1;
      top: 19px;
      right: -210px;
      right: -50px;
      color: rgb(24, 144, 255);
      display: none;
      display: inline-block;
      opacity: 0.4;
      transition: opacity 0.2s;
    }
@@ -25,9 +25,9 @@
      cursor: pointer;
      z-index: 1;
      top: 19px;
      right: -319px;
      right: -50px;
      color: rgb(24, 144, 255);
      display: none;
      display: inline-block;
      opacity: 0.4;
      transition: opacity 0.2s;
    }
@@ -36,19 +36,14 @@
      cursor: pointer;
      z-index: 1;
      top: 19px;
      right: -355px;
      right: -90px;
      color:purple;
      display: none;
      display: inline-block;
      opacity: 0.4;
      transition: opacity 0.2s;
    }
    .mk-copy-fields:hover, .mk-copy-datasource:hover, .mk-paste-datasource:hover {
      opacity: 1;
    }
    .ant-tabs-tab-active {
      .mk-copy-fields, .mk-copy-datasource, .mk-paste-datasource {
        display: inline-block;
      }
    }
    .count-tip {
      position: absolute;
src/menu/datasource/verifycard/settingform/index.jsx
@@ -460,7 +460,7 @@
              </Form.Item>
            </Col> : null}
            {/* 1、不分页且不存在上级模块 */}
            {!['navbar', 'interface'].includes(config.type) && !['editable', 'basetable'].includes(config.subtype) && (!config.pageable || (config.pageable && setting.laypage === 'false')) && (setting.supModule.length === 0 || setting.supModule[0] === 'empty') && setting.interType === 'system' ? <Col span={8}>
            {!['navbar', 'interface'].includes(config.type) && !['editable', 'basetable', 'dualdatacard'].includes(config.subtype) && (!config.pageable || (config.pageable && setting.laypage === 'false')) && (setting.supModule.length === 0 || setting.supModule[0] === 'empty') && setting.interType === 'system' ? <Col span={8}>
              <Form.Item label={
                <Tooltip placement="topLeft" title={'初始化加载时,是否与其他组件一同加载数据,注:仅在使用系统函数,且初始化加载数据时有效,分页请求时无效。'}>
                  <QuestionCircleOutlined className="mk-form-tip" />
src/menu/menushell/card.jsx
@@ -11,6 +11,7 @@
const AntvScatter = asyncComponent(() => import('@/menu/components/chart/antv-scatter'))
const AntvTabs = asyncComponent(() => import('@/menu/components/tabs/antv-tabs'))
const DataCard = asyncComponent(() => import('@/menu/components/card/data-card'))
const DoubleDataCard = asyncComponent(() => import('@/menu/components/card/double-data-card'))
const PropCard = asyncComponent(() => import('@/menu/components/card/prop-card'))
const Balcony = asyncComponent(() => import('@/menu/components/card/balcony'))
const NormalTree = asyncComponent(() => import('@/menu/components/tree/antd-tree'))
@@ -78,6 +79,8 @@
      return (<AntvBar card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
    } else if (card.type === 'search') {
      return (<MainSearch card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
    } else if (card.type === 'card' && card.subtype === 'dualdatacard') {
      return (<DoubleDataCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
    } else if (card.type === 'pie') {
      return (<AntvPie card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
    } else if (card.type === 'dashboard') {
src/menu/modulesource/option.jsx
@@ -40,6 +40,7 @@
  { type: 'menu', url: card1, component: 'card', subtype: 'datacard', title: '数据卡', width: 24 },
  { type: 'menu', url: card2, component: 'card', subtype: 'propcard', title: '属性卡', width: 24 },
  { type: 'menu', url: card2, component: 'balcony', subtype: 'balcony', title: '浮动卡', width: 24},
  // { type: 'menu', url: card1, component: 'card', subtype: 'dualdatacard', title: '双重数据卡', width: 24 },
  { type: 'menu', url: simpleform, component: 'form', subtype: 'simpleform', title: '表单', width: 24, forbid: ['billPrint'] },
  { type: 'menu', url: form, component: 'form', subtype: 'stepform', title: '表单(分步)', width: 24, forbid: ['billPrint'] },
  { type: 'menu', url: tabForm, component: 'form', subtype: 'tabform', title: '表单(tab页)', width: 24, forbid: ['billPrint'] },
src/tabviews/custom/components/module/voucher/index.jsx
@@ -82,8 +82,8 @@
      }
    }
    config.wrap.type = 'checkVoucher'
    BID = '20230214130744811P0K95RQ155KG0QIQOFV'
    // config.wrap.type = 'checkVoucher'
    // BID = '20230214130744811P0K95RQ155KG0QIQOFV'
    // config.wrap.type = 'checkTemp'
    // BID = '20230214174458780MFR8IA576ON4VKNOLVH'
src/tabviews/zshare/actionList/normalbutton/index.jsx
@@ -2424,6 +2424,12 @@
        this.setState({
          visible: true
        })
        if (btnconfig.setting.display === 'modal' && btnconfig.setting.moveable === 'true') {
          setTimeout(() => {
            this.setMove()
          }, 100)
        }
      }
    } else {
      Api.getCacheConfig({
@@ -2677,15 +2683,17 @@
        width = btnconfig.setting.width > 100 ? btnconfig.setting.width : btnconfig.setting.width + '%'
        container = () => document.getElementById(btn.ContainerId)
      }
      return (
        <Modal
          title={title}
          maskClosable={clickouter}
          getContainer={container}
          wrapClassName='action-modal'
          wrapClassName={'action-modal' + (btnconfig.setting.moveable === 'true' ? ' moveable-modal modal-' + btn.uuid : '')}
          visible={visible}
          width={width}
          onOk={this.handleOk}
          maskStyle={btnconfig.setting.moveable === 'true' ?  {backgroundColor: 'rgba(0, 0, 0, 0.15)'} : null}
          confirmLoading={this.state.confirmLoading}
          onCancel={this.handleCancel}
          destroyOnClose
@@ -2704,6 +2712,43 @@
    }
  }
  setMove = () => {
    const { btn } = this.props
    let wrap = document.getElementsByClassName('modal-' + btn.uuid)[0]
    if (!wrap) return
    let node = wrap.getElementsByClassName('ant-modal-header')[0]
    if (!node) return
    node.onmousedown = function(e) {
      let orileft = 0
      let oritop = 0
      let oleft = e.clientX
      let otop = e.clientY
      if (node.parentNode.style.left) {
        orileft = parseFloat(node.parentNode.style.left)
      }
      if (node.parentNode.style.top) {
        oritop = parseFloat(node.parentNode.style.top)
      }
      document.onmousemove = function(e) {
        let left = e.clientX - oleft
        let top = e.clientY - otop
        node.parentNode.style.left = (orileft + left) + 'px'
        node.parentNode.style.top = (oritop + top) + 'px'
      }
    }
    document.onmouseup = function() {
      document.onmousemove = null
    }
  }
  render() {
    const { btn } = this.props
    const { loadingNumber, loadingTotal, loading, disabled, hidden, check } = this.state
src/tabviews/zshare/actionList/printbutton/index.jsx
@@ -2013,6 +2013,12 @@
        this.setState({
          visible: true
        })
        if (btnconfig.setting.display === 'modal' && btnconfig.setting.moveable === 'true') {
          setTimeout(() => {
            this.setMove()
          }, 100)
        }
      }
    } else {
      Api.getCacheConfig({
@@ -2208,10 +2214,11 @@
        title={title}
        maskClosable={clickouter}
        getContainer={container}
        wrapClassName='action-modal'
        wrapClassName={'action-modal' + (btnconfig.setting.moveable === 'true' ? ' moveable-modal modal-' + btn.uuid : '')}
        visible={this.state.visible}
        confirmLoading={this.state.confirmLoading}
        width={width}
        maskStyle={btnconfig.setting.moveable === 'true' ?  {backgroundColor: 'rgba(0, 0, 0, 0.15)'} : null}
        onOk={this.handleOk}
        onCancel={this.handleCancel}
        destroyOnClose
@@ -2229,6 +2236,43 @@
    )
  }
  setMove = () => {
    const { btn } = this.props
    let wrap = document.getElementsByClassName('modal-' + btn.uuid)[0]
    if (!wrap) return
    let node = wrap.getElementsByClassName('ant-modal-header')[0]
    if (!node) return
    node.onmousedown = function(e) {
      let orileft = 0
      let oritop = 0
      let oleft = e.clientX
      let otop = e.clientY
      if (node.parentNode.style.left) {
        orileft = parseFloat(node.parentNode.style.left)
      }
      if (node.parentNode.style.top) {
        oritop = parseFloat(node.parentNode.style.top)
      }
      document.onmousemove = function(e) {
        let left = e.clientX - oleft
        let top = e.clientY - otop
        node.parentNode.style.left = (orileft + left) + 'px'
        node.parentNode.style.top = (oritop + top) + 'px'
      }
    }
    document.onmouseup = function() {
      document.onmousemove = null
    }
  }
  render() {
    const { btn } = this.props
    const { loading, disabled, hidden } = this.state
src/templates/modalconfig/settingform/index.jsx
@@ -111,7 +111,12 @@
            </Form.Item>
          </Col> */}
          <Col span={24}>
            <Form.Item label="显示方式">
            <Form.Item label={
              <Tooltip placement="topLeft" title="可选择表单的显示形式,注:标签打印按钮,暂不支持以抽屉显示表单。">
                <QuestionCircleOutlined className="mk-form-tip" />
                显示方式
              </Tooltip>
            }>
              {getFieldDecorator('display', {
                initialValue: display || 'modal'
              })(
@@ -335,6 +340,23 @@
              )}
            </Form.Item>
          </Col> : null}
          {!appType && display === 'modal' ? <Col span={12}>
            <Form.Item label={
              <Tooltip placement="topLeft" title="是否可拖拽移动模态框。">
                <QuestionCircleOutlined className="mk-form-tip" />
                可移动
              </Tooltip>
            }>
              {getFieldDecorator('moveable', {
                initialValue: config.setting.moveable || 'false'
              })(
                <Radio.Group>
                  <Radio value="false">否</Radio>
                  <Radio value="true">是</Radio>
                </Radio.Group>
              )}
            </Form.Item>
          </Col> : null}
        </Row>
      </Form>
    )