zhgdyunapp/pages/projectEnd/safeSame/ImageAnnotation.vue

645 lines
17 KiB
Vue
Raw Normal View History

2025-11-26 17:27:46 +08:00
<template>
<view class="simple-image-annotator">
<view class="fixedheader">
<headers :showBack="true" :themeType="true">
<view class="headerName">
{{form.drawingName}}
</view>
</headers>
</view>
<view class="controls" :style="{paddingTop: mobileTopHeight + 44 + 'px'}">
<!-- 只有在编辑模式下才显示操作按钮 -->
<!-- <view v-if="viewMode === 'edit'">
<button class="control-btn" :class="{ active: mode === 'add' }" @click="setAddMode">
新增点位
</button>
<button class="control-btn delete-btn" :class="{ active: mode === 'delete' }" @click="setDeleteMode">
删除点位
</button>
</view> -->
</view>
<view class="image-container" ref="imageContainer">
<image class="my-image" ref="image" :src="imagePath" alt="标注图片" mode="aspectFit" @load="handleImageLoad"
@tap="handleImageClick" />
<view class="annotation-layer" ref="annotationLayer">
<view v-for="point in points" :key="point.id" class="annotation-point" :style="{
left: calculatePointPosition(point, 'x') + 'px',
top: calculatePointPosition(point, 'y') + 'px'
}" @tap.stop="handlePointClick(point.id)" @longpress.stop="handleRightClick(point.id)">
<view class="bubble" @click="deletePoint(point.id)" v-if="point.isShow">
删除
</view>
</view>
</view>
</view>
<view class="text-tip" v-if="viewMode === 'edit'">
如需修改检查位置需先点击此位置删除然后重新定位位置
</view>
<view class="confrim-btn" v-if="viewMode === 'edit'">
<view @click="onCancelClick">取消</view>
<view @click="onSubmitType">保存</view>
</view>
</view>
</template>
<script>
export default {
name: 'SimpleImageAnnotator',
data() {
return {
points: [],
imageNaturalWidth: 0,
imageNaturalHeight: 0,
containerWidth: 0,
containerHeight: 0,
scale: 1,
mode: 'add', // 默认模式为新增点位
form: {},
imagePath: "",
viewMode: "edit",
mobileTopHeight: 0,
}
},
mounted() {
this.initContainerSize()
window.addEventListener('resize', this.handleResize)
// 初始化时根据viewMode设置mode
if (this.viewMode === 'detail') {
this.mode = 'detail'
}
},
onLoad(opts) {
console.log(opts);
this.points = JSON.parse(opts.drawingPointData).map(item => {
// 确保点位对象包含必要的属性,为旧数据添加默认值
return {
...item,
isShow: false,
platform: item.platform || 'unknown',
originalImageWidth: item.originalImageWidth || 0,
originalImageHeight: item.originalImageHeight || 0,
// 保存原始坐标用于后续计算
originalX: item.x,
originalY: item.y
};
});
if (opts.viewMode) {
this.viewMode = opts.viewMode;
}
this.getQualityRegionConstructionDrawById(opts.regionDrawId);
},
mounted() {
const that = this;
uni.getSystemInfo({
success(res) {
that.mobileTopHeight = res.statusBarHeight ? res.statusBarHeight : 0;
}
})
this.initContainerSize()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
},
methods: {
onCancelClick() {
uni.navigateBack({
delta: 1
})
},
onSubmitType() {
if(this.points.length == 0) {
uni.showToast({
title: '请至少选择一个点位!',
icon: "none",
});
return
}
uni.$emit('updateData', JSON.stringify(this.points))
uni.navigateBack({
delta: 1
})
},
getQualityRegionConstructionDrawById(regionDrawId) {
this.sendRequest({
url: 'xmgl/qualityRegionConstructionDraw/queryById',
method: 'get',
data: {
projectSn: this.projectSn,
id: regionDrawId,
},
success: res => {
if (res.code == 200) {
const data = res.result;
this.imagePath = this.url_config + 'image/' + data.fileUrl;
this.form.drawingName = data.drawingName;
this.resetViewer()
}
}
})
},
// 初始化容器大小
initContainerSize() {
if (this.$refs.imageContainer) {
// 在uni-app中使用uni.createSelectorQuery获取元素尺寸
uni.createSelectorQuery().in(this)
.select('.image-container')
.boundingClientRect(res => {
if (res) {
this.containerWidth = res.width
this.containerHeight = res.height
// 如果图片已经加载,重新计算缩放
if (this.imageNaturalWidth && this.imageNaturalHeight) {
this.calculateScale()
}
}
}).exec()
}
},
// 处理窗口大小变化
handleResize() {
this.initContainerSize()
},
// 处理图片加载
handleImageLoad(event) {
// 在uni-app中使用宽高比计算
const {
width,
height
} = event.detail;
// 不使用原始宽度,而是获取图片的实际渲染尺寸
uni.createSelectorQuery().in(this)
.select('.image-container .my-image')
.boundingClientRect(imgRes => {
console.log(6666, imgRes)
if (imgRes) {
// 使用图片的实际渲染宽度和高度
this.imageNaturalWidth = imgRes.width
this.imageNaturalHeight = imgRes.height
console.log(`使用实际渲染尺寸: ${this.imageNaturalWidth}x${this.imageNaturalHeight}`)
this.calculateScale()
} else {
// 降级方案,如果获取不到实际渲染尺寸,则使用原始尺寸
this.imageNaturalWidth = width
this.imageNaturalHeight = height
this.calculateScale()
}
})
.exec()
},
// 计算缩放比例
calculateScale() {
if (!this.imageNaturalWidth || !this.imageNaturalHeight || !this.$refs.image || !this.$refs
.annotationLayer) {
return
}
// 在uni-app中使用动态计算
uni.createSelectorQuery().in(this)
.select('.image-container')
.boundingClientRect(containerRes => {
if (!containerRes) return
uni.createSelectorQuery().in(this)
.select('.image-container image')
.boundingClientRect(imgRes => {
if (!imgRes) return
// 计算实际显示的缩放比例
this.scale = imgRes.width / this.imageNaturalWidth
console.log(`计算缩放比例: 图片尺寸(${this.imageNaturalWidth}x${this.imageNaturalHeight}), 显示尺寸(${imgRes.width}x${imgRes.height}), 缩放比例: ${this.scale}`)
// 直接将标注层与图片完全重叠
const layer = this.$refs.annotationLayer
layer.style.width = imgRes.width + 'px'
layer.style.height = imgRes.height + 'px'
layer.style.left = (containerRes.width - imgRes.width) / 2 + 'px'
layer.style.top = (containerRes.height - imgRes.height) / 2 + 'px'
// 更新所有点位位置
this.$nextTick(() => {
this.updateAllPointsPosition();
});
})
.exec()
}).exec()
},
// 处理图片点击事件
handleImageClick(event) {
// 只有在编辑模式且为新增模式时才允许添加点位
if (this.viewMode === 'edit' && this.mode === 'add') {
// 在uni-app中获取点击位置
const {
x,
y
} = event.detail
// 获取标注层位置信息
uni.createSelectorQuery().in(this)
.select('.annotation-layer')
.boundingClientRect(layerRes => {
if (!layerRes) return
// 计算相对位置
const relX = (x - layerRes.left) / this.scale
const relY = (y - layerRes.top) / this.scale
this.addPoint(relX, relY)
})
.exec()
}
// 删除模式或详情模式下,点击图片空白区域不执行操作
},
// 生成唯一ID
generateUniqueId() {
return Date.now() + '_' + Math.floor(Math.random() * 10000)
},
// 添加标注点
addPoint(x, y) {
console.log('Adding point at:', x, y)
console.log(this.imageNaturalWidth, this.imageNaturalHeight)
// 确保坐标在有效范围内
const validX = Math.max(0, Math.min(x, this.imageNaturalWidth))
const validY = Math.max(0, Math.min(y, this.imageNaturalHeight))
console.log('Valid X, Y:', validX, validY)
const newPoint = {
id: this.generateUniqueId(),
x: validX,
y: validY,
isShow: false,
originalImageWidth: this.imageNaturalWidth, // 记录点位创建时的图片宽度
originalImageHeight: this.imageNaturalHeight // 记录点位创建时的图片高度
}
this.points.push(newPoint)
this.$emit('pointsChanged', this.points)
this.$emit('pointAdded', newPoint)
},
// 处理标注点点击
handlePointClick(pointId) {
// 只有在编辑模式且为删除模式时才允许删除点位
if (this.viewMode === 'edit' && this.mode === 'delete') {
this.deletePoint(pointId)
} else if (this.viewMode === 'edit') {
// 新增模式或详情模式下,触发点位选中事件
const point = this.points.find(p => p.id === pointId)
if (point) {
point.isShow = !point.isShow
}
}
},
// 设置为新增模式
setAddMode() {
if (this.viewMode === 'edit') {
this.mode = 'add'
// uni-app中不直接操作cursor样式
this.$emit('modeChanged', 'add')
}
},
// 设置为删除模式
setDeleteMode() {
if (this.viewMode === 'edit') {
this.mode = 'delete'
this.$emit('modeChanged', 'delete')
}
},
// 设置为详情模式
setDetailMode() {
this.mode = 'detail'
if (this.$refs.image) {
this.$refs.image.style.cursor = 'default'
}
this.$emit('modeChanged', 'detail')
},
// 处理右键点击
handleRightClick(pointId) {
// 只有在编辑模式下才允许右键删除点位
if (this.viewMode === 'edit') {
this.deletePoint(pointId)
}
},
// 删除标注点
deletePoint(pointId) {
const pointIndex = this.points.findIndex(p => p.id === pointId)
if (pointIndex !== -1) {
const deletedPoint = this.points[pointIndex]
this.points.splice(pointIndex, 1)
this.$emit('pointsChanged', this.points)
this.$emit('pointDeleted', deletedPoint)
}
},
// 清空所有标注点
clearAllPoints() {
this.points = []
this.$emit('allPointsCleared')
},
// 重置查看器
resetViewer() {
// this.points = []
this.imageNaturalWidth = 0
this.imageNaturalHeight = 0
this.scale = 1
// 根据视图模式重置mode
if (this.viewMode === 'detail') {
this.mode = 'detail'
} else {
this.mode = 'add' // 重置为新增模式
}
if (this.$refs.image) {
this.$refs.image.style.cursor = this.mode === 'add' ? 'crosshair' : 'default'
}
// 确保在下一次图片加载后更新点位位置
this.$nextTick(() => {
if (this.imageNaturalWidth && this.imageNaturalHeight) {
this.calculateScale();
}
});
},
resetPosit() {
this.points = JSON.parse(JSON.stringify(this.drawingPointData));
this.mode = 'add' // 重置为新增模式
if (this.$refs.image) {
this.$refs.image.style.cursor = 'crosshair'
}
},
// 获取所有点位
getAllPoints() {
// 返回点位数组的深拷贝,避免外部直接修改原始数据
return JSON.parse(JSON.stringify(this.points))
},
// 获取当前模式
getCurrentMode() {
return this.mode
},
// 计算点位在当前图片上的实际位置
calculatePointPosition(point, axis) {
// 如果点位没有记录原始图片尺寸,使用默认缩放
if (!point.originalImageWidth || !point.originalImageHeight || point.originalImageWidth === 0 || point.originalImageHeight === 0) {
return axis === 'x' ? point.x * this.scale : point.y * this.scale;
}
// 计算原始图片到当前图片的缩放比例
const widthRatio = this.imageNaturalWidth / point.originalImageWidth;
const heightRatio = this.imageNaturalHeight / point.originalImageHeight;
// 使用平均比例确保点位位置正确
const avgRatio = (widthRatio + heightRatio) / 2;
// 获取原始坐标,如果存在则使用原始坐标进行计算
const originalCoord = point[axis === 'x' ? 'originalX' : 'originalY'] || point[axis];
console.log(888888888, originalCoord * avgRatio * this.scale)
// 返回计算后的位置
return originalCoord * avgRatio * this.scale;
},
// 更新所有点位位置
updateAllPointsPosition() {
// 根据当前平台和图片尺寸重新计算所有点位位置
if (!this.scale || this.points.length === 0) return;
console.log('更新点位位置,缩放比例:', this.scale);
// 为每个点位计算基于当前图片尺寸的实际位置
this.points.forEach(point => {
// 如果点位记录了原始图片尺寸,则进行尺寸适配计算
if (point.originalImageWidth && point.originalImageHeight && point.originalImageWidth !== 0 && point.originalImageHeight !== 0) {
// 计算原始图片到当前图片的缩放比例
const widthRatio = this.imageNaturalWidth / point.originalImageWidth;
const heightRatio = this.imageNaturalHeight / point.originalImageHeight;
// 存储原始坐标用于调试
if (!point.originalX) {
point.originalX = point.x;
point.originalY = point.y;
}
// 使用平均比例进行缩放,确保点位比例正确
const avgRatio = (widthRatio + heightRatio) / 2;
console.log(`点位 ${point.id} 缩放: 原图(${point.originalImageWidth}x${point.originalImageHeight}) -> 当前图(${this.imageNaturalWidth}x${this.imageNaturalHeight}), 比例: ${avgRatio}`);
}
});
this.$forceUpdate();
},
}
}
</script>
<style scoped lang="scss">
.text-tip {
font-size: 24rpx;
color: #B3B3B3;
display: flex;
justify-content: center;
padding-bottom: 26rpx;
}
.confrim-btn {
padding: 18rpx 26rpx;
background-color: #FFFFFF;
box-shadow: 0rpx -8rpx 8rpx 0rpx rgba(0, 0, 0, 0.05);
display: flex;
>view {
width: 50%;
height: 76rpx;
font-weight: 500;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
>view:first-child {
background-color: rgba(81, 129, 246, 0.1);
border-radius: 6rpx 0rpx 0rpx 6rpx;
color: #5181F6;
}
>view:last-child {
background-color: #5181F6;
border-radius: 0rpx 6rpx 6rpx 0rpx;
color: #FFFFFF;
}
}
.simple-image-annotator {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.image-container {
position: relative;
// flex: 1;
overflow: hidden;
// background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
margin: auto 0;
}
.image-container image {
max-width: 100%;
max-height: 100%;
transition: transform 0.2s;
width: 100%;
}
/* 详情模式下图片指针样式 */
.detail-mode img {
cursor: default !important;
}
.annotation-layer {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
transform-origin: top left;
}
.annotation-point {
position: absolute;
width: 20rpx;
height: 30rpx;
/* background-color: rgba(255, 69, 0, 0.8); */
/* border-radius: 50%; */
background-image: url("@/static/draw-point.png");
background-repeat: no-repeat;
background-size: 100% 100%;
transform: translate(-50%, -50%);
pointer-events: all;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); */
}
.bubble {
width: calc(120rpx);
height: 70rpx;
padding: 10rpx 32rpx;
background-image: url("@/static/bubble.png");
background-repeat: no-repeat;
background-size: 100% 100%;
position: absolute;
bottom: 100%;
font-weight: 500;
font-size: 28rpx;
color: #000000;
}
.annotation-point:hover {
/* background-color: rgba(255, 0, 0, 0.9); */
transform: translate(-50%, -50%) scale(1.1);
}
.annotation-point .delete-btn {
position: absolute;
top: -16rpx;
right: -16rpx;
width: 32rpx;
height: 32rpx;
background-color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ff4500;
font-size: 24rpx;
cursor: pointer;
border: 2rpx solid #ff4500;
}
.annotation-point .delete-btn:hover {
background-color: #ff4500;
color: white;
}
.point-index {
font-size: 20rpx;
}
.controls {
padding: 0 20rpx 20rpx;
background-color: white;
/* border-top: 1px solid #e0e0e0; */
display: flex;
gap: 20rpx;
align-items: center;
flex-wrap: wrap;
}
.control-btn {
padding: 12rpx 32rpx;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 8rpx;
cursor: pointer;
transition: background-color 0.2s;
}
.control-btn:hover {
background-color: #45a049;
}
.control-btn.active {
background-color: #2196F3;
}
.clear-btn {
padding: 12rpx 32rpx;
background-color: #ff4500;
color: white;
border: none;
border-radius: 8rpx;
cursor: pointer;
transition: background-color 0.2s;
margin-left: auto;
}
.clear-btn:hover {
background-color: #ff6347;
}
.point-count {
color: #666;
font-size: 28rpx;
margin-left: 20rpx;
}
.fixedheader {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 2;
.headerName {
z-index: 1;
}
}
</style>