This commit is contained in:
马诗敏 2025-08-13 09:31:36 +08:00
commit e526e80de3
11 changed files with 890 additions and 491 deletions

View File

@ -9,14 +9,38 @@ export const equipmentApprovalApi = {
*
*/
getPendingApprovals(params: EquipmentApprovalListReq) {
return http.get<EquipmentApprovalResp[]>('/equipment/approval/pending', params)
// 确保分页参数格式正确,与设备采购模块保持一致
const requestParams = {
...params,
// 直接使用 pageNum因为后端分页器需要的是 pageNum 参数
pageNum: params.pageNum || 1,
pageSize: params.pageSize || 10,
}
console.log('🔍 API - equipmentApprovalApi.getPendingApprovals 被调用')
console.log('🔍 API - 接收到的参数:', params)
console.log('🔍 API - 最终请求参数:', requestParams)
return http.get<EquipmentApprovalResp[]>('/equipment/approval/pending', requestParams)
},
/**
*
*/
getApprovedApprovals(params: EquipmentApprovalListReq) {
return http.get<EquipmentApprovalResp[]>('/equipment/approval/approved', params)
// 确保分页参数格式正确,与设备采购模块保持一致
const requestParams = {
...params,
// 直接使用 pageNum因为后端分页器需要的是 pageNum 参数
pageNum: params.pageNum || 1,
pageSize: params.pageSize || 10,
}
console.log('🔍 API - equipmentApprovalApi.getApprovedApprovals 被调用')
console.log('🔍 API - 接收到的参数:', params)
console.log('🔍 API - 最终请求参数:', requestParams)
return http.get<EquipmentApprovalResp[]>('/equipment/approval/approved', requestParams)
},
/**

View File

@ -20,8 +20,8 @@ export const equipmentProcurementApi = {
// 确保参数格式正确
const requestParams = {
...params,
// 确保分页参数存在
page: params.page || 1,
// 直接使用 pageNum因为后端分页器需要的是 pageNum 参数
pageNum: params.pageNum || 1,
pageSize: params.pageSize || 10,
}

View File

@ -0,0 +1,55 @@
import http from '@/utils/http'
/* 分页查询 */
export function getVideoPage(params: {
pageNo: number
pageSize: number
projectId: string
turbineId?: string
}) {
return http.get('/video-monitor/page', params )
}
/* 单文件上传 */
export function uploadSingleVideo(
projectId: string,
turbineId: string,
type: string,
file: File
) {
const fd = new FormData()
fd.append('file', file)
return http.post(
`/video-monitor/${projectId}/upload?turbineId=${turbineId}&type=${type}`,
fd,
{ headers: { 'Content-Type': 'multipart/form-data' } }
)
}
/* 批量上传 */
export function uploadBatchVideo(
projectId: string,
turbineId: string,
type: string,
files: File[]
) {
const fd = new FormData()
files.forEach(f => fd.append('files', f))
return http.post(
`/video-monitor/${projectId}/upload-batch?turbineId=${turbineId}&type=${type}`,
fd,
{ headers: { 'Content-Type': 'multipart/form-data' } }
)
}
/* 删除 */
export function deleteVideo(videoId: string) {
return http.del(`/video-monitor/${videoId}`)
}
/* 下载 */
export function downloadVideo(videoId: string) {
return http
.get(`/video-monitor/download/${videoId}`, { responseType: 'blob' })
.then(blob => URL.createObjectURL(blob))
}

View File

@ -0,0 +1,157 @@
<template>
<div class="circular-progress-container">
<div class="circular-progress" :style="{ width: size + 'px', height: size + 'px' }">
<svg class="circular-progress-svg" :width="size" :height="size">
<!-- 背景圆环 -->
<circle
class="circular-progress-bg"
:cx="center"
:cy="center"
:r="radius"
:stroke-width="strokeWidth"
fill="none"
/>
<!-- 进度圆环 -->
<circle
class="circular-progress-fill"
:cx="center"
:cy="center"
:r="radius"
:stroke-width="strokeWidth"
fill="none"
:stroke-dasharray="circumference"
:stroke-dashoffset="strokeDashoffset"
:style="{ stroke: progressColor }"
/>
</svg>
<!-- 中心文本 -->
<div class="circular-progress-text" v-if="showText">
<span class="progress-percent">{{ percent }}%</span>
<span class="progress-label" v-if="label">{{ label }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
percent: number
size?: number
strokeWidth?: number
showText?: boolean
label?: string
color?: string
}
const props = withDefaults(defineProps<Props>(), {
size: 60,
strokeWidth: 4,
showText: true,
label: '',
color: ''
})
//
const center = computed(() => props.size / 2)
const radius = computed(() => (props.size - props.strokeWidth) / 2)
const circumference = computed(() => 2 * Math.PI * radius.value)
const strokeDashoffset = computed(() => circumference.value - (props.percent / 100) * circumference.value)
//
const progressColor = computed(() => {
if (props.color) return props.color
if (props.percent >= 80) return '#52c41a'
if (props.percent >= 60) return '#1890ff'
if (props.percent >= 40) return '#faad14'
if (props.percent > 0) return '#ff4d4f'
return '#d9d9d9'
})
</script>
<style scoped lang="scss">
.circular-progress-container {
display: inline-block;
}
.circular-progress {
position: relative;
display: inline-block;
}
.circular-progress-svg {
transform: rotate(-90deg);
}
.circular-progress-bg {
stroke: #f0f2f5;
}
.circular-progress-fill {
transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1);
stroke-linecap: round;
}
.circular-progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
}
.progress-percent {
font-size: 14px;
font-weight: 600;
color: #1d2129;
line-height: 1;
}
.progress-label {
font-size: 10px;
color: #86909c;
margin-top: 2px;
line-height: 1;
}
//
.circular-progress[style*="width: 40px"],
.circular-progress[style*="width: 40px;"] {
.progress-percent {
font-size: 12px;
}
.progress-label {
font-size: 9px;
}
}
.circular-progress[style*="width: 80px"],
.circular-progress[style*="width: 80px;"] {
.progress-percent {
font-size: 16px;
}
.progress-label {
font-size: 11px;
}
}
.circular-progress[style*="width: 100px"],
.circular-progress[style*="width: 100px;"] {
.progress-percent {
font-size: 18px;
}
.progress-label {
font-size: 12px;
}
}
</style>

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -7,69 +7,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default']
ApprovalMessageItem: typeof import('./../components/NotificationCenter/ApprovalMessageItem.vue')['default']
Avatar: typeof import('./../components/Avatar/index.vue')['default']
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
CellCopy: typeof import('./../components/CellCopy/index.vue')['default']
Chart: typeof import('./../components/Chart/index.vue')['default']
ColumnSetting: typeof import('./../components/GiTable/src/components/ColumnSetting.vue')['default']
CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default']
CronModal: typeof import('./../components/GenCron/CronModal/index.vue')['default']
DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default']
DayForm: typeof import('./../components/GenCron/CronForm/component/day-form.vue')['default']
FilePreview: typeof import('./../components/FilePreview/index.vue')['default']
GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default']
GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default']
GiCellStatus: typeof import('./../components/GiCell/GiCellStatus.vue')['default']
GiCellTag: typeof import('./../components/GiCell/GiCellTag.vue')['default']
GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default']
GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default']
GiDot: typeof import('./../components/GiDot/index.tsx')['default']
GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default']
GiFooter: typeof import('./../components/GiFooter/index.vue')['default']
GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default']
GiIconBox: typeof import('./../components/GiIconBox/index.vue')['default']
GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default']
GiIframe: typeof import('./../components/GiIframe/index.vue')['default']
GiOption: typeof import('./../components/GiOption/index.vue')['default']
GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default']
GiPageLayout: typeof import('./../components/GiPageLayout/index.vue')['default']
GiSpace: typeof import('./../components/GiSpace/index.vue')['default']
GiSplitButton: typeof import('./../components/GiSplitButton/index.vue')['default']
GiSplitPane: typeof import('./../components/GiSplitPane/index.vue')['default']
GiSplitPaneFlexibleBox: typeof import('./../components/GiSplitPane/components/GiSplitPaneFlexibleBox.vue')['default']
GiSvgIcon: typeof import('./../components/GiSvgIcon/index.vue')['default']
GiTable: typeof import('./../components/GiTable/src/GiTable.vue')['default']
GiTag: typeof import('./../components/GiTag/index.tsx')['default']
GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default']
HourForm: typeof import('./../components/GenCron/CronForm/component/hour-form.vue')['default']
Icon403: typeof import('./../components/icons/Icon403.vue')['default']
Icon404: typeof import('./../components/icons/Icon404.vue')['default']
Icon500: typeof import('./../components/icons/Icon500.vue')['default']
IconBorders: typeof import('./../components/icons/IconBorders.vue')['default']
IconTableSize: typeof import('./../components/icons/IconTableSize.vue')['default']
IconTreeAdd: typeof import('./../components/icons/IconTreeAdd.vue')['default']
IconTreeReduce: typeof import('./../components/icons/IconTreeReduce.vue')['default']
ImageImport: typeof import('./../components/ImageImport/index.vue')['default']
ImageImportWizard: typeof import('./../components/ImageImportWizard/index.vue')['default']
IndustrialImageList: typeof import('./../components/IndustrialImageList/index.vue')['default']
JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default']
MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default']
MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default']
NotificationCenter: typeof import('./../components/NotificationCenter/index.vue')['default']
ParentView: typeof import('./../components/ParentView/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
TextCopy: typeof import('./../components/TextCopy/index.vue')['default']
TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default']
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
Verify: typeof import('./../components/Verify/index.vue')['default']
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']
VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default']
WeekForm: typeof import('./../components/GenCron/CronForm/component/week-form.vue')['default']
YearForm: typeof import('./../components/GenCron/CronForm/component/year-form.vue')['default']
}
}

