269 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			269 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | ||
|   <div class="generic-monitor-list">
 | ||
|     <el-card shadow="never">
 | ||
|       <el-form :inline="true" :model="filters" class="filter-form">
 | ||
|         <el-form-item v-for="col in filterableColumns" :key="col.dataIndex" :label="col.title">
 | ||
|           <template v-if="col.filterType === 'text'">
 | ||
|             <el-input
 | ||
|                 v-model="filters[col.dataIndex]"
 | ||
|                 :placeholder="`搜索 ${col.title}`"
 | ||
|                 clearable
 | ||
|                 @change="handleFilterChange(col.dataIndex, filters[col.dataIndex])"
 | ||
|             ></el-input>
 | ||
|           </template>
 | ||
|           <template v-else-if="col.filterType === 'number'">
 | ||
|             <el-input-number
 | ||
|                 v-model="filters[col.dataIndex]"
 | ||
|                 :placeholder="`搜索 ${col.title}`"
 | ||
|                 :controls="false"
 | ||
|                 clearable
 | ||
|                 @change="handleFilterChange(col.dataIndex, filters[col.dataIndex])"
 | ||
|             ></el-input-number>
 | ||
|           </template>
 | ||
|           <template v-else-if="col.filterType === 'dateRange'">
 | ||
|             <el-date-picker
 | ||
|                 v-model="filters[col.dataIndex]"
 | ||
|                 type="datetimerange"
 | ||
|                 range-separator="至"
 | ||
|                 start-placeholder="开始日期"
 | ||
|                 end-placeholder="结束日期"
 | ||
|                 @change="handleFilterChange(col.dataIndex, filters[col.dataIndex])"
 | ||
|             ></el-date-picker>
 | ||
|           </template>
 | ||
|           <template v-else-if="col.filterType === 'select'">
 | ||
|             <el-select
 | ||
|                 v-model="filters[col.dataIndex]"
 | ||
|                 :placeholder="`选择 ${col.title}`"
 | ||
|                 clearable
 | ||
|                 @change="handleFilterChange(col.dataIndex, filters[col.dataIndex])"
 | ||
|             >
 | ||
|               <el-option
 | ||
|                   v-for="option in col.filterOptions"
 | ||
|                   :key="option.value"
 | ||
|                   :label="option.text"
 | ||
|                   :value="option.value"
 | ||
|               ></el-option>
 | ||
|             </el-select>
 | ||
|           </template>
 | ||
|           <template v-else-if="col.filterType === 'boolean'">
 | ||
|             <el-select
 | ||
|                 v-model="filters[col.dataIndex]"
 | ||
|                 :placeholder="`选择 ${col.title}`"
 | ||
|                 clearable
 | ||
|                 @change="handleFilterChange(col.dataIndex, filters[col.dataIndex])"
 | ||
|             >
 | ||
|               <el-option label="是" :value="true"></el-option>
 | ||
|               <el-option label="否" :value="false"></el-option>
 | ||
|             </el-select>
 | ||
|           </template>
 | ||
|         </el-form-item>
 | ||
|         <el-form-item>
 | ||
|           <el-button type="primary" @click="loadData">查询</el-button>
 | ||
|           <el-button @click="resetFilters">重置</el-button>
 | ||
|         </el-form-item>
 | ||
|       </el-form>
 | ||
| 
 | ||
|       <el-table
 | ||
|           :data="data"
 | ||
|           v-loading="loading"
 | ||
|           border
 | ||
|           stripe
 | ||
|           style="width: 100%"
 | ||
|           table-layout="auto"
 | ||
|           :fit="true"
 | ||
|           :scrollbar-always-on="true"
 | ||
|           @sort-change="handleSortChange"
 | ||
|       >
 | ||
|         <el-table-column
 | ||
|             v-for="col in tableColumns"
 | ||
|             :key="col.key"
 | ||
|             :prop="col.prop"
 | ||
|             :label="col.title"
 | ||
|             :sortable="col.sorter ? 'custom' : false"
 | ||
