按钮收纳神器 - 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>
相关推荐
sorryhc6 分钟前
【AI解读源码系列】ant design mobile——Avatar头像
前端·javascript·react.js
Mintopia14 分钟前
🎭 一场浏览器里的文艺复兴
前端·javascript·aigc
Mintopia14 分钟前
🎬《Next 全栈 CRUD 的百老汇》
前端·后端·next.js
AryaNimbus29 分钟前
你不知道的Cursor系列:如何使用Cursor同时开发多项目?
前端·ai编程·cursor
国家不保护废物33 分钟前
Function Call与MCP:给AI插上连接现实的翅膀
前端·aigc·openai
500佰34 分钟前
阿里Qoder AI 新开发工具,长期记忆、Wiki和Quest模式是它的独有特性
前端
Juchecar35 分钟前
Vue3 Class 和 Style 绑定详解
前端·vue.js
coding随想36 分钟前
揭秘DOM键盘事件:从基础到高级技巧全解析!
前端
xianxin_38 分钟前
CSS Position(定位)
前端
xianxin_38 分钟前
CSS Float(浮动)
前端