index.vue 18 KB

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