From 4b5b5a0f13adceaa4a759580b5dc4ac3179f884d Mon Sep 17 00:00:00 2001
From: king <18310653075@163.com>
Date: 星期三, 10 三月 2021 18:26:29 +0800
Subject: [PATCH] 2021-03-10

---
 src/pc/modulesource/option.jsx                                            |    3 
 src/menu/components/carousel/prop-card/index.scss                         |   87 +
 src/menu/components/card/cardcomponent/settingform/index.jsx              |  119 ++
 src/menu/components/carousel/cardcomponent/settingform/index.scss         |   11 
 src/menu/components/carousel/data-card/wrapsetting/index.scss             |    7 
 src/tabviews/custom/components/carousel/data-card/index.scss              |   49 +
 src/pc/menushell/index.jsx                                                |    1 
 src/menu/components/carousel/cardcomponent/index.scss                     |    0 
 src/menu/components/carousel/data-card/wrapsetting/index.jsx              |   83 +
 src/tabviews/custom/components/carousel/data-card/index.jsx               |  357 +++++++
 src/tabviews/custom/components/carousel/prop-card/index.scss              |   42 
 src/menu/components/tabs/tabcomponents/index.jsx                          |    1 
 src/menu/menushell/card.jsx                                               |    6 
 src/menu/stylecombcontroller/index.jsx                                    |    2 
 src/menu/components/carousel/cardcomponent/index.jsx                      |  229 ++++
 src/tabviews/custom/components/card/data-card/index.scss                  |    3 
 src/menu/components/carousel/data-card/index.jsx                          |  287 +++++
 src/views/menudesign/index.jsx                                            |    9 
 src/menu/modulesource/option.jsx                                          |    3 
 src/tabviews/custom/components/card/data-card/index.jsx                   |  100 +
 src/tabviews/custom/components/card/prop-card/index.scss                  |    3 
 src/menu/components/carousel/cardcomponent/settingform/index.jsx          |  194 +++
 src/tabviews/custom/components/card/prop-card/index.jsx                   |   99 +
 src/menu/components/carousel/data-card/wrapsetting/settingform/index.scss |   11 
 src/assets/mobimg/carousel.png                                            |    0 
 src/menu/components/card/cardcellcomponent/index.jsx                      |    2 
 src/menu/stylecontroller/index.jsx                                        |    4 
 src/tabviews/custom/components/carousel/cardItem/index.jsx                |   56 +
 src/pc/menushell/card.jsx                                                 |    6 
 src/menu/components/carousel/prop-card/index.jsx                          |  369 +++++++
 src/menu/components/card/cardcomponent/settingform/index.scss             |    8 
 src/menu/components/carousel/data-card/wrapsetting/settingform/index.jsx  |  214 ++++
 src/tabviews/custom/components/carousel/prop-card/index.jsx               |  363 +++++++
 src/tabviews/custom/index.jsx                                             |   16 
 src/tabviews/custom/components/carousel/cardItem/index.scss               |   58 +
 src/menu/components/card/prop-card/index.jsx                              |    2 
 src/menu/components/carousel/data-card/index.scss                         |   87 +
 src/menu/components/tabs/tabcomponents/card.jsx                           |    6 
 src/menu/menushell/index.jsx                                              |    1 
 39 files changed, 2,884 insertions(+), 14 deletions(-)

diff --git a/src/assets/mobimg/carousel.png b/src/assets/mobimg/carousel.png
new file mode 100644
index 0000000..58750da
--- /dev/null
+++ b/src/assets/mobimg/carousel.png
Binary files differ
diff --git a/src/menu/components/card/cardcellcomponent/index.jsx b/src/menu/components/card/cardcellcomponent/index.jsx
index 286bed6..c37edfa 100644
--- a/src/menu/components/card/cardcellcomponent/index.jsx
+++ b/src/menu/components/card/cardcellcomponent/index.jsx
@@ -152,7 +152,7 @@
     const { cards, cardCell } = this.props
     const { card, elements } = this.state
 
-    if (comIds.length !== 3 || comIds[0] !== cards.uuid || comIds[1] !== cardCell.uuid) return
+    if (comIds.length !== 3 || comIds[0] !== cards.uuid || comIds[1] !== cardCell.uuid || !card) return
 
     let _card = this.resetCardStyle(card, style)
 
diff --git a/src/menu/components/card/cardcomponent/settingform/index.jsx b/src/menu/components/card/cardcomponent/settingform/index.jsx
index 4045ba3..738bd50 100644
--- a/src/menu/components/card/cardcomponent/settingform/index.jsx
+++ b/src/menu/components/card/cardcomponent/settingform/index.jsx
@@ -1,8 +1,10 @@
 import React, {Component} from 'react'
 import PropTypes from 'prop-types'
-import { Form, Row, Col, Radio, Tooltip, Icon, Input, InputNumber, Select } from 'antd'
+import { Form, Row, Col, Radio, Tooltip, Icon, Input, InputNumber, Select, Cascader } from 'antd'
 
 import './index.scss'
