【问题标题】:d3.js: enter-update-exit pattern - Old points not removedd3.js:进入-更新-退出模式 - 未删除旧点
【发布时间】:2018-08-01 00:50:56
【问题描述】:

我正在尝试在下图中执行进入-更新-退出模式(这是在 SO 的一些非常善良的 ppl 的巨大帮助下构建的,但不幸的是我现在又被卡住了。我无法使模式工作,但是我确定我选择了正确的对象(在下面的代码中命名为 heatDotsGroup)。

但是,我可以在 Chrome 的开发人员工具中检查该对象是否包含节点(省略号),但该模式不起作用,因此显然我做错了。

有什么想法吗?非常感谢!

function heatmap(dataset) {
    
    var svg = d3.select("#chart")
        .select("svg")
    
    var xLabels = [],
        yLabels = [];
    for (i = 0; i < dataset.length; i++) {
        if (i==0){
            xLabels.push(dataset[i].xLabel);
            var j = 0;
            while (dataset[j+1].xLabel == dataset[j].xLabel){
                yLabels.push(dataset[j].yLabel);
                j++;
            }
            yLabels.push(dataset[j].yLabel);
        } else {
            if (dataset[i-1].xLabel == dataset[i].xLabel){
                //do nothing
            } else {
                xLabels.push(dataset[i].xLabel);                    
            }
        }
    };

    var margin = {top: 0, right: 25,
                  bottom: 60, left: 75};  

    var width = +svg.attr("width") - margin.left - margin.right,
        height = +svg.attr("height") - margin.top - margin.bottom;

    var dotSpacing = 0,
        dotWidth = width/(2*(xLabels.length+1)),
        dotHeight = height/(2*yLabels.length);

    var daysRange = d3.extent(dataset, function (d) {return d.xKey}),
        days = daysRange[1] - daysRange[0];
    
    var hoursRange = d3.extent(dataset, function (d) {return d.yKey}),
        hours = hoursRange[1] - hoursRange[0];    
    
    var tRange = d3.extent(dataset, function (d) {return d.val}),
        tMin = tRange[0],
        tMax = tRange[1];

    var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
    
    // the scale
    var scale = {
        x: d3.scaleLinear()
           .range([-1, width]),
        y: d3.scaleLinear()
           .range([height, 0]),
    };
    
    var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
        yBand = d3.scaleBand().domain(yLabels).range([height, 0]);
    
    var axis = {
        x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
        y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
    };


    function updateScales(data){
        scale.x.domain([0, d3.max(data, d => d.xKey)]),
        scale.y.domain([ 0, d3.max(data, d => d.yKey)])
    }

    var colorScale = d3.scaleQuantile()
        .domain([0, colors.length - 1, d3.max(dataset, function (d) {return d.val;})])
        .range(colors);

    var zoom = d3.zoom()
        .scaleExtent([1, dotHeight])
        .on("zoom", zoomed);

    var tooltip = d3.select("body").append("div")
        .attr("id", "tooltip")
        .style("opacity", 0);

    // SVG canvas
    svg = d3.select("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .call(zoom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // Clip path
    svg.append("clipPath")
        .attr("id", "clip")
        .append("rect")
        .attr("width", width)
        .attr("height", height+dotHeight);


    // Heatmap dots
    var heatDotsGroup = svg.append("g")
        .attr("clip-path", "url(#clip)")
        .append("g");
        

    //Create X axis
    var renderXAxis = svg.append("g")
        .attr("class", "x axis")
        //.attr("transform", "translate(0," + scale.y(-0.5) + ")")
        //.call(axis.x)

    //Create Y axis
    var renderYAxis = svg.append("g")
        .attr("class", "y axis")
        .call(axis.y);


    function zoomed() {
        d3.event.transform.y = 0;
        d3.event.transform.x = Math.min(d3.event.transform.x, 5);
        d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
        // console.log(d3.event.transform)

        // update: rescale x axis
        renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));

        // Make sure that only the x axis is zoomed
        heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
    }
    
    svg.call(renderPlot, dataset)
    
    function renderPlot(selection, dataset){
        
        //Do the axes
        updateScales(dataset)
        selection.select('.y.axis').call(axis.y)
        selection.select('.x.axis')
                .attr("transform", "translate(0," + scale.y(-0.5) + ")")
                .call(axis.x)
           
        
        // Do the chart
        const update = heatDotsGroup.selectAll("ellipse")
        .data(dataset);
        
        update
        .enter()
        .append("ellipse")
        .attr("cx", function (d) {return scale.x(d.xKey) - xBand.bandwidth();})
        .attr("cy", function (d) {return scale.y(d.yKey) + yBand.bandwidth();})
        .attr("rx", dotWidth)
        .attr("ry", dotHeight)
        .attr("fill", function (d) {
            return colorScale(d.val);}
            )
        .merge(update).transition().duration(800);   
        
        update.exit().remove();
        
    }


};
#clickMe{
    height:50px;
    width:150px;
    background-color:lavender;
}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    
    <title>Heatmap Chart</title>

    <!-- Reference style.css -->
    <!--    <link rel="stylesheet" type="text/css" href="style.css">-->

    <!-- Reference minified version of D3 -->
    <script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
    <script src='heatmap_v4.js' type='text/javascript'></script>
