【问题标题】:Undo / redo not working properly and painting after zoom not working properly too撤消/重做不能正常工作,缩放后绘画也不能正常工作
【发布时间】:2016-08-11 07:55:46
【问题描述】:

我正在尝试实现具有撤消和重做功能的油漆桶工具。问题是撤消和重做第一次正常工作,但是当我多次撤消重做时,代码失败。谁能帮我解决这个问题?缩放也可以工作,但缩放后的绘画不能正常工作。这是我的完整代码。您只需复制粘贴即可。

<!DOCTYPE html>
<html>
    <head>
        <title>Painitng</title>
        <style>
            body {
                width: 100%;
                height: auto;
                text-align: center;
            }
            .colorpick {
                widh: 100%;
                height: atuo;
            }
            .pick {
                display: inline-block;
                width: 30px;
                height: 30px;
                margin: 5px;
                cursor: pointer;
            }
            canvas {
                border: 2px solid silver;
            }
        </style>
    </head>
    <body>
        <button id="zoomin">Zoom In</button>
        <button id="zoomout">Zoom Out</button>
        <button onclick="undo()">Undo</button>
        <button onclick="redo()">Redo</button>
        <div id="canvasDiv"></div>
        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script> 
        <script type="text/javascript">
            var colorYellow = {
                r: 255,
                g: 207,
                b: 51
            };
            var context;
            var canvasWidth = 500;
            var canvasHeight = 500;
            var myColor = colorYellow;
            var curColor = myColor;
            var outlineImage = new Image();
            var backgroundImage = new Image();
            var drawingAreaX = 0;
            var drawingAreaY = 0;
            var drawingAreaWidth = 500;
            var drawingAreaHeight = 500;
            var colorLayerData;
            var outlineLayerData;
            var totalLoadResources = 2;
            var curLoadResNum = 0;
            var undoarr = new Array();
            var redoarr = new Array();
            var uc = 0;
            var rc = 0;

            // Clears the canvas.
            function clearCanvas() {
                context.clearRect(0, 0, context.canvas.width, context.canvas.height);
            }

            function undo() {
                if (undoarr.length <= 0)
                    return;

                if (uc==0) {
                    redoarr.push(undoarr.pop());
                    uc = 1;
                }
                var a = undoarr.pop();
                colorLayerData = a;
                redoarr.push(a);
                clearCanvas();
                context.putImageData(a, 0, 0);
                context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
                context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
                console.log(undoarr);
            }

            function redo() {
                if (redoarr.length <= 0)
                    return;
                if (rc==0) {
                    undoarr.push(redoarr.pop());
                    rc = 1;
                }
                var a = redoarr.pop();
                colorLayerData = a;
                undoarr.push(a);
                clearCanvas();
                context.putImageData(a, 0, 0);
                context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
                context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
                console.log(redoarr);
            }
            // Draw the elements on the canvas
            function redraw() {
                uc = 0;
                rc = 0;
                var locX,
                        locY;

                // Make sure required resources are loaded before redrawing
                if (curLoadResNum < totalLoadResources) {
                    return; // To check if images are loaded successfully or not.
                }

                clearCanvas();
                // Draw the current state of the color layer to the canvas
                context.putImageData(colorLayerData, 0, 0);

                undoarr.push(context.getImageData(0, 0, canvasWidth, canvasHeight));
                console.log(undoarr);
                redoarr = new Array();
                // Draw the background
                context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);

                // Draw the outline image on top of everything. We could move this to a separate 
                //   canvas so we did not have to redraw this everyime.
                context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);


            }
            ;

            function matchOutlineColor(r, g, b, a) {

                return (r + g + b < 100 && a === 255);
            }
            ;

            function matchStartColor(pixelPos, startR, startG, startB) {

                var r = outlineLayerData.data[pixelPos],
                        g = outlineLayerData.data[pixelPos + 1],
                        b = outlineLayerData.data[pixelPos + 2],
                        a = outlineLayerData.data[pixelPos + 3];

                // If current pixel of the outline image is black
                if (matchOutlineColor(r, g, b, a)) {
                    return false;
                }

                r = colorLayerData.data[pixelPos];
                g = colorLayerData.data[pixelPos + 1];
                b = colorLayerData.data[pixelPos + 2];

                // If the current pixel matches the clicked color
                if (r === startR && g === startG && b === startB) {
                    return true;
                }

                // If current pixel matches the new color
                if (r === curColor.r && g === curColor.g && b === curColor.b) {
                    return false;
                }

                return true;
            }
            ;

            function colorPixel(pixelPos, r, g, b, a) {
                colorLayerData.data[pixelPos] = r;
                colorLayerData.data[pixelPos + 1] = g;
                colorLayerData.data[pixelPos + 2] = b;
                colorLayerData.data[pixelPos + 3] = a !== undefined ? a : 255;
            }
            ;

            function floodFill(startX, startY, startR, startG, startB) {
                var newPos,
                        x,
                        y,
                        pixelPos,
                        reachLeft,
                        reachRight,
                        drawingBoundLeft = drawingAreaX,
                        drawingBoundTop = drawingAreaY,
                        drawingBoundRight = drawingAreaX + drawingAreaWidth - 1,
                        drawingBoundBottom = drawingAreaY + drawingAreaHeight - 1,
                        pixelStack = [[startX, startY]];

                while (pixelStack.length) {

                    newPos = pixelStack.pop();
                    x = newPos[0];
                    y = newPos[1];

                    // Get current pixel position
                    pixelPos = (y * canvasWidth + x) * 4;

                    // Go up as long as the color matches and are inside the canvas
                    while (y >= drawingBoundTop && matchStartColor(pixelPos, startR, startG, startB)) {
                        y -= 1;
                        pixelPos -= canvasWidth * 4;
                    }

                    pixelPos += canvasWidth * 4;
                    y += 1;
                    reachLeft = false;
                    reachRight = false;

                    // Go down as long as the color matches and in inside the canvas
                    while (y <= drawingBoundBottom && matchStartColor(pixelPos, startR, startG, startB)) {
                        y += 1;

                        colorPixel(pixelPos, curColor.r, curColor.g, curColor.b);

                        if (x > drawingBoundLeft) {
                            if (matchStartColor(pixelPos - 4, startR, startG, startB)) {
                                if (!reachLeft) {
                                    // Add pixel to stack
                                    pixelStack.push([x - 1, y]);
                                    reachLeft = true;
                                }

                            } else if (reachLeft) {
                                reachLeft = false;
                            }
                        }

                        if (x < drawingBoundRight) {
                            if (matchStartColor(pixelPos + 4, startR, startG, startB)) {
                                if (!reachRight) {
                                    // Add pixel to stack
                                    pixelStack.push([x + 1, y]);
                                    reachRight = true;
                                }
                            } else if (reachRight) {
                                reachRight = false;
                            }
                        }

                        pixelPos += canvasWidth * 4;
                    }
                }
            }
            ;

            // Start painting with paint bucket tool starting from pixel specified by startX and startY
            function paintAt(startX, startY) {

                var pixelPos = (startY * canvasWidth + startX) * 4,
                        r = colorLayerData.data[pixelPos],
                        g = colorLayerData.data[pixelPos + 1],
                        b = colorLayerData.data[pixelPos + 2],
                        a = colorLayerData.data[pixelPos + 3];

                if (r === curColor.r && g === curColor.g && b === curColor.b) {
                    // Return because trying to fill with the same color
                    return;
                }

                if (matchOutlineColor(r, g, b, a)) {
                    // Return because clicked outline
                    return;
                }

                floodFill(startX, startY, r, g, b);

                redraw();
            }
            ;

            // Add mouse event listeners to the canvas
            function createMouseEvents() {

                $('#canvas').mousedown(function (e) {
                    // Mouse down location
                    var mouseX = e.pageX - this.offsetLeft,
                            mouseY = e.pageY - this.offsetTop;

                    if ((mouseY > drawingAreaY && mouseY < drawingAreaY + drawingAreaHeight) && (mouseX <= drawingAreaX + drawingAreaWidth)) {
                        paintAt(mouseX, mouseY);
                    }
                });
            }
            ;

            resourceLoaded = function () {

                curLoadResNum += 1;
                //if (curLoadResNum === totalLoadResources) {
                createMouseEvents();
                redraw();
                //}
            };

            function start() {

                var canvas = document.createElement('canvas');
                canvas.setAttribute('width', canvasWidth);
                canvas.setAttribute('height', canvasHeight);
                canvas.setAttribute('id', 'canvas');
                document.getElementById('canvasDiv').appendChild(canvas);

                if (typeof G_vmlCanvasManager !== "undefined") {
                    canvas = G_vmlCanvasManager.initElement(canvas);
                }
                context = canvas.getContext("2d");
                backgroundImage.onload = resourceLoaded();
                backgroundImage.src = "images/t1.png";

                outlineImage.onload = function () {
                    context.drawImage(outlineImage, drawingAreaX, drawingAreaY, drawingAreaWidth, drawingAreaHeight);

                    try {
                        outlineLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
                    } catch (ex) {
                        window.alert("Application cannot be run locally. Please run on a server.");
                        return;
                    }
                    clearCanvas();
                    colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
                    resourceLoaded();
                };
                outlineImage.src = "images/d.png";
            }
            ;

            getColor = function () {

            };

        </script> 
        <script type="text/javascript"> $(document).ready(function () {
                start();
            });</script> 
        <script language="javascript">
            $('#zoomin').click(function () {
                if ($("#canvas").width()==500){
                $("#canvas").width(750);
                $("#canvas").height(750);
                var ctx = canvas.getContext("2d");
                ctx.drawImage(backgroundImage, 0, 0, 749, 749);
                ctx.drawImage(outlineImage, 0, 0, 749, 749);
                redraw();
                 } else if ($("#canvas").width()==750){

                $("#canvas").width(1000);
                $("#canvas").height(1000);
                var ctx = canvas.getContext("2d");
                ctx.drawImage(backgroundImage, 0, 0, 999, 999);
                ctx.drawImage(outlineImage, 0, 0, 999, 999);
                redraw();
                 }
            });
            $('#zoomout').click(function () {
                if ($("#canvas").width() == 1000) {

                $("#canvas").width(750);
                $("#canvas").height(750);
                var ctx = canvas.getContext("2d");
                ctx.drawImage(backgroundImage, 0, 0, 749, 749);
                ctx.drawImage(outlineImage, 0, 0, 749, 749);
                redraw();
                } else if ($("#canvas").width() == 750) {

                $("#canvas").width(500);
                $("#canvas").height(500);
                var ctx = canvas.getContext("2d");
                ctx.drawImage(backgroundImage, 0, 0, 499, 499);
                ctx.drawImage(outlineImage, 0, 0, 499, 499);
                redraw();
                }
            });
        </script>
        <div class="colorpick">
            <div class="pick" style="background-color:rgb(150, 0, 0);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(0, 0, 152);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(0, 151, 0);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(255, 0, 5);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(255, 255, 0);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(0, 255, 255);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(255, 0, 255);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(255, 150, 0);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(255, 0, 150);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(0, 255, 150);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(150, 0, 255);" onclick="hello(this.style.backgroundColor);"></div>
            <div class="pick" style="background-color:rgb(0, 150, 255);" onclick="hello(this.style.backgroundColor);"></div>
        </div>
        <script>
            function hello(e) {
                var rgb = e.replace(/^(rgb|rgba)\(/, '').replace(/\)$/, '').replace(/\s/g, '').split(',');
                myColor.r = parseInt(rgb[0]);
                myColor.g = parseInt(rgb[1]);
                myColor.b = parseInt(rgb[2]);
                curColor = myColor;
                console.log(curColor);
            }
        </script>
    </body>
