index.vue 19 KB

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