smart-car-web/src/views/device/image-recognition/index.vue

545 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 设备管理-识别图片 -->
<el-card class="image-recognition-container">
<!-- 顶部过滤器 -->
<el-card v-show="showSearch" class="search-card">
<el-form :inline="true" ref="queryFormRef" :model="queryParams" label-width="auto">
<el-form-item>
<el-select clearable filterable v-model="queryParams.carType" placeholder="选择车辆类型"
style="width: 250px">
<el-option v-for="item in dict.type.car_type" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-date-picker v-model="timeRange" type="datetimerange" range-separator=" ~ "
start-placeholder="开始时间" end-placeholder="结束时间" value-format="yyyy-MM-dd HH:mm"
format="yyyy-MM-dd HH:mm" style="width: 400px" @change="handleTimeRangeChange" />
</el-form-item>
<el-form-item>
<el-button class="query-btn" @click="handleQuery">查询</el-button>
<el-button class="reset-btn" @click="handleReset">重置</el-button>
<el-button class="export-btn" @click="handleExport">导出</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据列表 -->
<el-card class="table-card">
<div class="table-header">
<h3 class="table-title">数据列表</h3>
</div>
<!-- 图像网格 -->
<div class="image-grid-container" v-loading="loading">
<div v-for="(item, index) in imageList" :key="index" class="image-card">
<div class="image-wrapper">
<el-image :src="item.imageUrl || test_image" fit="cover" class="detection-image"
:preview-src-list="previewImageList">
<div slot="error" class="image-error">
<i class="el-icon-picture"></i>
<span>图片加载失败</span>
</div>
</el-image>
<!-- 覆盖文本 -->
<div class="image-overlay">
<div class="overlay-text">
{{ formatOverlayText(item) }}
</div>
</div>
</div>
</div>
<div v-if="imageList.length === 0 && !loading" class="empty-state">
<i class="el-icon-picture-outline"></i>
<p>暂无数据</p>
</div>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination :total="total" @pagination="handlePagination" :page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize" />
</div>
</el-card>
</el-card>
</template>
<script>
import { imageRecognitionListAPI, exportImageRecognitionAPI } from '@/api/device/image-recognition'
import { isExternal } from '@/utils/validate'
import test_image from '@/assets/images/test_image.png'
export default {
name: 'ImageRecognition',
dicts: ['car_type'],
data() {
return {
showSearch: true,
loading: false,
imageList: [],
total: 0,
timeRange: null,
test_image,
queryParams: {
pageNum: 1,
pageSize: 10,
carType: '',
startTime: '',
endTime: '',
},
}
},
computed: {
previewImageList() {
return this.imageList.map(item => item.imageUrl).filter(url => url)
},
},
created() {
// 初始化默认时间为当天的 0点0分 到 23点59分
this.initDefaultTimeRange()
this.getImageList()
},
methods: {
/** 初始化默认时间范围 */
initDefaultTimeRange() {
const now = new Date()
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0)
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59)
const formatTime = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
const defaultTimeRange = [formatTime(todayStart), formatTime(todayEnd)]
this.timeRange = defaultTimeRange
this.queryParams.startTime = defaultTimeRange[0]
this.queryParams.endTime = defaultTimeRange[1]
},
/** 格式化日期时间字符串(从接口返回的格式转换) */
formatDateTimeFromString(dateStr) {
if (!dateStr) return ''
// 如果已经是格式化的字符串,直接返回
if (typeof dateStr === 'string' && dateStr.includes('-')) {
// 如果是 yyyy-MM-dd HH:mm:ss 格式,转换为 yyyy-MM-dd HH:mm
return dateStr.substring(0, 16)
}
// 如果是 Date 对象,格式化
const date = new Date(dateStr)
return this.formatDateTime(date)
},
/** 获取车辆类型标签 */
getVehicleTypeLabel(carType) {
if (!carType) return ''
const typeMap = {
'1': '油车',
'2': '电车'
}
return typeMap[carType] || carType
},
/** 获取车牌颜色标签 */
getCarColorLabel(carColor) {
if (!carColor) return ''
const colorMap = {
'1': '蓝色',
'2': '绿色'
}
return colorMap[carColor] || carColor
},
/** 格式化日期时间 */
formatDateTime(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
},
/** 获取图像列表 */
async getImageList() {
try {
this.loading = true
const params = { ...this.queryParams }
const res = await imageRecognitionListAPI(params)
if (res.code === 200) {
// 映射接口返回的数据到前端使用的格式
const list = (res.rows || []).map(item => {
// 处理图片路径,默认使用 test_image
let imageUrl = this.test_image
if (item.filePath) {
if (isExternal(item.filePath)) {
imageUrl = item.filePath
} else {
imageUrl = process.env.VUE_APP_BASE_API + item.filePath
}
}
return {
id: item.identificationDataId,
imageUrl: imageUrl,
location: item.identificationLocation || '',
detectTime: item.identificationTime ? this.formatDateTimeFromString(item.identificationTime) : '',
vehicleType: this.getVehicleTypeLabel(item.carType),
carColor: this.getCarColorLabel(item.carColor),
// 保留原始数据
originalData: item
}
})
this.imageList = list
this.total = res.total || 0
} else {
this.$message.error(res.msg || '获取数据失败')
this.imageList = []
this.total = 0
}
} catch (error) {
console.error('获取图像列表失败:', error)
this.$message.error('获取数据失败,请稍后重试')
this.imageList = []
this.total = 0
} finally {
this.loading = false
}
},
/** 查询操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getImageList()
},
/** 重置操作 */
handleReset() {
this.queryParams.carType = ''
// 重置时恢复默认时间范围(当天的 0点0分 到 23点59分
this.initDefaultTimeRange()
this.queryParams.pageNum = 1
this.getImageList()
},
/** 导出操作 */
async handleExport() {
try {
this.loading = true
const params = { ...this.queryParams }
const res = await exportImageRecognitionAPI(params)
if (res.code === 200) {
// 处理文件下载
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `图像识别数据_${new Date().getTime()}.xlsx`
link.click()
window.URL.revokeObjectURL(url)
this.$message.success('导出成功')
} else {
this.$message.error(res.msg || '导出失败')
}
} catch (error) {
console.error('导出失败:', error)
this.$message.error('导出失败')
} finally {
this.loading = false
}
},
/** 时间范围变化 */
handleTimeRangeChange(value) {
if (value && value.length === 2) {
this.queryParams.startTime = value[0]
this.queryParams.endTime = value[1]
} else {
this.queryParams.startTime = ''
this.queryParams.endTime = ''
}
},
/** 分页变化 */
handlePagination({ page, limit }) {
this.queryParams.pageNum = page
this.queryParams.pageSize = limit
this.getImageList()
},
/** 格式化覆盖文本 */
formatOverlayText(item) {
const parts = []
if (item.location) parts.push(item.location)
if (item.detectTime) parts.push(item.detectTime)
if (item.vehicleType) parts.push(item.vehicleType)
return parts.join(' ') || '未知位置'
},
},
}
</script>
<style scoped lang="scss">
.image-recognition-container {
height: calc(100vh - 84px);
overflow: hidden;
background: linear-gradient(180deg, #f1f6ff 20%, #e5efff 100%);
}
.search-card {
margin-bottom: 10px;
::v-deep .el-form-item {
margin-bottom: 10px;
}
.query-btn {
width: 98px;
height: 36px;
background: #1f72ea;
box-shadow: 0px 4px 8px 0px rgba(51, 135, 255, 0.5);
border-radius: 4px;
color: #fff;
border: none;
font-size: 14px;
transition: all 0.3s;
&:hover {
background: #4a8bff;
box-shadow: 0px 6px 12px 0px rgba(51, 135, 255, 0.6);
}
}
.reset-btn {
width: 98px;
height: 36px;
background: #ffffff;
box-shadow: 0px 4px 8px 0px rgba(76, 76, 76, 0.2);
border-radius: 4px;
color: #333;
border: none;
font-size: 14px;
transition: all 0.3s;
&:hover {
background: #f5f5f5;
box-shadow: 0px 6px 12px 0px rgba(76, 76, 76, 0.3);
color: #40a9ff;
}
}
.export-btn {
width: 98px;
height: 36px;
background: #1f72ea;
box-shadow: 0px 4px 8px 0px rgba(51, 135, 255, 0.5);
border-radius: 4px;
color: #fff;
border: none;
font-size: 14px;
transition: all 0.3s;
&:hover {
background: #4a8bff;
box-shadow: 0px 6px 12px 0px rgba(51, 135, 255, 0.6);
}
}
}
.table-card {
height: calc(100vh - 230px);
display: flex;
flex-direction: column;
overflow: hidden;
::v-deep .el-card__body {
padding: 20px;
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
overflow: hidden;
}
.table-header {
flex-shrink: 0;
padding-bottom: 16px;
.table-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
}
.image-grid-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
padding: 0 0 16px 0;
// 防止单行数据时占满整个高度
align-content: start;
grid-auto-rows: min-content;
// 使用 flex 布局让图片区域占据剩余空间超出时显示滚动条
flex: 1;
overflow-y: auto;
min-height: 0;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.3);
}
}
.image-card {
background: #fff;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
// 防止在 grid 中被拉伸
align-self: start;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.image-wrapper {
position: relative;
width: 100%;
padding-top: 75%; // 4:3 比例
overflow: hidden;
// 防止拉伸
height: 0;
.detection-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 8px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), transparent);
pointer-events: none;
.overlay-text {
color: #fff;
font-size: 12px;
line-height: 1.4;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
}
.detection-boxes {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
.detection-box {
position: absolute;
border: 2px solid #ff0000;
box-sizing: border-box;
.box-corner {
position: absolute;
width: 8px;
height: 8px;
background: #00ff00;
border: 1px solid #fff;
&.corner-tl {
top: -4px;
left: -4px;
}
&.corner-tr {
top: -4px;
right: -4px;
}
&.corner-bl {
bottom: -4px;
left: -4px;
}
&.corner-br {
bottom: -4px;
right: -4px;
}
}
}
}
.image-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #999;
i {
font-size: 32px;
display: block;
margin-bottom: 8px;
}
}
}
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px 0;
color: #999;
i {
font-size: 64px;
display: block;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
}
.pagination-wrapper {
flex-shrink: 0;
margin-top: 16px;
padding-top: 16px;
border-top: none;
}
}
</style>