【问题标题】:vue konva editable text on videovue konva 可编辑的视频文本
【发布时间】:2020-01-21 16:29:47
【问题描述】:

大家好,所以我构建了这个 vuejs 画布应用程序,它运行得非常好,但是我想为它添加更多功能。例如,我想缩放视频顶部的文本,并在文本周围放置某种框。可以在画布中直接编辑。我见过人们用包括 Konva 在内的多个库来做到这一点,但是我看到有一个 Knova-vue 并且文档很糟糕有没有人用这个库制作了一个简单的应用程序,你可以在其中写视频?

https://jsfiddle.net/bshyvpo0/

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
   <canvas id="canvas" width='500' height='500' ref='canvas' @mousedown='handleMouseDown' @mousemove='handleMouseMove' @mouseup='handleMouseUp' @mouseout='handleMouseOut'>></canvas>
   <input v-model="text" placeholder='type your text'>
   <button @click='addText'>
   add text
   </button>
   <div v-for="(text, index) in texts" @dblclick='selectText(index)'>
   {{index}}:{{text.text}} <div @click='removeText(index)'>X</div>
   </div>
   <img src ='https://shop-resources.prod.cms.tractorsupply.com/resource/image/18248/portrait_ratio3x4/595/793/4c37b7f6d6f9d8a5b223334f1390191b/JJ/ten-reasons-not-to-buy-an-easter-bunny-main.jpg' @click="changeBackground('http://upload.wikimedia.org/wikipedia/commons/7/79/Big_Buck_Bunny_small.ogv')">
   <img src ='https://ce.prismview.com/api/files/templates/43s327k3oqfsf7/poster/poster.jpg' @click="changeBackground('http://ce.prismview.com/api/files/templates/43s327k3oqfsf7/main/360/43s327k3oqfsf7_360.mp4')">

    <video id="video" ref='video' :src="source" controls="false" autoplay loop></video>

    </div>
<script>
        new Vue({
              el: '#app',
              data: {
                source: "http://upload.wikimedia.org/wikipedia/commons/7/79/Big_Buck_Bunny_small.ogv",
                canvas: null,
                canvasOffset: null,
                ctx: null,
                offsetX: null, 
                offsetY:null,
                startX: null,
                startY:null,
                selectedText:null,
                video: null,
                text:'',
                texts: [],
                timer: null,
                index: null
              },
                methods: {
                addText(){
                if(this.text.length){
                let textObj = {
                text: this.text,
                x: 20,
                y: this.texts.length * 20 + 20
                };
                this.texts.push(textObj);
                this.text = '';
                }
                },
                removeText(i){
                this.texts.splice(i, 1);
                },
                textHittest(x, y, textIndex) {
    var text = this.texts[textIndex];
    return (x >= text.x && x <= text.x + text.width && y >= text.y - text.height && y <= text.y);
},
 handleMouseDown(e) {
    e.preventDefault();
    this.startX = parseInt(e.clientX - this.offsetX);
    this.startY = parseInt(e.clientY - this.offsetY);

    // Put your mousedown stuff here
    let vm = this;
/*     for (var i = 0; i < this.texts.length; i++) {
        if (vm.textHittest(vm.startX, vm.startY, i) || 1===1) {
        console.log('selected', vm.selected);
            vm.selectedText = i; 
        }

    }*/
    if(this.index!=null){
    this.selectedText = this.index;
    }
},
selectText(i){
this.index = i;
console.log(this.selectedText);
},
 handleMouseUp(e) {
    e.preventDefault();
    this.selectedText = -1;
},

// also done dragging
handleMouseOut(e) {
    e.preventDefault();
    this.selectedText = -1;
},
 handleMouseMove(e) {
    if (this.selectedText < 0) {
        return;
    }
    e.preventDefault();
    let mouseX = parseInt(e.clientX - this.offsetX);
    let mouseY = parseInt(e.clientY - this.offsetY);

    // Put your mousemove stuff here
    var dx = mouseX - this.startX;
    var dy = mouseY - this.startY;
    this.startX = mouseX;
    this.startY = mouseY;

    var text = this.texts[this.selectedText] || 1;
    text.x += dx;
    text.y += dy;
    this.drawFrame();
},



               drawFrame (){
                        console.log("drawing");
                            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
                          this.ctx.drawImage(this.video, 0, 0,);
                          this.ctx.fillStyle = 'red';
                          this.ctx.font = "30px Arial";
                          for(let i =0; i<this.texts.length; i++){
                                                    this.ctx.fillText(this.texts[i].text, this.texts[i].x, this.texts[i].y);
                          }
                       this.timer = setTimeout(() => {
                       this.drawFrame()
                       }, 1000/30);

                      },

              initCanvas(){
                this.canvas = this.$refs['canvas'];
                this.video = this.$refs['video'];
                this.ctx = this.canvas.getContext('2d');
                this.canvasOffset = {left:this.canvas.offsetLeft, top: this.canvas.offsetTop};
                             this.offsetX = this.canvasOffset.left;
                            this.offsetY = this.canvasOffset.top;
               const vm = this;
               this.video.addEventListener('play', function(){
               vm.video.style.display = 'none';

               vm.drawFrame();
               })
              },
              changeBackground(source){
              if(source!=this.video.src){
              clearTimeout(this.timer);
              this.source = source;
              this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
              this.ctx.restore();
              }
              }
        },
        mounted: function(){
            this.initCanvas();
        }
      });