</head>

<body>
<input id="clickMe" type="button" value="click me to push new data" onclick="run();" />

    <div id='chart'>
    <svg width="700" height="500">
      <g class="focus">
        <g class="xaxis"></g>
        <g class="yaxis"></g>
      </g>
    </svg>
   </div>
    
   
    <script>
        function run() {
            var dataset = [];
                for (let i = 1; i < 360; i++) { //360
                    for (j = 1; j < 7; j++) {  //75
                        dataset.push({
                            xKey: i,
                            xLabel: "xMark " + i,
                            yKey: j,
                            yLabel: "yMark " + j,
                            val: Math.random() * 25,
                        })
                        }
                    };

                    heatmap(dataset)
        }

        $(document).ready(function() {});
    </script>
</body>

</html>

【问题讨论】:

    标签: javascript d3.js


    【解决方案1】:

    问题是您每次运行进入/退出/更新周期时都没有使用相同的选择。当按下按钮时:

    1. 生成新数据
    2. 运行热图函数
    3. 热图函数选择 svg 并附加一个名为 heatDotsGroup 的新 g
    4. 调用更新函数并传递新创建的g作为选择
    5. 回车循环追加所有内容,因为新的g 为空。

    因此,exit 和 udpate 周期都是空的。试试:

    console.log(update.size(),update.exit().size()) // *Without any merge* 
    

    您应该会看到每次更新都为空。这是因为每次都输入了所有元素,所以每次更新都会增加省略号的数量。

    我已经从 heatmap 函数中提取了一堆变量声明和附加语句,这些东西只需要运行一次(我可以更进一步,但我只做了最少的事情)。我还合并了您的更新并在设置属性之前输入选择(因为我们想在更新时设置新属性)。下面的 sn-p 应该演示此更改。

    在 sn-p 中,按下按钮会发生以下情况:

    1. 生成新数据
    2. 运行热图函数
    3. 热图功能选择现有的选择并且不附加任何新内容
    4. 调用更新函数并传递包含任何现有节点的先前更新/进入/退出循环使用的选择。
    5. 更新功能根据现有节点根据需要进入/退出/更新元素。

    这是基于上述内容的工作版本:

    // Things to set/append once:
    var svg = d3.select("#chart")
      .select("svg")
    
    var margin = {top: 0, right: 25,bottom: 60, left: 75};  
    var width = +svg.attr("width") - margin.left - margin.right,
      height = +svg.attr("height") - margin.top - margin.bottom;
      
    svg = svg.attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .append("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
    var clip = svg.append("clipPath")
            .attr("id", "clip")
            .append("rect")
    
    
    var heatDotsGroup = svg.append("g")
      .attr("clip-path", "url(#clip)")
      .append("g");
    
    var xAxis = svg.append("g").attr("class", "x axis");
    var yAxis = svg.append("g").attr("class", "y axis")
         
    
            
    function heatmap(dataset) {
        
        
        var xLabels = [],
            yLabels = [];
        for (i = 0; i < dataset.length; i++) {
            if (i==0){
                xLabels.push(dataset[i].xLabel);
                var j = 0;
                while (dataset[j+1].xLabel == dataset[j].xLabel){
                    yLabels.push(dataset[j].yLabel);
                    j++;
                }
                yLabels.push(dataset[j].yLabel);
            } else {
                if (dataset[i-1].xLabel == dataset[i].xLabel){
                    //do nothing
                } else {
                    xLabels.push(dataset[i].xLabel);                    
                }
            }
        };
    
    
    
        var dotSpacing = 0,
            dotWidth = width/(2*(xLabels.length+1)),
            dotHeight = height/(2*yLabels.length);
    
        var daysRange = d3.extent(dataset, function (d) {return d.xKey}),
            days = daysRange[1] - daysRange[0];
        
        var hoursRange = d3.extent(dataset, function (d) {return d.yKey}),
            hours = hoursRange[1] - hoursRange[0];    
        
        var tRange = d3.extent(dataset, function (d) {return d.val}),
            tMin = tRange[0],
            tMax = tRange[1];
    
        var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
        
        // the scale
        var scale = {
            x: d3.scaleLinear()
               .range([-1, width]),
            y: d3.scaleLinear()
               .range([height, 0]),
        };
        
        var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
            yBand = d3.scaleBand().domain(yLabels).range([height, 0]);
        
        var axis = {
            x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
            y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
        };
    
    
        function updateScales(data){
            scale.x.domain([0, d3.max(data, d => d.xKey)]),
            scale.y.domain([ 0, d3.max(data, d => d.yKey)])
        }
    
        var colorScale = d3.scaleQuantile()
            .domain([0, colors.length - 1, d3.max(dataset, function (d) {return d.val;})])
            .range(colors);
    
        var zoom = d3.zoom()
            .scaleExtent([1, dotHeight])
            .on("zoom", zoomed);
    
        var tooltip = d3.select("body").append("div")
            .attr("id", "tooltip")
            .style("opacity", 0);
    
        // SVG canvas
        svg.call(zoom);
    
    
        // Clip path
      clip.attr("width", width)
            .attr("height", height+dotHeight);
    
    
    
            
    
        //Create X axis
        var renderXAxis = xAxis 
            //.attr("transform", "translate(0," + scale.y(-0.5) + ")")
            //.call(axis.x)
    
        //Create Y axis
        var renderYAxis = yAxis.call(axis.y);
    
    
        function zoomed() {
            d3.event.transform.y = 0;
            d3.event.transform.x = Math.min(d3.event.transform.x, 5);
            d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
            // console.log(d3.event.transform)
    
            // update: rescale x axis
            renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
    
            // Make sure that only the x axis is zoomed
            heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
        }
        
        svg.call(renderPlot, dataset)
        
        function renderPlot(selection, dataset){
            
            //Do the axes
            updateScales(dataset)
            selection.select('.y.axis').call(axis.y)
            selection.select('.x.axis')
                    .attr("transform", "translate(0," + scale.y(-0.5) + ")")
                    .call(axis.x)
               
            
            // Do the chart
            const update = heatDotsGroup.selectAll("ellipse")
            .data(dataset);
            
            update
            .enter()
            .append("ellipse")
            .merge(update)
            .attr("cx", function (d) {return scale.x(d.xKey) - xBand.bandwidth();})
            .attr("cy", function (d) {return scale.y(d.yKey) + yBand.bandwidth();})
            .attr("rx", dotWidth)
            .attr("ry", dotHeight)
            .attr("fill", function (d) {
                return colorScale(d.val);}
                )
             
            
            update.exit().remove();
            
        }
    
    
    };
    #clickMe{
        height:50px;
        width:150px;
        background-color:lavender;
    }
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="utf-8">
        
        <title>Heatmap Chart</title>
    
        <!-- Reference style.css -->
        <!--    <link rel="stylesheet" type="text/css" href="style.css">-->
    
        <!-- Reference minified version of D3 -->
        <script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
        <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
        <script src='heatmap_v4.js' type='text/javascript'></script>
    </head>
    
    <body>
    <input id="clickMe" type="button" value="click me to push new data" onclick="run();" />
    
        <div id='chart'>
        <svg width="700" height="500">
          <g class="focus">
            <g class="xaxis"></g>
            <g class="yaxis"></g>
          </g>
        </svg>
       </div>
        
       
        <script>
            function run() {
                var dataset = [];
                    for (let i = 1; i < 360; i++) { //360
                        for (j = 1; j < 7; j++) {  //75
                            dataset.push({
                                xKey: i,
                                xLabel: "xMark " + i,
                                yKey: j,
                                yLabel: "yMark " + j,
                                val: Math.random() * 25,
                            })
                            }
                        };
    
                        heatmap(dataset)
            }
    
            $(document).ready(function() {});
        </script>
    </body>
    
    </html>

    这里的退出选择仍然是空的,因为数据数组的大小是固定的。 D3 假设新数据替换旧数据,但它不知道新数据应该表示为新元素,除非我们指定一个关键函数,如现在删除的注释中所述。这可能是也可能不是您想要的功能。

    【讨论】:

    • Andrew 和 rioV8,非常感谢你们!你极大地帮助了我,你对进入-更新-退出模式有了很好的了解事实上我确实尝试在 html 代码中设置 svg(我的 html sn-p 中的那些 g 元素是这项工作的剩余部分)但是我再次未能成功运行该模式。可能是我在别处搞砸了
    • 我希望我能接受这两个答案,两者都绝对值得,但是我选择 Andrew,因为他更详细一些,因为在我之前的帖子中你们也提供了惊人的见解,我选择了 rioV8。所以总体上打平看起来是公平的。 (对不起,如果这听起来很傻......)无论如何......再次感谢一百万,我至少欠你一杯啤酒......抱歉这么晚才回复,我直到现在才仔细看。你们为什么不用这个做一个 bl.ocks 条目?我还没有在那里看到类似的东西。再次,非常感谢
    • @Aenaon,没问题,很高兴我们能够提供帮助。至于 bl.ock,如果您能够分享(或显示有限版本),那么看到您的最终产品会很整洁。
    【解决方案2】:

    我的方法与 Andrew 略有不同。

    当你有多个图表时,一堆全局变量会变得混乱。

    当你点击按钮时:

    1. 拨打renderPlot(dataset)
    2. 检查 svg 中是否有 #clip 元素
    3. 如果没有:致电heatmap(dataset)
      1. 构造所有静态内容并附加到 svg。
      2. datum 对象附加到带有更新所需变量的svg
    4. 从 svg 中获取 datum
    5. 使用 datum 对象更新 svg 的内容

    function heatmap(dataset) {
    
    var svg = d3.select("#chart")
        .select("svg");
    
    var xLabels = [],
        yLabels = [];
    for (i = 0; i < dataset.length; i++) {
        if (i==0){
            xLabels.push(dataset[i].xLabel);
            var j = 0;
            while (dataset[j+1].xLabel == dataset[j].xLabel){
                yLabels.push(dataset[j].yLabel);
                j++;
            }
            yLabels.push(dataset[j].yLabel);
        } else {
            if (dataset[i-1].xLabel == dataset[i].xLabel){
                //do nothing
            } else {
                xLabels.push(dataset[i].xLabel);                    
            }
        }
    };
    
    var margin = {top: 0, right: 25,
                  bottom: 60, left: 75};  
    
    var width = +svg.attr("width") - margin.left - margin.right,
        height = +svg.attr("height") - margin.top - margin.bottom;
    
    var dotSpacing = 0,
        dotWidth = width/(2*(xLabels.length+1)),
        dotHeight = height/(2*yLabels.length);
    
    var daysRange = d3.extent(dataset, function (d) {return d.xKey}),
        days = daysRange[1] - daysRange[0];
    
    var hoursRange = d3.extent(dataset, function (d) {return d.yKey}),
        hours = hoursRange[1] - hoursRange[0];    
    
    var tRange = d3.extent(dataset, function (d) {return d.val}),
        tMin = tRange[0],
        tMax = tRange[1];
    
    var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
    
    // the scale
    var scale = {
        x: d3.scaleLinear()
           .range([-1, width]),
        y: d3.scaleLinear()
           .range([height, 0]),
    };
    
    var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
        yBand = d3.scaleBand().domain(yLabels).range([height, 0]);
    
    var axis = {
        x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
        y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
    };
    
    var colorScale = d3.scaleQuantile()
        .domain([0, colors.length - 1, d3.max(dataset, function (d) {return d.val;})])
        .range(colors);
    
    var zoom = d3.zoom()
        .scaleExtent([1, dotHeight])
        .on("zoom", zoomed);
    
    var tooltip = d3.select("body").append("div")
        .attr("id", "tooltip")
        .style("opacity", 0);
    
    // SVG canvas
    svg .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .call(zoom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
    // Clip path
    svg.append("clipPath")
        .attr("id", "clip")
        .append("rect")
        .attr("width", width)
        .attr("height", height+dotHeight);
    
    
    // Heatmap dots
    var heatDotsGroup = svg.append("g")
        .attr("clip-path", "url(#clip)")
        .append("g");
    
    //Create X axis
    var renderXAxis = svg.append("g")
        .attr("class", "x axis")
        //.attr("transform", "translate(0," + scale.y(-0.5) + ")")
        //.call(axis.x)
    
    //Create Y axis
    var renderYAxis = svg.append("g")
        .attr("class", "y axis")
        .call(axis.y);
    
    
    function zoomed() {
        d3.event.transform.y = 0;
        d3.event.transform.x = Math.min(d3.event.transform.x, 5);
        d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
        // console.log(d3.event.transform)
    
        // update: rescale x axis
        renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
    
        // Make sure that only the x axis is zoomed
        heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
    }
    
    var chartData = {};
    chartData.scale = scale;
    chartData.axis = axis;
    chartData.xBand = xBand;
    chartData.yBand = yBand;
    chartData.colorScale = colorScale;
    chartData.heatDotsGroup = heatDotsGroup;
    chartData.dotWidth = dotWidth;
    chartData.dotHeight = dotHeight;
    
    svg.datum(chartData);
    
    //svg.call(renderPlot, dataset)
    }
    function updateScales(data, scale){
        scale.x.domain([0, d3.max(data, d => d.xKey)]),
        scale.y.domain([0, d3.max(data, d => d.yKey)])
    }
    
    function renderPlot(dataset){
        
        var svg = d3.select("#chart")
            .select("svg");
        if (svg.select("#clip").empty()) { heatmap(dataset); }
        chartData = svg.datum();
        //Do the axes
        updateScales(dataset, chartData.scale);
        svg.select('.y.axis').call(chartData.axis.y)
        svg.select('.x.axis')
                .attr("transform", "translate(0," + chartData.scale.y(-0.5) + ")")
                .call(chartData.axis.x)
        
        // Do the chart
        const update = chartData.heatDotsGroup.selectAll("ellipse")
        .data(dataset);
        
        update
        .enter()
        .append("ellipse")
        .attr("rx", chartData.dotWidth)
        .attr("ry", chartData.dotHeight)
        .merge(update)
        .transition().duration(800)
            .attr("cx", function (d) {return chartData.scale.x(d.xKey) - chartData.xBand.bandwidth();})
            .attr("cy", function (d) {return chartData.scale.y(d.yKey) + chartData.yBand.bandwidth();})
            .attr("fill", function (d) { return chartData.colorScale(d.val);} );
        
        update.exit().remove();
    }
    #clickMe{
    height:50px;
    width:150px;
    background-color:lavender;
    }
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
    <meta charset="utf-8">
    
    <title>Heatmap Chart</title>
    
    <!-- Reference style.css -->
    <!--    <link rel="stylesheet" type="text/css" href="style.css">-->
    
    <!-- Reference minified version of D3 -->
    <script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
    <script src='heatmap_v4.js' type='text/javascript'></script>
    </head>
    
    <body>
    <input id="clickMe" type="button" value="click me to push new data" onclick="run();" />
    
    <div id='chart'>
    <svg width="700" height="500">
      <g class="focus">
        <g class="xaxis"></g>
        <g class="yaxis"></g>
      </g>
    </svg>
       </div>
    
       
    <script>
        function run() {
            var dataset = [];
            for (let i = 1; i < 360; i++) { //360
                for (j = 1; j < 7; j++) {  //75
                    dataset.push({
                        xKey: i,
                        xLabel: "xMark " + i,
                        yKey: j,
                        yLabel: "yMark " + j,
                        val: Math.random() * 25,
                    })
                }
            };
    
            renderPlot(dataset)
        }
    
        $(document).ready(function() {});
    </script>
    </body>
    
    </html>

    【讨论】:

    • rioV8,再次感谢您!抱歉,尽管您的回答很完整,但我没有接受您的回答,请参阅下面我对 Andrew 的评论以获得解释,尽管这不是最好的...
    • rioV8,我对您的代码做了一些小改动,由于某种原因,导致边距表现得很疯狂,并且 y 轴刻度线不可见(例如,增加左边距确实移动了图表),昨天我几乎花了一整天的时间才弄清楚(仍然不是 100% 发生了什么......)。下面的代码,我现在正在尝试修复顶部和右侧的空白并移动点,因为它们被切断了。 [plunker][1] [1] 中的代码:plnkr.co/edit/EZAM825ZcNyjUefwMUjG?p=preview
    • 主要更改,第 103、157 和 171 行。我希望它们有意义
    猜你喜欢
    • 1970-01-01
    • 2020-06-27
    • 2022-01-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多