+
+const { TextArea } = Input
 
 class SettingForm extends Component {
   static propTpyes = {
@@ -13,7 +15,32 @@
   }
 
   state = {
-    type: this.props.setting.type || 'simple'
+    type: this.props.setting.type || 'simple',
+    click: this.props.setting.click || '',
+    isApp: sessionStorage.getItem('appType') === 'pc',
+    menulist: []
+  }
+
+  UNSAFE_componentWillMount() {
+    const { isApp } = this.state
+    let menulist = null
+
+    if (isApp) {
+      menulist = sessionStorage.getItem('appMenus')
+    } else {
+      menulist = sessionStorage.getItem('fstMenuList')
+    }
+
+    if (menulist) {
+      try {
+        menulist = JSON.parse(menulist)
+      } catch {
+        menulist = []
+      }
+    } else {
+      menulist = []
+    }
+    this.setState({menulist})
   }
 
   handleConfirm = () => {
@@ -40,6 +67,7 @@
   render() {
     const { setting, cards } = this.props
     const { getFieldDecorator } = this.props.form
+    const { menulist, click, isApp } = this.state
 
     const formItemLayout = {
       labelCol: {
@@ -126,6 +154,93 @@
                 })(<Input placeholder="" autoComplete="off" onPressEnter={this.handleSubmit}/>)}
               </Form.Item>
             </Col> : null}
+            <Col span={12}>
+              <Form.Item label="鐐瑰嚮浜嬩欢">
+                {getFieldDecorator('click', {
+                  initialValue: click
+                })(
+                  <Radio.Group onChange={(e) => this.setState({click: e.target.value})}>
+                    <Radio value="">鏃�</Radio>
+                    <Radio value="menu">鑿滃崟</Radio>
+                    <Radio value="link">閾炬帴</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col>
+            {!isApp && click === 'menu' ? <Col span={12}>
+              <Form.Item label="鑿滃崟">
+                {getFieldDecorator('menu', {
+                  initialValue: setting.menu || [],
+                  rules: [
+                    {
+                      required: true,
+                      message: this.props.dict['form.required.select'] + '鑿滃崟!'
+                    }
+                  ]
+                })(
+                  <Cascader options={menulist} placeholder=""/>
+                )}
+              </Form.Item>
+            </Col> : null}
+            {isApp && click === 'menu' ? <Col span={12}>
+              <Form.Item label="鍏宠仈鑿滃崟">
+                {getFieldDecorator('menu', {
+                  initialValue: setting.menu || '',
+                  rules: [
+                    {
+                      required: true,
+                      message: this.props.dict['form.required.select'] + '鍏宠仈鑿滃崟!'
+                    }
+                  ]
+                })(
+                  <Select
+                    showSearch
+                    filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
+                  >
+                    {menulist.map(option =>
+                      <Select.Option key={option.MenuID} value={option.MenuID}>{option.MenuName}</Select.Option>
+                    )}
+                  </Select>
+                )}
+              </Form.Item>
+            </Col> : null}
+            {click === 'link' ? <Col span={24} className="textarea">
+              <Form.Item label="閾炬帴">
+                {getFieldDecorator('linkurl', {
+                  initialValue: setting.linkurl || '',
+                  rules: [
+                    {
+                      required: true,
+                      message: this.props.dict['form.required.input'] + '閾炬帴!'
+                    }
+                  ]
+                })( <TextArea rows={2}/> )}
+              </Form.Item>
+            </Col> : null}
+            {isApp ? <Col span={12}>
+              <Form.Item label="鎵撳紑鏂瑰紡">
+                {getFieldDecorator('open', {
+                  initialValue: setting.open || 'blank'
+                })(
+                  <Radio.Group>
+                    <Radio value="blank">鏂扮獥鍙�</Radio>
+                    <Radio value="self">褰撳墠绐楀彛</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col> : null}
+            {click !== '' ? <Col span={12}>
+              <Form.Item label="鍙傛暟鎷兼帴">
+                {getFieldDecorator('joint', {
+                  initialValue: setting.joint || 'true'
+                })(
+                  <Radio.Group>
+                    <Radio value="true">鏄�</Radio>
+                    <Radio value="false">鍚�</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col> : null}
           </Row>
         </Form>
       </div>
diff --git a/src/menu/components/card/cardcomponent/settingform/index.scss b/src/menu/components/card/cardcomponent/settingform/index.scss
index 159130b..8898344 100644
--- a/src/menu/components/card/cardcomponent/settingform/index.scss
+++ b/src/menu/components/card/cardcomponent/settingform/index.scss
@@ -8,4 +8,12 @@
   .ant-input-number {
     width: 100%;
   }
+  .textarea {
+    .ant-form-item-label {
+      width: 16%;
+    }
+    .ant-form-item-control-wrapper {
+      width: 84%;
+    }
+  }
 }
\ No newline at end of file
diff --git a/src/menu/components/card/prop-card/index.jsx b/src/menu/components/card/prop-card/index.jsx
index 04b042a..bf52a3c 100644
--- a/src/menu/components/card/prop-card/index.jsx
+++ b/src/menu/components/card/prop-card/index.jsx
@@ -55,7 +55,7 @@
         name: card.name,
         subtype: card.subtype,
         setting: { interType: 'system' },
-        wrap: { name: card.name, width: card.width || 24, title: '', addable: 'false', switch: 'false', datatype: 'static' },
+        wrap: { name: card.name, width: card.width || 24, title: '', switch: 'false', datatype: 'static' },
         style: { marginLeft: '0px', marginRight: '0px', marginTop: '8px', marginBottom: '8px' },
         headerStyle: { fontSize: '16px', borderBottomWidth: '1px', borderBottomColor: '#e8e8e8' },
         columns: [],
diff --git a/src/menu/components/carousel/cardcomponent/index.jsx b/src/menu/components/carousel/cardcomponent/index.jsx
new file mode 100644
index 0000000..5a3909e
--- /dev/null
+++ b/src/menu/components/carousel/cardcomponent/index.jsx
@@ -0,0 +1,229 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import { is, fromJS } from 'immutable'
+import { Modal, Popover, Icon } from 'antd'
+
+import asyncComponent from '@/utils/asyncComponent'
+import asyncIconComponent from '@/utils/asyncIconComponent'
+import zhCN from '@/locales/zh-CN/model.js'
+import enUS from '@/locales/en-US/model.js'
+import SettingForm from './settingform'
+
+import Utils from '@/utils/utils.js'
+import MKEmitter from '@/utils/events.js'
+import './index.scss'
+
+const CardCellComponent = asyncComponent(() => import('@/menu/components/card/cardcellcomponent'))
+const CopyComponent = asyncIconComponent(() => import('@/menu/components/share/copycomponent'))
+
+class CardBoxComponent extends Component {
+  static propTpyes = {
+    cards: PropTypes.object,         // 鍗$墖琛岄厤缃俊鎭�
+    card: PropTypes.object,          // 鍗$墖閰嶇疆淇℃伅
+    move: PropTypes.func,            // 鍗$墖绉诲姩
+    deleteElement: PropTypes.func,   // 鍗$墖鍒犻櫎
+    updateElement: PropTypes.func    // 鑿滃崟閰嶇疆鏇存柊
+  }
+
+  state = {
+    dict: sessionStorage.getItem('lang') !== 'en-US' ? zhCN : enUS,
+    card: null,            // 鍗$墖淇℃伅锛屽寘鎷鍙嶉潰
+    formlist: null,        // 璁剧疆琛ㄥ崟淇℃伅
+    elements: null,        // 缂栬緫缁�
+    visible: false,        // 妯℃�佹鎺у埗
+    settingVisible: false,
+  }
+
+  /**
+   * @description 鎼滅储鏉′欢鍒濆鍖�
+   */
+  UNSAFE_componentWillMount () {
+    const { card } = this.props
+
+    this.setState({
+      card: fromJS(card).toJS(),
+      elements: fromJS(card.elements).toJS(),
+    })
+  }
+
+  componentDidMount () {
+    MKEmitter.addListener('submitStyle', this.getStyle)
+  }
+
+  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
+    }
+    MKEmitter.removeListener('submitStyle', this.getStyle)
+  }
+
+  getStyle = (comIds, style) => {
+    const { cards } = this.props
+    const { card } = this.state
+
+    if (comIds.length !== 2 || comIds[0] !== cards.uuid || comIds[1] !== card.uuid) return
+
+    let _card = fromJS(card).toJS()
+    _card.style = style
+
+    this.setState({
+      card: _card
+    })
+
+    this.props.updateElement(_card)
+  }
+
+  updateCard = (elements) => {
+    const { card } = this.state
+
+    let _card = {...card, elements: elements}
+
+    this.setState({
+      card: _card
+    })
+
+    this.props.updateElement(_card)
+  }
+
+  addElement = () => {
+    const { cards } = this.props
+    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', [cards.uuid, card.uuid], newcard)
+  }
+
+  addButton = () => {
+    const { cards } = this.props
+    const { card } = this.state
+
+    let newcard = {}
+    newcard.uuid = Utils.getuuid()
+    newcard.focus = true
+    
+    newcard.eleType = 'button'
+    newcard.label = 'button'
+    newcard.sqlType = ''
+    newcard.Ot = 'requiredSgl'
+    newcard.OpenType = 'prompt'
+    newcard.icon = ''
+    newcard.class = 'primary'
+    newcard.intertype = 'system'
+    newcard.execSuccess = 'grid'
+    newcard.execError = 'never'
+    newcard.popClose = 'never'
+    newcard.errorTime = 10
+    newcard.verify = null
+    newcard.show = 'link'
+
+    // 娉ㄥ唽浜嬩欢-娣诲姞鍏冪礌
+    MKEmitter.emit('cardAddElement', [cards.uuid, card.uuid], newcard)
+  }
+
+  changeStyle = () => {
+    const { cards } = this.props
+    const { card } = this.state
+
+    let options = ['background', 'border', 'padding', 'margin', 'shadow']
+
+    MKEmitter.emit('changeStyle', [cards.uuid, card.uuid], options, fromJS(card.style).toJS())
+  }
+
+  settingSubmit = () => {
+    const { card } = this.state
+
+    this.settingRef.handleConfirm().then(res => {
+      this.setState({
+        settingVisible: false,
+        card: {...card, setting: res}
+      })
+
+      this.props.updateElement({...card, setting: res})
+    })
+  }
+
+  clickComponent = (e) => {
+    if ((sessionStorage.getItem('style-control') === 'true' || sessionStorage.getItem('style-control') === 'propcard') && this.props.cards.subtype === 'propcard') {
+      e.stopPropagation()
+      MKEmitter.emit('clickComponent', this.state.card, this.props.cards, 'propcard')
+    }
+  }
+
+  render() {
+    const { cards } = this.props
+    const { card, elements, settingVisible, dict } = this.state
+
+    let _style = {...card.style}
+
+    if (_style.shadow) {
+      _style.boxShadow = '0 0 4px ' + _style.shadow
+    }
+    _style.height = cards.style.height
+
+    return (
+      <div className="card-item" style={_style} onClick={this.clickComponent} id={card.uuid}>
+        <CardCellComponent cards={cards} cardCell={card} side="front" elements={elements} updateElement={this.updateCard}/>
+        <div className="card-control">
+          <Popover overlayClassName="mk-popover-control-wrap" mouseLeaveDelay={0.2} mouseEnterDelay={0.2} content={
+            <div className="mk-popover-control">
+              <Icon className="plus" title="娣诲姞鍏冪礌" onClick={this.addElement} type="plus" />
+              <Icon className="plus" title="娣诲姞鎸夐挳" onClick={this.addButton} type="plus-square" />
+              <Icon className="edit" title="缂栬緫" type="edit" onClick={() => this.setState({settingVisible: true})} />
+              <CopyComponent type="cardcell" card={card}/>
+              <Icon className="style" title="璋冩暣鏍峰紡" onClick={this.changeStyle} type="font-colors" />
+              {cards.subtype === 'propcard' ? <Popover overlayClassName="mk-popover-control-wrap" mouseLeaveDelay={0.2} mouseEnterDelay={0.2} content={
+                <div className="mk-popover-control">
+                  <Icon className="plus" title="宸︾Щ" type="arrow-left" onClick={() => this.props.move(card, 'left')} />
+                  <Icon className="close" title="鍙崇Щ" type="arrow-right" onClick={() => this.props.move(card, 'right')} />
+                </div>
+              } trigger="hover" getPopupContainer={() => document.getElementById(card.uuid + 'swap')}>
+                <Icon type="swap" id={card.uuid + 'swap'}/>
+              </Popover> : null}
+              {cards.subtype === 'propcard' ? <Icon className="close" title="鍒犻櫎鍗$墖" type="delete" onClick={() => this.props.deleteElement(card)} /> : null}
+            </div>
+          } trigger="hover">
+            <Icon type="tool" />
+          </Popover>
+        </div>
+        <Modal
+          wrapClassName="popview-modal"
+          title={'鍗$墖璁剧疆'}
+          visible={settingVisible}
+          width={700}
+          maskClosable={false}
+          okText={dict['model.submit']}
+          onOk={this.settingSubmit}
+          onCancel={() => { this.setState({ settingVisible: false }) }}
+          destroyOnClose
+        >
+          <SettingForm
+            dict={dict}
+            cards={cards}
+            setting={card.setting}
+            inputSubmit={this.settingSubmit}
+            wrappedComponentRef={(inst) => this.settingRef = inst}
+          />
+        </Modal>
+      </div>
+    )
+  }
+}
+
+export default CardBoxComponent
\ No newline at end of file
diff --git a/src/menu/components/carousel/cardcomponent/index.scss b/src/menu/components/carousel/cardcomponent/index.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/menu/components/carousel/cardcomponent/index.scss
diff --git a/src/menu/components/carousel/cardcomponent/settingform/index.jsx b/src/menu/components/carousel/cardcomponent/settingform/index.jsx
new file mode 100644
index 0000000..d8046cc
--- /dev/null
+++ b/src/menu/components/carousel/cardcomponent/settingform/index.jsx
@@ -0,0 +1,194 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import { Form, Row, Col, Radio, Tooltip, Icon, Input, Cascader, Select } from 'antd'
+
+import './index.scss'
+
+const { TextArea } = Input
+
+class SettingForm extends Component {
+  static propTpyes = {
+    dict: PropTypes.object,      // 瀛楀吀椤�
+    cards: PropTypes.object,     // 鍗$墖闆�
+    setting: PropTypes.object,   // 鏁版嵁婧愰厤缃�
+    inputSubmit: PropTypes.func  // 鍥炶溅浜嬩欢
+  }
+
+  state = {
+    type: this.props.setting.type || 'simple',
+    click: this.props.setting.click || '',
+    isApp: sessionStorage.getItem('appType') === 'pc',
+    menulist: []
+  }
+
+  UNSAFE_componentWillMount() {
+    const { isApp } = this.state
+    let menulist = null
+
+    if (isApp) {
+      menulist = sessionStorage.getItem('appMenus')
+    } else {
+      menulist = sessionStorage.getItem('fstMenuList')
+    }
+
+    if (menulist) {
+      try {
+        menulist = JSON.parse(menulist)
+      } catch {
+        menulist = []
+      }
+    } else {
+      menulist = []
+    }
+
+    this.setState({menulist})
+  }
+
+  handleConfirm = () => {
+    // 琛ㄥ崟鎻愪氦鏃舵鏌ヨ緭鍏ュ�兼槸鍚︽纭�
+    return new Promise((resolve, reject) => {
+      this.props.form.validateFieldsAndScroll((err, values) => {
+        if (!err) {
+          resolve(values)
+        } else {
+          reject(err)
+        }
+      })
+    })
+  }
+
+  handleSubmit = (e) => {
+    e.preventDefault()
+
+    if (this.props.inputSubmit) {
+      this.props.inputSubmit()
+    }
+  }
+
+  render() {
+    const { setting, cards } = this.props
+    const { getFieldDecorator } = this.props.form
+    const { click, menulist, isApp } = this.state
+
+    const formItemLayout = {
+      labelCol: {
+        xs: { span: 24 },
+        sm: { span: 8 }
+      },
+      wrapperCol: {
+        xs: { span: 24 },
+        sm: { span: 16 }
+      }
+    }
+
+    return (
+      <div className="model-menu-setting-form">
+        <Form {...formItemLayout}>
+          <Row gutter={24}>
+            {cards.subtype === 'propcard' ? <Col span={12}>
+              <Form.Item label={
+                <Tooltip placement="topLeft" title="鍗$墖鐐瑰嚮鏃讹紝鍚戝叾浠栫粍浠朵紶閫掔殑BID鍊笺��">
+                  <Icon type="question-circle" />
+                  涓婚敭鍊�
+                </Tooltip>
+              }>
+                {getFieldDecorator('primaryId', {
+                  initialValue: setting.primaryId || ''
+                })(<Input placeholder="" autoComplete="off" onPressEnter={this.handleSubmit}/>)}
+              </Form.Item>
+            </Col> : null}
+            <Col span={12}>
+              <Form.Item label="鐐瑰嚮浜嬩欢">
+                {getFieldDecorator('click', {
+                  initialValue: click
+                })(
+                  <Radio.Group onChange={(e) => this.setState({click: e.target.value})}>
+                    <Radio value="">鏃�</Radio>
+                    <Radio value="menu">鑿滃崟</Radio>
+                    <Radio value="link">閾炬帴</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col>
+            {!isApp && click === 'menu' ? <Col span={12}>
+              <Form.Item label="鑿滃崟">
+                {getFieldDecorator('menu', {
+                  initialValue: setting.menu || [],
+                  rules: [
+                    {
+                      required: true,
+                      message: this.props.dict['form.required.select'] + '鑿滃崟!'
+                    }
+                  ]
+                })(
+                  <Cascader options={menulist} placeholder=""/>
+                )}
+              </Form.Item>
+            </Col> : null}
+            {isApp && click === 'menu' ? <Col span={12}>
+              <Form.Item label="鍏宠仈鑿滃崟">
+                {getFieldDecorator('menu', {
+                  initialValue: setting.menu || '',
+                  rules: [
+                    {
+                      required: true,
+                      message: this.props.dict['form.required.select'] + '鍏宠仈鑿滃崟!'
+                    }
+                  ]
+                })(
+                  <Select
+                    showSearch
+                    filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
+                  >
+                    {menulist.map(option =>
+                      <Select.Option key={option.MenuID} value={option.MenuID}>{option.MenuName}</Select.Option>
+                    )}
+                  </Select>
+                )}
+              </Form.Item>
+            </Col> : null}
+            {click === 'link' ? <Col span={24} className="textarea">
+              <Form.Item label="閾炬帴">
+                {getFieldDecorator('linkurl', {
+                  initialValue: setting.linkurl || '',
+                  rules: [
+                    {
+                      required: true,
+                      message: this.props.dict['form.required.input'] + '閾炬帴!'
+                    }
+                  ]
+                })( <TextArea rows={2}/> )}
+              </Form.Item>
+            </Col> : null}
+            {isApp ? <Col span={12}>
+              <Form.Item label="鎵撳紑鏂瑰紡">
+                {getFieldDecorator('open', {
+                  initialValue: setting.open || 'blank'
+                })(
+                  <Radio.Group>
+                    <Radio value="blank">鏂扮獥鍙�</Radio>
+                    <Radio value="self">褰撳墠绐楀彛</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col> : null}
+            {click !== '' ? <Col span={12}>
+              <Form.Item label="鍙傛暟鎷兼帴">
+                {getFieldDecorator('joint', {
+                  initialValue: setting.joint || 'true'
+                })(
+                  <Radio.Group>
+                    <Radio value="true">鏄�</Radio>
+                    <Radio value="false">鍚�</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col> : null}
+          </Row>
+        </Form>
+      </div>
+    )
+  }
+}
+
+export default Form.create()(SettingForm)
\ No newline at end of file
diff --git a/src/menu/components/carousel/cardcomponent/settingform/index.scss b/src/menu/components/carousel/cardcomponent/settingform/index.scss
new file mode 100644
index 0000000..159130b
--- /dev/null
+++ b/src/menu/components/carousel/cardcomponent/settingform/index.scss
@@ -0,0 +1,11 @@
+.model-menu-setting-form {
+  position: relative;
+
+  .anticon-question-circle {
+    color: #c49f47;
+    margin-right: 3px;
+  }
+  .ant-input-number {
+    width: 100%;
+  }
+}
\ No newline at end of file
diff --git a/src/menu/components/carousel/data-card/index.jsx b/src/menu/components/carousel/data-card/index.jsx
new file mode 100644
index 0000000..b96cfe1
--- /dev/null
+++ b/src/menu/components/carousel/data-card/index.jsx
@@ -0,0 +1,287 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { is, fromJS } from 'immutable'
+import { Icon, Popover, Modal, notification } from 'antd'
+
+import asyncComponent from '@/utils/asyncComponent'
+import asyncIconComponent from '@/utils/asyncIconComponent'
+
+import MKEmitter from '@/utils/events.js'
+import Utils from '@/utils/utils.js'
+import zhCN from '@/locales/zh-CN/model.js'
+import enUS from '@/locales/en-US/model.js'
+import './index.scss'
+
+const SettingComponent = asyncIconComponent(() => import('@/menu/datasource'))
+const WrapComponent = asyncIconComponent(() => import('./wrapsetting'))
+const CardComponent = asyncComponent(() => import('../cardcomponent'))
+const LogComponent = asyncIconComponent(() => import('@/menu/components/share/logcomponent'))
+const CopyComponent = asyncIconComponent(() => import('@/menu/components/share/copycomponent'))
+const UserComponent = asyncIconComponent(() => import('@/menu/components/share/usercomponent'))
+
+const { confirm } = Modal
+
+class DataCardEditComponent extends Component {
+  static propTpyes = {
+    card: PropTypes.object,
+    deletecomponent: PropTypes.func,
+    updateConfig: PropTypes.func,
+  }
+
+  state = {
+    dict: sessionStorage.getItem('lang') !== 'en-US' ? zhCN : enUS,
+    card: null,
+    back: false
+  }
+
+  UNSAFE_componentWillMount () {
+    const { card } = this.props
+
+    if (card.isNew) {
+      let _card = {
+        uuid: card.uuid,
+        type: card.type,
+        floor: card.floor,
+        tabId: card.tabId || '',
+        parentId: card.parentId || '',
+        format: 'array',    // 缁勪欢灞炴�� - 鏁版嵁鏍煎紡
+        pageable: false,    // 缁勪欢灞炴�� - 鏄惁鍙垎椤�
+        switchable: false,   // 缁勪欢灞炴�� - 鏁版嵁鏄惁鍙垏鎹�
+        dataName: card.dataName || '',
+        width: card.width || 24,
+        name: card.name,
+        subtype: card.subtype,
+        setting: { interType: 'system' },
+        wrap: { name: card.name, width: card.width || 24, autoplay: 'false', dots: 'true' },
+        style: { borderWidth: '1px', borderColor: '#e8e8e8', marginTop: '8px', marginBottom: '8px', height: '300px' },
+        columns: [],
+        scripts: [],
+        btnlog: [],
+        subcards: [{
+          uuid: Utils.getuuid(),
+          setting: {},
+          style: {
+            paddingTop: '15px', paddingBottom: '15px', paddingLeft: '15px', paddingRight: '15px',
+          },
+          elements: []
+        }]
+      }
+
+      if (card.config) {
+        let config = fromJS(card.config).toJS()
+
+        _card.wrap = config.wrap
+        _card.wrap.name = card.name
+        _card.style = config.style
+
+        _card.subcards = config.subcards.map(scard => {
+          scard.uuid = Utils.getuuid()
+          scard.elements = scard.elements.map(elem => {
+            elem.uuid = Utils.getuuid()
+            return elem
+          })
+          return scard
+        })
+      }
+
+      this.setState({
+        card: _card
+      })
+      this.props.updateConfig(_card)
+    } else {
+      this.setState({
+        card: fromJS(card).toJS()
+      })
+    }
+  }
+
+  componentDidMount () {
+    MKEmitter.addListener('submitStyle', this.getStyle)
+    MKEmitter.addListener('logButton', this.logButton)
+  }
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return !is(fromJS(this.state), fromJS(nextState)) || (!this.props.menu && nextProps.menu)
+  }
+
+  /**
+   * @description 缁勪欢閿�姣侊紝娓呴櫎state鏇存柊锛屾竻闄ゅ揩鎹烽敭璁剧疆
+   */
+  componentWillUnmount () {
+    this.setState = () => {
+      return
+    }
+    MKEmitter.removeListener('submitStyle', this.getStyle)
+    MKEmitter.removeListener('logButton', this.logButton)
+  }
+
+  logButton = (id, item) => {
+    const { card } = this.state
+
+    if (id !== card.uuid) return
+
+    let btnlog = card.btnlog || []
+    btnlog.push(item)
+
+    this.setState({
+      card: {...card, btnlog}
+    })
+    this.props.updateConfig({...card, btnlog})
+  }
+  
+  /**
+   * @description 鍗$墖琛屽灞備俊鎭洿鏂帮紙鏁版嵁婧愶紝鏍峰紡绛夛級
+   */
+  updateComponent = (component) => {
+    this.setState({
+      card: component
+    })
+
+    component.width = component.wrap.width
+    component.name = component.wrap.name
+
+    this.props.updateConfig(component)
+  }
+
+  /**
+   * @description 鍗曚釜鍗$墖淇℃伅鏇存柊
+   */
+  updateCard = (cell) => {
+    let card = fromJS(this.state.card).toJS()
+
+    card.subcards = card.subcards.map(item => {
+      if (item.uuid === cell.uuid) return cell
+      return item
+    })
+
+    this.setState({card})
+
+    this.props.updateConfig(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)
+
+        let uuids = []
+        cell.elements && cell.elements.forEach(c => {
+          if (c.eleType === 'button') {
+            uuids.push(c.uuid)
+          }
+        })
+        MKEmitter.emit('delButtons', uuids)
+
+        if (card.btnlog) {
+          card.btnlog = card.btnlog.filter(c => c.$parentId !== cell.uuid)
+        }
+
+        _this.setState({card})
+        _this.props.updateConfig(card)
+      },
+      onCancel() {}
+    })
+  }
+
+  changeStyle = () => {
+    const { card } = this.state
+
+    MKEmitter.emit('changeStyle', [card.uuid], ['height', 'background', 'border', 'padding', 'margin'], card.style)
+  }
+
+  getStyle = (comIds, style) => {
+    const { card } = this.state
+
+    if (comIds.length !== 1 || comIds[0] !== card.uuid) return
+
+    let _card = {...card, style}
+
+    this.setState({
+      card: _card
+    })
+    
+    this.props.updateConfig(_card)
+  }
+
+  handleLog = (type, logs, item) => {
+    let card = fromJS(this.state.card).toJS()
+
+    if (type === 'revert') {
+      card.subcards.forEach(col => {
+        col.elements = [...col.elements, item]
+        if (item.$parentId === col.uuid) {
+          col.elements = [...col.elements, item]
+        }
+      })
+
+      card.btnlog = logs
+
+      this.setState({ card })
+      this.props.updateConfig(card)
+      notification.success({
+        top: 92,
+        message: '鎭㈠鎴愬姛锛�',
+        duration: 2
+      })
+    } else {
+      card.btnlog = logs
+      this.setState({ card })
+      this.props.updateConfig(card)
+      notification.success({
+        top: 92,
+        message: '娓呴櫎鎴愬姛锛�',
+        duration: 2
+      })
+    }
+  }
+
+  clickComponent = (e) => {
+    if (sessionStorage.getItem('style-control') === 'true' || sessionStorage.getItem('style-control') === 'component') {
+      e.stopPropagation()
+      MKEmitter.emit('clickComponent', this.state.card)
+    }
+  }
+
+  render() {
+    const { card } = this.state
+
+    return (
+      <div className="menu-data-carousel-edit-box" style={{...card.style}} onClick={this.clickComponent} id={card.uuid}>
+        <Popover overlayClassName="mk-popover-control-wrap" mouseLeaveDelay={0.2} mouseEnterDelay={0.2} content={
+          <div className="mk-popover-control">
+            <WrapComponent config={card} updateConfig={this.updateComponent}/>
+            <CopyComponent type="datacard" card={card}/>
+            <Icon className="style" title="璋冩暣鏍峰紡" onClick={this.changeStyle} type="font-colors"/>
+            <LogComponent btnlog={card.btnlog || []} handlelog={this.handleLog}/>
+            <UserComponent config={card}/>
+            <Icon className="close" title="鍒犻櫎缁勪欢" type="delete" onClick={() => this.props.deletecomponent(card.uuid)}/>
+            <SettingComponent config={card} updateConfig={this.updateComponent}/>
+          </div>
+        } trigger="hover">
+          <Icon type="tool"/>
+        </Popover>
+        <CardComponent cards={card} card={card.subcards[0]} updateElement={this.updateCard} deleteElement={this.deleteCard}/>
+      </div>
+    )
+  }
+}
+
+const mapStateToProps = (state) => {
+  return {
+    menu: state.customMenu
+  }
+}
+
+const mapDispatchToProps = () => {
+  return {}
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DataCardEditComponent)
\ No newline at end of file
diff --git a/src/menu/components/carousel/data-card/index.scss b/src/menu/components/carousel/data-card/index.scss
new file mode 100644
index 0000000..b62bd6b
--- /dev/null
+++ b/src/menu/components/carousel/data-card/index.scss
@@ -0,0 +1,87 @@
+.menu-data-carousel-edit-box {
+  position: relative;
+  box-sizing: border-box;
+  background: #ffffff;
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-size: cover;
+  min-height: 30px;
+  
+  .card-control {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    .anticon-tool {
+      right: auto;
+      left: 1px;
+      padding: 1px;
+    }
+  }
+  .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-position: center center;
+    background-repeat: no-repeat;
+    background-size: cover;
+    min-height: 20px;
+    height: 100%;
+  }
+  
+  .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;
+    }
+  }
+  .card-add-button {
+    text-align: right;
+    clear: left;
+    .anticon-plus {
+      font-size: 20px;
+      color: #26C281;
+      padding: 5px;
+      margin-right: 10px;
+    }
+  }
+  .ant-pagination {
+    float: right;
+    margin: 10px;
+  }
+
+  .model-menu-action-list {
+    .page-card {
+      line-height: 55px;
+    }
+  }
+}
+.menu-carousel-edit-box::after {
+  display: block;
+  content: ' ';
+  clear: both;
+}
+.menu-carousel-edit-box:hover {
+  z-index: 1;
+  box-shadow: 0px 0px 4px #1890ff;
+}
diff --git a/src/menu/components/carousel/data-card/wrapsetting/index.jsx b/src/menu/components/carousel/data-card/wrapsetting/index.jsx
new file mode 100644
index 0000000..7b28116
--- /dev/null
+++ b/src/menu/components/carousel/data-card/wrapsetting/index.jsx
@@ -0,0 +1,83 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import { is, fromJS } from 'immutable'
+import { Icon, Modal } from 'antd'
+
+import zhCN from '@/locales/zh-CN/model.js'
+import enUS from '@/locales/en-US/model.js'
+import SettingForm from './settingform'
+import './index.scss'
+
+class DataSource extends Component {
+  static propTpyes = {
+    config: PropTypes.any,
+    updateConfig: PropTypes.func
+  }
+
+  state = {
+    dict: sessionStorage.getItem('lang') !== 'en-US' ? zhCN : enUS,
+    visible: false,
+    wrap: null
+  }
+
+  UNSAFE_componentWillMount () {
+    const { config } = this.props
+
+    this.setState({wrap: fromJS(config.wrap).toJS()})
+  }
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState))
+  }
+
+  editDataSource = () => {
+    this.setState({
+      visible: true
+    })
+  }
+
+  verifySubmit = () => {
+    const { config } = this.props
+
+    this.verifyRef.handleConfirm().then(res => {
+
+      this.setState({
+        wrap: res,
+        visible: false
+      })
+      this.props.updateConfig({...config, wrap: res})
+    })
+  }
+
+  render () {
+    const { config } = this.props
+    const { visible, dict, wrap } = this.state
+
+    return (
+      <div className="model-menu-setting-wrap">
+        <Icon type="edit" title="缂栬緫" onClick={() => this.editDataSource()} />
+        <Modal
+          wrapClassName="popview-modal"
+          title="鍗$墖璁剧疆"
+          visible={visible}
+          width={800}
+          maskClosable={false}
+          okText={dict['model.submit']}
+          onOk={this.verifySubmit}
+          onCancel={() => { this.setState({ visible: false }) }}
+          destroyOnClose
+        >
+          <SettingForm
+            dict={dict}
+            wrap={wrap}
+            config={config}
+            inputSubmit={this.verifySubmit}
+            wrappedComponentRef={(inst) => this.verifyRef = inst}
+          />
+        </Modal>
+      </div>
+    )
+  }
+}
+
+export default DataSource
\ No newline at end of file
diff --git a/src/menu/components/carousel/data-card/wrapsetting/index.scss b/src/menu/components/carousel/data-card/wrapsetting/index.scss
new file mode 100644
index 0000000..04372e6
--- /dev/null
+++ b/src/menu/components/carousel/data-card/wrapsetting/index.scss
@@ -0,0 +1,7 @@
+.model-menu-setting-wrap {
+  display: inline-block;
+
+  >.anticon-edit {
+    color: #1890ff;
+  }
+}
\ No newline at end of file
diff --git a/src/menu/components/carousel/data-card/wrapsetting/settingform/index.jsx b/src/menu/components/carousel/data-card/wrapsetting/settingform/index.jsx
new file mode 100644
index 0000000..0007143
--- /dev/null
+++ b/src/menu/components/carousel/data-card/wrapsetting/settingform/index.jsx
@@ -0,0 +1,214 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import { Form, Row, Col, Input, Radio, Tooltip, Icon, InputNumber, Select } from 'antd'
+
+import './index.scss'
+
+class SettingForm extends Component {
+  static propTpyes = {
+    dict: PropTypes.object,      // 瀛楀吀椤�
+    config: PropTypes.object,    // 鍗$墖琛屼俊鎭�
+    wrap: PropTypes.object,      // 鏁版嵁婧愰厤缃�
+    inputSubmit: PropTypes.func  // 鍥炶溅浜嬩欢
+  }
+
+  state = {
+    roleList: []
+  }
+
+  UNSAFE_componentWillMount () {
+    let roleList = sessionStorage.getItem('sysRoles')
+    if (roleList) {
+      try {
+        roleList = JSON.parse(roleList)
+      } catch {
+        roleList = []
+      }
+    } else {
+      roleList = []
+    }
+
+    this.setState({roleList})
+  }
+
+  handleConfirm = () => {
+    // 琛ㄥ崟鎻愪氦鏃舵鏌ヨ緭鍏ュ�兼槸鍚︽纭�
+    return new Promise((resolve, reject) => {
+      this.props.form.validateFieldsAndScroll((err, values) => {
+        if (!err) {
+          resolve(values)
+        } else {
+          reject(err)
+        }
+      })
+    })
+  }
+
+  handleSubmit = (e) => {
+    e.preventDefault()
+
+    if (this.props.inputSubmit) {
+      this.props.inputSubmit()
+    }
+  }
+
+  render() {
+    const { wrap, config } = this.props
+    const { getFieldDecorator } = this.props.form
+    const { roleList } = this.state
+
+    const formItemLayout = {
+      labelCol: {
+        xs: { span: 24 },
+        sm: { span: 8 }
+      },
+      wrapperCol: {
+        xs: { span: 24 },
+        sm: { span: 16 }
+      }
+    }
+
+    return (
+      <div className="model-menu-setting-form">
+        <Form {...formItemLayout}>
+          <Row gutter={24}>
+            <Col span={12}>
+              <Form.Item label={
+                <Tooltip placement="topLeft" title="鐢ㄤ簬缁勪欢闂寸殑鍖哄垎銆�">
+                  <Icon type="question-circle" />
+                  缁勪欢鍚嶇О
+                </Tooltip>
+              }>
+                {getFieldDecorator('name', {
+                  initialValue: wrap.name,
+                  rules: [
+                    {
+                      required: true,
+                      message: this.props.dict['form.required.input'] + '缁勪欢鍚嶇О!'
+                    }
+                  ]
+                })(<Input placeholder={''} autoComplete="off" onPressEnter={this.handleSubmit} />)}
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item label={
+                <Tooltip placement="topLeft" title="鏍呮牸甯冨眬锛屾瘡琛岀瓑鍒嗕负24鍒椼��">
+                  <Icon type="question-circle" />
+                  瀹藉害
+                </Tooltip>
+              }>
+                {getFieldDecorator('width', {
+                  initialValue: wrap.width || 24,
+                  rules: [
+                    {
+                      required: true,
+                      message: this.props.dict['form.required.input'] + '瀹藉害!'
+                    }
+                  ]
+                })(<InputNumber min={1} max={24} precision={0} onPressEnter={this.handleSubmit} />)}
+              </Form.Item>
+            </Col>
+            {config.subtype === 'propcard' ? <Col span={12}>
+              <Form.Item label={
+                <Tooltip placement="topLeft" title="閫夋嫨闈欐�佸�硷紝鏃犻渶閰嶇疆鏁版嵁婧愩��">
+                  <Icon type="question-circle" />
+                  鏁版嵁鏉ユ簮
+                </Tooltip>
+              }>
+                {getFieldDecorator('datatype', {
+                  initialValue: wrap.datatype || 'dynamic'
+                })(
+                  <Radio.Group>
+                    <Radio value="dynamic">鍔ㄦ��</Radio>
+                    <Radio value="static">闈欐��</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col> : null}
+            <Col span={12}>
+              <Form.Item label="鑷姩鍒囨崲">
+                {getFieldDecorator('autoplay', {
+                  initialValue: wrap.autoplay || 'false'
+                })(
+                  <Radio.Group>
+                    <Radio value="false">鍚�</Radio>
+                    <Radio value="true">鏄�</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item label={
+                <Tooltip placement="topLeft" title="浣跨敤鑷姩鍒囨崲鏃舵湁鏁堬紝榛樿涓�3绉掋��">
+                  <Icon type="question-circle" />
+                  鏃堕棿闂撮殧
+                </Tooltip>
+              }>
+                {getFieldDecorator('speed', {
+                  initialValue: wrap.speed || 3
+                })(<InputNumber min={1} max={100} precision={0} onPressEnter={this.handleSubmit} />)}
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item label="鎸囩ず鐐�">
+                {getFieldDecorator('dots', {
+                  initialValue: wrap.dots || 'true'
+                })(
+                  <Radio.Group>
+                    <Radio value="true">鏄剧ず</Radio>
+                    <Radio value="false">闅愯棌</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item label="鎸囩ず鐐逛綅缃�">
+                {getFieldDecorator('dotPosition', {
+                  initialValue: wrap.dotPosition || 'bottom'
+                })(
+                  <Radio.Group>
+                    <Radio value="top">涓�</Radio>
+                    <Radio value="bottom">涓�</Radio>
+                    <Radio value="left">宸�</Radio>
+                    <Radio value="right">鍙�</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item label="鍔ㄧ敾鏁堟灉">
+                {getFieldDecorator('effect', {
+                  initialValue: wrap.effect || 'scrollx'
+                })(
+                  <Radio.Group>
+                    <Radio value="scrollx">鍒囨崲</Radio>
+                    <Radio value="fade">娓愭樉</Radio>
+                  </Radio.Group>
+                )}
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item label="榛戝悕鍗�">
+                {getFieldDecorator('blacklist', {
+                  initialValue: wrap.blacklist || []
+                })(
+                  <Select
+                    showSearch
+                    mode="multiple"
+                    filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
+                  >
+                    {roleList.map(option =>
+                      <Select.Option key={option.uuid} value={option.value}>{option.text}</Select.Option>
+                    )}
+                  </Select>
+                )}
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+      </div>
+    )
+  }
+}
+
+export default Form.create()(SettingForm)
\ No newline at end of file
diff --git a/src/menu/components/carousel/data-card/wrapsetting/settingform/index.scss b/src/menu/components/carousel/data-card/wrapsetting/settingform/index.scss
new file mode 100644
index 0000000..159130b
--- /dev/null
+++ b/src/menu/components/carousel/data-card/wrapsetting/settingform/index.scss
@@ -0,0 +1,11 @@
+.model-menu-setting-form {
+  position: relative;
+
+  .anticon-question-circle {
+    color: #c49f47;
+    margin-right: 3px;
+  }
+  .ant-input-number {
+    width: 100%;
+  }
+}
\ No newline at end of file
diff --git a/src/menu/components/carousel/prop-card/index.jsx b/src/menu/components/carousel/prop-card/index.jsx
new file mode 100644
index 0000000..2bdb7d0
--- /dev/null
+++ b/src/menu/components/carousel/prop-card/index.jsx
@@ -0,0 +1,369 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import {connect} from 'react-redux'
+import { is, fromJS } from 'immutable'
+import { Icon, Popover, Modal, notification, Carousel } from 'antd'
+
+import asyncComponent from '@/utils/asyncComponent'
+import asyncIconComponent from '@/utils/asyncIconComponent'
+
+import MKEmitter from '@/utils/events.js'
+import Utils from '@/utils/utils.js'
+import zhCN from '@/locales/zh-CN/model.js'
+import enUS from '@/locales/en-US/model.js'
+import './index.scss'
+
+const SettingComponent = asyncIconComponent(() => import('@/menu/datasource'))
+const WrapComponent = asyncIconComponent(() => import('../data-card/wrapsetting'))
+const CardComponent = asyncComponent(() => import('../cardcomponent'))
+const CopyComponent = asyncIconComponent(() => import('@/menu/components/share/copycomponent'))
+const PasteComponent = asyncIconComponent(() => import('@/menu/components/share/pastecomponent'))
+const LogComponent = asyncIconComponent(() => import('@/menu/components/share/logcomponent'))
+const UserComponent = asyncIconComponent(() => import('@/menu/components/share/usercomponent'))
+
+const { confirm } = Modal
+
+class PropCardEditComponent extends Component {
+  static propTpyes = {
+    card: PropTypes.object,
+    deletecomponent: PropTypes.func,
+    updateConfig: PropTypes.func,
+  }
+
+  state = {
+    dict: sessionStorage.getItem('lang') !== 'en-US' ? zhCN : enUS,
+    card: null,
+    back: false
+  }
+
+  UNSAFE_componentWillMount () {
+    const { card } = this.props
+
+    if (card.isNew) {
+      let _card = {
+        uuid: card.uuid,
+        type: card.type,
+        floor: card.floor,
+        tabId: card.tabId || '',
+        parentId: card.parentId || '',
+        format: 'object',   // 缁勪欢灞炴�� - 鏁版嵁鏍煎紡
+        pageable: false,    // 缁勪欢灞炴�� - 鏄惁鍙垎椤�
+        switchable: false,  // 缁勪欢灞炴�� - 鏁版嵁鏄惁鍙垏鎹�
+        dataName: card.dataName || '',
+        width: card.width || 24,
+        name: card.name,
+        subtype: card.subtype,
+        setting: { },
+        wrap: { name: card.name, width: card.width || 24, datatype: 'static', autoplay: 'false', dots: 'true' },
+        style: { borderWidth: '1px', borderColor: '#e8e8e8', marginLeft: '0px', marginRight: '0px', marginTop: '8px', marginBottom: '8px', height: '300px' },
+        columns: [],
+        scripts: [],
+        subcards: [{
+          uuid: Utils.getuuid(),
+          setting: {},
+          style: {},
+          elements: [],
+        }],
+        btnlog: [],
+      }
+
+      if (card.config) {
+        let config = fromJS(card.config).toJS()
+
+        _card.wrap = config.wrap
+        _card.wrap.name = card.name
+        _card.style = config.style
+
+        _card.subcards = config.subcards.map(scard => {
+          scard.uuid = Utils.getuuid()
+          scard.elements = scard.elements.map(elem => {
+            elem.uuid = Utils.getuuid()
+            return elem
+          })
+          return scard
+        })
+      }
+      this.setState({
+        card: _card
+      })
+      this.props.updateConfig(_card)
+    } else {
+      this.setState({
+        card: fromJS(card).toJS()
+      })
+    }
+  }
+
+  componentDidMount () {
+    MKEmitter.addListener('submitStyle', this.getStyle)
+    MKEmitter.addListener('logButton', this.logButton)
+    MKEmitter.addListener('submitComponentStyle', this.updateComponentStyle)
+  }
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return !is(fromJS(this.state), fromJS(nextState)) || (!this.props.menu && nextProps.menu)
+  }
+
+  /**
+   * @description 缁勪欢閿�姣侊紝娓呴櫎state鏇存柊锛屾竻闄ゅ揩鎹烽敭璁剧疆
+   */
+  componentWillUnmount () {
+    this.setState = () => {
+      return
+    }
+    MKEmitter.removeListener('submitStyle', this.getStyle)
+    MKEmitter.removeListener('logButton', this.logButton)
+    MKEmitter.removeListener('submitComponentStyle', this.updateComponentStyle)
+  }
+
+  updateComponentStyle = (parentId, keys, style) => {
+    const { card } = this.state
+
+    if (card.uuid !== parentId) return
+
+    let subcards = card.subcards.map(item => {
+      if (keys.includes(item.uuid)) {
+        item.style = {...item.style, ...style}
+      }
+      return item
+    })
+
+    this.setState({card: {...card, subcards: []}}, () => {
+      this.updateComponent({...card, subcards: subcards})
+    })
+  }
+
+  logButton = (id, item) => {
+    const { card } = this.state
+
+    if (id !== card.uuid) return
+
+    let btnlog = card.btnlog || []
+    btnlog.push(item)
+
+    this.setState({
+      card: {...card, btnlog}
+    })
+    this.props.updateConfig({...card, btnlog})
+  }
+
+  /**
+   * @description 鍗$墖琛屽灞備俊鎭洿鏂帮紙鏁版嵁婧愶紝鏍峰紡绛夛級
+   */
+  updateComponent = (component) => {
+    this.setState({
+      card: component
+    })
+
+    component.width = component.wrap.width
+    component.name = component.wrap.name
+
+    this.props.updateConfig(component)
+  }
+
+  /**
+   * @description 鍗曚釜鍗$墖淇℃伅鏇存柊
+   */
+  updateCard = (cell) => {
+    let card = fromJS(this.state.card).toJS()
+
+    card.subcards = card.subcards.map(item => {
+      if (item.uuid === cell.uuid) return cell
+      return item
+    })
+
+    this.setState({card})
+
+    this.props.updateConfig(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)
+        
+        let uuids = []
+        cell.elements && cell.elements.forEach(c => {
+          if (c.eleType === 'button') {
+            uuids.push(c.uuid)
+          }
+        })
+        MKEmitter.emit('delButtons', uuids)
+
+        if (card.btnlog) {
+          card.btnlog = card.btnlog.filter(c => c.$parentId !== cell.uuid)
+        }
+
+        _this.setState({card})
+        _this.props.updateConfig(card)
+      },
+      onCancel() {}
+    })
+  }
+
+  changeStyle = () => {
+    const { card } = this.state
+
+    MKEmitter.emit('changeStyle', [card.uuid], ['height', 'background', 'border', 'padding', 'margin'], card.style)
+  }
+
+  getStyle = (comIds, style) => {
+    const { card } = this.state
+
+    if (comIds.length !== 1 || comIds[0] !== card.uuid) return
+
+    let _card = {...card, style}
+
+    this.setState({
+      card: _card
+    })
+    
+    this.props.updateConfig(_card)
+  }
+
+  addCard = () => {
+    let card = fromJS(this.state.card).toJS()
+
+    let newcard = {
+      uuid: Utils.getuuid(),
+      setting: {},
+      style: {},
+      elements: [],
+    }
+
+    if (card.subcards.length > 0) {
+      newcard = fromJS(card.subcards.slice(-1)[0]).toJS()
+      newcard.uuid = Utils.getuuid()
+      newcard.elements = newcard.elements.map(elem => {
+        elem.uuid = Utils.getuuid()
+        return elem
+      })
+    }
+
+    card.subcards.push(newcard)
+    
+    this.setState({card})
+    this.props.updateConfig(card)
+  }
+
+  handleLog = (type, logs, item) => {
+    let card = fromJS(this.state.card).toJS()
+
+    if (type === 'revert') {
+      let done = false
+      if (item.$parentId) {
+        card.subcards.forEach(col => {
+          if (item.$parentId === col.uuid) {
+            col.elements = [...col.elements, item]
+            done = true
+          }
+        })
+      }
+
+      card.btnlog = logs
+
+      this.setState({ card: {...card, subcards: []} }, () => {
+        this.setState({ card })
+        this.props.updateConfig(card)
+      })
+      if (!done) {
+        notification.warning({
+          top: 92,
+          message: '闄勫睘鍗$墖宸插垹闄わ紒',
+          duration: 2
+        })
+      } else {
+        notification.success({
+          top: 92,
+          message: '鎭㈠鎴愬姛锛�',
+          duration: 2
+        })
+      }
+    } else {
+      card.btnlog = logs
+      this.setState({ card })
+      this.props.updateConfig(card)
+      notification.success({
+        top: 92,
+        message: '娓呴櫎鎴愬姛锛�',
+        duration: 2
+      })
+    }
+  }
+
+  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.setState({card: {...card, subcards: []}}, () => {
+      this.setState({card})
+    })
+    this.props.updateConfig(card)
+  }
+
+  clickComponent = (e) => {
+    if (sessionStorage.getItem('style-control') === 'true' || sessionStorage.getItem('style-control') === 'component') {
+      e.stopPropagation()
+      MKEmitter.emit('clickComponent', this.state.card)
+    }
+  }
+
+  render() {
+    const { card } = this.state
+
+    return (
+      <div className="menu-prop-carousel-edit-box" style={{...card.style}} onClick={this.clickComponent} id={card.uuid}>
+        <Popover overlayClassName="mk-popover-control-wrap" mouseLeaveDelay={0.2} mouseEnterDelay={0.2} content={
+          <div className="mk-popover-control">
+            <Icon className="plus" title="娣诲姞鍗$墖" onClick={this.addCard} type="plus" />
+            <WrapComponent config={card} updateConfig={this.updateComponent} />
+            <CopyComponent type="propcard" card={card}/>
+            <PasteComponent config={card} options={['cardcell']} updateConfig={this.updateComponent} />
+            <Icon className="style" title="璋冩暣鏍峰紡" onClick={this.changeStyle} type="font-colors" />
+            <LogComponent btnlog={card.btnlog || []} handlelog={this.handleLog} />
+            <UserComponent config={card}/>
+            <Icon className="close" title="鍒犻櫎缁勪欢" type="delete" onClick={() => this.props.deletecomponent(card.uuid)} />
+            {card.wrap.datatype !== 'static' ? <SettingComponent config={card} updateConfig={this.updateComponent} /> : null}
+            {card.wrap.datatype === 'static' ? <Icon style={{color: '#eeeeee', cursor: 'not-allowed'}} type="setting"/> : null}
+          </div>
+        } trigger="hover">
+          <Icon type="tool" />
+        </Popover>
+        {card.subcards.length > 0 ? <Carousel dotPosition={card.wrap.dotPosition || 'bottom'} effect={card.wrap.effect || 'scrollx'}>
+          {card.subcards.map((subcard) => (<CardComponent key={subcard.uuid} cards={card} card={subcard} move={this.move} updateElement={this.updateCard} deleteElement={this.deleteCard}/>))}
+        </Carousel> : null}
+      </div>
+    )
+  }
+}
+
+const mapStateToProps = (state) => {
+  return {
+    menu: state.customMenu
+  }
+}
+
+const mapDispatchToProps = () => {
+  return {}
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(PropCardEditComponent)
\ No newline at end of file
diff --git a/src/menu/components/carousel/prop-card/index.scss b/src/menu/components/carousel/prop-card/index.scss
new file mode 100644
index 0000000..a53e20c
--- /dev/null
+++ b/src/menu/components/carousel/prop-card/index.scss
@@ -0,0 +1,87 @@
+.menu-prop-carousel-edit-box {
+  position: relative;
+  box-sizing: border-box;
+  background: #ffffff;
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-size: cover;
+  min-height: 30px;
+  
+  .card-control {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    .anticon-tool {
+      right: auto;
+      left: 1px;
+      padding: 1px;
+    }
+  }
+  .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-position: center center;
+    background-repeat: no-repeat;
+    background-size: cover;
+    min-height: 20px;
+    height: 100%;
+  }
+  .ant-carousel:not(.ant-carousel-vertical) {
+    .slick-dots li button {
+      height: 5px;
+    }
+  }
+  .ant-carousel.ant-carousel-vertical {
+    .slick-dots li button {
+      width: 5px;
+    }
+  }
+  
+  .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;
+    }
+  }
+  .card-add-button {
+    text-align: right;
+    clear: left;
+    .anticon-plus {
+      font-size: 20px;
+      color: #26C281;
+      padding: 5px;
+      margin-right: 10px;
+    }
+  }
+}
+.menu-prop-card-edit-box::after {
+  display: block;
+  content: ' ';
+  clear: both;
+}
+.menu-prop-card-edit-box:hover {
+  z-index: 1;
+  box-shadow: 0px 0px 4px #1890ff;
+}
diff --git a/src/menu/components/tabs/tabcomponents/card.jsx b/src/menu/components/tabs/tabcomponents/card.jsx
index 416ca65..ca9c1fe 100644
--- a/src/menu/components/tabs/tabcomponents/card.jsx
+++ b/src/menu/components/tabs/tabcomponents/card.jsx
@@ -10,6 +10,8 @@
 const AntvTabs = asyncComponent(() => import('@/menu/components/tabs/antv-tabs'))
 const DataCard = asyncComponent(() => import('@/menu/components/card/data-card'))
 const PropCard = asyncComponent(() => import('@/menu/components/card/prop-card'))
+const CarouselDataCard = asyncComponent(() => import('@/menu/components/carousel/data-card'))
+const CarouselPropCard = asyncComponent(() => import('@/menu/components/carousel/prop-card'))
 const TableCard = asyncComponent(() => import('@/menu/components/card/table-card'))
 const NormalTable = asyncComponent(() => import('@/menu/components/table/normal-table'))
 const NormalGroup = asyncComponent(() => import('@/menu/components/group/normal-group'))
@@ -61,6 +63,10 @@
       return (<DataCard card={card} updateConfig={updateConfig} deletecomponent={delCard} />)
     } else if (card.type === 'card' && card.subtype === 'propcard') {
       return (<PropCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
+    } else if (card.type === 'carousel' && card.subtype === 'datacard') {
+      return (<CarouselDataCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
+    } else if (card.type === 'carousel' && card.subtype === 'propcard') {
+      return (<CarouselPropCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
     } else if (card.type === 'table' && card.subtype === 'tablecard') {
       return (<TableCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
     } else if (card.type === 'table' && card.subtype === 'normaltable') {
diff --git a/src/menu/components/tabs/tabcomponents/index.jsx b/src/menu/components/tabs/tabcomponents/index.jsx
index 6bf8943..60a625c 100644
--- a/src/menu/components/tabs/tabcomponents/index.jsx
+++ b/src/menu/components/tabs/tabcomponents/index.jsx
@@ -97,6 +97,7 @@
         table: '琛ㄦ牸',
         group: '鍒嗙粍',
         editor: '瀵屾枃鏈�',
+        carousel: '杞挱',
         card: '鍗$墖'
       }
       let i = 1
diff --git a/src/menu/menushell/card.jsx b/src/menu/menushell/card.jsx
index 1413357..8150032 100644
--- a/src/menu/menushell/card.jsx
+++ b/src/menu/menushell/card.jsx
@@ -10,6 +10,8 @@
 const AntvTabs = asyncComponent(() => import('@/menu/components/tabs/antv-tabs'))
 const DataCard = asyncComponent(() => import('@/menu/components/card/data-card'))
 const PropCard = asyncComponent(() => import('@/menu/components/card/prop-card'))
+const CarouselDataCard = asyncComponent(() => import('@/menu/components/carousel/data-card'))
+const CarouselPropCard = asyncComponent(() => import('@/menu/components/carousel/prop-card'))
 const TableCard = asyncComponent(() => import('@/menu/components/card/table-card'))
 const NormalTable = asyncComponent(() => import('@/menu/components/table/normal-table'))
 const NormalGroup = asyncComponent(() => import('@/menu/components/group/normal-group'))
@@ -62,6 +64,10 @@
       return (<DataCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
     } else if (card.type === 'card' && card.subtype === 'propcard') {
       return (<PropCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
+    } else if (card.type === 'carousel' && card.subtype === 'datacard') {
+      return (<CarouselDataCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
+    } else if (card.type === 'carousel' && card.subtype === 'propcard') {
+      return (<CarouselPropCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
     } else if (card.type === 'table' && card.subtype === 'tablecard') {
       return (<TableCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
     } else if (card.type === 'table' && card.subtype === 'normaltable') {
diff --git a/src/menu/menushell/index.jsx b/src/menu/menushell/index.jsx
index cb51df3..fc5d648 100644
--- a/src/menu/menushell/index.jsx
+++ b/src/menu/menushell/index.jsx
@@ -91,6 +91,7 @@
         group: '鍒嗙粍',
         editor: '瀵屾枃鏈�',
         code: '鑷畾涔�',
+        carousel: '杞挱',
         card: '鍗$墖'
       }
       let i = 1
diff --git a/src/menu/modulesource/option.jsx b/src/menu/modulesource/option.jsx
index 9c0a333..1996e96 100644
--- a/src/menu/modulesource/option.jsx
+++ b/src/menu/modulesource/option.jsx
@@ -14,6 +14,7 @@
 import Pie1 from '@/assets/mobimg/ring.png'
 import Pie2 from '@/assets/mobimg/nightingale.png'
 import Mainsearch from '@/assets/mobimg/mainsearch.png'
+import Carousel from '@/assets/mobimg/carousel.png'
 
 // 缁勪欢閰嶇疆淇℃伅
 export const menuOptions = [
@@ -21,6 +22,8 @@
   { type: 'menu', url: Mainsearch, component: 'search', subtype: 'mainsearch', title: '鎼滅储鏉′欢', width: 24, forbid: ['billPrint'] },
   { 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: Carousel, component: 'carousel', subtype: 'datacard', title: '杞挱-鍔ㄦ�佹暟鎹�', width: 24, forbid: ['billPrint'] },
+  { type: 'menu', url: Carousel, component: 'carousel', subtype: 'propcard', title: '杞挱-闈欐�佹暟鎹�', width: 24, forbid: ['billPrint'] },
   { type: 'menu', url: NormalTable, component: 'table', subtype: 'normaltable', title: '甯哥敤琛�', width: 24 },
   { type: 'menu', url: TableCard, component: 'table', subtype: 'tablecard', title: '琛ㄦ牸', width: 12 },
   { type: 'menu', url: line, component: 'line', subtype: 'line', title: '鎶樼嚎鍥�', width: 24 },
diff --git a/src/menu/stylecombcontroller/index.jsx b/src/menu/stylecombcontroller/index.jsx
index b69ffb2..6a9d18a 100644
--- a/src/menu/stylecombcontroller/index.jsx
+++ b/src/menu/stylecombcontroller/index.jsx
@@ -260,7 +260,7 @@
                     label={<Icon title="楂樺害" type="column-height" />}
                     labelCol={{xs: { span: 24 }, sm: { span: 4 }}} wrapperCol={ {xs: { span: 24 }, sm: { span: 20 }} }
                   >
-                    <StyleInput defaultValue={''} options={['px']} onChange={this.changeHeight}/>
+                    <StyleInput defaultValue={''} options={['px', 'vh', 'vw']} onChange={this.changeHeight}/>
                   </Form.Item>
                 </Col>
               </Panel> : null}
diff --git a/src/menu/stylecontroller/index.jsx b/src/menu/stylecontroller/index.jsx
index cc674a1..595e8ff 100644
--- a/src/menu/stylecontroller/index.jsx
+++ b/src/menu/stylecontroller/index.jsx
@@ -329,7 +329,7 @@
                     label={<Icon title="瀹藉害" type="column-width" />}
                     labelCol={{xs: { span: 24 }, sm: { span: 4 }}} wrapperCol={ {xs: { span: 24 }, sm: { span: 20 }} }
                   >
-                    <StyleInput defaultValue={card.width || ''} options={['px']} onChange={this.changeWidth}/>
+                    <StyleInput defaultValue={card.width || ''} options={['px', 'vh', 'vw']} onChange={this.changeWidth}/>
                   </Form.Item>
                 </Col>
               </Panel> : null}
@@ -340,7 +340,7 @@
                     label={<Icon title="楂樺害" type="column-height" />}
                     labelCol={{xs: { span: 24 }, sm: { span: 4 }}} wrapperCol={ {xs: { span: 24 }, sm: { span: 20 }} }
                   >
-                    <StyleInput defaultValue={card.height || ''} options={['px']} onChange={this.changeHeight}/>
+                    <StyleInput defaultValue={card.height || ''} options={['px', 'vh', 'vw']} onChange={this.changeHeight}/>
                   </Form.Item>
                 </Col>
               </Panel> : null}
diff --git a/src/pc/menushell/card.jsx b/src/pc/menushell/card.jsx
index 06beb23..aef7d95 100644
--- a/src/pc/menushell/card.jsx
+++ b/src/pc/menushell/card.jsx
@@ -16,6 +16,8 @@
 const BraftEditor = asyncComponent(() => import('@/menu/components/editor/braft-editor'))
 const CodeSandbox = asyncComponent(() => import('@/menu/components/code/sandbox'))
 const NormalNavbar = asyncComponent(() => import('@/pc/components/navbar/normal-navbar'))
+const CarouselDataCard = asyncComponent(() => import('@/menu/components/carousel/data-card'))
+const CarouselPropCard = asyncComponent(() => import('@/menu/components/carousel/prop-card'))
 
 const Card = ({ id, card, moveCard, findCard, delCard, updateConfig }) => {
   const originalIndex = findCard(id).index
@@ -69,6 +71,10 @@
       return (<DataCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
     } else if (card.type === 'card' && card.subtype === 'propcard') {
       return (<PropCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
+    } else if (card.type === 'carousel' && card.subtype === 'datacard') {
+      return (<CarouselDataCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
+    } else if (card.type === 'carousel' && card.subtype === 'propcard') {
+      return (<CarouselPropCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
     } else if (card.type === 'table' && card.subtype === 'tablecard') {
       return (<TableCard card={card} updateConfig={updateConfig} deletecomponent={delCard}/>)
     } else if (card.type === 'table' && card.subtype === 'normaltable') {
diff --git a/src/pc/menushell/index.jsx b/src/pc/menushell/index.jsx
index 714e6c9..f8e35be 100644
--- a/src/pc/menushell/index.jsx
+++ b/src/pc/menushell/index.jsx
@@ -101,6 +101,7 @@
         editor: '瀵屾枃鏈�',
         code: '鑷畾涔�',
         navbar: '瀵艰埅鏍�',
+        carousel: '杞挱',
         card: '鍗$墖'
       }
       let i = 1
diff --git a/src/pc/modulesource/option.jsx b/src/pc/modulesource/option.jsx
index 81ce70d..2627a6c 100644
--- a/src/pc/modulesource/option.jsx
+++ b/src/pc/modulesource/option.jsx
@@ -15,6 +15,7 @@
 import Pie2 from '@/assets/mobimg/nightingale.png'
 import Mainsearch from '@/assets/mobimg/mainsearch.png'
 import Navbar from '@/assets/mobimg/navbar.png'
+import Carousel from '@/assets/mobimg/carousel.png'
 
 // 缁勪欢閰嶇疆淇℃伅
 export const menuOptions = [
@@ -23,6 +24,8 @@
   { type: 'menu', url: Mainsearch, component: 'search', subtype: 'mainsearch', title: '鎼滅储鏉′欢', width: 24 },
   { 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: Carousel, component: 'carousel', subtype: 'datacard', title: '杞挱-鍔ㄦ�佹暟鎹�', width: 24 },
+  { type: 'menu', url: Carousel, component: 'carousel', subtype: 'propcard', title: '杞挱-闈欐�佹暟鎹�', width: 24 },
   { type: 'menu', url: NormalTable, component: 'table', subtype: 'normaltable', title: '甯哥敤琛�', width: 24 },
   { type: 'menu', url: TableCard, component: 'table', subtype: 'tablecard', title: '琛ㄦ牸', width: 12 },
   { type: 'menu', url: line, component: 'line', subtype: 'line', title: '鎶樼嚎鍥�', width: 24 },
diff --git a/src/tabviews/custom/components/card/data-card/index.jsx b/src/tabviews/custom/components/card/data-card/index.jsx
index 30ba645..313cd88 100644
--- a/src/tabviews/custom/components/card/data-card/index.jsx
+++ b/src/tabviews/custom/components/card/data-card/index.jsx
@@ -1,6 +1,7 @@
 import React, {Component} from 'react'
 import PropTypes from 'prop-types'
 import { is, fromJS } from 'immutable'
+import { connect } from 'react-redux'
 import { Spin, Empty, notification, Col, Pagination } from 'antd'
 
 import Api from '@/api'
@@ -10,6 +11,7 @@
 import nextImg from '@/assets/img/next.png'
 import MKEmitter from '@/utils/events.js'
 import asyncComponent from '@/utils/asyncComponent'
+import { modifyTabview } from '@/store/action'
 import './index.scss'
 
 const CardItem = asyncComponent(() => import('../cardItem'))
@@ -394,8 +396,10 @@
   changeCard = (index, item) => {
     const { config, selectKeys, selectedData, activeKey } = this.state
 
-    if (!config.wrap.cardType) return
+    this.openView(item)
 
+    if (!config.wrap.cardType) return
+    
     let _selectKeys = []
     let _selectedData = []
     let _activeKey = ''
@@ -430,6 +434,83 @@
     })
 
     MKEmitter.emit('resetSelectLine', config.uuid, (_item ? _item.$$uuid : ''), _item)
+  }
+
+  openView = (item) => {
+    const { card } = this.state
+
+    if (card.setting.click === 'menu') {
+      let menu = null
+
+      if (card.setting.menu && card.setting.menu.length > 0) {
+        let menu_id = card.setting.menu.slice(-1)[0]
+        menu = this.props.permMenus.filter(m => m.MenuID === menu_id)[0] || ''
+      }
+
+      if (!menu) {
+        notification.warning({
+          top: 92,
+          message: '鑿滃崟宸插垹闄ゆ垨娌℃湁璁块棶鏉冮檺锛�',
+          duration: 5
+        })
+        return
+      }
+
+      let newtab = {
+        ...menu,
+        selected: true,
+        param: {}
+      }
+
+      if (card.setting.joint === 'true') {
+        newtab.param.BID = item.$$uuid
+      }
+
+      if (['linkage_navigation', 'linkage'].includes(window.GLOB.navBar)) {
+        this.props.modifyTabview([newtab])
+      } else {
+        let tabs = this.props.tabviews.filter((tab, i) => {
+          tab.selected = false
+          return tab.MenuID !== newtab.MenuID
+        })
+
+        if (this.props.tabviews.length !== tabs.length) {
+          this.props.modifyTabview(fromJS(tabs).toJS())
+        }
+
+        this.setState({}, () => {
+          tabs.push(newtab)
+          this.props.modifyTabview(tabs)
+        })
+      }
+    } else if (card.setting.click === 'link') {
+      let src = card.setting.linkurl
+
+      if (src.indexOf('paramsmain/') > -1) {
+        try {
+          let _url = src.split('paramsmain/')[0] + 'paramsmain/'
+          let _param = JSON.parse(window.decodeURIComponent(window.atob(src.split('paramsmain/')[1])))
+
+          _param.UserID = sessionStorage.getItem('UserID')
+          _param.LoginUID = sessionStorage.getItem('LoginUID')
+          _param.User_Name = sessionStorage.getItem('User_Name')
+          _param.param = { BID: item.$$uuid }
+          src = _url + window.btoa(window.encodeURIComponent(JSON.stringify(_param)))
+        } catch {
+          console.warn('鑿滃崟鍙傛暟瑙f瀽閿欒锛�')
+        }
+      } else if (card.setting.joint === 'true') {
+        let con = '?'
+
+        if (/\?/ig.test(src)) {
+          con = '&'
+        }
+
+        src = src + `${con}id=${item.$$uuid}&appkey=${window.GLOB.appkey}&userid=${sessionStorage.getItem('UserID')}&LoginUID=${sessionStorage.getItem('LoginUID') || ''}`
+      }
+
+      window.open(src)
+    }
   }
 
   render() {
@@ -475,7 +556,7 @@
           {switchable ? <div className={'prev-page ' + (pageIndex === 1 ? 'disabled' : '')} onClick={this.prevPage}><div><div><img src={preImg} alt=""/></div></div></div> : null}
           {data && data.length > 0 ? <div className="card-row-list">
             {data.map((item, index) => (
-              <Col className={activeKey === index ? 'active' : (selectKeys.indexOf(index) > -1 ? 'selected' : '')} key={index} span={card.setting.width} offset={!index ? offset : 0} onClick={() => {this.changeCard(index, item)}}>
+              <Col className={(activeKey === index ? 'active' : (selectKeys.indexOf(index) > -1 ? 'selected' : '')) + (card.setting.click ? ' pointer' : '')} key={index} span={card.setting.width} offset={!index ? offset : 0} onClick={() => {this.changeCard(index, item)}}>
                 <CardItem card={card} cards={config} data={item}/>
               </Col>
             ))}
@@ -489,4 +570,17 @@
   }
 }
 
-export default DataCard
\ No newline at end of file
+const mapStateToProps = (state) => {
+  return {
+    permMenus: state.permMenus,
+    tabviews: state.tabviews,
+  }
+}
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    modifyTabview: (tabviews) => dispatch(modifyTabview(tabviews))
+  }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DataCard)
\ No newline at end of file
diff --git a/src/tabviews/custom/components/card/data-card/index.scss b/src/tabviews/custom/components/card/data-card/index.scss
index c9e972b..49e69ef 100644
--- a/src/tabviews/custom/components/card/data-card/index.scss
+++ b/src/tabviews/custom/components/card/data-card/index.scss
@@ -74,6 +74,9 @@
       background-color: #ffffff;
       transition: all 0.3s;
     }
+    >.pointer {
+      cursor: pointer;
+    }
     >.active >.card-item-box {
       border-color: #1890ff!important;
       box-shadow: 0 0 4px #1890ff;
diff --git a/src/tabviews/custom/components/card/prop-card/index.jsx b/src/tabviews/custom/components/card/prop-card/index.jsx
index f621976..bce3459 100644
--- a/src/tabviews/custom/components/card/prop-card/index.jsx
+++ b/src/tabviews/custom/components/card/prop-card/index.jsx
@@ -1,18 +1,20 @@
 import React, {Component} from 'react'
 import PropTypes from 'prop-types'
 import { is, fromJS } from 'immutable'
+import { connect } from 'react-redux'
 import { Spin, notification, Col } from 'antd'
 
 import asyncComponent from '@/utils/asyncComponent'
 import Api from '@/api'
 import UtilsDM from '@/utils/utils-datamanage.js'
 import MKEmitter from '@/utils/events.js'
+import { modifyTabview } from '@/store/action'
 import './index.scss'
 
 const CardItem = asyncComponent(() => import('../cardItem'))
 const NormalHeader = asyncComponent(() => import('@/tabviews/custom/components/share/normalheader'))
 
-class DataCard extends Component {
+class PropCard extends Component {
   static propTpyes = {
     BID: PropTypes.any,              // 鐖剁骇Id
     data: PropTypes.array,           // 缁熶竴鏌ヨ鏁版嵁
@@ -257,6 +259,8 @@
   changeCard = (index, item) => {
     const { config, data, activeKey } = this.state
 
+    this.openView(item)
+
     if (!config.wrap.cardType || activeKey === index) return
 
     this.setState({
@@ -265,6 +269,82 @@
 
     MKEmitter.emit('resetSelectLine', config.uuid, (item.setting.primaryId || ''), data)
   }
+
+  openView = (item) => {
+    if (item.setting.click === 'menu') {
+      let menu = null
+
+      if (item.setting.menu && item.setting.menu.length > 0) {
+        let menu_id = item.setting.menu.slice(-1)[0]
+        menu = this.props.permMenus.filter(m => m.MenuID === menu_id)[0] || ''
+      }
+
+      if (!menu) {
+        notification.warning({
+          top: 92,
+          message: '鑿滃崟宸插垹闄ゆ垨娌℃湁璁块棶鏉冮檺锛�',
+          duration: 5
+        })
+        return
+      }
+
+      let newtab = {
+        ...menu,
+        selected: true,
+        param: {}
+      }
+
+      if (item.setting.joint === 'true') {
+        newtab.param.BID = item.setting.primaryId
+      }
+
+      if (['linkage_navigation', 'linkage'].includes(window.GLOB.navBar)) {
+        this.props.modifyTabview([newtab])
+      } else {
+        let tabs = this.props.tabviews.filter((tab, i) => {
+          tab.selected = false
+          return tab.MenuID !== newtab.MenuID
+        })
+
+        if (this.props.tabviews.length !== tabs.length) {
+          this.props.modifyTabview(fromJS(tabs).toJS())
+        }
+
+        this.setState({}, () => {
+          tabs.push(newtab)
+          this.props.modifyTabview(tabs)
+        })
+      }
+    } else if (item.setting.click === 'link') {
+      let src = item.setting.linkurl
+
+      if (src.indexOf('paramsmain/') > -1) {
+        try {
+          let _url = src.split('paramsmain/')[0] + 'paramsmain/'
+          let _param = JSON.parse(window.decodeURIComponent(window.atob(src.split('paramsmain/')[1])))
+
+          _param.UserID = sessionStorage.getItem('UserID')
+          _param.LoginUID = sessionStorage.getItem('LoginUID')
+          _param.User_Name = sessionStorage.getItem('User_Name')
+          _param.param = { BID: item.setting.primaryId }
+          src = _url + window.btoa(window.encodeURIComponent(JSON.stringify(_param)))
+        } catch {
+          console.warn('鑿滃崟鍙傛暟瑙f瀽閿欒锛�')
+        }
+      } else if (item.setting.joint === 'true') {
+        let con = '?'
+
+        if (/\?/ig.test(src)) {
+          con = '&'
+        }
+
+        src = src + `${con}id=${item.setting.primaryId}&appkey=${window.GLOB.appkey}&userid=${sessionStorage.getItem('UserID')}&LoginUID=${sessionStorage.getItem('LoginUID') || ''}`
+      }
+
+      window.open(src)
+    }
+  }
+
 
   render() {
     const { config, loading, data, activeKey } = this.state
@@ -280,7 +360,7 @@
         <NormalHeader config={config}/>
         <div className={`card-row-list ${config.wrap.cardType || ''} ${config.wrap.scale || ''}`}>
           {config.subcards.map((item, index) => (
-            <Col className={activeKey === index ? 'active' : ''} key={index} span={item.setting.width || 6} offset={item.offset || 0} onClick={() => {this.changeCard(index, item)}}>
+            <Col className={(activeKey === index ? 'active' : '') + (item.setting.click ? ' pointer' : '')} key={index} span={item.setting.width || 6} offset={item.offset || 0} onClick={() => {this.changeCard(index, item)}}>
               <CardItem card={item} cards={config} data={data}/>
             </Col>
           ))}
@@ -290,4 +370,17 @@
   }
 }
 
-export default DataCard
\ No newline at end of file
+const mapStateToProps = (state) => {
+  return {
+    permMenus: state.permMenus,
+    tabviews: state.tabviews,
+  }
+}
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    modifyTabview: (tabviews) => dispatch(modifyTabview(tabviews))
+  }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(PropCard)
\ No newline at end of file
diff --git a/src/tabviews/custom/components/card/prop-card/index.scss b/src/tabviews/custom/components/card/prop-card/index.scss
index 7ec304d..12c5e88 100644
--- a/src/tabviews/custom/components/card/prop-card/index.scss
+++ b/src/tabviews/custom/components/card/prop-card/index.scss
@@ -17,6 +17,9 @@
       background-color: #ffffff;
       transition: all 0.3s;
     }
+    >.pointer {
+      cursor: pointer;
+    }
     >.active >.card-item-box {
       border-color: #1890ff!important;
       box-shadow: 0 0 4px #1890ff;
diff --git a/src/tabviews/custom/components/carousel/cardItem/index.jsx b/src/tabviews/custom/components/carousel/cardItem/index.jsx
new file mode 100644
index 0000000..8b06726
--- /dev/null
+++ b/src/tabviews/custom/components/carousel/cardItem/index.jsx
@@ -0,0 +1,56 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import { is, fromJS } from 'immutable'
+
+import asyncComponent from '@/utils/asyncComponent'
+import zhCN from '@/locales/zh-CN/model.js'
+import enUS from '@/locales/en-US/model.js'
+
+import './index.scss'
+
+const CardCellComponent = asyncComponent(() => import('@/tabviews/custom/components/card/cardcellList'))
+
+class CardBoxComponent extends Component {
+  static propTpyes = {
+    cards: PropTypes.object,    // 鍗$墖琛岄厤缃俊鎭�
+    card: PropTypes.object,     // 鍗$墖閰嶇疆淇℃伅
+    data: PropTypes.object,
+  }
+
+  state = {
+    dict: sessionStorage.getItem('lang') !== 'en-US' ? zhCN : enUS,
+    card: null,            // 鍗$墖淇℃伅锛屽寘鎷鍙嶉潰
+  }
+
+  /**
+   * @description 鎼滅储鏉′欢鍒濆鍖�
+   */
+  UNSAFE_componentWillMount () {
+
+  }
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return !is(fromJS(this.state), fromJS(nextState)) || !is(fromJS(this.props), fromJS(nextProps))
+  }
+
+  /**
+   * @description 缁勪欢閿�姣侊紝娓呴櫎state鏇存柊锛屾竻闄ゅ揩鎹烽敭璁剧疆
+   */
+  componentWillUnmount () {
+    this.setState = () => {
+      return
+    }
+  }
+
+  render() {
+    const { card, data, cards } = this.props
+
+    return (
+      <div className="card-item-box" style={card.style}>
+        <CardCellComponent data={data} cards={cards} cardCell={card} elements={card.elements}/>
+      </div>
+    )
+  }
+}
+
+export default CardBoxComponent
\ No newline at end of file
diff --git a/src/tabviews/custom/components/carousel/cardItem/index.scss b/src/tabviews/custom/components/carousel/cardItem/index.scss
new file mode 100644
index 0000000..605a96b
--- /dev/null
+++ b/src/tabviews/custom/components/carousel/cardItem/index.scss
@@ -0,0 +1,58 @@
+.card-item-box {
+  position: relative;
+  overflow: hidden;
+
+  .back-side {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    right: 0px;
+    bottom: 0px;
+    background-color: #ffffff;
+    transition: all 0.3s;
+    background-position: center center;
+    background-repeat: no-repeat;
+    background-size: cover;
+  }
+  .back-side.up {
+    transform: translate(0, 100%);
+  }
+  .back-side.down {
+    transform: translate(0, -100%);
+  }
+  .back-side.left {
+    transform: translate(100%, 0);
+  }
+  .back-side.right {
+    transform: translate(-100%, 0);
+  }
+  .back-side.opacity {
+    opacity: 0;
+  }
+  .back-side.rotateX {
+    transform: rotateX(90deg);
+  }
+  .back-side.rotateY {
+    transform: rotateY(90deg);
+  }
+  .back-side.scale {
+    transform: scale(0, 0);
+  }
+}
+.card-item-box:hover {
+  .back-side.up, .back-side.down, .back-side.left, .back-side.right {
+    transform: translate(0, 0);
+  }
+  .back-side.opacity {
+    opacity: 1;
+  }
+  .back-side.rotateX {
+    transform: rotateX(0deg);
+  }
+  .back-side.rotateY {
+    transform: rotateY(0deg);
+  }
+  .back-side.scale {
+    transform: scale(1, 1);
+  }
+}
\ No newline at end of file
diff --git a/src/tabviews/custom/components/carousel/data-card/index.jsx b/src/tabviews/custom/components/carousel/data-card/index.jsx
new file mode 100644
index 0000000..56b9535
--- /dev/null
+++ b/src/tabviews/custom/components/carousel/data-card/index.jsx
@@ -0,0 +1,357 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import { is, fromJS } from 'immutable'
+import { connect } from 'react-redux'
+import { Spin, Empty, notification, Carousel } from 'antd'
+
+import Api from '@/api'
+import UtilsDM from '@/utils/utils-datamanage.js'
+import MKEmitter from '@/utils/events.js'
+import asyncComponent from '@/utils/asyncComponent'
+import { modifyTabview } from '@/store/action'
+import './index.scss'
+
+const CardItem = asyncComponent(() => import('../cardItem'))
+
+class DataCard extends Component {
+  static propTpyes = {
+    BID: PropTypes.any,              // 鐖剁骇Id
+    data: PropTypes.array,           // 缁熶竴鏌ヨ鏁版嵁
+    config: PropTypes.object,        // 缁勪欢閰嶇疆淇℃伅
+    mainSearch: PropTypes.any,       // 澶栧眰鎼滅储鏉′欢
+    menuType: PropTypes.any,         // 鑿滃崟绫诲瀷
+  }
+
+  state = {
+    BID: '',                   // 涓婄骇ID
+    config: null,              // 鍥捐〃閰嶇疆淇℃伅
+    loading: false,            // 鏁版嵁鍔犺浇鐘舵��
+    sync: false,               // 鏄惁缁熶竴璇锋眰鏁版嵁
+    card: null,                // 鍗$墖璁剧疆
+    data: null,                // 鏁版嵁
+  }
+
+  UNSAFE_componentWillMount () {
+    const { data, initdata, BID } = this.props
+    let _config = fromJS(this.props.config).toJS()
+    let _card = _config.subcards[0]
+    let _cols = new Map()
+
+    let _data = null
+    let _sync = _config.setting.sync === 'true'
+
+    if (_config.setting.sync === 'true' && data) {
+      _data = data[_config.dataName] || []
+      _sync = false
+    } else if (_config.setting.sync === 'true' && initdata) {
+      _data = initdata || []
+      _sync = false
+    }
+
+    if (_data) {
+      _data = _data.map((item, index) => {
+        item.key = index
+        item.$$uuid = item[_config.setting.primaryKey] || ''
+        item.$$BID = BID || ''
+        return item
+      })
+    }
+
+    _config.columns.forEach(item => {
+      _cols.set(item.field, item)
+    })
+
+    _card.style.height = _config.style.height
+
+    if (_card.setting.click) {
+      _card.style.cursor = 'pointer'
+    }
+
+    _card.elements = _card.elements.map(item => {
+      if (item.field && _cols.has(item.field)) {
+        item.col = _cols.get(item.field)
+      }
+      return item
+    })
+
+    _config.wrap.speed = (_config.wrap.speed || 3) * 1000
+
+    this.setState({
+      sync: _sync,
+      data: _data,
+      BID: BID || '',
+      config: _config,
+      card: _card,
+      arr_field: _config.columns.map(col => col.field).join(','),
+    }, () => {
+      if (_config.setting.sync !== 'true' && _config.setting.onload === 'true') {
+        this.loadData()
+      }
+    })
+  }
+
+  componentDidMount () {
+    MKEmitter.addListener('resetSelectLine', this.resetParentParam)
+    MKEmitter.addListener('getexceloutparam', this.getexceloutparam)
+    MKEmitter.addListener('refreshByButtonResult', this.refreshByButtonResult)
+  }
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return !is(fromJS(this.state), fromJS(nextState))
+  }
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    const { sync, config, BID } = this.state
+
+    if (sync && !is(fromJS(this.props.data), fromJS(nextProps.data))) {
+      let _data = []
+      if (nextProps.data && nextProps.data[config.dataName]) {
+        _data = nextProps.data[config.dataName] || []
+      }
+
+      _data = _data.map((item, index) => {
+        item.key = index
+        item.$$uuid = item[config.setting.primaryKey] || ''
+        item.$$BID = BID || ''
+        return item
+      })
+
+      this.setState({sync: false, data: _data})
+    } else if (nextProps.mainSearch && !is(fromJS(this.props.mainSearch), fromJS(nextProps.mainSearch))) {
+      if (config.setting.syncRefresh === 'true') {
+        this.setState({}, () => {
+          this.loadData()
+        })
+      }
+    }
+  }
+
+  componentWillUnmount () {
+    this.setState = () => {
+      return
+    }
+    MKEmitter.removeListener('resetSelectLine', this.resetParentParam)
+    MKEmitter.removeListener('getexceloutparam', this.getexceloutparam)
+    MKEmitter.removeListener('refreshByButtonResult', this.refreshByButtonResult)
+  }
+
+  /**
+   * @description 鎸夐挳鎵ц瀹屾垚鍚庨〉闈㈠埛鏂�
+   * @param {*} menuId     // 鑿滃崟Id
+   * @param {*} position   // 鍒锋柊浣嶇疆
+   * @param {*} btn        // 鎵ц鐨勬寜閽�
+   */
+  refreshByButtonResult = (menuId, position, btn) => {
+    const { config, BID } = this.state
+
+    if (config.uuid !== menuId) return
+
+    this.loadData(btn)                                                         // 鏁版嵁鍒锋柊
+
+    if (btn.syncComponentId && btn.syncComponentId !== config.uuid && btn.syncComponentId !== config.setting.supModule) {
+      MKEmitter.emit('reloadData', btn.syncComponentId)                        // 鍚岀骇鏍囩鍒锋柊
+    }
+
+    if (position === 'mainline' && config.setting.supModule) {                 // 涓昏〃琛屽埛鏂�
+      MKEmitter.emit('reloadData', config.setting.supModule, (BID || 'empty'))
+    } else if (position === 'popclose') {                                      // 鏍囩鍏抽棴鍒锋柊
+      config.setting.supModule && MKEmitter.emit('reloadData', config.setting.supModule, (BID || 'empty'))
+      btn.$tabId && MKEmitter.emit('refreshPopButton', btn.$tabId)
+    }
+  }
+
+  resetParentParam = (MenuID, id, data) => {
+    const { config } = this.state
+
+    if (!config.setting.supModule || config.setting.supModule !== MenuID) return
+    if (id !== this.state.BID) {
+      this.setState({ BID: id }, () => {
+        this.loadData()
+      })
+    }
+  }
+
+  /**
+   * @description 瀵煎嚭Excel鏃讹紝鑾峰彇椤甸潰鎼滅储鎺掑簭绛夊弬鏁�
+   */
+  getexceloutparam = (menuId, btnId) => {
+    const { mainSearch } = this.props
+    const { arr_field, config } = this.state
+
+    if (config.uuid !== menuId) return
+
+    let searches = mainSearch ? fromJS(mainSearch).toJS() : []
+
+    MKEmitter.emit('execExcelout', config.uuid, btnId, {
+      arr_field: arr_field,
+      orderBy: config.setting.order || '',
+      search: searches,
+      menuName: config.name
+    })
+  }
+
+  async loadData () {
+    const { mainSearch, menuType } = this.props
+    const { config, arr_field, BID } = this.state
+
+    if (config.setting.supModule && !BID) { // BID 涓嶅瓨鍦ㄦ椂锛屼笉鍋氭煡璇�
+      this.setState({
+        data: [],
+        loading: false
+      })
+      return
+    }
+
+    let searches = mainSearch ? fromJS(mainSearch).toJS() : []
+
+    this.setState({
+      loading: true
+    })
+
+    let _orderBy = config.setting.order || ''
+    let param = UtilsDM.getQueryDataParams(config.setting, arr_field, searches, _orderBy, '', '', BID, menuType)
+
+    let result = await Api.genericInterface(param)
+    if (result.status) {
+      this.setState({
+        data: result.data.map((item, index) => {
+          item.key = index
+          item.$$uuid = item[config.setting.primaryKey] || ''
+          item.$$BID = BID || ''
+          return item
+        }),
+        loading: false
+      })
+    } else {
+      this.setState({
+        loading: false
+      })
+      notification.error({
+        top: 92,
+        message: result.message,
+        duration: 10
+      })
+    }
+  }
+
+  openView = (item) => {
+    const { card } = this.state
+
+    if (card.setting.click === 'menu') {
+      let menu = null
+
+      if (card.setting.menu && card.setting.menu.length > 0) {
+        let menu_id = card.setting.menu.slice(-1)[0]
+        menu = this.props.permMenus.filter(m => m.MenuID === menu_id)[0] || ''
+      }
+
+      if (!menu) {
+        notification.warning({
+          top: 92,
+          message: '鑿滃崟宸插垹闄ゆ垨娌℃湁璁块棶鏉冮檺锛�',
+          duration: 5
+        })
+        return
+      }
+
+      let newtab = {
+        ...menu,
+        selected: true,
+        param: {}
+      }
+
+      if (card.setting.joint === 'true') {
+        newtab.param.BID = item.$$uuid
+      }
+
+      if (['linkage_navigation', 'linkage'].includes(window.GLOB.navBar)) {
+        this.props.modifyTabview([newtab])
+      } else {
+        let tabs = this.props.tabviews.filter((tab, i) => {
+          tab.selected = false
+          return tab.MenuID !== newtab.MenuID
+        })
+
+        if (this.props.tabviews.length !== tabs.length) {
+          this.props.modifyTabview(fromJS(tabs).toJS())
+        }
+
+        this.setState({}, () => {
+          tabs.push(newtab)
+          this.props.modifyTabview(tabs)
+        })
+      }
+    } else if (card.setting.click === 'link') {
+      let src = card.setting.linkurl
+
+      if (src.indexOf('paramsmain/') > -1) {
+        try {
+          let _url = src.split('paramsmain/')[0] + 'paramsmain/'
+          let _param = JSON.parse(window.decodeURIComponent(window.atob(src.split('paramsmain/')[1])))
+
+          _param.UserID = sessionStorage.getItem('UserID')
+          _param.LoginUID = sessionStorage.getItem('LoginUID')
+          _param.User_Name = sessionStorage.getItem('User_Name')
+          _param.param = { BID: item.$$uuid }
+          src = _url + window.btoa(window.encodeURIComponent(JSON.stringify(_param)))
+        } catch {
+          console.warn('鑿滃崟鍙傛暟瑙f瀽閿欒锛�')
+        }
+      } else if (card.setting.joint === 'true') {
+        let con = '?'
+
+        if (/\?/ig.test(src)) {
+          con = '&'
+        }
+
+        src = src + `${con}id=${item.$$uuid}&appkey=${window.GLOB.appkey}&userid=${sessionStorage.getItem('UserID')}&LoginUID=${sessionStorage.getItem('LoginUID') || ''}`
+      }
+
+      window.open(src)
+    }
+  }
+
+  render() {
+    const { config, loading, data, card } = this.state
+
+    return (
+      <div className="custom-data-carousel-box" style={{...config.style, minHeight: config.wrap.minHeight}}>
+        {loading ?
+          <div className="loading-mask">
+            {data ? <div className="ant-spin-blur"></div> : null}
+            <Spin />
+          </div> : null
+        }
+        {data && data.length > 0 ? <Carousel
+          autoplay={config.wrap.autoplay !== 'false'}
+          dots={config.wrap.dots !== 'false'}
+          dotPosition={config.wrap.dotPosition || 'bottom'}
+          effect={config.wrap.effect || 'scrollx'}
+          autoplaySpeed={config.wrap.speed}
+        >
+          {data.map((item, index) => (
+            <div key={index} onClick={() => {this.openView(item)}}>
+              <CardItem card={card} cards={config} data={item}/>
+            </div>
+          ))}
+        </Carousel> : null}
+        {!data || data.length === 0 ? <Empty description={false}/> : null}
+      </div>
+    )
+  }
+}
+
+const mapStateToProps = (state) => {
+  return {
+    permMenus: state.permMenus,
+    tabviews: state.tabviews,
+  }
+}
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    modifyTabview: (tabviews) => dispatch(modifyTabview(tabviews))
+  }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DataCard)
\ No newline at end of file
diff --git a/src/tabviews/custom/components/carousel/data-card/index.scss b/src/tabviews/custom/components/carousel/data-card/index.scss
new file mode 100644
index 0000000..15319b2
--- /dev/null
+++ b/src/tabviews/custom/components/carousel/data-card/index.scss
@@ -0,0 +1,49 @@
+.custom-data-carousel-box {
+  background: #ffffff;
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-size: cover;
+  min-height: 30px;
+
+  .card-item-box {
+    background-position: center center;
+    background-repeat: no-repeat;
+    background-size: cover;
+  }
+
+  .ant-empty {
+    width: 100%;
+    min-height: 100px;
+    padding-top: 15px;
+
+    .ant-empty-image {
+      height: 60px;
+    }
+  }
+  .loading-mask {
+    position: absolute;
+    left: 0px;
+    top: 0;
+    right: 0px;
+    bottom: 0px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    text-align: justify;
+    z-index: 1;
+
+    .ant-spin-blur {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      opacity: 0.5;
+      background: #ffffff;
+    }
+  }
+}
+
+.custom-data-carousel-box::after {
+  content: ' ';
+  display: block;
+  clear: both;
+}
diff --git a/src/tabviews/custom/components/carousel/prop-card/index.jsx b/src/tabviews/custom/components/carousel/prop-card/index.jsx
new file mode 100644
index 0000000..4387d0c
--- /dev/null
+++ b/src/tabviews/custom/components/carousel/prop-card/index.jsx
@@ -0,0 +1,363 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import { is, fromJS } from 'immutable'
+import { connect } from 'react-redux'
+import { Spin, notification, Carousel } from 'antd'
+
+import asyncComponent from '@/utils/asyncComponent'
+import Api from '@/api'
+import UtilsDM from '@/utils/utils-datamanage.js'
+import MKEmitter from '@/utils/events.js'
+import { modifyTabview } from '@/store/action'
+import './index.scss'
+
+const CardItem = asyncComponent(() => import('../cardItem'))
+
+class PropCard extends Component {
+  static propTpyes = {
+    BID: PropTypes.any,              // 鐖剁骇Id
+    data: PropTypes.array,           // 缁熶竴鏌ヨ鏁版嵁
+    config: PropTypes.object,        // 缁勪欢閰嶇疆淇℃伅
+    mainSearch: PropTypes.any,       // 澶栧眰鎼滅储鏉′欢
+    menuType: PropTypes.any,         // 鑿滃崟绫诲瀷
+  }
+
+  state = {
+    BID: '',                   // 涓婄骇ID
+    config: null,              // 鍥捐〃閰嶇疆淇℃伅
+    loading: false,            // 鏁版嵁鍔犺浇鐘舵��
+    sync: false,               // 鏄惁缁熶竴璇锋眰鏁版嵁
+    data: {}                   // 鏁版嵁
+  }
+
+  UNSAFE_componentWillMount () {
+    const { data, initdata, BID } = this.props
+    let _config = fromJS(this.props.config).toJS()
+    let _cols = new Map()
+
+    let _data = {}
+    let _sync = false
+    
+    if (_config.setting && _config.wrap.datatype !== 'static') {
+      _sync = _config.setting.sync === 'true'
+
+      if (_sync && data) {
+        _data = data[_config.dataName] || {}
+        if (_data && Array.isArray(_data)) {
+          _data = _data[0] || {}
+        }
+        _sync = false
+      } else if (_sync && initdata) {
+        _data = initdata || {}
+        if (_data && Array.isArray(_data)) {
+          _data = _data[0] || {}
+        }
+        _sync = false
+      }
+    } else {
+      _data = {}
+    }
+
+    if (_data) {
+      _data.$$BID = BID || ''
+    }
+
+    _config.columns.forEach(item => {
+      _cols.set(item.field, item)
+    })
+
+    _config.subcards.forEach(card => {
+      card.style.height = _config.style.height
+      if (card.setting.click) {
+        card.style.cursor = 'pointer'
+      }
+      card.elements = card.elements.map(item => {
+        if (item.field && _cols.has(item.field)) {
+          item.col = _cols.get(item.field)
+        }
+        return item
+      })
+    })
+
+    _config.wrap.speed = (_config.wrap.speed || 3) * 1000
+
+    this.setState({
+      sync: _sync,
+      data: _data,
+      BID: BID || '',
+      config: _config,
+      arr_field: _config.columns.map(col => col.field).join(','),
+    }, () => {
+      if (_config.wrap.datatype !== 'static' && _config.setting && _config.setting.sync !== 'true' && _config.setting.onload === 'true') {
+        this.loadData()
+      }
+    })
+  }
+
+  componentDidMount () {
+    MKEmitter.addListener('reloadData', this.reloadData)
+    MKEmitter.addListener('resetSelectLine', this.resetParentParam)
+    MKEmitter.removeListener('refreshByButtonResult', this.refreshByButtonResult)
+  }
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return !is(fromJS(this.state), fromJS(nextState))
+  }
+
+  componentWillUnmount () {
+    this.setState = () => {
+      return
+    }
+    MKEmitter.removeListener('reloadData', this.reloadData)
+    MKEmitter.removeListener('resetSelectLine', this.resetParentParam)
+    MKEmitter.removeListener('refreshByButtonResult', this.refreshByButtonResult)
+  }
+
+  /**
+   * @description 鍥捐〃鏁版嵁鏇存柊锛屽埛鏂板唴瀹�
+   */
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    const { sync, config, BID } = this.state
+
+    if (sync && !is(fromJS(this.props.data), fromJS(nextProps.data))) {
+      let _data = {}
+      if (nextProps.data && nextProps.data[config.dataName]) {
+        _data = nextProps.data[config.dataName]
+        if (_data && Array.isArray(_data)) {
+          _data = _data[0]
+        }
+      }
+
+      if (_data) {
+        _data.$$BID = BID || ''
+      }
+
+      this.setState({sync: false, data: _data})
+    } else if (nextProps.mainSearch && !is(fromJS(this.props.mainSearch), fromJS(nextProps.mainSearch))) {
+      if (config.wrap.datatype !== 'static' && config.setting.syncRefresh === 'true') {
+        this.setState({}, () => {
+          this.loadData()
+        })
+      }
+    }
+  }
+
+  /**
+   * @description 鎸夐挳鎵ц瀹屾垚鍚庨〉闈㈠埛鏂�
+   * @param {*} menuId     // 鑿滃崟Id
+   * @param {*} position   // 鍒锋柊浣嶇疆
+   * @param {*} btn        // 鎵ц鐨勬寜閽�
+   */
+  refreshByButtonResult = (menuId, position, btn) => {
+    const { config, BID } = this.state
+
+    if (config.uuid !== menuId) return
+
+    this.loadData()                                                            // 鏁版嵁鍒锋柊
+
+    if (btn.syncComponentId && btn.syncComponentId !== config.uuid && btn.syncComponentId !== config.setting.supModule) {
+      MKEmitter.emit('reloadData', btn.syncComponentId)                        // 鍚岀骇鏍囩鍒锋柊
+    }
+
+    if (position === 'mainline' && config.setting.supModule) {                 // 涓昏〃琛屽埛鏂�
+      MKEmitter.emit('reloadData', config.setting.supModule, (BID || 'empty'))
+    } else if (position === 'popclose') {                                      // 鏍囩鍏抽棴鍒锋柊
+      config.setting.supModule && MKEmitter.emit('reloadData', config.setting.supModule, (BID || 'empty'))
+      btn.$tabId && MKEmitter.emit('refreshPopButton', btn.$tabId)
+    }
+  }
+
+  resetParentParam = (MenuID, id) => {
+    const { config } = this.state
+
+    if (config.wrap.datatype === 'static' || !config.setting.supModule || config.setting.supModule !== MenuID) return
+    if (id !== this.state.BID) {
+      this.setState({ BID: id }, () => {
+        this.loadData()
+      })
+    }
+  }
+
+  reloadData = (menuId) => {
+    const { config } = this.state
+
+    if (menuId !== config.uuid) return
+
+    this.loadData()
+  }
+
+  async loadData () {
+    const { mainSearch, menuType } = this.props
+    const { config, arr_field, BID } = this.state
+
+    if (config.wrap.datatype === 'static') {
+      this.setState({
+        data: {$$BID: BID || ''},
+        loading: false
+      })
+      return
+    } else if (config.setting.supModule && !BID) { // BID 涓嶅瓨鍦ㄦ椂锛屼笉鍋氭煡璇�
+      this.setState({
+        data: {$$BID: BID || ''},
+        loading: false
+      })
+      return
+    }
+
+    let searches = []
+    if (mainSearch && mainSearch.length > 0) { // 涓昏〃鎼滅储鏉′欢
+      let keys = searches.map(item => item.key)
+      mainSearch.forEach(item => {
+        if (!keys.includes(item.key)) {
+          searches.push(item)
+        }
+      })
+    }
+
+    this.setState({
+      loading: true
+    })
+
+    let _orderBy = config.setting.order || ''
+    let param = UtilsDM.getQueryDataParams(config.setting, arr_field, searches, _orderBy, 1, 1, BID, menuType)
+
+    let result = await Api.genericInterface(param)
+    if (result.status) {
+      let _data = result.data && result.data[0] ? result.data[0] : {}
+      _data.$$BID = BID || ''
+
+      this.setState({
+        data: _data,
+        loading: false
+      })
+    } else {
+      this.setState({
+        loading: false
+      })
+      notification.error({
+        top: 92,
+        message: result.message,
+        duration: 10
+      })
+    }
+  }
+
+  openView = (item) => {
+    if (item.setting.click === 'menu') {
+      let menu = null
+
+      if (item.setting.menu && item.setting.menu.length > 0) {
+        let menu_id = item.setting.menu.slice(-1)[0]
+        menu = this.props.permMenus.filter(m => m.MenuID === menu_id)[0] || ''
+      }
+
+      if (!menu) {
+        notification.warning({
+          top: 92,
+          message: '鑿滃崟宸插垹闄ゆ垨娌℃湁璁块棶鏉冮檺锛�',
+          duration: 5
+        })
+        return
+      }
+
+      let newtab = {
+        ...menu,
+        selected: true,
+        param: {}
+      }
+
+      if (item.setting.joint === 'true') {
+        newtab.param.BID = item.setting.primaryId
+      }
+
+      if (['linkage_navigation', 'linkage'].includes(window.GLOB.navBar)) {
+        this.props.modifyTabview([newtab])
+      } else {
+        let tabs = this.props.tabviews.filter((tab, i) => {
+          tab.selected = false
+          return tab.MenuID !== newtab.MenuID
+        })
+
+        if (this.props.tabviews.length !== tabs.length) {
+          this.props.modifyTabview(fromJS(tabs).toJS())
+        }
+
+        this.setState({}, () => {
+          tabs.push(newtab)
+          this.props.modifyTabview(tabs)
+        })
+      }
+    } else if (item.setting.click === 'link') {
+      let src = item.setting.linkurl
+
+      if (src.indexOf('paramsmain/') > -1) {
+        try {
+          let _url = src.split('paramsmain/')[0] + 'paramsmain/'
+          let _param = JSON.parse(window.decodeURIComponent(window.atob(src.split('paramsmain/')[1])))
+
+          _param.UserID = sessionStorage.getItem('UserID')
+          _param.LoginUID = sessionStorage.getItem('LoginUID')
+          _param.User_Name = sessionStorage.getItem('User_Name')
+          _param.param = { BID: item.setting.primaryId }
+          src = _url + window.btoa(window.encodeURIComponent(JSON.stringify(_param)))
+        } catch {
+          console.warn('鑿滃崟鍙傛暟瑙f瀽閿欒锛�')
+        }
+      } else if (item.setting.joint === 'true') {
+        let con = '?'
+
+        if (/\?/ig.test(src)) {
+          con = '&'
+        }
+
+        src = src + `${con}id=${item.setting.primaryId}&appkey=${window.GLOB.appkey}&userid=${sessionStorage.getItem('UserID')}&LoginUID=${sessionStorage.getItem('LoginUID') || ''}`
+      }
+
+      window.open(src)
+    }
+  }
+
+
+  render() {
+    const { config, loading, data } = this.state
+
+    return (
+      <div className="custom-prop-carousel-box" style={config.style}>
+        {loading ?
+          <div className="loading-mask">
+            <div className="ant-spin-blur"></div>
+            <Spin />
+          </div> : null
+        }
+        <Carousel
+          autoplay={config.wrap.autoplay !== 'false'}
+          dots={config.wrap.dots !== 'false'}
+          dotPosition={config.wrap.dotPosition || 'bottom'}
+          effect={config.wrap.effect || 'scrollx'}
+          autoplaySpeed={config.wrap.speed}
+        >
+          {config.subcards.map((item, index) => (
+            <div key={index} onClick={() => {this.openView(item)}}>
+              <CardItem card={item} cards={config} data={data}/>
+            </div>
+          ))}
+        </Carousel>
+      </div>
+    )
+  }
+}
+
+const mapStateToProps = (state) => {
+  return {
+    permMenus: state.permMenus,
+    tabviews: state.tabviews,
+  }
+}
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    modifyTabview: (tabviews) => dispatch(modifyTabview(tabviews))
+  }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(PropCard)
\ No newline at end of file
diff --git a/src/tabviews/custom/components/carousel/prop-card/index.scss b/src/tabviews/custom/components/carousel/prop-card/index.scss
new file mode 100644
index 0000000..b5e46e6
--- /dev/null
+++ b/src/tabviews/custom/components/carousel/prop-card/index.scss
@@ -0,0 +1,42 @@
+.custom-prop-carousel-box {
+  background: #ffffff;
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-size: cover;
+  min-height: 30px;
+  position: relative;
+
+  .card-item-box {
+    position: relative;
+    background-position: center center;
+    background-repeat: no-repeat;
+    background-size: cover;
+  }
+
+  .loading-mask {
+    position: absolute;
+    left: 40px;
+    top: 0;
+    right: 40px;
+    bottom: 0px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    text-align: justify;
+    z-index: 1;
+
+    .ant-spin-blur {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      opacity: 0.5;
+      background: #ffffff;
+    }
+  }
+}
+
+.custom-prop-carousel-box::after {
+  content: ' ';
+  display: block;
+  clear: both;
+}
diff --git a/src/tabviews/custom/index.jsx b/src/tabviews/custom/index.jsx
index 1cf5080..d3f6298 100644
--- a/src/tabviews/custom/index.jsx
+++ b/src/tabviews/custom/index.jsx
@@ -23,6 +23,8 @@
 const AntvTabs = asyncComponent(() => import('./components/tabs/antv-tabs'))
 const DataCard = asyncComponent(() => import('./components/card/data-card'))
 const PropCard = asyncComponent(() => import('./components/card/prop-card'))
+const CarouselDataCard = asyncComponent(() => import('./components/carousel/data-card'))
+const CarouselPropCard = asyncComponent(() => import('./components/carousel/prop-card'))
 const TableCard = asyncComponent(() => import('./components/card/table-card'))
 const MainSearch = asyncComponent(() => import('@/tabviews/zshare/topSearch'))
 const NormalTable = asyncComponent(() => import('./components/table/normal-table'))
@@ -547,7 +549,7 @@
             return cell.eleType !== 'button' || isHS || permAction[cell.uuid]
           })
         })