</script>

【问题讨论】:

    标签: vue.js konva


    【解决方案1】:

    进行了重大更改,但最终解决了。

    <template>
      <div>
        <button @click="render">Render</button>
        <h2>Backgrounds</h2>
        <template v-for="background in backgrounds">
          <img
            :src="background.poster"
            class="backgrounds"
            @click="changeBackground(background.video)"
          />
        </template>
        <h2>Images</h2>
        <template v-for="image in images">
          <img :src="image.source" @click="addImage(image.source)" class="images" />
        </template>
        <br />
        <button @click="addText">Add Text</button>
        <button v-if="selectedNode" @click="removeNode">
          Remove selected {{ selectedNode.type }}
        </button>
        <label>Font:</label>
        <select v-model="selectedFont">
          <option value="Arial">Arial</option>
          <option value="Courier New">Courier New</option>
          <option value="Times New Roman">Times New Roman</option>
          <option value="Desoto">Desoto</option>
          <option value="Kalam">Kalam</option>
        </select>
        <label>Font Size</label>
        <input type="number" v-model="selectedFontSize" />
        <label>Font Style:</label>
        <select v-model="selectedFontStyle">
          <option value="normal">Normal</option>
          <option value="bold">Bold</option>
          <option value="italic">Italic</option>
        </select>
        <label>Color:</label>
        <input type="color" v-model="selectedColor" />
        <button
          v-if="selectedNode && selectedNode.type === 'text'"
          @click="updateText"
        >
          Update Text
        </button>
        <br />
        <video
          id="preview"
          v-show="preview"
          :src="preview"
          :width="width"
          :height="height"
          preload="auto"
          controls
        />
        <a v-if="file" :href="file" download="dopeness.mp4">download</a>
        <div id="container"></div>
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          source: null,
          stage: null,
          layer: null,
          video: null,
          captures: [],
          backgrounds: [
            {
              poster: "/api/files/stock/3oref310k1uud86w/poster/poster.jpg",
              video:
                "/api/files/stock/3oref310k1uud86w/main/1080/3oref310k1uud86w_1080.mp4"
            },
            {
              poster: "/api/files/stock/3yj2e30tk5x6x0ww/poster/poster.jpg",
              video:
                "/api/files/stock/3yj2e30tk5x6x0ww/main/1080/3yj2e30tk5x6x0ww_1080.mp4"
            },
            {
              poster: "/api/files/stock/2ez931ik1mggd6j/poster/poster.jpg",
              video:
                "/api/files/stock/2ez931ik1mggd6j/main/1080/2ez931ik1mggd6j_1080.mp4"
            },
            {
              poster: "/api/files/stock/yxrt4ej4jvimyk15/poster/poster.jpg",
              video:
                "/api/files/stock/yxrt4ej4jvimyk15/main/1080/yxrt4ej4jvimyk15_1080.mp4"
            },
            {
              poster:
                "https://images.costco-static.com/ImageDelivery/imageService?profileId=12026540&itemId=100424771-847&recipeName=680",
              video: "/api/files/jedi/surfing.mp4"
            },
            {
              poster:
                "https://thedefensepost.com/wp-content/uploads/2018/04/us-soldiers-afghanistan-4308413-1170x610.jpg",
              video: "/api/files/jedi/soldiers.mp4"
            }
          ],
          images: [
            { source: "/api/files/jedi/solo.jpg" },
            { source: "api/files/jedi/yoda.jpg" },
            { source: "api/files/jedi/yodaChristmas.jpg" },
            { source: "api/files/jedi/darthMaul.jpg" },
            { source: "api/files/jedi/darthMaul1.jpg" },
            { source: "api/files/jedi/trump.jpg" },
            { source: "api/files/jedi/hat.png" },
            { source: "api/files/jedi/trump.png" },
            { source: "api/files/jedi/bernie.png" },
            { source: "api/files/jedi/skywalker.png" },
            { source: "api/files/jedi/vader.gif" },
            { source: "api/files/jedi/vader2.gif" },
            { source: "api/files/jedi/yoda.gif" },
            { source: "api/files/jedi/kylo.gif" }
          ],
          backgroundVideo: null,
          imageGroups: [],
          anim: null,
          selectedNode: null,
          selectedFont: "Arial",
          selectedColor: "black",
          selectedFontSize: 20,
          selectedFontStyle: "normal",
          width: 1920,
          height: 1080,
          texts: [],
          preview: null,
          file: null,
          canvas: null
        };
      },
      mounted: function() {
        this.initCanvas();
      },
      methods: {
        changeBackground(source) {
          this.source = source;
          this.video.src = this.source;
          this.anim.stop();
          this.anim.start();
          this.video.play();
        },
        removeNode() {
          if (this.selectedNode && this.selectedNode.type === "text") {
            this.selectedNode.transformer.destroy(
              this.selectedNode.text.transformer
            );
            this.selectedNode.text.destroy(this.selectedNode.text);
            this.texts.splice(this.selectedNode.text.index - 1, 1);
            this.selectedNode = null;
            this.layer.draw();
          } else if (this.selectedNode && this.selectedNode.type == "image") {
            this.selectedNode.group.destroy(this.selectedNode);
            this.imageGroups.splice(this.selectedNode.group.index - 1, 1);
            this.selectedNode = null;
            this.layer.draw();
          }
        },
        async addImage(src) {
          let imageObj = null;
          const type = src.slice(src.lastIndexOf("."));
          const vm = this;
          function process(img) {
            return new Promise((resolve, reject) => {
              img.onload = () => resolve({ width: img.width, height: img.height });
            });
          }
          imageObj = new Image();
          imageObj.src = src;
          imageObj.width = 200;
          imageObj.height = 200;
          await process(imageObj);
    
          if (type === ".gif") {
            const canvas = document.createElement("canvas");
            canvas.setAttribute("id", "gif");
            async function onDrawFrame(ctx, frame) {
              ctx.drawImage(frame.buffer, frame.x, frame.y);
              // redraw the layer
              vm.layer.draw();
            }
            gifler(src).frames(canvas, onDrawFrame);
    
            canvas.onload = async () => {
              canvas.parentNode.removeChild(canvas);
            };
            imageObj = canvas;
            const gif = new Image();
            gif.src = src;
            const gifImage = await process(gif);
            imageObj.width = gifImage.width;
            imageObj.height = gifImage.height;
          }
          const image = new Konva.Image({
            x: 50,
            y: 50,
            image: imageObj,
            width: imageObj.width,
            height: imageObj.height,
            position: (0, 0),
            strokeWidth: 10,
            stroke: "blue",
            strokeEnabled: false
          });
    
          this.frame = null;
          const group = new Konva.Group({
            draggable: true
          });
          // add the shape to the layer
          addAnchor(group, 0, 0, "topLeft");
          addAnchor(group, imageObj.width, 0, "topRight");
          addAnchor(group, imageObj.width, imageObj.height, "bottomRight");
          addAnchor(group, 0, imageObj.height, "bottomLeft");
          imageObj = null;
          image.on("click", function() {
            vm.hideAllHelpers();
            vm.selectedNode = { type: "image", group };
            group.find("Circle").show();
    
            vm.layer.draw();
          });
          image.on("mouseover", function(evt) {
            if (vm.selectedNode && vm.selectedNode.type === "image") {
              const index = image.getParent().index;
              const groupId = vm.selectedNode.group.index;
              if (index != groupId) {
                evt.target.strokeEnabled(true);
                vm.layer.draw();
              }
            } else {
              evt.target.strokeEnabled(true);
              vm.layer.draw();
            }
          });
          image.on("mouseout", function(evt) {
            evt.target.strokeEnabled(false);
            vm.layer.draw();
          });
          vm.hideAllHelpers();
          group.find("Circle").show();
          group.add(image);
          vm.layer.add(group);
          vm.imageGroups.push(group);
    
          vm.selectedNode = { type: "image", group };
          vm.layer.draw();
    
          function update(activeAnchor) {
            const group = activeAnchor.getParent();
    
            let topLeft = group.get(".topLeft")[0];
            let topRight = group.get(".topRight")[0];
            let bottomRight = group.get(".bottomRight")[0];
            let bottomLeft = group.get(".bottomLeft")[0];
            let image = group.get("Image")[0];
    
            let anchorX = activeAnchor.getX();
            let anchorY = activeAnchor.getY();
    
            // update anchor positions
            switch (activeAnchor.getName()) {
              case "topLeft":
                topRight.y(anchorY);
                bottomLeft.x(anchorX);
                break;
              case "topRight":
                topLeft.y(anchorY);
                bottomRight.x(anchorX);
                break;
              case "bottomRight":
                bottomLeft.y(anchorY);
                topRight.x(anchorX);
                break;
              case "bottomLeft":
                bottomRight.y(anchorY);
                topLeft.x(anchorX);
                break;
            }
    
            image.position(topLeft.position());
    
            let width = topRight.getX() - topLeft.getX();
            let height = bottomLeft.getY() - topLeft.getY();
            if (width && height) {
              image.width(width);
              image.height(height);
            }
          }
          function addAnchor(group, x, y, name) {
            let stage = vm.stage;
            let layer = vm.layer;
    
            let anchor = new Konva.Circle({
              x: x,
              y: y,
              stroke: "#666",
              fill: "#ddd",
              strokeWidth: 2,
              radius: 8,
              name: name,
              draggable: true,
              dragOnTop: false
            });
    
            anchor.on("dragmove", function() {
              update(this);
              layer.draw();
            });
            anchor.on("mousedown touchstart", function() {
              group.draggable(false);
              this.moveToTop();
            });
            anchor.on("dragend", function() {
              group.draggable(true);
              layer.draw();
            });
            // add hover styling
            anchor.on("mouseover", function() {
              let layer = this.getLayer();
              document.body.style.cursor = "pointer";
              this.strokeWidth(4);
              layer.draw();
            });
            anchor.on("mouseout", function() {
              let layer = this.getLayer();
              document.body.style.cursor = "default";
              this.strokeWidth(2);
              layer.draw();
            });
    
            group.add(anchor);
          }
        },
        hideAllHelpers() {
          for (let i = 0; i < this.texts.length; i++) {
            this.texts[i].transformer.hide();
          }
          for (let b = 0; b < this.imageGroups.length; b++) {
            this.imageGroups[b].find("Circle").hide();
          }
        },
        async startRecording(duration) {
          const chunks = []; // here we will store our recorded media chunks (Blobs)
          const stream = this.canvas.captureStream(30); // grab our canvas MediaStream
          const rec = new MediaRecorder(stream, {
            videoBitsPerSecond: 20000 * 1000
          });
          // every time the recorder has new data, we will store it in our array
          rec.ondataavailable = e => chunks.push(e.data);
          // // only when the recorder stops, we construct a complete Blob from all the chunks
          rec.onstop = async e => {
            this.anim.stop();
    
            const blob = new Blob(chunks, {
              type: "video/webm"
            });
    
            this.preview = await URL.createObjectURL(blob);
            const video = window.document.getElementById("preview");
            const previewVideo = new Konva.Image({
              image: video,
              draggable: false,
              width: this.width,
              height: this.height
            });
            this.layer.add(previewVideo);
    
            console.log("video", video);
            video.addEventListener("ended", () => {
              console.log("preview ended");
              if (!this.file) {
                const vid = new Whammy.fromImageArray(this.captures, 30);
                this.file = URL.createObjectURL(vid);
              }
              previewVideo.destroy();
              this.anim.stop();
              this.anim.start();
              this.video.play();
            });
            let seekResolve;
    
            video.addEventListener("seeked", async () => {
              if (seekResolve) seekResolve();
            });
            video.addEventListener("loadeddata", async () => {
              let interval = 1 / 30;
              let currentTime = 0;
              while (currentTime <= duration && !this.file) {
                video.currentTime = currentTime;
                await new Promise(r => (seekResolve = r));
    
                this.layer.draw();
                let base64ImageData = this.canvas.toDataURL("image/webp");
                this.captures.push(base64ImageData);
                currentTime += interval;
                video.currentTime = currentTime;
              }
    
              this.layer.draw();
            });
          };
          rec.start();
          setTimeout(() => rec.stop(), duration);
        },
        async render() {
          this.captures = [];
          this.preview = null;
          this.file = null;
          console.log(this.captures.length);
          this.hideAllHelpers();
          this.selectedNode = null;
          this.video.currentTime = 0;
          this.video.loop = false;
          const duration = this.video.duration * 1000;
          this.startRecording(duration);
          this.layer.draw();
        },
        updateText() {
          if (this.selectedNode && this.selectedNode.type === "text") {
            const text = this.selectedNode.text;
            const transformer = this.selectedNode.transformer;
            text.fontSize(this.selectedFontSize);
            text.fontFamily(this.selectedFont);
            text.fontStyle(this.selectedFontStyle);
            text.fill(this.selectedColor);
            this.layer.draw();
          }
        },
        addText() {
          const vm = this;
          const text = new Konva.Text({
            text: "new text " + (vm.texts.length + 1),
            x: 50,
            y: 80,
            fontSize: this.selectedFontSize,
            fontFamily: this.selectedFont,
            fontStyle: this.selectedFontStyle,
            fill: this.selectedColor,
            align: "center",
            width: this.width * 0.5,
            draggable: true
          });
          const transformer = new Konva.Transformer({
            node: text,
            keepRatio: true,
            enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"]
          });
          text.on("click", async () => {
            for (let i = 0; i < this.texts.length; i++) {
              let item = this.texts[i];
              if (item.index === text.index) {
                let transformer = item.transformer;
                this.selectedNode = { type: "text", text, transformer };
                this.selectedFontSize = text.fontSize();
                this.selectedFont = text.fontFamily();
                this.selectedFontStyle = text.fontStyle();
                this.selectedColor = text.fill();
                vm.hideAllHelpers();
                transformer.show();
                transformer.moveToTop();
                text.moveToTop();
                vm.layer.draw();
                break;
              }
            }
          });
          text.on("mouseover", () => {
            transformer.show();
            this.layer.draw();
          });
          text.on("mouseout", () => {
            if (
              (this.selectedNode &&
                this.selectedNode.text &&
                this.selectedNode.text.index != text.index) ||
              (this.selectedNode && this.selectedNode.type === "image") ||
              !this.selectedNode
            ) {
              transformer.hide();
              this.layer.draw();
            }
          });
          text.on("dblclick", () => {
            text.hide();
            transformer.hide();
            vm.layer.draw();
            let textPosition = text.absolutePosition();
    
            let stageBox = vm.stage.container().getBoundingClientRect();
    
            let areaPosition = {
              x: stageBox.left + textPosition.x,
              y: stageBox.top + textPosition.y
            };
    
            let textarea = document.createElement("textarea");
            window.document.body.appendChild(textarea);
            textarea.value = text.text();
            textarea.style.position = "absolute";
            textarea.style.top = areaPosition.y + "px";
            textarea.style.left = areaPosition.x + "px";
            textarea.style.width = text.width() - text.padding() * 2 + "px";
            textarea.style.height = text.height() - text.padding() * 2 + 5 + "px";
            textarea.style.fontSize = text.fontSize() + "px";
            textarea.style.border = "none";
            textarea.style.padding = "0px";
            textarea.style.margin = "0px";
            textarea.style.overflow = "hidden";
            textarea.style.background = "none";
            textarea.style.outline = "none";
            textarea.style.resize = "none";
            textarea.style.lineHeight = text.lineHeight();
            textarea.style.fontFamily = text.fontFamily();
            textarea.style.transformOrigin = "left top";
            textarea.style.textAlign = text.align();
            textarea.style.color = text.fill();
            let rotation = text.rotation();
            let transform = "";
            if (rotation) {
              transform += "rotateZ(" + rotation + "deg)";
            }
            let px = 0;
            let isFirefox =
              navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
            if (isFirefox) {
              px += 2 + Math.round(text.fontSize() / 20);
            }
            transform += "translateY(-" + px + "px)";
            textarea.style.transform = transform;
            textarea.style.height = "auto";
            textarea.focus();
    
            // start
            function removeTextarea() {
              textarea.parentNode.removeChild(textarea);
              window.removeEventListener("click", handleOutsideClick);
              text.show();
              transformer.show();
              transformer.forceUpdate();
              vm.layer.draw();
            }
    
            function setTextareaWidth(newWidth) {
              if (!newWidth) {
                // set width for placeholder
                newWidth = text.placeholder.length * text.fontSize();
              }
              // some extra fixes on different browsers
              let isSafari = /^((?!chrome|android).)*safari/i.test(
                navigator.userAgent
              );
              let isFirefox =
                navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
              if (isSafari || isFirefox) {
                newWidth = Math.ceil(newWidth);
              }
    
              let isEdge =
                document.documentMode || /Edge/.test(navigator.userAgent);
              if (isEdge) {
                newWidth += 1;
              }
              textarea.style.width = newWidth + "px";
            }
    
            textarea.addEventListener("keydown", function(e) {
              // hide on enter
              // but don't hide on shift + enter
              if (e.keyCode === 13 && !e.shiftKey) {
                text.text(textarea.value);
                removeTextarea();
              }
              // on esc do not set value back to node
              if (e.keyCode === 27) {
                removeTextarea();
              }
            });
    
            textarea.addEventListener("keydown", function(e) {
              let scale = text.getAbsoluteScale().x;
              setTextareaWidth(text.width() * scale);
              textarea.style.height = "auto";
              textarea.style.height =
                textarea.scrollHeight + text.fontSize() + "px";
            });
    
            function handleOutsideClick(e) {
              if (e.target !== textarea) {
                text.text(textarea.value);
                removeTextarea();
              }
            }
            setTimeout(() => {
              window.addEventListener("click", handleOutsideClick);
            });
            // end
          });
          text.transformer = transformer;
          this.texts.push(text);
          this.layer.add(text);
          this.layer.add(transformer);
          this.hideAllHelpers();
          this.selectedNode = { type: "text", text, transformer };
          transformer.show();
          this.layer.draw();
        },
        initCanvas() {
          const vm = this;
          this.stage = new Konva.Stage({
            container: "container",
            width: vm.width,
            height: vm.height
          });
          this.layer = new Konva.Layer();
    
          this.stage.add(this.layer);
    
          let video = document.createElement("video");
          video.setAttribute("id", "video");
          video.setAttribute("ref", "video");
          if (this.source) {
            video.src = this.source;
          }
          video.preload = "auto";
          video.loop = "loop";
          video.style.display = "none";
          this.video = video;
          this.backgroundVideo = new Konva.Image({
            image: vm.video,
            draggable: false
          });
          this.video.addEventListener("loadedmetadata", function(e) {
            vm.backgroundVideo.width(vm.width);
            vm.backgroundVideo.height(vm.height);
          });
          this.video.addEventListener("ended", () => {
            console.log("the video ended");
            this.anim.stop();
            this.anim.start();
            this.video.loop = "loop";
            this.video.play();
          });
    
          this.anim = new Konva.Animation(function() {
            console.log("animation called");
            // do nothing, animation just need to update the layer
          }, vm.layer);
    
          this.layer.add(this.backgroundVideo);
          this.layer.draw();
          const canvas = document.getElementsByTagName("canvas")[0];
          canvas.style.border = "3px solid red";
          this.canvas = canvas;
        }
      }
    };
    </script>
    <style scoped>
    body {
      margin: 0;
      padding: 0;
      background-color: #f0f0f0;
    }
    .backgrounds,
    .images {
      width: 100px;
      height: 100px;
      padding-left: 2px;
      padding-right: 2px;
    }
    </style>
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-07-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多