效果:
页面描述:
对给定的几张图片,每张能用鼠标在图上画框,标注相关文字,框的颜色和文字内容能自定义改变,能删除任意画过的框。
实现思路:
1、对给定的这几张图片,用分页器绑定展示,能选择图片;
2、图片上绑定事件@mousedown鼠标按下——开始画矩形、@mousemove鼠标移动——绘制中临时画矩形、@mouseup鼠标抬起——结束画矩形重新渲染;
开始画矩形:鼠标按下,记录鼠标按下的位置。遍历标签数组,找到check值为true的标签,用其样式和名字创建新的标签,加入该图片的矩形框们的数组。注意,监听鼠标如果是按下后马上抬起,结束标注。
更新矩形:识别到新的标签存在,鼠标移动时监听移动距离,更新当前矩形宽高,用canvas绘制实时临时矩形。
结束画矩形:刷新该图片的矩形框们的数组,触发重新渲染。
3、在图片上v-for遍历渲染矩形框,盒子绑定动态样式改变宽高;
4、右侧能添加、修改矩形框颜色和文字;
5、列举出每个矩形框名称,能选择进行删除,还能一次清空;
- <template>
- <div class="allbody">
- <div class="body-top">
- <button class="top-item2" @click="clearAnnotations">清空button>
- div>
- <div class="body-btn">
- <div class="btn-content">
- <div class="image-container">
-
- <img :src="state.imageUrls[state.currentPage - 1]" @mousedown="startAnnotation" @mousemove="updateAnnotation" @mouseup="endAnnotation" />
-
- <canvas ref="annotationCanvas">canvas>
- <div v-for="annotation in annotations[state.currentPage - 1]" :key="annotation.id" class="annotation" :style="annotationStyle(annotation)">
- <div class="label">{{ annotation.label }}div>
- div>
- div>
- <Pagination
- v-model:current="state.currentPage"
- v-model:page-size="state.pageSize"
- show-quick-jumper
- :total="state.imageUrls.length"
- :showSizeChanger="false"
- :show-total="total => `共 ${total} 张`" />
- div>
- <div class="sidebar">
- <div class="sidebar-title">标签div>
- <div class="tags">
- <div class="tags-item" v-for="(tags, index2) in state.tagsList" :key="index2" @click="checkTag(index2)">
- <div class="tags-checkbox">
- <div :class="tags.check === true ? 'checkbox-two' : 'notcheckbox-two'">div>
- div>
- <div class="tags-right">
- <input class="tags-color" type="color" v-model="tags.color" />
- <input type="type" class="tags-input" v-model="tags.name" />
- <button class="tags-not" @click="deleteTag(index2)"><DeleteOutlined style="color: #ff0202" />button>
- div>
- div>
- div>
- <div class="sidebar-btn">
- <button class="btn-left" @click="addTags()">添加button>
- div>
- <div class="sidebar-title">数据div>
- <div class="sidebars">
- <div class="sidebar-item" v-for="(annotation, index) in annotations[state.currentPage - 1]" :key="annotation.id">
- <div class="sidebar-item-font">{{ index + 1 }}.{{ annotation.name }}div>
- <button class="sidebar-item-icon" @click="removeAnnotation(annotation.id)"><DeleteOutlined style="color: #ff0202" />button>
- >div>
- div>
- div>
- div>
- template>
- <script lang="ts" setup>
- import { DeleteOutlined } from '@ant-design/icons-vue';
- import { Pagination } from 'ant-design-vue';
-
- interface State {
- tagsList: any;
- canvasX: number;
- canvasY: number;
- currentPage: number;
- pageSize: number;
- imageUrls: string[];
- };
-
- const state = reactive<State>({
- tagsList: [], // 标签列表
- canvasX: 0,
- canvasY: 0,
- currentPage: 1,
- pageSize: 1,
- imageUrls: [apiUrl.value + '/api/File/Image/annexpic/20241203Q9NHJ.jpg', apiUrl.value + '/api/file/Image/document/20241225QBYXZ.jpg'],
- });
-
- interface Annotation {
- id: string;
- name: string;
- x: number;
- y: number;
- width: number;
- height: number;
- color: string;
- label: string;
- border: string;
- };
-
- const annotations = reactive<Array<Annotation[]>>([[]]);
- let currentAnnotation: Annotation | null = null;
-
- //开始标注
- function startAnnotation(event: MouseEvent) {
- // 获取当前选中的标签
- var tagsCon = { id: 1, check: true, color: '#000000', name: '安全帽' };
- // 遍历标签列表,获取当前选中的标签
- for (var i = 0; i < state.tagsList.length; i++) {
- if (state.tagsList[i].check) {
- tagsCon.id = state.tagsList[i].id;
- tagsCon.check = state.tagsList[i].check;
- tagsCon.color = state.tagsList[i].color;
- tagsCon.name = state.tagsList[i].name;
- }
- }
- // 创建新的标注
- currentAnnotation = {
- id: crypto.randomUUID(),
- name: tagsCon.name,
- x: event.offsetX,
- y: event.offsetY,
- width: 0,
- height: 0,
- color: '#000000',
- label: (annotations[state.currentPage - 1].length || 0) + 1 + tagsCon.name,
- border: tagsCon.color,
- };
- annotations[state.currentPage - 1].push(currentAnnotation);
-
- //记录鼠标按下的位置
- state.canvasX = event.offsetX;
- state.canvasY = event.offsetY;
-
- //监听鼠标如果是按下后马上抬起,结束标注
- const mouseupHandler = () => {
- endAnnotation();
- window.removeEventListener('mouseup', mouseupHandler);
- };
- window.addEventListener('mouseup', mouseupHandler);
- }
-
- //更新标注
- function updateAnnotation(event: MouseEvent) {
- if (currentAnnotation) {
- //更新当前标注的宽高,为负数时,鼠标向左或向上移动
- currentAnnotation.width = event.offsetX - currentAnnotation.x;
- currentAnnotation.height = event.offsetY - currentAnnotation.y;
- }
-
- //如果正在绘制中,更新临时矩形的位置
- if (annotationCanvas.value) {
- const canvas = annotationCanvas.value;
- //取得类名为image-container的div的宽高
- const imageContainer = document.querySelector('.image-container');
- canvas.width = imageContainer?.clientWidth || 800;
- canvas.height = imageContainer?.clientHeight || 534;
- const context = canvas.getContext('2d');
- if (context) {
- context.clearRect(0, 0, canvas.width, canvas.height);
- context.strokeStyle = currentAnnotation?.border || '#000000';
- context.lineWidth = 2;
- context.strokeRect(state.canvasX, state.canvasY, currentAnnotation?.width || 0, currentAnnotation?.height || 0);
- }
- }
- }
-
- function endAnnotation() {
- //刷新annotations[state.currentPage - 1],触发重新渲染
- annotations[state.currentPage - 1] = annotations[state.currentPage - 1].slice();
- currentAnnotation = null;
- }
-
- function annotationStyle(annotation: Annotation) {
- //如果宽高为负数,需要调整left和top的位置
- const left = annotation.width < 0 ? annotation.x + annotation.width : annotation.x;
- const top = annotation.height < 0 ? annotation.y + annotation.height : annotation.y;
- return {
- left: `${left}px`,
- top: `${top}px`,
- width: `${Math.abs(annotation.width)}px`,
- height: `${Math.abs(annotation.height)}px`,
- border: `2px solid ${annotation.border}`,
- };
- }
-
- // 选择标签
- function checkTag(index2: number) {
- state.tagsList.forEach((item, index) => {
- if (index === index2) {
- item.check = true;
- } else {
- item.check = false;
- }
- });
- }
-
- // 删除标签
- function deleteTag(index: number) {
- state.tagsList.splice(index, 1);
- }
-
- function addTags() {
- state.tagsList.push({ id: state.tagsList.length + 1, check: false, color: '#000000', name: '' });
- }
-
- // 移除某个标注
- function removeAnnotation(id: string) {
- const index = annotations[state.currentPage - 1].findIndex(a => a.id === id);
- if (index !== -1) {
- annotations[state.currentPage - 1].splice(index, 1);
- }
- }
-
- // 清空所有标注
- function clearAnnotations() {
- annotations[state.currentPage - 1].splice(0, annotations[state.currentPage - 1].length);
- }
-
- onMounted(() => {
- for (let i = 0; i < state.imageUrls.length; i++) {
- annotations.push([]);
- }
- });
-
- script>
- <style>
- .body-top {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- margin-bottom: 10px;
- width: 85%;
- }
- .top-item1 {
- width: 70px;
- height: 28px;
- line-height: 26px;
- text-align: center;
- background-color: #028dff;
- border: 1px solid #028dff;
- border-radius: 5px;
- font-size: 14px;
- color: #fff;
- margin-left: 20px;
- }
- .top-item2 {
- width: 70px;
- height: 28px;
- line-height: 26px;
- text-align: center;
- background-color: rgb(255, 2, 2);
- border: 1px solid rgb(255, 2, 2);
- border-radius: 5px;
- font-size: 14px;
- color: #fff;
- margin-left: 20px;
- }
- .body-btn {
- margin: 0;
- padding: 10px 13px 0 0;
- min-height: 630px;
- display: flex;
- background-color: #f5f5f5;
- }
- .btn-content {
- flex-grow: 1;
- padding: 10px;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .image-container {
- height: 500px;
- margin: 40px;
- }
- .image-container img {
- height: 500px !important;
- }
- .ant-pagination {
- margin-bottom: 18px;
- }
- .number-input {
- width: 70px;
- border: 1px solid #ccc;
- border-radius: 4px;
- text-align: center;
- font-size: 16px;
- background-color: #f9f9f9;
- outline: none;
- color: #66afe9;
- }
- .sidebar {
- display: flex;
- flex-direction: column;
- width: 280px;
- height: 640px;
- background-color: #fff;
- padding: 10px;
- border-radius: 7px;
- }
- .sidebar-title {
- font-size: 16px;
- font-weight: 600;
- margin-bottom: 10px;
- }
- .sidebars {
- overflow: auto;
- }
- .sidebar .tags {
- margin-bottom: 10px;
- }
- .tags-item {
- display: flex;
- flex-direction: row;
- align-items: center;
- }
- .tags-checkbox {
- width: 24px;
- height: 24px;
- border-radius: 50px;
- border: 1px solid #028dff;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- margin-right: 7px;
- }
- .checkbox-two {
- background-color: #028dff;
- width: 14px;
- height: 14px;
- border-radius: 50px;
- }
- .notcheckbox-two {
- width: 14px;
- height: 14px;
- border-radius: 50px;
- border: 1px solid #028dff;
- }
- .tags-right {
- display: flex;
- flex-direction: row;
- align-items: center;
- background-color: #f5f5f5;
- border-radius: 5px;
- padding: 5px;
- width: 90%;
- }
- .tags-color {
- width: 26px;
- height: 26px;
- border-radius: 5px;
- }
- .tags-input {
- border: 1px solid #fff;
- width: 153px;
- margin: 0 10px;
- }
- .tags-not {
- border: 1px solid #f5f5f5;
- font-size: 12px;
- }
- .sidebar-btn {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: right;
- }
- .btn-left {
- width: 60px;
- height: 28px;
- line-height: 26px;
- text-align: center;
- border: 1px solid #028dff;
- border-radius: 5px;
- font-size: 14px;
- color: #028dff;
- }
- .btn-right {
- width: 60px;
- height: 28px;
- line-height: 26px;
- text-align: center;
- background-color: #028dff;
- border: 1px solid #028dff;
- border-radius: 5px;
- font-size: 14px;
- color: #fff;
- margin-left: 10px;
- }
- .sidebar-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-right: 2px;
- }
- .sidebar-item-font {
- margin-right: 10px;
- }
- .sidebar-item-icon {
- font-size: 12px;
- border: 1px solid #fff;
- }
-
- .image-annotator {
- display: flex;
- height: 100%;
- }
-
- .image-container {
- flex: 1;
- position: relative;
- overflow: auto;
- }
-
- .image-container img {
- max-width: 100%;
- height: auto;
- }
-
- .annotation {
- position: absolute;
-
- box-sizing: border-box;
- }
-
- canvas {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- pointer-events: none; /* 防止遮挡鼠标事件 */
- }
- style>
评论记录:
回复评论: