按钮收纳神器 - ActionRender

按钮收纳效果对比

思路

  1. ActionRender 组件内获取并遍历$slots.default 虚拟节点
  2. 根据每个按钮要放置的位置分类为外部按钮集合groupList下拉按钮集合moreList
  3. 使用render函数动态渲染两个集合的vNode节点

props扩展

  1. 展示个数自定义 btnNumber(默认为2个)
  2. 按钮间距自定义 gap, 内部使用antdv 的Space 组件
  3. 组件使用位置 inRow 行内、其他
  4. 下拉按钮文字填充text
  5. ActionRender组件内部使用mergeProps设置groupList 和 moreList中按钮的props (type,size) ,使用时无需配置,如配置优先使用配置的属性

完整代码

ActionRender.vue 复制代码
<script>
import { merge, pick, omit } from 'lodash'
import mergeProps from '@vue/babel-helper-vue-jsx-merge-props'

const props = {
  inLine: { type: 'link', size: 'small' },
  outLine: { type: 'primary', size: 'default' },
}

// 在vnode生成后的VnodeData中使用mergeProps 将事件和属性合并进去
function mergeVNodeProps(vnode, ...args) {
  if (vnode.componentOptions) {
    const { propsData, listeners, Ctor } = vnode.componentOptions

    // 为了便于合并, 我们只取on和attrs两个参数,分别合并事件和属性
    const { on, attrs } = mergeProps([
      { on: listeners, attrs: propsData },
      ...args,
    ])
    // 获取组件中定义的props
    const keys = Object.keys(Ctor.options.props || {})
    // 分离出组件需要的propsData和attrs
    const [$props, $attrs] = [pick(attrs, keys), omit(attrs, keys)]
    // 这里偷懒使用lodash.merge函数进行递归合并
    merge(vnode, {
      data: { attrs: $attrs },
      componentOptions: {
        listeners: on,
        propsData: $props,
      },
    })
  } else {
    // 非组件VNode,直接合并data。但需要注意事件使用的是on而非nativeOn
    // 属性根据情况可以使用attrs或domProps,这二者的区别见后文
    vnode.data = mergeProps([vnode.data, ...args])
  }
  return vnode
}

function setNewPropos(vNodes, lay, isInMore) {
  return vNodes.map((vNode) => {
    if (vNode?.componentOptions?.tag === 'a-popconfirm') {
      const canMap =
        Array.isArray(vNode?.componentOptions?.children) &&
        vNode?.componentOptions?.children.length
      if (canMap) {
        const newChildren = vNode.componentOptions.children.map(
          (vNodeChild) => {
            const childNewItem = mergeVNodeProps(
              vNodeChild,
              { on: { click: (e) => e.stopPropagation() } },
              { attrs: { ...props[`${lay}`] } }
            )
            return childNewItem
          }
        )
        vNode.componentOptions.children = newChildren
      }
      const item = isInMore
        ? mergeVNodeProps(vNode, {
            attrs: {
              getPopupContainer: (node) => node.parentNode,
              placement: 'topRight',
              overlayStyle: { width: '155px' },
            },
          })
        : vNode
      return item
    } else if (vNode?.componentOptions?.tag === 'a-button') {
      const item = mergeVNodeProps(
        vNode,
        { on: { click: (e) => e.stopPropagation() } },
        { attrs: { ...props[`${lay}`] } }
      )
      return item
    } else {
      return vNode
    }
  })
}

export function isEmptyElement(c) {
  const isHidden = c.data?.directives?.filter(
    (item) => ['has', 'show'].includes(item.name) && !item.value
  )?.length
  return !c.tag || isHidden
}

function filterEmpty(children = []) {
  return children.filter((c) => !isEmptyElement(c))
}

export default {
  name: 'EhActionRender',
  props: {
    btnNumber: {
      type: [Number, String],
      default: 2,
    },
    /**
     * small | middle | large | number
     */
    gap: {
      type: [Number, String],
      default: 'small',
    },
    text: {
      type: String,
      default: '更多',
    },
    /**
     * 放置的位置 inRow 行内操作列,
     */
    inRow: {
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {
      visible: false,
    }
  },
  render() {
    const { gap, inRow, text, btnNumber } = this
    const ifRenderList = filterEmpty(this.$slots?.default)
    const groupList = () => {
      const vNodes =
        ifRenderList.length >= btnNumber
          ? ifRenderList.slice(0, btnNumber)
          : ifRenderList
      return setNewPropos(vNodes, inRow ? 'inLine' : 'outLine')
    }
    const moreList = () => {
      const vNodes =
        ifRenderList.length >= btnNumber ? ifRenderList.slice(btnNumber) : []
      return setNewPropos(vNodes, 'inLine', true)
    }
    const showMore = moreList().length > 0
    const showGroup = groupList().length > 0

    const groupVNodes = () => groupList()?.map((v) => v)
    const moreVNodes = () =>
      moreList()?.map((v, i) => {
        return <a-menu-item key={i}>{v}</a-menu-item>
      })
    return (
      <a-space size={inRow ? 0 : gap}>
        {showGroup && groupVNodes()}
        {showMore && (
          <a-dropdown>
            <a-button
              type={inRow ? 'link' : 'default'}
              size={inRow ? 'small' : 'default'}
            >
              {text}
              <a-icon type="down" style={{ marginLeft: 0 }} />
            </a-button>
            <a-menu slot="overlay">{moreVNodes()}</a-menu>
          </a-dropdown>
        )}
      </a-space>
    )
  },
}
</script>

xml 复制代码
<a-table ...>
     <template #action2="text, record">
        <eh-action-render>
          <a-button @click="handleDetail(record)">查看</a-button>
          <a-button @click="handleEdit(record)">编辑</a-button>
          <a-button @click="handleEdit(record)">重算</a-button>
          <a-button @click="handleEdit(record)">推送</a-button>
          <a-popconfirm
            title="确定要删除吗?"
            @confirm="handleDelete(record.id)"
          >
            <a-button>删除</a-button>
          </a-popconfirm>
        </eh-action-render>
      </template>
</a-table>
相关推荐
O败者食尘D9 分钟前
【Vue2】结合chrome与element-ui的网页端条码打印
前端·vue.js·chrome
橘颂TA1 小时前
【C++】C++11特性的介绍和使用(第三篇)
前端·c++·算法·c++11
爷_7 小时前
字节跳动震撼开源Coze平台!手把手教你本地搭建AI智能体开发环境
前端·人工智能·后端
charlee449 小时前
行业思考:不是前端不行,是只会前端不行
前端·ai
Amodoro10 小时前
nuxt更改页面渲染的html,去除自定义属性、
前端·html·nuxt3·nuxt2·nuxtjs
Wcowin10 小时前
Mkdocs相关插件推荐(原创+合作)
前端·mkdocs
伍哥的传说11 小时前
CSS+JavaScript 禁用浏览器复制功能的几种方法
前端·javascript·css·vue.js·vue·css3·禁用浏览器复制
lichenyang45311 小时前
Axios封装以及添加拦截器
前端·javascript·react.js·typescript
Trust yourself24311 小时前
想把一个easyui的表格<th>改成下拉怎么做
前端·深度学习·easyui
三口吃掉你11 小时前
Web服务器(Tomcat、项目部署)
服务器·前端·tomcat