|             :formatter="col.formatter"
 | ||
|             :min-width="col.minWidth"
 | ||
|         >
 | ||
|           <template v-if="col.render" #default="{ row }">
 | ||
|             <component :is="col.render(row)"/>
 | ||
|           </template>
 | ||
|         </el-table-column>
 | ||
|       </el-table>
 | ||
| 
 | ||
|       <el-pagination
 | ||
|           @size-change="handleSizeChange"
 | ||
|           @current-change="handleCurrentChange"
 | ||
|           :current-page="pagination.currentPage"
 | ||
|           :page-sizes="[10, 20, 50, 100]"
 | ||
|           :page-size="pagination.pageSize"
 | ||
|           layout="total, sizes, prev, pager, next, jumper"
 | ||
|           :total="pagination.total"
 | ||
|           background
 | ||
|           style="margin-top: 20px; text-align: right;"
 | ||
|       ></el-pagination>
 | ||
|     </el-card>
 | ||
|   </div>
 | ||
| </template>
 | ||
| 
 | ||
| <script setup>
 | ||
| import {ref, reactive, onMounted, watch, computed} from 'vue';
 | ||
| import {ElMessage} from 'element-plus';
 | ||
| 
 | ||
| const props = defineProps({
 | ||
|   fetchData: {
 | ||
|     type: Function,
 | ||
|     required: true,
 | ||
|   },
 | ||
|   columnsConfig: {
 | ||
|     type: Array,
 | ||
|     required: true,
 | ||
|   },
 | ||
| });
 | ||
| 
 | ||
| const data = ref([]);
 | ||
| const loading = ref(false);
 | ||
| const pagination = reactive({
 | ||
|   currentPage: 1,
 | ||
|   pageSize: 10,
 | ||
|   total: 0,
 | ||
| });
 | ||
| const filters = reactive({});
 | ||
| const sortOrder = reactive({
 | ||
|   prop: undefined,
 | ||
|   order: undefined,
 | ||
| });
 | ||
| 
 | ||
| const filterableColumns = computed(() => {
 | ||
|   return props.columnsConfig.filter(col => col.filterType);
 | ||
| });
 | ||
| 
 | ||
| const tableColumns = computed(() => {
 | ||
|   return props.columnsConfig.map(col => {
 | ||
|     const newCol = {...col};
 | ||
|     newCol.prop = Array.isArray(col.dataIndex) ? col.dataIndex.join('.') : col.dataIndex;
 | ||
| 
 | ||
|     // 添加智能默认 formatter
 | ||
|     if (!newCol.formatter) {
 | ||
|       newCol.formatter = (row, column, cellValue) => {
 | ||
|         if (typeof cellValue === 'object' && cellValue !== null) {
 | ||
|           try {
 | ||
|             return JSON.stringify(cellValue, null, 2); // 格式化为可读的JSON字符串
 | ||
|           } catch (e) {
 | ||
|             console.warn('Failed to stringify object for display:', cellValue, e);
 | ||
|             return '[Object]'; // 无法序列化时显示简短提示
 | ||
|           }
 | ||
|         } else if (Array.isArray(cellValue)) {
 | ||
|             return cellValue.join(', '); // 数组也默认用逗号连接
 | ||
|         }
 | ||
|         return cellValue;
 | ||
|       };
 | ||
|     }
 | ||
| 
 | ||
|     return newCol;
 | ||
|   });
 | ||
| });
 | ||
| 
 | ||
