前端需求心路(一股子翻译腔)

其他

遇到弄不懂的需求,程序员靠搜索引擎检索和实践,现在多了人工智能,确实也是非常大的助力

前期,ya哥问我:前端有方法可以 把环检打过标签的图片 合成新的图片上传吗?

我的第一个解决方案

    // JavaScript 代码
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const image1 = new Image();
    image1.src = '	http://localhost:9999/note/images/hutao/hutao7.webp';
    const image2 = new Image();
    image2.src = '	http://localhost:9999/note/images/hutao/hutao1.webp';
    image1.onload = () => {
        // ctx.drawImage(image1, 0, 0); // 绘制图片1
        // ctx.drawImage(image2, 0, 0); // 绘制图片2

        // 将合并后的图片保存到本地
        const dataURL = canvas.toDataURL();
        console.log(dataURL);

        let mergeImg = document.querySelector('#merge');
        mergeImg.src = dataURL;
        console.log(mergeImg);
        };

我的第二个解决方案

    const fileInput = document.getElementById('fileInput');
    const canvas = document.getElementById('canvas');
    const mergeButton = document.getElementById('mergeButton');

    // 当点击合并按钮时,读取文件并将图片绘制到 canvas 上
    mergeButton.addEventListener('click', () => {
        // 获取文件列表
        const files = fileInput.files;
        if (!files.length) {
            return alert('请选择至少一张图片');
        }

        // 设置 canvas 尺寸
        const width = files.length * 200; // 每张图片宽度为 200px
        const height = 200; // 图片高度为 200px
        canvas.width = width;
        canvas.height = height;

        // 获取 canvas 的 2D 绘图上下文
        const ctx = canvas.getContext('2d');

        // 循环遍历文件列表,并将图片绘制到 canvas 上
        for (let i = 0; i < files.length; i++) {
            const file = files[i];
            const reader = new FileReader();
            // 当读取完成时,将图片绘制到 canvas 上
            reader.addEventListener('load', () => {
                const img = new Image();
                img.addEventListener('load', () => {
                    ctx.drawImage(img, i * 200, 0, 200, 200);
                });
                img.src = reader.result;
            });
            reader.readAsDataURL(file);
        }
    });

该方案的缺点是:图片的合并是在客户端进行的,如果要合并的图片数量很多或者图片较大,可能会对性能造成影响。 由于图片的合并是在客户端进行的,因此合并的图片不能直接保存到服务器,需要使用其他方法将合并后的图片传回服务器。

使用 XMLHttpRequest 对象将图片发送到服务器的示例代码如下:
// 定义服务器端地址
const url = '/upload';

// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest();

// 监听响应状态变化
xhr.onreadystatechange = function() {
  if (xhr.readyState === XMLHttpRequest.DONE) {
    if (xhr.status === 200) {
      console.log('图片传输成功');
    } else {
      console.error('图片传输失败');
    }
  }
};

// 发送请求
xhr.open('POST', url);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({
  image: canvas.toDataURL()
}));
使用 Fetch API 将图片发送到服务器的示例代码如下
// 定义服务器端地址
var url = '/upload';
// 发送请求
fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    image: canvas.toDataURL()
  })
}).then(response => {
  if (response.ok) {
    console.log('图片传输成功');
  } else {
    console.error('图片传输失败');
  }
});

但其实vue2项目代码里面用的axios很方便(小声bb)

后面看意思是把html的标签做成图片

(这样弄是最简单的,但是我没考虑这样弄之前怎么把标签贴到图片,这里可看后面)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/html2canvas@1.0.0-rc.5/dist/html2canvas.js"></script>
    <style>
        img{
            width: 200px;
        }

        .hh{
            width: 200px;
            height: 200px;
            background-image: url('https://www.baidu.com/img/flexible/logo/pc/result@2.png');
        }
    </style>
</head>

<body>
    <!-- 引入 html2canvas 库 -->

    <!-- 定义要转换的 HTML 元素 -->
    <div id="myElement">
        <p>这是一个段落</p>
        <div class="hh"></div>
        <img src="" alt="">
    </div>

    <!-- 定义一个按钮,点击时将 HTML 元素转换为图像 -->
    <button onclick="convertToImage()">转换为图像</button>

    <!-- 定义转换函数 -->
    <script>
        function convertToImage() {
            // 使用 html2canvas 将 HTML 元素转换为 canvas
            html2canvas(document.getElementById('myElement')).then(canvas => {
                // 使用 canvas 的 toDataURL() 方法将 canvas 转换为图像
                const img = new Image();
                img.src = canvas.toDataURL();
                document.body.appendChild(img);
            });
        }
    </script>

</body>

</html>

这里可以尝试把script的type改成模块(module),因为用到html2canvasopen in new window需要es6 Promise支持或者esllint包装的npm包(我的说法)

然后ya哥说下个月的需求是改成图片和标签一起合成,坐标是随机的,还要做到合成一张图片上传给后端

我写的时候,就是现在,突然意识到还有一种方法,就是前端实现标签定位和显示,把标签和坐标和图片传给后端,后端处理合成也不失为一种方案。

