index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. <template>
  2. <div class="header-actions" v-if="getActionList.length != 0">
  3. <div class="overflow-box">
  4. <el-button v-for="(item, index) in getActionList" :key="index" :type="item.type || 'primary'" :plain="item.plain || false"
  5. v-bind="getHeaderActions(item)" @click="item.action" :disabled="item.disabled || false">
  6. {{ item.text }}
  7. </el-button>
  8. </div>
  9. </div>
  10. <!-- statWarpHeight > 200 && -->
  11. <div class="stat-warp" v-if="statConfig.length != 0" :class="isMore ? 'show-more' : ''">
  12. <div class="title">
  13. <select v-model="statSelectVal" v-if="statConfig.length > 1" @change="changeStatData">
  14. <option :value="index" v-for="(i, index) in statConfig" :key="index">
  15. {{ i.label }}
  16. </option>
  17. </select>
  18. <div v-if="statConfig.length === 1">{{ statConfig[0].label }}</div>
  19. </div>
  20. <!-- v-if="statWarpHeight > 200" -->
  21. <div class="more-btn" @click="isMore = !isMore">
  22. <span>
  23. <i v-if="!isMore" class="iconfont icon-btn_xiala22"></i>
  24. <i v-else class="iconfont icon-btn_shouqi22"></i>
  25. </span>
  26. </div>
  27. <ul id="statWarp">
  28. <li v-show="!i.data" :class="'theme' + i.type" v-for="(i, index) in statConfig[statSelectVal].data" :key="index"
  29. @click="i.click ? i.click(i, index) : ''" :style="i.click ? 'cursor: pointer' : ''">
  30. <div class="label">{{ i.label }}</div>
  31. <div class="num">{{ i.num }}</div>
  32. </li>
  33. <li v-show="i.data" v-for="(i, index) in statConfig[statSelectVal].data" :key="index" class="multi-data" :class="'theme' + i.type"
  34. @click="i.click ? i.click(i, index) : ''" :style="i.click ? 'cursor: pointer' : ''">
  35. <div class="label">{{ i.label }}</div>
  36. <div class="num-warp">
  37. <div class="num-box" v-for="(j, jindex) in i.data" :key="jindex">
  38. <div class="num-small" :style="'color:' + j.color">
  39. {{ j.num }}
  40. </div>
  41. <div class="label-small">{{ j.label }}</div>
  42. </div>
  43. </div>
  44. </li>
  45. </ul>
  46. </div>
  47. <div class="table-list-container by-table" v-loading="loading">
  48. <!-- v-if="!hideHeader" -->
  49. <header v-if="false" class="header">
  50. <h2>{{ title }}</h2>
  51. </header>
  52. <div class="by-search" v-if="!hideSearch">
  53. <div style="display: flex">
  54. <div class="by-dropdown" v-for="(i, index) in selectConfigCopy" :key="i.prop" style="margin-right: 10px">
  55. <div class="by-dropdown-title">
  56. {{
  57. pagination[i.prop]
  58. ? i.data.find((j) => j.value === pagination[i.prop])
  59. ? i.data.find((j) => j.value === pagination[i.prop]).label
  60. : i.label
  61. : i.labelCopy
  62. }}
  63. <!-- {{ i.label || i.labelCopy }} -->
  64. <i style="margin-left: 5px" class="iconfont icon-iconm_xialan1"></i>
  65. </div>
  66. <ul class="by-dropdown-lists">
  67. <li @click="searchItemSelct('all', i, index)" v-if="i.isShowAll === false ? i.isShowAll : true" style="">
  68. {{ $t("common.all") }}
  69. </li>
  70. <li v-for="j in i.data" :key="j.value" @click="searchItemSelct(j, i)" style="">
  71. {{ j.label }}
  72. </li>
  73. </ul>
  74. </div>
  75. </div>
  76. <div style="display: flex">
  77. <el-input :placeholder="$t('common.pleaseEnterKeywords')" suffix-icon="search" size="mini" v-model="pagination.keyword"
  78. @keyup.enter="searchFn">
  79. </el-input>
  80. <el-button type="primary" style="margin-left: 10px" size="default" v-no-double-click="searchFn">{{ $t("common.search") }}</el-button>
  81. <div class="more-icon" @click="retrievalModalFn" v-if="$attrs.onMoreSearch">
  82. <i class="iconfont icon-iconx_saixuan"></i>
  83. </div>
  84. </div>
  85. </div>
  86. <component :is="containerTag">
  87. <div class="filter-form-container">
  88. <slot />
  89. </div>
  90. <el-table ref="hocElTable" :data="source" v-if="!hideTable" style="width: 100%" v-bind="$attrs" v-on="tableEvents" row-key="id" lazy
  91. :load="load" :tree-props="{
  92. children: 'children',
  93. hasChildren: 'hasChildren',
  94. }" :height="tableHeight">
  95. <el-table-column v-for="(item, index) in config" :key="index" v-bind="getAttrsValue(item)" :type="item.type || ''" :selectable="
  96. (rowData, rowIndex) => isSelectable(rowData, rowIndex, item)
  97. ">
  98. <template #default="scope" v-if="!item.type">
  99. <slot :name="item.attrs.slot" :item="scope.row" v-if="item.attrs.slot">
  100. 插槽占位符
  101. </slot>
  102. <div v-else-if="isFunction(getValue(scope, item))">
  103. <component :is="renderTypeList[getMatchRenderFunction(item)].target" :cell-list="getValue(scope, item)()" :row="scope.row"
  104. :parent="getParent" @click="
  105. ($event) => {
  106. handleNativeClick(getAttrsValue(item), $event, item);
  107. }
  108. " />
  109. </div>
  110. <div v-else>
  111. {{ getValue(scope, item) }}
  112. </div>
  113. </template>
  114. </el-table-column>
  115. </el-table>
  116. <el-row v-if="!hidePagination" class="table-pagination" justify="end" type="flex">
  117. <el-pagination background layout="total, sizes, prev, pager, next, jumper" :current-page="getPagination.pageNum"
  118. :page-size="getPagination.pageSize" :page-sizes="[10,50,100,500,1000]" :total="getPagination.total"
  119. @size-change="handleSizeChange" @current-change="handlePageChange" />
  120. </el-row>
  121. </component>
  122. </div>
  123. </template>
  124. <script>
  125. import { isFunction as isFn, isBoolean } from "./type";
  126. import ElementsMapping from "./ElementsMapping";
  127. import ComponentsMapping from "./ComponentsMapping";
  128. import { computed, defineComponent, getCurrentInstance, ref, watch } from "vue";
  129. import expand from "./expand";
  130. import Sortable from "sortablejs";
  131. export default defineComponent({
  132. name: "Table",
  133. components: {
  134. ElementsMapping,
  135. ComponentsMapping,
  136. },
  137. props: {
  138. hideSearch: {
  139. type: Boolean,
  140. default: false,
  141. },
  142. hideTable: {
  143. type: Boolean,
  144. default: false,
  145. },
  146. //顶部搜索下拉配置
  147. selectConfig: {
  148. type: Array,
  149. default() {
  150. return [];
  151. },
  152. },
  153. // 获取表格元数据时携带的参数
  154. filterParams: {
  155. type: Object,
  156. default() {
  157. return {};
  158. },
  159. },
  160. // 表格加载 loading
  161. loading: {
  162. type: Boolean,
  163. default: false,
  164. },
  165. // 表格名称
  166. title: {
  167. type: String,
  168. default: "",
  169. },
  170. // 表格元数据
  171. source: {
  172. type: Array,
  173. required: true,
  174. default() {
  175. return [];
  176. },
  177. },
  178. tableHeight: {
  179. type: Number,
  180. required: false,
  181. },
  182. searchConfig: {
  183. type: Object,
  184. default() {
  185. return {
  186. keyword: "",
  187. };
  188. },
  189. },
  190. statConfig: {
  191. type: Array,
  192. default() {
  193. return [];
  194. },
  195. },
  196. // 指定外层容器的渲染组件
  197. containerTag: {
  198. type: String,
  199. default: "div",
  200. },
  201. // 是否隐藏表头
  202. hideHeader: {
  203. type: Boolean,
  204. default: false,
  205. },
  206. // 是否隐藏分页
  207. hidePagination: {
  208. type: Boolean,
  209. default: false,
  210. },
  211. // 分页配置
  212. pagination: {
  213. type: Object,
  214. default() {
  215. return {};
  216. },
  217. },
  218. // 表格配置文件
  219. config: {
  220. type: Array,
  221. default() {
  222. return [];
  223. },
  224. },
  225. // 表头右上方的按钮组
  226. actionList: {
  227. type: Array,
  228. default() {
  229. return [{ text: "", action: () => {} }];
  230. },
  231. },
  232. // element table 原生事件
  233. tableEvents: {
  234. type: Object,
  235. default() {
  236. return {};
  237. },
  238. },
  239. searchKey: {
  240. type: String,
  241. default: "keyword",
  242. },
  243. // 是否显示过滤的全部选项
  244. // isShowAll: {
  245. // type: Boolean,
  246. // default: true,
  247. // },
  248. },
  249. setup(props) {
  250. const { proxy } = getCurrentInstance();
  251. const selectConfigCopy = computed(() => {
  252. return props.selectConfig.map((item) => {
  253. if (!item.labelCopy) item.labelCopy = { ...item }.label;
  254. return item;
  255. });
  256. });
  257. let isMore = ref(false);
  258. const changeStatData = () => {
  259. statWarpHeight.value = document.getElementById("statWarp").offsetHeight;
  260. };
  261. let statWarpHeight = ref(0);
  262. watch(
  263. proxy.statConfig,
  264. (newValue, oldValue) => {
  265. setTimeout(() => {
  266. //获取statWarp的height
  267. statWarpHeight.value =
  268. document.getElementById("statWarp").offsetHeight;
  269. }, 500);
  270. },
  271. { immediate: true }
  272. );
  273. let statSelectVal = ref(0);
  274. const retrievalModal = ref(false);
  275. const getAttrsValue = (item) => {
  276. const { attrs } = item;
  277. const result = {
  278. ...attrs,
  279. };
  280. delete result.prop;
  281. return result;
  282. };
  283. const renderTypeList = ref({
  284. render: {},
  285. renderHTML: {
  286. target: "elements-mapping",
  287. },
  288. renderComponent: {
  289. target: "components-mapping",
  290. },
  291. renderMoreBtn: {
  292. target: "more-btn",
  293. },
  294. });
  295. const getParent = computed(() => {
  296. return proxy.$parent;
  297. });
  298. const getPagination = computed(() => {
  299. const params = {
  300. pageNum: 1,
  301. pageSize: 10,
  302. total: 0,
  303. };
  304. return Object.assign({}, params, props.pagination);
  305. });
  306. const getActionList = computed(() => {
  307. return props.actionList
  308. .slice()
  309. .reverse()
  310. .filter((it) => it.text);
  311. });
  312. const getValue = (scope, configItem) => {
  313. const prop = configItem.attrs.prop;
  314. const renderName = getMatchRenderFunction(configItem);
  315. const renderObj = renderTypeList.value[renderName];
  316. if (renderObj && isFunction(configItem[renderName])) {
  317. return renderObj.target
  318. ? getRenderValue(scope, configItem, {
  319. name: renderName,
  320. type: "bind",
  321. })
  322. : getRenderValue(scope, configItem);
  323. }
  324. return scope.row[prop];
  325. };
  326. const getRenderValue = (
  327. scope,
  328. item,
  329. fn = { name: "render", type: "call" }
  330. ) => {
  331. const prop = item.attrs.prop;
  332. const propValue = prop && scope.row[prop];
  333. scope.row.$index = scope.$index;
  334. const args = propValue !== undefined ? propValue : scope.row;
  335. return item[fn.name][fn.type](getParent.value, args);
  336. };
  337. // 匹配 render 开头的函数
  338. const getMatchRenderFunction = (obj) => {
  339. return Object.keys(obj).find((key) => {
  340. const matchRender = key.match(/^render.*/);
  341. return matchRender && matchRender[0];
  342. });
  343. };
  344. const isFunction = (fn) => {
  345. return isFn(fn);
  346. };
  347. const searchFn = (val) => {
  348. if (props.loading) return;
  349. proxy.$emit(
  350. "getList",
  351. Object.assign(props.filterParams, {
  352. [props.searchKey]: props.pagination.keyword,
  353. })
  354. );
  355. };
  356. const retrievalModalFn = () => {
  357. proxy.$emit("moreSearch", "");
  358. //获取父组件定义的moreSearch方法
  359. };
  360. const handlePageChange = (val) => {
  361. proxy.$emit(
  362. "getList",
  363. Object.assign(props.filterParams, { pageNum: val })
  364. );
  365. };
  366. const handleSizeChange = (val) => {
  367. proxy.$emit(
  368. "getList",
  369. Object.assign(props.filterParams, { pageSize: val })
  370. );
  371. };
  372. const getHeaderActions = (item) => {
  373. return {
  374. ...item.attrs,
  375. };
  376. };
  377. const stopBubbles = (e) => {
  378. const event = e || window.event;
  379. if (event && event.stopPropagation) {
  380. event.stopPropagation();
  381. } else {
  382. event.cancelBubble = true;
  383. }
  384. };
  385. const handleNativeClick = ({ isBubble }, e, item) => {
  386. // 考虑到单元格内渲染了组件,并且组件自身可能含有点击事件,故添加了阻止冒泡机制
  387. // 若指定 isBubble 为 false,则当前单元格恢复冒泡机制
  388. if (isBoolean(isBubble) && !isBubble) return;
  389. stopBubbles(e);
  390. };
  391. //下拉搜索相关
  392. const searchItemSelct = (item, i, index) => {
  393. if (item == "all") {
  394. i.label = { ...props.selectConfig[index] }.labelCopy;
  395. proxy.$emit(
  396. "getList",
  397. Object.assign(props.filterParams, { [i.prop]: "" })
  398. );
  399. return;
  400. }
  401. i.label = item.label;
  402. proxy.$emit(
  403. "getList",
  404. Object.assign(props.filterParams, { [i.prop]: item.value })
  405. );
  406. };
  407. const isSelectable = (row, index, item) => {
  408. if (item.type === "selection") {
  409. if (item.attrs && item.attrs.checkAtt) {
  410. if (row[item.attrs.checkAtt]) {
  411. return row[item.attrs.checkAtt];
  412. }
  413. } else {
  414. return true;
  415. }
  416. }
  417. };
  418. const hocElTable = ref();
  419. return {
  420. getParent,
  421. getPagination,
  422. renderTypeList,
  423. getActionList,
  424. getAttrsValue,
  425. getValue,
  426. getRenderValue,
  427. getMatchRenderFunction,
  428. isFunction,
  429. handlePageChange,
  430. handleSizeChange,
  431. getHeaderActions,
  432. stopBubbles,
  433. handleNativeClick,
  434. searchFn,
  435. searchItemSelct,
  436. selectConfigCopy,
  437. isSelectable,
  438. retrievalModal,
  439. retrievalModalFn,
  440. statSelectVal,
  441. statWarpHeight,
  442. isMore,
  443. changeStatData,
  444. hocElTable,
  445. };
  446. },
  447. });
  448. </script>
  449. <style>
  450. .table-list-container th {
  451. color: #333 !important;
  452. }
  453. .by-table td .el-button + .el-button {
  454. margin-left: 0 !important;
  455. }
  456. .by-table td .el-button {
  457. background: none !important;
  458. margin: 0 !important;
  459. padding: 8px 6px !important;
  460. }
  461. .el-checkbox__input.is-disabled .el-checkbox__inner {
  462. background-color: #dee1e6;
  463. border-color: #b2b4b9;
  464. }
  465. .el-table .cell {
  466. line-height: 34px;
  467. }
  468. </style>
  469. <style lang="scss" scoped>
  470. .sortableActive {
  471. background: #f5f7fa !important;
  472. }
  473. .show-more {
  474. height: auto !important;
  475. }
  476. .stat-warp {
  477. margin-bottom: 20px;
  478. background: #fff;
  479. padding: 0 20px;
  480. height: 200px;
  481. overflow: hidden;
  482. position: relative;
  483. .more-btn {
  484. position: absolute;
  485. right: 0;
  486. bottom: 0;
  487. left: 0;
  488. height: 40px;
  489. cursor: pointer;
  490. font-size: 12px;
  491. line-height: 30px;
  492. text-align: center;
  493. background: linear-gradient(
  494. 180deg,
  495. rgba(255, 255, 255, 0) 0%,
  496. rgba(255, 255, 255, 0.8) 100%
  497. );
  498. i {
  499. color: #999;
  500. }
  501. }
  502. .title {
  503. height: 60px;
  504. select {
  505. height: 60px;
  506. border: none;
  507. outline: none;
  508. -webkit-appearance: none;
  509. appearance: none;
  510. font-size: 14px;
  511. font-weight: bold;
  512. background: url("@/assets/images/sanjiao.png") no-repeat right center;
  513. padding-right: 20px;
  514. }
  515. div {
  516. height: 60px;
  517. font-size: 14px;
  518. font-weight: bold;
  519. line-height: 60px;
  520. }
  521. }
  522. ul {
  523. padding: 0;
  524. overflow: hidden;
  525. margin: 0;
  526. li {
  527. list-style: none;
  528. min-width: 285px;
  529. box-sizing: border-box;
  530. margin: 0 20px 20px 0;
  531. background: #eff6ff;
  532. float: left;
  533. overflow: hidden;
  534. padding: 20px;
  535. color: #333333;
  536. border-radius: 10px;
  537. .label {
  538. font-size: 14px;
  539. }
  540. .label::before {
  541. width: 10px;
  542. height: 10px;
  543. content: "";
  544. border-radius: 50%;
  545. background: #0084ff;
  546. display: inline-block;
  547. margin-right: 10px;
  548. }
  549. .num {
  550. margin-top: 10px;
  551. font-size: 24px;
  552. font-weight: bold;
  553. }
  554. }
  555. //#F5F3FF #9E64ED
  556. .theme2 {
  557. background: #f5f3ff;
  558. .label::before {
  559. background: #9e64ed;
  560. }
  561. }
  562. //#FFF1E1 #FF9315
  563. .theme3 {
  564. background: #fff1e1;
  565. .label::before {
  566. background: #ff9315;
  567. }
  568. }
  569. //#E2FBE8 #39C55A
  570. .theme4 {
  571. background: #e2fbe8;
  572. .label::before {
  573. background: #39c55a;
  574. }
  575. }
  576. .theme5 {
  577. background: #ffebe9;
  578. .label::before {
  579. background: #f94539;
  580. }
  581. }
  582. .theme6 {
  583. background: #e4f9f9;
  584. .label::before {
  585. background: #53cbcb;
  586. }
  587. }
  588. .multi-data {
  589. .label::before {
  590. display: none;
  591. }
  592. .label {
  593. font-size: 14px;
  594. font-weight: bold;
  595. color: #333;
  596. margin-bottom: 8px;
  597. }
  598. .num-warp {
  599. overflow: hidden;
  600. .num-box {
  601. float: left;
  602. min-width: 80px;
  603. margin-right: 20px;
  604. .num-small {
  605. font-size: 16px;
  606. font-weight: bold;
  607. margin-bottom: 8px;
  608. }
  609. .label-small {
  610. color: #666;
  611. font-size: 14px;
  612. }
  613. }
  614. }
  615. }
  616. }
  617. }
  618. .by-search {
  619. display: flex;
  620. justify-content: space-between;
  621. margin-bottom: 10px;
  622. .more-icon {
  623. float: right;
  624. cursor: pointer;
  625. line-height: 32px;
  626. text-align: center;
  627. margin-left: 5px;
  628. }
  629. }
  630. .by-dropdown {
  631. position: relative;
  632. text-align: left;
  633. height: 32px;
  634. z-index: 1010;
  635. padding: 0 10px;
  636. transition: all 0.5s ease;
  637. cursor: pointer;
  638. line-height: 32px;
  639. .by-dropdown-title {
  640. font-size: 14px;
  641. background-color: #fff;
  642. }
  643. ul {
  644. position: absolute;
  645. left: 0;
  646. top: 32px;
  647. padding: 0;
  648. margin: 0;
  649. z-index: 100;
  650. display: none;
  651. white-space: nowrap;
  652. min-width: 100%;
  653. li {
  654. list-style: none;
  655. font-size: 12px;
  656. height: 30px;
  657. padding: 0 10px;
  658. text-align: left;
  659. line-height: 30px;
  660. }
  661. li:hover {
  662. background-color: #eff6ff;
  663. color: #0084ff;
  664. }
  665. }
  666. }
  667. .by-dropdown::before {
  668. display: block;
  669. width: 1px;
  670. content: " ";
  671. position: absolute;
  672. height: 14px;
  673. top: 8px;
  674. background-color: #ddd;
  675. right: 0;
  676. z-index: 1011;
  677. }
  678. .by-dropdown:hover {
  679. background: #ffffff;
  680. border-radius: 2px 2px 2px 2px;
  681. opacity: 1;
  682. ul {
  683. background: #ffffff;
  684. box-shadow: 0px 2px 16px 1px rgba(0, 0, 0, 0.06);
  685. border-radius: 2px 2px 2px 2px;
  686. opacity: 1;
  687. display: block;
  688. text-align: left;
  689. }
  690. }
  691. .header-actions {
  692. flex: 1;
  693. overflow-x: auto;
  694. padding: 20px;
  695. background: #fff;
  696. margin-bottom: 20px;
  697. .overflow-box {
  698. :deep() .el-button:nth-child(1) {
  699. margin-left: 10px;
  700. }
  701. }
  702. }
  703. .table-list-container {
  704. background: #fff;
  705. padding: 13px 20px 20px;
  706. .table-pagination {
  707. padding-top: 20px;
  708. }
  709. .header {
  710. display: flex;
  711. padding-bottom: 20px;
  712. }
  713. .el-table {
  714. :deep() th {
  715. font-size: 14px;
  716. }
  717. :deep() td {
  718. font-size: 14px;
  719. }
  720. }
  721. }
  722. .by-dropdown-lists {
  723. max-height: 50vh;
  724. overflow-y: auto;
  725. line-height: 1;
  726. }
  727. </style>