| const loadData = async () => {
 | ||
|   loading.value = true;
 | ||
|   try {
 | ||
|     const params = {
 | ||
|       page: pagination.currentPage,
 | ||
|       pageSize: pagination.pageSize,
 | ||
|       ...filters,
 | ||
|       orderBy: sortOrder.prop,
 | ||
|       order: sortOrder.order === 'ascending' ? 'asc' : (sortOrder.order === 'descending' ? 'desc' : undefined),
 | ||
|     };
 | ||
| 
 | ||
|     // Custom function to format Date objects to YYYY-MM-DDTHH:mm:ssZ
 | ||
|     const formatToRFC3339WithOffset = (date) => {
 | ||
|       if (!date) return ''; // Handle null or undefined dates
 | ||
|       const year = date.getUTCFullYear();
 | ||
|       const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
 | ||
|       const day = date.getUTCDate().toString().padStart(2, '0');
 | ||
|       const hours = date.getUTCHours().toString().padStart(2, '0');
 | ||
|       const minutes = date.getUTCMinutes().toString().padStart(2, '0');
 | ||
|       const seconds = date.getUTCSeconds().toString().padStart(2, '0');
 | ||
|       return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`;
 | ||
|     };
 | ||
| 
 | ||
|     // 将日期范围筛选转换为 start_time 和 end_time,并确保是 RFC3339 UTC 格式 (不带毫秒)
 | ||
|     filterableColumns.value.forEach(col => {
 | ||
|       if (col.filterType === 'dateRange' && filters[col.dataIndex] && filters[col.dataIndex].length === 2) {
 | ||
|         // filters[col.dataIndex] will now contain Date objects directly from el-date-picker
 | ||
|         const startDateObj = filters[col.dataIndex][0];
 | ||
|         const endDateObj = filters[col.dataIndex][1];
 | ||
| 
 | ||
|         params[`start_time`] = formatToRFC3339WithOffset(startDateObj);
 | ||
|         params[`end_time`] = formatToRFC3339WithOffset(endDateObj);
 | ||
|         delete params[col.dataIndex];
 | ||
|       }
 | ||
|     });
 | ||
| 
 | ||
|     console.log('Sending parameters to fetchData:', params);
 | ||
| 
 | ||
|     const result = await props.fetchData(params);
 | ||
|     data.value = result.list;
 | ||
|     pagination.total = result.total;
 | ||
|   } catch (error) {
 | ||
|     console.error('Failed to fetch data:', error);
 | ||
|     ElMessage.error('获取数据失败,请稍后再试。');
 | ||
|   } finally {
 | ||
|     loading.value = false;
 | ||
|   }
 | ||
| };
 | ||
| 
 | ||
| const handleSizeChange = (val) => {
 | ||
|   pagination.pageSize = val;
 | ||
|   pagination.currentPage = 1;
 | ||
|   loadData();
 | ||
| };
 | ||
| 
 | ||
| const handleCurrentChange = (val) => {
 | ||
|   pagination.currentPage = val;
 | ||
|   loadData();
 | ||
| };
 | ||
| 
 | ||
| const handleSortChange = ({prop, order}) => {
 | ||
|   sortOrder.prop = prop;
 | ||
|   sortOrder.order = order;
 | ||
|   loadData();
 | ||
| };
 | ||
| 
 | ||
| const handleFilterChange = (key, value) => {
 | ||
|   filters[key] = value;
 | ||
|   pagination.currentPage = 1;
 | ||
| };
 | ||
| 
 | ||
| const resetFilters = () => {
 | ||
|   for (const key in filters) {
 | ||
|     delete filters[key];
 | ||
|   }
 | ||
|   sortOrder.prop = undefined;
 | ||
|   sortOrder.order = undefined;
 | ||
|   pagination.currentPage = 1;
 | ||
|   loadData();
 | ||
| };
 | ||
| 
 | ||
| onMounted(() => {
 | ||
|   loadData();
 | ||
| });
 | ||
| 
 | ||
| </script>
 | ||
| 
 | ||
| <style scoped>
 | ||
| .generic-monitor-list {
 | ||
|   padding: 20px;
 | ||
| }
 | ||
| 
 | ||
| .filter-form {
 | ||
|   margin-bottom: 20px;
 | ||
| }
 | ||
| 
 | ||
| .filter-form .el-form-item {
 | ||
|   min-width: 220px; /* 增加最小宽度 */
 | ||
| }
 | ||
| 
 | ||
| .el-card {
 | ||
|   border: none;
 | ||
| }
 | ||
| </style>
 |