我的第三个解决方案

然后当时我的反应是,去保存html标签为图片,通过chatGPT检索找到了html2canvas这个库,于是在本vue3的博客试了试。

控制查看打印
如果改成图片,变成下载
vue3实现贴固定文字,并点击按钮下载图片
vue3实现贴标签,并点击按钮下载图片

看起来实现了

但是本博客用的是vue3,vue2我需要看看支不支持,我问了chatGPT,确实也是可以的

查看代码
<template>
  <div>
    <button @click="capture">Capture</button>
  </div>
</template>

<script>
import html2canvas from 'html2canvas'

export default {
  methods: {
    async capture () {
      const canvas = await html2canvas(document.body)
      document.body.appendChild(canvas)
    }
  }
}
</script>

当前的实现还有一些瑕疵

  • 点(操作)的实际位置在标签的左上角
  • 图片太大的话性能很差
  • 需要浏览器的支持,cordova做出来的还真没试过,待会试试

改进点击位置为底部中间

查看代码

<template>
    <div class="container">
        <div class="capture" ref="capture" v-if="!props.type">
            <h4 style="color: #000; ">Hello world!</h4>
        </div>

        <div class="capture" ref="capture" v-if="props.type === 'tag'">
            <div ref="imageRef" class="image linear" @click="addRadioTag"></div>
            <div ref="tagsRef" class="tags" v-html="tagsHtml">
            </div>
        </div>

        <el-radio-group v-model="radio1" class="radio" v-if="props.type === 'tag'">
            <h5>损伤标签:</h5>
            <el-radio size="large" v-for="item in selectSunShang" :label="item">{{ item ? item : '取消选择' }}</el-radio>
        </el-radio-group>

        <el-radio-group v-model="radio2" class="radio" v-if="props.type === 'tag'">
            <h5>部位标签:</h5>
            <el-radio size="large" v-for="item in selectBuWei" :label="item">{{ item }}</el-radio>
        </el-radio-group>
        <el-button class="el-button" @click="method">转换为图像</el-button>
    </div>

</template>

<script setup name="EnhancedShoot" >

import html2canvas from 'html2canvas';
import { ref } from 'vue'
import { downloadBase64File } from './index'
const capture = ref(null)
const filename = 'html标签.png';
const translate = () => {
    html2canvas(capture.value).then(function (canvas) {
        console.log(canvas.toDataURL());
    });
}
const download = () => {
    html2canvas(capture.value).then(function (canvas) {
        downloadBase64File(canvas.toDataURL(), filename);
    }).catch(e => {
        console.log(e);

    });
}
const props = defineProps({
    type: String
})
let method = translate;
const methods = {
    tag: download
}
if (props.type) {
    method = methods[props.type]
}
const selectSunShang = ref(['凹陷', '掉漆', '划痕', '破损', '锈蚀', '']);
const selectBuWei = ref(['前机盖', '左前门', '右前门', '前保杠', '']);
const radio1 = ref(selectSunShang.value[0])
const radio2 = ref(selectBuWei.value[0])

const tagStyle = `
color: white;
    position: absolute;
    padding: 0.1em;
    background: red;
    text-align:center;
`
const spanStyle = `
position: absolute;
    bottom: -1em;
    left: calc(50% - 0.5em);
    width: 1em;
    height: 1em;
    border-radius: 50%;
    background: red;
`
const tags = ref([]);
const tagsRef = ref('');
const imageRef = ref('');
const tagsHtml = ref('')
const addRadioTag = (event) => {
    const imageRect = imageRef.value.getBoundingClientRect();
    const x = event.clientX - imageRect.left;
    const y = event.clientY - imageRect.top;
    let maxRadio = radio1.value.length > radio2.value.length ? radio1.value.length : radio2.value.length;
    const divLength = maxRadio + 0.2;
    //padding+text
    const xx = `calc(${x}px - ${divLength / 2}em)`
    const yy = `calc(${y}px - ${3.2}em)`
    tags.value.push({ x, y });
    tagsHtml.value = `${tagsHtml.value}<div style='${tagStyle}left:${xx};top:${yy}'>
        ${radio1.value}
        <br/>
        ${radio2.value}
        <span style='${spanStyle}'></span></div>`
}
</script>
<style lang="scss" scoped>
.capture {
    padding: 0 10px 10px 0;
    background-color: #f5da55;
    position: relative;
    box-sizing: border-box;
}

.image {
    background-image: url('/note/images/logo.jpg');
    width: 100%;
    height: 50vh;
    background-size: contain;

    &.linear {
        background-image: linear-gradient();
        background-image: linear-gradient(rgba(0, 0, 255, 0.5), rgba(255, 255, 0, 0.5)),
            url("/note/images/logo.jpg");
    }
}

.tags {
    &>div {
        position: relative;
    }
}

.el-button {
    margin-top: 2em;
}

.radio {
    width: 100%;
    margin-top: 2em;
}
</style>

最终改进版

  • 改进文字非选择为空且带来的点的高度位响应等问题
  • border-radius
  • 增加padding距离计算

结束了,在cordova里也能使用成功了🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