zhgdyunapp/pages/projectEnd/safeSame/ImageAnnotation.vue
2025-11-26 17:27:46 +08:00

645 lines
17 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>
<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>