View File

@ -23,8 +23,8 @@ export interface EquipmentPageQuery {
invoice?: string
barcode?: string
importer?: string
page?: number
pageSize?: number
pageNum?: number // 当前页码 - 与后端分页器期望的参数名保持一致
pageSize?: number // 每页大小
orderBy?: string
orderDirection?: string
}

View File

@ -1,407 +1,311 @@
<template>
<GiPageLayout>
<div class="raw-data-container">
<!-- <div class="page-header">
<div class="page-title">原始数据管理</div>
<div class="page-subtitle">管理和分析原始视频数据</div>
</div> -->
<!-- 顶部按钮 -->
<div class="action-bar">
<div class="action-buttons">
<a-button type="primary" @click="showUploadModal = true">
<a-button type="primary" @click="openUploadModal">
<template #icon>
<IconUpload />
</template>
上传视频
</a-button>
<a-button type="primary" @click="handleBatchAnalysis">
<template #icon>
<IconPlayCircle />
</template>
批量分析
</a-button>
<a-button type="primary" @click="handleExportData">
<template #icon>
<IconDownload />
</template>
导出数据
</a-button>
</div>
<div class="filter-section">
<a-form :model="filterForm" layout="inline">
<a-form-item label="项目">
<a-select v-model="filterForm.projectId" placeholder="请选择项目">
<a-option value="project-1">风电场A区</a-option>
<a-option value="project-2">风电场B区</a-option>
<a-option value="project-3">风电场C区</a-option>
</a-select>
</a-form-item>
<a-form-item label="机组号">
<a-input v-model="filterForm.unitNumber" placeholder="请输入机组号" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model="filterForm.status" placeholder="请选择状态">
<a-option value="completed">已完成</a-option>
<a-option value="pending">待分析</a-option>
<a-option value="analyzing">分析中</a-option>
<a-option value="failed">失败</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleFilterChange">查询</a-button>
</a-form-item>
</a-form>
</div>
</div>
<div class="project-sections">
<div v-for="project in filteredProjects" :key="project.id" class="project-section">
<div class="project-header">
<div class="project-title">{{ project.name }}</div>
<div class="project-stats">
<div class="stat-item">
<IconVideoCamera />
{{ project.totalVideos }} 个视频
</div>
<div class="stat-item">
<IconCheckCircle />
{{ project.completedCount }} 个已完成
</div>
<div class="stat-item">
<IconClockCircle />
{{ project.pendingCount }} 个待分析
</div>
</div>
</div>
<div class="units-grid">
<div v-for="unit in project.units" :key="unit.id" class="unit-card">
<div class="unit-header">
<div class="unit-title">{{ unit.number }}</div>
<div class="unit-actions">
<a-button type="primary" size="small" @click="handleViewUnitVideos(unit)">查看全部</a-button>
<a-button type="primary" size="small" @click="handleAnalyzeUnit(unit)">{{
getAnalysisButtonText(unit.status)
}}</a-button>
</div>
</div>
<div class="videos-list">
<div v-for="video in unit.videos" :key="video.id" class="video-item">
<div class="video-thumbnail">
<img :src="video.thumbnail" alt="Video Thumbnail" />
<div class="video-overlay" @click="handlePlayVideo(video)">
<IconPlayArrowFill style="font-size: 24px; color: #fff;" />
</div>
</div>
<div class="video-info">
<div class="video-name">{{ video.name }}</div>
<div class="video-meta">
<span>{{ video.duration }}</span>
<span>{{ video.angle }}°</span>
</div>
<div class="video-status">
<a-tag :color="getStatusColor(video.status)">{{ getStatusText(video.status) }}</a-tag>
</div>
</div>
</div>
</div>
<div class="analysis-progress">
<div class="progress-info">
<span>分析进度</span>
<span>{{ unit.progress }}%</span>
</div>
<a-progress :percent="unit.progress" :show-text="false" status="active" />
</div>
</div>
</div>
</div>
<!-- 筛选 -->
<div class="filter-section">
<a-form :model="filterForm" layout="inline">
<a-form-item label="项目" required>
<a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions" allow-clear
@change="" />
</a-form-item>
<a-form-item label="机组">
<a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions" allow-clear
:disabled="!filterForm.projectId" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleQuery">查询</a-button>
</a-form-item>
</a-form>
</div>
<!-- 视频播放模态框 -->
<a-modal v-model:visible="videoModalVisible" title="原始视频播放" width="900px" @ok="videoModalVisible = false"
@cancel="videoModalVisible = false">
<video v-if="selectedVideo" :src="selectedVideo.url" controls
style="width: 100%; height: 480px; border-radius: 8px; background: #000;"></video>
<div v-if="selectedVideo" class="video-meta-info">
<p>项目{{ selectedVideo.projectName }}</p>
<p>机组号{{ selectedVideo.unitNumber }}</p>
<p>采集人{{ selectedVideo.collector }}</p>
<p>风速{{ selectedVideo.windSpeed }} m/s</p>
<p>转速{{ selectedVideo.rpm }} rpm</p>
<p>采集时间{{ selectedVideo.time }}</p>
<p>角度{{ selectedVideo.angle }}°</p>
</div>
</a-modal>
<!-- 列表 -->
<a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
:scroll="{ y: 'calc(100vh - 380px)' }">
<template #type="{ record }">
<a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
</template>
<template #status="{ record }">
<a-tag :color="record.preTreatment ? 'green' : 'red'">
{{ record.preTreatment ? '已处理' : '未处理' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button size="mini" @click="handlePreview(record)">预览</a-button>
<a-button size="mini" @click="handleDownload(record)">下载</a-button>
<a-popconfirm content="确认删除?" @ok="handleDelete(record)">
<a-button size="mini" status="danger">删除</a-button>
</a-popconfirm>
</a-space>
<!-- 上传视频模态框 -->
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" @ok="handleUpload"
</template>
</a-table>
<!-- 上传弹窗 -->
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading" @ok="handleUpload"
@cancel="showUploadModal = false">
<a-form :model="uploadForm" layout="vertical">
<a-form-item label="项目" required>
<a-select v-model="uploadForm.projectId" placeholder="请选择项目">
<a-option value="project-1">风电场A区</a-option>
<a-option value="project-2">风电场B区</a-option>
<a-option value="project-3">风电场C区</a-option>
</a-select>
<a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions" allow-clear
@change="onProjectChangeUpload" />
</a-form-item>
<a-form-item label="机组号" required>
<a-input v-model="uploadForm.unitNumber" placeholder="请输入机组号" />
<a-form-item label="机组">
<a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload" allow-clear
:disabled="!uploadForm.projectId" />
</a-form-item>
<a-form-item label="采集人" required>
<a-input v-model="uploadForm.collector" placeholder="请输入采集人姓名" />
<a-form-item label="类型" required>
<a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
</a-form-item>
<a-form-item label="风速 (m/s)">
<a-input-number v-model="uploadForm.windSpeed" :min="0" />
<a-form-item label="视频文件" required>
<a-upload v-model:file-list="uploadForm.fileList" :multiple="uploadMode === 'batch'"
:limit="uploadMode === 'batch' ? 10 : 1" accept="video/*" :auto-upload="false" list-type="picture-card" />
</a-form-item>
<a-form-item label="转速 (rpm)">
<a-input-number v-model="uploadForm.rpm" :min="0" />
</a-form-item>
<a-form-item label="采集时间" required>
<a-date-picker v-model="uploadForm.time" show-time format="YYYY-MM-DD HH:mm" style="width: 100%;" />
</a-form-item>
<a-form-item label="视频文件可多选建议3个角度" required>
<a-upload v-model:file-list="uploadForm.fileList" :multiple="true" :limit="3" accept="video/*"
:auto-upload="false" list-type="picture-card">
<template #upload-button>
<a-button>选择视频</a-button>
</template>
</a-upload>
<a-form-item>
<a-radio-group v-model="uploadMode" type="button">
<a-radio value="single">单文件</a-radio>
<a-radio value="batch">批量</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
</div>
</GiPageLayout>
<!-- 视频预览弹窗 -->
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false" @cancel="previewVisible = false">
<a-tabs v-model:active-key="activePreviewTab" @change="activePreviewTab = $event as any">
<!-- 原始视频 -->
<a-tab-pane key="video" title="原始视频">
<video v-if="previewUrl" :src="previewUrl" controls
style="width: 100%; max-height: 60vh; border-radius: 4px"></video>
</a-tab-pane>
<!-- 处理结果 -->
<a-tab-pane key="result" title="处理结果">
<a-spin :loading="loadingResult">
<a-space direction="vertical" size="medium" style="width: 100%">
<!-- 图片 -->
<img v-if="resultImgUrl" :src="resultImgUrl" style="max-width: 100%; border-radius: 4px" alt="last frame" />
<!-- JSON 预览 -->
<a-card title="results.json" size="small">
<pre>{{ JSON.stringify(resultJson, null, 2) }}</pre>
</a-card>
</a-space>
</a-spin>
</a-tab-pane>
</a-tabs>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ref, reactive, onMounted, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import { IconUpload } from '@arco-design/web-vue/es/icon'
import {
IconUpload,
IconPlayCircle,
IconDownload,
IconVideoCamera,
IconCheckCircle,
IconClockCircle,
IconPlayArrowFill
} from '@arco-design/web-vue/es/icon'
getProjectList,
getTurbineList
} from '@/apis/industrial-image'
import {
getVideoPage,
uploadBatchVideo,
uploadSingleVideo,
deleteVideo,
downloadVideo
} from '@/apis/video-monitor'
const showUploadModal = ref(false)
const videoModalVisible = ref(false)
const selectedVideo = ref<any>(null)
/* ---------------- 下拉 & 表单 ---------------- */
const projectOptions = ref<{ label: string; value: string }[]>([])
const turbineOptions = ref<{ label: string; value: string }[]>([]) //
const turbineOptionsUpload = ref<{ label: string; value: string }[]>([]) //
const typeOptions = [
{ label: '净空', value: 'clearance' },
{ label: '形变', value: 'deformation' }
]
const filterForm = reactive({
projectId: '',
unitNumber: '',
status: ''
turbineId: ''
})
const uploadForm = reactive({
projectId: '',
unitNumber: '',
collector: '',
windSpeed: null,
rpm: null,
time: '',
fileList: []
turbineId: '',
type: '',
fileList: [] as any[]
})
//
const projects = ref([
{
id: 'project-1',
name: '风电场A区',
totalVideos: 6,
completedCount: 4,
pendingCount: 2,
units: [
{
id: 'A-001',
number: 'A-001',
status: 'completed',
progress: 100,
videos: [
{
id: 'v1',
name: 'A-001-正面',
url: '/videos/A-001-front.mp4',
thumbnail: '/images/A-001-front.jpg',
angle: 0,
duration: '00:30',
status: 'completed',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
},
{
id: 'v2',
name: 'A-001-侧面',
url: '/videos/A-001-side.mp4',
thumbnail: '/images/A-001-side.jpg',
angle: 90,
duration: '00:30',
status: 'completed',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
},
{
id: 'v3',
name: 'A-001-背面',
url: '/videos/A-001-back.mp4',
thumbnail: '/images/A-001-back.jpg',
angle: 180,
duration: '00:30',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
}
]
},
{
id: 'A-002',
number: 'A-002',
status: 'analyzing',
progress: 60,
videos: [
{
id: 'v4',
name: 'A-002-正面',
url: '/videos/A-002-front.mp4',
thumbnail: '/images/A-002-front.jpg',
angle: 0,
duration: '00:28',
status: 'analyzing',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
},
{
id: 'v5',
name: 'A-002-侧面',
url: '/videos/A-002-side.mp4',
thumbnail: '/images/A-002-side.jpg',
angle: 90,
duration: '00:28',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
},
{
id: 'v6',
name: 'A-002-背面',
url: '/videos/A-002-back.mp4',
thumbnail: '/images/A-002-back.jpg',
angle: 180,
duration: '00:28',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
}
]
}
]
}
// ...
])
const uploadMode = ref<'single' | 'batch'>('single')
const filteredProjects = computed(() => {
//
return projects.value
.filter(p => !filterForm.projectId || p.id === filterForm.projectId)
.map(project => ({
...project,
units: project.units
.filter(u => !filterForm.unitNumber || u.number === filterForm.unitNumber)
.map(unit => ({
...unit,
videos: unit.videos.filter(v => !filterForm.status || v.status === filterForm.status)
}))
.filter(u => u.videos.length > 0)
}))
.filter(p => p.units.length > 0)
/* ---------------- 列表 ---------------- */
const columns: TableColumnData[] = [
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
// { title: '', dataIndex: 'projectName' },
// { title: '', dataIndex: 'turbineName' },
{ title: '类型', slotName: 'type' },
{ title: '上传时间', dataIndex: 'uploadTime' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
]
const tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
/* ---------------- 控制弹窗 ---------------- */
const showUploadModal = ref(false)
const uploading = ref(false)
/* ---------------- 初始化 ---------------- */
onMounted(async () => {
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
projectOptions.value = data.map((p: any) => ({ label: p.projectName, value: p.projectId }))
handleQuery()
})
const activePreviewTab = ref<'video' | 'result'>('video') //
const resultImgUrl = ref('')
const resultJson = ref<Record<string, any>>({})
const loadingResult = ref(false)
function handleFilterChange() {
// API
async function loadResultFiles(row: any) {
if (!row.preTreatment) return
loadingResult.value = true
try {
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
//
resultImgUrl.value = `${base}${row.preImagePath}/last_frame.jpg`
// JSON
const jsonUrl = `${base}${row.preImagePath}/results.json`
const res = await fetch(jsonUrl)
resultJson.value = await res.json()
} catch (e) {
console.error(e)
resultJson.value = {}
} finally {
loadingResult.value = false
}
console.log('result', resultImgUrl.value)
}
/* 项目 -> 机组(筛选) */
watch(
() => filterForm.projectId,
async (val) => {
filterForm.turbineId = ''
turbineOptions.value = []
if (!val) return
const { data } = await getTurbineList({ projectId: val })
turbineOptions.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
}
)
const previewVisible = ref(false)
const previewUrl = ref('')
function handlePlayVideo(video: any) {
selectedVideo.value = video
videoModalVisible.value = true
}
function handleViewUnitVideos(unit: any) {
//
Message.info(`查看机组 ${unit.number} 的所有视频`)
}
function handleAnalyzeUnit(unit: any) {
//
Message.success(`已提交机组 ${unit.number} 的分析任务`)
// API
}
function getStatusColor(status: string) {
switch (status) {
case 'completed': return 'green'
case 'pending': return 'gray'
case 'analyzing': return 'blue'
case 'failed': return 'red'
default: return 'gray'
function handlePreview(row: any) {
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
previewVisible.value = true
activePreviewTab.value = 'video' //
if (row.preTreatment) {
loadResultFiles(row) //
}
}
function getStatusText(status: string) {
switch (status) {
case 'completed': return '已完成'
case 'pending': return '待分析'
case 'analyzing': return '分析中'
case 'failed': return '失败'
default: return '未知'
}
/* 项目 -> 机组(上传弹窗) */
async function onProjectChangeUpload(projectId: string) {
uploadForm.turbineId = ''
turbineOptionsUpload.value = []
if (!projectId) return
const { data } = await getTurbineList({ projectId })
turbineOptionsUpload.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
}
function getAnalysisButtonText(status: string) {
switch (status) {
case 'completed': return '重新分析'
case 'pending': return '分析'
case 'analyzing': return '分析中...'
case 'failed': return '重新分析'
default: return '分析'
/* ---------------- 查询 ---------------- */
function handleQuery() {
pagination.current = 1
loadTable()
}
async function loadTable() {
loading.value = true
try {
const params = {
pageNo: pagination.current,
pageSize: pagination.pageSize,
projectId: filterForm.projectId,
turbineId: filterForm.turbineId || undefined
}
const { data } = await getVideoPage(params)
console.log(data)
tableData.value = data
pagination.total = data.length
} finally {
loading.value = false
}
}
function handleBatchAnalysis() {
Message.success('批量分析任务已提交')
/* ---------------- 上传 ---------------- */
function openUploadModal() {
uploadForm.projectId = ''
uploadForm.turbineId = ''
uploadForm.type = ''
uploadForm.fileList = []
showUploadModal.value = true
}
function handleExportData() {
Message.success('数据导出成功')
async function handleUpload() {
if (!uploadForm.projectId || !uploadForm.type || !uploadForm.fileList.length) {
Message.warning('请完整填写')
return
}
uploading.value = true
try {
const files = uploadForm.fileList.map((f: any) => f.file)
if (uploadMode.value === 'single') {
await uploadSingleVideo(
uploadForm.projectId,
uploadForm.turbineId || '',
uploadForm.type,
files[0]
)
} else {
await uploadBatchVideo(
uploadForm.projectId,
uploadForm.turbineId || '',
uploadForm.type,
files
)
}
Message.success('上传成功')
showUploadModal.value = false
loadTable()
} finally {
uploading.value = false
}
}
function handleUpload() {
Message.success('上传成功')
showUploadModal.value = false
/* ---------------- 下载 / 删除 ---------------- */
async function handleDownload(row: any) {
const url = await downloadVideo(row.videoId)
window.open(url, '_blank')
}
async function handleDelete(row: any) {
await deleteVideo(row.videoId)
Message.success('删除成功')
loadTable()
}
</script>
<style scoped lang="scss">
.raw-data-container {
padding: 20px;

View File

@ -117,8 +117,9 @@
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:pagination="false"
row-key="approvalId"
:scroll="{ x: 'max-content', y: 400 }"
@change="handleTableChange"
>
<!-- 业务类型 -->
@ -193,6 +194,23 @@
</a-space>
</template>
</a-table>
<!-- 分页器 - 固定在表格下方 -->
<div class="pagination-container">
<a-pagination
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:show-total="true"
:show-jumper="true"
:show-page-size="true"
:page-size-options="[10, 20, 50, 100]"
:hide-on-single-page="false"
size="default"
@change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</a-card>
<!-- 审批详情弹窗 -->
@ -249,6 +267,7 @@ const pagination = reactive<any>({
showPageSize: true,
showJumper: true,
showTotal: (total: number) => `${total} 条记录`,
pageSizeOptions: [10, 20, 50, 100]
})
//
@ -460,8 +479,8 @@ const loadData = async (searchParams?: EquipmentApprovalListReq) => {
try {
// -
const params: EquipmentApprovalListReq = {
pageNum: pagination.current, // pageNum
pageSize: pagination.pageSize,
page: pagination.current,
...(searchParams || {}),
}
@ -478,46 +497,52 @@ const loadData = async (searchParams?: EquipmentApprovalListReq) => {
console.log('API响应:', res)
//
let dataList: any[] = []
let total = 0
if (res.code === 200 || res.success || res.status === 200) {
let dataList: any[] = []
let totalCount = 0
if (res && res.data) {
if (Array.isArray(res.data)) {
// - PageResult
if ((res as any).rows && Array.isArray((res as any).rows)) {
// PageResult rows
dataList = (res as any).rows
totalCount = (res as any).total || 0
console.log('从 rows 字段获取数据,总数:', totalCount)
} else if (Array.isArray(res.data)) {
dataList = res.data
total = res.data.length
totalCount = (res as any).total || dataList.length || 0
console.log('从 data 字段获取数据,总数:', totalCount)
} else if (res.data && Array.isArray((res.data as any).records)) {
dataList = (res.data as any).records
total = (res.data as any).total || 0
totalCount = (res.data as any).total || dataList.length || 0
console.log('从 records 字段获取数据,总数:', totalCount)
} else if (res.data && Array.isArray((res.data as any).list)) {
dataList = (res.data as any).list
total = (res.data as any).total || 0
} else if (res.data && Array.isArray((res.data as any).rows)) {
dataList = (res.data as any).rows
total = (res.data as any).total || 0
} else if (res.data && Array.isArray((res.data as any).data)) {
dataList = (res.data as any).data
total = (res.data as any).total || 0
totalCount = (res.data as any).total || dataList.length || 0
console.log('从 list 字段获取数据,总数:', totalCount)
} else {
console.warn('未找到有效的数据字段,响应结构:', res)
dataList = []
totalCount = 0
}
} else if (Array.isArray(res)) {
dataList = res
total = res.length
}
console.log('处理后的数据列表:', dataList)
console.log('总数:', total)
if (dataList.length > 0) {
const transformedData = transformBackendData(dataList)
tableData.value = transformedData
console.log('数据转换完成,设置到表格:', transformedData.length, '条')
} else {
tableData.value = []
console.log('没有数据,清空表格')
}
if (dataList.length > 0) {
const transformedData = transformBackendData(dataList)
console.log('转换后的数据:', transformedData)
tableData.value = transformedData
// - 使
pagination.total = totalCount
console.log('设置分页总数:', totalCount)
} else {
console.log('没有数据,设置空数组')
console.error('请求失败,响应:', res)
message.error(res.msg || (res as any).message || '加载数据失败')
tableData.value = []
pagination.total = 0
}
pagination.total = total
console.log('设置分页总数:', pagination.total)
} catch (error: any) {
console.error('加载数据失败:', error)
message.error(error?.message || '加载数据失败')
@ -525,7 +550,6 @@ const loadData = async (searchParams?: EquipmentApprovalListReq) => {
pagination.total = 0
} finally {
loading.value = false
console.log('📊 loadData - 加载完成')
}
}
@ -552,6 +576,21 @@ const handleTableChange = (pag: any) => {
loadData(currentSearchParams.value || {})
}
//
const handlePageChange = (page: number) => {
console.log('页码变化:', page)
pagination.current = page
loadData(currentSearchParams.value || {})
}
//
const handlePageSizeChange = (pageSize: number) => {
console.log('每页条数变化:', pageSize)
pagination.pageSize = pageSize
pagination.current = 1 //
loadData(currentSearchParams.value || {})
}
//
const handleTabChange = (key: string) => {
activeTab.value = key
@ -781,6 +820,42 @@ onMounted(() => {
color: var(--color-text-4);
font-style: italic;
}
// -
.pagination-container {
position: sticky;
bottom: 0;
background: white;
padding: 16px 24px;
border-top: 1px solid var(--color-border);
display: flex;
justify-content: flex-end;
align-items: center;
z-index: 10;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
.arco-pagination {
margin: 0;
.arco-pagination-item {
border-radius: 6px;
margin: 0 4px;
&.arco-pagination-item-active {
background: var(--color-primary);
border-color: var(--color-primary);
}
}
.arco-pagination-size-changer {
margin-left: 16px;
}
.arco-pagination-jumper {
margin-left: 16px;
}
}
}
//
.approval-search-container {

View File

@ -117,9 +117,13 @@
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:pagination="false"
row-key="equipmentId"
@change="handleTableChange"
:scroll="{ x: 'max-content', y: 400 }"
:bordered="false"
:stripe="true"
size="medium"
table-layout="auto"
>
<template #equipmentType="{ record }">
<a-tag :color="getEquipmentTypeColor(record.equipmentType)">
@ -205,6 +209,23 @@
</a-space>
</template>
</a-table>
<!-- 分页器 - 固定在表格下方 -->
<div class="pagination-container">
<a-pagination
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:show-total="true"
:show-jumper="true"
:show-page-size="true"
:page-size-options="[10, 20, 50, 100]"
:hide-on-single-page="false"
size="default"
@change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</a-card>
<!-- 新增/编辑弹窗 -->
@ -247,13 +268,18 @@ const tableData = ref<EquipmentResp[]>([])
const loading = ref(false)
//
const pagination = reactive<any>({
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showPageSize: true,
showTotal: true,
showJumper: true,
showTotal: (total: number) => `${total} 条记录`,
showPageSize: true,
pageSizeOptions: [10, 20, 50, 100],
//
hideOnSinglePage: false,
//
size: 'default'
})
//
@ -568,46 +594,101 @@ const transformBackendData = (data: any[]) => {
const loadData = async (searchParams?: EquipmentPageQuery) => {
loading.value = true
try {
//
const params = {
pageSize: pagination.pageSize,
page: pagination.current,
pageNum: pagination.current, // - 使pageNum
pageSize: pagination.pageSize, //
...(searchParams || {}), //
}
console.log('🚀 发送分页请求参数:', params)
console.log('📊 当前分页状态:', {
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total
})
const res = await EquipmentAPI.pageEquipment(params)
console.log('📡 后端响应数据:', res)
//
if (res.success || res.status === 200 || res.code === 200) {
if (res.code === 200 || res.success || res.status === 200) {
//
let dataList: any[] = []
let totalCount = 0
//
if (Array.isArray(res.data)) {
dataList = res.data
} else if (res.data && Array.isArray((res.data as any).records)) {
dataList = (res.data as any).records
} else if (res.data && Array.isArray((res.data as any).list)) {
dataList = (res.data as any).list
} else if (res.data && Array.isArray((res.data as any).rows)) {
dataList = (res.data as any).rows
console.log('🔍 开始解析响应数据,响应结构:', res)
// - PageResult
if ((res as any).rows && Array.isArray((res as any).rows)) {
// PageResult rows
dataList = (res as any).rows
totalCount = (res as any).total || 0
console.log('✅ 从 rows 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
} else if ((res as any).data && Array.isArray((res as any).data)) {
dataList = (res as any).data
totalCount = (res as any).total || dataList.length || 0
console.log('✅ 从 data 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
} else if ((res as any).data && (res as any).data.rows && Array.isArray((res as any).data.rows)) {
// res.data.rows res.data.total
dataList = (res as any).data.rows
totalCount = (res as any).data.total || 0
console.log('✅ 从嵌套 data.rows 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
} else if ((res as any).data && Array.isArray(((res as any).data as any).records)) {
dataList = ((res as any).data as any).records
totalCount = (((res as any).data as any).total) || dataList.length || 0
console.log('✅ 从 records 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
} else if ((res as any).data && Array.isArray(((res as any).data as any).list)) {
dataList = ((res as any).data as any).list
totalCount = (((res as any).data as any).total) || dataList.length || 0
console.log('✅ 从 list 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
} else {
console.warn('⚠️ 未找到有效的数据字段,响应结构:', res)
dataList = []
totalCount = 0
}
// total使
if (dataList.length > 0 && totalCount === 0) {
totalCount = dataList.length
console.log('⚠️ 后端未返回total使用当前页数据长度作为总数:', totalCount)
}
// total0
if (dataList.length > 0 && totalCount === 0) {
console.warn('🚨 后端分页逻辑问题返回了数据但total为0使用当前页数据长度作为总数')
totalCount = dataList.length
}
if (dataList.length > 0) {
const transformedData = transformBackendData(dataList)
tableData.value = transformedData
console.log('✅ 数据转换完成,设置到表格:', transformedData.length, '条')
} else {
tableData.value = []
console.log(' 没有数据,清空表格')
}
//
pagination.total = (res.data as any)?.total || (res as any).total || dataList.length || 0
// - 使
pagination.total = totalCount
console.log('📊 设置分页总数:', totalCount)
console.log('📊 分页组件状态:', {
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total
})
} else {
message.error(res.msg || '加载数据失败')
console.error('❌ 请求失败,响应:', res)
message.error((res as any).msg || (res as any).message || '加载数据失败')
tableData.value = []
pagination.total = 0
}
} catch (error: any) {
console.error('加载数据失败:', error)
console.error('加载数据失败:', error)
message.error(error?.message || '加载数据失败')
tableData.value = []
pagination.total = 0
} finally {
loading.value = false
}
@ -631,11 +712,42 @@ const handleReset = () => {
//
const handleTableChange = (pag: any) => {
pagination.current = pag.current || 1
pagination.pageSize = pag.pageSize || 10
console.log('表格变化,分页参数:', pag) //
//
if (pag.current !== undefined) {
pagination.current = pag.current
}
if (pag.pageSize !== undefined) {
pagination.pageSize = pag.pageSize
}
console.log('更新分页配置:', {
current: pagination.current,
pageSize: pagination.pageSize
})
//
loadData(currentSearchParams.value)
}
//
const handlePageChange = (page: number) => {
console.log('页码变化:', page)
pagination.current = page
loadData(currentSearchParams.value)
}
//
const handlePageSizeChange = (pageSize: number) => {
console.log('每页条数变化:', pageSize)
pagination.pageSize = pageSize
pagination.current = 1 //
loadData(currentSearchParams.value)
}
//
const handleAdd = () => {
modalMode.value = 'add'
@ -920,7 +1032,8 @@ onMounted(() => {
.table-card {
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
overflow: visible; // visible
min-height: 600px; //
.card-title {
display: flex;
@ -943,6 +1056,11 @@ onMounted(() => {
}
.arco-table {
//
.arco-table-container {
overflow: visible;
}
.arco-table-th {
background: var(--color-fill-2);
font-weight: 600;
@ -956,6 +1074,12 @@ onMounted(() => {
.arco-table-tr:hover {
background: var(--color-fill-1);
}
//
.arco-table-body {
overflow-y: auto;
max-height: 500px; //
}
}
}
@ -999,18 +1123,38 @@ onMounted(() => {
}
}
//
.arco-pagination {
margin-top: 24px;
justify-content: center;
// -
.pagination-container {
position: sticky;
bottom: 0;
background: white;
padding: 16px 24px;
border-top: 1px solid var(--color-border);
display: flex;
justify-content: flex-end;
align-items: center;
z-index: 10;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
.arco-pagination-item {
border-radius: 6px;
margin: 0 4px;
.arco-pagination {
margin: 0;
&.arco-pagination-item-active {
background: var(--color-primary);
border-color: var(--color-primary);
.arco-pagination-item {
border-radius: 6px;
margin: 0 4px;
&.arco-pagination-item-active {
background: var(--color-primary);
border-color: var(--color-primary);
}
}
.arco-pagination-size-changer {
margin-left: 16px;
}
.arco-pagination-jumper {
margin-left: 16px;
}
}
}

View File

@ -116,9 +116,10 @@
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:pagination="false"
:row-selection="rowSelection"
row-key="equipmentId"
:scroll="{ x: 'max-content', y: 400 }"
@change="handleTableChange"
>
<!-- 设备状态 -->
@ -218,6 +219,23 @@
</a-space>
</template>
</a-table>
<!-- 分页器 - 固定在表格下方 -->
<div class="pagination-container">
<a-pagination
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:show-total="true"
:show-jumper="true"
:show-page-size="true"
:page-size-options="[10, 20, 50, 100]"
:hide-on-single-page="false"
size="default"
@change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</a-card>
<!-- 新增/编辑弹窗 -->
@ -277,6 +295,7 @@ const pagination = reactive<any>({
showPageSize: true,
showJumper: true,
showTotal: (total: number) => `${total} 条记录`,
pageSizeOptions: [10, 20, 50, 100]
})
//
@ -592,31 +611,52 @@ const loadData = async (searchParams?: EquipmentListReq) => {
loading.value = true
try {
//
const params: EquipmentListReq = {
pageSize: pagination.pageSize,
page: pagination.current,
pageNum: pagination.current, //
pageSize: pagination.pageSize, //
minPrice: undefined,
maxPrice: undefined,
...(searchParams || {}),
}
console.log('📊 loadData - 构建的完整请求参数:', params)
console.log('📊 当前分页状态:', {
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total
})
const res = await equipmentProcurementApi.page(params)
console.log('API响应:', res)
if (res.success || res.status === 200 || res.code === 200) {
if (res.code === 200 || res.success || res.status === 200) {
let dataList: any[] = []
let totalCount = 0
if (Array.isArray(res.data)) {
// - PageResult
if ((res as any).rows && Array.isArray((res as any).rows)) {
// PageResult rows
dataList = (res as any).rows
totalCount = (res as any).total || 0
console.log('✅ 从 rows 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
} else if (Array.isArray(res.data)) {
dataList = res.data
totalCount = (res as any).total || dataList.length || 0
console.log('✅ 从 data 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
} else if (res.data && Array.isArray((res.data as any).records)) {
dataList = (res.data as any).records
totalCount = (res.data as any).total || dataList.length || 0
console.log('✅ 从 records 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
} else if (res.data && Array.isArray((res.data as any).list)) {
dataList = (res.data as any).list
} else if (res.data && Array.isArray((res.data as any).rows)) {
dataList = (res.data as any).rows
totalCount = (res.data as any).total || dataList.length || 0
console.log('✅ 从 list 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
} else {
console.warn('⚠️ 未找到有效的数据字段,响应结构:', res)
dataList = []
totalCount = 0
}
console.log('处理后的数据列表:', dataList)
@ -633,14 +673,25 @@ const loadData = async (searchParams?: EquipmentListReq) => {
tableData.value = []
}
pagination.total = (res.data as any)?.total || (res as any).total || dataList.length || 0
console.log('总数:', pagination.total)
// - 使
pagination.total = totalCount
console.log('📊 总数:', totalCount)
console.log('📊 分页组件状态:', {
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total
})
} else {
message.error(res.msg || '加载数据失败')
console.error('❌ 请求失败,响应:', res)
message.error(res.msg || (res as any).message || '加载数据失败')
tableData.value = []
pagination.total = 0
}
} catch (error: any) {
console.error('加载数据失败:', error)
console.error('加载数据失败:', error)
message.error(error?.message || '加载数据失败')
tableData.value = []
pagination.total = 0
} finally {
loading.value = false
}
@ -672,6 +723,21 @@ const handleTableChange = (pag: any) => {
loadData(currentSearchParams.value)
}
//
const handlePageChange = (page: number) => {
console.log('页码变化:', page)
pagination.current = page
loadData(currentSearchParams.value)
}
//
const handlePageSizeChange = (pageSize: number) => {
console.log('每页条数变化:', pageSize)
pagination.pageSize = pageSize
pagination.current = 1 //
loadData(currentSearchParams.value)
}
//
const handleAdd = () => {
modalMode.value = 'add'
@ -958,6 +1024,42 @@ onMounted(() => {
color: var(--color-text-4);
font-style: italic;
}
// -
.pagination-container {
position: sticky;
bottom: 0;
background: white;
padding: 16px 24px;
border-top: 1px solid var(--color-border);
display: flex;
justify-content: flex-end;
align-items: center;
z-index: 10;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
.arco-pagination {
margin: 0;
.arco-pagination-item {
border-radius: 6px;
margin: 0 4px;
&.arco-pagination-item-active {
background: var(--color-primary);
border-color: var(--color-primary);
}
}
.arco-pagination-size-changer {
margin-left: 16px;
}
.arco-pagination-jumper {
margin-left: 16px;
}
}
}
}
//