| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837 |
- <script setup lang="ts">
- import type { PropsParams, TablePaginationSetting } from '@/types/table.ts'
- import type { ReqConfig } from '@/types/dataAnalysis.ts'
- import { FieldSpecialEffectType } from '@/types/tableText.ts'
- import { fuzzySearch, throttleFunc } from '@/utils/common'
- import { useTable } from '@/hooks/useTable.ts'
- import { useRequest } from '@/hooks/useRequest.ts'
- import {
- computed,
- type ComputedRef,
- markRaw,
- nextTick,
- onMounted,
- reactive,
- ref,
- toRaw,
- watch
- } from 'vue'
- import TableFilterForm from '@/components/table/TableFilterForm/TableFilterForm.vue'
- import axiosInstance from '@/utils/axios/axiosInstance.ts'
- import TableTools from './TableTools.vue'
- import TableColumn from './TableColumn/TableColumn.vue'
- import type { ResponseInfo } from '@/types/res.ts'
- type TableFilterFormRef = InstanceType<typeof TableFilterForm>
- // 查询表单的Ref
- const filterFormRef = ref<TableFilterFormRef>()
- const { analysisResCode } = useRequest()
- // 节流的延迟时间
- const throttleTime = 500
- const props = withDefaults(defineProps<PropsParams>(), {
- needRowindex: true,
- needAverage: false,
- needTotal: false,
- openFilterQuery: true,
- openPageQuery: true,
- loadingState: false,
- openRemoteReqData: true,
- openRemoteQuery: true
- })
- const emits = defineEmits(['addNewItem', 'upload', 'downLoad'])
- // 加载动画
- const loading = ref(false)
- // 表格数据
- const tableData: Array<any> = reactive([])
- // 展开表格数据
- const expandTableData: Record<string, Array<any>> = reactive({})
- // 分页展开表格数据
- const expandPaginationConfig = reactive<Record<string, TablePaginationSetting>>({})
- const expandPageData = computed(() => {
- const nowActive = activeExpandTablePageConfig.value
- const { currentPage, limit } = expandPaginationConfig[nowActive]
- if (!expandTableData[nowActive]) return []
- // const data = expandTableData[nowActive].slice((currentPage - 1) * limit, currentPage * limit)
- return expandTableData[nowActive].slice((currentPage - 1) * limit, currentPage * limit)
- })
- // 现在使用的分页
- const activeExpandTablePageConfig = ref<string>('')
- // 查询表单的数据
- const queryFormData = ref<{ [key: string]: any }>({})
- // 分页数据
- const paginationConfig = reactive<TablePaginationSetting>({
- currentPage: 1,
- limit: 0,
- total: 0,
- pageSizeList: []
- })
- // 请求配置
- const reqConfig = reactive<ReqConfig>({
- url: '',
- otherOptions: {}
- })
- // 一些公用方法
- const {
- getTableData,
- setTableData,
- computedRowIndex,
- handleCurrentChange,
- handleSizeChange,
- insertAverageRow,
- getDecimalFromRange,
- resetTableData,
- initPageConfig,
- initReqConfig,
- createRowKey,
- downLoadTable
- } = useTable(tableData, paginationConfig, props.tableFieldsInfo)
- /**
- * 本地使用的数据,只有在使用外部数据的情况下使用
- */
- let localTableData: ComputedRef | [] = []
- if (!props.openRemoteReqData && props.dataList) {
- if (Array.isArray(props.dataList)) {
- localTableData = computed<Array<any>>(() => {
- let curPage = paginationConfig.currentPage
- let limit = paginationConfig.limit
- let begin = curPage * limit - limit
- let end = curPage * limit
- return tableData.slice(begin, end)
- })
- }
- }
- /**
- * 获取表格数据
- * @returns [表格数据,数据总数]
- */
- const getData = async (): Promise<[Array<any>, number]> => {
- try {
- for (let [k, v] of Object.entries(expandTableData)) {
- if (v) {
- delete expandTableData[k]
- }
- }
- expandRowKeys.value = []
- // expandPageData.value.splice(0, expandPageData.value.length)
- // 使用传入数据源
- // 如果使用前端查询,则需要传入dataList作为数据源
- if (!props.openRemoteReqData) {
- if (props.dataList) {
- return [JSON.parse(JSON.stringify(props.dataList)), 0]
- } else {
- console.error('请传入dataList,没有数据源!')
- return [[], 0]
- }
- } else {
- // 如果需要表格自己请求数据的
- // 必须要传入requestConfig
- if (props.requestConfig) {
- // 当使用远程请求数据源的时候,默认使用远程查询
- reqConfig.otherOptions.offset = (paginationConfig.currentPage - 1) * paginationConfig.limit
- reqConfig.otherOptions.limit = paginationConfig.limit
- return await getTableData(reqConfig.url, reqConfig.otherOptions)
- } else {
- console.error('缺少请求配置')
- return [[], 0]
- }
- }
- } catch (err) {
- console.error('请求错误:', err)
- return [[], 0] // 确保返回 false 表示失败
- }
- }
- /**
- * 将获取的数据赋值给tableData,同时设置分页数据的总数
- * @param tableList 表格数据
- * @param total 数据总数
- * @returns 设置是否成功
- */
- const setData = (tableList: Array<any>, total: number): boolean => {
- try {
- setTableData(tableList, total, props.openPageQuery, props.openRemoteReqData)
- insertAverageRow(props.needAverage, props.openRemoteReqData)
- return true
- } catch (err) {
- console.error(err)
- return false
- }
- }
- /**
- * 更新表格数据
- * @returns 设置是否成功
- */
- const updateTableData = async (): Promise<boolean> => {
- try {
- loading.value = true
- let [tableList, total] = await getData()
- setData(tableList, total)
- return true
- } catch (err) {
- console.log(err)
- return false
- } finally {
- loading.value = false
- }
- }
- // 包装一下获取数据
- const throttleGetData = throttleFunc(updateTableData, 1000)
- /**
- * @description 按条件查询,如果开启了分页查询,那么会直接重新查询数据,否则,会根据现有数据进行查询
- */
- const queryTableData = () => {
- if (props.openRemoteQuery && props.openRemoteReqData && props.requestConfig) {
- reqConfig.otherOptions = { ...reqConfig.otherOptions, ...queryFormData.value }
- // 需要在查询前清除掉目前的数据,不然会导致之前缓存的数据混入
- // 比如第一页已经缓存了,在第二页重新查询,在切回第一页,还是显示查询前的数据,因为缓存没被清除
- tableData.splice(0, tableData.length)
- updateTableData()
- console.log(reqConfig.otherOptions)
- } else {
- let filteredTable: any[]
- // 过滤出来所有符合formData数据的条件
- if (props.dataList) {
- filteredTable = props.dataList.filter((item) => {
- let state = true
- for (let [k, v] of Object.entries(queryFormData.value)) {
- // 模糊查询,看值是否跟表格中的数据匹配
- if (!fuzzySearch(v, item[k])) {
- state = false
- break
- }
- }
- return state
- })
- paginationConfig.total = filteredTable.length
- tableData.splice(0, tableData.length, ...filteredTable)
- } else {
- console.error('没有数据源')
- }
- }
- }
- // 把查询方法包装一下,节流
- const throttleQueryTableData = throttleFunc(queryTableData, throttleTime)
- /**
- * @description: 单独处理拥有均值行的表格每个单元格的样式,均值字段均加粗,其他需要比较的字段根据自身百分比显示颜色
- * 其中使用row-style无效,scope会导致无法覆盖样式
- * 同时由于自定义了表格内容,哪里的样式会覆盖row的样式,所以只能单独对单元格设置
- * @param {*} info 每个单元格的信息
- */
- const tableCellStyle = (info: any) => {
- if (info.row.date === '均值')
- return {
- 'font-weight': 'bold'
- }
- else if (info.column.property != 'count' && info.column.property != 'date' && props.needAverage) {
- return {
- 'background-color': `rgba(59, 157, 247,${getDecimalFromRange(info.row[info.column.property])})`
- }
- } else return {}
- }
- /**
- * 监听limit的变化,改变后将页码置为1,同时去重新请求数据
- */
- const watchLimit = watch(
- () => paginationConfig.limit,
- (newLimit, oldLimit) => {
- if (newLimit !== oldLimit) {
- paginationConfig.currentPage = 1
- if (props.openFilterQuery) {
- filterFormRef.value?.throttleResetQuery()
- } else {
- throttleQueryTableData()
- }
- }
- },
- { deep: true }
- )
- /**
- * 监听currentPage的变化,如果开启了分页查询,并且当前页发生变化,并且当前页的数据不存在,则重新请求数据
- */
- const watchCurPage = watch(
- () => paginationConfig.currentPage,
- (newPage, oldPage) => {
- if (newPage !== oldPage && !tableData[newPage]) {
- updateTableData()
- }
- },
- {
- deep: true
- }
- )
- // 如果没有开启分页查询,直接关闭这两个监听
- if (!props.openPageQuery) {
- watchLimit()
- watchCurPage()
- }
- // 监听传入的datalist的变化,然后去更新数据
- const changeDataList = watch(
- () => [props.dataList],
- () => {
- updateTableData()
- },
- {
- deep: true
- }
- )
- /**
- * 对传入的props进行检查,对错误配置进行提示
- */
- const checkPropsConfig = () => {
- const {
- openFilterQuery,
- queryInfo,
- openPageQuery,
- paginationConfig,
- openRemoteReqData,
- requestConfig,
- openRemoteQuery,
- dataList
- } = props
- if (openFilterQuery && !queryInfo) {
- console.error('请输入查询的配置信息')
- }
- if (openPageQuery && !paginationConfig) {
- console.error('请输入分页配置信息')
- }
- if (openRemoteReqData || openRemoteQuery) {
- if (!requestConfig) {
- console.error('请输入请求配置信息')
- }
- }
- if (!openRemoteReqData) {
- if (!dataList) {
- console.error('请至少确保一个数据源')
- }
- if (openRemoteQuery) {
- console.error('远程查询无效,请开启数据远程请求')
- }
- }
- }
- /**
- * 监听请求配置的变化,如果开启了查询功能,则需要重置表单后查询
- * 否则直接请求数据
- */
- const watchReqConfig = watch(
- () => props.requestConfig,
- () => {
- Object.assign(reqConfig, props.requestConfig)
- if (props.openFilterQuery) {
- filterFormRef.value?.throttleResetQuery()
- } else {
- throttleQueryTableData()
- }
- },
- {
- deep: true
- }
- )
- /**
- * 不使用远程数据,则不用监听请求数据的变化
- */
- if (!props.openRemoteReqData) {
- watchReqConfig()
- }
- // 如果是使用远程数据源,则取消监听
- if (props.openRemoteReqData) {
- changeDataList()
- }
- /**
- * @description: 表格排序
- * @param {*} data 获取到的数据
- */
- const tableSortChange = (data: { column: any; prop: string; order: any }) => {
- filterFormRef.value?.resetFormData()
- let { order } = { ...data }
- if (order === 'ascending') order = 'asc'
- else if (order === 'descending') order = 'desc'
- else order = ''
- reqConfig.otherOptions.order = order
- throttleQueryTableData()
- }
- /**
- * 删除行
- * @param url 请求地址
- * @param fieldsInfo 行数据
- */
- const deleteRow = (url: string, fieldsInfo: any) => {
- axiosInstance
- .post(url, { ...fieldsInfo })
- .then((data) => {
- analysisResCode(data).then(() => {
- if (props.openFilterQuery) {
- filterFormRef.value?.throttleResetQuery()
- } else {
- throttleQueryTableData()
- }
- })
- })
- .catch((err) => {
- console.log(err)
- })
- }
- /**
- * @description: 外部获取数据
- */
- const outGetTableData = () => {
- return toRaw(tableData).flat()
- }
- const getTotal = () => {
- if (!props.needTotal || !props.totalFunc) {
- return []
- }
- return props.totalFunc(tableData, paginationConfig.total)
- }
- const getFormQueryData = (): { [p: string]: any } => {
- return markRaw(queryFormData.value)
- }
- // 定义暴露出去的方法
- defineExpose({
- updateTableData,
- resetTableData,
- deleteRow,
- downLoadTable,
- outGetTableData,
- getFormQueryData
- })
- onMounted(() => {
- initPageConfig(props.paginationConfig)
- initReqConfig(reqConfig, props.requestConfig)
- checkPropsConfig()
- nextTick(() => {
- // changeTableFooterPos()
- })
- })
- const expandRowKeys = ref<string[]>([])
- const getRowKey = (row: any) => {
- const id = row['actionId'] ?? row['id']
- if (!id) {
- console.warn('请检查表格数据,没有可用的ID 作为RowKey,当前使用随机key')
- return createRowKey()
- }
- return id + ''
- }
- const handleExpand = async (row: any, expandedRows: any[]) => {
- if (!props.expandConfig) return
- let id = row['actionId'] ?? row['id']
- if (!id) {
- console.warn('请检查表格数据,没有可用的ID 作为RowKey,当前使用随机key')
- id = createRowKey()
- }
- // expandRowKeys.value.push(id)
- if (expandedRows.length) {
- //展开
- expandRowKeys.value = [] //先干掉之前展开的行
- if (row) {
- expandRowKeys.value.push(id) //push新的行 (原理有点类似防抖)
- }
- } else {
- expandRowKeys.value = [] //折叠 就清空expand-row-keys对应的数组
- }
- activeExpandTablePageConfig.value = id
- if (!expandPaginationConfig[id]) {
- expandPaginationConfig[id] = {
- currentPage: 1,
- limit: 10,
- total: 0,
- pageSizeList: [10, 20, 30]
- }
- }
- // 有数据后就不要重新请求数据
- // 这里只判断是否已经请求过了,而不判断是否有数据,防止重复请求
- if (expandTableData[id] !== undefined) return
- const params = {
- ...props.expandConfig.expandReqConfig.otherOptions,
- actionId: id + ''
- }
- const res = (await axiosInstance.post(
- props.expandConfig.expandReqConfig.url,
- params
- )) as ResponseInfo
- if (res.code !== 0) {
- ElMessage.error('获取展开信息失败')
- return
- }
- expandTableData[id] = res.data
- expandPaginationConfig[id].total = res.data.length
- }
- const handleExpandPageChange = (page: number) => {
- const config = expandPaginationConfig[activeExpandTablePageConfig.value]
- config.currentPage = page
- }
- const handleExpandSizeChange = (size: number) => {
- const config = expandPaginationConfig[activeExpandTablePageConfig.value]
- config.currentPage = 1
- config.limit = size
- }
- const handleRowClass = (row: any, _rowIndex: number): string => {
- const hasOptions = row.row.haveOption
- return hasOptions ? '' : 'noneExpandIcon'
- }
- </script>
- <template>
- <div class="tableContent">
- <div class="filterBox" v-if="openFilterQuery && queryInfo">
- <div class="filterHeader">
- <span>查询条件</span>
- </div>
- <div class="filterBody">
- <TableFilterForm
- v-model:query-form-data="queryFormData"
- :queryInfo="queryInfo"
- @query="throttleQueryTableData"
- ref="filterFormRef"
- ></TableFilterForm>
- </div>
- </div>
- <!-- <div class="chartContainer" v-if="needChart && props.chartOptions">-->
- <!-- <PieBorderRadius :options="props.chartOptions"></PieBorderRadius>-->
- <!-- </div>-->
- <slot name="chart" v-if="$slots.chart"></slot>
- <div class="tableTools">
- <TableTools
- :table-fields-info="props.tableFieldsInfo"
- :tools-config="props.tools"
- @add="emits('addNewItem')"
- @download="downLoadTable(props.tools?.headerMap)"
- @refresh="throttleGetData"
- ></TableTools>
- </div>
- <div class="tableBox">
- <p class="totalRow" v-if="props.needTotal">
- <span class="totalItemLabel totalItem"> 总计 </span>
- <span class="totalItem" :key="'totalKey' + item" v-for="item in getTotal()">
- {{ item }}
- </span>
- </p>
- <!-- 没有分页的时候需要重新计算一下data -->
- <!-- :show-summary="props.needTotal"-->
- <!-- :summary-method="-->
- <!-- () => {-->
- <!-- if (props.totalFunc) {-->
- <!-- return props.totalFunc(tableData, paginationConfig.total)-->
- <!-- }-->
- <!-- return [] "-->
- <!-- }-->
- <el-table
- :data="
- openRemoteReqData && openRemoteQuery
- ? tableData[paginationConfig.currentPage]
- : localTableData
- "
- style="width: 100%"
- class="tableBody"
- :cell-style="tableCellStyle"
- v-loading="openRemoteReqData ? loading : props.loadingState"
- :row-key="getRowKey"
- :expand-row-keys="expandRowKeys"
- @sort-change="tableSortChange"
- @query="throttleGetData"
- @expand-change="handleExpand"
- table-layout="auto"
- :row-class-name="handleRowClass"
- >
- <el-table-column
- v-if="props.needRowindex"
- align="center"
- label="#"
- type="index"
- :index="computedRowIndex"
- />
- <el-table-column align="center" show-overflow-tooltip type="expand" v-if="props.needExpand">
- <template #default="rowInfo">
- <el-table :data="expandPageData">
- <template v-for="child in props.expandConfig?.expandField" :key="child.name">
- <el-table-column
- :prop="child.name"
- :label="child.cnName"
- align="center"
- show-overflow-tooltip
- v-if="child.isShow"
- :sortable="child.needSort ? 'custom' : false"
- >
- <template v-slot="scope">
- <TableColumn
- :column-config="child"
- :row="scope.row"
- :need-average="props.needAverage"
- ></TableColumn>
- </template>
- <!-- {{ expandTableData[rowInfo.row.actionId] }}-->
- <!-- <TableColumn-->
- <!-- :column-config="child"-->
- <!-- :row="rowInfo.row"-->
- <!-- :need-average="rowInfo.needAverage"-->
- <!-- ></TableColumn>-->
- </el-table-column>
- </template>
- </el-table>
- <div class="expandPageConfig">
- <el-pagination
- class="userTablePagination"
- background
- :page-size="expandPaginationConfig[rowInfo.row.actionId].limit"
- :page-sizes="expandPaginationConfig[rowInfo.row.actionId].pageSizeList"
- table-layout="fixed"
- layout="prev, pager, next ,jumper ,sizes,total,"
- :total="expandPaginationConfig[rowInfo.row.actionId].total"
- :current-page="expandPaginationConfig[rowInfo.row.actionId].currentPage"
- @current-change="handleExpandPageChange"
- @size-change="handleExpandSizeChange"
- />
- </div>
- </template>
- </el-table-column>
- <template v-for="item in tableFieldsInfo" :key="item.name">
- <el-table-column
- :prop="item.name"
- :label="item.cnName"
- :min-width="item.specialEffect?.type === FieldSpecialEffectType.DROPDOWN ? '170px' : ''"
- align="center"
- show-overflow-tooltip
- v-if="item.isShow"
- :sortable="item.needSort ? 'custom' : false"
- >
- <template v-slot="scope">
- <TableColumn
- :column-config="item"
- :row="scope.row"
- :need-average="props.needAverage"
- ></TableColumn>
- </template>
- </el-table-column>
- </template>
- <slot name="tableOperation"></slot>
- </el-table>
- <div class="userTablePaginationBox" v-if="openPageQuery">
- <el-pagination
- class="userTablePagination"
- background
- :page-size="paginationConfig.limit"
- :page-sizes="paginationConfig.pageSizeList"
- table-layout="fixed"
- layout="prev, pager, next ,jumper ,sizes,total,"
- :total="paginationConfig.total"
- :current-page="paginationConfig.currentPage"
- @current-change="handleCurrentChange"
- @size-change="handleSizeChange"
- />
- </div>
- </div>
- </div>
- </template>
- <style scoped>
- .tableContent {
- margin: 10px auto 20px;
- width: 100%;
- box-shadow:
- 0 4px 8px 0 rgba(0, 0, 0, 0.02),
- 0 1px 3px 0 rgba(0, 0, 0, 0.02);
- }
- .filterBox,
- .tableBox {
- width: 100%;
- }
- .filterBox {
- margin-top: 1%;
- min-height: 18%;
- display: flex;
- flex-direction: column;
- }
- .filterHeader,
- .filterBody {
- width: 98%;
- box-sizing: border-box;
- margin: 10px auto;
- }
- .filterHeader {
- display: flex;
- align-items: center;
- color: black;
- font-size: 16px;
- font-weight: bold;
- }
- .filterBody {
- display: flex;
- padding: 0 24px;
- }
- .queryBox {
- width: 10%;
- display: flex;
- justify-content: space-around;
- }
- .queryBtnBox {
- width: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- }
- .refreshBtn {
- margin-top: 10%;
- margin-bottom: 10%;
- }
- .queryPartition {
- height: 90%;
- }
- .queryForm {
- width: 90%;
- display: flex;
- flex-wrap: wrap;
- }
- .filterItem {
- /*width: 20%;*/
- display: flex;
- align-items: center;
- }
- .selectLabelContainer,
- .selectSupplement {
- display: flex;
- align-items: center;
- }
- .selectSupplement {
- margin-left: 5px;
- }
- .leftTools,
- .rightTools {
- width: 10%;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .rightTools {
- width: 5%;
- }
- .tableBox {
- width: 98%;
- margin: 5px auto;
- box-shadow:
- 0 4px 8px 0 rgba(0, 0, 0, 0.02),
- 0 1px 3px 0 rgba(0, 0, 0, 0.02);
- }
- .tableBody {
- box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.1);
- }
- .userTablePaginationBox {
- box-sizing: border-box;
- width: 98%;
- margin: 0 auto;
- padding: 1% 0;
- display: flex;
- justify-content: center;
- }
- .expandPageConfig {
- /*width: 98%;*/
- /*height: 600px;*/
- box-sizing: border-box;
- width: 98%;
- margin: 0 auto;
- padding: 1% 0;
- display: flex;
- justify-content: center;
- }
- .leftToolBtn {
- margin-right: 5px;
- }
- .totalRow {
- padding: 10px 0px;
- display: flex;
- /*justify-content: space-between;*/
- }
- .totalRow > .totalItemLabel {
- width: 60px;
- font-weight: bold;
- }
- .totalItem {
- width: 130px;
- margin-right: 15px;
- text-align: center;
- }
- </style>
|