ButtonGroup.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <template>
  2. <RenderButtonGroup />
  3. </template>
  4. <script setup lang="tsx" name="ButtonGroup">
  5. import { ref, watch, computed, nextTick, onMounted, onUnmounted, onDeactivated, onActivated } from "vue";
  6. import type { BreakPoint } from "@/components/Grid/interface/index";
  7. import mittBus from "@/utils/mittBus";
  8. import { MittEnum } from "@/enums/mittEnum";
  9. import { cloneDeep, pick, get, omit, defaultsDeep } from "lodash-es";
  10. import { More, ArrowDown, ArrowRight } from "@element-plus/icons-vue";
  11. import { isArray, isFunction, isObject } from "@/utils/is";
  12. type Props = {
  13. /**
  14. * @description 父组件id值
  15. */
  16. target?: any;
  17. buttons: any;
  18. assemblySize: string;
  19. data: any;
  20. update: any;
  21. };
  22. const props = withDefaults(defineProps<Props>(), {
  23. assemblySize: ""
  24. });
  25. /**
  26. * @description 按钮默认宽度px
  27. */
  28. const itemWidth = 80;
  29. const innerwidth = ref(0);
  30. const skeletonItemSize = computed(() => {
  31. let _size = "";
  32. switch (props.assemblySize) {
  33. case "large":
  34. _size = "width: 90px; height: 40px";
  35. break;
  36. case "small":
  37. _size = "width: 64px; height: 24px";
  38. break;
  39. default:
  40. _size = "width: 80px; height: 32px";
  41. break;
  42. }
  43. return _size;
  44. });
  45. /**
  46. * 排除多余属性,仅展示需要的属性继承
  47. * @param data Object
  48. */
  49. const omitAttr = (data: any) => {
  50. return omit(data, ["label", "power", "clickFunc", "slotName", "render", "disabledTextCallBack"]);
  51. };
  52. /**
  53. * @description 监听框架属性变化
  54. */
  55. mittBus.on(MittEnum.FoldLayoutChange, layout => {
  56. console.log("ittBus.on(MittEnum.FoldLayoutChang :>> ", layout);
  57. nextTick(() => {
  58. resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent);
  59. });
  60. });
  61. // 监听屏幕变化
  62. const resize = (e: UIEvent) => {
  63. nextTick(() => {
  64. let width = (e.target as Window).innerWidth;
  65. if (props.target) {
  66. let _target = document.getElementById(props.target) as HTMLElement;
  67. _target && (width = _target.offsetWidth);
  68. }
  69. console.log("resize bg width :>> ", width);
  70. innerwidth.value = width;
  71. });
  72. };
  73. /**
  74. * @description 过滤出合法操作的按钮
  75. * @param data buttongroup [[],{}]
  76. */
  77. const getNotLimited = (data: any) => {
  78. let arr = data.map((item: any) => {
  79. if (isArray(item)) {
  80. return item.filter(itm => {
  81. if (isFunction(itm?.limited)) {
  82. return !itm?.limited();
  83. } else {
  84. return !itm?.limited;
  85. }
  86. });
  87. } else if (isObject(item)) {
  88. if (isFunction(item?.limited)) {
  89. return !item?.limited() && item;
  90. } else {
  91. return !item?.limited && item;
  92. }
  93. }
  94. });
  95. return arr.filter((item: any) => item);
  96. };
  97. const getIconSlot = (item: any) => {
  98. // console.log("item , item.icon, typeof item.icon :>> ", JSON.stringify(item), item.icon, typeof item.icon);
  99. let iconRender = {};
  100. switch (typeof item.icon) {
  101. case "function":
  102. iconRender = { icon: () => item.icon() };
  103. break;
  104. case "string":
  105. iconRender = {
  106. icon: () => {
  107. return <i class={["iconfont", item.icon]} />;
  108. }
  109. };
  110. // delete item.icon;
  111. break;
  112. default:
  113. break;
  114. }
  115. return iconRender;
  116. };
  117. /**
  118. * @description el-skeleton loading
  119. */
  120. const isLoading = computed(() => {
  121. if (typeof props.data == "undefined") {
  122. return false;
  123. } else {
  124. return Object.keys(props.data).length == 0;
  125. }
  126. });
  127. /**
  128. * @description 按钮组渲染
  129. */
  130. const buttonItemRender = (item: any) => {
  131. // console.log("buttonItemRender item :>> ", item);
  132. if (!item) return;
  133. let iconRender = getIconSlot(item);
  134. let _item = cloneDeep(item);
  135. typeof _item.icon == "string" && delete _item.icon;
  136. // let btnDisabledTxt: any = computed(() => {
  137. // return props.data && item.disabledTextCallBack && item.disabledTextCallBack(props.data);
  138. // });
  139. let btnDisabledTxt = props.data && item.disabledTextCallBack ? item.disabledTextCallBack(props.data) : "";
  140. let _loading = isFunction(item.loading) ? item.loading() : false;
  141. // 排除多余属性,仅展示需要的属性继承
  142. let btnAttr = omitAttr(_item);
  143. const childrenSlot = {
  144. template: () => {
  145. return <el-skeleton-item variant="button" class="mr-4" style={skeletonItemSize} />;
  146. },
  147. default: () => {
  148. if (item.render) {
  149. return item.render(item);
  150. } else if (item.hasOwnProperty("disabledTextCallBack")) {
  151. let disabled = Boolean(btnDisabledTxt);
  152. return (
  153. <el-tooltip
  154. class="box-item"
  155. disabled={!disabled}
  156. effect="dark"
  157. content={btnDisabledTxt}
  158. placement="top"
  159. enterable={false}
  160. hide-after={0}
  161. >
  162. <el-button
  163. {...btnAttr}
  164. class={{ "is-disabled": disabled }}
  165. disabled={disabled}
  166. loading={_loading}
  167. onClick={item.clickFunc}
  168. v-slots={iconRender}
  169. size={props.assemblySize}
  170. >
  171. {item.label}
  172. </el-button>
  173. </el-tooltip>
  174. );
  175. } else {
  176. return (
  177. <el-button
  178. size={props.assemblySize}
  179. {...btnAttr}
  180. disabled={_loading}
  181. loading={_loading}
  182. onClick={item.clickFunc}
  183. v-slots={iconRender}
  184. >
  185. {item.label}
  186. </el-button>
  187. );
  188. }
  189. }
  190. };
  191. return <el-skeleton animated={true} loading={isLoading.value} class="mr-12" v-slots={childrenSlot}></el-skeleton>;
  192. };
  193. /**
  194. * @description 折叠后,按钮组渲染
  195. */
  196. const buttonsMenuRender = (item: any) => {
  197. if (!item) return;
  198. // console.log("buttonsMenuRender item :>> ", item);
  199. let iconRender = getIconSlot(item);
  200. let _item = cloneDeep(item);
  201. typeof _item.icon == "string" && delete _item.icon;
  202. let btnDisabledTxt = props.data && item.disabledTextCallBack ? item.disabledTextCallBack(props.data) : "";
  203. // console.log("btnDisabledTxt :>> ", btnDisabledTxt.value);
  204. // 排除多余属性,仅展示需要的属性继承
  205. let btnAttr = omitAttr(_item);
  206. let _loading = isFunction(item?.loading) ? item.loading() : false;
  207. if (item.render) {
  208. // return item.render(item);
  209. return <el-dropdown-item>{item.render(item)}</el-dropdown-item>;
  210. } else if (item.hasOwnProperty("disabledTextCallBack")) {
  211. let disabled = Boolean(btnDisabledTxt);
  212. return (
  213. <el-dropdown-item {...btnAttr}>
  214. <el-tooltip
  215. class="box-item"
  216. disabled={!disabled}
  217. effect="dark"
  218. content={btnDisabledTxt}
  219. placement="right"
  220. enterable={false}
  221. hide-after={0}
  222. >
  223. <el-text
  224. text
  225. {...btnAttr}
  226. class={{ "is-disabled": disabled, "flx-1": true }}
  227. disabled={disabled || _loading}
  228. loading={_loading}
  229. onClick={item.clickFunc}
  230. v-slots={iconRender}
  231. >
  232. {item.label}
  233. </el-text>
  234. </el-tooltip>
  235. </el-dropdown-item>
  236. );
  237. } else {
  238. return (
  239. <el-dropdown-item {...btnAttr} disabled={_loading} loading={_loading} onClick={item.clickFunc} class="flx-1">
  240. {item.label}
  241. </el-dropdown-item>
  242. );
  243. }
  244. };
  245. const buttonGroupInnderRender = (item: any, ifMenu: boolean, placement: string) => {
  246. return (
  247. <div class="flx-center">
  248. <span class="flx-1">{item.label}</span>
  249. {!ifMenu && <el-icon>{placement == "bottom" ? <ArrowDown /> : <ArrowRight />}</el-icon>}
  250. </div>
  251. );
  252. };
  253. /**
  254. * @description 按钮组,下拉菜单渲染器
  255. * @param item
  256. * @param placement el-dropdown 菜单弹出位置
  257. */
  258. const bottonDropdownRender = (item: any, placement?: string) => {
  259. if (!item) return;
  260. let _item = cloneDeep(item[0]);
  261. let iconRender = getIconSlot(item[0]);
  262. let _placement = placement ?? "bottom";
  263. /**
  264. * @description 判断是否为菜单按钮
  265. */
  266. let ifMenu = !_item.hasOwnProperty("clickFunc");
  267. let idx = ifMenu ? 1 : 0; // 若是菜单按钮,下拉菜单跳过第一个
  268. let _buttonsRd = item.map((itm: any, index: number) => {
  269. return index >= idx && buttonsMenuRender(itm);
  270. });
  271. _buttonsRd = _buttonsRd.filter((itm: any) => itm);
  272. console.log("_buttonsRd menu :>> ", _buttonsRd);
  273. let _dropdown = {
  274. dropdown: () => {
  275. return (
  276. <>
  277. <el-dropdown-menu>{_buttonsRd}</el-dropdown-menu>
  278. </>
  279. );
  280. }
  281. };
  282. typeof _item.icon == "string" && delete _item.icon;
  283. let _btnAttr = omitAttr(_item);
  284. let btnDisabledTxt = props.data && _item.disabledTextCallBack ? _item.disabledTextCallBack(props.data) : "";
  285. let disabled = Boolean(btnDisabledTxt);
  286. let _loading = isFunction(_item.loading) ? _item.loading() : false;
  287. const childrenSlot = {
  288. template: () => {
  289. return <el-skeleton-item variant="button" class="mr-4" style={skeletonItemSize} />;
  290. },
  291. default: () => {
  292. return (
  293. <el-dropdown v-slots={_dropdown} placement={_placement} popper-class="button-group__menu">
  294. {_placement == "bottom" ? (
  295. <span>
  296. <el-tooltip
  297. class="box-item"
  298. disabled={!disabled}
  299. effect="dark"
  300. content={btnDisabledTxt}
  301. placement="top"
  302. enterable={false}
  303. hide-after={0}
  304. >
  305. <el-button
  306. {..._btnAttr}
  307. class={{ "is-disabled": disabled }}
  308. disabled={disabled || _loading}
  309. loading={_loading}
  310. size={props.assemblySize}
  311. onClick={_item.clickFunc}
  312. v-slots={iconRender}
  313. style={{ cursor: ifMenu ? "default" : "pointer" }}
  314. >
  315. {buttonGroupInnderRender(_item, ifMenu, _placement)}
  316. </el-button>
  317. </el-tooltip>
  318. </span>
  319. ) : (
  320. <span>
  321. <el-tooltip
  322. class="box-item"
  323. disabled={!disabled}
  324. effect="dark"
  325. content={btnDisabledTxt}
  326. placement="top"
  327. enterable={false}
  328. hide-after={0}
  329. >
  330. <el-text
  331. {..._btnAttr}
  332. class={{ "is-disabled": disabled, "menu-more__text": true }}
  333. disabled={disabled || _loading}
  334. loading={_loading}
  335. onClick={_item.clickFunc}
  336. v-slots={iconRender}
  337. >
  338. {buttonGroupInnderRender(_item, ifMenu, _placement)}
  339. </el-text>
  340. </el-tooltip>
  341. </span>
  342. )}
  343. </el-dropdown>
  344. );
  345. }
  346. };
  347. return <el-skeleton animated={true} loading={isLoading.value} v-slots={childrenSlot}></el-skeleton>;
  348. };
  349. const RenderButtonGroup = () => {
  350. let btnListRender: any = [];
  351. let menuListRender: any = [];
  352. // console.log("rProp.buttons :>> ", props.buttons);
  353. let butttons = [];
  354. if (props.buttons?.length) {
  355. // console.log("innerwidth.value :>> ", innerwidth.value);
  356. let keyIndex = Math.floor(innerwidth.value / itemWidth) - 1;
  357. // butttons = props.buttons.filter((item: any) => !item?.limited);
  358. butttons = getNotLimited(props.buttons);
  359. // console.log("keyIndex :>> ", keyIndex, butttons.length, innerwidth.value, butttons);
  360. btnListRender = butttons.map((item: any, index: number) => {
  361. console.log("index, keyIndex, item :>> ", index, keyIndex, item);
  362. if (index + 1 > keyIndex) return;
  363. if (isArray(item)) {
  364. if (item.length > 1) {
  365. return bottonDropdownRender(item);
  366. } else {
  367. return buttonItemRender(item[0]);
  368. }
  369. } else {
  370. return buttonItemRender(item);
  371. }
  372. });
  373. btnListRender = btnListRender.filter((item: any) => item);
  374. // console.log("RenderButtonGroup btnListRender :>> ", btnListRender, butttons);
  375. let curButtons = butttons?.filter(item => !Array.isArray(item) || item.length > 0);
  376. if (btnListRender.length < curButtons?.length) {
  377. let dropdownRender = butttons.map((item: any, index: number) => {
  378. if (index + 1 <= keyIndex) return;
  379. if (isArray(item)) {
  380. let _menuRender = null;
  381. if (item.length > 1) {
  382. _menuRender = bottonDropdownRender(item, "right-start");
  383. } else {
  384. _menuRender = buttonsMenuRender(item[0]);
  385. }
  386. return _menuRender;
  387. } else {
  388. return buttonsMenuRender(item);
  389. }
  390. });
  391. dropdownRender = dropdownRender.filter((item: any) => item);
  392. console.log("dropdownRender :>> ", dropdownRender);
  393. let _buttonsRdMenu = dropdownRender.map((itm: any) => {
  394. return (
  395. <>
  396. <el-dropdown-item>{itm}</el-dropdown-item>
  397. </>
  398. );
  399. });
  400. let dropdowmSlot = {
  401. dropdown: () => {
  402. return (
  403. <>
  404. <el-dropdown-menu>{_buttonsRdMenu}</el-dropdown-menu>
  405. </>
  406. );
  407. }
  408. };
  409. let childrenSlot = {
  410. template: () => {
  411. return <el-skeleton-item variant="button" style={skeletonItemSize} />;
  412. },
  413. default: () => {
  414. return (
  415. <>
  416. <el-dropdown popper-class="button-group__menu-more" v-slots={dropdowmSlot} trigger="click">
  417. <el-button text circle icon={More}></el-button>
  418. </el-dropdown>
  419. </>
  420. );
  421. }
  422. };
  423. menuListRender.push(<el-skeleton animated loading={isLoading.value} class="mr-12" v-slots={childrenSlot}></el-skeleton>);
  424. }
  425. }
  426. // console.log("btnListRender :>> ", btnListRender);
  427. // console.log("RenderButtonGroup menuListRender :>> ", menuListRender);
  428. return (
  429. <>
  430. <el-button-group class="detail-menu__btn-group">{btnListRender}</el-button-group>
  431. {menuListRender.length > 0 && menuListRender}
  432. </>
  433. );
  434. };
  435. watch(
  436. () => props.update,
  437. () => {
  438. nextTick(() => {
  439. resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent);
  440. });
  441. }
  442. );
  443. onMounted(() => {
  444. resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent);
  445. window.addEventListener("resize", resize);
  446. });
  447. onActivated(() => {
  448. resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent);
  449. window.addEventListener("resize", resize);
  450. });
  451. onUnmounted(() => {
  452. window.removeEventListener("resize", resize);
  453. });
  454. onDeactivated(() => {
  455. window.removeEventListener("resize", resize);
  456. });
  457. </script>
  458. <style lang="scss">
  459. .button-group__menu {
  460. .el-dropdown-menu__item {
  461. min-width: 60px;
  462. }
  463. }
  464. .button-group__menu-more {
  465. .el-dropdown-menu.el-dropdown-menu--default > .el-dropdown-menu__item {
  466. padding: 0 !important;
  467. }
  468. .menu-more__text {
  469. display: block;
  470. padding: 5px 16px;
  471. line-height: $space-a3;
  472. }
  473. }
  474. .detail-menu__btn-group {
  475. display: inline-flex;
  476. & > * {
  477. border: none;
  478. .el-button {
  479. border: none;
  480. }
  481. }
  482. & > .el-dropdown *:focus-visible {
  483. outline: unset;
  484. }
  485. }
  486. </style>