• React的井字过三关(2)


    这篇笔记是官方教程的延续笔记,所有代码基于第一篇笔记的结尾代码。旨在解决教程后面提出的五个问题。


    一 . 用(X,Y)来取代原有的数字坐标

    原来的数字坐标是这样的:

    现在的要求是把原来的代码坐标改为二维坐标系的表达形式,并且在历史记录面板中打出转换后的坐标。

    如果只是为了输出好看。只需要写一个转换方法,这些在顶层的Game组件中实现就够了。而不需要修改原来的代码核心。

    很自然想到用switch语句,其实都行,怎么喜欢怎么写。

    convert:function(i){//i是一维坐标
                    var x=Math.floor(i/3)+1;
                    var y=0;
                    if((i+1)%3===0){
                        y=3;
                    }else{
                        y=(i+1)%3;
                    }
                    return [x,y];
                },
    

    调用就直接在渲染前调用。把它存到state里面去。

    接下来问题是把这个方法怎么获取参数i,最直接的办法是写一个全局变量,然后从handleClick里面拿到i。但是全局变量不环保。或许再设一个顶层状态lastLocation是个不错的选择,渲染队列是一个数组,姑且称之为historyLocation

    根据React的价值观,能根据其它原有状态计算出来的东西,就不需要设置额外的状态。但如果思路不明确,就在这里先写出来。

    回退步骤的本质

    在上一篇笔记最后,官方文档没有说清楚一个问题。就是状态中的stepNumber是什么。现在再次遇到,需要写明白给自己提个醒。

    回退步骤

    每走一步,history状态就会在最后追加一个最新版本。

    stepNumber实际上是一个指针,根据这个指针,发送history状态的版本(可能是旧的,也可能是最新的),用它来调控渲染状态。

    点击回退步骤,就是把指针往前挪。

    通过暂存器刷新状态

    如果没有任何其它操作直接触发handleClick,stepNumber指针直接指向最新的版本。

    如果在回退步骤上发生了handleClick,那么将发生以下事情:

    • 根据指针生成若干个状态暂存器,这个暂存器是独立且不具备任何效力的,抛开环境来看就是普通变量;
    • 追加新的状态到暂存器;
    • 再用这个暂存器替换掉原有的状态,在此,回退步骤列表将被刷新。

    究竟有几个状态暂存器?在这里就两个:

    • 一个储存history的当前指针版本:

      handleClick:function(i){
          //history指针版本暂存
          var history=this.state.history.slice(0,this.state.stepNumber+1);
          var lastHistory=history[this.state.stepNumber];
          var squares=lastHistory.squares.slice();
      
    • 一个储存二维坐标轴版本:

      /**上接handleClick***/
      //二维坐标数据暂存器
      var historyLocation=this.state.historyLocation;
      historyLocation=historyLocation.slice(0,this.state.stepNumber);
      historyLocation.push(this.convert(i));//刷新状态暂存器
      

    通过状态暂存器,既可以在指针位置重新开始,又能在屏幕上保留历史步骤数方便查看,即实现官方文档所谓的“时间旅行”。

    在这里意识到状态暂存器其实应该只有一个是最好的。

    judgeWinner的完善

    按理来说,判断胜负的函数judgeWinner应该是在组件的里面,这样比较环保,也可以更好地调用组件中的状态。

    现在就把它拿进来。直接生成渲染方法中的status。并且添加一个和棋的判断。实现思路是调用指针版本的history状态数组。然后遍历这个数组对象,如果发现9个位置全部不为null,就返回和棋。

    放进来之后,参数也没有必要再写,全部改为state相关的表达。

    这样一来就没办法用原来的禁着点判断了。因为不好判断棋局是否完结。在此根据返回的结果进行indexOf判断,留下的坑后面填。

    所以到此为止,Game组件应该是:

    getInitialState:function(){
                    return {
                        history:[
                            {squares:Array(9).fill(null)}
                        ],
                        turnToX:true,
                        stepNumber:0,
                        historyLocation:[]
                    }
                },
                // 判断胜负的函数,穷举法
                judgeWinner:function(){
                    var history=this.state.history;
                    var lastHistory=history[this.state.stepNumber];
                    var squares=lastHistory.squares;
                    var win=[
                        [0,1,2],
                        [0,3,6],
                        [0,4,8],
                        [1,4,7],
                        [2,5,8],
                        [2,4,6],
                        [3,4,5],
                        [6,7,8]
                    ];
                    for(var i=0;i<win.length;i++){
                        var winCase=win[i];
                        if(squares[winCase[0]]&&squares[winCase[0]]===squares[winCase[1]]&&squares[winCase[1]]===squares[winCase[2]]){//三子一线
                            return ('获胜方是:'+squares[winCase[0]]);//返回胜利一方的标识
                        }
                    }
    
                    // 定义当前棋盘上被填满的格子数量
                    var fill=lastHistory.squares.filter((item)=>item!=null).length;
                    if(fill==9){
                        return '和棋!'
                    }else{
                        var player=this.state.turnToX?'X':'O';
                        return ('轮到'+player+'走');
                    }
                },
                // 点击事件是把暂存器的内容存为真正的状态。
                handleClick:function(i){
                    //历史squares暂存
                    var history=this.state.history;
                    history=history.slice(0,this.state.stepNumber+1);
    
                    var lastHistory=history[this.state.stepNumber];
                    var winner=this.judgeWinner();
                    var squares=lastHistory.squares.slice();
    
                    //历史步骤暂存器
                    var historyLocation=this.state.historyLocation;
                    historyLocation=historyLocation.slice(0,this.state.stepNumber);
                    historyLocation.push(this.convert(i));
    
                    if((winner.indexOf('轮到')==-1)||squares[i]){
                        return false;
                        //胜负已分或是已有子则不可落子。indexOf这是一种暂时的非主流写法
                    }
                    // 判断下棋的轮换色
                    squares[i]=this.state.turnToX?'X':'O';
    
                    this.setState({
                        history:history.concat([{squares:squares}]),
                        turnToX:!this.state.turnToX,
                        stepNumber:history.length,
                        historyLocation:historyLocation
                    });
    
                },
                // 历史步骤跳转是把状态还原到某个时间点,状态根据stepNumber呈现内容,但不会改变最终状态。
                jumpTo:function(step){
                    this.setState({
                        stepNumber:step,
                        turnToX:step%2?false:true
                    });
                },
                // 坐标转换函数
                convert:function(i){
                    var x=Math.floor(i/3)+1;
                    var y=0;
                    if((i+1)%3===0){
                        y=3;
                    }else{
                        y=(i+1)%3;
                    }
                    return [x,y];
                },
    
                render:function(){
                    var history=this.state.history.slice();
                    var lastHistory=history[this.state.stepNumber];//渲染方法遵照的是stepNumber而不是最后一步
                    var status=this.judgeWinner();//获胜状态
                    var arr=[];
                    var location=this.state.historyLocation.slice();
                    var _this=this;
                    history.map(function(step,move){
                        var content='';
                        if(move!==0){
                            content='Move#'+move+':'+'('+location[move-1][0]+','+location[move-1][1]+')';
                            //console.log(location[move-1])
                        }else{
                            content='游戏开始~';
                        }
                        arr.push(<li key={move}><a onClick={()=>_this.jumpTo(move)} href="javascript:;">{content}</a></li>);
                    });
    
                    return (
                        <div className="game">
                            <Board lastHistory={lastHistory.squares} onClick={(i)=>this.handleClick(i)} />
                            <div className="info">
                                <div>{status}</div>
                                <ol>{arr}</ol>
                            </div>
                        </div>
                    );
                }
            });
    

    二. 对右方的被选中的当前记录进行加粗显示

    样式这种东西,就交给CSS来实现吧!

    .back-active{
    	font-weight: bolder;
    	color: #EE9611;
    }
    

    简单实现

    思路就是加个class。操作方法是jumpTo。

    问题在于,当前的jumpTo已经给定了参数。为了拿到e.target还得在改改。

    jumpTo在这个问题中实际上要完成两件事,删除所有a的class中可能.back-active;给当前对象加个.back-active

    有了e.target,就能用DOM找到该有的内容。比方说e.target.parentNode.parentNode.childNode就代表所有点击对象上层的所有li集合

    然而这个集合不是一个数组啊,不能map。只能用for循环。根据查到的性能资料,for循环还真的比其它迭代方法高。

    jumpTo:function(e,step){
                    // console.log(e.target)
                    var aList=e.target.parentNode.parentNode.childNodes;
                    for(var i=0;i<aList.length;i++){
                        var item=aList[i];
                        if(item.childNodes[0].classList.contains('back-active')){
                            item.childNodes[0].classList.remove('back-active');
                        }
                    }
    
                    e.target.classList.add('back-active');
                    this.setState({
                        stepNumber:step,
                        turnToX:step%2?false:true
                    });
                },
    

    这个问题就算解决了。

    点击实现高亮当前的步骤

    其实就个人理解来说,不应该再对handleClick再加什么高亮当前步骤的操作了。当前步骤明摆着就是最后一个。纵观就当前的代码实现,用户体验已经很好了,进程不会乱七八糟,用户还可以很清晰地知道指针指向的还原点。还高亮什么?

    但是假设老板就要求点击按钮时最后一步也高亮,那也只能照做。

    显然,这个应该放渲染前判断:如果这是状态最后一步(是this.state.history.length-1,不是this.state.stepNumber),那么就高亮。反正样式也不要钱,就多写一个样式给它。

    .process-active{
    	font-weight: bolder;
    	color: green;
    }/*写在.back-active之后,方便覆盖*/
    

    这样,渲染前的处理里还得多加一个判断:是最后一个就加.process-active——这段获取历史步骤的方法已经变得太长了。为了阅读方便把它放一个getMoveList函数里吧。

    ...
    	getMoveList:function(){
                    var history=this.state.history.slice();
                    var arr=[];
                    var location=this.state.historyLocation.slice();
                    var _this=this;
                    history.map(function(step,move){
                        var content='';
                        if(move!==0){
                            content='Move#'+move+':'+'('+location[move-1][0]+','+location[move-1][1]+')';
                            //console.log(location[move-1])
                        }else{
                            content='游戏开始~';
                        }
                        //console.log(_this.state.stepNumber)
                        if(arr.length==_this.state.history.length-1){
                            arr.push(<li key={move}><a className="process-active" onClick={(e)=>_this.jumpTo(e,move)} href="javascript:;">{content}</a></li>);
                        }else{
                            arr.push(<li key={move}><a onClick={(e)=>_this.jumpTo(e,move)} href="javascript:;">{content}</a></li>);
                        }
                    });
                    return arr;
                },
                  ...
    

    这样,第二个问题就解决了。


    三. 用两个循环重写Board组件,替代掉原来生硬的代码结构

    因为只有9宫格,复用也毫无意义。所以写死也问题不大。

    想到的处理方法就是这样了。

    var Board=React.createClass({
                renderSquare:function(i){
                    return <Square key={i} value={this.props.lastHistory[i]} onClick={() => this.props.onClick(i)} />
                },
                getSquare:function(rows){
                    var index=rows*3;
                    var arr=[];
                    for(var i=index;i<index+3;i++){
                        arr.push(this.renderSquare(i));
                    }
                    return arr;
                },
                getBoardRow:function(){
                    var arr=[];
                    for(var i=0;i<3;i++){
                        arr.push(<div key={i} className="board-row">{this.getSquare(i)}</div>)
                    }
                    return arr;
                },
                render:function(){
                    return (
                        <div clasName="board">
                            <div className="status"></div>
                            {this.getBoardRow()}
                        </div>
                    );
                }
            });
    

    四. 对你的历史记录进行升降序排列

    接下来又回到Game组件上面来了。在渲染结构中加一个按钮。点击,触发事件。大概就是这样。

    <input type="button" value={this.state.isAscending} onClick={this.sortToggle} />
    

    因为默认就是降序,因此这个toggleSort只做一件事:切换开关。至于是升序还是降序,又要多设置一个开关状态(isAscending,初始为降序排列)。

    根据这个状态,getMoveList方法决定生成数组后是直接return还是return arr.reverse()

    sortToggle:function(){
        this.setState(function(prevState){
            var sort=prevState.isAscending;
            var content='';
            if(sort=='升序排列'){
                content='降序排列';
            }else{
                content='升序排列'
            }
            return {
                isAscending:content
            }
        })
    },
    

    然后再到getMoveList方法的最后,加一个判断:

    .....
    				if(this.state.isAscending=='降序排列'){
                        return arr;
                    }else{
                        return arr.reverse();
                    }
    }
    

    嗯,第四个问题解决。


    五. 高亮显示获胜的结果

    扩展judgeWinner的功能

    judgeWinner判断函数已经被纳入到了组件中,而且只是返回一个status,现在要扩展它的功能,把胜负情况反应出来。

    在原来的判断胜负函数里面加个console就可以知道胜负手了。

    for(var i=0;i<win.length;i++){
                        var winCase=win[i];
                        if(squares[winCase[0]]&&squares[winCase[0]]===squares[winCase[1]]&&squares[winCase[1]]===squares[winCase[2]]){//三子一线
                            console.log(winCase)//这里的winCase就是胜负手
                            return ('获胜方是:'+squares[winCase[0]]);//返回胜利一方的标识
                        }
                    }
    

    既然是扩展功能,再来大改就没必要了。可以考虑把return一个字符串改为return一个数组。第0项放status,第1项放winCase或null

    有了这个方法,handleClick中那种非主流写法就可以删掉了。

    				var winner=this.judgeWinner();
                    if(winner[1]||squares[i]){
                        return false;
                        //胜负已分或已有子:则不可落子。
                    }
    

    传递胜负手

    再写一个 CSS

    .win-case{
    	color: red;
    }
    

    现在可以通过winner[1]拿到胜负手了。它是一个数组。现在就得在Game组件render方法里面在var一个数据。通过props传下去,传到Board组件之后,做一个判断,看看参数是否符合点位条件,是的话就继续把class名传下去。

    /********<Game/>*******/
    render:function{
    	var winCase=this.judgeWinner()[1];//获胜状态
    	return (
    		<div className="game">
    		<Board winCase={winCase} lastHistory={lastHistory.squares} onClick={(i)=>this.handleClick(i)} />
    ...
    /**********<Board/>***********/
    renderSquare:function(i){
                    if(this.props.winCase){
                        for(var j=0;j<this.props.winCase.length;j++){
                            if(this.props.winCase[j]==i){
                                return <Square winCase="win-case" key={i} value={this.props.lastHistory[i]} onClick={() => this.props.onClick(i)} />
                            }
                        }
                    }
                    return <Square key={i} value={this.props.lastHistory[i]} onClick={() => this.props.onClick(i)} />
                },
    ...
    /************<square/>******************/
      var Square=React.createClass({
                  render:function(){
                      if(this.props.winCase){
                          return (
                              <button className={"square "+this.props.winCase} onClick={() => this.props.onClick()}>{this.props.value}</button>
                          );
                      }else{
                          return (
                              <button className="square" onClick={() => this.props.onClick()}>{this.props.value}</button>
                          );
                      }
                  }
            });
    。。。。。。
    

    那么第五个问题就完成了。


    结束

    现在,功能已经完备。思路已经理清。再看之前留下的大坑:historyLocation

    之前提到过,historyLocation是可以和history相互计算得出的。historyLocation只用于展示步数。组件的判断引擎是用兼容history的一维数组实现的,为了后期实现AI书写方便,也显然是history更好。还是删掉这个historyLocation。

    不好之处在于每次都要多一点计算,相比React每次动辄重新渲染,这点计算也不是很多。

    写一个根据history获取坐标的方法,拿到坐标之后在转换为二维坐标,这本质上是一件事,所以convert方法也可以删掉了。

    getRectangular:function(){
                    var arr=[];
                    var mainArr=this.state.history.slice();
                    for(var i=0;i<mainArr.length;i++){
                        if(i<mainArr.length-1){
                            for(var j=0;j<9;j++){
                                //比较mainArr[i].squares和mainArr[i+1].squares[j])不同,拿到坐标值
                                if(mainArr[i].squares[j]!==mainArr[i+1].squares[j]){
                                    arr.push(j);
                                }
                            }
                        }
                    }
                    var result=[]
                    for(var i=0;i<arr.length;i++){
                        var x=Math.floor(arr[i]/3)+1;
                        var y=(arr[i]+1)%3===0?3:(arr[i]+1)%3;
                        result.push([x,y])
                    }
                    return result;
                },
    

    可以再自己优化下算法和css,或者加个重置button之类的。把不必要的变量删掉。

    效果:

    下一篇笔记将解决最大的一个坑。


    附录:组件代码

    var Game=React.createClass({
                getInitialState:function(){
                    return {
                        history:[
                            {squares:Array(9).fill(null)}
                        ],
                        turnToX:true,
                        stepNumber:0,
                        isAscending:'降序排列'
                    }
                },
                // 判断胜负的函数,穷举法,返回一个数组,如果胜负已定,第二个元素就是胜负手
                judgeWinner:function(){
                    var lastHistory=this.state.history[this.state.stepNumber];//获取指针版本
                    var squares=lastHistory.squares;
                    var win=[
                        [0,1,2],
                        [0,3,6],
                        [0,4,8],
                        [1,4,7],
                        [2,5,8],
                        [2,4,6],
                        [3,4,5],
                        [6,7,8]
                    ];
                    for(var i=0;i<win.length;i++){
                        var winCase=win[i];
                        if(squares[winCase[0]]
                          &&squares[winCase[0]]===squares[winCase[1]]
                          &&squares[winCase[1]]===squares[winCase[2]]){//三子一线
                            return [('获胜方是:'+squares[winCase[0]]),winCase];//返回一个status和胜负情况
                        }
                    }
                    // 获取当前棋盘上被填满的格子数量
                    var fill=lastHistory.squares.filter((item)=>item!=null).length;
                    if(fill==9){
                        return ['和棋!',null];
                    }else{
                        var player=this.state.turnToX?'X':'O';
                        return [('轮到'+player+'走'),null];
                    }
                },
    
                // 点击事件是把暂存器的内容存为真正的状态。
                handleClick:function(i){
                    //history指针版本暂存
                    var history=this.state.history.slice(0,this.state.stepNumber+1);
                    var lastHistory=history[this.state.stepNumber];
                    var squares=lastHistory.squares.slice();
    
    
                    var winner=this.judgeWinner();
                    if(winner[1]||squares[i]){
                        return false;
                        //胜负已分或是已有子则不可落子。
                    }
                    // 判断下棋的轮换色
                    squares[i]=this.state.turnToX?'X':'O';
                    //覆盖掉原来的状态!
                    this.setState({
                        history:history.concat([{squares:squares}]),
                        turnToX:!this.state.turnToX,
                        stepNumber:history.length
                    });
    
                },
                // 转化history状态为各个版本的平面直角坐标
                getRectangular:function(){
                    var arr=[];
                    var mainArr=this.state.history.slice();
                    for(var i=0;i<mainArr.length;i++){
                        if(i<mainArr.length-1){
                            for(var j=0;j<9;j++){
                                //比较mainArr[i].squares和mainArr[i+1].squares[j])不同,拿到坐标值
                                if(mainArr[i].squares[j]!==mainArr[i+1].squares[j]){
                                    arr.push(j);
                                }
                            }
                        }
                    }
                    var result=[]
                    for(var i=0;i<arr.length;i++){
                        var x=Math.floor(arr[i]/3)+1;
                        var y=(arr[i]+1)%3===0?3:(arr[i]+1)%3;
                        result.push([x,y])
                    }
                    return result;
                },
                // 历史步骤跳转是把状态还原到某个时间点,状态根据stepNumber呈现内容,但不会改变最终状态。
                jumpTo:function(e,step){
                    var aList=e.target.parentNode.parentNode.childNodes;
                    for(var i=0;i<aList.length;i++){
                        var item=aList[i];
                        if(item.childNodes[0].classList.contains('back-active')){
                            item.childNodes[0].classList.remove('back-active');
                        }
                    }
                    e.target.classList.add('back-active');
                    this.setState({
                        stepNumber:step,
                        turnToX:step%2?false:true
                    });
                },
                // 坐标转换函数
                convert:function(i){
                    var x=Math.floor(i/3)+1;
                    var y=(i+1)%3===0?3:y=(i+1)%3;
    
                    return [x,y];
                },
                // 获取历史步骤列表
                getMoveList:function(){
                    var history=this.state.history.slice();
                    var arr=[];
                    var _this=this;
                    var rectangular=this.getRectangular();//获取二维坐标
    
                    history.forEach(function(step,move){
                        var content='';
                        if(move!==0){
                            content='Move#'+move+':'+'('+rectangular[move-1][0]+','+rectangular[move-1][1]+')';
                        }else{
                            content='游戏开始~';
                        }
                        // 高亮最后一个
                        if(arr.length==_this.state.history.length-1){
                            arr.push(
                                <li key={move}>
                                    <a
                                      className="process-active"
                                      onClick={(e)=>_this.jumpTo(e,move)}
                                      href="javascript:;">
                                          {content}
                                    </a>
                                </li>
                            );
                        }else{
                            arr.push(
                                <li key={move}>
                                    <a onClick={(e)=>_this.jumpTo(e,move)}
                                      href="javascript:;">
                                      {content}
                                    </a>
                                </li>
                            );
                        }
                    });
                    // 排序方式
                    if(this.state.isAscending=='降序排列'){
                        return arr;
                    }else{
                        return arr.reverse();
                    }
                },
                // 切换排序方式
                sortToggle:function(){
                    this.setState(function(prevState){
                        var sort=prevState.isAscending;
                        var content='';
                        if(sort=='升序排列'){
                            content='降序排列';
                        }else{
                            content='升序排列'
                        }
                        return {
                            isAscending:content
                        }
                    })
                },
                // 重置
                reset:function(){
                    this.setState({
                        history:[
                            {squares:Array(9).fill(null)}
                        ],
                        turnToX:true,
                        stepNumber:0,
                        isAscending:'降序排列'
                    });
                },
    
                render:function(){
                    var lastHistory=this.state.history[this.state.stepNumber];//渲染遵照的是stepNumber而不是最后一步
                    var status=this.judgeWinner()[0];//获胜描述
                    var winCase=this.judgeWinner()[1];//获胜状态
    
                    return (
                        <div className="game">
    
                            <div>
                                <h1 classNme="status">React的井字过三关(2)</h1>
                                <Board
                                  winCase={winCase}
                                  lastHistory={lastHistory.squares}
                                  onClick={(i)=>this.handleClick(i)} />
                            </div>
                            <div className="info">
                                <div>{status}</div>
                                <input
                                  type="button"
                                  value={this.state.isAscending}
                                  onClick={this.sortToggle} />
                                <input
                                  type="button"
                                  value="重置"
                                  onClick={this.reset} />
                                <ol>{this.getMoveList()}</ol>
                            </div>
                        </div>
                    );
                }
            });
    
    
            var Board=React.createClass({
                renderSquare:function(i){
                    if(this.props.winCase){
                        for(var j=0;j<this.props.winCase.length;j++){
                            if(this.props.winCase[j]==i){
                                return (
                                    <Square
                                      winCase="win-case"
                                      key={i}
                                      value={this.props.lastHistory[i]}
                                      onClick={() => this.props.onClick(i)} />
                                );
                            }
                        }
                    }
                    return (
                        <Square key={i}
                          value={this.props.lastHistory[i]}
                          onClick={() => this.props.onClick(i)} />
                    );
                },
    
                getSquare:function(rows){
                    var index=rows*3;
                    var arr=[];
                    for(var i=index;i<index+3;i++){
                        arr.push(this.renderSquare(i));
                    }
                    return arr;
                },
    
                getBoardRow:function(){
                    var arr=[];
                    for(var i=0;i<3;i++){
                        arr.push(
                            <div key={i}
                              className="board-row">
                              {this.getSquare(i)}
                            </div>
                        );
                    }
                    return arr;
                },
    
                render:function(){
                    return (
                        <div clasName="board">
                            <div className="status"></div>
                            {this.getBoardRow()}
                        </div>
                    );
                }
            });
    
            var Square=React.createClass({
                render:function(){
                    if(this.props.winCase){
                        return (
                            <button
                              className={"square "+this.props.winCase}
                              onClick={() => this.props.onClick()}>
                                {this.props.value}
                            </button>
                        );
                    }
                    return (
                        <button
                          className="square"
                          onClick={() => this.props.onClick()}>
                            {this.props.value}
                        </button>
                    );
    
                }
            });
    
            ReactDOM.render(
                <Game />,
                document.getElementById('container')
            );
    
  • 相关阅读:
    HDU1883 Phone Cell
    HDU2297 Run
    关于反射的疑惑
    struts2+spring 实例教程
    在.Net 模板页中使用CSS样式
    到底是什么反射,泛型,委托,泛型
    asp.net如何实现删除文件夹及文件内容操作
    学好C#方法
    Web网页安全色谱
    总结一些ASP.NET常用代码
  • 原文地址:https://www.cnblogs.com/djtao/p/6213473.html
Copyright © 2020-2023  润新知