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) { 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) { 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 = { const requestParams = {
...params, ...params,
// 确保分页参数存在 // 直接使用 pageNum因为后端分页器需要的是 pageNum 参数
page: params.page || 1, pageNum: params.pageNum || 1,
pageSize: params.pageSize || 10, 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 // for type re-export
declare global { declare global {
// @ts-ignore // @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') import('vue')
} }

View File

@ -7,69 +7,7 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { 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'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] 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 invoice?: string
barcode?: string barcode?: string
importer?: string importer?: string
page?: number pageNum?: number // 当前页码 - 与后端分页器期望的参数名保持一致
pageSize?: number pageSize?: number // 每页大小
orderBy?: string orderBy?: string
orderDirection?: string orderDirection?: string
} }

View File

@ -1,407 +1,311 @@
<template> <template>
<GiPageLayout> <GiPageLayout>
<div class="raw-data-container"> <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-bar">
<div class="action-buttons"> <div class="action-buttons">
<a-button type="primary" @click="showUploadModal = true"> <a-button type="primary" @click="openUploadModal">
<template #icon> <template #icon>
<IconUpload /> <IconUpload />
</template> </template>
上传视频 上传视频
</a-button> </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> </div>
<div class="project-sections"> <!-- 筛选 -->
<div v-for="project in filteredProjects" :key="project.id" class="project-section"> <div class="filter-section">
<div class="project-header"> <a-form :model="filterForm" layout="inline">
<div class="project-title">{{ project.name }}</div> <a-form-item label="项目" required>
<div class="project-stats"> <a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions" allow-clear
<div class="stat-item"> @change="" />
<IconVideoCamera /> </a-form-item>
{{ project.totalVideos }} 个视频 <a-form-item label="机组">
</div> <a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions" allow-clear
<div class="stat-item"> :disabled="!filterForm.projectId" />
<IconCheckCircle /> </a-form-item>
{{ project.completedCount }} 个已完成 <a-form-item>
</div> <a-button type="primary" @click="handleQuery">查询</a-button>
<div class="stat-item"> </a-form-item>
<IconClockCircle /> </a-form>
{{ 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> </div>
<!-- 视频播放模态框 --> <!-- 列表 -->
<a-modal v-model:visible="videoModalVisible" title="原始视频播放" width="900px" @ok="videoModalVisible = false" <a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
@cancel="videoModalVisible = false"> :scroll="{ y: 'calc(100vh - 380px)' }">
<video v-if="selectedVideo" :src="selectedVideo.url" controls <template #type="{ record }">
style="width: 100%; height: 480px; border-radius: 8px; background: #000;"></video> <a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
<div v-if="selectedVideo" class="video-meta-info"> </template>
<p>项目{{ selectedVideo.projectName }}</p> <template #status="{ record }">
<p>机组号{{ selectedVideo.unitNumber }}</p> <a-tag :color="record.preTreatment ? 'green' : 'red'">
<p>采集人{{ selectedVideo.collector }}</p> {{ record.preTreatment ? '已处理' : '未处理' }}
<p>风速{{ selectedVideo.windSpeed }} m/s</p> </a-tag>
<p>转速{{ selectedVideo.rpm }} rpm</p> </template>
<p>采集时间{{ selectedVideo.time }}</p> <template #action="{ record }">
<p>角度{{ selectedVideo.angle }}°</p> <a-space>
</div> <a-button size="mini" @click="handlePreview(record)">预览</a-button>
</a-modal> <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>
<!-- 上传视频模态框 --> </template>
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" @ok="handleUpload" </a-table>
<!-- 上传弹窗 -->
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading" @ok="handleUpload"
@cancel="showUploadModal = false"> @cancel="showUploadModal = false">
<a-form :model="uploadForm" layout="vertical"> <a-form :model="uploadForm" layout="vertical">
<a-form-item label="项目" required> <a-form-item label="项目" required>
<a-select v-model="uploadForm.projectId" placeholder="请选择项目"> <a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions" allow-clear
<a-option value="project-1">风电场A区</a-option> @change="onProjectChangeUpload" />
<a-option value="project-2">风电场B区</a-option>
<a-option value="project-3">风电场C区</a-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item label="机组号" required> <a-form-item label="机组">
<a-input v-model="uploadForm.unitNumber" placeholder="请输入机组号" /> <a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload" allow-clear
:disabled="!uploadForm.projectId" />
</a-form-item> </a-form-item>
<a-form-item label="采集人" required> <a-form-item label="类型" required>
<a-input v-model="uploadForm.collector" placeholder="请输入采集人姓名" /> <a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
</a-form-item> </a-form-item>
<a-form-item label="风速 (m/s)"> <a-form-item label="视频文件" required>
<a-input-number v-model="uploadForm.windSpeed" :min="0" /> <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>
<a-form-item label="转速 (rpm)"> <a-form-item>
<a-input-number v-model="uploadForm.rpm" :min="0" /> <a-radio-group v-model="uploadMode" type="button">
</a-form-item> <a-radio value="single">单文件</a-radio>
<a-form-item label="采集时间" required> <a-radio value="batch">批量</a-radio>
<a-date-picker v-model="uploadForm.time" show-time format="YYYY-MM-DD HH:mm" style="width: 100%;" /> </a-radio-group>
</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-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
</div> </div>
</GiPageLayout> </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> </template>
<script setup lang="ts"> <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 { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import { IconUpload } from '@arco-design/web-vue/es/icon'
import { import {
IconUpload, getProjectList,
IconPlayCircle, getTurbineList
IconDownload, } from '@/apis/industrial-image'
IconVideoCamera, import {
IconCheckCircle, getVideoPage,
IconClockCircle, uploadBatchVideo,
IconPlayArrowFill uploadSingleVideo,
} from '@arco-design/web-vue/es/icon' deleteVideo,
downloadVideo
} from '@/apis/video-monitor'
const showUploadModal = ref(false) /* ---------------- 下拉 & 表单 ---------------- */
const videoModalVisible = ref(false) const projectOptions = ref<{ label: string; value: string }[]>([])
const selectedVideo = ref<any>(null) 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({ const filterForm = reactive({
projectId: '', projectId: '',
unitNumber: '', turbineId: ''
status: ''
}) })
const uploadForm = reactive({ const uploadForm = reactive({
projectId: '', projectId: '',
unitNumber: '', turbineId: '',
collector: '', type: '',
windSpeed: null, fileList: [] as any[]
rpm: null,
time: '',
fileList: []
}) })
// const uploadMode = ref<'single' | 'batch'>('single')
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 filteredProjects = computed(() => { /* ---------------- 列表 ---------------- */
// const columns: TableColumnData[] = [
return projects.value { title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
.filter(p => !filterForm.projectId || p.id === filterForm.projectId) // { title: '', dataIndex: 'projectName' },
.map(project => ({ // { title: '', dataIndex: 'turbineName' },
...project, { title: '类型', slotName: 'type' },
units: project.units { title: '上传时间', dataIndex: 'uploadTime' },
.filter(u => !filterForm.unitNumber || u.number === filterForm.unitNumber) { title: '状态', slotName: 'status' },
.map(unit => ({ { title: '操作', slotName: 'action', width: 120, fixed: 'right' }
...unit, ]
videos: unit.videos.filter(v => !filterForm.status || v.status === filterForm.status)
})) const tableData = ref<any[]>([])
.filter(u => u.videos.length > 0) const loading = ref(false)
})) const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
.filter(p => p.units.length > 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() { async function loadResultFiles(row: any) {
// API 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) { function handlePreview(row: any) {
selectedVideo.value = video const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
videoModalVisible.value = true previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
} previewVisible.value = true
activePreviewTab.value = 'video' //
function handleViewUnitVideos(unit: any) { if (row.preTreatment) {
// loadResultFiles(row) //
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 getStatusText(status: string) { /* 项目 -> 机组(上传弹窗) */
switch (status) { async function onProjectChangeUpload(projectId: string) {
case 'completed': return '已完成' uploadForm.turbineId = ''
case 'pending': return '待分析' turbineOptionsUpload.value = []
case 'analyzing': return '分析中' if (!projectId) return
case 'failed': return '失败' const { data } = await getTurbineList({ projectId })
default: return '未知' turbineOptionsUpload.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
}
} }
function getAnalysisButtonText(status: string) {
switch (status) { /* ---------------- 查询 ---------------- */
case 'completed': return '重新分析' function handleQuery() {
case 'pending': return '分析' pagination.current = 1
case 'analyzing': return '分析中...' loadTable()
case 'failed': return '重新分析' }
default: return '分析' 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() { async function handleUpload() {
Message.success('数据导出成功') 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.raw-data-container { .raw-data-container {
padding: 20px; padding: 20px;

View File

@ -117,8 +117,9 @@
:columns="columns" :columns="columns"
:data="tableData" :data="tableData"
:loading="loading" :loading="loading"
:pagination="pagination" :pagination="false"
row-key="approvalId" row-key="approvalId"
:scroll="{ x: 'max-content', y: 400 }"
@change="handleTableChange" @change="handleTableChange"
> >
<!-- 业务类型 --> <!-- 业务类型 -->
@ -193,6 +194,23 @@
</a-space> </a-space>
</template> </template>
</a-table> </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> </a-card>
<!-- 审批详情弹窗 --> <!-- 审批详情弹窗 -->
@ -249,6 +267,7 @@ const pagination = reactive<any>({
showPageSize: true, showPageSize: true,
showJumper: true, showJumper: true,
showTotal: (total: number) => `${total} 条记录`, showTotal: (total: number) => `${total} 条记录`,
pageSizeOptions: [10, 20, 50, 100]
}) })
// //
@ -460,8 +479,8 @@ const loadData = async (searchParams?: EquipmentApprovalListReq) => {
try { try {
// - // -
const params: EquipmentApprovalListReq = { const params: EquipmentApprovalListReq = {
pageNum: pagination.current, // pageNum
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
page: pagination.current,
...(searchParams || {}), ...(searchParams || {}),
} }
@ -478,46 +497,52 @@ const loadData = async (searchParams?: EquipmentApprovalListReq) => {
console.log('API响应:', res) console.log('API响应:', res)
// if (res.code === 200 || res.success || res.status === 200) {
let dataList: any[] = [] let dataList: any[] = []
let total = 0 let totalCount = 0
if (res && res.data) { // - PageResult
if (Array.isArray(res.data)) { 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 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)) { } else if (res.data && Array.isArray((res.data as any).records)) {
dataList = (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)) { } else if (res.data && Array.isArray((res.data as any).list)) {
dataList = (res.data as any).list dataList = (res.data as any).list
total = (res.data as any).total || 0 totalCount = (res.data as any).total || dataList.length || 0
} else if (res.data && Array.isArray((res.data as any).rows)) { console.log('从 list 字段获取数据,总数:', totalCount)
dataList = (res.data as any).rows } else {
total = (res.data as any).total || 0 console.warn('未找到有效的数据字段,响应结构:', res)
} else if (res.data && Array.isArray((res.data as any).data)) { dataList = []
dataList = (res.data as any).data totalCount = 0
total = (res.data as any).total || 0
} }
} else if (Array.isArray(res)) {
dataList = res
total = res.length
}
console.log('处理后的数据列表:', dataList) if (dataList.length > 0) {
console.log('总数:', total) const transformedData = transformBackendData(dataList)
tableData.value = transformedData
console.log('数据转换完成,设置到表格:', transformedData.length, '条')
} else {
tableData.value = []
console.log('没有数据,清空表格')
}
if (dataList.length > 0) { // - 使
const transformedData = transformBackendData(dataList) pagination.total = totalCount
console.log('转换后的数据:', transformedData) console.log('设置分页总数:', totalCount)
tableData.value = transformedData
} else { } else {
console.log('没有数据,设置空数组') console.error('请求失败,响应:', res)
message.error(res.msg || (res as any).message || '加载数据失败')
tableData.value = [] tableData.value = []
pagination.total = 0
} }
pagination.total = total
console.log('设置分页总数:', pagination.total)
} catch (error: any) { } catch (error: any) {
console.error('加载数据失败:', error) console.error('加载数据失败:', error)
message.error(error?.message || '加载数据失败') message.error(error?.message || '加载数据失败')
@ -525,7 +550,6 @@ const loadData = async (searchParams?: EquipmentApprovalListReq) => {
pagination.total = 0 pagination.total = 0
} finally { } finally {
loading.value = false loading.value = false
console.log('📊 loadData - 加载完成')
} }
} }
@ -552,6 +576,21 @@ const handleTableChange = (pag: any) => {
loadData(currentSearchParams.value || {}) 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) => { const handleTabChange = (key: string) => {
activeTab.value = key activeTab.value = key
@ -781,6 +820,42 @@ onMounted(() => {
color: var(--color-text-4); color: var(--color-text-4);
font-style: italic; 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 { .approval-search-container {

View File

@ -117,9 +117,13 @@
:columns="columns" :columns="columns"
:data="tableData" :data="tableData"
:loading="loading" :loading="loading"
:pagination="pagination" :pagination="false"
row-key="equipmentId" row-key="equipmentId"
@change="handleTableChange" :scroll="{ x: 'max-content', y: 400 }"
:bordered="false"
:stripe="true"
size="medium"
table-layout="auto"
> >
<template #equipmentType="{ record }"> <template #equipmentType="{ record }">
<a-tag :color="getEquipmentTypeColor(record.equipmentType)"> <a-tag :color="getEquipmentTypeColor(record.equipmentType)">
@ -205,6 +209,23 @@
</a-space> </a-space>
</template> </template>
</a-table> </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> </a-card>
<!-- 新增/编辑弹窗 --> <!-- 新增/编辑弹窗 -->
@ -247,13 +268,18 @@ const tableData = ref<EquipmentResp[]>([])
const loading = ref(false) const loading = ref(false)
// //
const pagination = reactive<any>({ const pagination = reactive({
current: 1, current: 1,
pageSize: 10, pageSize: 10,
total: 0, total: 0,
showPageSize: true, showTotal: true,
showJumper: 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) => { const loadData = async (searchParams?: EquipmentPageQuery) => {
loading.value = true loading.value = true
try { try {
//
const params = { const params = {
pageSize: pagination.pageSize, pageNum: pagination.current, // - 使pageNum
page: pagination.current, pageSize: pagination.pageSize, //
...(searchParams || {}), // ...(searchParams || {}), //
} }
console.log('🚀 发送分页请求参数:', params)
console.log('📊 当前分页状态:', {
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total
})
const res = await EquipmentAPI.pageEquipment(params) 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 dataList: any[] = []
let totalCount = 0
// console.log('🔍 开始解析响应数据,响应结构:', res)
if (Array.isArray(res.data)) {
dataList = res.data // - PageResult
} else if (res.data && Array.isArray((res.data as any).records)) { if ((res as any).rows && Array.isArray((res as any).rows)) {
dataList = (res.data as any).records // PageResult rows
} else if (res.data && Array.isArray((res.data as any).list)) { dataList = (res as any).rows
dataList = (res.data as any).list totalCount = (res as any).total || 0
} else if (res.data && Array.isArray((res.data as any).rows)) { console.log('✅ 从 rows 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
dataList = (res.data as any).rows } 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) { if (dataList.length > 0) {
const transformedData = transformBackendData(dataList) const transformedData = transformBackendData(dataList)
tableData.value = transformedData tableData.value = transformedData
console.log('✅ 数据转换完成,设置到表格:', transformedData.length, '条')
} else { } else {
tableData.value = [] 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 { } 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) { } catch (error: any) {
console.error('加载数据失败:', error) console.error('加载数据失败:', error)
message.error(error?.message || '加载数据失败') message.error(error?.message || '加载数据失败')
tableData.value = []
pagination.total = 0
} finally { } finally {
loading.value = false loading.value = false
} }
@ -631,11 +712,42 @@ const handleReset = () => {
// //
const handleTableChange = (pag: any) => { const handleTableChange = (pag: any) => {
pagination.current = pag.current || 1 console.log('表格变化,分页参数:', pag) //
pagination.pageSize = pag.pageSize || 10
//
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) 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 = () => { const handleAdd = () => {
modalMode.value = 'add' modalMode.value = 'add'
@ -920,7 +1032,8 @@ onMounted(() => {
.table-card { .table-card {
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden; overflow: visible; // visible
min-height: 600px; //
.card-title { .card-title {
display: flex; display: flex;
@ -943,6 +1056,11 @@ onMounted(() => {
} }
.arco-table { .arco-table {
//
.arco-table-container {
overflow: visible;
}
.arco-table-th { .arco-table-th {
background: var(--color-fill-2); background: var(--color-fill-2);
font-weight: 600; font-weight: 600;
@ -956,6 +1074,12 @@ onMounted(() => {
.arco-table-tr:hover { .arco-table-tr:hover {
background: var(--color-fill-1); background: var(--color-fill-1);
} }
//
.arco-table-body {
overflow-y: auto;
max-height: 500px; //
}
} }
} }
@ -999,18 +1123,38 @@ onMounted(() => {
} }
} }
// // -
.arco-pagination { .pagination-container {
margin-top: 24px; position: sticky;
justify-content: center; 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 { .arco-pagination {
border-radius: 6px; margin: 0;
margin: 0 4px;
&.arco-pagination-item-active { .arco-pagination-item {
background: var(--color-primary); border-radius: 6px;
border-color: var(--color-primary); 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" :columns="columns"
:data="tableData" :data="tableData"
:loading="loading" :loading="loading"
:pagination="pagination" :pagination="false"
:row-selection="rowSelection" :row-selection="rowSelection"
row-key="equipmentId" row-key="equipmentId"
:scroll="{ x: 'max-content', y: 400 }"
@change="handleTableChange" @change="handleTableChange"
> >
<!-- 设备状态 --> <!-- 设备状态 -->
@ -218,6 +219,23 @@
</a-space> </a-space>
</template> </template>
</a-table> </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> </a-card>
<!-- 新增/编辑弹窗 --> <!-- 新增/编辑弹窗 -->
@ -277,6 +295,7 @@ const pagination = reactive<any>({
showPageSize: true, showPageSize: true,
showJumper: true, showJumper: true,
showTotal: (total: number) => `${total} 条记录`, showTotal: (total: number) => `${total} 条记录`,
pageSizeOptions: [10, 20, 50, 100]
}) })
// //
@ -592,31 +611,52 @@ const loadData = async (searchParams?: EquipmentListReq) => {
loading.value = true loading.value = true
try { try {
//
const params: EquipmentListReq = { const params: EquipmentListReq = {
pageSize: pagination.pageSize, pageNum: pagination.current, //
page: pagination.current, pageSize: pagination.pageSize, //
minPrice: undefined, minPrice: undefined,
maxPrice: undefined, maxPrice: undefined,
...(searchParams || {}), ...(searchParams || {}),
} }
console.log('📊 loadData - 构建的完整请求参数:', params) console.log('📊 loadData - 构建的完整请求参数:', params)
console.log('📊 当前分页状态:', {
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total
})
const res = await equipmentProcurementApi.page(params) const res = await equipmentProcurementApi.page(params)
console.log('API响应:', res) 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 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 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)) { } else if (res.data && Array.isArray((res.data as any).records)) {
dataList = (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)) { } else if (res.data && Array.isArray((res.data as any).list)) {
dataList = (res.data as any).list dataList = (res.data as any).list
} else if (res.data && Array.isArray((res.data as any).rows)) { totalCount = (res.data as any).total || dataList.length || 0
dataList = (res.data as any).rows console.log('✅ 从 list 字段获取数据,总数:', totalCount, '当前页数据:', dataList.length)
} else {
console.warn('⚠️ 未找到有效的数据字段,响应结构:', res)
dataList = []
totalCount = 0
} }
console.log('处理后的数据列表:', dataList) console.log('处理后的数据列表:', dataList)
@ -633,14 +673,25 @@ const loadData = async (searchParams?: EquipmentListReq) => {
tableData.value = [] 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 { } else {
message.error(res.msg || '加载数据失败') console.error('❌ 请求失败,响应:', res)
message.error(res.msg || (res as any).message || '加载数据失败')
tableData.value = []
pagination.total = 0
} }
} catch (error: any) { } catch (error: any) {
console.error('加载数据失败:', error) console.error('加载数据失败:', error)
message.error(error?.message || '加载数据失败') message.error(error?.message || '加载数据失败')
tableData.value = []
pagination.total = 0
} finally { } finally {
loading.value = false loading.value = false
} }
@ -672,6 +723,21 @@ const handleTableChange = (pag: any) => {
loadData(currentSearchParams.value) 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 = () => { const handleAdd = () => {
modalMode.value = 'add' modalMode.value = 'add'
@ -958,6 +1024,42 @@ onMounted(() => {
color: var(--color-text-4); color: var(--color-text-4);
font-style: italic; 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;
}
}
}
} }
// //