</html>

【问题讨论】:

    标签: javascript jquery canvas html5-canvas undo-redo


    【解决方案1】:

    这个函数matchOutlineColor 接受代表RGBA 颜色的4 个数字。

    红、绿、蓝、Alpha(颜色的透明度)

    RGBA 颜色范围为 0-255,因此从 0(无颜色)到 255(全彩),白色为 rgba(255,255,255,255),黑色为 rgba(0,0 ,0,255) 和透明是 rgba(0,0,0,0)

    此代码不检查颜色是否为黑色,只是检查红色 + 绿色 + 黄色加在一起的颜色至少小于 100(总共 750)。我怀疑该函数会检查颜色是否为深色。

    例如,这将全部通过:

    <div style="background-color:rgba(99,0,0,255)">Dark RED</div>
    <div style="background-color:rgba(0,99,0,255)">Dark GREEN</div>
    <div style="background-color:rgba(0,0,99,255)">Dark BLUE</div>

    如果要检查边框是否为黑色,可以将功能更改为

    function matchOutlineColorBlack(r, g, b, a) {
        //Ensures red + green + blue is none
        return (r + g + b == 0 && a === 255);
    };
    
    function matchOutlineColorWhite(r, g, b, a) {
        //Checks that color is white (255+255+255=750)
        return (r + g + b == 750 && a === 255);
    };
    

    【讨论】:

    • 浅灰色像素如何处理?
    【解决方案2】:

    画布尺寸和状态历史

    画布尺寸

    如果您曾经浏览过 DOM,您会注意到许多元素既有高度和宽度作为属性,又有高度和宽度作为样式属性。

    对于画布,它们有两种不同的含义。所以让我们创建一个画布。

    var canvas = document.createElement("canvas");
    

    现在可以设置画布元素的宽度和高度。这定义了画布图像中的像素数(分辨率)

    canvas.width = 500;
    canvas.height = 500;
    

    默认情况下,当图像(画布只是图像)显示在 DOM 中时,它会以一对一的像素大小显示。这意味着对于图像中的每个像素,页面上都有一个像素。

    您可以通过设置画布样式的宽度和高度来更改此设置

    canvas.style.width = "1000px"; // Note you must add the unit type "px" in this case
    canvas.style.width = "1000px";
    

    这不会改变画布分辨率,只会改变显示大小。现在对于画布中的每个像素,它在页面上占用 4 个像素。

    当您使用鼠标在画布上绘图时,这会成为一个问题,因为鼠标坐标的屏幕像素不再匹配画布分辨率。

    解决这个问题。作为 OP 代码的一个例子。您需要重新调整鼠标坐标以匹配画布分辨率。这已添加到 OP mousedown 事件侦听器中。它首先获取显示宽度/高度,然后获取分辨率宽度和高度。它通过除以显示宽度/高度来标准化鼠标坐标。这会将鼠标坐标带到 0

    // assuming that the mouseX and mouseY are the mouse coords.
    if(this.style.width){   // make sure there is a width in the style 
                            // (assumes if width is there then height will be too
        var w = Number(this.style.width.replace("px",""));  // warning this will not work if size is not in pixels
        var h = Number(this.style.height.replace("px","")); // convert the height to a number
        var pixelW = this.width;  // get  the canvas resolution
        var pixelH = this.height;
        mouseX = Math.floor((mouseX / w) * pixelW); // convert the mouse coords to pixel coords
        mouseY = Math.floor((mouseY / h) * pixelH);
    }
    

    这将解决您的缩放问题。但是看看你的代码,它是一团糟,你不应该每次都搜索节点树,重新获取上下文。我很惊讶它可以工作,但这可能是 Jquery(我不知道,因为我从不使用它)或者可能是你在其他地方渲染。

    国家历史

    计算机程序的当前状态是定义当前状态的所有条件和数据。当您保存某些内容时,您正在保存状态,而当您加载时,您将恢复状态。

    历史记录只是一种保存和加载状态的方式,而不会在文件系统中造成混乱。它有一些约定将统计信息存储为堆栈。先进后出,它有一个重做堆栈,允许您重做先前的撤消但保持正确的状态,并且由于状态依赖于先前的状态,因此重做只能从关联状态重做。因此,如果您撤消然后绘制某些内容,则会使任何现有的重做状态无效,并且应该转储它们。

    此外,保存状态,无论是在磁盘上,还是撤消堆栈都必须与当前状态分离。如果您对当前状态进行更改,您不希望这些更改影响保存的状态。

    我认为这是您出错的地方,因为您在撤消或重做时使用colorLayerData 填充(绘制)您使用保留在撤消/重做缓冲区中的引用数据,因此当您画了你实际上正在更改仍在撤消缓冲区中的数据。

    历史管理器

    这是一个通用的状态管理器,可以满足任何撤消/重做需求,您所要做的就是确保将当前状态收集到一个对象中。

    为了帮助我写了一个简单的历史管理器。它有两个缓冲区作为堆栈,一个用于撤消,一个用于重做。它还保存当前状态,这是它所知道的最新状态。

    当你推送到历史管理器时,它将获取它所知道的当前状态并将其推送到撤消堆栈,保存当前状态,并使任何重做数据无效(使重做数组长度为 0)

    当你撤消时,它会将当前状态推入重做堆栈,从撤消堆栈中弹出一个状态并将其置于当前状态,然后它将返回当前状态。

    当你重做时,它将当前状态压入撤消堆栈,从重做堆栈中弹出一个状态并将其置于当前状态,然后它将返回当前状态。

    制作从状态管理器返回的状态的副本很重要,这样您就不会无意中更改存储在缓冲区中的数据。

    你可能会问。 “为什么状态管理器不能保证数据是副本?”一个很好的问题,但这不是状态管理器的角色,它保存状态并且无论它必须保存什么都必须这样做,它本质上完全不知道它存储的数据的含义。通过这种方式,它可以用于图像、文本、游戏状态等任何东西,就像文件系统一样,它不能(不应该)知道含义,因此知道如何创建有意义的副本。您推送到状态管理器的数据只是对像素数据的单一引用(64 位长),或者您可以推送像素数据的每个字节,它不知道区别。

    另外,我还向状态管理器添加了一些 UI 控件。这允许它显示其当前状态,即禁用和启用撤消重做按钮。提供反馈对于良好的 UI 设计始终很重要。

    代码

    您需要对代码进行以下所有更改才能使用历史记录管理器。您可以这样做,或者仅将其用作指南并编写自己的指南。我在检测到您的错误之前写了这个。如果这是唯一的错误,那么您可能只需要更改。

       // your old code (from memory)
       colorLayerData = undoArr.pop(); 
       context.putImageData(colorLayerData, 0, 0);  
    
    
       // the fix same applies to redo and just makes a copy rather than use 
       // the reference that is still stored in the undoe buff
       context.putImageData(undoArr, 0, 0);   // put the undo onto the canvas
       colorLayerData = context.getImageData(0, 0, canvasWidth, canvaHeight); 
    

    删除所有用于撤消/重做的代码。

    将页面顶部的撤消/重做按钮更改为,用一个函数来处理这两个事件。

        <button id = "undo-button" onclick="history('undo')">Undo</button>
        <button id = "redo-button" onclick="history('redo')">Redo</button>
    

    将以下两个函数添加到您的代码中

            function history(command){ // handles undo/redo button events.
                var data;
                if(command === "redo"){
                    data = historyManager.redo(); // get data for redo
                }else
                if(command === "undo"){
                    data = historyManager.undo(); // get data for undo
                }
                if(data !== undefined){ // if data has been found
                    setColorLayer(data); // set the data
                }
            }
    
            // sets colour layer and creates copy into colorLayerData
            function setColorLayer(data){
                context.putImageData(data, 0, 0);  
                colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
                context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
                context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);            
            }
    

    在重绘功能中,您已经替换了要撤消的内容,并在同一位置添加了这一行。这会将当前状态保存在历史管理器中。

               historyManager.push(context.getImageData(0, 0, canvasWidth, canvasHeight));
    

    在启动函数中,您必须将 UI 元素添加到状态管理器。这取决于您,可以忽略,如果未定义,统计管理器将忽略它们。

                if(historyManager !== undefined){
                    // only for visual feedback and not required for the history manager to function.
                    historyManager.UI.assignUndoButton(document.querySelector("#undo-button"));
                    historyManager.UI.assignRedoButton(document.querySelector("#redo-button"));
                }
    

    当然还有 historyManager 本身。它封装了数据,所以你只能通过提供的接口访问它的内部状态。

    historyManager (hM) API

    • hM.UI ui 管理器只是更新和分配按钮 禁用/启用状态
    • hM.UI.assignUndoButton(element)设置撤消元素
    • hM.UI.assignRedoButton(element)设置重做元素
    • nM.UI.update() 更新按钮状态以反映当前 内部状态。所有内部状态都会自动调用它,所以 仅当您更改重做/撤消按钮统计您的自我时才需要
    • hM.reset() 重置历史管理器,清除所有堆栈和当前保存的状态。在加载或创建新项目时调用它。
    • nM.push(data) 将提供的数据添加到历史记录中。
    • nM.undo() 获取之前的历史状态并返回存储的数据。如果没有数据,那么这将返回未定义。
    • nM.redo() 获取下一个历史状态并返回存储的数据。如果没有数据,那么这将返回未定义。

    自调用函数创建历史管理器,通过变量historyManager访问接口

    var historyManager = (function (){  // Anon for private (closure) scope
        var uBuffer = []; // this is undo buff
        var rBuffer = []; // this is redo buff
        var currentState = undefined; // this holds the current history state
        var undoElement = undefined;
        var redoElement = undefined;
        var manager = {
            UI : {  // UI interface just for disable and enabling redo undo buttons
                assignUndoButton : function(element){
                    undoElement = element;
                    this.update();
                },
                assignRedoButton : function(element){
                    redoElement = element;
                    this.update();
                },
                update : function(){
                    if(redoElement !== undefined){
                        redoElement.disabled = (rBuffer.length === 0);
                    }
                    if(undoElement !== undefined){
                        undoElement.disabled = (uBuffer.length === 0);                                
                    }
                }
            },
            reset : function(){
                uBuffer.length = 0;
                rBuffer.length = 0;
                currentState = undefined;
                this.UI.update();
            },
            push : function(data){
                if(currentState !== undefined){
                    uBuffer.push(currentState);                        
                }
                currentState = data;
                rBuffer.length = 0;
                this.UI.update();
            },
            undo : function(){
               if(uBuffer.length > 0){
                   if(currentState !== undefined){
                        rBuffer.push(currentState);                        
                    }
                    currentState = uBuffer.pop();
                }
                this.UI.update();
                return currentState; // return data or unfefined
            },
            redo : function(){
                if(rBuffer.length > 0){
                   if(currentState !== undefined){
                        uBuffer.push(currentState);                        
                    }
                    currentState = rBuffer.pop();
                }
                this.UI.update();    
                return currentState;
            },
        }
        return manager;
    })();
    

    这将解决您的缩放问题和撤消问题。祝你的项目好运。

    【讨论】:

    • 好吧,让我玩一下。我会告诉你的。
    • 这解决了这两个问题。太感谢了。你是救生员。非常感谢。
    • 最后一件事。在缩放时,缩放适用于 chrome,但不适用于 Firefox。可能是什么问题?
    • @AliZia。我在firefox上看了看,没有发现任何问题,放大和缩小工作和绘画工作。
    • 嗯,我想这是我的问题
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-30
    • 2010-11-30
    • 2021-10-02
    • 2017-08-06
    相关资源
    最近更新 更多