index.vue 18 KB

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