-      } else if (item.type === 'table' && item.subtype === 'tablecard') {
+      } else if ((item.type === 'table' && item.subtype === 'tablecard') || item.type === 'carousel') {
         item.subcards.forEach(card => {
           let _hasheight = card.style.height && card.style.height !== 'auto'
           card.elements = card.elements.filter(cell => {
@@ -995,6 +997,18 @@
             <PropCard config={item} data={data} BID={_bid} mainSearch={mainSearch} menuType={menuType} />
           </Col>
         )
+      } else if (item.type === 'carousel' && item.subtype === 'datacard') {
+        return (
+          <Col span={item.width} key={item.uuid}>
+            <CarouselDataCard config={item} data={data} BID={_bid} mainSearch={mainSearch} menuType={menuType} />
+          </Col>
+        )
+      } else if (item.type === 'carousel' && item.subtype === 'propcard') {
+        return (
+          <Col span={item.width} key={item.uuid}>
+            <CarouselPropCard config={item} data={data} BID={_bid} mainSearch={mainSearch} menuType={menuType} />
+          </Col>
+        )
       } else if (item.type === 'table' && item.subtype === 'tablecard') {
         return (
           <Col span={item.width} key={item.uuid}>
diff --git a/src/views/menudesign/index.jsx b/src/views/menudesign/index.jsx
index f227a6a..07d3ad2 100644
--- a/src/views/menudesign/index.jsx
+++ b/src/views/menudesign/index.jsx
@@ -384,6 +384,15 @@
               _sort++
             })
           })
+        } else if (item.type === 'carousel') {
+          item.subcards.forEach(card => {
+            card.elements && card.elements.forEach(cell => {
+              if (cell.eleType !== 'button') return
+              this.checkBtn(cell)
+              buttons.push(`select '${cell.uuid}' as menuid, '${item.name + '-' + cell.label}' as menuname, '${_sort * 10}' as Sort`)
+              _sort++
+            })
+          })
         } else if (item.type === 'line' || item.type === 'bar') {
           item.action && item.action.forEach(btn => {
             this.checkBtn(btn)

--
Gitblit v1.8.0